Skip to content

Commit 75ba5b8

Browse files
Final updates
1 parent bb8a602 commit 75ba5b8

3 files changed

Lines changed: 94 additions & 143 deletions

File tree

contentcuration/contentcuration/frontend/shared/views/StudioNavigationTab.vue

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
<span
2020
v-if="showBadge"
2121
class="studio-navigation-tab-badge"
22+
:style="badgeStyles"
2223
>
2324
{{ $formatNumber(badgeValue) }}
2425
</span>
@@ -69,6 +70,12 @@
6970
color: this.$themeTokens.text,
7071
};
7172
},
73+
badgeStyles() {
74+
return {
75+
color: this.$themeTokens.surface,
76+
backgroundColor: this.$themeTokens.text,
77+
};
78+
},
7279
indicatorStyles() {
7380
return {
7481
backgroundColor: this.$themeTokens.surface,
@@ -140,8 +147,6 @@
140147
width: 22px;
141148
height: 22px;
142149
font-size: 14px;
143-
color: white;
144-
background-color: black;
145150
border-radius: 50%;
146151
transition: 0.3s;
147152
}

contentcuration/contentcuration/frontend/shared/views/__tests__/StudioNavigation.spec.js

Lines changed: 85 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import StudioNavigation from '../StudioNavigation';
77

88
const localVue = createLocalVue();
99
localVue.use(Vuex);
10+
localVue.use(VueRouter);
11+
12+
1013

1114
global.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

13184
describe('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: /Open navigation menu/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: /Guest menu/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: /Open navigation menu/i })).toBeVisible();
178139
expect(screen.getByRole('button', { name: /User menu/i })).toBeVisible();
179140
expect(screen.getByText('TestUser')).toBeVisible();
180-
expect(screen.queryByRole('link', { name: /Kolibri Logo/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: /User menu/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: /User menu/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: /User menu/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: /Open navigation menu/i }));
221190

222-
const dialog = screen.getByRole('dialog', { name: /Navigation menu/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(/Kolibri Studio/)).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: /Open navigation menu/i }));
235201

236-
const dialog = screen.getByRole('dialog', { name: /Navigation menu/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: /Open navigation menu/i }));
253-
254-
const dialog = screen.getByRole('dialog', { name: /Navigation menu/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: /Open navigation menu/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
});

contentcuration/contentcuration/frontend/shared/views/__tests__/StudioNavigationOption.spec.js

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { render, screen, cleanup } from '@testing-library/vue';
2-
import userEvent from '@testing-library/user-event';
2+
33
import { createLocalVue } from '@vue/test-utils';
44
import VueRouter from 'vue-router';
55
import StudioNavigationOption from '../StudioNavigationOption.vue';
@@ -41,7 +41,7 @@ const renderComponent = (props = {}, options = {}) => {
4141
};
4242

4343
describe('StudioNavigationOption', () => {
44-
const user = userEvent.setup();
44+
4545

4646
beforeEach(() => {
4747
jest.clearAllMocks();
@@ -89,19 +89,4 @@ describe('StudioNavigationOption', () => {
8989
});
9090
});
9191

92-
describe('Click handling', () => {
93-
it('should emit select event when clicked without link', async () => {
94-
const { emitted } = renderComponent({
95-
label: 'Logout',
96-
icon: validIcons.logout,
97-
link: null,
98-
});
99-
100-
const menuItem = screen.getByRole('menuitem', { name: 'Logout' });
101-
await user.click(menuItem);
102-
103-
expect(emitted().select).toHaveLength(1);
104-
expect(emitted().click).toHaveLength(1);
105-
});
106-
});
10792
});

0 commit comments

Comments
 (0)