@@ -7,6 +7,9 @@ import StudioNavigation from '../StudioNavigation';
77
88const localVue = createLocalVue ( ) ;
99localVue . use ( Vuex ) ;
10+ localVue . use ( VueRouter ) ;
11+
12+
1013
1114global . window . Urls = {
1215 channels : jest . fn ( ( ) => '/channels' ) ,
@@ -19,26 +22,18 @@ const mockActions = {
1922 trackClick : jest . fn ( ) ,
2023} ;
2124
22- const createMockStore = ( currentUser = null ) => {
23- return new Vuex . Store ( {
25+ const renderComponent = ( props = { } , user = null ) => {
26+ const store = new Vuex . Store ( {
2427 modules : {
2528 session : {
26- state : {
27- currentUser : currentUser ,
28- } ,
29- getters : {
30- loggedIn : ( ) => ! ! currentUser ,
31- } ,
32- actions : {
33- logout : mockActions . logout ,
34- } ,
29+ state : { currentUser : user } ,
30+ getters : { loggedIn : ( ) => ! ! user } ,
31+ actions : { logout : mockActions . logout } ,
3532 } ,
3633 } ,
3734 } ) ;
38- } ;
3935
40- const createMockRouter = ( ) => {
41- return new VueRouter ( {
36+ const router = new VueRouter ( {
4237 routes : [
4338 { path : '/' , name : 'home' } ,
4439 { path : '/channels' , name : 'channels' } ,
@@ -47,88 +42,59 @@ const createMockRouter = () => {
4742 { path : '/library' , name : 'library' } ,
4843 ] ,
4944 } ) ;
50- } ;
51-
52- const stubs = {
53- KToolbar : { template : '<div><slot name="icon"></slot><slot name="brand"></slot><slot name="actions"></slot></div>' } ,
54- KIconButton : {
55- props : [ 'icon' , 'ariaLabel' ] ,
56- template : '<button :aria-label="ariaLabel || icon" v-on="$listeners"><slot></slot></button>'
57- } ,
58- KExternalLink : {
59- props : [ 'href' , 'text' ] ,
60- template : '<a :href="href">{{ text }}<slot></slot></a>'
61- } ,
62- KLogo : { template : '<img alt="Kolibri Logo" />' } ,
63- KDropdownMenu : {
64- props : [ 'options' ] ,
65- template : `
66- <div data-testid="dropdown-menu">
67- <button v-for="opt in options" :key="opt.value" @click="$emit('select', opt)">
68- {{ opt.label }}
69- </button>
70- </div>
71- `
72- } ,
73- StudioNavigationTab : {
74- props : [ 'to' ] ,
75- template : '<router-link :to="to"><slot></slot></router-link>'
76- } ,
77- SidePanelModal : {
78- template : `
79- <div role="dialog" aria-label="Navigation menu">
80- <button aria-label="Close" @click="$emit('closePanel')">Close</button>
81- <slot name="header"></slot>
82- <slot></slot>
83- </div>
84- `
85- } ,
86- StudioNavigationOption : {
87- props : [ 'label' , 'link' ] ,
88- template : '<a :href="link" @click="$emit(\'select\') || $emit(\'click\')">{{ label }}</a>'
89- } ,
90- SkipNavigationLink : { template : '<a>Skip</a>' } ,
91- LanguageSwitcherModal : { template : '<div role="dialog">Language Modal</div>' } ,
92- } ;
93-
94- const renderComponent = ( props = { } , user = null ) => {
95- const store = createMockStore ( user ) ;
96- const router = createMockRouter ( ) ;
9745
9846 return render ( StudioNavigation , {
9947 localVue,
10048 store,
101- routes : router ,
49+ router,
10250 props : {
10351 title : 'Kolibri Studio' ,
10452 ...props ,
10553 } ,
106- stubs ,
54+
10755 mocks : {
56+ $formatNumber : n => n ,
10857 $tr : ( key ) => {
109- const strings = {
58+ const map = {
11059 title : 'Kolibri Studio' ,
11160 openMenu : 'Open navigation menu' ,
112- navigationMenu : 'Navigation menu' ,
11361 userMenuLabel : 'User menu' ,
11462 guestMenuLabel : 'Guest menu' ,
63+ navigationMenu : 'Navigation menu' ,
11564 signIn : 'Sign in' ,
11665 signOut : 'Sign out' ,
117- administration : 'Administration ' ,
66+ channels : 'Channels ' ,
11867 settings : 'Settings' ,
11968 help : 'Help and support' ,
12069 changeLanguage : 'Change language' ,
121- channels : 'Channels ' ,
70+ administration : 'Administration ' ,
12271 copyright : '© 2026 Learning Equality' ,
12372 giveFeedback : 'Give feedback' ,
73+ confirmAction : 'Confirm' ,
74+ cancelAction : 'Cancel' ,
75+ changeLanguageModalHeader : 'Change language' ,
76+ skipToMainContentAction : 'Skip to main content' ,
12477 } ;
125- return strings [ key ] || key ;
78+ return map [ key ] || key ;
12679 } ,
12780 } ,
12881 } ) ;
12982} ;
13083
13184describe ( 'StudioNavigation' , ( ) => {
85+ const originalLocation = window . location ;
86+ beforeAll ( ( ) => {
87+ delete window . location ;
88+ window . location = { href : '' , assign : jest . fn ( ) } ;
89+ window . open = jest . fn ( ) ;
90+
91+ } ) ;
92+
93+ afterAll ( ( ) => {
94+ window . location = originalLocation ;
95+ jest . restoreAllMocks ( ) ;
96+ } ) ;
97+
13298 beforeEach ( ( ) => {
13399 jest . clearAllMocks ( ) ;
134100 } ) ;
@@ -148,20 +114,15 @@ describe('StudioNavigation', () => {
148114 expect ( screen . queryByRole ( 'button' , { name : / O p e n n a v i g a t i o n m e n u / i } ) ) . not . toBeInTheDocument ( ) ;
149115 } ) ;
150116
151- it ( 'should display correct guest dropdown options' , async ( ) => {
117+ it ( 'opens guest menu and shows options' , async ( ) => {
152118 const user = userEvent . setup ( ) ;
153119 renderComponent ( { } , null ) ;
154120
155121 const guestMenuTrigger = screen . getByRole ( 'button' , { name : / G u e s t m e n u / i } ) ;
156122 expect ( guestMenuTrigger ) . toBeVisible ( ) ;
157123 await user . click ( guestMenuTrigger ) ;
158-
159- // Verify buttons exist
160- expect ( screen . getByRole ( 'button' , { name : 'Sign in' } ) ) . toBeVisible ( ) ;
161- // Dropdown items are inside the button trigger scope in the DOM structure often,
162- // but strictly speaking, "Change language" appears in both Guest Dropdown and User Dropdown.
163- // Since we are in Guest View, we just check existence.
164- expect ( screen . getByRole ( 'button' , { name : 'Change language' } ) ) . toBeVisible ( ) ;
124+ expect ( screen . getByText ( 'Sign in' ) ) . toBeVisible ( ) ;
125+ expect ( screen . getByText ( 'Change language' ) ) . toBeVisible ( ) ;
165126 } ) ;
166127 } ) ;
167128
@@ -177,32 +138,40 @@ describe('StudioNavigation', () => {
177138 expect ( screen . getByRole ( 'button' , { name : / O p e n n a v i g a t i o n m e n u / i } ) ) . toBeVisible ( ) ;
178139 expect ( screen . getByRole ( 'button' , { name : / U s e r m e n u / i } ) ) . toBeVisible ( ) ;
179140 expect ( screen . getByText ( 'TestUser' ) ) . toBeVisible ( ) ;
180- expect ( screen . queryByRole ( 'link' , { name : / K o l i b r i L o g o / i } ) ) . not . toBeInTheDocument ( ) ;
181141 } ) ;
182142
183- it ( 'should show correct user dropdown options for non-admin' , ( ) => {
143+ it ( 'renders user menu options when clicked' , async ( ) => {
144+ const user = userEvent . setup ( ) ;
184145 renderComponent ( { } , defaultUser ) ;
185146
186- // We explicitly check items that are unique to the user menu or common
187- expect ( screen . getByRole ( 'button' , { name : 'Settings' } ) ) . toBeVisible ( ) ;
188- expect ( screen . getByRole ( 'button' , { name : 'Help and support' } ) ) . toBeVisible ( ) ;
189- expect ( screen . getByRole ( 'button' , { name : 'Change language' } ) ) . toBeVisible ( ) ;
190- expect ( screen . getByRole ( 'button' , { name : 'Sign out' } ) ) . toBeVisible ( ) ;
191-
192- expect ( screen . queryByRole ( 'button' , { name : 'Administration' } ) ) . not . toBeInTheDocument ( ) ;
147+ const userMenuTrigger = screen . getByRole ( 'button' , { name : / U s e r m e n u / i } ) ;
148+ expect ( userMenuTrigger ) . toBeVisible ( ) ;
149+ await user . click ( userMenuTrigger ) ;
150+
151+ expect ( screen . getByText ( 'Settings' ) ) . toBeVisible ( ) ;
152+ expect ( screen . getByText ( 'Sign out' ) ) . toBeVisible ( ) ;
153+ expect ( screen . getByText ( 'Help and support' ) ) . toBeVisible ( ) ;
154+ expect ( screen . getByText ( 'Change language' ) ) . toBeVisible ( ) ;
155+ expect ( screen . queryByText ( 'Administration' ) ) . not . toBeInTheDocument ( ) ;
193156 } ) ;
194157
195- it ( 'should show administration option for admin user' , ( ) => {
158+ it ( 'renders admin options for admin users' , async ( ) => {
159+ const user = userEvent . setup ( ) ;
196160 renderComponent ( { } , { ...defaultUser , is_admin : true } ) ;
197- expect ( screen . getByRole ( 'button' , { name : 'Administration' } ) ) . toBeVisible ( ) ;
161+
162+ const userMenuTrigger = screen . getByRole ( 'button' , { name : / U s e r m e n u / i } ) ;
163+ expect ( userMenuTrigger ) . toBeVisible ( ) ;
164+ await user . click ( userMenuTrigger ) ;
165+
166+ expect ( screen . getByText ( 'Administration' ) ) . toBeVisible ( ) ;
198167 } ) ;
199168
200- it ( 'should call logout action when Sign out is clicked ' , async ( ) => {
169+ it ( 'triggers logout action' , async ( ) => {
201170 const user = userEvent . setup ( ) ;
202171 renderComponent ( { } , defaultUser ) ;
203172
204- const logoutBtn = screen . getByRole ( 'button' , { name : 'Sign out' } ) ;
205- await user . click ( logoutBtn ) ;
173+ await user . click ( screen . getByRole ( 'button' , { name : / U s e r m e n u / i } ) ) ;
174+ await user . click ( screen . getByText ( 'Sign out' ) ) ;
206175
207176 expect ( mockActions . logout ) . toHaveBeenCalled ( ) ;
208177 } ) ;
@@ -219,61 +188,53 @@ describe('StudioNavigation', () => {
219188
220189 await user . click ( screen . getByRole ( 'button' , { name : / O p e n n a v i g a t i o n m e n u / i } ) ) ;
221190
222- const dialog = screen . getByRole ( 'dialog' , { name : / N a v i g a t i o n m e n u / i } ) ;
223- expect ( dialog ) . toBeVisible ( ) ;
224-
225- // FIX 1: Ambiguous "Kolibri Studio" text (Title in bar vs Title in panel)
226- // We check that the title exists specifically INSIDE the dialog
227- expect ( within ( dialog ) . getByText ( 'Kolibri Studio' ) ) . toBeVisible ( ) ;
191+ const sidePanel = screen . getByRole ( 'region' , { name : 'Navigation menu' } ) ;
192+ expect ( sidePanel ) . toBeVisible ( ) ;
193+ const panelScope = within ( sidePanel ) ;
194+ expect ( panelScope . getByText ( / K o l i b r i S t u d i o / ) ) . toBeVisible ( ) ;
228195 } ) ;
229-
230196 it ( 'should display correct navigation links in side panel' , async ( ) => {
231197 const user = userEvent . setup ( ) ;
232198 renderComponent ( { } , defaultUser ) ;
233199
234200 await user . click ( screen . getByRole ( 'button' , { name : / O p e n n a v i g a t i o n m e n u / i } ) ) ;
235201
236- const dialog = screen . getByRole ( 'dialog ' , { name : / N a v i g a t i o n m e n u / i } ) ;
202+ const sidePanel = screen . getByRole ( 'region ' , { name : ' Navigation menu' } ) ;
237203
238- // FIX 2: Ambiguous "Settings" text (Button in User Dropdown vs Link in Side Panel)
239-
240- const sidePanel = within ( dialog ) ;
204+ const panelScope = within ( sidePanel ) ;
241205
242- expect ( sidePanel . getByText ( 'Channels' ) ) . toBeVisible ( ) ;
243- expect ( sidePanel . getByText ( 'Settings' ) ) . toBeVisible ( ) ;
244- expect ( sidePanel . getByText ( 'Help and support' ) ) . toBeVisible ( ) ;
245- expect ( sidePanel . getByText ( 'Sign out' ) ) . toBeVisible ( ) ;
206+ expect ( panelScope . getByText ( 'Channels' ) ) . toBeVisible ( ) ;
207+ expect ( panelScope . getByText ( 'Settings' ) ) . toBeVisible ( ) ;
208+ expect ( panelScope . getByText ( 'Help and support' ) ) . toBeVisible ( ) ;
209+ expect ( panelScope . getByText ( 'Sign out' ) ) . toBeVisible ( ) ;
246210 } ) ;
247211
248- it ( 'should trigger Language Modal from side panel' , async ( ) => {
249- const user = userEvent . setup ( ) ;
250- renderComponent ( { } , defaultUser ) ;
251-
252- await user . click ( screen . getByRole ( 'button' , { name : / O p e n n a v i g a t i o n m e n u / i } ) ) ;
253-
254- const dialog = screen . getByRole ( 'dialog' , { name : / N a v i g a t i o n m e n u / i } ) ;
255- const sidePanel = within ( dialog ) ;
256-
257- // FIX 3: Ambiguous "Change language" text
258- // Scope to side panel
259- const changeLangLink = sidePanel . getByText ( 'Change language' ) ;
260- await user . click ( changeLangLink ) ;
212+ it ( 'opens language modal from side panel' , async ( ) => {
213+ const user = userEvent . setup ( ) ;
214+ renderComponent ( { } , defaultUser ) ;
261215
262- expect ( screen . getByText ( 'Language Modal' ) ) . toBeVisible ( ) ;
216+ await user . click ( screen . getByRole ( 'button' , { name : / O p e n n a v i g a t i o n m e n u / i } ) ) ;
217+ const sidePanel = screen . getByRole ( 'region' , { name : 'Navigation menu' } ) ;
218+ await user . click ( within ( sidePanel ) . getByText ( 'Change language' ) ) ;
219+ expect ( await screen . findByRole ( 'dialog' , { name : 'Change language' } ) ) . toBeVisible ( ) ;
220+ expect ( screen . getByLabelText ( 'English' ) ) . toBeVisible ( ) ;
221+
263222 } ) ;
264223 } ) ;
265224
266- describe ( 'navigation tabs ' , ( ) => {
225+ describe ( 'Tab Navigation ' , ( ) => {
267226 const tabs = [
268- { id : '1' , label : 'My Channels' , to : { name : 'channels' } } ,
269- { id : '2' , label : 'Content Library' , to : { name : 'library' } } ,
227+ { id : '1' , label : 'My Channels' , to : { name : 'channels' } , badgeValue : 0 } ,
228+ { id : '2' , label : 'Content Library' , to : { name : 'library' } , badgeValue : 5 } ,
270229 ] ;
271230
272- it ( 'should render navigation tabs when provided' , ( ) => {
273- renderComponent ( { tabs } , null ) ;
231+ it ( 'renders tabs correctly' , ( ) => {
232+ renderComponent ( { tabs } , { first_name : 'User' } ) ;
233+
274234
275235 expect ( screen . getByText ( 'My Channels' ) ) . toBeVisible ( ) ;
276236 expect ( screen . getByText ( 'Content Library' ) ) . toBeVisible ( ) ;
237+ expect ( screen . getByText ( '5' ) ) . toBeVisible ( ) ;
277238 } ) ;
278239 } ) ;
279240} ) ;
0 commit comments