feat(webui): highlight file previews and diffs
This commit is contained in:
@@ -88,6 +88,21 @@ describe("CodeBlock", () => {
|
||||
expect(screen.getByText("const value = 1;")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("normalizes file language aliases before loading Prism", async () => {
|
||||
render(
|
||||
<ThemeProvider theme="light">
|
||||
<CodeBlock language="html" code="<main />" />
|
||||
</ThemeProvider>,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(screen.getByTestId("highlighted-code")).toHaveAttribute("data-language", "markup");
|
||||
});
|
||||
|
||||
it("renders ANSI output without mounting the syntax highlighter", () => {
|
||||
render(
|
||||
<ThemeProvider theme="dark">
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { codeLanguageFromPath, normalizeCodeLanguage } from "@/lib/code-language";
|
||||
|
||||
describe("code language helpers", () => {
|
||||
it.each([
|
||||
["src/App.tsx", "tsx"],
|
||||
["templates/index.html", "markup"],
|
||||
["Dockerfile", "docker"],
|
||||
["Dockerfile.dev", "docker"],
|
||||
["CMakeLists.txt", "cmake"],
|
||||
["scripts/setup.sh:12:4", "bash"],
|
||||
["config/settings.yaml?raw=1", "yaml"],
|
||||
["unknown.customlang", "customlang"],
|
||||
])("infers %s as %s", (path, language) => {
|
||||
expect(codeLanguageFromPath(path)).toBe(language);
|
||||
});
|
||||
|
||||
it("normalizes aliases used by the file preview API", () => {
|
||||
expect(normalizeCodeLanguage("html")).toBe("markup");
|
||||
expect(normalizeCodeLanguage("dockerfile")).toBe("docker");
|
||||
expect(normalizeCodeLanguage(undefined)).toBe("text");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { DiffSyntaxHighlight } from "@/components/thread/activity/DiffSyntaxHighlight";
|
||||
import { ThemeProvider } from "@/hooks/useTheme";
|
||||
|
||||
describe("DiffSyntaxHighlight with Prism", () => {
|
||||
it("loads the TSX grammar and renders styled syntax tokens", async () => {
|
||||
render(
|
||||
<ThemeProvider theme="light">
|
||||
<DiffSyntaxHighlight
|
||||
language="tsx"
|
||||
lines={[
|
||||
{
|
||||
kind: "context",
|
||||
old_lineno: 1,
|
||||
new_lineno: 1,
|
||||
content: 'import { useMemo } from "react";',
|
||||
},
|
||||
{
|
||||
kind: "delete",
|
||||
old_lineno: 2,
|
||||
new_lineno: null,
|
||||
content: "export function StatusBadge() {",
|
||||
},
|
||||
{
|
||||
kind: "add",
|
||||
old_lineno: null,
|
||||
new_lineno: 2,
|
||||
content: "export function StatusBadge(): JSX.Element {",
|
||||
},
|
||||
{
|
||||
kind: "context",
|
||||
old_lineno: 3,
|
||||
new_lineno: 3,
|
||||
content: ' return <span className="badge">ready</span>;',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</ThemeProvider>,
|
||||
);
|
||||
|
||||
const highlighted = await screen.findByTestId("syntax-highlighted-diff-hunk");
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(highlighted.querySelectorAll('td:last-child span[style*="color"]')).not.toHaveLength(
|
||||
0,
|
||||
);
|
||||
},
|
||||
{ timeout: 10_000 },
|
||||
);
|
||||
|
||||
expect(highlighted).toHaveAttribute("data-language", "tsx");
|
||||
expect(highlighted.querySelectorAll("tbody tr")).toHaveLength(4);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,92 @@
|
||||
import { act, render, screen } from "@testing-library/react";
|
||||
import type { ReactNode } from "react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { DiffSyntaxHighlight } from "@/components/thread/activity/DiffSyntaxHighlight";
|
||||
import { ThemeProvider } from "@/hooks/useTheme";
|
||||
|
||||
vi.mock("react-syntax-highlighter/dist/esm/prism-async-light", () => {
|
||||
const MockSyntaxHighlighter = ({
|
||||
children,
|
||||
language,
|
||||
renderer,
|
||||
...props
|
||||
}: {
|
||||
children: string;
|
||||
language: string;
|
||||
renderer: (args: {
|
||||
rows: Array<{
|
||||
type: "element";
|
||||
tagName: "span";
|
||||
properties: { className: string[] };
|
||||
children: Array<{ type: "text"; value: string }>;
|
||||
}>;
|
||||
stylesheet: Record<string, React.CSSProperties>;
|
||||
useInlineStyles: boolean;
|
||||
}) => ReactNode;
|
||||
[key: string]: unknown;
|
||||
}) => (
|
||||
<div data-testid={String(props["data-testid"])} data-language={language}>
|
||||
{renderer({
|
||||
rows: children.split("\n").map((line) => ({
|
||||
type: "element" as const,
|
||||
tagName: "span" as const,
|
||||
properties: { className: ["token", "keyword"] },
|
||||
children: [{ type: "text" as const, value: `${line}\n` }],
|
||||
})),
|
||||
stylesheet: {},
|
||||
useInlineStyles: true,
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
return { default: MockSyntaxHighlighter };
|
||||
});
|
||||
|
||||
vi.mock("react-syntax-highlighter/dist/esm/create-element", () => ({
|
||||
default: ({ node }: { node: { children?: Array<{ value?: string }> } }) => (
|
||||
<span data-testid="syntax-token">{node.children?.[0]?.value}</span>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("react-syntax-highlighter/dist/esm/styles/prism/one-dark", () => ({
|
||||
default: { dark: { color: "#fff" } },
|
||||
}));
|
||||
|
||||
vi.mock("react-syntax-highlighter/dist/esm/styles/prism/one-light", () => ({
|
||||
default: { light: { color: "#111" } },
|
||||
}));
|
||||
|
||||
describe("DiffSyntaxHighlight", () => {
|
||||
it("highlights a complete hunk while preserving diff rows and line numbers", async () => {
|
||||
render(
|
||||
<ThemeProvider theme="light">
|
||||
<DiffSyntaxHighlight
|
||||
language="typescript"
|
||||
lines={[
|
||||
{ kind: "context", old_lineno: 4, new_lineno: 4, content: "export function run() {" },
|
||||
{ kind: "delete", old_lineno: 5, new_lineno: null, content: " return oldValue;" },
|
||||
{ kind: "add", old_lineno: null, new_lineno: 5, content: " return newValue;" },
|
||||
]}
|
||||
/>
|
||||
</ThemeProvider>,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
const highlighted = await screen.findByTestId("syntax-highlighted-diff-hunk");
|
||||
expect(highlighted).toHaveAttribute("data-language", "typescript");
|
||||
expect(screen.getAllByTestId("syntax-token")).toHaveLength(3);
|
||||
expect(screen.getByText("return oldValue;", { exact: false }).closest("tr")).toHaveClass(
|
||||
"bg-rose-500/[0.09]",
|
||||
);
|
||||
expect(screen.getByText("return newValue;", { exact: false }).closest("tr")).toHaveClass(
|
||||
"bg-emerald-500/[0.09]",
|
||||
);
|
||||
expect(screen.getAllByText("5")).toHaveLength(2);
|
||||
expect(screen.getAllByTestId("syntax-token").some((node) => node.textContent?.endsWith("\n"))).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -6,7 +6,23 @@ import { FilePreviewPanel } from "@/components/FilePreviewPanel";
|
||||
import { fetchFilePreview } from "@/lib/api";
|
||||
|
||||
vi.mock("@/components/CodeBlock", () => ({
|
||||
CodeBlock: ({ code }: { code: string }) => <pre data-testid="mock-code-block">{code}</pre>,
|
||||
CodeBlock: ({
|
||||
code,
|
||||
language,
|
||||
highlight,
|
||||
}: {
|
||||
code: string;
|
||||
language?: string;
|
||||
highlight?: boolean;
|
||||
}) => (
|
||||
<pre
|
||||
data-testid="mock-code-block"
|
||||
data-language={language}
|
||||
data-highlight={String(highlight)}
|
||||
>
|
||||
{code}
|
||||
</pre>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/api", async (importOriginal) => {
|
||||
@@ -42,7 +58,10 @@ describe("FilePreviewPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(await screen.findByTestId("mock-code-block")).toHaveTextContent("print('ok')");
|
||||
const codeBlock = await screen.findByTestId("mock-code-block");
|
||||
expect(codeBlock).toHaveTextContent("print('ok')");
|
||||
expect(codeBlock).toHaveAttribute("data-language", "python");
|
||||
expect(codeBlock).toHaveAttribute("data-highlight", "true");
|
||||
expect(screen.getByTestId("file-preview-breadcrumb")).toHaveTextContent("...");
|
||||
expect(screen.getByTestId("file-preview-breadcrumb")).toHaveTextContent("workspace");
|
||||
expect(screen.getByTestId("file-preview-title")).toHaveTextContent("quicksort.py");
|
||||
|
||||
Reference in New Issue
Block a user