返回 DeepSeek-Reasonix
launcher-card-state.test.ts
根目录 / desktop / frontend / src / __tests__ / launcher-card-state.test.ts
1 // Run: tsx src/__tests__/launcher-card-state.test.ts
2 //
3 // Guards the floating launcher card state machine (the regressions this
4 // protects: the card's render condition and the toggle's pressed state must
5 // never diverge). Two layers:
6 // 1. Truth-table over resolveLauncherCardState — every combination of
7 // spaceMode × dismissed. The card is independent of the dock panel's
8 // open state: the panel has its own toggle.
9 // 2. Source contracts — App must drive the toggle through the shared
10 // resolver (not inline the condition again), and DockLauncher must keep
11 // reporting its space-yield mode upward.
12
13 import { strict as assert } from "node:assert";
14 import { readFileSync } from "node:fs";
15 import { dirname, resolve } from "node:path";
16 import { fileURLToPath } from "node:url";
17 import { resolveLauncherCardState, type SpaceMode } from "../lib/launcherCardState";
18
19 let passed = 0;
20 let failed = 0;
21
22 function eq<T>(actual: T, expected: T, label: string) {
23 if (Object.is(actual, expected)) {
24 process.stdout.write(` PASS ${label}\n`);
25 passed += 1;
26 } else {
27 process.stdout.write(` FAIL ${label}: expected ${String(expected)}, got ${String(actual)}\n`);
28 failed += 1;
29 }
30 }
31
32 const modes: SpaceMode[] = ["full", "hidden"];
33
34 // ---- 1. Truth table -------------------------------------------------------
35 process.stdout.write("truth table: spaceMode × dismissed\n");
36 for (const spaceMode of modes) {
37 for (const dismissed of [false, true]) {
38 const { renderable, visible } = resolveLauncherCardState({ spaceMode, dismissed });
39 const tag = `m=${spaceMode} d=${dismissed}`;
40 eq(renderable, spaceMode === "full", `renderable ${tag}`);
41 eq(visible, spaceMode === "full" && !dismissed, `visible ${tag}`);
42 // A visible card is always renderable (consistency invariant).
43 assert.ok(!visible || renderable, `invariant: visible implies renderable ${tag}`);
44 }
45 }
46
47 // ---- 2. Source contracts --------------------------------------------------
48 process.stdout.write("source contracts\n");
49 const testDir = dirname(fileURLToPath(import.meta.url));
50 const commandsSource = readFileSync(resolve(testDir, "../app-runtime/useWorkspacePanelCommands.ts"), "utf8");
51 const spaceSource = readFileSync(resolve(testDir, "../lib/useDockLauncherSpace.ts"), "utf8");
52 const toggleSource = readFileSync(resolve(testDir, "../app-shell/LauncherToggleButton.tsx"), "utf8");
53
54 assert.match(
55 commandsSource,
56 /const launcherCard = resolveLauncherCardState\(\{ spaceMode: launcherCardSpaceMode, dismissed: launcherDismissed \}\);/,
57 "the dock command owner drives the card through the shared resolver",
58 );
59 assert.doesNotMatch(
60 commandsSource,
61 /const launcherCardRenderable = !/,
62 "the renderable condition is not inlined (single source of truth)",
63 );
64 assert.match(
65 commandsSource,
66 /if \(launcherCardSpaceMode === "hidden"\) return;/,
67 "the toggle stays inert only while the surface is too narrow for the card",
68 );
69 assert.match(
70 spaceSource,
71 /onSpaceModeChange\?\.\(next\)/,
72 "the space hook keeps reporting the card's space-yield mode upward",
73 );
74 assert.match(
75 toggleSource,
76 /aria-pressed=\{visible\}/,
77 "the toggle's pressed state mirrors the card's actual visibility",
78 );
79
80 passed += 5; // the five assert.* checks above
81 process.stdout.write(`launcher card state: ${passed} checks passed, ${failed} failed\n`);
82 if (failed > 0) process.exit(1);
83
83 lines TYPESCRIPT