22 * @vitest -environment jsdom
33 */
44import { act } from 'react'
5+ import { NuqsTestingAdapter , type UrlUpdateEvent } from 'nuqs/adapters/testing'
56import { createRoot , type Root } from 'react-dom/client'
67import { afterEach , beforeEach , describe , expect , it , vi } from 'vitest'
78
8- const { mockCaptureEvent, mockSetSearchQuery, mockSetSearchFilters, modeState } = vi . hoisted (
9- ( ) => ( {
10- mockCaptureEvent : vi . fn ( ) ,
11- mockSetSearchQuery : vi . fn ( ) ,
12- mockSetSearchFilters : vi . fn ( ) ,
13- /** The URL `mode` param as the nuqs mock serves it; `set` is the live setter once mounted. */
14- modeState : { initial : 'build' , set : ( _next : string ) => { } } ,
15- } )
16- )
9+ const { mockCaptureEvent, mockLeaveSearch } = vi . hoisted ( ( ) => ( {
10+ mockCaptureEvent : vi . fn ( ) ,
11+ mockLeaveSearch : vi . fn ( ) ,
12+ } ) )
13+ const mockUrlUpdate = vi . fn < ( event : UrlUpdateEvent ) => void > ( )
1714
1815vi . mock ( 'next/navigation' , ( ) => ( {
1916 useParams : ( ) => ( { workspaceId : 'workspace-1' } ) ,
2017} ) )
21- vi . mock ( 'nuqs' , async ( ) => {
22- const { useState } = await import ( 'react' )
23- return {
24- useQueryState : ( key : string ) => {
25- const [ mode , setMode ] = useState ( modeState . initial )
26- if ( key !== 'mode' ) return [ null , mockSetSearchQuery ]
27- modeState . set = setMode
28- return [ mode , setMode ]
29- } ,
30- useQueryStates : ( ) => [ { } , mockSetSearchFilters ] ,
31- }
32- } )
3318vi . mock ( 'posthog-js/react' , ( ) => ( { usePostHog : ( ) => null } ) )
3419vi . mock ( '@/lib/posthog/client' , ( ) => ( { captureEvent : mockCaptureEvent } ) )
3520
@@ -38,12 +23,18 @@ import { ModeSwitcher } from '@/app/workspace/[workspaceId]/home/components/user
3823let root : Root | null = null
3924let container : HTMLDivElement | null = null
4025
41- function mount ( ) {
26+ function mount ( searchParams = '' ) {
4227 ; ( globalThis as { IS_REACT_ACT_ENVIRONMENT ?: boolean } ) . IS_REACT_ACT_ENVIRONMENT = true
4328 container = document . createElement ( 'div' )
4429 document . body . appendChild ( container )
4530 root = createRoot ( container )
46- act ( ( ) => root ?. render ( < ModeSwitcher /> ) )
31+ act ( ( ) =>
32+ root ?. render (
33+ < NuqsTestingAdapter hasMemory searchParams = { searchParams } onUrlUpdate = { mockUrlUpdate } >
34+ < ModeSwitcher onLeaveSearch = { mockLeaveSearch } />
35+ </ NuqsTestingAdapter >
36+ )
37+ )
4738}
4839
4940function trigger ( ) : HTMLButtonElement {
@@ -63,24 +54,26 @@ function items(): HTMLElement[] {
6354 return Array . from ( document . querySelectorAll < HTMLElement > ( '[role="menuitem"]' ) )
6455}
6556
66- function select ( index : number ) {
67- act ( ( ) => {
57+ async function select ( index : number ) {
58+ await act ( async ( ) => {
6859 items ( ) [ index ] . dispatchEvent ( new MouseEvent ( 'click' , { bubbles : true , button : 0 } ) )
60+ await vi . advanceTimersByTimeAsync ( 1 )
6961 } )
7062}
7163
7264beforeEach ( ( ) => {
65+ vi . useFakeTimers ( { toFake : [ 'setTimeout' , 'clearTimeout' ] } )
7366 mockCaptureEvent . mockClear ( )
74- mockSetSearchQuery . mockClear ( )
75- mockSetSearchFilters . mockClear ( )
76- modeState . initial = 'build'
67+ mockLeaveSearch . mockClear ( )
68+ mockUrlUpdate . mockClear ( )
7769} )
7870
7971afterEach ( ( ) => {
8072 if ( root ) act ( ( ) => root ?. unmount ( ) )
8173 container ?. remove ( )
8274 root = null
8375 container = null
76+ vi . useRealTimers ( )
8477} )
8578
8679describe ( 'ModeSwitcher' , ( ) => {
@@ -108,47 +101,52 @@ describe('ModeSwitcher', () => {
108101 expect ( rows [ 2 ] . querySelector ( 'svg' ) ) . toBeNull ( )
109102 } )
110103
111- it ( 'writes the chosen mode to the URL and reports the change' , ( ) => {
104+ it ( 'writes the chosen mode to the URL and reports the change' , async ( ) => {
112105 mount ( )
113106 openMenu ( )
114- select ( 1 )
107+ await select ( 1 )
115108
116109 expect ( trigger ( ) . textContent ) . toBe ( 'Search' )
117110 expect ( mockCaptureEvent ) . toHaveBeenCalledWith ( null , 'chat_mode_changed' , {
118111 workspace_id : 'workspace-1' ,
119112 mode : 'search' ,
120113 } )
121- expect ( mockSetSearchQuery ) . not . toHaveBeenCalled ( )
114+ expect ( mockUrlUpdate . mock . lastCall ?. [ 0 ] . searchParams . get ( 'mode' ) ) . toBe ( 'search' )
115+ expect ( mockLeaveSearch ) . not . toHaveBeenCalled ( )
122116 } )
123117
124118 it ( 'reads the mode from the URL on mount' , ( ) => {
125- modeState . initial = 'assistant'
126- mount ( )
119+ mount ( '?mode=assistant' )
127120
128121 expect ( trigger ( ) . textContent ) . toBe ( 'Assistant' )
129122 expect ( trigger ( ) . getAttribute ( 'aria-label' ) ) . toBe ( 'Mode: Assistant' )
130123 } )
131124
132- it ( 'drops the search query from the URL when leaving Search' , ( ) => {
133- modeState . initial = 'search'
134- mount ( )
125+ it ( 'clears the composer and search parameters together when leaving Search' , async ( ) => {
126+ mount ( '?mode=search&q=budget&source=upload&updated=7d&resource=report' )
135127 openMenu ( )
136- select ( 0 )
128+ await select ( 0 )
137129
138130 expect ( trigger ( ) . textContent ) . toBe ( 'Build' )
139- expect ( mockSetSearchQuery ) . toHaveBeenCalledWith ( null , { history : 'replace' , scroll : false } )
140- expect ( mockSetSearchFilters ) . toHaveBeenCalledWith (
141- { source : null , updated : null } ,
142- { history : 'replace' , scroll : false }
131+ expect ( mockLeaveSearch ) . toHaveBeenCalledOnce ( )
132+ expect ( mockUrlUpdate ) . toHaveBeenCalledOnce ( )
133+ expect ( mockUrlUpdate . mock . lastCall ?. [ 0 ] . searchParams . toString ( ) ) . toBe ( 'resource=report' )
134+ expect ( mockUrlUpdate . mock . lastCall ?. [ 0 ] . options ) . toMatchObject ( {
135+ history : 'replace' ,
136+ scroll : false ,
137+ } )
138+ expect ( mockLeaveSearch . mock . invocationCallOrder [ 0 ] ) . toBeLessThan (
139+ mockUrlUpdate . mock . invocationCallOrder [ 0 ]
143140 )
144141 } )
145142
146- it ( 'does not report re-selecting the active mode' , ( ) => {
143+ it ( 'does not report re-selecting the active mode' , async ( ) => {
147144 mount ( )
148145 openMenu ( )
149- select ( 0 )
146+ await select ( 0 )
150147
151148 expect ( trigger ( ) . textContent ) . toBe ( 'Build' )
152149 expect ( mockCaptureEvent ) . not . toHaveBeenCalled ( )
150+ expect ( mockLeaveSearch ) . not . toHaveBeenCalled ( )
153151 } )
154152} )
0 commit comments