1import { compileResolvedKeybindingsConfig } from "@t3tools/shared/keybindings";
2import { describe, expect, it, vi } from "vite-plus/test";
3import { page, userEvent } from "vite-plus/test/browser";
4
5import { render } from "../../test/render";
6import { openCommandPalette } from "../commandPaletteBus";
7import {
8 dispatchCommandPalette,
9 useCommandPaletteMount,
10 useCommandPaletteStore,
11} from "../commandPaletteStore";
12import { installCommandPaletteCommands } from "./commandPaletteCommands";
13
14const keybindings = compileResolvedKeybindingsConfig([
15 { key: "mod+k", command: "commandPalette.toggle" },
16]);
17
18function MountedPalette() {
19 useCommandPaletteMount();
20 return null;
21}
22
23function setup() {
24 useCommandPaletteStore.setState(useCommandPaletteStore.getInitialState(), true);
25}
26
27describe("command palette shortcuts", () => {
28 it("stay inert on routes without the palette", async () => {
29 setup();
30 using _platform = vi.spyOn(navigator, "platform", "get").mockReturnValue("Linux x86_64");
31 await using screen = await render(null, { router: "/pair" });
32 using commands = new DisposableStack();
33 commands.defer(
34 installCommandPaletteCommands(screen.router!, { keybindings: () => keybindings }),
35 );
36
37 await userEvent.keyboard("{Control>}k{/Control}");
38
39 expect(useCommandPaletteStore.getState().open).toBe(false);
40 });
41
42 it("open the palette while it is mounted, and it closes when it unmounts", async () => {
43 setup();
44 using _platform = vi.spyOn(navigator, "platform", "get").mockReturnValue("Linux x86_64");
45 await using screen = await render(<MountedPalette />, { router: "/" });
46 using commands = new DisposableStack();
47 commands.defer(
48 installCommandPaletteCommands(screen.router!, { keybindings: () => keybindings }),
49 );
50
51 await userEvent.keyboard("{Control>}k{/Control}");
52 expect(useCommandPaletteStore.getState().open).toBe(true);
53
54 await screen.rerender(<div />);
55 expect(useCommandPaletteStore.getState().open).toBe(false);
56 });
57
58 it("keep a child search open on a composing Escape", async () => {
59 setup();
60 await using screen = await render(
61 <>
62 <MountedPalette />
63 <input aria-label="Search files" />
64 </>,
65 { router: "/" },
66 );
67 using commands = new DisposableStack();
68 commands.defer(
69 installCommandPaletteCommands(screen.router!, { keybindings: () => keybindings }),
70 );
71 dispatchCommandPalette({ _tag: "ToggleMode", mode: "files" });
72 const input = page.getByRole("textbox", { name: "Search files" });
73 await input.fill("にほん");
74
75 for (const composition of [
76 { key: "Escape", isComposing: true, keyCode: 27 },
77 { key: "Escape", isComposing: false, keyCode: 229 },
78 ]) {
79 const event = new KeyboardEvent("keydown", {
80 ...composition,
81 bubbles: true,
82 cancelable: true,
83 });
84 input.element().dispatchEvent(event);
85 expect(event.defaultPrevented, JSON.stringify(composition)).toBe(false);
86 expect(useCommandPaletteStore.getState()).toMatchObject({ open: true, mode: "files" });
87 await expect.element(input).toHaveValue("にほん");
88 await expect.element(input).toHaveFocus();
89 }
90
91 const escape = new KeyboardEvent("keydown", {
92 key: "Escape",
93 keyCode: 27,
94 bubbles: true,
95 cancelable: true,
96 });
97 input.element().dispatchEvent(escape);
98 expect(escape.defaultPrevented).toBe(true);
99 });
100});
101
102describe("openCommandPalette", () => {
103 it("drops a request made while no palette is mounted", async () => {
104 setup();
105 await using screen = await render(null, { router: "/pair" });
106
107 openCommandPalette({ open: "new-thread-in" });
108 await screen.rerender(<MountedPalette />);
109 expect(useCommandPaletteStore.getState()).toMatchObject({ open: false, openIntent: null });
110
111 openCommandPalette({ open: "new-thread-in" });
112 expect(useCommandPaletteStore.getState()).toMatchObject({
113 open: true,
114 openIntent: { kind: "new-thread-in" },
115 });
116 });
117});