返回 DeepSeek-Reasonix
automation-management.test.tsx
根目录 / desktop / frontend / src / __tests__ / automation-management.test.tsx
1 import assert from "node:assert/strict";
2 import { managementDom } from "../test-support/managementDom";
3 import type { HeartbeatTask } from "../custom/features/heartbeat/heartbeat.types";
4 import { installDesktopHostStub } from "./desktopHostStub";
5 const dom = managementDom();
6 const { default: React, act } = await import("react");
7 const { createRoot } = await import("react-dom/client");
8 const { LocaleProvider } = await import("../lib/i18n");
9 const { HeartbeatView } = await import("../custom/features/heartbeat/HeartbeatPanel");
10 const { useAutomationDraftStore: store } = await import("../store/automationDrafts");
11 let tasks: HeartbeatTask[] = [
12 { id: "a", title: "Task A", prompt: "Saved", interval: "1h", enabled: true, createdAt: 1 },
13 { id: "b", title: "Task B", prompt: "Saved B", interval: "2h", enabled: false, createdAt: 2 },
14 ];
15 let release: (() => void) | undefined;
16 let failSave = false;
17 installDesktopHostStub({
18 async HeartbeatReloadConfig() { return { revision: 1, etag: "a", tasks }; },
19 async HeartbeatSaveConfig(value: { tasks: HeartbeatTask[] }) {
20 if (failSave) throw new Error("failed");
21 await new Promise<void>((resolve) => { release = resolve; });
22 tasks = value.tasks; return { revision: 2, etag: "b", tasks };
23 },
24 async ListWorkspaces() { return []; }, async HeartbeatGenerateID() { return "draft-new"; },
25 });
26 const root = createRoot(document.getElementById("root")!);
27 const render = (active = true) => <LocaleProvider><HeartbeatView active={active} /></LocaleProvider>;
28 const button = (text: string) => Array.from(document.querySelectorAll<HTMLButtonElement>("button")).find((node) => node.textContent?.trim() === text)!;
29 await act(async () => root.render(render()));
30 await act(async () => button("Task A").click());
31 await act(async () => store.getState().edit("a", (draft) => ({ ...draft, title: "Edited A", prompt: "Keep this draft" })));
32 await act(async () => button("Task B").click());
33 await act(async () => button("Task A · Unsaved").click());
34 assert.equal(document.querySelector<HTMLInputElement>('[aria-label="Title"]')?.value, "Edited A");
35 await act(async () => root.render(render(false)));
36 await act(async () => root.render(render()));
37 assert.equal(document.querySelector<HTMLInputElement>('[aria-label="Title"]')?.value, "Edited A");
38 await act(async () => button("Paused").click());
39 assert.equal(document.querySelector<HTMLInputElement>('[aria-label="Title"]')?.value, "Edited A", "filters do not discard the active editor");
40 await act(async () => button("All").click());
41 await act(async () => button("Save").click());
42 assert.equal(store.getState().entries.a.busy, true);
43 await act(async () => button("Task B").click());
44 await act(async () => release?.());
45 assert.equal(document.querySelector<HTMLInputElement>('[aria-label="Title"]')?.value, "Task B", "save completion must not steal selection");
46 assert.equal(store.getState().entries.a.draft.title, "Edited A");
47 assert.equal(store.getState().entries.a.busy, false);
48 await act(async () => button("Edited A").click());
49 assert.equal(button("Save").disabled, true, "saved baseline immediately clears dirty footer state");
50 assert.ok(document.querySelector(".automation-save-status")?.textContent?.includes("Saved"));
51 await act(async () => store.getState().edit("a", (draft) => ({ ...draft, prompt: "Survives failed save" })));
52 failSave = true;
53 await act(async () => button("Save").click());
54 assert.equal(store.getState().entries.a.draft.prompt, "Survives failed save");
55 assert.equal(store.getState().entries.a.error, true);
56 await act(async () => root.unmount());
57 assert.equal(store.getState().entries.a.draft.prompt, "Survives failed save", "component unmount does not own draft lifetime");
58 dom.window.close();
59 console.log("PASS automation page/filter retention, save-switch race, failure and unmount draft retention");
60
60 lines Plain Text