1import { EnvironmentId, type ThreadPullRequestLink } from "@t3tools/contracts";
2import { describe, expect, it } from "vite-plus/test";
3
4import {
5 resolvePullRequestTabLink,
6 surfaceShortcutActionForKey,
7 surfaceShortcutTargetsTypingContext,
8} from "./RightPanelTabs";
9
10function shortcutEvent(
11 key: string,
12 overrides: Partial<Parameters<typeof surfaceShortcutActionForKey>[1]> = {},
13): Parameters<typeof surfaceShortcutActionForKey>[1] {
14 return {
15 key,
16 altKey: false,
17 ctrlKey: false,
18 defaultPrevented: false,
19 isComposing: false,
20 metaKey: false,
21 ...overrides,
22 };
23}
24
25describe("surface shortcuts", () => {
26 const actions = [
27 { shortcut: "B", available: true, label: "Browser" },
28 { shortcut: "D", available: false, label: "Diff" },
29 { shortcut: "T", available: true, label: "Terminal" },
30 { shortcut: "M", available: true, label: "Device" },
31 ] as const;
32
33 it("falls back to the physical key when the layout types a non-Latin letter or mark", () => {
34 // Ukrainian layout: the B key types "и".
35 expect(surfaceShortcutActionForKey(actions, shortcutEvent("\u0438", { code: "KeyB" }))).toBe(
36 actions[0],
37 );
38 // Greek layout: the B key types "β".
39 expect(surfaceShortcutActionForKey(actions, shortcutEvent("\u03b2", { code: "KeyB" }))).toBe(
40 actions[0],
41 );
42 // Thai layout: the B key types the vowel sign "\u0e34", a combining mark.
43 expect(surfaceShortcutActionForKey(actions, shortcutEvent("\u0e34", { code: "KeyB" }))).toBe(
44 actions[0],
45 );
46 // Arabic (101) layout: the B key types lam alef "\u0644\u0627", two code points.
47 expect(
48 surfaceShortcutActionForKey(actions, shortcutEvent("\u0644\u0627", { code: "KeyB" })),
49 ).toBe(actions[0]);
50 // Ukrainian layout: the D key types "\u0432", but the Diff surface is unavailable.
51 expect(
52 surfaceShortcutActionForKey(actions, shortcutEvent("\u0432", { code: "KeyD" })),
53 ).toBeNull();
54 });
55
56 it("follows the typed letter on Latin layouts that move it", () => {
57 // Dvorak: the key at QWERTY B types "x", and "b" sits on QWERTY N.
58 expect(surfaceShortcutActionForKey(actions, shortcutEvent("x", { code: "KeyB" }))).toBeNull();
59 // The same key with Shift or Caps Lock types "X".
60 expect(surfaceShortcutActionForKey(actions, shortcutEvent("X", { code: "KeyB" }))).toBeNull();
61 expect(surfaceShortcutActionForKey(actions, shortcutEvent("b", { code: "KeyN" }))).toBe(
62 actions[0],
63 );
64 });
65
66 it("keeps accented Latin letters typed on a letter key", () => {
67 // Bépo: the T key types "è".
68 expect(
69 surfaceShortcutActionForKey(actions, shortcutEvent("\u00e8", { code: "KeyT" })),
70 ).toBeNull();
71 });
72
73 it("keeps punctuation typed on a letter key", () => {
74 // French AZERTY: the M key types ",".
75 expect(surfaceShortcutActionForKey(actions, shortcutEvent(",", { code: "KeyM" }))).toBeNull();
76 });
77
78 it("keeps a dead key on a letter key", () => {
79 // A dead key starts an accented letter on a Latin layout.
80 expect(
81 surfaceShortcutActionForKey(actions, shortcutEvent("Dead", { code: "KeyT" })),
82 ).toBeNull();
83 });
84
85 it("leaves modified, composing, and already-handled key events alone", () => {
86 expect(surfaceShortcutActionForKey(actions, shortcutEvent("b", { metaKey: true }))).toBeNull();
87 expect(
88 surfaceShortcutActionForKey(actions, shortcutEvent("b", { isComposing: true })),
89 ).toBeNull();
90 expect(
91 surfaceShortcutActionForKey(actions, shortcutEvent("b", { defaultPrevented: true })),
92 ).toBeNull();
93 });
94
95 it("leaves AltGr text alone unless it is a plain letter", () => {
96 const getModifierState = (key: string) => key === "AltGraph";
97 // German layout: AltGr+M types "\u00b5" and sets only AltGraph on Linux and Windows.
98 expect(
99 surfaceShortcutActionForKey(
100 actions,
101 shortcutEvent("\u00b5", { code: "KeyM", getModifierState }),
102 ),
103 ).toBeNull();
104 // Finnish layout: AltGr+B types "b", which still matches, as it does in keybindings.ts.
105 expect(
106 surfaceShortcutActionForKey(actions, shortcutEvent("b", { code: "KeyB", getModifierState })),
107 ).toBe(actions[0]);
108 });
109});
110
111describe("surface shortcut typing contexts", () => {
112 const makeTarget = (matches: string | null) => ({
113 closest(selectors: string) {
114 if (matches === null || !selectors.includes(matches)) return null;
115 return {};
116 },
117 });
118
119 it("treats form fields and every editable region as typing contexts", () => {
120 expect(surfaceShortcutTargetsTypingContext(makeTarget("input"))).toBe(true);
121 expect(surfaceShortcutTargetsTypingContext(makeTarget("textarea"))).toBe(true);
122 expect(surfaceShortcutTargetsTypingContext(makeTarget("select"))).toBe(true);
123 expect(surfaceShortcutTargetsTypingContext(makeTarget("[contenteditable]"))).toBe(true);
124 });
125});
126
127describe("pull request tab snapshots", () => {
128 const environmentId = EnvironmentId.make("local");
129 const link: ThreadPullRequestLink = {
130 host: "github.com",
131 repository: "acme/api",
132 number: 7,
133 url: "https://github.com/acme/api/pull/7",
134 source: "manual",
135 linkedAt: "2026-01-01T00:00:00Z",
136 stack: null,
137 snapshot: null,
138 };
139 it("keeps unknown linked state authoritative and scopes matches to environment and host", () => {
140 const threads = [{ environmentId, pullRequests: [link] }];
141 expect(resolvePullRequestTabLink(threads, environmentId, "github.com", link)).toBe(link);
142 expect(
143 resolvePullRequestTabLink(threads, EnvironmentId.make("remote"), "github.com", link),
144 ).toBeUndefined();
145 expect(
146 resolvePullRequestTabLink(threads, environmentId, "github.enterprise.test", link),
147 ).toBeUndefined();
148 });
149 it("uses the newest snapshot when several threads link the same PR", () => {
150 const snapshot = {
151 state: "merged" as const,
152 title: "API",
153 headBranch: "api",
154 baseBranch: "main",
155 isDraft: false,
156 updatedAt: null,
157 syncedAt: "2026-02-01T00:00:00Z",
158 };
159 const newer = { ...link, snapshot };
160 expect(
161 resolvePullRequestTabLink(
162 [{ environmentId, pullRequests: [link, newer] }],
163 environmentId,
164 "github.com",
165 link,
166 ),
167 ).toBe(newer);
168 });
169});