返回 DeepSeek-Reasonix
app-lifecycle.test.tsx
根目录 / desktop / frontend / src / __tests__ / app-lifecycle.test.tsx
1 import React, { StrictMode, Suspense, startTransition } from "react";
2 import { act } from "react";
3 import { createRoot } from "react-dom/client";
4 import { JSDOM } from "jsdom";
5 import assert from "node:assert/strict";
6 import {
7 createOperationOwner,
8 operationTargetsEqual,
9 type OperationIdentity,
10 type OperationTarget,
11 } from "../app-runtime/operationOwner";
12 import { useCommittedCommand } from "../lib/useCommittedCommand";
13 import { useCommittedAsyncCommand } from "../lib/useCommittedAsyncCommand";
14 import { createSessionSurfaceFence } from "../app-runtime/sessionTarget";
15
16 const dom = new JSDOM("<div id='root'></div>");
17 globalThis.window = dom.window as unknown as Window & typeof globalThis;
18 globalThis.document = dom.window.document;
19 Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });
20
21 const root = createRoot(document.getElementById("root")!);
22 const never = new Promise<void>(() => undefined);
23 let command!: (value: number) => number | undefined;
24 let asyncCommand!: (value: number) => Promise<{ status: string; value?: number; reason?: string }>;
25 let releaseAsync!: (value: number) => void;
26 let asyncGate = new Promise<number>((resolve) => { releaseAsync = resolve; });
27
28 function CommandProbe({ revision, suspend = false }: { revision: number; suspend?: boolean }) {
29 command = useCommittedCommand((value: number) => revision + value);
30 if (suspend) throw never;
31 return null;
32 }
33
34 async function executeAddition(input: { base: number; gate: Promise<number> }) {
35 return input.base + await input.gate;
36 }
37 function AsyncCommandProbe({ revision }: { revision: number }) {
38 asyncCommand = useCommittedAsyncCommand((value: number) => ({ base: revision + value, gate: asyncGate }), executeAddition);
39 return null;
40 }
41
42 const session = (tabId: string, sessionKey: string): OperationTarget => ({
43 kind: "session",
44 tabId,
45 sessionKey,
46 });
47
48 try {
49 await act(async () => root.render(
50 <StrictMode><Suspense><CommandProbe revision={1} /></Suspense></StrictMode>,
51 ));
52 const retainedCommand = command;
53 assert.equal(retainedCommand(4), 5);
54
55 for (let revision = 2; revision <= 512; revision += 1) {
56 await act(async () => root.render(
57 <StrictMode><Suspense><CommandProbe revision={revision} /></Suspense></StrictMode>,
58 ));
59 assert.equal(command, retainedCommand, "the entry point is stable across presentation commits");
60 assert.equal(retainedCommand(4), revision + 4, "only committed input owns command dispatch");
61 }
62
63 await act(async () => startTransition(() => root.render(
64 <StrictMode><Suspense><CommandProbe revision={999} suspend /></Suspense></StrictMode>,
65 )));
66 assert.equal(retainedCommand(4), 516, "abandoned render input never becomes authoritative");
67
68 await act(async () => root.render(<StrictMode><Suspense><CommandProbe revision={999} suspend /></Suspense></StrictMode>));
69 assert.equal(retainedCommand(4), undefined, "a hidden Suspense subtree has no layout-owned command authority");
70 await act(async () => root.render(<StrictMode><Suspense><CommandProbe revision={512} /></Suspense></StrictMode>));
71 assert.equal(command, retainedCommand, "revealing a suspended surface preserves the stable entry");
72 assert.equal(retainedCommand(4), 516, "revealing publishes the current committed input in a fresh lifecycle");
73
74 await act(async () => root.unmount());
75 assert.equal(retainedCommand(4), undefined, "a retained command is inert after its owner unmounts");
76
77 const asyncHost = document.createElement("div");
78 document.body.append(asyncHost);
79 const asyncRoot = createRoot(asyncHost);
80 await act(async () => asyncRoot.render(<AsyncCommandProbe revision={1} />));
81 const retainedAsyncCommand = asyncCommand;
82 const superseded = retainedAsyncCommand(2);
83 const releaseSuperseded = releaseAsync;
84 asyncGate = new Promise<number>((resolve) => { releaseAsync = resolve; });
85 await act(async () => asyncRoot.render(<AsyncCommandProbe revision={10} />));
86 const current = retainedAsyncCommand(3);
87 releaseSuperseded(4);
88 releaseAsync(4);
89 assert.deepEqual(await superseded, { status: "cancelled", reason: "superseded" });
90 assert.deepEqual(await current, { status: "completed", value: 17 });
91 await act(async () => asyncRoot.unmount());
92 assert.deepEqual(await retainedAsyncCommand(1), { status: "cancelled", reason: "disposed" });
93 asyncHost.remove();
94
95 const owner = createOperationOwner();
96 const ownerEpoch = owner.mount();
97 const a = session("tab-a", "session-a:1");
98 const b = session("tab-b", "session-b:1");
99
100 const firstA = owner.begin(a, 10);
101 assert.equal(owner.owns(firstA), true);
102 const firstB = owner.begin(b, 11);
103 assert.equal(owner.owns(firstA), false, "new navigation supersedes the prior UI operation");
104 assert.equal(owner.owns(firstB), true);
105
106 const secondA = owner.begin(a, 12);
107 assert.equal(owner.owns(firstB), false);
108 assert.equal(owner.owns(firstA), false, "A → B → A does not revive the first A operation");
109 assert.equal(owner.owns(secondA), true);
110
111 const thirdA = owner.begin(a, 13);
112 assert.equal(owner.finish(secondA), false, "an old finally cannot clear the replacement request");
113 assert.equal(owner.owns(thirdA), true);
114 assert.equal(owner.finish(thirdA), true);
115 assert.equal(owner.activeCount, 0);
116
117 const pending = owner.begin(a, 14);
118 owner.unmount(ownerEpoch);
119 assert.equal(owner.owns(pending), false, "disposed owner rejects stale async continuations");
120 assert.equal(owner.activeCount, 0, "disposed owner releases every operation input");
121
122 const remountedEpoch = owner.mount();
123 const remounted = owner.begin(a, 15);
124 assert.notEqual(remounted.ownerEpoch, pending.ownerEpoch, "StrictMode remount receives a new epoch");
125 assert.equal(owner.owns(remounted), true);
126 owner.unmount(remountedEpoch);
127
128 const sameTarget: OperationTarget = { kind: "session", tabId: "tab-a", sessionKey: "session-a:1" };
129 assert.equal(operationTargetsEqual(a, sameTarget), true);
130 assert.equal(operationTargetsEqual(a, session("tab-a", "session-a:2")), false);
131 assert.equal(operationTargetsEqual(a, b), false);
132
133 const surfaceFence = createSessionSurfaceFence();
134 const surfaceA1 = surfaceFence.commit("tab-a", "session-a:1")!;
135 surfaceFence.commit("tab-b", "session-b:1");
136 const surfaceA2 = surfaceFence.commit("tab-a", "session-a:1")!;
137 assert.equal(surfaceFence.owns(surfaceA1), false, "A → B → A cannot reacquire old UI ownership");
138 assert.equal(surfaceFence.owns(surfaceA2), true, "the latest committed A surface owns UI continuation");
139 surfaceFence.dispose();
140 assert.equal(surfaceFence.owns(surfaceA2), false, "surface disposal invalidates every retained operation");
141
142 const identities = new Set<OperationIdentity>([firstA, firstB, secondA, thirdA, pending, remounted]);
143 assert.equal(identities.size, 6, "every operation has a distinct identity object");
144 console.log("PASS App committed commands and source-bound operation ownership are lifecycle safe");
145 } finally {
146 if (document.getElementById("root")?.hasChildNodes()) await act(async () => root.unmount());
147 dom.window.close();
148 }
149
149 lines Plain Text