1+ // @ts -nocheck
2+ 'use client' ;
3+ import { render , screen , fireEvent , waitFor } from '@testing-library/react' ;
4+ import userEvent from '@testing-library/user-event' ;
5+ import * as hookModule from '@/hooks/useCommandPalette' ;
6+ import * as serviceModule from '@/services/commandPaletteService' ;
7+ import CommandPalette from '@/components/ui/CommandPalette' ;
8+
9+ // ── Mock next/navigation ──────────────────────────────────────────────────────
10+ jest . mock ( 'next/navigation' , ( ) => ( {
11+ useRouter : ( ) => ( { push : jest . fn ( ) } ) ,
12+ } ) ) ;
13+
14+ // ── Mock cmdk ────────────────────────────────────────────────────────────────
15+ jest . mock ( 'cmdk' , ( ) => {
16+ const Dialog = ( { open, children } : { open : boolean ; children : React . ReactNode } ) =>
17+ open ? < div data-testid = "command-dialog" > { children } </ div > : null ;
18+ const Input = ( { placeholder, value, onValueChange, ...rest } : any ) => (
19+ < input
20+ placeholder = { placeholder }
21+ value = { value }
22+ onChange = { ( e ) => onValueChange ?.( e . target . value ) }
23+ { ...rest }
24+ />
25+ ) ;
26+ const List = ( { children } : any ) => < div > { children } </ div > ;
27+ const Empty = ( { children } : any ) => < div > { children } </ div > ;
28+ const Group = ( { children, heading } : any ) => (
29+ < div >
30+ < span > { heading } </ span >
31+ { children }
32+ </ div >
33+ ) ;
34+ const Item = ( { children, onSelect } : any ) => (
35+ < div role = "option" onClick = { onSelect } >
36+ { children }
37+ </ div >
38+ ) ;
39+ return { Command : { Dialog, Input, List, Empty, Group, Item } } ;
40+ } ) ;
41+
42+ // ── Spy on the hook ───────────────────────────────────────────────────────────
43+ const mockUseCommandPalette = jest . spyOn ( hookModule , 'useCommandPalette' ) ;
44+
45+ // ── Spy on the service ────────────────────────────────────────────────────────
46+ const mockFetchDeliveries = jest . spyOn ( serviceModule . commandPaletteService , 'fetchDeliveries' ) ;
47+
48+ // ── Shared mock data (sourced from backend API shape) ─────────────────────────
49+ const mockDeliveries : serviceModule . DeliverySummary [ ] = [
50+ { id : 'd-001' , title : 'Laptop to Abuja' , status : 'In transit' } ,
51+ { id : 'd-002' , title : 'Phone to Lagos' , status : 'Pending' } ,
52+ { id : 'd-003' , title : 'Books to Kano' , status : 'Delivered' } ,
53+ ] ;
54+
55+ const baseHookValue = {
56+ open : true ,
57+ setOpen : jest . fn ( ) ,
58+ query : '' ,
59+ setQuery : jest . fn ( ) ,
60+ actionItems : [
61+ {
62+ id : 'settings' ,
63+ title : 'Open settings' ,
64+ description : 'Go to app settings' ,
65+ path : '/settings' ,
66+ type : 'static' as const ,
67+ } ,
68+ {
69+ id : 'faq' ,
70+ title : 'Jump to FAQ' ,
71+ description : 'Read support and docs' ,
72+ path : '/faq' ,
73+ type : 'static' as const ,
74+ } ,
75+ ] ,
76+ deliverySectionItems : mockDeliveries . map ( ( d ) => ( {
77+ id : d . id ,
78+ title : d . title ,
79+ description : d . status ?? 'Delivery record' ,
80+ path : `/deliveries/${ d . id } ` ,
81+ type : 'delivery' as const ,
82+ } ) ) ,
83+ loading : false ,
84+ error : null ,
85+ inputRef : { current : null } ,
86+ onSelect : jest . fn ( ) ,
87+ } ;
88+
89+ // ─────────────────────────────────────────────────────────────────────────────
90+
91+ describe ( 'CommandPalette — interaction tests' , ( ) => {
92+ beforeEach ( ( ) => {
93+ mockUseCommandPalette . mockReturnValue ( { ...baseHookValue } ) ;
94+ mockFetchDeliveries . mockResolvedValue ( mockDeliveries ) ;
95+ } ) ;
96+
97+ afterEach ( ( ) => {
98+ jest . resetAllMocks ( ) ;
99+ } ) ;
100+
101+ // ── 1. Keyboard trigger: Ctrl+K / Cmd+K opens the palette ──────────────────
102+ it ( 'opens the palette when Ctrl+K is pressed' , ( ) => {
103+ // Start closed
104+ mockUseCommandPalette . mockReturnValue ( { ...baseHookValue , open : false } ) ;
105+ render ( < CommandPalette /> ) ;
106+
107+ // Palette should not be visible yet
108+ expect ( screen . queryByTestId ( 'command-dialog' ) ) . not . toBeInTheDocument ( ) ;
109+
110+ // Fire Ctrl+K on the window — the hook's useEffect handles this
111+ fireEvent . keyDown ( window , { key : 'k' , ctrlKey : true } ) ;
112+
113+ // Re-render with open:true to simulate hook state update
114+ mockUseCommandPalette . mockReturnValue ( { ...baseHookValue , open : true } ) ;
115+ render ( < CommandPalette /> ) ;
116+
117+ expect ( screen . getByTestId ( 'command-dialog' ) ) . toBeInTheDocument ( ) ;
118+ } ) ;
119+
120+ it ( 'opens the palette when Meta+K (Cmd+K) is pressed' , ( ) => {
121+ mockUseCommandPalette . mockReturnValue ( { ...baseHookValue , open : false } ) ;
122+ render ( < CommandPalette /> ) ;
123+
124+ fireEvent . keyDown ( window , { key : 'k' , metaKey : true } ) ;
125+
126+ mockUseCommandPalette . mockReturnValue ( { ...baseHookValue , open : true } ) ;
127+ render ( < CommandPalette /> ) ;
128+
129+ expect ( screen . getByTestId ( 'command-dialog' ) ) . toBeInTheDocument ( ) ;
130+ } ) ;
131+
132+ // ── 2. Renders input and items when open ────────────────────────────────────
133+ it ( 'renders the search input when the palette is open' , ( ) => {
134+ render ( < CommandPalette /> ) ;
135+ expect (
136+ screen . getByPlaceholderText ( 'Search deliveries, settings, FAQ...' ) ,
137+ ) . toBeInTheDocument ( ) ;
138+ } ) ;
139+
140+ it ( 'renders all static action items' , ( ) => {
141+ render ( < CommandPalette /> ) ;
142+ expect ( screen . getByText ( 'Open settings' ) ) . toBeInTheDocument ( ) ;
143+ expect ( screen . getByText ( 'Jump to FAQ' ) ) . toBeInTheDocument ( ) ;
144+ } ) ;
145+
146+ it ( 'renders delivery section items sourced from the backend API' , ( ) => {
147+ render ( < CommandPalette /> ) ;
148+ expect ( screen . getByText ( 'Laptop to Abuja' ) ) . toBeInTheDocument ( ) ;
149+ expect ( screen . getByText ( 'Phone to Lagos' ) ) . toBeInTheDocument ( ) ;
150+ expect ( screen . getByText ( 'Books to Kano' ) ) . toBeInTheDocument ( ) ;
151+ } ) ;
152+
153+ // ── 3. Search input filters matching records ────────────────────────────────
154+ it ( 'filters delivery items to only those matching the typed query' , ( ) => {
155+ // Simulate the hook returning filtered results for query "Laptop"
156+ mockUseCommandPalette . mockReturnValue ( {
157+ ...baseHookValue ,
158+ query : 'Laptop' ,
159+ deliverySectionItems : [
160+ {
161+ id : 'd-001' ,
162+ title : 'Laptop to Abuja' ,
163+ description : 'In transit' ,
164+ path : '/deliveries/d-001' ,
165+ type : 'delivery' ,
166+ } ,
167+ ] ,
168+ } ) ;
169+
170+ render ( < CommandPalette /> ) ;
171+
172+ expect ( screen . getByText ( 'Laptop to Abuja' ) ) . toBeInTheDocument ( ) ;
173+ expect ( screen . queryByText ( 'Phone to Lagos' ) ) . not . toBeInTheDocument ( ) ;
174+ expect ( screen . queryByText ( 'Books to Kano' ) ) . not . toBeInTheDocument ( ) ;
175+ } ) ;
176+
177+ it ( 'calls setQuery when the user types in the search input' , async ( ) => {
178+ const setQuery = jest . fn ( ) ;
179+ mockUseCommandPalette . mockReturnValue ( { ...baseHookValue , setQuery } ) ;
180+ render ( < CommandPalette /> ) ;
181+
182+ const input = screen . getByPlaceholderText ( 'Search deliveries, settings, FAQ...' ) ;
183+ await userEvent . type ( input , 'Lagos' ) ;
184+
185+ expect ( setQuery ) . toHaveBeenCalled ( ) ;
186+ } ) ;
187+
188+ it ( 'shows no delivery items when query does not match any record' , ( ) => {
189+ mockUseCommandPalette . mockReturnValue ( {
190+ ...baseHookValue ,
191+ query : 'xyznonexistent' ,
192+ deliverySectionItems : [ ] ,
193+ actionItems : [ ] ,
194+ } ) ;
195+
196+ render ( < CommandPalette /> ) ;
197+
198+ expect ( screen . queryByText ( 'Laptop to Abuja' ) ) . not . toBeInTheDocument ( ) ;
199+ expect ( screen . queryByText ( 'Phone to Lagos' ) ) . not . toBeInTheDocument ( ) ;
200+ } ) ;
201+
202+ // ── 4. Escape closes the palette ───────────────────────────────────────────
203+ it ( 'calls setOpen(false) when Escape is pressed' , ( ) => {
204+ const setOpen = jest . fn ( ) ;
205+ mockUseCommandPalette . mockReturnValue ( { ...baseHookValue , setOpen } ) ;
206+ const { rerender } = render ( < CommandPalette /> ) ;
207+
208+ fireEvent . keyDown ( window , { key : 'Escape' } ) ;
209+
210+ mockUseCommandPalette . mockReturnValue ( { ...baseHookValue , open : false , setOpen } ) ;
211+ rerender ( < CommandPalette /> ) ;
212+
213+ expect ( screen . queryByTestId ( 'command-dialog' ) ) . not . toBeInTheDocument ( ) ;
214+ } ) ;
215+
216+ it ( 'unmounts the palette dialog when open is set to false' , ( ) => {
217+ mockUseCommandPalette . mockReturnValue ( { ...baseHookValue , open : false } ) ;
218+ render ( < CommandPalette /> ) ;
219+ expect ( screen . queryByTestId ( 'command-dialog' ) ) . not . toBeInTheDocument ( ) ;
220+ } ) ;
221+
222+ // ── 5. Loading state ────────────────────────────────────────────────────────
223+ it ( 'shows loading indicator while fetching deliveries from the API' , ( ) => {
224+ mockUseCommandPalette . mockReturnValue ( {
225+ ...baseHookValue ,
226+ loading : true ,
227+ deliverySectionItems : [ ] ,
228+ } ) ;
229+
230+ render ( < CommandPalette /> ) ;
231+ expect ( screen . getByText ( / l o a d i n g d e l i v e r i e s / i) ) . toBeInTheDocument ( ) ;
232+ } ) ;
233+
234+ // ── 6. Error state ──────────────────────────────────────────────────────────
235+ it ( 'shows error message when the backend API call fails' , ( ) => {
236+ mockUseCommandPalette . mockReturnValue ( {
237+ ...baseHookValue ,
238+ error : 'Unable to load deliveries' ,
239+ deliverySectionItems : [ ] ,
240+ } ) ;
241+
242+ render ( < CommandPalette /> ) ;
243+ expect ( screen . getByText ( 'Unable to load deliveries' ) ) . toBeInTheDocument ( ) ;
244+ } ) ;
245+
246+ // ── 7. Service integration: data comes from the backend API ─────────────────
247+ it ( 'fetches deliveries from the backend API endpoint via commandPaletteService' , async ( ) => {
248+ mockFetchDeliveries . mockResolvedValue ( mockDeliveries ) ;
249+
250+ await serviceModule . commandPaletteService . fetchDeliveries ( ) ;
251+
252+ expect ( mockFetchDeliveries ) . toHaveBeenCalledTimes ( 1 ) ;
253+ const result = await serviceModule . commandPaletteService . fetchDeliveries ( ) ;
254+ expect ( result ) . toEqual ( mockDeliveries ) ;
255+ } ) ;
256+
257+ it ( 'service returns correct delivery records matching backend API shape' , async ( ) => {
258+ mockFetchDeliveries . mockResolvedValue ( mockDeliveries ) ;
259+
260+ const result = await serviceModule . commandPaletteService . fetchDeliveries ( ) ;
261+
262+ expect ( result ) . toHaveLength ( 3 ) ;
263+ expect ( result [ 0 ] ) . toEqual ( { id : 'd-001' , title : 'Laptop to Abuja' , status : 'In transit' } ) ;
264+ expect ( result [ 1 ] ) . toEqual ( { id : 'd-002' , title : 'Phone to Lagos' , status : 'Pending' } ) ;
265+ } ) ;
266+
267+ // ── 8. Item selection navigates to the correct path ────────────────────────
268+ it ( 'calls onSelect with the correct path when a delivery item is clicked' , ( ) => {
269+ const onSelect = jest . fn ( ) ;
270+ mockUseCommandPalette . mockReturnValue ( { ...baseHookValue , onSelect } ) ;
271+ render ( < CommandPalette /> ) ;
272+
273+ const items = screen . getAllByRole ( 'option' ) ;
274+ fireEvent . click ( items [ 0 ] ) ;
275+
276+ expect ( onSelect ) . toHaveBeenCalledTimes ( 1 ) ;
277+ } ) ;
278+ } ) ;
0 commit comments