@@ -70,15 +70,10 @@ function fakeApps(): INavigationEntry[] {
7070// ships getAll('settings') keyed by entry id.
7171function mockActiveSettingsEntry ( overrides : Partial < INavigationEntry > ) : void {
7272 const entry = makeApp ( { type : 'settings' , active : true , ...overrides } )
73- initialState . loadState . mockImplementation ( ( _a : string , key : string , fallback : unknown ) => {
74- if ( key === 'apps' ) {
75- return [ makeApp ( { id : 'files' , name : 'Files' , active : false } ) ]
76- }
77- if ( key === 'settingsNavEntries' ) {
78- return { [ entry . id ] : entry }
79- }
80- return fallback
81- } )
73+ initialState . loadState . mockImplementation ( stateFor ( {
74+ apps : [ makeApp ( { id : 'files' , name : 'Files' , active : false } ) ] ,
75+ settingsNavEntries : { [ entry . id ] : entry } ,
76+ } ) )
8277}
8378
8479// Navigation actions (INavigationManager::TYPE_ACTION). Without an `href` the
@@ -98,17 +93,16 @@ function fakeActions(count: number): INavigationEntry[] {
9893 return ids . slice ( 0 , count ) . map ( ( id ) => makeAction ( id ) )
9994}
10095
96+ // AppMenu hides the app store tile when the state is absent, so a default
97+ // instance has to supply it.
98+ function stateFor ( states : Record < string , unknown > ) {
99+ const all : Record < string , unknown > = { appStoreLinkShown : true , ...states }
100+ return ( _app : string , key : string , fallback : unknown ) => key in all ? all [ key ] : fallback
101+ }
102+
101103// loadState implementation serving both apps and navigation actions.
102104function stateWith ( apps : INavigationEntry [ ] , actions : INavigationEntry [ ] ) {
103- return ( _app : string , key : string , fallback : unknown ) => {
104- if ( key === 'apps' ) {
105- return apps
106- }
107- if ( key === 'navigationActions' ) {
108- return actions
109- }
110- return fallback
111- }
105+ return stateFor ( { apps, navigationActions : actions } )
112106}
113107
114108function eightApps ( activeIndex : number = - 1 ) : INavigationEntry [ ] {
@@ -132,7 +126,7 @@ beforeEach(async () => {
132126 for ( const k of Object . keys ( eventBus . __handlers ) ) {
133127 delete eventBus . __handlers [ k ]
134128 }
135- initialState . loadState . mockImplementation ( ( _app : string , key : string , fallback : unknown ) => key === 'apps' ? fakeApps ( ) : fallback )
129+ initialState . loadState . mockImplementation ( stateFor ( { apps : fakeApps ( ) } ) )
136130 auth . getCurrentUser . mockReturnValue ( { isAdmin : false } )
137131 AppMenu = ( await import ( '../../components/AppMenu.vue' ) ) . default
138132} )
@@ -144,6 +138,11 @@ afterEach(() => {
144138 }
145139} )
146140
141+ function gridLabels ( ) : string [ ] {
142+ return Array . from ( document . querySelectorAll ( '.app-menu__grid [role="menuitem"]' ) )
143+ . map ( ( el ) => el . querySelector ( '.app-item__label' ) ?. textContent ?. trim ( ) ?? '' )
144+ }
145+
147146// Click the waffle trigger and poll until the teleported menuitems are in the
148147// DOM. NcPopover teleports to <body> so wrapper.find() can't see them; vi.waitFor
149148// retries the DOM query rather than relying on flaky nextTick/setTimeout flushes.
@@ -166,10 +165,24 @@ describe('core: AppMenu', () => {
166165 const wrapper = mount ( AppMenu , { attachTo : document . body } )
167166 await openPopover ( wrapper )
168167
169- const items = document . querySelectorAll ( '.app-menu__grid [role="menuitem"]' )
170- expect ( items ) . toHaveLength ( 4 )
171- const labels = Array . from ( items ) . map ( ( el ) => el . querySelector ( '.app-item__label' ) ?. textContent ?. trim ( ) ?? '' )
172- expect ( labels ) . toEqual ( [ 'Files' , 'Mail' , 'Calendar' , 'App store' ] )
168+ expect ( gridLabels ( ) ) . toEqual ( [ 'Files' , 'Mail' , 'Calendar' , 'App store' ] )
169+ } )
170+
171+ it ( 'omits the "App store" tile when the instance does not offer it' , async ( ) => {
172+ initialState . loadState . mockImplementation ( stateFor ( { apps : fakeApps ( ) , appStoreLinkShown : false } ) )
173+ const wrapper = mount ( AppMenu , { attachTo : document . body } )
174+ await openPopover ( wrapper )
175+
176+ expect ( gridLabels ( ) ) . toEqual ( [ 'Files' , 'Mail' , 'Calendar' ] )
177+ } )
178+
179+ it ( 'keeps the "More apps" tile for admins when the app store link is hidden' , async ( ) => {
180+ initialState . loadState . mockImplementation ( stateFor ( { apps : fakeApps ( ) , appStoreLinkShown : false } ) )
181+ auth . getCurrentUser . mockReturnValue ( { isAdmin : true } )
182+ const wrapper = mount ( AppMenu , { attachTo : document . body } )
183+ await openPopover ( wrapper )
184+
185+ expect ( gridLabels ( ) ) . toEqual ( [ 'Files' , 'Mail' , 'Calendar' , 'More apps' ] )
173186 } )
174187
175188 it ( 'renders the "More apps" tile when the current user is an admin' , async ( ) => {
@@ -196,7 +209,7 @@ describe('core: AppMenu', () => {
196209 } )
197210
198211 it ( 'ArrowRight moves the roving stop from index 0 to index 1 and focuses it' , async ( ) => {
199- initialState . loadState . mockImplementation ( ( _a : string , key : string , fallback : unknown ) => key === 'apps' ? eightApps ( ) : fallback )
212+ initialState . loadState . mockImplementation ( stateFor ( { apps : eightApps ( ) } ) )
200213 const wrapper = mount ( AppMenu , { attachTo : document . body } )
201214 await openPopover ( wrapper )
202215
@@ -274,15 +287,10 @@ describe('core: AppMenu', () => {
274287 } )
275288
276289 it ( 'prefers the active app over a settings entry when both are marked active' , ( ) => {
277- initialState . loadState . mockImplementation ( ( _a : string , key : string , fallback : unknown ) => {
278- if ( key === 'apps' ) {
279- return [ makeApp ( { id : 'files' , name : 'Files' , active : true } ) ]
280- }
281- if ( key === 'settingsNavEntries' ) {
282- return { settings_administration : makeApp ( { id : 'settings_administration' , name : 'Administration settings' , type : 'settings' , active : true } ) }
283- }
284- return fallback
285- } )
290+ initialState . loadState . mockImplementation ( stateFor ( {
291+ apps : [ makeApp ( { id : 'files' , name : 'Files' , active : true } ) ] ,
292+ settingsNavEntries : { settings_administration : makeApp ( { id : 'settings_administration' , name : 'Administration settings' , type : 'settings' , active : true } ) } ,
293+ } ) )
286294 const wrapper = mount ( AppMenu , { attachTo : document . body } )
287295 expect ( wrapper . find ( '.app-menu__current-app-name' ) . text ( ) ) . toBe ( 'Files' )
288296 } )
@@ -292,15 +300,10 @@ describe('core: AppMenu', () => {
292300 // "current section" even though it carries type=settings. NavigationManager
293301 // today never marks it active, but a future regression shouldn't leak a
294302 // "Log out" label into the header.
295- initialState . loadState . mockImplementation ( ( _a : string , key : string , fallback : unknown ) => {
296- if ( key === 'apps' ) {
297- return [ makeApp ( { id : 'files' , name : 'Files' , active : false } ) ]
298- }
299- if ( key === 'settingsNavEntries' ) {
300- return { logout : makeApp ( { id : 'logout' , name : 'Log out' , type : 'settings' , href : '/logout' , active : true } ) }
301- }
302- return fallback
303- } )
303+ initialState . loadState . mockImplementation ( stateFor ( {
304+ apps : [ makeApp ( { id : 'files' , name : 'Files' , active : false } ) ] ,
305+ settingsNavEntries : { logout : makeApp ( { id : 'logout' , name : 'Log out' , type : 'settings' , href : '/logout' , active : true } ) } ,
306+ } ) )
304307 const wrapper = mount ( AppMenu , { attachTo : document . body } )
305308 expect ( wrapper . find ( '.app-menu__current-app' ) . exists ( ) ) . toBe ( false )
306309 } )
0 commit comments