返回 DeepSeek-Reasonix
decision-footer-lifecycle.test.tsx
根目录 / desktop / frontend / src / __tests__ / decision-footer-lifecycle.test.tsx
1 import React, { act, type ComponentProps } from "react";
2 import { createRoot } from "react-dom/client";
3 import { JSDOM } from "jsdom";
4 import assert from "node:assert/strict";
5 import { DecisionFooterRegion } from "../app-shell/DecisionFooterRegion";
6 import { LocaleProvider } from "../lib/i18n";
7 import { ToastProvider } from "../lib/toast";
8
9 const dom = new JSDOM("<div id='root'></div>", { url: "http://localhost/", pretendToBeVisual: true });
10 Object.assign(globalThis, {
11 window: dom.window, document: dom.window.document, localStorage: dom.window.localStorage,
12 IS_REACT_ACT_ENVIRONMENT: true,
13 });
14 for (const name of ["Node", "Element", "HTMLElement", "HTMLTextAreaElement", "Event", "CustomEvent", "MutationObserver"]) {
15 Object.defineProperty(globalThis, name, { configurable: true, value: Reflect.get(dom.window, name) });
16 }
17 Object.defineProperty(globalThis, "navigator", { configurable: true, value: dom.window.navigator });
18 Object.defineProperty(window, "matchMedia", { value: () => ({ matches: true, addEventListener() {}, removeEventListener() {} }) });
19 Object.assign(globalThis, {
20 requestAnimationFrame: () => 1, cancelAnimationFrame() {},
21 ResizeObserver: class { observe() {} disconnect() {} unobserve() {} },
22 });
23 Object.defineProperty(dom.window.HTMLElement.prototype, "attachEvent", { value() {} });
24 Object.defineProperty(dom.window.HTMLElement.prototype, "detachEvent", { value() {} });
25 const root = createRoot(document.getElementById("root")!);
26 type Props = ComponentProps<typeof DecisionFooterRegion>;
27 const noop = () => {};
28 const composer: Props["composer"] = { hidden: false, inert: false, hero: false, props: {
29 running: false, collaborationMode: "normal", toolApprovalMode: "ask", goal: "", cwd: "/fixture",
30 modelLabel: "fixture-model", ready: true,
31 onSend: noop, onCancel: noop, onCycleMode: noop, onSetMode: noop,
32 onSetCollaborationMode: noop, onSetToolApprovalMode: noop,
33 onClearGoal: noop, onSwitchModel: noop, onSetEffort: noop,
34 insertRequest: { id: 1, text: "retained draft", mode: "replace" },
35 } };
36 let props: Props = {
37 hidden: false, className: "footer", footerRef: noop, composer,
38 todo: { identity: "todo-a", props: { stateKey: "todo-a", todos: [{ content: "visible work", status: "in_progress" }],
39 running: true, pendingPrompt: false, onDismiss: noop } },
40 undo: { identity: "undo-a", props: { meta: { turns: 1, filesRestored: [], filesRemoved: [], onUndo: noop } } },
41 };
42 async function paint() {
43 await act(async () => {
44 root.render(<LocaleProvider><ToastProvider><DecisionFooterRegion {...props} /></ToastProvider></LocaleProvider>);
45 await Promise.all([import("../components/TodoPanel"), import("../components/UndoRewindBanner"), import("../components/ClearContextCard")]);
46 });
47 }
48
49 try {
50 await paint();
51 const textarea = document.querySelector<HTMLTextAreaElement>("#composer-input")!;
52 assert.ok(textarea);
53 assert.equal(textarea.value, "retained draft");
54 const undo = document.querySelector(".undo-rewind")!;
55 assert.ok(undo);
56 const todo = Array.from(document.querySelectorAll(".prompt-shelf")).find((node) => node.textContent?.includes("visible work"))!;
57 assert.ok(todo);
58
59 props = { ...props, composer: { ...composer, hidden: true, inert: true } };
60 await paint();
61 const host = document.querySelector<HTMLElement>(".composer-decision-host")!;
62 assert.equal(host.hidden, false, "navigation mask preserves the composer footprint");
63 assert.ok(host.classList.contains("composer-decision-host--footprint-hidden"));
64 assert.ok(host.hasAttribute("inert"), "masked composer rejects interactive input");
65 assert.ok(document.querySelector("footer")?.hasAttribute("inert"));
66 assert.equal(document.querySelector(".undo-rewind"), undo, "target rewind stays laid out below the mask");
67 assert.ok(todo.isConnected, "target Todo stays mounted below the mask");
68 assert.equal(document.querySelector("#composer-input"), textarea);
69 assert.equal(textarea.value, "retained draft");
70
71 props = { ...props, composer, decision: { kind: "clear-context", identity: "clear-a", props: { onCancel: noop, onConfirm: noop } } };
72 await paint();
73 assert.equal(document.querySelector("#composer-input"), textarea, "decision card does not remount Composer");
74 assert.equal(host.hidden, true, "decision card hides the mounted Composer");
75 props = { ...props, decision: undefined };
76 await paint();
77 assert.equal(document.querySelector("#composer-input"), textarea);
78 assert.equal(textarea.value, "retained draft", "dismissed decision restores the original draft");
79 console.log("PASS Decision Footer preserves masked layout, Todo/rewind hosts, and Composer identity/draft");
80 } finally {
81 await act(async () => root.unmount());
82 dom.window.close();
83 }
84
84 lines Plain Text