返回 DeepSeek-Reasonix
ask-card-identity.test.tsx
根目录 / desktop / frontend / src / __tests__ / ask-card-identity.test.tsx
1 import assert from "node:assert/strict";
2 import { JSDOM } from "jsdom";
3 import type { QuestionAnswer, WireAsk } from "../lib/types";
4
5 const dom = new JSDOM('<div id="root"></div>', { url: "http://localhost/", pretendToBeVisual: true });
6 Object.assign(globalThis, {
7 window: dom.window, document: dom.window.document, Element: dom.window.Element,
8 HTMLElement: dom.window.HTMLElement, Node: dom.window.Node, localStorage: dom.window.localStorage,
9 IS_REACT_ACT_ENVIRONMENT: true,
10 });
11 Object.defineProperty(globalThis, "navigator", { configurable: true, value: dom.window.navigator });
12 const { default: React, act } = await import("react");
13 const { createRoot } = await import("react-dom/client");
14 const { AskCard } = await import("../components/AskCard");
15 const { LocaleProvider } = await import("../lib/i18n");
16 const root = createRoot(document.getElementById("root")!);
17 const answers: QuestionAnswer[][] = [];
18 let onStop: () => void | Promise<void> = () => {};
19 const ask: WireAsk = {
20 id: "1", runtimeEpoch: "runtime-a", turnId: "turn-a",
21 questions: [{ id: "q1", prompt: "Choose", options: [{ label: "Option A" }, { label: "Option B" }] }],
22 };
23 async function render(next: WireAsk | null, scope = "tab-a") {
24 await act(async () => root.render(<LocaleProvider>{next && <AskCard ask={next} draftScope={scope}
25 onAnswer={(_id, value) => { answers.push(value); }} onStop={onStop} />}</LocaleProvider>));
26 }
27 function input() { return document.querySelector<HTMLInputElement>(".ask-shelf__custom")!; }
28 async function type(text: string) {
29 await act(async () => input().focus()); // Keyboard focus does not click the custom row.
30 await act(async () => {
31 Object.getOwnPropertyDescriptor(dom.window.HTMLInputElement.prototype, "value")!.set!.call(input(), text);
32 input().dispatchEvent(new dom.window.Event("input", { bubbles: true }));
33 });
34 }
35
36 await render(ask);
37 await type("Keyboard answer");
38 await act(async () => input().dispatchEvent(new dom.window.KeyboardEvent("keydown", { key: "Enter", bubbles: true })));
39 assert.deepEqual(answers, [[{ questionId: "q1", selected: ["Keyboard answer"] }]]);
40 console.log(" PASS keyboard focus and Enter submit the custom answer exactly once");
41
42 await render(null);
43 await render(ask);
44 assert.equal(input().value, "", "successful submission clears its draft");
45 await type("Keep this draft");
46 await render(null);
47 await render({ ...ask });
48 assert.equal(input().value, "Keep this draft", "same prompt survives unmount and replay");
49
50 await render({ ...ask, runtimeEpoch: "runtime-b" });
51 assert.equal(input().value, "", "runtime replacement resets a mounted card even with the same prompt and turn IDs");
52 await type("Runtime B draft");
53 await render({ ...ask, runtimeEpoch: "runtime-b", turnId: "turn-b" });
54 assert.equal(input().value, "", "new turn cannot inherit the previous turn's draft");
55 await render(null);
56 await render({ ...ask, runtimeEpoch: "runtime-c" });
57 assert.equal(input().value, "", "a replacement runtime cannot inherit an unmounted card's draft");
58 await render(ask, "tab-b");
59 assert.equal(input().value, "", "tabs have separate drafts");
60 await render(ask);
61 assert.equal(input().value, "Keep this draft", "switching back restores only the original prompt's draft");
62 console.log(" PASS drafts follow tab, runtime, turn, and prompt identity across replay and replacement");
63
64 let stopCalls = 0;
65 onStop = async () => { stopCalls++; throw new Error("transport unavailable"); };
66 await render(ask);
67 function stopButton() { return document.querySelector<HTMLButtonElement>(".prompt-shelf__header-button:last-child")!; }
68 await act(async () => stopButton().click());
69 assert.equal(stopCalls, 1);
70 assert.equal(stopButton().disabled, false, "failed cancellation permits retry");
71 assert.ok(document.querySelector('[role="alert"]'), "failed cancellation is visible");
72 await render(null);
73 await render(ask);
74 assert.equal(input().value, "Keep this draft", "failed cancellation preserves the draft across navigation");
75 onStop = async () => { stopCalls++; };
76 await render(ask);
77 await act(async () => stopButton().click());
78 assert.equal(stopCalls, 2, "retry reaches the transport");
79 await render(null);
80 await render(ask);
81 assert.equal(input().value, "", "accepted cancellation clears its draft");
82 console.log(" PASS failed Stop preserves the draft and permits a successful retry");
83 await act(async () => root.unmount());
84 dom.window.close();
85
85 lines Plain Text