返回 DeepSeek-Reasonix
navigation-surface-lifecycle.test.tsx
根目录 / desktop / frontend / src / __tests__ / navigation-surface-lifecycle.test.tsx
1 import React, { act, useLayoutEffect } from "react";
2 import { createRoot } from "react-dom/client";
3 import { JSDOM } from "jsdom";
4 import assert from "node:assert/strict";
5 import { useNavigationSurface } from "../lib/useNavigationSurface";
6 import { projectNavigationSurfaceTarget } from "../app-runtime/conversationProjection";
7 import { initialState } from "../lib/useController";
8 import type { RemoteSessionApi } from "../lib/useRemoteSession";
9
10 const dom = new JSDOM("<div id='root'></div>");
11 Object.assign(globalThis, { window: dom.window, document: dom.window.document, IS_REACT_ACT_ENVIRONMENT: true });
12 const root = createRoot(document.getElementById("root")!);
13 let surface!: ReturnType<typeof useNavigationSurface>;
14 function Probe({ tab, session, remote }: { tab: string; session: string;
15 remote?: Pick<RemoteSessionApi, "state" | "hydrated" | "surfaceGeneration" | "error"> }) {
16 const next = useNavigationSurface(projectNavigationSurfaceTarget({ activeTabId: tab, sessionKey: session,
17 local: { ...initialState, meta: { ...initialState.meta, ready: !remote } as typeof initialState.meta,
18 backendActivationPending: Boolean(remote), hydrating: Boolean(remote) }, remote }));
19 useLayoutEffect(() => { surface = next; });
20 return null;
21 }
22
23 try {
24 await act(async () => root.render(<Probe tab="a" session="a:1" />));
25 act(() => { surface.begin(1); });
26 act(() => { surface.maskTarget(1); });
27 const firstToken = surface.surfaceCommitToken!;
28 assert.ok(firstToken);
29 await act(async () => root.render(<Probe tab="a" session="a:2" />));
30 const secondToken = surface.surfaceCommitToken!;
31 assert.notEqual(secondToken, firstToken, "replacement within the same intent receives a distinct paint receipt");
32 act(() => { assert.equal(surface.commitPaint(firstToken, "ready"), null); });
33 let receipt: unknown;
34 act(() => { receipt = surface.commitPaint(secondToken, "ready"); });
35 assert.deepEqual(receipt, { token: secondToken, intent: 1, targetTabId: "a", targetSessionKey: "a:2" });
36 act(() => { assert.equal(surface.commitPaint(secondToken, "ready"), null, "a receipt commits once"); });
37 const remote = { state: "ready", hydrated: true, surfaceGeneration: 1, error: "" } as const;
38 await act(async () => root.render(<Probe tab="remote" session="workspace" remote={remote} />));
39 act(() => { surface.begin(2); surface.maskTarget(2); });
40 const remoteToken = surface.surfaceCommitToken!;
41 assert.ok(remoteToken, "remote readiness is independent of the inactive local controller's pending hydration");
42 await act(async () => root.render(<Probe tab="remote" session="workspace" remote={{ ...remote, surfaceGeneration: 2 }} />));
43 act(() => { assert.equal(surface.commitPaint(remoteToken, "ready"), null, "a former Serve generation cannot acknowledge the new surface"); });
44 act(() => { assert.ok(surface.commitPaint(surface.surfaceCommitToken!, "ready")); });
45 assert.equal(surface.transitioning, false, "the same paint transaction releases remote Composer readiness");
46 await act(async () => root.render(<Probe tab="remote" session="workspace" remote={{ ...remote, state: "error", hydrated: false, error: "offline" }} />));
47 act(() => { surface.begin(3); surface.maskTarget(3); });
48 assert.equal(surface.transitioning, false, "remote hydration failure terminates the masked navigation, leaving recovery reachable");
49 const retained = surface.begin;
50 act(() => { root.unmount(); retained(2); });
51 console.log("PASS navigation receipts are unique, source-bound, and consumed exactly once");
52 } finally {
53 dom.window.close();
54 }
55
55 lines Plain Text