返回 DeepSeek-Reasonix
session-loading-feedback.test.tsx
根目录 / desktop / frontend / src / __tests__ / session-loading-feedback.test.tsx
1 import assert from "node:assert/strict";
2 import { mock } from "node:test";
3 import { register } from "node:module";
4 import React, { act } from "react";
5 import { createRoot } from "react-dom/client";
6 import { installRemoteSurfaceDom } from "./helpers/remoteSurfaceDom";
7 import { LocaleProvider } from "../lib/i18n";
8 import type { LoadingFixtureInput } from "../test-support/sessionLoadingFixture";
9
10 register(new URL("../../scripts/svg-loader.mjs", import.meta.url));
11 register(new URL("../../scripts/css-loader.mjs", import.meta.url));
12 const dom = installRemoteSurfaceDom();
13 globalThis.ResizeObserver = class { observe() {} unobserve() {} disconnect() {} } as unknown as typeof ResizeObserver;
14 const { SessionLoadingFixture } = await import("../test-support/sessionLoadingFixture");
15 const root = createRoot(document.getElementById("root")!);
16 const paint = (input: LoadingFixtureInput) => act(async () => root.render(
17 <LocaleProvider><SessionLoadingFixture {...input} /></LocaleProvider>,
18 ));
19 const tick = (ms: number) => act(async () => mock.timers.tick(ms));
20 const indicator = () => document.querySelector(".session-loading-indicator");
21 const noRecovery = () => assert.equal(document.querySelector(".session-recovery, .session-recovery-placeholder"), null,
22 "normal loading never presents recovery or retry language");
23 mock.timers.enable({ apis: ["setTimeout"] });
24 try {
25 for (const surface of ["local", "remote", "transcript"] as const) {
26 const input: LoadingFixtureInput = { surface, identity: `${surface}-A`, phase: "loading" };
27 await paint(input);
28 await tick(249);
29 assert.equal(indicator(), null, `${surface}: quick loads have no visible loading feedback`);
30 noRecovery();
31 await paint({ ...input, phase: "ready" });
32 assert.match(document.body.textContent!, new RegExp(`Content ${input.identity}`), "ready content appears without a minimum wait");
33 await tick(2500);
34 assert.equal(indicator(), null, "completed timers cannot resurrect the indicator");
35
36 await paint(input);
37 await tick(250);
38 assert.equal(document.querySelectorAll(".session-loading-indicator").length, 1, "one loading owner across surfaces");
39 const shortLabel = indicator()!.textContent;
40 await tick(1750);
41 assert.notEqual(indicator()!.textContent, shortLabel, "slow load adds a specific explanation after two seconds");
42 noRecovery();
43 await paint({ ...input, identity: `${surface}-B` });
44 assert.equal(indicator(), null, "a new session never inherits the old loading stage");
45 await tick(200);
46 await paint(input);
47 await tick(50);
48 assert.equal(indicator(), null, "rapid A/B/A switching cancels B's deadline");
49 await tick(200);
50 assert.ok(indicator(), "the latest A owns its own deadline");
51 await paint({ ...input, phase: "ready" });
52 assert.equal(indicator(), null, "ready content removes an already visible indicator immediately");
53
54 await paint({ ...input, cached: true });
55 const cachedNode = document.querySelector(".chat-node");
56 assert.ok(cachedNode, "cached target content is mounted while history refreshes");
57 await tick(2000);
58 assert.equal(document.querySelector(".chat-node"), cachedNode, "loading stages do not replace cached content");
59 assert.equal(document.querySelectorAll(".session-loading-indicator").length, 1);
60 noRecovery();
61 await paint({ ...input, phase: "ready" });
62 assert.equal(document.querySelector(".chat-node"), cachedNode, "completion preserves the cached node");
63 await paint(input);
64 await tick(2000);
65 await paint({ ...input, generation: 2 });
66 assert.equal(indicator(), null, "same-tab session replacement resets feedback before paint");
67 await tick(249);
68 assert.equal(indicator(), null, "replacement generation owns a fresh deadline");
69 await paint({ ...input, generation: 2, phase: "ready" });
70 }
71 for (const surface of ["local", "remote"] as const) {
72 let retries = 0;
73 const input: LoadingFixtureInput = { surface, identity: `${surface}-error`, phase: "loading", onRetry: async () => { retries++; } };
74 await paint(input);
75 await tick(100);
76 await paint({ ...input, phase: "error" });
77 assert.ok(document.querySelector('[role="alert"]'), "real errors are immediate, even inside the loading grace period");
78 assert.equal(indicator(), null);
79 await act(async () => document.querySelector<HTMLButtonElement>(".session-recovery .btn--primary")!.click());
80 assert.equal(retries, 1, "error offers the owning retry action");
81 await paint(input);
82 noRecovery();
83 await tick(249);
84 assert.equal(indicator(), null, "retry gets a fresh quiet period");
85 await paint({ ...input, phase: "ready" });
86 await tick(2500);
87 assert.equal(indicator(), null);
88 }
89 await paint({ surface: "local", identity: "readable-runtime", phase: "loading", cached: true, source: "runtime" });
90 await tick(2000);
91 assert.equal(indicator(), null, "runtime activation does not obscure already readable history");
92 assert.match(document.body.textContent!, /Content readable-runtime/);
93 await paint({ surface: "remote", identity: "connecting", phase: "loading", source: "connection" });
94 await tick(2000);
95 assert.ok(indicator());
96 noRecovery();
97 await act(async () => root.unmount());
98 await tick(3000);
99 console.log("PASS session loading: quiet fast loads, staged slow loads, cache continuity, retry, and identity-fenced timers on local/remote/standalone surfaces");
100 } finally {
101 mock.timers.reset();
102 dom.window.close();
103 }
104
104 lines Plain Text