返回 DeepSeek-Reasonix
committed-command-execution.test.tsx
根目录 / desktop / frontend / src / __tests__ / committed-command-execution.test.tsx
1 import React, { act, StrictMode, Suspense, startTransition, useLayoutEffect } from "react";
2 import { createRoot } from "react-dom/client";
3 import { JSDOM } from "jsdom";
4 import assert from "node:assert/strict";
5 import { useCommittedAsyncCommand } from "../lib/useCommittedAsyncCommand";
6 import type { CommandAuthority, CommandOutcome } from "../lib/commandOutcome";
7
8 function deferred() {
9 let resolve!: () => void;
10 const promise = new Promise<void>((done) => { resolve = done; });
11 return { promise, resolve };
12 }
13 type Input = { target: string; gate: Promise<void>; apply: (target: string) => void };
14 async function execute(input: Input, authority: CommandAuthority) {
15 await input.gate;
16 authority.checkpoint();
17 input.apply(input.target);
18 return input.target;
19 }
20
21 const dom = new JSDOM("<div id='root'></div>");
22 Object.assign(globalThis, { window: dom.window, document: dom.window.document, IS_REACT_ACT_ENVIRONMENT: true });
23 const root = createRoot(document.getElementById("root")!);
24 const effects: string[] = [];
25 const apply = (target: string) => { effects.push(target); };
26 let command!: (gate: Promise<void>) => Promise<CommandOutcome<string>>;
27 let duringRender!: Promise<CommandOutcome<string>>;
28 const never = new Promise<void>(() => {});
29 function Probe({ target, suspend = false, tryBeforeCommit = false }: {
30 target: string; suspend?: boolean; tryBeforeCommit?: boolean;
31 }) {
32 const next = useCommittedAsyncCommand((gate: Promise<void>): Input => ({ target, gate, apply }), execute);
33 if (tryBeforeCommit) duringRender = next(Promise.resolve());
34 useLayoutEffect(() => { command = next; });
35 if (suspend) throw never;
36 return null;
37 }
38
39 try {
40 await act(async () => root.render(<StrictMode><Suspense><Probe target="a" tryBeforeCommit /></Suspense></StrictMode>));
41 assert.deepEqual(await duringRender, { status: "cancelled", reason: "not-ready" });
42 assert.deepEqual(effects, []);
43 const original = command;
44 const first = deferred();
45 const oldA = command(first.promise);
46 await act(async () => root.render(<StrictMode><Suspense><Probe target="b" /></Suspense></StrictMode>));
47 assert.equal(command, original);
48 const second = deferred();
49 const currentB = command(second.promise);
50 first.resolve();
51 assert.deepEqual(await oldA, { status: "cancelled", reason: "superseded" });
52 assert.deepEqual(effects, [], "stale executor performs no intermediate effect");
53 second.resolve();
54 assert.deepEqual(await currentB, { status: "completed", value: "b" });
55 assert.deepEqual(effects, ["b"]);
56
57 const third = deferred();
58 const surviving = command(third.promise);
59 await act(async () => root.render(<StrictMode><Suspense><Probe target="a" /></Suspense></StrictMode>));
60 third.resolve();
61 assert.deepEqual(await surviving, { status: "completed", value: "b" }, "rerender does not cancel a source-captured operation");
62 assert.deepEqual(effects, ["b", "b"], "source data completion does not retarget to the latest render");
63
64 await act(async () => startTransition(() => root.render(<StrictMode><Suspense><Probe target="abandoned" suspend /></Suspense></StrictMode>)));
65 assert.deepEqual(await command(Promise.resolve()), { status: "completed", value: "a" });
66 assert.deepEqual(effects, ["b", "b", "a"]);
67
68 const disposedGate = deferred();
69 const disposed = command(disposedGate.promise);
70 act(() => { root.unmount(); });
71 disposedGate.resolve();
72 assert.deepEqual(await disposed, { status: "cancelled", reason: "disposed" });
73 assert.deepEqual(await original(Promise.resolve()), { status: "cancelled", reason: "disposed" });
74 assert.deepEqual(effects, ["b", "b", "a"], "unmount fences the effect after an uncancellable promise");
75 console.log("PASS committed capture/executor separates source input from lifecycle-owned effects");
76 } finally {
77 dom.window.close();
78 }
79
79 lines Plain Text