Files
openidea/src/ui/SourcePreview.test.tsx
T
Qingandqtvbwfn 78a3768ec1 feat(ui): syntax-highlight the read-only source previews (#1284)
The read-only previews beside Problems, Show Usages and TODO results are syntax-coloured from one whole-file parse (capped for very large files), so a line inside a block comment or a multi-line string is coloured as such; the pane renders plain first and recolours when the parse lands. SourcePreview takes the colouring as a colorize prop (the editor's previewColors), keeping src/ui free of editor imports, and the tok-* colours live once in global.css instead of a copy per view, with the theme's comment style.

Co-authored-by: qtvbwfn <[email protected]>
2026-11-05 00:55:03 +08:00

99 lines
4.2 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";
import { previewColors } from "@/editor/lineTokens";
import { SourcePreview } from "./SourcePreview";
/** A stable loader reference (a new function reloads, like the real module-level ones). */
const loadOf = (content: string) => () => Promise.resolve(content);
const firstTok = (container: HTMLElement) => container.querySelector<HTMLElement>("[class*='tok-']");
describe("SourcePreview", () => {
it("colours lines with the whole file as context", async () => {
const { container } = render(
<SourcePreview
colorize={previewColors}
path="/p/a.ts"
line={1}
load={loadOf('const lib = "x";\n/* block\ncomment */')}
/>,
);
await waitFor(() => expect(container.querySelector(".tok-string")).toBeTruthy(), { timeout: 5000 });
expect(container.querySelector(".tok-string")?.textContent).toBe('"x"');
// `comment */` only reads as a comment tail when the file was parsed whole.
expect(container.querySelector('[data-line="2"] .tok-comment')?.textContent).toBe("comment */");
});
it("marks ranges inside tokens without losing text", async () => {
const content = 'const lib = "xyz";';
const { container } = render(
<SourcePreview
colorize={previewColors}
path="/p/a.ts"
line={0}
load={loadOf(content)}
highlights={[{ line: 0, ranges: [[13, 15]] }]}
/>,
);
await waitFor(() => expect(container.querySelector("mark .tok-string")).toBeTruthy(), { timeout: 5000 });
// `xy` sits inside the string token: marked, while the rest stays coloured.
expect(container.querySelector("mark")?.textContent).toBe("xy");
expect(container.querySelector("mark .tok-string")?.textContent).toBe("xy");
expect(container.textContent).toBe(`1${content}`);
});
it("leaves unknown languages plain", async () => {
const content = 'const lib = "x";';
const { container } = render(
<SourcePreview colorize={previewColors} path="/p/a.unknownext" line={0} load={loadOf(content)} />,
);
await screen.findByText(content);
expect(firstTok(container)).toBeNull();
expect(container.textContent).toContain(content);
});
it("stays plain beyond the parse cap", async () => {
const filler = "// filler line to pass the parse cap\n".repeat(16000);
const { container } = render(
<SourcePreview colorize={previewColors} path="/p/big.ts" line={0} load={loadOf(`${filler}const lib = "x";`)} />,
);
await waitFor(() => expect(container.querySelector('[data-line="0"]')?.textContent).toContain("filler"));
expect(firstTok(container)).toBeNull();
});
it("recolours when the file changes", async () => {
const load = loadOf('const lib = "x";');
const { container, rerender } = render(
<SourcePreview colorize={previewColors} path="/p/a.ts" line={0} load={load} />,
);
await waitFor(() => expect(container.querySelector(".tok-string")).toBeTruthy(), { timeout: 5000 });
rerender(<SourcePreview colorize={previewColors} path="/p/b.unknownext" line={0} load={load} />);
await screen.findByText('const lib = "x";');
expect(firstTok(container)).toBeNull();
});
it("renders the loaded lines around the current one", async () => {
const { container } = render(
<SourcePreview
path="/p/a.ts"
line={1}
load={loadOf("one\ntwo\nthree")}
highlights={[{ line: 1, ranges: [[0, 3]] }]}
/>,
);
await screen.findByText("two");
expect(container.querySelector('[data-line="1"] mark')?.textContent).toBe("two");
expect(container.querySelector('[data-line="0"]')?.textContent).toContain("one");
});
it("makes its text selectable as a selection boundary", async () => {
const { container } = render(
<SourcePreview colorize={previewColors} path="/p/a.ts" line={0} load={loadOf("// TODO")} />,
);
await screen.findByText("// TODO");
// The scroll pane is a selection scope: drags stay inside, text stays copyable.
// Set by an effect, which can run after the text is on screen.
await waitFor(() => expect(container.querySelector("[data-selection-scope]")).toBeTruthy());
});
});