fix: retain completed results during live editing
This commit is contained in:
@@ -37,6 +37,7 @@ const syntaxHarness = vi.hoisted(() => ({
|
||||
|
||||
const engineHarness = vi.hoisted(() => ({
|
||||
requests: [] as RegexExecutionRequest[],
|
||||
emitWholeFirstCharacterMatch: false,
|
||||
}));
|
||||
|
||||
vi.mock("../editors/CodeEditor", () => ({
|
||||
@@ -45,18 +46,35 @@ vi.mock("../editors/CodeEditor", () => ({
|
||||
onChange,
|
||||
label,
|
||||
readOnly,
|
||||
marks,
|
||||
selectedRange,
|
||||
onSelectionChange,
|
||||
}: {
|
||||
readonly value: string;
|
||||
readonly onChange?: (value: string) => void;
|
||||
readonly label: string;
|
||||
readonly readOnly?: boolean;
|
||||
readonly marks?: readonly unknown[];
|
||||
readonly selectedRange?: { readonly startUtf16: number };
|
||||
readonly onSelectionChange?: (range: {
|
||||
readonly startUtf16: number;
|
||||
readonly endUtf16: number;
|
||||
}) => void;
|
||||
}) {
|
||||
return (
|
||||
<textarea
|
||||
aria-label={label}
|
||||
value={value}
|
||||
readOnly={readOnly}
|
||||
data-mark-count={marks?.length ?? 0}
|
||||
data-selected-start={selectedRange?.startUtf16}
|
||||
onChange={(event) => onChange?.(event.target.value)}
|
||||
onSelect={(event) =>
|
||||
onSelectionChange?.({
|
||||
startUtf16: event.currentTarget.selectionStart,
|
||||
endUtf16: event.currentTarget.selectionEnd,
|
||||
})
|
||||
}
|
||||
/>
|
||||
);
|
||||
},
|
||||
@@ -115,7 +133,18 @@ vi.mock("../regex/execution/EngineSupervisor", () => ({
|
||||
internallyAddedIndicesFlag: false,
|
||||
internallyAddedGlobalFlag: false,
|
||||
},
|
||||
matches: [],
|
||||
matches: engineHarness.emitWholeFirstCharacterMatch
|
||||
? [
|
||||
{
|
||||
matchNumber: 1,
|
||||
value: request.subject.slice(0, 1),
|
||||
valueStatus: "complete" as const,
|
||||
range: { startUtf16: 0, endUtf16: 1 },
|
||||
nativeRange: { start: 0, end: 1, unit: "utf16" as const },
|
||||
captures: [],
|
||||
},
|
||||
]
|
||||
: [],
|
||||
diagnostics: [],
|
||||
elapsedMs: 1,
|
||||
truncated: false,
|
||||
@@ -231,6 +260,7 @@ afterEach(() => {
|
||||
syntaxHarness.patterns.length = 0;
|
||||
syntaxHarness.replacements.length = 0;
|
||||
engineHarness.requests.length = 0;
|
||||
engineHarness.emitWholeFirstCharacterMatch = false;
|
||||
});
|
||||
|
||||
describe("Workbench syntax coordination", () => {
|
||||
@@ -346,6 +376,79 @@ describe("Workbench syntax coordination", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("does not revive an old syntax snapshot when rapid edits return to the same text", async () => {
|
||||
vi.useFakeTimers();
|
||||
render(<Workbench />);
|
||||
fireEvent.click(screen.getByRole("checkbox", { name: "Live" }));
|
||||
|
||||
await advanceParseDebounce();
|
||||
await resolvePattern(0, []);
|
||||
const patternInput = screen.getByRole("textbox", {
|
||||
name: "Regular expression pattern",
|
||||
});
|
||||
const initialPattern = (patternInput as HTMLTextAreaElement).value;
|
||||
const runButton = screen.getByRole("button", { name: "Run" });
|
||||
expect(runButton).toBeEnabled();
|
||||
|
||||
fireEvent.change(patternInput, { target: { value: "temporary" } });
|
||||
fireEvent.change(patternInput, { target: { value: initialPattern } });
|
||||
|
||||
expect(runButton).toBeDisabled();
|
||||
fireEvent.click(screen.getByRole("treeitem"));
|
||||
expect(patternInput).toHaveAttribute("data-mark-count", "0");
|
||||
await advanceParseDebounce();
|
||||
await resolvePattern(1, []);
|
||||
expect(patternInput).toHaveAttribute("data-mark-count", "0");
|
||||
expect(runButton).toBeEnabled();
|
||||
});
|
||||
|
||||
it("retains one exact result frame until the edited subject has a replacement frame", async () => {
|
||||
vi.useFakeTimers();
|
||||
engineHarness.emitWholeFirstCharacterMatch = true;
|
||||
render(<Workbench />);
|
||||
fireEvent.click(screen.getByRole("checkbox", { name: "Live" }));
|
||||
|
||||
await advanceParseDebounce();
|
||||
await resolvePattern(0, []);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Run" }));
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
const subjectInput = screen.getByRole("textbox", { name: "Test text" });
|
||||
const originalSubject = (subjectInput as HTMLTextAreaElement).value;
|
||||
expect(subjectInput).toHaveAttribute("data-mark-count", "1");
|
||||
expect(document.querySelector(".extraction-row")).toHaveTextContent("2");
|
||||
|
||||
fireEvent.change(subjectInput, { target: { value: "Z new subject" } });
|
||||
|
||||
expect(screen.getByTestId("stale-result-frame")).toHaveTextContent(
|
||||
"bound to the last completed inputs",
|
||||
);
|
||||
expect(subjectInput).toHaveAttribute("data-mark-count", "0");
|
||||
expect(document.querySelector(".extraction-row")).toHaveTextContent("2");
|
||||
|
||||
fireEvent.change(subjectInput, { target: { value: originalSubject } });
|
||||
expect(screen.getByTestId("stale-result-frame")).toBeInTheDocument();
|
||||
expect(subjectInput).toHaveAttribute("data-mark-count", "0");
|
||||
|
||||
fireEvent.change(subjectInput, { target: { value: "Z new subject" } });
|
||||
(subjectInput as HTMLTextAreaElement).setSelectionRange(0, 1);
|
||||
fireEvent.select(subjectInput);
|
||||
expect(
|
||||
document.querySelector(".extraction-row.is-selected"),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Run" }));
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(screen.queryByTestId("stale-result-frame")).not.toBeInTheDocument();
|
||||
expect(subjectInput).toHaveAttribute("data-mark-count", "1");
|
||||
expect(document.querySelector(".extraction-row")).toHaveTextContent("Z");
|
||||
});
|
||||
|
||||
it("captures a complete result and reruns only the selected saved test", async () => {
|
||||
vi.useFakeTimers();
|
||||
render(<Workbench />);
|
||||
|
||||
Reference in New Issue
Block a user