1import type { EnvironmentConnectionPhase } from "@t3tools/client-runtime/connection";
2import { EnvironmentId, type KeybindingRule, type ServerConfig } from "@t3tools/contracts";
3import { compileResolvedKeybindingsConfig } from "@t3tools/shared/keybindings";
4import { Atom, AtomRegistry } from "effect/reactivity";
5import { afterEach, describe, expect, it } from "vite-plus/test";
6
7import { resolveShortcutCommand } from "../keybindings";
8import { createShortcutKeybindingsAtom } from "./server";
9
10const PRIMARY = EnvironmentId.make("primary");
11const A = EnvironmentId.make("a");
12const B = EnvironmentId.make("b");
13
14// The reporter's swap: Cmd+N opens a local thread instead of a regular one.
15const SWAPPED: KeybindingRule[] = [{ key: "mod+n", command: "chat.newLocal" }];
16const STALE: KeybindingRule[] = [{ key: "mod+n", command: "chat.newWithoutProject" }];
17
18function config(rules: readonly KeybindingRule[]) {
19 return {
20 keybindings: compileResolvedKeybindingsConfig(rules),
21 providers: [],
22 } as unknown as ServerConfig;
23}
24
25function environment(phase: EnvironmentConnectionPhase, serverConfig: ServerConfig | null) {
26 return { connection: { phase, error: null, traceId: null }, serverConfig };
27}
28
29type Environment = ReturnType<typeof environment>;
30
31function makeHarness(input: {
32 primaryEnvironmentId?: EnvironmentId | null;
33 primaryConfig?: ServerConfig | null;
34 environments?: ReadonlyArray<readonly [EnvironmentId, Environment]>;
35}) {
36 const primaryEnvironmentIdAtom = Atom.make(input.primaryEnvironmentId ?? null);
37 const primaryConfigAtom = Atom.make(input.primaryConfig ?? null);
38 const environmentsAtom = Atom.make<ReadonlyMap<EnvironmentId, Environment>>(
39 new Map(input.environments ?? []),
40 );
41 const keybindingsAtom = createShortcutKeybindingsAtom({
42 primaryEnvironmentIdAtom,
43 primaryConfigAtom,
44 environmentsAtom,
45 });
46 const registry = AtomRegistry.make();
47 registries.push(registry);
48 // Keep the atom mounted, like the shortcut handlers do.
49 registry.subscribe(keybindingsAtom, () => {});
50 return {
51 registry,
52 primaryEnvironmentIdAtom,
53 primaryConfigAtom,
54 environmentsAtom,
55 keybindings: () => registry.get(keybindingsAtom),
56 cmdN: () =>
57 resolveShortcutCommand(
58 { key: "n", metaKey: true, ctrlKey: false, shiftKey: false, altKey: false },
59 registry.get(keybindingsAtom),
60 { platform: "MacIntel" },
61 ),
62 };
63}
64
65const registries: AtomRegistry.AtomRegistry[] = [];
66afterEach(() => {
67 for (const registry of registries.splice(0)) registry.dispose();
68});
69
70describe("createShortcutKeybindingsAtom", () => {
71 it("updates shortcuts from the first connected, configured environment without a primary", () => {
72 const harness = makeHarness({});
73 const aConfig = config(STALE);
74 const bConfig = config(SWAPPED);
75 const edited = config([{ key: "mod+n", command: "sidebar.toggle" }]);
76 expect(harness.cmdN()).toBe("chat.new");
77
78 // One mounted consumer follows config loading, edits, disconnects and reconnects.
79 for (const [a, b, command] of [
80 [environment("offline", aConfig), environment("connected", bConfig), "chat.newLocal"],
81 [environment("connected", null), environment("connected", bConfig), "chat.newLocal"],
82 [
83 environment("connected", aConfig),
84 environment("connected", bConfig),
85 "chat.newWithoutProject",
86 ],
87 [environment("connected", edited), environment("connected", bConfig), "sidebar.toggle"],
88 [environment("offline", edited), environment("connected", bConfig), "chat.newLocal"],
89 [environment("offline", edited), environment("offline", bConfig), "chat.new"],
90 [environment("reconnecting", edited), environment("offline", bConfig), "chat.new"],
91 [
92 environment("connected", aConfig),
93 environment("offline", bConfig),
94 "chat.newWithoutProject",
95 ],
96 ] as const) {
97 harness.registry.set(
98 harness.environmentsAtom,
99 new Map([
100 [A, a],
101 [B, b],
102 ]),
103 );
104 expect(harness.cmdN()).toBe(command);
105 }
106 });
107
108 it("uses the primary config or defaults until the primary is removed", () => {
109 const harness = makeHarness({
110 environments: [[B, environment("connected", config(SWAPPED))]],
111 });
112 expect(harness.cmdN()).toBe("chat.newLocal");
113
114 harness.registry.set(harness.primaryEnvironmentIdAtom, PRIMARY);
115 expect(harness.cmdN()).toBe("chat.new");
116
117 harness.registry.set(harness.primaryConfigAtom, config(STALE));
118 expect(harness.cmdN()).toBe("chat.newWithoutProject");
119
120 harness.registry.set(harness.primaryConfigAtom, null);
121 expect(harness.cmdN()).toBe("chat.new");
122
123 harness.registry.set(harness.primaryEnvironmentIdAtom, null);
124 expect(harness.cmdN()).toBe("chat.newLocal");
125 });
126});