44import { act } from 'react'
55import { createRoot } from 'react-dom/client'
66import { renderToStaticMarkup } from 'react-dom/server'
7- import { describe , expect , it , vi } from 'vitest'
7+ import { afterEach , describe , expect , it , vi } from 'vitest'
88import type { TableViewWire } from '@/lib/api/contracts/tables'
99import { ViewsMenu } from '@/app/workspace/[workspaceId]/tables/[tableId]/components/views-menu/views-menu'
1010
@@ -26,6 +26,15 @@ const SECOND_VIEW: TableViewWire = {
2626 isDefault : false ,
2727}
2828
29+ const PRIMARY_VIEW : TableViewWire = {
30+ ...DEFAULT_VIEW ,
31+ name : 'Primary view' ,
32+ }
33+
34+ afterEach ( ( ) => {
35+ vi . useRealTimers ( )
36+ } )
37+
2938function renderMenu ( views : TableViewWire [ ] , activeViewId : string | null ) : string {
3039 return renderToStaticMarkup (
3140 < ViewsMenu
@@ -56,7 +65,7 @@ describe('ViewsMenu', () => {
5665 expect ( markup ) . not . toContain ( '>View<' )
5766 } )
5867
59- it ( 'offers a set-default action only for non-default views ' , ( ) => {
68+ it ( 'shows filled and outline pins without a Default badge and keeps the menu open ' , ( ) => {
6069 const container = document . createElement ( 'div' )
6170 document . body . appendChild ( container )
6271 const root = createRoot ( container )
@@ -65,8 +74,8 @@ describe('ViewsMenu', () => {
6574 act ( ( ) => {
6675 root . render (
6776 < ViewsMenu
68- views = { [ DEFAULT_VIEW , SECOND_VIEW ] }
69- activeViewId = { DEFAULT_VIEW . id }
77+ views = { [ PRIMARY_VIEW , SECOND_VIEW ] }
78+ activeViewId = { PRIMARY_VIEW . id }
7079 onSelect = { vi . fn ( ) }
7180 onRename = { vi . fn ( ) }
7281 onSetDefault = { onSetDefault }
@@ -78,12 +87,103 @@ describe('ViewsMenu', () => {
7887 } )
7988 act ( ( ) => container . querySelector < HTMLButtonElement > ( 'button[aria-label="Views"]' ) ?. click ( ) )
8089
81- const actions = document . body . querySelectorAll < HTMLButtonElement > (
90+ const defaultPin = document . body . querySelector < HTMLButtonElement > (
91+ 'button[aria-label="Current default view"]'
92+ )
93+ const setDefaultPin = document . body . querySelector < HTMLButtonElement > (
8294 'button[aria-label="Set as default"]'
8395 )
84- expect ( actions ) . toHaveLength ( 1 )
85- act ( ( ) => actions [ 0 ] ?. click ( ) )
96+
97+ expect ( defaultPin ?. querySelector ( 'svg' ) ) . toHaveClass ( 'fill-current' )
98+ expect ( setDefaultPin ?. querySelector ( 'svg' ) ) . not . toHaveClass ( 'fill-current' )
99+ expect ( document . body ) . not . toHaveTextContent ( 'Default' )
100+
101+ act ( ( ) => setDefaultPin ?. click ( ) )
86102 expect ( onSetDefault ) . toHaveBeenCalledWith ( SECOND_VIEW . id )
103+ expect ( document . body ) . toHaveTextContent ( 'New view' )
104+
105+ act ( ( ) => root . unmount ( ) )
106+ container . remove ( )
107+ } )
108+
109+ it ( 'keeps the menu open when keyboard focus moves from the trigger to the default pin' , ( ) => {
110+ vi . useFakeTimers ( )
111+ const container = document . createElement ( 'div' )
112+ document . body . appendChild ( container )
113+ const root = createRoot ( container )
114+
115+ act ( ( ) => {
116+ root . render (
117+ < ViewsMenu
118+ views = { [ PRIMARY_VIEW , SECOND_VIEW ] }
119+ activeViewId = { PRIMARY_VIEW . id }
120+ onSelect = { vi . fn ( ) }
121+ onRename = { vi . fn ( ) }
122+ onSetDefault = { vi . fn ( ) }
123+ onDelete = { vi . fn ( ) }
124+ onNewView = { vi . fn ( ) }
125+ canEdit
126+ />
127+ )
128+ } )
129+
130+ const trigger = container . querySelector < HTMLButtonElement > ( 'button[aria-label="Views"]' )
131+ act ( ( ) => trigger ?. focus ( ) )
132+
133+ const setDefaultPin = document . body . querySelector < HTMLButtonElement > (
134+ 'button[aria-label="Set as default"]'
135+ )
136+ expect ( setDefaultPin ) . not . toBeNull ( )
137+ act ( ( ) => {
138+ setDefaultPin ?. focus ( )
139+ vi . advanceTimersByTime ( 121 )
140+ } )
141+
142+ expect ( document . activeElement ) . toBe ( setDefaultPin )
143+ expect ( document . body ) . toHaveTextContent ( 'New view' )
144+ expect ( document . body . querySelector ( '[data-native-surface-overlay]' ) ) . not . toBeNull ( )
145+
146+ act ( ( ) => root . unmount ( ) )
147+ container . remove ( )
148+ } )
149+
150+ it ( 'shows disabled pins without closing the menu for read-only members' , ( ) => {
151+ const container = document . createElement ( 'div' )
152+ document . body . appendChild ( container )
153+ const root = createRoot ( container )
154+ const onSetDefault = vi . fn ( )
155+
156+ act ( ( ) => {
157+ root . render (
158+ < ViewsMenu
159+ views = { [ PRIMARY_VIEW , SECOND_VIEW ] }
160+ activeViewId = { PRIMARY_VIEW . id }
161+ onSelect = { vi . fn ( ) }
162+ onRename = { vi . fn ( ) }
163+ onSetDefault = { onSetDefault }
164+ onDelete = { vi . fn ( ) }
165+ onNewView = { vi . fn ( ) }
166+ canEdit = { false }
167+ />
168+ )
169+ } )
170+ act ( ( ) => container . querySelector < HTMLButtonElement > ( 'button[aria-label="Views"]' ) ?. click ( ) )
171+
172+ const defaultPin = document . body . querySelector < HTMLButtonElement > (
173+ 'button[aria-label="Current default view"]'
174+ )
175+ const setDefaultPin = document . body . querySelector < HTMLButtonElement > (
176+ 'button[aria-label="Set as default"]'
177+ )
178+
179+ expect ( defaultPin ?. querySelector ( 'svg' ) ) . toHaveClass ( 'fill-current' )
180+ expect ( setDefaultPin ?. querySelector ( 'svg' ) ) . not . toHaveClass ( 'fill-current' )
181+ expect ( setDefaultPin ) . toBeDisabled ( )
182+
183+ act ( ( ) => setDefaultPin ?. click ( ) )
184+
185+ expect ( onSetDefault ) . not . toHaveBeenCalled ( )
186+ expect ( document . body . querySelector ( '[data-native-surface-overlay]' ) ) . not . toBeNull ( )
87187
88188 act ( ( ) => root . unmount ( ) )
89189 container . remove ( )
0 commit comments