@@ -5,19 +5,36 @@ import { act, type ComponentProps } from 'react'
55import { createRoot , type Root } from 'react-dom/client'
66import { afterEach , beforeEach , describe , expect , it , vi } from 'vitest'
77
8- const { mockNavigate, mockPush } = vi . hoisted ( ( ) => ( {
8+ const { mockNavigate, mockPush, context } = vi . hoisted ( ( ) => ( {
99 mockNavigate : vi . fn ( ) ,
1010 mockPush : vi . fn ( ) ,
11+ context : {
12+ organization : { id : 'org-1' } ,
13+ viewer : { isAdmin : true } ,
14+ settingsFeatures : {
15+ billingEnabled : true ,
16+ hasEnterprisePlan : false ,
17+ hosted : true ,
18+ selfHosted : { } ,
19+ } ,
20+ connectedAccountsAvailable : true ,
21+ searchAccess : { memberScoped : true } ,
22+ } ,
1123} ) )
1224
1325vi . mock ( 'next/navigation' , ( ) => ( {
1426 useRouter : ( ) => ( { push : mockPush } ) ,
27+ usePathname : ( ) => '/o/org-1/home' ,
1528} ) )
1629vi . mock ( 'next/link' , ( ) => ( {
1730 default : ( {
1831 onNavigate,
32+ prefetch : _prefetch ,
1933 ...props
20- } : ComponentProps < 'a' > & { onNavigate ?: ( event : { preventDefault : ( ) => void } ) => void } ) => (
34+ } : ComponentProps < 'a' > & {
35+ prefetch ?: boolean
36+ onNavigate ?: ( event : { preventDefault : ( ) => void } ) => void
37+ } ) => (
2138 < a
2239 { ...props }
2340 href = { props . href }
@@ -34,6 +51,10 @@ vi.mock('next/link', () => ({
3451 />
3552 ) ,
3653} ) )
54+ vi . mock ( '@/lib/auth/sign-out' , ( ) => ( { signOutAndRedirect : vi . fn ( ) } ) )
55+ vi . mock ( '@/lib/auth/auth-client' , ( ) => ( {
56+ useSession : ( ) => ( { data : { user : { id : 'user-1' } } } ) ,
57+ } ) )
3758vi . mock ( '@/lib/desktop' , ( ) => ( { getDesktopUpdates : ( ) => null } ) )
3859vi . mock ( '@/hooks/use-desktop-update-state' , ( ) => ( {
3960 useDesktopUpdateState : ( ) => ( { status : 'idle' } ) ,
@@ -42,12 +63,16 @@ vi.mock('@/hooks/queries/user-profile', () => ({
4263 useUserProfile : ( ) => ( { data : { id : 'user-1' , name : 'Ada' , email : 'ada@example.com' } } ) ,
4364} ) )
4465vi . mock ( '@/app/o/[organizationId]/providers/organization-provider' , ( ) => ( {
45- useOrganizationContext : ( ) => ( { organization : { id : 'org-1' } } ) ,
66+ useOrganizationContext : ( ) => context ,
4667} ) )
4768vi . mock ( '@/app/workspace/[workspaceId]/w/components/sidebar/components' , ( ) => ( {
4869 SidebarTooltip : ( { children } : { children : React . ReactNode } ) => children ,
4970} ) )
50- vi . mock ( '@/components/icons' , ( ) => ( { SlackIcon : ( ) => < svg /> } ) )
71+ vi . mock ( '@/components/icons' , ( ) => ( {
72+ CodeIcon : ( ) => < svg /> ,
73+ McpIcon : ( ) => < svg /> ,
74+ SlackIcon : ( ) => < svg /> ,
75+ } ) )
5176
5277import { OrganizationFooter } from '@/app/o/[organizationId]/components/organization-sidebar/components/organization-footer/organization-footer'
5378import { useSettingsDirtyStore } from '@/stores/settings/dirty/store'
@@ -58,6 +83,8 @@ let root: Root
5883beforeEach ( ( ) => {
5984 vi . stubGlobal ( 'IS_REACT_ACT_ENVIRONMENT' , true )
6085 vi . clearAllMocks ( )
86+ context . viewer . isAdmin = true
87+ context . settingsFeatures . billingEnabled = true
6188 useSettingsDirtyStore . getState ( ) . reset ( )
6289 container = document . createElement ( 'div' )
6390 document . body . appendChild ( container )
@@ -71,7 +98,7 @@ afterEach(async () => {
7198 vi . unstubAllGlobals ( )
7299} )
73100
74- async function selectSettings ( ) {
101+ async function openProfileMenu ( ) {
75102 await act ( async ( ) => {
76103 root . render (
77104 < OrganizationFooter
@@ -80,6 +107,7 @@ async function selectSettings() {
80107 showCollapsedTooltips = { false }
81108 onOpenDocs = { ( ) => { } }
82109 onJoinSlack = { ( ) => { } }
110+ onContactSupport = { ( ) => { } }
83111 />
84112 )
85113 } )
@@ -88,15 +116,59 @@ async function selectSettings() {
88116 await act ( async ( ) => {
89117 trigger . dispatchEvent ( new MouseEvent ( 'pointerdown' , { bubbles : true , button : 0 } ) )
90118 } )
119+ }
120+
121+ async function selectSettings ( ) {
122+ await openProfileMenu ( )
91123 const link = document . querySelector < HTMLAnchorElement > ( 'a[href="/o/org-1/settings/general"]' )
92124 if ( ! link ) throw new Error ( 'Settings link is missing' )
93125 await act ( async ( ) => link . click ( ) )
94126}
95127
96128describe ( 'OrganizationFooter settings navigation' , ( ) => {
129+ it ( 'preserves unsaved settings when opening a shortcut' , async ( ) => {
130+ useSettingsDirtyStore . getState ( ) . setDirty ( true )
131+ await openProfileMenu ( )
132+ const members = document . querySelector < HTMLAnchorElement > ( 'a[href="/o/org-1/settings/members"]' )
133+ if ( ! members ) throw new Error ( 'Members shortcut is missing' )
134+ await act ( async ( ) => members . click ( ) )
135+ expect ( mockPush ) . not . toHaveBeenCalled ( )
136+ act ( ( ) => useSettingsDirtyStore . getState ( ) . confirmLeave ( ) )
137+ expect ( mockPush ) . toHaveBeenCalledWith ( '/o/org-1/settings/members' )
138+ } )
139+
140+ it . each ( [
141+ { isAdmin : true , billingEnabled : true , showBilling : true } ,
142+ { isAdmin : false , billingEnabled : true , showBilling : false } ,
143+ { isAdmin : true , billingEnabled : false , showBilling : false } ,
144+ ] ) (
145+ 'matches settings visibility for $isAdmin admin, $billingEnabled billing' ,
146+ async ( { isAdmin, billingEnabled, showBilling } ) => {
147+ context . viewer . isAdmin = isAdmin
148+ context . settingsFeatures . billingEnabled = billingEnabled
149+ await openProfileMenu ( )
150+ expect (
151+ [ ...document . querySelectorAll ( '[role="menuitem"]' ) ] . map ( ( item ) => item . textContent )
152+ ) . toEqual ( [
153+ 'Settings' ,
154+ ...( showBilling ? [ 'Subscription' ] : [ ] ) ,
155+ 'Members' ,
156+ 'Recently deleted' ,
157+ 'Sign out' ,
158+ ] )
159+ expect ( document . querySelector ( '[role="separator"]' ) ) . toBeNull ( )
160+ const members = document . querySelector < HTMLAnchorElement > (
161+ 'a[href="/o/org-1/settings/members"]'
162+ )
163+ if ( ! members ) throw new Error ( 'Members shortcut is missing' )
164+ await act ( async ( ) => members . click ( ) )
165+ expect ( mockPush ) . toHaveBeenCalledWith ( '/o/org-1/settings/members' )
166+ }
167+ )
168+
97169 it ( 'navigates immediately when settings are clean' , async ( ) => {
98170 await selectSettings ( )
99- expect ( mockNavigate ) . toHaveBeenCalledWith ( '/o/org-1/settings/general' )
171+ expect ( mockPush ) . toHaveBeenCalledWith ( '/o/org-1/settings/general' )
100172 expect ( useSettingsDirtyStore . getState ( ) . pendingLeave ) . toBeNull ( )
101173 } )
102174
0 commit comments