254 lines
6.9 KiB
TypeScript
254 lines
6.9 KiB
TypeScript
import {
|
|
act,
|
|
cleanup,
|
|
fireEvent,
|
|
render,
|
|
screen,
|
|
} from "@testing-library/react";
|
|
import userEvent from "@testing-library/user-event";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import type { RegexExecutionRequest } from "../regex/model/match";
|
|
|
|
const engineHarness = vi.hoisted(() => ({
|
|
cancelCalls: 0,
|
|
disposeCalls: 0,
|
|
executeRequests: [] as RegexExecutionRequest[],
|
|
}));
|
|
|
|
vi.mock("../regex/execution/EngineSupervisor", () => ({
|
|
EngineSupervisor: class {
|
|
execute(request: RegexExecutionRequest) {
|
|
engineHarness.executeRequests.push(request);
|
|
return Promise.resolve({
|
|
accepted: true,
|
|
engine: {
|
|
flavour: "ecmascript",
|
|
adapterVersion: "test",
|
|
engineName: "Test engine",
|
|
engineVersion: "1",
|
|
runtimeVersion: "1",
|
|
offsetUnit: "utf16",
|
|
capabilities: {
|
|
compilation: true,
|
|
matching: true,
|
|
replacement: true,
|
|
namedCaptures: true,
|
|
captureHistory: false,
|
|
actualTrace: false,
|
|
benchmark: false,
|
|
},
|
|
},
|
|
flags: {
|
|
userFlags: "",
|
|
effectiveFlags: "g",
|
|
internallyAddedIndicesFlag: true,
|
|
internallyAddedGlobalFlag: true,
|
|
},
|
|
matches: [
|
|
{
|
|
matchNumber: 1,
|
|
value: "42",
|
|
valueStatus: "complete",
|
|
range: { startUtf16: 0, endUtf16: 2 },
|
|
nativeRange: { start: 0, end: 2, unit: "utf16" },
|
|
captures: [],
|
|
},
|
|
],
|
|
diagnostics: [],
|
|
elapsedMs: 2,
|
|
truncated: false,
|
|
});
|
|
}
|
|
|
|
replace(): Promise<never> {
|
|
return Promise.reject(new Error("Unexpected replacement call"));
|
|
}
|
|
|
|
cancel(): void {
|
|
engineHarness.cancelCalls += 1;
|
|
}
|
|
|
|
dispose(): void {
|
|
engineHarness.disposeCalls += 1;
|
|
}
|
|
},
|
|
}));
|
|
|
|
import { CorpusPanel } from "./CorpusPanel";
|
|
|
|
afterEach(() => {
|
|
cleanup();
|
|
engineHarness.cancelCalls = 0;
|
|
engineHarness.disposeCalls = 0;
|
|
engineHarness.executeRequests.length = 0;
|
|
});
|
|
|
|
function renderPanel(
|
|
overrides: Partial<Parameters<typeof CorpusPanel>[0]> = {},
|
|
) {
|
|
return render(
|
|
<CorpusPanel
|
|
active
|
|
flavour="ecmascript"
|
|
flavourVersion="2025"
|
|
options={{}}
|
|
pattern="\\d+"
|
|
flags={[]}
|
|
captureMetadata={[]}
|
|
patternAccepted
|
|
replacement="[$&]"
|
|
replacementAccepted
|
|
onReplacementChange={() => {}}
|
|
timeoutMs={2_000}
|
|
{...overrides}
|
|
/>,
|
|
);
|
|
}
|
|
|
|
describe("CorpusPanel", () => {
|
|
it("adds pasted text and produces an exhaustive per-document summary", async () => {
|
|
renderPanel();
|
|
await userEvent.type(
|
|
screen.getByRole("textbox", { name: "Pasted corpus text" }),
|
|
"42",
|
|
);
|
|
await userEvent.click(
|
|
screen.getByRole("button", { name: "Add pasted text" }),
|
|
);
|
|
|
|
expect(
|
|
screen.getByRole("list", { name: "Corpus documents" }),
|
|
).toHaveTextContent("pasted-text.txt");
|
|
await userEvent.click(screen.getByRole("button", { name: "Scan corpus" }));
|
|
|
|
expect(
|
|
await screen.findByRole("table", { name: "Corpus document results" }),
|
|
).toHaveTextContent("complete");
|
|
expect(engineHarness.executeRequests).toHaveLength(1);
|
|
expect(engineHarness.executeRequests[0]).toMatchObject({
|
|
subject: "42",
|
|
scanAll: true,
|
|
});
|
|
expect(
|
|
screen.getByRole("button", { name: "Download summary JSON" }),
|
|
).toBeEnabled();
|
|
});
|
|
|
|
it("blocks invalid runs and clears results when configuration changes", async () => {
|
|
const rendered = renderPanel({ patternAccepted: false });
|
|
fireEvent.click(screen.getByRole("button", { name: "Add pasted text" }));
|
|
expect(screen.getByRole("button", { name: "Scan corpus" })).toBeDisabled();
|
|
expect(screen.getByRole("alert")).toHaveTextContent(
|
|
"syntax provider accepts",
|
|
);
|
|
|
|
rendered.rerender(
|
|
<CorpusPanel
|
|
active
|
|
flavour="ecmascript"
|
|
flavourVersion="2025"
|
|
options={{}}
|
|
pattern="changed"
|
|
flags={[]}
|
|
captureMetadata={[]}
|
|
patternAccepted
|
|
replacement="[$&]"
|
|
replacementAccepted
|
|
onReplacementChange={() => {}}
|
|
timeoutMs={2_000}
|
|
/>,
|
|
);
|
|
expect(screen.getByRole("button", { name: "Scan corpus" })).toBeEnabled();
|
|
expect(screen.getByRole("status")).toHaveTextContent(
|
|
"previous corpus results were cleared",
|
|
);
|
|
});
|
|
|
|
it("cancels an active batch when the workspace is left", async () => {
|
|
let resolveExecution: ((value: never) => void) | undefined;
|
|
const pending = new Promise<never>((resolve) => {
|
|
resolveExecution = resolve;
|
|
});
|
|
const execute = vi
|
|
.spyOn(
|
|
(await import("../regex/execution/EngineSupervisor")).EngineSupervisor
|
|
.prototype,
|
|
"execute",
|
|
)
|
|
.mockReturnValue(pending);
|
|
const rendered = renderPanel();
|
|
fireEvent.click(screen.getByRole("button", { name: "Add pasted text" }));
|
|
fireEvent.click(screen.getByRole("button", { name: "Scan corpus" }));
|
|
expect(
|
|
screen.getByRole("button", { name: "Cancel corpus run" }),
|
|
).toBeEnabled();
|
|
|
|
rendered.rerender(
|
|
<CorpusPanel
|
|
active={false}
|
|
flavour="ecmascript"
|
|
flavourVersion="2025"
|
|
options={{}}
|
|
pattern="\\d+"
|
|
flags={[]}
|
|
captureMetadata={[]}
|
|
patternAccepted
|
|
replacement="[$&]"
|
|
replacementAccepted
|
|
onReplacementChange={() => {}}
|
|
timeoutMs={2_000}
|
|
/>,
|
|
);
|
|
expect(engineHarness.cancelCalls).toBeGreaterThan(0);
|
|
execute.mockRestore();
|
|
await act(async () => {
|
|
resolveExecution?.(undefined as never);
|
|
});
|
|
});
|
|
|
|
it("does not publish stale batch results after configuration changes", async () => {
|
|
let resolveExecution: ((value: never) => void) | undefined;
|
|
const pending = new Promise<never>((resolve) => {
|
|
resolveExecution = resolve;
|
|
});
|
|
const execute = vi
|
|
.spyOn(
|
|
(await import("../regex/execution/EngineSupervisor")).EngineSupervisor
|
|
.prototype,
|
|
"execute",
|
|
)
|
|
.mockReturnValue(pending);
|
|
const rendered = renderPanel();
|
|
fireEvent.click(screen.getByRole("button", { name: "Add pasted text" }));
|
|
fireEvent.click(screen.getByRole("button", { name: "Scan corpus" }));
|
|
|
|
rendered.rerender(
|
|
<CorpusPanel
|
|
active
|
|
flavour="ecmascript"
|
|
flavourVersion="2025"
|
|
options={{}}
|
|
pattern="changed"
|
|
flags={[]}
|
|
captureMetadata={[]}
|
|
patternAccepted
|
|
replacement="[$&]"
|
|
replacementAccepted
|
|
onReplacementChange={() => {}}
|
|
timeoutMs={2_000}
|
|
/>,
|
|
);
|
|
await act(async () => {
|
|
resolveExecution?.(undefined as never);
|
|
});
|
|
|
|
expect(
|
|
screen.queryByRole("table", { name: "Corpus document results" }),
|
|
).not.toBeInTheDocument();
|
|
expect(screen.getByRole("status")).toHaveTextContent(
|
|
"previous corpus results were cleared",
|
|
);
|
|
execute.mockRestore();
|
|
});
|
|
});
|