@@ -8,6 +8,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
88import type { OrganizationChat } from '@/app/o/[organizationId]/components/organization-sidebar/hooks'
99
1010const hoverState = vi . hoisted ( ( ) => ( { isOpen : false } ) )
11+ const mockRequestJson = vi . hoisted ( ( ) => vi . fn ( ) )
12+
13+ vi . mock ( '@/lib/api/client/request' , ( ) => ( { requestJson : mockRequestJson } ) )
1114
1215vi . mock ( 'next/link' , ( ) => ( {
1316 default : ( {
@@ -25,7 +28,7 @@ vi.mock('next/link', () => ({
2528 </ a >
2629 ) ,
2730} ) )
28- vi . mock ( '@/app/workspace/[workspaceId]/w/components/sidebar/hooks' , ( ) => ( {
31+ vi . mock ( '@/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-hover-menu ' , ( ) => ( {
2932 useHoverMenu : ( ) => ( {
3033 isOpen : hoverState . isOpen ,
3134 open : vi . fn ( ) ,
@@ -37,6 +40,7 @@ vi.mock('@/app/workspace/[workspaceId]/w/components/sidebar/hooks', () => ({
3740} ) )
3841
3942import { ChatsSection } from '@/app/o/[organizationId]/components/organization-sidebar/components/chats-section/chats-section'
43+ import { mothershipChatKeys } from '@/hooks/queries/mothership-chats'
4044
4145const CHATS : OrganizationChat [ ] = Array . from ( { length : 8 } , ( _ , index ) => ( {
4246 id : `chat-${ index + 1 } ` ,
@@ -59,6 +63,8 @@ beforeEach(() => {
5963 disconnect ( ) { }
6064 }
6165 )
66+ vi . clearAllMocks ( )
67+ mockRequestJson . mockResolvedValue ( { success : true } )
6268 hoverState . isOpen = false
6369 queryClient = new QueryClient ( { defaultOptions : { queries : { retry : false } } } )
6470 prefetchQuery = vi . spyOn ( queryClient , 'prefetchQuery' ) . mockResolvedValue ( )
@@ -83,9 +89,7 @@ async function render(props: Partial<Parameters<typeof ChatsSection>[0]> = {}) {
8389 isLoading = { false }
8490 isCollapsed = { false }
8591 pathname = { null }
86- menuOpenHref = { null }
87- onContextMenu = { ( ) => { } }
88- onMoreClick = { ( ) => { } }
92+ organizationId = 'org-1'
8993 { ...props }
9094 />
9195 </ QueryClientProvider >
@@ -94,11 +98,19 @@ async function render(props: Partial<Parameters<typeof ChatsSection>[0]> = {}) {
9498}
9599
96100describe ( 'ChatsSection' , ( ) => {
97- it ( 'lists every chat with no paging control ' , async ( ) => {
101+ it ( 'shows five chats with the workspace-style See more and See less controls ' , async ( ) => {
98102 await render ( )
99-
103+ expect ( container . querySelectorAll ( 'a[href^="/o/org-1/chat/"]' ) ) . toHaveLength ( 5 )
104+ const more = Array . from ( container . querySelectorAll ( 'button' ) ) . find (
105+ ( button ) => button . textContent === 'See more'
106+ ) !
107+ await act ( async ( ) => more . click ( ) )
100108 expect ( container . querySelectorAll ( 'a[href^="/o/org-1/chat/"]' ) ) . toHaveLength ( 8 )
101- expect ( container . textContent ) . not . toContain ( 'See more' )
109+ const less = Array . from ( container . querySelectorAll ( 'button' ) ) . find (
110+ ( button ) => button . textContent === 'See less'
111+ ) !
112+ await act ( async ( ) => less . click ( ) )
113+ expect ( container . querySelectorAll ( 'a[href^="/o/org-1/chat/"]' ) ) . toHaveLength ( 5 )
102114 } )
103115
104116 it ( 'marks the chat on the current route active' , async ( ) => {
@@ -110,17 +122,108 @@ describe('ChatsSection', () => {
110122 expect ( other ?. className ) . not . toContain ( 'surface-active' )
111123 } )
112124
113- it ( 'reports the row href when its options button is pressed' , async ( ) => {
114- const onMoreClick = vi . fn ( )
115- await render ( { onMoreClick } )
125+ it . each ( [ false , true ] ) ( 'renames via the options menu with collapsed=%s' , async ( isCollapsed ) => {
126+ hoverState . isOpen = isCollapsed
127+ await render ( { isCollapsed } )
128+ const button =
129+ document . body . querySelector < HTMLButtonElement > (
130+ 'a[href="/o/org-1/chat/chat-2"] button[aria-label="Chat options"]'
131+ ) ?? document . body . querySelector < HTMLButtonElement > ( '[aria-label="Chat options"]' ) !
132+ await act ( async ( ) => button . click ( ) )
133+ const rename = Array . from (
134+ document . body . querySelectorAll < HTMLElement > ( '[role="menuitem"]' )
135+ ) . find ( ( item ) => item . textContent === 'Rename' ) !
136+ expect ( rename ) . toBeDefined ( )
137+ await act ( async ( ) => rename . click ( ) )
138+ const input = document . body . querySelector < HTMLInputElement > ( 'input[aria-label^="Rename chat"]' ) !
139+ expect ( input ) . not . toBeNull ( )
140+ expect ( input . value ) . toMatch ( / ^ C h a t / )
141+ await act ( async ( ) => {
142+ Object . getOwnPropertyDescriptor ( HTMLInputElement . prototype , 'value' ) ! . set ! . call (
143+ input ,
144+ 'Planning'
145+ )
146+ input . dispatchEvent ( new Event ( 'input' , { bubbles : true } ) )
147+ } )
148+ await act ( async ( ) =>
149+ input . dispatchEvent ( new KeyboardEvent ( 'keydown' , { key : 'Enter' , bubbles : true } ) )
150+ )
151+ expect ( mockRequestJson ) . toHaveBeenCalledWith (
152+ expect . objectContaining ( { method : 'PATCH' } ) ,
153+ expect . objectContaining ( { body : { title : 'Planning' } } )
154+ )
155+ expect ( document . body . querySelector ( 'input[aria-label^="Rename chat"]' ) ) . toBeNull ( )
156+ } )
157+
158+ it ( 'rolls back only the organization list when rename fails' , async ( ) => {
159+ const pending = Promise . withResolvers < { success : boolean } > ( )
160+ mockRequestJson . mockReturnValueOnce ( pending . promise )
161+ const key = mothershipChatKeys . organizationList ( 'org-1' )
162+ queryClient . setQueryData ( key , [ { id : 'chat-1' , name : 'Chat 1' } ] )
163+ const workspaceKey = mothershipChatKeys . list ( 'workspace-1' )
164+ queryClient . setQueryData ( workspaceKey , [ { id : 'workspace-chat' , name : 'Workspace chat' } ] )
165+ await render ( )
166+ await act ( async ( ) =>
167+ container . querySelector < HTMLButtonElement > ( '[aria-label="Chat options"]' ) ! . click ( )
168+ )
169+ const action = Array . from (
170+ document . body . querySelectorAll < HTMLElement > ( '[role="menuitem"]' )
171+ ) . find ( ( item ) => item . textContent === 'Rename' ) !
172+ await act ( async ( ) => action . click ( ) )
173+ const input = document . body . querySelector < HTMLInputElement > ( 'input[aria-label^="Rename chat"]' ) !
174+ await act ( async ( ) => {
175+ Object . getOwnPropertyDescriptor ( HTMLInputElement . prototype , 'value' ) ! . set ! . call (
176+ input ,
177+ 'Pending title'
178+ )
179+ input . dispatchEvent ( new Event ( 'input' , { bubbles : true } ) )
180+ } )
181+ await act ( async ( ) =>
182+ input . dispatchEvent ( new KeyboardEvent ( 'keydown' , { key : 'Enter' , bubbles : true } ) )
183+ )
184+ expect ( queryClient . getQueryData ( key ) ) . toEqual ( [ { id : 'chat-1' , name : 'Pending title' } ] )
185+ await act ( async ( ) => pending . reject ( new Error ( 'Rename rejected' ) ) )
186+ expect ( queryClient . getQueryData ( key ) ) . toEqual ( [ { id : 'chat-1' , name : 'Chat 1' } ] )
187+ expect ( queryClient . getQueryData ( workspaceKey ) ) . toEqual ( [
188+ { id : 'workspace-chat' , name : 'Workspace chat' } ,
189+ ] )
190+ expect ( input . value ) . toBe ( 'Chat 1' )
191+ expect ( input . disabled ) . toBe ( false )
192+ } )
116193
117- const button = container . querySelector < HTMLButtonElement > (
118- 'a[href="/o/org-1/chat/chat-2"] button[aria-label="Chat options"]'
194+ it ( 'cancels rename on Escape without a mutation' , async ( ) => {
195+ await render ( )
196+ await act ( async ( ) =>
197+ container . querySelector < HTMLButtonElement > ( '[aria-label="Chat options"]' ) ! . click ( )
198+ )
199+ const rename = Array . from (
200+ document . body . querySelectorAll < HTMLElement > ( '[role="menuitem"]' )
201+ ) . find ( ( item ) => item . textContent === 'Rename' ) !
202+ await act ( async ( ) => rename . click ( ) )
203+ const input = document . body . querySelector < HTMLInputElement > ( 'input[aria-label^="Rename chat"]' ) !
204+ await act ( async ( ) =>
205+ input . dispatchEvent ( new KeyboardEvent ( 'keydown' , { key : 'Escape' , bubbles : true } ) )
119206 )
120- await act ( async ( ) => button ?. click ( ) )
207+ expect ( mockRequestJson ) . not . toHaveBeenCalled ( )
208+ expect ( document . body . querySelector ( 'input[aria-label^="Rename chat"]' ) ) . toBeNull ( )
209+ } )
121210
122- expect ( onMoreClick ) . toHaveBeenCalledWith ( expect . anything ( ) , '/o/org-1/chat/chat-2' )
123- expect ( prefetchQuery ) . not . toHaveBeenCalled ( )
211+ it . each ( [
212+ [ 'Pin' , { pinned : true } ] ,
213+ [ 'Mark as unread' , { isUnread : true } ] ,
214+ ] ) ( 'offers %s for organization chats' , async ( label , body ) => {
215+ await render ( )
216+ await act ( async ( ) =>
217+ container . querySelector < HTMLButtonElement > ( '[aria-label="Chat options"]' ) ! . click ( )
218+ )
219+ const action = Array . from (
220+ document . body . querySelectorAll < HTMLElement > ( '[role="menuitem"]' )
221+ ) . find ( ( item ) => item . textContent === label ) !
222+ await act ( async ( ) => action . click ( ) )
223+ expect ( mockRequestJson ) . toHaveBeenCalledWith ( expect . objectContaining ( { method : 'PATCH' } ) , {
224+ params : { chatId : 'chat-1' } ,
225+ body,
226+ } )
124227 } )
125228
126229 it . each ( [ false , true ] ) (
0 commit comments