Skip to content

Commit f8f60a0

Browse files
authored
Merge pull request #476 from objectstack-ai/copilot/complete-roadmap-development-tasks-another-one
2 parents 7e2da72 + 4c41213 commit f8f60a0

26 files changed

+6573
-3
lines changed
Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
/**
2+
* CommandPalette Tests
3+
*
4+
* Tests that the command palette searches across all entity types:
5+
* objects, dashboards, pages, and reports.
6+
*/
7+
8+
import { describe, it, expect, vi, beforeEach } from 'vitest';
9+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
10+
import '@testing-library/jest-dom';
11+
import { CommandPalette } from '../components/CommandPalette';
12+
import { MemoryRouter, Route, Routes } from 'react-router-dom';
13+
14+
// Mock @object-ui/components Command primitives
15+
vi.mock('@object-ui/components', () => ({
16+
CommandDialog: ({ open, children }: any) =>
17+
open ? <div data-testid="command-dialog">{children}</div> : null,
18+
CommandInput: ({ placeholder }: any) => (
19+
<input data-testid="command-input" placeholder={placeholder} />
20+
),
21+
CommandList: ({ children }: any) => <div data-testid="command-list">{children}</div>,
22+
CommandEmpty: ({ children }: any) => <div data-testid="command-empty">{children}</div>,
23+
CommandGroup: ({ heading, children }: any) => (
24+
<div data-testid={`command-group-${heading}`} data-heading={heading}>
25+
<span>{heading}</span>
26+
{children}
27+
</div>
28+
),
29+
CommandItem: ({ children, onSelect, value }: any) => (
30+
<div data-testid={`command-item-${value}`} data-value={value} onClick={onSelect} role="option">
31+
{children}
32+
</div>
33+
),
34+
CommandSeparator: () => <hr data-testid="command-separator" />,
35+
}));
36+
37+
// Mock Lucide icons - use importOriginal to handle the dynamic `import * as LucideIcons`
38+
vi.mock('lucide-react', async (importOriginal) => {
39+
const actual = await importOriginal<any>();
40+
return {
41+
...actual,
42+
};
43+
});
44+
45+
// Mock theme provider
46+
vi.mock('../components/theme-provider', () => ({
47+
useTheme: () => ({ theme: 'light', setTheme: vi.fn() }),
48+
}));
49+
50+
// Mock expression provider
51+
vi.mock('../context/ExpressionProvider', () => ({
52+
useExpressionContext: () => ({ evaluator: {} }),
53+
evaluateVisibility: () => true,
54+
}));
55+
56+
// Mock i18n
57+
vi.mock('@object-ui/i18n', () => ({
58+
useObjectTranslation: () => ({
59+
t: (key: string) => {
60+
const map: Record<string, string> = {
61+
'console.commandPalette.placeholder': 'Type a command or search...',
62+
'console.commandPalette.noResults': 'No results found.',
63+
'console.commandPalette.objects': 'Objects',
64+
'console.commandPalette.dashboards': 'Dashboards',
65+
'console.commandPalette.pages': 'Pages',
66+
'console.commandPalette.reports': 'Reports',
67+
'console.commandPalette.switchApp': 'Switch App',
68+
'console.commandPalette.preferences': 'Preferences',
69+
'console.commandPalette.lightTheme': 'Light',
70+
'console.commandPalette.darkTheme': 'Dark',
71+
'console.commandPalette.systemTheme': 'System',
72+
'console.commandPalette.current': '(current)',
73+
'console.commandPalette.actions': 'Actions',
74+
'console.commandPalette.openFullSearch': 'Open full search',
75+
};
76+
return map[key] ?? key;
77+
},
78+
language: 'en',
79+
direction: 'ltr',
80+
}),
81+
}));
82+
83+
// Fixtures
84+
const navigation = [
85+
{ id: 'nav-contact', label: 'Contacts', type: 'object', objectName: 'contact', icon: 'Users' },
86+
{ id: 'nav-deal', label: 'Deals', type: 'object', objectName: 'deal', icon: 'Briefcase' },
87+
{
88+
id: 'nav-sales-dashboard',
89+
label: 'Sales Dashboard',
90+
type: 'dashboard',
91+
dashboardName: 'sales_overview',
92+
},
93+
{
94+
id: 'nav-help-page',
95+
label: 'Help Center',
96+
type: 'page',
97+
pageName: 'help',
98+
},
99+
{
100+
id: 'nav-monthly-report',
101+
label: 'Monthly Report',
102+
type: 'report',
103+
reportName: 'monthly',
104+
},
105+
];
106+
107+
const apps = [
108+
{ name: 'crm', label: 'CRM', active: true, navigation },
109+
];
110+
111+
const activeApp = apps[0];
112+
113+
function renderPalette(overrideProps: Partial<React.ComponentProps<typeof CommandPalette>> = {}) {
114+
return render(
115+
<MemoryRouter initialEntries={['/apps/crm']}>
116+
<Routes>
117+
<Route
118+
path="/apps/:appName"
119+
element={
120+
<CommandPalette
121+
apps={apps}
122+
activeApp={activeApp}
123+
objects={[]}
124+
onAppChange={vi.fn()}
125+
{...overrideProps}
126+
/>
127+
}
128+
/>
129+
</Routes>
130+
</MemoryRouter>,
131+
);
132+
}
133+
134+
describe('CommandPalette', () => {
135+
beforeEach(() => {
136+
vi.clearAllMocks();
137+
});
138+
139+
it('opens with ⌘+K keyboard shortcut', () => {
140+
renderPalette();
141+
142+
// Dialog should be closed initially
143+
expect(screen.queryByTestId('command-dialog')).not.toBeInTheDocument();
144+
145+
// Trigger ⌘+K
146+
fireEvent.keyDown(document, { key: 'k', metaKey: true });
147+
148+
expect(screen.getByTestId('command-dialog')).toBeInTheDocument();
149+
});
150+
151+
it('opens with Ctrl+K keyboard shortcut', () => {
152+
renderPalette();
153+
154+
fireEvent.keyDown(document, { key: 'k', ctrlKey: true });
155+
156+
expect(screen.getByTestId('command-dialog')).toBeInTheDocument();
157+
});
158+
159+
it('toggles closed on second ⌘+K', () => {
160+
renderPalette();
161+
162+
fireEvent.keyDown(document, { key: 'k', metaKey: true });
163+
expect(screen.getByTestId('command-dialog')).toBeInTheDocument();
164+
165+
fireEvent.keyDown(document, { key: 'k', metaKey: true });
166+
expect(screen.queryByTestId('command-dialog')).not.toBeInTheDocument();
167+
});
168+
169+
describe('Entity type groups', () => {
170+
beforeEach(() => {
171+
renderPalette();
172+
fireEvent.keyDown(document, { key: 'k', metaKey: true });
173+
});
174+
175+
it('displays Objects group with object navigation items', () => {
176+
expect(screen.getByTestId('command-group-Objects')).toBeInTheDocument();
177+
expect(screen.getByText('Contacts')).toBeInTheDocument();
178+
expect(screen.getByText('Deals')).toBeInTheDocument();
179+
});
180+
181+
it('displays Dashboards group', () => {
182+
expect(screen.getByTestId('command-group-Dashboards')).toBeInTheDocument();
183+
expect(screen.getByText('Sales Dashboard')).toBeInTheDocument();
184+
});
185+
186+
it('displays Pages group', () => {
187+
expect(screen.getByTestId('command-group-Pages')).toBeInTheDocument();
188+
expect(screen.getByText('Help Center')).toBeInTheDocument();
189+
});
190+
191+
it('displays Reports group', () => {
192+
expect(screen.getByTestId('command-group-Reports')).toBeInTheDocument();
193+
expect(screen.getByText('Monthly Report')).toBeInTheDocument();
194+
});
195+
});
196+
197+
describe('Theme preferences', () => {
198+
it('shows light, dark, and system theme options', () => {
199+
renderPalette();
200+
fireEvent.keyDown(document, { key: 'k', metaKey: true });
201+
202+
expect(screen.getByTestId('command-group-Preferences')).toBeInTheDocument();
203+
expect(screen.getByText('Light')).toBeInTheDocument();
204+
expect(screen.getByText('Dark')).toBeInTheDocument();
205+
expect(screen.getByText('System')).toBeInTheDocument();
206+
});
207+
});
208+
209+
describe('App switching', () => {
210+
it('shows app switching section when multiple apps exist', () => {
211+
const multiApps = [
212+
{ name: 'crm', label: 'CRM', active: true, navigation },
213+
{ name: 'hr', label: 'HR', active: true, navigation: [] },
214+
];
215+
216+
renderPalette({ apps: multiApps });
217+
fireEvent.keyDown(document, { key: 'k', metaKey: true });
218+
219+
expect(screen.getByTestId('command-group-Switch App')).toBeInTheDocument();
220+
expect(screen.getByText('CRM')).toBeInTheDocument();
221+
expect(screen.getByText('HR')).toBeInTheDocument();
222+
});
223+
224+
it('does not show app switching when only one app exists', () => {
225+
renderPalette();
226+
fireEvent.keyDown(document, { key: 'k', metaKey: true });
227+
228+
expect(screen.queryByTestId('command-group-Switch App')).not.toBeInTheDocument();
229+
});
230+
});
231+
232+
describe('Search action', () => {
233+
it('shows full search action', () => {
234+
renderPalette();
235+
fireEvent.keyDown(document, { key: 'k', metaKey: true });
236+
237+
expect(screen.getByTestId('command-group-Actions')).toBeInTheDocument();
238+
expect(screen.getByText('Open full search')).toBeInTheDocument();
239+
});
240+
});
241+
242+
describe('Nested navigation', () => {
243+
it('flattens grouped navigation items', () => {
244+
const groupedNav = [
245+
{
246+
id: 'group1',
247+
type: 'group',
248+
label: 'Sales',
249+
children: [
250+
{ id: 'nav-lead', label: 'Leads', type: 'object', objectName: 'lead' },
251+
],
252+
},
253+
{ id: 'nav-activity-dash', label: 'Activity', type: 'dashboard', dashboardName: 'activity' },
254+
];
255+
256+
const app = { name: 'crm', label: 'CRM', active: true, navigation: groupedNav };
257+
258+
renderPalette({ apps: [app], activeApp: app });
259+
fireEvent.keyDown(document, { key: 'k', metaKey: true });
260+
261+
expect(screen.getByText('Leads')).toBeInTheDocument();
262+
expect(screen.getByText('Activity')).toBeInTheDocument();
263+
});
264+
});
265+
266+
describe('Empty navigation', () => {
267+
it('does not render entity groups when navigation is empty', () => {
268+
const emptyApp = { name: 'empty', label: 'Empty', active: true, navigation: [] };
269+
270+
renderPalette({ apps: [emptyApp], activeApp: emptyApp });
271+
fireEvent.keyDown(document, { key: 'k', metaKey: true });
272+
273+
expect(screen.queryByTestId('command-group-Objects')).not.toBeInTheDocument();
274+
expect(screen.queryByTestId('command-group-Dashboards')).not.toBeInTheDocument();
275+
expect(screen.queryByTestId('command-group-Pages')).not.toBeInTheDocument();
276+
expect(screen.queryByTestId('command-group-Reports')).not.toBeInTheDocument();
277+
// Preferences and actions should still show
278+
expect(screen.getByTestId('command-group-Preferences')).toBeInTheDocument();
279+
});
280+
});
281+
});

0 commit comments

Comments
 (0)