@agent-native/toolkit 0.216.0 → 0.217.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent-native.eject.json +5 -0
- package/dist/app/auth/AuthPage.d.ts.map +1 -1
- package/dist/app/auth/AuthPage.js +33 -44
- package/dist/app/auth/AuthPage.js.map +1 -1
- package/dist/app/onboarding/GettingStartedCard.d.ts +35 -0
- package/dist/app/onboarding/GettingStartedCard.d.ts.map +1 -0
- package/dist/app/onboarding/GettingStartedCard.js +38 -0
- package/dist/app/onboarding/GettingStartedCard.js.map +1 -0
- package/dist/app/onboarding/index.d.ts +1 -0
- package/dist/app/onboarding/index.d.ts.map +1 -1
- package/dist/app/onboarding/index.js +1 -0
- package/dist/app/onboarding/index.js.map +1 -1
- package/dist/app/providers/AppProviders.d.ts.map +1 -1
- package/dist/app/providers/AppProviders.js +3 -1
- package/dist/app/providers/AppProviders.js.map +1 -1
- package/dist/app/settings/AccountSettingsCard.d.ts.map +1 -1
- package/dist/app/settings/AccountSettingsCard.js +2 -1
- package/dist/app/settings/AccountSettingsCard.js.map +1 -1
- package/dist/app/settings/index.d.ts +1 -0
- package/dist/app/settings/index.d.ts.map +1 -1
- package/dist/app/settings/index.js +1 -0
- package/dist/app/settings/index.js.map +1 -1
- package/dist/app/settings/shell/SettingsShell.d.ts +40 -1
- package/dist/app/settings/shell/SettingsShell.d.ts.map +1 -1
- package/dist/app/settings/shell/SettingsShell.js +87 -20
- package/dist/app/settings/shell/SettingsShell.js.map +1 -1
- package/dist/app/settings/shell/bridge.d.ts +5 -3
- package/dist/app/settings/shell/bridge.d.ts.map +1 -1
- package/dist/app/settings/shell/bridge.js +8 -5
- package/dist/app/settings/shell/bridge.js.map +1 -1
- package/dist/app/settings/shell/index.d.ts +2 -2
- package/dist/app/settings/shell/index.d.ts.map +1 -1
- package/dist/app/settings/shell/index.js.map +1 -1
- package/dist/app/settings/shell/registry.d.ts +9 -1
- package/dist/app/settings/shell/registry.d.ts.map +1 -1
- package/dist/app/settings/shell/registry.js +16 -0
- package/dist/app/settings/shell/registry.js.map +1 -1
- package/dist/app/settings/shell/routing.d.ts +5 -4
- package/dist/app/settings/shell/routing.d.ts.map +1 -1
- package/dist/app/settings/shell/routing.js +38 -10
- package/dist/app/settings/shell/routing.js.map +1 -1
- package/dist/app/shared/CommandMenu.d.ts +86 -6
- package/dist/app/shared/CommandMenu.d.ts.map +1 -1
- package/dist/app/shared/CommandMenu.js +303 -41
- package/dist/app/shared/CommandMenu.js.map +1 -1
- package/dist/app/shared/ConsentBanner.d.ts +9 -0
- package/dist/app/shared/ConsentBanner.d.ts.map +1 -0
- package/dist/app/shared/ConsentBanner.js +35 -0
- package/dist/app/shared/ConsentBanner.js.map +1 -0
- package/dist/ui/sonner.d.ts +2 -2
- package/dist/ui/sonner.d.ts.map +1 -1
- package/dist/ui/sonner.js +2 -2
- package/dist/ui/sonner.js.map +1 -1
- package/package.json +3 -3
- package/src/app/auth/AuthPage.interaction.spec.tsx +74 -0
- package/src/app/auth/AuthPage.tsx +32 -39
- package/src/app/onboarding/GettingStartedCard.tsx +203 -0
- package/src/app/onboarding/index.ts +1 -0
- package/src/app/providers/AppProviders.tsx +7 -1
- package/src/app/settings/AccountSettingsCard.spec.tsx +31 -0
- package/src/app/settings/AccountSettingsCard.tsx +8 -0
- package/src/app/settings/index.ts +1 -0
- package/src/app/settings/shell/SettingsShell.spec.tsx +472 -1
- package/src/app/settings/shell/SettingsShell.tsx +245 -31
- package/src/app/settings/shell/bridge.spec.ts +12 -0
- package/src/app/settings/shell/bridge.ts +17 -4
- package/src/app/settings/shell/index.ts +6 -1
- package/src/app/settings/shell/registry.ts +33 -1
- package/src/app/settings/shell/routing.spec.ts +92 -1
- package/src/app/settings/shell/routing.ts +52 -9
- package/src/app/shared/CommandMenu.spec.tsx +1662 -1
- package/src/app/shared/CommandMenu.tsx +763 -164
- package/src/app/shared/ConsentBanner.spec.tsx +141 -0
- package/src/app/shared/ConsentBanner.tsx +141 -0
- package/src/ui/sonner.spec.ts +10 -0
- package/src/ui/sonner.tsx +2 -2
|
@@ -44,6 +44,9 @@ import {
|
|
|
44
44
|
openAgentSettings,
|
|
45
45
|
useCommandMenuNestedDialog,
|
|
46
46
|
useCommandMenuShortcut,
|
|
47
|
+
type AppCommandGroup,
|
|
48
|
+
type CommandMenuItemMatchContext,
|
|
49
|
+
type CommandMenuItemMatcher,
|
|
47
50
|
type CommandMenuDoc,
|
|
48
51
|
} from "./CommandMenu.js";
|
|
49
52
|
|
|
@@ -56,12 +59,13 @@ const DOCS: CommandMenuDoc[] = [
|
|
|
56
59
|
},
|
|
57
60
|
];
|
|
58
61
|
|
|
59
|
-
describe("CommandMenu
|
|
62
|
+
describe("CommandMenu", () => {
|
|
60
63
|
let container: HTMLDivElement;
|
|
61
64
|
let root: Root;
|
|
62
65
|
|
|
63
66
|
beforeEach(() => {
|
|
64
67
|
actionMocks.callAction.mockReset();
|
|
68
|
+
onOpenChange.mockReset();
|
|
65
69
|
agentEngineStatusMock.state = "missing";
|
|
66
70
|
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
|
|
67
71
|
vi.stubGlobal("requestAnimationFrame", (cb: FrameRequestCallback) => {
|
|
@@ -109,6 +113,87 @@ describe("CommandMenu docs group", () => {
|
|
|
109
113
|
});
|
|
110
114
|
}
|
|
111
115
|
|
|
116
|
+
function renderAppCommands<TAppContext>({
|
|
117
|
+
groups,
|
|
118
|
+
context,
|
|
119
|
+
filterLauncherSlot,
|
|
120
|
+
embeddedChatSlot,
|
|
121
|
+
initialSelection,
|
|
122
|
+
footerSlot,
|
|
123
|
+
itemMatcher,
|
|
124
|
+
onSearchChange,
|
|
125
|
+
renderResults,
|
|
126
|
+
renderResultsHasItems,
|
|
127
|
+
renderResultsPosition,
|
|
128
|
+
emptyState,
|
|
129
|
+
noMatchSuggestionsSlot,
|
|
130
|
+
container: portalContainer,
|
|
131
|
+
size,
|
|
132
|
+
showSettingsCommand,
|
|
133
|
+
showSignOutCommand,
|
|
134
|
+
showAgentFallback = false,
|
|
135
|
+
open = true,
|
|
136
|
+
}: {
|
|
137
|
+
groups: AppCommandGroup<TAppContext>[];
|
|
138
|
+
context?: TAppContext;
|
|
139
|
+
initialSelection?: "first" | "none";
|
|
140
|
+
itemMatcher?: CommandMenuItemMatcher;
|
|
141
|
+
onSearchChange?: (search: string) => void;
|
|
142
|
+
renderResults?: (search: string) => React.ReactNode;
|
|
143
|
+
renderResultsHasItems?: (search: string) => boolean;
|
|
144
|
+
renderResultsPosition?: "before" | "after";
|
|
145
|
+
emptyState?: React.ReactNode;
|
|
146
|
+
noMatchSuggestionsSlot?: (search: string) => React.ReactNode;
|
|
147
|
+
container?: HTMLElement;
|
|
148
|
+
size?: "default" | "wide";
|
|
149
|
+
showSettingsCommand?: boolean;
|
|
150
|
+
showSignOutCommand?: boolean;
|
|
151
|
+
showAgentFallback?: boolean;
|
|
152
|
+
open?: boolean;
|
|
153
|
+
filterLauncherSlot?: (options: {
|
|
154
|
+
search: string;
|
|
155
|
+
appContext: TAppContext | undefined;
|
|
156
|
+
close: () => void;
|
|
157
|
+
}) => React.ReactNode;
|
|
158
|
+
embeddedChatSlot?: (options: {
|
|
159
|
+
search: string;
|
|
160
|
+
appContext: TAppContext | undefined;
|
|
161
|
+
close: () => void;
|
|
162
|
+
}) => React.ReactNode;
|
|
163
|
+
footerSlot?: React.ReactNode;
|
|
164
|
+
}) {
|
|
165
|
+
act(() => {
|
|
166
|
+
root.render(
|
|
167
|
+
<CommandMenu
|
|
168
|
+
open={open}
|
|
169
|
+
onOpenChange={onOpenChange}
|
|
170
|
+
showAgentFallback={showAgentFallback}
|
|
171
|
+
showSettingsCommand={showSettingsCommand}
|
|
172
|
+
showSignOutCommand={showSignOutCommand}
|
|
173
|
+
appCommandGroups={groups}
|
|
174
|
+
commandContext={context}
|
|
175
|
+
filterLauncherSlot={filterLauncherSlot}
|
|
176
|
+
embeddedChatSlot={embeddedChatSlot}
|
|
177
|
+
initialSelection={initialSelection}
|
|
178
|
+
footerSlot={footerSlot}
|
|
179
|
+
itemMatcher={itemMatcher}
|
|
180
|
+
onSearchChange={onSearchChange}
|
|
181
|
+
renderResults={renderResults}
|
|
182
|
+
renderResultsHasItems={renderResultsHasItems}
|
|
183
|
+
renderResultsPosition={renderResultsPosition}
|
|
184
|
+
emptyState={emptyState}
|
|
185
|
+
noMatchSuggestionsSlot={noMatchSuggestionsSlot}
|
|
186
|
+
container={portalContainer}
|
|
187
|
+
size={size}
|
|
188
|
+
>
|
|
189
|
+
{null}
|
|
190
|
+
</CommandMenu>,
|
|
191
|
+
);
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const onOpenChange = vi.fn();
|
|
196
|
+
|
|
112
197
|
it("opens chat surfaces before requesting settings on the next task", () => {
|
|
113
198
|
let scheduledFrame: FrameRequestCallback | undefined;
|
|
114
199
|
vi.stubGlobal(
|
|
@@ -144,6 +229,9 @@ describe("CommandMenu docs group", () => {
|
|
|
144
229
|
|
|
145
230
|
it("filters app docs entries through the shared search field", () => {
|
|
146
231
|
renderMenu();
|
|
232
|
+
expect(
|
|
233
|
+
document.querySelector('[data-testid="command-menu-filter-launchers"]'),
|
|
234
|
+
).toBeNull();
|
|
147
235
|
|
|
148
236
|
search("logs");
|
|
149
237
|
expect(document.body.textContent).toContain(
|
|
@@ -156,6 +244,18 @@ describe("CommandMenu docs group", () => {
|
|
|
156
244
|
);
|
|
157
245
|
});
|
|
158
246
|
|
|
247
|
+
it("keeps the default dialog width and supports a shared wide variant", () => {
|
|
248
|
+
renderMenu();
|
|
249
|
+
expect(document.querySelector('[role="dialog"]')?.classList).toContain(
|
|
250
|
+
"max-w-lg",
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
renderAppCommands({ groups: [], size: "wide" });
|
|
254
|
+
expect(document.querySelector('[role="dialog"]')?.classList).toContain(
|
|
255
|
+
"max-w-3xl",
|
|
256
|
+
);
|
|
257
|
+
});
|
|
258
|
+
|
|
159
259
|
it("matches every sign-out search alias", () => {
|
|
160
260
|
renderMenu();
|
|
161
261
|
|
|
@@ -282,6 +382,32 @@ describe("CommandMenu docs group", () => {
|
|
|
282
382
|
expect(document.body.textContent).toContain("Open transcript");
|
|
283
383
|
});
|
|
284
384
|
|
|
385
|
+
it("does not show the empty state beside groups wrapped in a fragment", () => {
|
|
386
|
+
act(() => {
|
|
387
|
+
root.render(
|
|
388
|
+
<CommandMenu
|
|
389
|
+
open
|
|
390
|
+
onOpenChange={() => undefined}
|
|
391
|
+
emptyState="No matching commands"
|
|
392
|
+
showAgentFallback={false}
|
|
393
|
+
>
|
|
394
|
+
<>
|
|
395
|
+
<CommandMenu.Group heading="Actions">
|
|
396
|
+
<CommandMenu.Item onSelect={() => undefined}>
|
|
397
|
+
Open item
|
|
398
|
+
</CommandMenu.Item>
|
|
399
|
+
</CommandMenu.Group>
|
|
400
|
+
</>
|
|
401
|
+
</CommandMenu>,
|
|
402
|
+
);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
search("open");
|
|
406
|
+
|
|
407
|
+
expect(document.body.textContent).toContain("Open item");
|
|
408
|
+
expect(document.querySelector('[role="status"]')).toBeNull();
|
|
409
|
+
});
|
|
410
|
+
|
|
285
411
|
it("offers the shared About Agent-Native surface and matches version searches", () => {
|
|
286
412
|
act(() => {
|
|
287
413
|
root.render(
|
|
@@ -821,6 +947,88 @@ describe("CommandMenu docs group", () => {
|
|
|
821
947
|
expect(onOpenChange).not.toHaveBeenCalled();
|
|
822
948
|
});
|
|
823
949
|
|
|
950
|
+
it("dismisses a nested dialog before returning from an app page", async () => {
|
|
951
|
+
const dismissNested = vi.fn();
|
|
952
|
+
function NestedDialog() {
|
|
953
|
+
useCommandMenuNestedDialog(dismissNested);
|
|
954
|
+
return <div role="dialog" aria-label="Nested picker" />;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
act(() => {
|
|
958
|
+
root.render(
|
|
959
|
+
<CommandMenu
|
|
960
|
+
open
|
|
961
|
+
onOpenChange={onOpenChange}
|
|
962
|
+
showAgentFallback={false}
|
|
963
|
+
showSettingsCommand={false}
|
|
964
|
+
showSignOutCommand={false}
|
|
965
|
+
appCommandGroups={[
|
|
966
|
+
{
|
|
967
|
+
id: "main",
|
|
968
|
+
order: 1,
|
|
969
|
+
commands: [
|
|
970
|
+
{
|
|
971
|
+
id: "open-tools",
|
|
972
|
+
order: 1,
|
|
973
|
+
label: "Open tools",
|
|
974
|
+
page: {
|
|
975
|
+
id: "tools",
|
|
976
|
+
title: "Tools",
|
|
977
|
+
groups: [
|
|
978
|
+
{
|
|
979
|
+
id: "tool-actions",
|
|
980
|
+
order: 1,
|
|
981
|
+
commands: [
|
|
982
|
+
{
|
|
983
|
+
id: "refresh",
|
|
984
|
+
order: 1,
|
|
985
|
+
label: "Refresh data",
|
|
986
|
+
onSelect() {},
|
|
987
|
+
},
|
|
988
|
+
],
|
|
989
|
+
},
|
|
990
|
+
],
|
|
991
|
+
},
|
|
992
|
+
},
|
|
993
|
+
],
|
|
994
|
+
},
|
|
995
|
+
]}
|
|
996
|
+
renderContent={({ renderList }) => (
|
|
997
|
+
<>
|
|
998
|
+
{renderList()}
|
|
999
|
+
<NestedDialog />
|
|
1000
|
+
</>
|
|
1001
|
+
)}
|
|
1002
|
+
>
|
|
1003
|
+
{null}
|
|
1004
|
+
</CommandMenu>,
|
|
1005
|
+
);
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
const openPage = document.querySelector<HTMLElement>(
|
|
1009
|
+
'[cmdk-item][data-value="app-command:main:open-tools"]',
|
|
1010
|
+
);
|
|
1011
|
+
expect(openPage).toBeTruthy();
|
|
1012
|
+
act(() => openPage!.click());
|
|
1013
|
+
expect(document.body.textContent).toContain("Refresh data");
|
|
1014
|
+
|
|
1015
|
+
act(() => {
|
|
1016
|
+
document.dispatchEvent(
|
|
1017
|
+
new KeyboardEvent("keydown", {
|
|
1018
|
+
key: "Escape",
|
|
1019
|
+
bubbles: true,
|
|
1020
|
+
cancelable: true,
|
|
1021
|
+
}),
|
|
1022
|
+
);
|
|
1023
|
+
});
|
|
1024
|
+
await act(async () => Promise.resolve());
|
|
1025
|
+
|
|
1026
|
+
expect(dismissNested).toHaveBeenCalledOnce();
|
|
1027
|
+
expect(document.body.textContent).toContain("Refresh data");
|
|
1028
|
+
expect(document.body.textContent).toContain("Back");
|
|
1029
|
+
expect(onOpenChange).not.toHaveBeenCalled();
|
|
1030
|
+
});
|
|
1031
|
+
|
|
824
1032
|
it("can opt into opening from a contenteditable target", () => {
|
|
825
1033
|
function ShortcutHarness() {
|
|
826
1034
|
const [open, setOpen] = React.useState(false);
|
|
@@ -1040,4 +1248,1457 @@ describe("CommandMenu docs group", () => {
|
|
|
1040
1248
|
expect(editorHandled).toHaveBeenCalledWith(false);
|
|
1041
1249
|
expect(event.defaultPrevented).toBe(false);
|
|
1042
1250
|
});
|
|
1251
|
+
|
|
1252
|
+
it("orders app groups and commands by their explicit positions", () => {
|
|
1253
|
+
renderAppCommands({
|
|
1254
|
+
groups: [
|
|
1255
|
+
{
|
|
1256
|
+
id: "later",
|
|
1257
|
+
order: 20,
|
|
1258
|
+
heading: "Later group",
|
|
1259
|
+
commands: [
|
|
1260
|
+
{ id: "last", order: 20, label: "Last command", onSelect() {} },
|
|
1261
|
+
{ id: "first", order: 1, label: "First command", onSelect() {} },
|
|
1262
|
+
],
|
|
1263
|
+
},
|
|
1264
|
+
{
|
|
1265
|
+
id: "earlier",
|
|
1266
|
+
order: 1,
|
|
1267
|
+
heading: "Earlier group",
|
|
1268
|
+
commands: [
|
|
1269
|
+
{ id: "only", order: 1, label: "Earlier command", onSelect() {} },
|
|
1270
|
+
],
|
|
1271
|
+
},
|
|
1272
|
+
],
|
|
1273
|
+
});
|
|
1274
|
+
|
|
1275
|
+
const rows = Array.from(
|
|
1276
|
+
document.querySelectorAll<HTMLElement>(
|
|
1277
|
+
'[cmdk-item][data-value^="app-command:"]',
|
|
1278
|
+
),
|
|
1279
|
+
);
|
|
1280
|
+
expect(rows.map((row) => row.textContent?.trim())).toEqual([
|
|
1281
|
+
"Earlier command",
|
|
1282
|
+
"First command",
|
|
1283
|
+
"Last command",
|
|
1284
|
+
]);
|
|
1285
|
+
});
|
|
1286
|
+
|
|
1287
|
+
it("shows shortcut hints, searches aliases, and activates the matching command", () => {
|
|
1288
|
+
const onSelect = vi.fn();
|
|
1289
|
+
renderAppCommands({
|
|
1290
|
+
groups: [
|
|
1291
|
+
{
|
|
1292
|
+
id: "actions",
|
|
1293
|
+
order: 1,
|
|
1294
|
+
commands: [
|
|
1295
|
+
{
|
|
1296
|
+
id: "jump-to-today",
|
|
1297
|
+
order: 1,
|
|
1298
|
+
label: "Today",
|
|
1299
|
+
keywords: ["current date", "jump"],
|
|
1300
|
+
shortcutHint: "⌘ J",
|
|
1301
|
+
deferSelect: false,
|
|
1302
|
+
onSelect,
|
|
1303
|
+
},
|
|
1304
|
+
],
|
|
1305
|
+
},
|
|
1306
|
+
],
|
|
1307
|
+
});
|
|
1308
|
+
|
|
1309
|
+
expect(document.body.textContent).toContain("⌘ J");
|
|
1310
|
+
search("current date");
|
|
1311
|
+
expect(document.body.textContent).toContain("Today");
|
|
1312
|
+
|
|
1313
|
+
const input = document.querySelector<HTMLInputElement>("[cmdk-input]");
|
|
1314
|
+
expect(input).toBeTruthy();
|
|
1315
|
+
act(() => {
|
|
1316
|
+
input!.dispatchEvent(
|
|
1317
|
+
new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true }),
|
|
1318
|
+
);
|
|
1319
|
+
input!.dispatchEvent(
|
|
1320
|
+
new KeyboardEvent("keydown", { key: "Enter", bubbles: true }),
|
|
1321
|
+
);
|
|
1322
|
+
});
|
|
1323
|
+
|
|
1324
|
+
expect(onSelect).toHaveBeenCalledOnce();
|
|
1325
|
+
expect(onOpenChange).toHaveBeenCalledWith(false);
|
|
1326
|
+
|
|
1327
|
+
search("jump-to-today");
|
|
1328
|
+
expect(
|
|
1329
|
+
document.querySelector(
|
|
1330
|
+
'[cmdk-item][data-value="app-command:actions:jump-to-today"]',
|
|
1331
|
+
),
|
|
1332
|
+
).toBeNull();
|
|
1333
|
+
search("⌘ J");
|
|
1334
|
+
expect(
|
|
1335
|
+
document.querySelector(
|
|
1336
|
+
'[cmdk-item][data-value="app-command:actions:jump-to-today"]',
|
|
1337
|
+
),
|
|
1338
|
+
).toBeNull();
|
|
1339
|
+
});
|
|
1340
|
+
|
|
1341
|
+
it("uses the latest nested page commands while the page is open", () => {
|
|
1342
|
+
const createGroups = (
|
|
1343
|
+
label: string,
|
|
1344
|
+
onSelect: () => void,
|
|
1345
|
+
): AppCommandGroup[] => [
|
|
1346
|
+
{
|
|
1347
|
+
id: "main",
|
|
1348
|
+
order: 1,
|
|
1349
|
+
commands: [
|
|
1350
|
+
{
|
|
1351
|
+
id: "open-tools",
|
|
1352
|
+
order: 1,
|
|
1353
|
+
label: "Open tools",
|
|
1354
|
+
page: {
|
|
1355
|
+
id: "tools",
|
|
1356
|
+
title: "Tools",
|
|
1357
|
+
groups: [
|
|
1358
|
+
{
|
|
1359
|
+
id: "tool-actions",
|
|
1360
|
+
order: 1,
|
|
1361
|
+
commands: [
|
|
1362
|
+
{
|
|
1363
|
+
id: "refresh",
|
|
1364
|
+
order: 1,
|
|
1365
|
+
label,
|
|
1366
|
+
deferSelect: false,
|
|
1367
|
+
onSelect,
|
|
1368
|
+
},
|
|
1369
|
+
],
|
|
1370
|
+
},
|
|
1371
|
+
],
|
|
1372
|
+
},
|
|
1373
|
+
},
|
|
1374
|
+
],
|
|
1375
|
+
},
|
|
1376
|
+
];
|
|
1377
|
+
const originalSelect = vi.fn();
|
|
1378
|
+
const latestSelect = vi.fn();
|
|
1379
|
+
|
|
1380
|
+
renderAppCommands({ groups: createGroups("Old command", originalSelect) });
|
|
1381
|
+
const parentRow = document.querySelector<HTMLElement>(
|
|
1382
|
+
'[cmdk-item][data-value="app-command:main:open-tools"]',
|
|
1383
|
+
);
|
|
1384
|
+
expect(parentRow).toBeTruthy();
|
|
1385
|
+
act(() => parentRow!.click());
|
|
1386
|
+
|
|
1387
|
+
renderAppCommands({ groups: createGroups("Latest command", latestSelect) });
|
|
1388
|
+
|
|
1389
|
+
expect(document.body.textContent).toContain("Latest command");
|
|
1390
|
+
expect(document.body.textContent).not.toContain("Old command");
|
|
1391
|
+
const currentRow = document.querySelector<HTMLElement>(
|
|
1392
|
+
'[cmdk-item][data-value="app-command:tool-actions:refresh"]',
|
|
1393
|
+
);
|
|
1394
|
+
expect(currentRow).toBeTruthy();
|
|
1395
|
+
act(() => currentRow!.click());
|
|
1396
|
+
expect(latestSelect).toHaveBeenCalledOnce();
|
|
1397
|
+
expect(originalSelect).not.toHaveBeenCalled();
|
|
1398
|
+
});
|
|
1399
|
+
|
|
1400
|
+
it("pushes nested command pages and returns by Back or Escape", () => {
|
|
1401
|
+
renderAppCommands({
|
|
1402
|
+
groups: [
|
|
1403
|
+
{
|
|
1404
|
+
id: "main",
|
|
1405
|
+
order: 1,
|
|
1406
|
+
commands: [
|
|
1407
|
+
{
|
|
1408
|
+
id: "open-tools",
|
|
1409
|
+
order: 1,
|
|
1410
|
+
label: "Open tools",
|
|
1411
|
+
keywords: ["tools alias"],
|
|
1412
|
+
page: {
|
|
1413
|
+
id: "tools",
|
|
1414
|
+
title: "Tools",
|
|
1415
|
+
groups: [
|
|
1416
|
+
{
|
|
1417
|
+
id: "tool-actions",
|
|
1418
|
+
order: 1,
|
|
1419
|
+
heading: "Available tools",
|
|
1420
|
+
commands: [
|
|
1421
|
+
{
|
|
1422
|
+
id: "refresh",
|
|
1423
|
+
order: 1,
|
|
1424
|
+
label: "Refresh data",
|
|
1425
|
+
onSelect() {},
|
|
1426
|
+
},
|
|
1427
|
+
],
|
|
1428
|
+
},
|
|
1429
|
+
],
|
|
1430
|
+
},
|
|
1431
|
+
},
|
|
1432
|
+
],
|
|
1433
|
+
},
|
|
1434
|
+
],
|
|
1435
|
+
});
|
|
1436
|
+
|
|
1437
|
+
search("tools alias");
|
|
1438
|
+
const parentRow = document.querySelector<HTMLElement>(
|
|
1439
|
+
'[cmdk-item][data-value="app-command:main:open-tools"]',
|
|
1440
|
+
);
|
|
1441
|
+
expect(parentRow).toBeTruthy();
|
|
1442
|
+
act(() => parentRow!.click());
|
|
1443
|
+
expect(document.body.textContent).toContain("Refresh data");
|
|
1444
|
+
expect(document.body.textContent).toContain("Back");
|
|
1445
|
+
|
|
1446
|
+
const input = document.querySelector<HTMLInputElement>("[cmdk-input]");
|
|
1447
|
+
expect(input).toBeTruthy();
|
|
1448
|
+
const backRow = document.querySelector<HTMLElement>(
|
|
1449
|
+
'[data-testid="command-menu-page-back"]',
|
|
1450
|
+
);
|
|
1451
|
+
expect(backRow).toBeTruthy();
|
|
1452
|
+
backRow!.focus();
|
|
1453
|
+
expect(document.activeElement).toBe(backRow);
|
|
1454
|
+
act(() => backRow!.click());
|
|
1455
|
+
|
|
1456
|
+
expect(input?.value).toBe("tools alias");
|
|
1457
|
+
expect(document.activeElement).toBe(input);
|
|
1458
|
+
expect(document.body.textContent).toContain("Open tools");
|
|
1459
|
+
expect(document.body.textContent).not.toContain("Refresh data");
|
|
1460
|
+
|
|
1461
|
+
const returnedParentRow = document.querySelector<HTMLElement>(
|
|
1462
|
+
'[cmdk-item][data-value="app-command:main:open-tools"]',
|
|
1463
|
+
);
|
|
1464
|
+
expect(returnedParentRow).toBeTruthy();
|
|
1465
|
+
act(() => returnedParentRow!.click());
|
|
1466
|
+
expect(document.body.textContent).toContain("Refresh data");
|
|
1467
|
+
|
|
1468
|
+
act(() => {
|
|
1469
|
+
document.dispatchEvent(
|
|
1470
|
+
new KeyboardEvent("keydown", {
|
|
1471
|
+
key: "Escape",
|
|
1472
|
+
bubbles: true,
|
|
1473
|
+
cancelable: true,
|
|
1474
|
+
}),
|
|
1475
|
+
);
|
|
1476
|
+
});
|
|
1477
|
+
|
|
1478
|
+
expect(input?.value).toBe("tools alias");
|
|
1479
|
+
expect(document.body.textContent).toContain("Open tools");
|
|
1480
|
+
expect(document.body.textContent).not.toContain("Refresh data");
|
|
1481
|
+
});
|
|
1482
|
+
|
|
1483
|
+
it("shows an empty state when a nested page has no matching commands", () => {
|
|
1484
|
+
const hasRootResults = vi.fn(() => true);
|
|
1485
|
+
renderAppCommands({
|
|
1486
|
+
groups: [
|
|
1487
|
+
{
|
|
1488
|
+
id: "main",
|
|
1489
|
+
order: 1,
|
|
1490
|
+
commands: [
|
|
1491
|
+
{
|
|
1492
|
+
id: "open-tools",
|
|
1493
|
+
order: 1,
|
|
1494
|
+
label: "Open tools",
|
|
1495
|
+
page: {
|
|
1496
|
+
id: "tools",
|
|
1497
|
+
title: "Tools",
|
|
1498
|
+
groups: [
|
|
1499
|
+
{
|
|
1500
|
+
id: "tool-actions",
|
|
1501
|
+
order: 1,
|
|
1502
|
+
commands: [
|
|
1503
|
+
{
|
|
1504
|
+
id: "refresh",
|
|
1505
|
+
order: 1,
|
|
1506
|
+
label: "Refresh data",
|
|
1507
|
+
onSelect() {},
|
|
1508
|
+
},
|
|
1509
|
+
],
|
|
1510
|
+
},
|
|
1511
|
+
],
|
|
1512
|
+
},
|
|
1513
|
+
},
|
|
1514
|
+
],
|
|
1515
|
+
},
|
|
1516
|
+
],
|
|
1517
|
+
renderResults: () => (
|
|
1518
|
+
<CommandMenu.Group heading="Root results">
|
|
1519
|
+
<CommandMenu.Item onSelect={() => undefined}>
|
|
1520
|
+
Root fallback
|
|
1521
|
+
</CommandMenu.Item>
|
|
1522
|
+
</CommandMenu.Group>
|
|
1523
|
+
),
|
|
1524
|
+
renderResultsHasItems: hasRootResults,
|
|
1525
|
+
emptyState: "No matching commands",
|
|
1526
|
+
});
|
|
1527
|
+
|
|
1528
|
+
const parentRow = document.querySelector<HTMLElement>(
|
|
1529
|
+
'[cmdk-item][data-value="app-command:main:open-tools"]',
|
|
1530
|
+
);
|
|
1531
|
+
expect(parentRow).toBeTruthy();
|
|
1532
|
+
act(() => parentRow!.click());
|
|
1533
|
+
expect(document.body.textContent).toContain("Refresh data");
|
|
1534
|
+
expect(document.body.textContent).not.toContain("Root fallback");
|
|
1535
|
+
|
|
1536
|
+
hasRootResults.mockClear();
|
|
1537
|
+
search("no-such-command");
|
|
1538
|
+
|
|
1539
|
+
expect(hasRootResults).not.toHaveBeenCalled();
|
|
1540
|
+
expect(document.querySelector('[role="status"]')?.textContent).toContain(
|
|
1541
|
+
"No matching commands",
|
|
1542
|
+
);
|
|
1543
|
+
expect(
|
|
1544
|
+
document.querySelector('[data-testid="command-menu-page-back"]'),
|
|
1545
|
+
).toBeTruthy();
|
|
1546
|
+
});
|
|
1547
|
+
|
|
1548
|
+
it("selects the first matching page command instead of Back", () => {
|
|
1549
|
+
const onSelect = vi.fn();
|
|
1550
|
+
renderAppCommands({
|
|
1551
|
+
groups: [
|
|
1552
|
+
{
|
|
1553
|
+
id: "main",
|
|
1554
|
+
order: 1,
|
|
1555
|
+
commands: [
|
|
1556
|
+
{
|
|
1557
|
+
id: "open-tools",
|
|
1558
|
+
order: 1,
|
|
1559
|
+
label: "Open tools",
|
|
1560
|
+
page: {
|
|
1561
|
+
id: "tools",
|
|
1562
|
+
title: "Tools",
|
|
1563
|
+
groups: [
|
|
1564
|
+
{
|
|
1565
|
+
id: "tool-actions",
|
|
1566
|
+
order: 1,
|
|
1567
|
+
commands: [
|
|
1568
|
+
{
|
|
1569
|
+
id: "refresh",
|
|
1570
|
+
order: 1,
|
|
1571
|
+
label: "Refresh data",
|
|
1572
|
+
onSelect() {},
|
|
1573
|
+
},
|
|
1574
|
+
{
|
|
1575
|
+
id: "open-settings",
|
|
1576
|
+
order: 2,
|
|
1577
|
+
label: "Open settings",
|
|
1578
|
+
deferSelect: false,
|
|
1579
|
+
onSelect,
|
|
1580
|
+
},
|
|
1581
|
+
],
|
|
1582
|
+
},
|
|
1583
|
+
],
|
|
1584
|
+
},
|
|
1585
|
+
},
|
|
1586
|
+
],
|
|
1587
|
+
},
|
|
1588
|
+
],
|
|
1589
|
+
});
|
|
1590
|
+
|
|
1591
|
+
const parentRow = document.querySelector<HTMLElement>(
|
|
1592
|
+
'[cmdk-item][data-value="app-command:main:open-tools"]',
|
|
1593
|
+
);
|
|
1594
|
+
expect(parentRow).toBeTruthy();
|
|
1595
|
+
act(() => parentRow!.click());
|
|
1596
|
+
|
|
1597
|
+
search("settings");
|
|
1598
|
+
const pageCommand = document.querySelector<HTMLElement>(
|
|
1599
|
+
'[cmdk-item][data-value="app-command:tool-actions:open-settings"]',
|
|
1600
|
+
);
|
|
1601
|
+
expect(pageCommand?.getAttribute("aria-selected")).toBe("true");
|
|
1602
|
+
|
|
1603
|
+
const input = document.querySelector<HTMLInputElement>("[cmdk-input]");
|
|
1604
|
+
expect(input).toBeTruthy();
|
|
1605
|
+
act(() =>
|
|
1606
|
+
input!.dispatchEvent(
|
|
1607
|
+
new KeyboardEvent("keydown", {
|
|
1608
|
+
key: "Enter",
|
|
1609
|
+
bubbles: true,
|
|
1610
|
+
cancelable: true,
|
|
1611
|
+
}),
|
|
1612
|
+
),
|
|
1613
|
+
);
|
|
1614
|
+
expect(onSelect).toHaveBeenCalledOnce();
|
|
1615
|
+
});
|
|
1616
|
+
|
|
1617
|
+
it("keeps page transitions unselected in none-selection mode", () => {
|
|
1618
|
+
renderAppCommands({
|
|
1619
|
+
initialSelection: "none",
|
|
1620
|
+
groups: [
|
|
1621
|
+
{
|
|
1622
|
+
id: "main",
|
|
1623
|
+
order: 1,
|
|
1624
|
+
commands: [
|
|
1625
|
+
{
|
|
1626
|
+
id: "open-tools",
|
|
1627
|
+
order: 1,
|
|
1628
|
+
label: "Open tools",
|
|
1629
|
+
page: {
|
|
1630
|
+
id: "tools",
|
|
1631
|
+
title: "Tools",
|
|
1632
|
+
groups: [
|
|
1633
|
+
{
|
|
1634
|
+
id: "tool-actions",
|
|
1635
|
+
order: 1,
|
|
1636
|
+
commands: [
|
|
1637
|
+
{
|
|
1638
|
+
id: "refresh",
|
|
1639
|
+
order: 1,
|
|
1640
|
+
label: "Refresh data",
|
|
1641
|
+
onSelect() {},
|
|
1642
|
+
},
|
|
1643
|
+
],
|
|
1644
|
+
},
|
|
1645
|
+
],
|
|
1646
|
+
},
|
|
1647
|
+
},
|
|
1648
|
+
],
|
|
1649
|
+
},
|
|
1650
|
+
],
|
|
1651
|
+
});
|
|
1652
|
+
|
|
1653
|
+
const input = document.querySelector<HTMLInputElement>("[cmdk-input]");
|
|
1654
|
+
expect(input).toBeTruthy();
|
|
1655
|
+
act(() =>
|
|
1656
|
+
input!.dispatchEvent(
|
|
1657
|
+
new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true }),
|
|
1658
|
+
),
|
|
1659
|
+
);
|
|
1660
|
+
act(() =>
|
|
1661
|
+
input!.dispatchEvent(
|
|
1662
|
+
new KeyboardEvent("keydown", { key: "Enter", bubbles: true }),
|
|
1663
|
+
),
|
|
1664
|
+
);
|
|
1665
|
+
expect(document.body.textContent).toContain("Refresh data");
|
|
1666
|
+
expect(
|
|
1667
|
+
document.querySelector('[cmdk-item][aria-selected="true"]'),
|
|
1668
|
+
).toBeNull();
|
|
1669
|
+
|
|
1670
|
+
const backButton = document.querySelector<HTMLButtonElement>(
|
|
1671
|
+
'[data-testid="command-menu-page-back"]',
|
|
1672
|
+
);
|
|
1673
|
+
expect(backButton).toBeTruthy();
|
|
1674
|
+
act(() => backButton!.click());
|
|
1675
|
+
expect(document.body.textContent).toContain("Open tools");
|
|
1676
|
+
expect(
|
|
1677
|
+
document.querySelector('[cmdk-item][aria-selected="true"]'),
|
|
1678
|
+
).toBeNull();
|
|
1679
|
+
});
|
|
1680
|
+
|
|
1681
|
+
it("uses app context to control command visibility and enabled state", () => {
|
|
1682
|
+
const onSelect = vi.fn();
|
|
1683
|
+
renderAppCommands({
|
|
1684
|
+
context: { canEdit: false },
|
|
1685
|
+
groups: [
|
|
1686
|
+
{
|
|
1687
|
+
id: "actions",
|
|
1688
|
+
order: 1,
|
|
1689
|
+
commands: [
|
|
1690
|
+
{
|
|
1691
|
+
id: "hidden-record",
|
|
1692
|
+
order: 0,
|
|
1693
|
+
label: "Hidden record",
|
|
1694
|
+
visibleWhen: ({ appContext }) => appContext?.canEdit === true,
|
|
1695
|
+
onSelect,
|
|
1696
|
+
},
|
|
1697
|
+
{
|
|
1698
|
+
id: "edit-record",
|
|
1699
|
+
order: 1,
|
|
1700
|
+
label: "Edit record",
|
|
1701
|
+
enabledWhen: ({ appContext }) => appContext?.canEdit ?? false,
|
|
1702
|
+
onSelect,
|
|
1703
|
+
},
|
|
1704
|
+
],
|
|
1705
|
+
},
|
|
1706
|
+
],
|
|
1707
|
+
});
|
|
1708
|
+
|
|
1709
|
+
const row = document.querySelector<HTMLElement>(
|
|
1710
|
+
'[cmdk-item][data-value="app-command:actions:edit-record"]',
|
|
1711
|
+
);
|
|
1712
|
+
expect(
|
|
1713
|
+
document.querySelector(
|
|
1714
|
+
'[cmdk-item][data-value="app-command:actions:hidden-record"]',
|
|
1715
|
+
),
|
|
1716
|
+
).toBeNull();
|
|
1717
|
+
expect(row?.getAttribute("aria-disabled")).toBe("true");
|
|
1718
|
+
act(() => row?.click());
|
|
1719
|
+
expect(onSelect).not.toHaveBeenCalled();
|
|
1720
|
+
});
|
|
1721
|
+
|
|
1722
|
+
it("applies the public item matcher to app command rows", () => {
|
|
1723
|
+
const itemMatcher = vi.fn(
|
|
1724
|
+
({ query, label, value, keywords }: CommandMenuItemMatchContext) =>
|
|
1725
|
+
query === "alias" &&
|
|
1726
|
+
label === "Open record" &&
|
|
1727
|
+
value === "record" &&
|
|
1728
|
+
keywords.includes("unrelated"),
|
|
1729
|
+
);
|
|
1730
|
+
renderAppCommands({
|
|
1731
|
+
itemMatcher,
|
|
1732
|
+
groups: [
|
|
1733
|
+
{
|
|
1734
|
+
id: "actions",
|
|
1735
|
+
order: 1,
|
|
1736
|
+
commands: [
|
|
1737
|
+
{
|
|
1738
|
+
id: "record",
|
|
1739
|
+
order: 1,
|
|
1740
|
+
label: "Open record",
|
|
1741
|
+
keywords: ["unrelated"],
|
|
1742
|
+
onSelect() {},
|
|
1743
|
+
},
|
|
1744
|
+
],
|
|
1745
|
+
},
|
|
1746
|
+
],
|
|
1747
|
+
});
|
|
1748
|
+
|
|
1749
|
+
search("alias");
|
|
1750
|
+
|
|
1751
|
+
expect(itemMatcher).toHaveBeenCalledWith({
|
|
1752
|
+
query: "alias",
|
|
1753
|
+
label: "Open record",
|
|
1754
|
+
value: "record",
|
|
1755
|
+
keywords: ["unrelated"],
|
|
1756
|
+
});
|
|
1757
|
+
expect(
|
|
1758
|
+
document.querySelector(
|
|
1759
|
+
'[cmdk-item][data-value="app-command:actions:record"]',
|
|
1760
|
+
),
|
|
1761
|
+
).toBeTruthy();
|
|
1762
|
+
});
|
|
1763
|
+
|
|
1764
|
+
it("uses the app matcher even when the group heading matches", () => {
|
|
1765
|
+
const itemMatcher = vi.fn(() => false);
|
|
1766
|
+
renderAppCommands({
|
|
1767
|
+
itemMatcher,
|
|
1768
|
+
groups: [
|
|
1769
|
+
{
|
|
1770
|
+
id: "reports",
|
|
1771
|
+
order: 1,
|
|
1772
|
+
heading: "Reports",
|
|
1773
|
+
commands: [
|
|
1774
|
+
{
|
|
1775
|
+
id: "open-report",
|
|
1776
|
+
order: 1,
|
|
1777
|
+
label: "Open report",
|
|
1778
|
+
onSelect() {},
|
|
1779
|
+
},
|
|
1780
|
+
],
|
|
1781
|
+
},
|
|
1782
|
+
],
|
|
1783
|
+
});
|
|
1784
|
+
|
|
1785
|
+
search("reports");
|
|
1786
|
+
|
|
1787
|
+
expect(itemMatcher).toHaveBeenCalled();
|
|
1788
|
+
expect(
|
|
1789
|
+
document.querySelector(
|
|
1790
|
+
'[cmdk-item][data-value="app-command:reports:open-report"]',
|
|
1791
|
+
),
|
|
1792
|
+
).toBeNull();
|
|
1793
|
+
});
|
|
1794
|
+
|
|
1795
|
+
it("renders the selected command inline preview", () => {
|
|
1796
|
+
renderAppCommands({
|
|
1797
|
+
groups: [
|
|
1798
|
+
{
|
|
1799
|
+
id: "results",
|
|
1800
|
+
order: 1,
|
|
1801
|
+
commands: [
|
|
1802
|
+
{
|
|
1803
|
+
id: "record",
|
|
1804
|
+
order: 1,
|
|
1805
|
+
label: "Meeting notes",
|
|
1806
|
+
preview: <p>First preview</p>,
|
|
1807
|
+
onSelect() {},
|
|
1808
|
+
},
|
|
1809
|
+
{
|
|
1810
|
+
id: "meeting-agenda",
|
|
1811
|
+
order: 2,
|
|
1812
|
+
label: "Meeting agenda",
|
|
1813
|
+
preview: <p>Selected preview</p>,
|
|
1814
|
+
onSelect() {},
|
|
1815
|
+
},
|
|
1816
|
+
],
|
|
1817
|
+
},
|
|
1818
|
+
],
|
|
1819
|
+
});
|
|
1820
|
+
|
|
1821
|
+
expect(
|
|
1822
|
+
document.querySelector('[data-testid="command-menu-inline-preview"]')
|
|
1823
|
+
?.textContent,
|
|
1824
|
+
).toContain("First preview");
|
|
1825
|
+
|
|
1826
|
+
const input = document.querySelector<HTMLInputElement>("[cmdk-input]");
|
|
1827
|
+
expect(input).toBeTruthy();
|
|
1828
|
+
act(() =>
|
|
1829
|
+
input!.dispatchEvent(
|
|
1830
|
+
new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true }),
|
|
1831
|
+
),
|
|
1832
|
+
);
|
|
1833
|
+
expect(
|
|
1834
|
+
document.querySelector('[data-testid="command-menu-inline-preview"]')
|
|
1835
|
+
?.textContent,
|
|
1836
|
+
).toContain("Selected preview");
|
|
1837
|
+
});
|
|
1838
|
+
|
|
1839
|
+
it("hides the app preview when a non-app result is selected", () => {
|
|
1840
|
+
renderAppCommands({
|
|
1841
|
+
groups: [
|
|
1842
|
+
{
|
|
1843
|
+
id: "results",
|
|
1844
|
+
order: 1,
|
|
1845
|
+
commands: [
|
|
1846
|
+
{
|
|
1847
|
+
id: "record",
|
|
1848
|
+
order: 1,
|
|
1849
|
+
label: "Meeting notes",
|
|
1850
|
+
preview: <p>Record preview</p>,
|
|
1851
|
+
onSelect() {},
|
|
1852
|
+
},
|
|
1853
|
+
],
|
|
1854
|
+
},
|
|
1855
|
+
],
|
|
1856
|
+
renderResults: () => (
|
|
1857
|
+
<CommandMenu.Group heading="Live results">
|
|
1858
|
+
<CommandMenu.Item onSelect={() => undefined}>
|
|
1859
|
+
Open live result
|
|
1860
|
+
</CommandMenu.Item>
|
|
1861
|
+
</CommandMenu.Group>
|
|
1862
|
+
),
|
|
1863
|
+
});
|
|
1864
|
+
|
|
1865
|
+
expect(
|
|
1866
|
+
document.querySelector('[cmdk-item][aria-selected="true"]')?.textContent,
|
|
1867
|
+
).toContain("Open live result");
|
|
1868
|
+
expect(
|
|
1869
|
+
document.querySelector('[data-testid="command-menu-inline-preview"]'),
|
|
1870
|
+
).toBeNull();
|
|
1871
|
+
});
|
|
1872
|
+
|
|
1873
|
+
it("passes current search and close behavior to the embedded chat slot", () => {
|
|
1874
|
+
renderAppCommands({
|
|
1875
|
+
groups: [],
|
|
1876
|
+
context: { workspace: "alpha" },
|
|
1877
|
+
embeddedChatSlot: ({ search: currentSearch, close, appContext }) => (
|
|
1878
|
+
<div>
|
|
1879
|
+
<span>{currentSearch}</span>
|
|
1880
|
+
<span>{appContext?.workspace}</span>
|
|
1881
|
+
<button type="button" onClick={close}>
|
|
1882
|
+
Close embedded chat
|
|
1883
|
+
</button>
|
|
1884
|
+
</div>
|
|
1885
|
+
),
|
|
1886
|
+
});
|
|
1887
|
+
|
|
1888
|
+
search("summarize this");
|
|
1889
|
+
expect(document.body.textContent).toContain("summarize this");
|
|
1890
|
+
expect(document.body.textContent).toContain("alpha");
|
|
1891
|
+
|
|
1892
|
+
const closeButton = Array.from(document.querySelectorAll("button")).find(
|
|
1893
|
+
(button) => button.textContent === "Close embedded chat",
|
|
1894
|
+
);
|
|
1895
|
+
expect(closeButton).toBeTruthy();
|
|
1896
|
+
act(() => closeButton!.click());
|
|
1897
|
+
expect(onOpenChange).toHaveBeenCalledWith(false);
|
|
1898
|
+
});
|
|
1899
|
+
|
|
1900
|
+
it("keeps embedded chat keyboard input out of command selection", () => {
|
|
1901
|
+
const onSelect = vi.fn();
|
|
1902
|
+
renderAppCommands({
|
|
1903
|
+
groups: [
|
|
1904
|
+
{
|
|
1905
|
+
id: "actions",
|
|
1906
|
+
order: 1,
|
|
1907
|
+
commands: [
|
|
1908
|
+
{
|
|
1909
|
+
id: "open-record",
|
|
1910
|
+
order: 1,
|
|
1911
|
+
label: "Open record",
|
|
1912
|
+
onSelect,
|
|
1913
|
+
},
|
|
1914
|
+
],
|
|
1915
|
+
},
|
|
1916
|
+
],
|
|
1917
|
+
embeddedChatSlot: () => <input aria-label="Embedded chat" />,
|
|
1918
|
+
});
|
|
1919
|
+
|
|
1920
|
+
const input = document.querySelector<HTMLInputElement>(
|
|
1921
|
+
'[aria-label="Embedded chat"]',
|
|
1922
|
+
);
|
|
1923
|
+
expect(input).toBeTruthy();
|
|
1924
|
+
const selectedBefore = document.querySelector(
|
|
1925
|
+
'[cmdk-item][aria-selected="true"]',
|
|
1926
|
+
);
|
|
1927
|
+
const dialog = document.querySelector<HTMLElement>('[role="dialog"]');
|
|
1928
|
+
const dialogKeyDown = vi.fn();
|
|
1929
|
+
dialog?.addEventListener("keydown", dialogKeyDown);
|
|
1930
|
+
const tab = new KeyboardEvent("keydown", {
|
|
1931
|
+
key: "Tab",
|
|
1932
|
+
bubbles: true,
|
|
1933
|
+
cancelable: true,
|
|
1934
|
+
});
|
|
1935
|
+
act(() => input!.dispatchEvent(tab));
|
|
1936
|
+
expect(dialogKeyDown).toHaveBeenCalledOnce();
|
|
1937
|
+
const arrowDown = new KeyboardEvent("keydown", {
|
|
1938
|
+
key: "ArrowDown",
|
|
1939
|
+
bubbles: true,
|
|
1940
|
+
cancelable: true,
|
|
1941
|
+
});
|
|
1942
|
+
const enter = new KeyboardEvent("keydown", {
|
|
1943
|
+
key: "Enter",
|
|
1944
|
+
bubbles: true,
|
|
1945
|
+
cancelable: true,
|
|
1946
|
+
});
|
|
1947
|
+
act(() => {
|
|
1948
|
+
input!.dispatchEvent(arrowDown);
|
|
1949
|
+
input!.dispatchEvent(enter);
|
|
1950
|
+
});
|
|
1951
|
+
|
|
1952
|
+
expect(arrowDown.defaultPrevented).toBe(false);
|
|
1953
|
+
expect(enter.defaultPrevented).toBe(false);
|
|
1954
|
+
expect(onSelect).not.toHaveBeenCalled();
|
|
1955
|
+
expect(onOpenChange).not.toHaveBeenCalled();
|
|
1956
|
+
expect(document.querySelector('[cmdk-item][aria-selected="true"]')).toBe(
|
|
1957
|
+
selectedBefore,
|
|
1958
|
+
);
|
|
1959
|
+
});
|
|
1960
|
+
|
|
1961
|
+
it("passes palette context to filter launchers and keeps their keys out of command navigation", () => {
|
|
1962
|
+
renderAppCommands({
|
|
1963
|
+
groups: [
|
|
1964
|
+
{
|
|
1965
|
+
id: "reports",
|
|
1966
|
+
order: 1,
|
|
1967
|
+
commands: [
|
|
1968
|
+
{
|
|
1969
|
+
id: "revenue-report",
|
|
1970
|
+
order: 1,
|
|
1971
|
+
label: "Revenue report",
|
|
1972
|
+
onSelect() {},
|
|
1973
|
+
},
|
|
1974
|
+
{
|
|
1975
|
+
id: "revenue-forecast",
|
|
1976
|
+
order: 2,
|
|
1977
|
+
label: "Revenue forecast",
|
|
1978
|
+
onSelect() {},
|
|
1979
|
+
},
|
|
1980
|
+
],
|
|
1981
|
+
},
|
|
1982
|
+
],
|
|
1983
|
+
context: { workspace: "alpha" },
|
|
1984
|
+
showSettingsCommand: false,
|
|
1985
|
+
showSignOutCommand: false,
|
|
1986
|
+
filterLauncherSlot: ({ search: currentSearch, appContext, close }) => (
|
|
1987
|
+
<button type="button" onClick={close}>
|
|
1988
|
+
{currentSearch}:{appContext?.workspace}:Filter records
|
|
1989
|
+
</button>
|
|
1990
|
+
),
|
|
1991
|
+
});
|
|
1992
|
+
|
|
1993
|
+
search("revenue");
|
|
1994
|
+
const slot = document.querySelector(
|
|
1995
|
+
'[data-testid="command-menu-filter-launchers"]',
|
|
1996
|
+
);
|
|
1997
|
+
expect(slot?.textContent).toContain("revenue:alpha:Filter records");
|
|
1998
|
+
|
|
1999
|
+
const selectedBefore = document.querySelector<HTMLElement>(
|
|
2000
|
+
'[cmdk-item][aria-selected="true"]',
|
|
2001
|
+
);
|
|
2002
|
+
expect(selectedBefore?.textContent).toContain("Revenue report");
|
|
2003
|
+
const arrowDown = new KeyboardEvent("keydown", {
|
|
2004
|
+
key: "ArrowDown",
|
|
2005
|
+
bubbles: true,
|
|
2006
|
+
cancelable: true,
|
|
2007
|
+
});
|
|
2008
|
+
const launcher = slot?.querySelector("button");
|
|
2009
|
+
expect(launcher).toBeTruthy();
|
|
2010
|
+
act(() => launcher!.dispatchEvent(arrowDown));
|
|
2011
|
+
expect(document.querySelector('[cmdk-item][aria-selected="true"]')).toBe(
|
|
2012
|
+
selectedBefore,
|
|
2013
|
+
);
|
|
2014
|
+
|
|
2015
|
+
act(() => launcher!.click());
|
|
2016
|
+
expect(onOpenChange).toHaveBeenCalledWith(false);
|
|
2017
|
+
});
|
|
2018
|
+
|
|
2019
|
+
it("keeps disabled items searchable and unselectable", () => {
|
|
2020
|
+
const onSelect = vi.fn();
|
|
2021
|
+
act(() => {
|
|
2022
|
+
root.render(
|
|
2023
|
+
<CommandMenu
|
|
2024
|
+
open
|
|
2025
|
+
onOpenChange={() => undefined}
|
|
2026
|
+
showAgentFallback={false}
|
|
2027
|
+
showSettingsCommand={false}
|
|
2028
|
+
showSignOutCommand={false}
|
|
2029
|
+
>
|
|
2030
|
+
<CommandMenu.Group heading="Actions">
|
|
2031
|
+
<CommandMenu.Item
|
|
2032
|
+
value="locked-action"
|
|
2033
|
+
keywords={["locked alias"]}
|
|
2034
|
+
disabled
|
|
2035
|
+
onSelect={onSelect}
|
|
2036
|
+
>
|
|
2037
|
+
Locked action
|
|
2038
|
+
</CommandMenu.Item>
|
|
2039
|
+
</CommandMenu.Group>
|
|
2040
|
+
</CommandMenu>,
|
|
2041
|
+
);
|
|
2042
|
+
});
|
|
2043
|
+
|
|
2044
|
+
search("locked alias");
|
|
2045
|
+
const row = document.querySelector<HTMLElement>(
|
|
2046
|
+
'[cmdk-item][data-value="locked-action"]',
|
|
2047
|
+
);
|
|
2048
|
+
expect(row?.textContent).toContain("Locked action");
|
|
2049
|
+
expect(row?.getAttribute("aria-disabled")).toBe("true");
|
|
2050
|
+
act(() => row?.click());
|
|
2051
|
+
expect(onSelect).not.toHaveBeenCalled();
|
|
2052
|
+
});
|
|
2053
|
+
|
|
2054
|
+
it("shows a dynamic item's preview inside only its selected row", () => {
|
|
2055
|
+
act(() => {
|
|
2056
|
+
root.render(
|
|
2057
|
+
<CommandMenu
|
|
2058
|
+
open
|
|
2059
|
+
onOpenChange={() => undefined}
|
|
2060
|
+
showAgentFallback={false}
|
|
2061
|
+
showSettingsCommand={false}
|
|
2062
|
+
showSignOutCommand={false}
|
|
2063
|
+
initialSelection="first"
|
|
2064
|
+
>
|
|
2065
|
+
<CommandMenu.Group heading="Records">
|
|
2066
|
+
<CommandMenu.Item
|
|
2067
|
+
value="record-one"
|
|
2068
|
+
preview={<span>First record details</span>}
|
|
2069
|
+
onSelect={() => undefined}
|
|
2070
|
+
>
|
|
2071
|
+
First record
|
|
2072
|
+
</CommandMenu.Item>
|
|
2073
|
+
<CommandMenu.Item
|
|
2074
|
+
value="record-two"
|
|
2075
|
+
preview={<span>Second record details</span>}
|
|
2076
|
+
onSelect={() => undefined}
|
|
2077
|
+
>
|
|
2078
|
+
Second record
|
|
2079
|
+
</CommandMenu.Item>
|
|
2080
|
+
</CommandMenu.Group>
|
|
2081
|
+
</CommandMenu>,
|
|
2082
|
+
);
|
|
2083
|
+
});
|
|
2084
|
+
|
|
2085
|
+
const selectedRow = document.querySelector<HTMLElement>(
|
|
2086
|
+
'[cmdk-item][data-value="record-one"]',
|
|
2087
|
+
);
|
|
2088
|
+
const otherRow = document.querySelector<HTMLElement>(
|
|
2089
|
+
'[cmdk-item][data-value="record-two"]',
|
|
2090
|
+
);
|
|
2091
|
+
expect(
|
|
2092
|
+
selectedRow?.querySelector('[data-testid="command-menu-item-preview"]')
|
|
2093
|
+
?.textContent,
|
|
2094
|
+
).toBe("First record details");
|
|
2095
|
+
expect(
|
|
2096
|
+
otherRow?.querySelector('[data-testid="command-menu-item-preview"]'),
|
|
2097
|
+
).toBeNull();
|
|
2098
|
+
|
|
2099
|
+
const input = document.querySelector<HTMLInputElement>("[cmdk-input]");
|
|
2100
|
+
act(() =>
|
|
2101
|
+
input!.dispatchEvent(
|
|
2102
|
+
new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true }),
|
|
2103
|
+
),
|
|
2104
|
+
);
|
|
2105
|
+
|
|
2106
|
+
expect(
|
|
2107
|
+
document
|
|
2108
|
+
.querySelector('[cmdk-item][data-value="record-two"]')
|
|
2109
|
+
?.querySelector('[data-testid="command-menu-item-preview"]')
|
|
2110
|
+
?.textContent,
|
|
2111
|
+
).toBe("Second record details");
|
|
2112
|
+
});
|
|
2113
|
+
|
|
2114
|
+
it("uses an app matcher and reports each search change", () => {
|
|
2115
|
+
const itemMatcher = vi.fn(
|
|
2116
|
+
({
|
|
2117
|
+
query,
|
|
2118
|
+
label,
|
|
2119
|
+
value,
|
|
2120
|
+
keywords,
|
|
2121
|
+
}: Parameters<CommandMenuItemMatcher>[0]) =>
|
|
2122
|
+
query === "alias" &&
|
|
2123
|
+
label === "Open record" &&
|
|
2124
|
+
value === "record" &&
|
|
2125
|
+
keywords.includes("unrelated"),
|
|
2126
|
+
);
|
|
2127
|
+
const onSearchChange = vi.fn();
|
|
2128
|
+
|
|
2129
|
+
act(() => {
|
|
2130
|
+
root.render(
|
|
2131
|
+
<CommandMenu
|
|
2132
|
+
open
|
|
2133
|
+
onOpenChange={() => undefined}
|
|
2134
|
+
showAgentFallback={false}
|
|
2135
|
+
showSettingsCommand={false}
|
|
2136
|
+
showSignOutCommand={false}
|
|
2137
|
+
itemMatcher={itemMatcher}
|
|
2138
|
+
onSearchChange={onSearchChange}
|
|
2139
|
+
>
|
|
2140
|
+
<CommandMenu.Group heading="Actions">
|
|
2141
|
+
<CommandMenu.Item
|
|
2142
|
+
value="record"
|
|
2143
|
+
keywords={["unrelated"]}
|
|
2144
|
+
onSelect={() => undefined}
|
|
2145
|
+
>
|
|
2146
|
+
Open record
|
|
2147
|
+
</CommandMenu.Item>
|
|
2148
|
+
</CommandMenu.Group>
|
|
2149
|
+
</CommandMenu>,
|
|
2150
|
+
);
|
|
2151
|
+
});
|
|
2152
|
+
|
|
2153
|
+
search("alias");
|
|
2154
|
+
|
|
2155
|
+
expect(itemMatcher).toHaveBeenCalledWith({
|
|
2156
|
+
query: "alias",
|
|
2157
|
+
label: "Open record",
|
|
2158
|
+
value: "record",
|
|
2159
|
+
keywords: ["unrelated"],
|
|
2160
|
+
});
|
|
2161
|
+
expect(
|
|
2162
|
+
document.querySelector('[cmdk-item][data-value="record"]'),
|
|
2163
|
+
).toBeTruthy();
|
|
2164
|
+
expect(onSearchChange).toHaveBeenLastCalledWith("alias");
|
|
2165
|
+
expect(onSearchChange.mock.calls.map(([query]) => query)).toEqual([
|
|
2166
|
+
"alias",
|
|
2167
|
+
]);
|
|
2168
|
+
|
|
2169
|
+
search("unrelated");
|
|
2170
|
+
expect(
|
|
2171
|
+
document.querySelector('[cmdk-item][data-value="record"]'),
|
|
2172
|
+
).toBeNull();
|
|
2173
|
+
expect(onSearchChange.mock.calls.map(([query]) => query)).toEqual([
|
|
2174
|
+
"alias",
|
|
2175
|
+
"unrelated",
|
|
2176
|
+
]);
|
|
2177
|
+
|
|
2178
|
+
act(() => {
|
|
2179
|
+
root.render(
|
|
2180
|
+
<CommandMenu
|
|
2181
|
+
open={false}
|
|
2182
|
+
onOpenChange={() => undefined}
|
|
2183
|
+
showAgentFallback={false}
|
|
2184
|
+
onSearchChange={onSearchChange}
|
|
2185
|
+
>
|
|
2186
|
+
{null}
|
|
2187
|
+
</CommandMenu>,
|
|
2188
|
+
);
|
|
2189
|
+
});
|
|
2190
|
+
expect(onSearchChange).toHaveBeenLastCalledWith("");
|
|
2191
|
+
expect(onSearchChange.mock.calls.map(([query]) => query)).toEqual([
|
|
2192
|
+
"alias",
|
|
2193
|
+
"unrelated",
|
|
2194
|
+
"",
|
|
2195
|
+
]);
|
|
2196
|
+
});
|
|
2197
|
+
|
|
2198
|
+
it("places dynamic results around app command groups", () => {
|
|
2199
|
+
const groups = [
|
|
2200
|
+
{
|
|
2201
|
+
id: "app-actions",
|
|
2202
|
+
order: 1,
|
|
2203
|
+
heading: "App actions",
|
|
2204
|
+
commands: [
|
|
2205
|
+
{
|
|
2206
|
+
id: "open-record",
|
|
2207
|
+
order: 1,
|
|
2208
|
+
label: "Open record",
|
|
2209
|
+
keywords: ["match"],
|
|
2210
|
+
onSelect() {},
|
|
2211
|
+
},
|
|
2212
|
+
],
|
|
2213
|
+
},
|
|
2214
|
+
];
|
|
2215
|
+
const renderResults = () => (
|
|
2216
|
+
<CommandMenu.Group heading="Live results">
|
|
2217
|
+
<CommandMenu.Item onSelect={() => undefined}>
|
|
2218
|
+
Live match
|
|
2219
|
+
</CommandMenu.Item>
|
|
2220
|
+
</CommandMenu.Group>
|
|
2221
|
+
);
|
|
2222
|
+
|
|
2223
|
+
renderAppCommands({
|
|
2224
|
+
groups,
|
|
2225
|
+
renderResultsPosition: "after",
|
|
2226
|
+
renderResults,
|
|
2227
|
+
});
|
|
2228
|
+
search("match");
|
|
2229
|
+
|
|
2230
|
+
const listText = document.querySelector("[cmdk-list]")?.textContent ?? "";
|
|
2231
|
+
expect(listText.indexOf("Open record")).toBeGreaterThanOrEqual(0);
|
|
2232
|
+
expect(listText.indexOf("Live match")).toBeGreaterThan(
|
|
2233
|
+
listText.indexOf("Open record"),
|
|
2234
|
+
);
|
|
2235
|
+
|
|
2236
|
+
renderAppCommands({ groups, renderResults });
|
|
2237
|
+
search("match");
|
|
2238
|
+
const defaultListText =
|
|
2239
|
+
document.querySelector("[cmdk-list]")?.textContent ?? "";
|
|
2240
|
+
expect(defaultListText.indexOf("Live match")).toBeLessThan(
|
|
2241
|
+
defaultListText.indexOf("Open record"),
|
|
2242
|
+
);
|
|
2243
|
+
});
|
|
2244
|
+
|
|
2245
|
+
it("allows apps to hide Settings and Sign out while preserving their defaults", () => {
|
|
2246
|
+
renderMenu();
|
|
2247
|
+
expect(document.body.textContent).toContain("Settings");
|
|
2248
|
+
expect(document.body.textContent).toContain("Log out");
|
|
2249
|
+
|
|
2250
|
+
renderAppCommands({
|
|
2251
|
+
groups: [],
|
|
2252
|
+
showSettingsCommand: false,
|
|
2253
|
+
showSignOutCommand: false,
|
|
2254
|
+
});
|
|
2255
|
+
expect(document.body.textContent).not.toContain("Settings");
|
|
2256
|
+
expect(document.body.textContent).not.toContain("Log out");
|
|
2257
|
+
});
|
|
2258
|
+
|
|
2259
|
+
it("renders a custom empty state beside the agent fallback and palette footer", async () => {
|
|
2260
|
+
actionMocks.callAction.mockResolvedValue({ engines: [], current: {} });
|
|
2261
|
+
vi.stubGlobal(
|
|
2262
|
+
"fetch",
|
|
2263
|
+
vi.fn(async (input: unknown) => {
|
|
2264
|
+
const url = String(input);
|
|
2265
|
+
if (url.includes("env-status")) return Response.json([]);
|
|
2266
|
+
if (url.includes("agent-engine/status")) {
|
|
2267
|
+
return Response.json({ configured: false });
|
|
2268
|
+
}
|
|
2269
|
+
return Response.json({});
|
|
2270
|
+
}),
|
|
2271
|
+
);
|
|
2272
|
+
|
|
2273
|
+
renderAppCommands({
|
|
2274
|
+
groups: [],
|
|
2275
|
+
emptyState: "No matching records",
|
|
2276
|
+
footerSlot: <span>⌘ K to search</span>,
|
|
2277
|
+
showAgentFallback: true,
|
|
2278
|
+
});
|
|
2279
|
+
await act(async () => {
|
|
2280
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
2281
|
+
});
|
|
2282
|
+
|
|
2283
|
+
search("no-such-command");
|
|
2284
|
+
|
|
2285
|
+
expect(document.querySelector('[role="status"]')?.textContent).toContain(
|
|
2286
|
+
"No matching records",
|
|
2287
|
+
);
|
|
2288
|
+
expect(document.querySelector('[role="note"]')?.textContent).toContain(
|
|
2289
|
+
"⌘ K to search",
|
|
2290
|
+
);
|
|
2291
|
+
expect(document.body.textContent).toContain("Ask AI");
|
|
2292
|
+
});
|
|
2293
|
+
|
|
2294
|
+
it("shows the custom empty state when dynamic results contain no command items", () => {
|
|
2295
|
+
renderAppCommands({
|
|
2296
|
+
groups: [],
|
|
2297
|
+
renderResults: () => (
|
|
2298
|
+
<CommandMenu.Group heading="Dynamic results">{null}</CommandMenu.Group>
|
|
2299
|
+
),
|
|
2300
|
+
emptyState: "No matching records",
|
|
2301
|
+
});
|
|
2302
|
+
|
|
2303
|
+
search("missing");
|
|
2304
|
+
|
|
2305
|
+
expect(document.querySelector('[role="status"]')?.textContent).toContain(
|
|
2306
|
+
"No matching records",
|
|
2307
|
+
);
|
|
2308
|
+
});
|
|
2309
|
+
|
|
2310
|
+
it("uses result presence for opaque components that render no rows", () => {
|
|
2311
|
+
function EmptyResults() {
|
|
2312
|
+
return null;
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2315
|
+
renderAppCommands({
|
|
2316
|
+
groups: [],
|
|
2317
|
+
renderResults: () => <EmptyResults />,
|
|
2318
|
+
renderResultsHasItems: () => false,
|
|
2319
|
+
emptyState: "No matching records",
|
|
2320
|
+
});
|
|
2321
|
+
|
|
2322
|
+
search("missing");
|
|
2323
|
+
|
|
2324
|
+
expect(document.querySelector('[role="status"]')?.textContent).toContain(
|
|
2325
|
+
"No matching records",
|
|
2326
|
+
);
|
|
2327
|
+
});
|
|
2328
|
+
|
|
2329
|
+
it("shows app search suggestions only when a non-empty query has no results", () => {
|
|
2330
|
+
renderAppCommands({
|
|
2331
|
+
groups: [],
|
|
2332
|
+
emptyState: "No matching records",
|
|
2333
|
+
noMatchSuggestionsSlot: (query) => (
|
|
2334
|
+
<button type="button">Search {query} everywhere</button>
|
|
2335
|
+
),
|
|
2336
|
+
});
|
|
2337
|
+
|
|
2338
|
+
search("missing");
|
|
2339
|
+
|
|
2340
|
+
expect(document.querySelector('[role="status"]')?.textContent).toContain(
|
|
2341
|
+
"Search missing everywhere",
|
|
2342
|
+
);
|
|
2343
|
+
});
|
|
2344
|
+
|
|
2345
|
+
it("does not show the empty state beside component-rendered results", () => {
|
|
2346
|
+
function LiveResults() {
|
|
2347
|
+
return (
|
|
2348
|
+
<CommandMenu.Group heading="Live results">
|
|
2349
|
+
<CommandMenu.Item onSelect={() => undefined}>
|
|
2350
|
+
Live result
|
|
2351
|
+
</CommandMenu.Item>
|
|
2352
|
+
</CommandMenu.Group>
|
|
2353
|
+
);
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
renderAppCommands({
|
|
2357
|
+
groups: [],
|
|
2358
|
+
renderResults: () => <LiveResults />,
|
|
2359
|
+
renderResultsHasItems: () => true,
|
|
2360
|
+
emptyState: "No matching records",
|
|
2361
|
+
});
|
|
2362
|
+
|
|
2363
|
+
search("active");
|
|
2364
|
+
|
|
2365
|
+
expect(document.body.textContent).toContain("Live result");
|
|
2366
|
+
expect(document.querySelector('[role="status"]')).toBeNull();
|
|
2367
|
+
});
|
|
2368
|
+
|
|
2369
|
+
it("uses the render-results presence signal for components that render no rows", () => {
|
|
2370
|
+
function EmptyLiveResults() {
|
|
2371
|
+
return null;
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2374
|
+
renderAppCommands({
|
|
2375
|
+
groups: [],
|
|
2376
|
+
renderResults: () => <EmptyLiveResults />,
|
|
2377
|
+
renderResultsHasItems: () => false,
|
|
2378
|
+
emptyState: "No matching records",
|
|
2379
|
+
});
|
|
2380
|
+
|
|
2381
|
+
search("missing");
|
|
2382
|
+
|
|
2383
|
+
expect(document.querySelector('[role="status"]')?.textContent).toContain(
|
|
2384
|
+
"No matching records",
|
|
2385
|
+
);
|
|
2386
|
+
});
|
|
2387
|
+
|
|
2388
|
+
it("uses the explicit list's rows instead of the default result signal", () => {
|
|
2389
|
+
const hasDefaultResults = vi.fn(() => true);
|
|
2390
|
+
|
|
2391
|
+
act(() => {
|
|
2392
|
+
root.render(
|
|
2393
|
+
<CommandMenu
|
|
2394
|
+
open
|
|
2395
|
+
onOpenChange={() => undefined}
|
|
2396
|
+
showAgentFallback={false}
|
|
2397
|
+
renderResults={() => (
|
|
2398
|
+
<CommandMenu.Group heading="Default results">
|
|
2399
|
+
<CommandMenu.Item onSelect={() => undefined}>
|
|
2400
|
+
Default result
|
|
2401
|
+
</CommandMenu.Item>
|
|
2402
|
+
</CommandMenu.Group>
|
|
2403
|
+
)}
|
|
2404
|
+
renderResultsHasItems={hasDefaultResults}
|
|
2405
|
+
renderContent={({ renderList }) => renderList(null)}
|
|
2406
|
+
emptyState="No matching records"
|
|
2407
|
+
>
|
|
2408
|
+
{null}
|
|
2409
|
+
</CommandMenu>,
|
|
2410
|
+
);
|
|
2411
|
+
});
|
|
2412
|
+
|
|
2413
|
+
expect(document.body.textContent).not.toContain("Default result");
|
|
2414
|
+
expect(document.querySelector('[role="status"]')?.textContent).toContain(
|
|
2415
|
+
"No matching records",
|
|
2416
|
+
);
|
|
2417
|
+
expect(hasDefaultResults).not.toHaveBeenCalled();
|
|
2418
|
+
});
|
|
2419
|
+
|
|
2420
|
+
it("does not show the empty state beside custom list rows", () => {
|
|
2421
|
+
const hasDefaultResults = vi.fn(() => false);
|
|
2422
|
+
|
|
2423
|
+
act(() => {
|
|
2424
|
+
root.render(
|
|
2425
|
+
<CommandMenu
|
|
2426
|
+
open
|
|
2427
|
+
onOpenChange={() => undefined}
|
|
2428
|
+
showAgentFallback={false}
|
|
2429
|
+
renderResults={() => null}
|
|
2430
|
+
renderResultsHasItems={hasDefaultResults}
|
|
2431
|
+
renderContent={({ renderList }) =>
|
|
2432
|
+
renderList(
|
|
2433
|
+
<CommandMenu.Group heading="Custom results">
|
|
2434
|
+
<CommandMenu.Item onSelect={() => undefined}>
|
|
2435
|
+
Custom result
|
|
2436
|
+
</CommandMenu.Item>
|
|
2437
|
+
</CommandMenu.Group>,
|
|
2438
|
+
)
|
|
2439
|
+
}
|
|
2440
|
+
emptyState="No matching records"
|
|
2441
|
+
>
|
|
2442
|
+
{null}
|
|
2443
|
+
</CommandMenu>,
|
|
2444
|
+
);
|
|
2445
|
+
});
|
|
2446
|
+
|
|
2447
|
+
expect(document.body.textContent).toContain("Custom result");
|
|
2448
|
+
expect(document.querySelector('[role="status"]')).toBeNull();
|
|
2449
|
+
expect(hasDefaultResults).not.toHaveBeenCalled();
|
|
2450
|
+
});
|
|
2451
|
+
|
|
2452
|
+
it("accepts a presence hint for custom component results", () => {
|
|
2453
|
+
function EmptyCustomResults() {
|
|
2454
|
+
return null;
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
const hasDefaultResults = vi.fn(() => true);
|
|
2458
|
+
|
|
2459
|
+
act(() => {
|
|
2460
|
+
root.render(
|
|
2461
|
+
<CommandMenu
|
|
2462
|
+
open
|
|
2463
|
+
onOpenChange={() => undefined}
|
|
2464
|
+
showAgentFallback={false}
|
|
2465
|
+
renderResults={() => (
|
|
2466
|
+
<CommandMenu.Group heading="Default results">
|
|
2467
|
+
<CommandMenu.Item onSelect={() => undefined}>
|
|
2468
|
+
Default result
|
|
2469
|
+
</CommandMenu.Item>
|
|
2470
|
+
</CommandMenu.Group>
|
|
2471
|
+
)}
|
|
2472
|
+
renderResultsHasItems={hasDefaultResults}
|
|
2473
|
+
renderContent={({ renderList }) =>
|
|
2474
|
+
renderList(<EmptyCustomResults />, { hasSelectableItems: false })
|
|
2475
|
+
}
|
|
2476
|
+
emptyState="No matching records"
|
|
2477
|
+
>
|
|
2478
|
+
{null}
|
|
2479
|
+
</CommandMenu>,
|
|
2480
|
+
);
|
|
2481
|
+
});
|
|
2482
|
+
|
|
2483
|
+
expect(document.body.textContent).not.toContain("Default result");
|
|
2484
|
+
expect(document.querySelector('[role="status"]')?.textContent).toContain(
|
|
2485
|
+
"No matching records",
|
|
2486
|
+
);
|
|
2487
|
+
expect(hasDefaultResults).not.toHaveBeenCalled();
|
|
2488
|
+
});
|
|
2489
|
+
|
|
2490
|
+
it("honors first and none initial selection modes", () => {
|
|
2491
|
+
const groups = [
|
|
2492
|
+
{
|
|
2493
|
+
id: "actions",
|
|
2494
|
+
order: 1,
|
|
2495
|
+
commands: [
|
|
2496
|
+
{ id: "first", order: 1, label: "First", onSelect() {} },
|
|
2497
|
+
{ id: "second", order: 2, label: "Second", onSelect() {} },
|
|
2498
|
+
],
|
|
2499
|
+
},
|
|
2500
|
+
];
|
|
2501
|
+
|
|
2502
|
+
renderAppCommands({ groups, initialSelection: "first" });
|
|
2503
|
+
expect(
|
|
2504
|
+
document.querySelector(
|
|
2505
|
+
'[cmdk-item][data-value="app-command:actions:first"][aria-selected="true"]',
|
|
2506
|
+
),
|
|
2507
|
+
).toBeTruthy();
|
|
2508
|
+
|
|
2509
|
+
renderAppCommands({ groups, initialSelection: "none", open: false });
|
|
2510
|
+
renderAppCommands({ groups, initialSelection: "none" });
|
|
2511
|
+
expect(
|
|
2512
|
+
document.querySelector('[cmdk-item][aria-selected="true"]'),
|
|
2513
|
+
).toBeNull();
|
|
2514
|
+
|
|
2515
|
+
const input = document.querySelector<HTMLInputElement>("[cmdk-input]");
|
|
2516
|
+
expect(input).toBeTruthy();
|
|
2517
|
+
act(() =>
|
|
2518
|
+
input!.dispatchEvent(
|
|
2519
|
+
new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true }),
|
|
2520
|
+
),
|
|
2521
|
+
);
|
|
2522
|
+
expect(
|
|
2523
|
+
document.querySelector('[cmdk-item][aria-selected="true"]'),
|
|
2524
|
+
).toBeTruthy();
|
|
2525
|
+
|
|
2526
|
+
renderAppCommands({ groups, open: false });
|
|
2527
|
+
renderAppCommands({ groups });
|
|
2528
|
+
expect(
|
|
2529
|
+
document.querySelector(
|
|
2530
|
+
'[cmdk-item][data-value="app-command:actions:first"][aria-selected="true"]',
|
|
2531
|
+
),
|
|
2532
|
+
).toBeTruthy();
|
|
2533
|
+
});
|
|
2534
|
+
|
|
2535
|
+
it("keeps typing unselected until keyboard navigation begins", () => {
|
|
2536
|
+
const onSelect = vi.fn();
|
|
2537
|
+
renderAppCommands({
|
|
2538
|
+
groups: [
|
|
2539
|
+
{
|
|
2540
|
+
id: "actions",
|
|
2541
|
+
order: 1,
|
|
2542
|
+
commands: [
|
|
2543
|
+
{
|
|
2544
|
+
id: "match",
|
|
2545
|
+
order: 1,
|
|
2546
|
+
label: "Matching action",
|
|
2547
|
+
deferSelect: false,
|
|
2548
|
+
onSelect,
|
|
2549
|
+
},
|
|
2550
|
+
],
|
|
2551
|
+
},
|
|
2552
|
+
],
|
|
2553
|
+
initialSelection: "none",
|
|
2554
|
+
});
|
|
2555
|
+
|
|
2556
|
+
search("matching");
|
|
2557
|
+
expect(
|
|
2558
|
+
document.querySelector('[cmdk-item][aria-selected="true"]'),
|
|
2559
|
+
).toBeNull();
|
|
2560
|
+
|
|
2561
|
+
const input = document.querySelector<HTMLInputElement>("[cmdk-input]");
|
|
2562
|
+
expect(input).toBeTruthy();
|
|
2563
|
+
act(() =>
|
|
2564
|
+
input!.dispatchEvent(
|
|
2565
|
+
new KeyboardEvent("keydown", {
|
|
2566
|
+
key: "Enter",
|
|
2567
|
+
bubbles: true,
|
|
2568
|
+
cancelable: true,
|
|
2569
|
+
}),
|
|
2570
|
+
),
|
|
2571
|
+
);
|
|
2572
|
+
expect(onSelect).not.toHaveBeenCalled();
|
|
2573
|
+
|
|
2574
|
+
act(() =>
|
|
2575
|
+
input!.dispatchEvent(
|
|
2576
|
+
new KeyboardEvent("keydown", {
|
|
2577
|
+
key: "ArrowDown",
|
|
2578
|
+
bubbles: true,
|
|
2579
|
+
cancelable: true,
|
|
2580
|
+
}),
|
|
2581
|
+
),
|
|
2582
|
+
);
|
|
2583
|
+
expect(
|
|
2584
|
+
document.querySelector('[cmdk-item][aria-selected="true"]'),
|
|
2585
|
+
).toBeTruthy();
|
|
2586
|
+
act(() =>
|
|
2587
|
+
input!.dispatchEvent(
|
|
2588
|
+
new KeyboardEvent("keydown", {
|
|
2589
|
+
key: "Enter",
|
|
2590
|
+
bubbles: true,
|
|
2591
|
+
cancelable: true,
|
|
2592
|
+
}),
|
|
2593
|
+
),
|
|
2594
|
+
);
|
|
2595
|
+
expect(onSelect).toHaveBeenCalledOnce();
|
|
2596
|
+
});
|
|
2597
|
+
|
|
2598
|
+
it("selects the first app result when it arrives after the menu opens", () => {
|
|
2599
|
+
renderAppCommands({
|
|
2600
|
+
groups: [],
|
|
2601
|
+
initialSelection: "first",
|
|
2602
|
+
showSettingsCommand: false,
|
|
2603
|
+
showSignOutCommand: false,
|
|
2604
|
+
});
|
|
2605
|
+
expect(
|
|
2606
|
+
document.querySelector('[cmdk-item][aria-selected="true"]'),
|
|
2607
|
+
).toBeNull();
|
|
2608
|
+
|
|
2609
|
+
renderAppCommands({
|
|
2610
|
+
groups: [
|
|
2611
|
+
{
|
|
2612
|
+
id: "async-results",
|
|
2613
|
+
order: 1,
|
|
2614
|
+
commands: [
|
|
2615
|
+
{
|
|
2616
|
+
id: "first-result",
|
|
2617
|
+
order: 1,
|
|
2618
|
+
label: "First result",
|
|
2619
|
+
onSelect() {},
|
|
2620
|
+
},
|
|
2621
|
+
],
|
|
2622
|
+
},
|
|
2623
|
+
],
|
|
2624
|
+
initialSelection: "first",
|
|
2625
|
+
showSettingsCommand: false,
|
|
2626
|
+
showSignOutCommand: false,
|
|
2627
|
+
});
|
|
2628
|
+
|
|
2629
|
+
expect(
|
|
2630
|
+
document.querySelector(
|
|
2631
|
+
'[cmdk-item][data-value="app-command:async-results:first-result"][aria-selected="true"]',
|
|
2632
|
+
),
|
|
2633
|
+
).toBeTruthy();
|
|
2634
|
+
});
|
|
2635
|
+
|
|
2636
|
+
it("allows pointer selection when initialSelection is none", () => {
|
|
2637
|
+
renderAppCommands({
|
|
2638
|
+
groups: [
|
|
2639
|
+
{
|
|
2640
|
+
id: "actions",
|
|
2641
|
+
order: 1,
|
|
2642
|
+
commands: [{ id: "first", order: 1, label: "First", onSelect() {} }],
|
|
2643
|
+
},
|
|
2644
|
+
],
|
|
2645
|
+
initialSelection: "none",
|
|
2646
|
+
});
|
|
2647
|
+
|
|
2648
|
+
const row = document.querySelector<HTMLElement>(
|
|
2649
|
+
'[cmdk-item][data-value="app-command:actions:first"]',
|
|
2650
|
+
);
|
|
2651
|
+
expect(row).toBeTruthy();
|
|
2652
|
+
act(() => {
|
|
2653
|
+
row!.dispatchEvent(new Event("pointermove", { bubbles: true }));
|
|
2654
|
+
});
|
|
2655
|
+
|
|
2656
|
+
expect(row?.getAttribute("aria-selected")).toBe("true");
|
|
2657
|
+
});
|
|
2658
|
+
|
|
2659
|
+
it("allows direct pointer activation when initialSelection is none", () => {
|
|
2660
|
+
const onSelect = vi.fn();
|
|
2661
|
+
renderAppCommands({
|
|
2662
|
+
groups: [
|
|
2663
|
+
{
|
|
2664
|
+
id: "actions",
|
|
2665
|
+
order: 1,
|
|
2666
|
+
commands: [
|
|
2667
|
+
{
|
|
2668
|
+
id: "first",
|
|
2669
|
+
order: 1,
|
|
2670
|
+
label: "First",
|
|
2671
|
+
deferSelect: false,
|
|
2672
|
+
onSelect,
|
|
2673
|
+
},
|
|
2674
|
+
],
|
|
2675
|
+
},
|
|
2676
|
+
],
|
|
2677
|
+
initialSelection: "none",
|
|
2678
|
+
});
|
|
2679
|
+
|
|
2680
|
+
const row = document.querySelector<HTMLElement>(
|
|
2681
|
+
'[cmdk-item][data-value="app-command:actions:first"]',
|
|
2682
|
+
);
|
|
2683
|
+
expect(row).toBeTruthy();
|
|
2684
|
+
act(() => {
|
|
2685
|
+
row!.dispatchEvent(new Event("pointerdown", { bubbles: true }));
|
|
2686
|
+
row!.click();
|
|
2687
|
+
});
|
|
2688
|
+
|
|
2689
|
+
expect(onSelect).toHaveBeenCalledOnce();
|
|
2690
|
+
});
|
|
2691
|
+
|
|
2692
|
+
it("portals the menu into an app-provided container", () => {
|
|
2693
|
+
const portalContainer = document.createElement("div");
|
|
2694
|
+
portalContainer.dataset.testid = "command-menu-portal";
|
|
2695
|
+
document.body.appendChild(portalContainer);
|
|
2696
|
+
|
|
2697
|
+
renderAppCommands({ groups: [], container: portalContainer });
|
|
2698
|
+
|
|
2699
|
+
expect(portalContainer.querySelector('[role="dialog"]')).toBeTruthy();
|
|
2700
|
+
expect(document.body.querySelector('[role="dialog"]')).toBe(
|
|
2701
|
+
portalContainer.querySelector('[role="dialog"]'),
|
|
2702
|
+
);
|
|
2703
|
+
});
|
|
1043
2704
|
});
|