Skip to content

Commit 7c3cdc0

Browse files
committed
fix(a11y): label account QR code button
1 parent 5508414 commit 7c3cdc0

3 files changed

Lines changed: 146 additions & 2 deletions

File tree

core/src/components/AccountMenu/AccountMenuProfileEntry.vue

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@
1616
{{ name }}
1717
</template>
1818
<template v-if="canCreateAppToken" #extra-actions>
19-
<NcButton variant="secondary" @click="handleQrCodeClick">
19+
<NcButton
20+
:aria-label="t('core', 'Show QR code for mobile app login')"
21+
variant="secondary"
22+
@click="handleQrCodeClick">
2023
<template #icon>
2124
<IconQrcodeScan :size="20" />
2225
</template>
@@ -36,6 +39,7 @@ import axios from '@nextcloud/axios'
3639
import { getCapabilities } from '@nextcloud/capabilities'
3740
import { subscribe, unsubscribe } from '@nextcloud/event-bus'
3841
import { loadState } from '@nextcloud/initial-state'
42+
import { t } from '@nextcloud/l10n'
3943
import { addPasswordConfirmationInterceptors, PwdConfirmationMode } from '@nextcloud/password-confirmation'
4044
import { generateUrl } from '@nextcloud/router'
4145
import { spawnDialog } from '@nextcloud/vue/functions/dialog'
@@ -88,8 +92,9 @@ export default defineComponent({
8892
setup() {
8993
return {
9094
canCreateAppToken,
91-
profileEnabled,
9295
displayName: getCurrentUser()!.displayName,
96+
profileEnabled,
97+
t,
9398
}
9499
},
95100
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/*!
2+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import { mount } from '@vue/test-utils'
7+
import { beforeEach, describe, expect, it, vi } from 'vitest'
8+
9+
const capabilities = vi.hoisted(() => ({
10+
getCapabilities: vi.fn(),
11+
}))
12+
vi.mock('@nextcloud/capabilities', () => capabilities)
13+
14+
vi.mock('@nextcloud/auth', () => ({
15+
getCurrentUser: () => ({ uid: 'user', displayName: 'User' }),
16+
}))
17+
18+
vi.mock('@nextcloud/axios', () => ({ default: { post: vi.fn() } }))
19+
20+
vi.mock('@nextcloud/event-bus', () => ({
21+
subscribe: vi.fn(),
22+
unsubscribe: vi.fn(),
23+
}))
24+
25+
vi.mock('@nextcloud/initial-state', () => ({
26+
loadState: vi.fn((_app: string, key: string, fallback: unknown) => {
27+
if (key === 'profileEnabled') {
28+
return { profileEnabled: false }
29+
}
30+
return fallback
31+
}),
32+
}))
33+
34+
vi.mock('@nextcloud/l10n', () => ({
35+
getLanguage: () => 'en',
36+
t: (_app: string, text: string) => text,
37+
}))
38+
39+
vi.mock('@nextcloud/password-confirmation', () => ({
40+
addPasswordConfirmationInterceptors: vi.fn(),
41+
PwdConfirmationMode: {
42+
Strict: 0,
43+
},
44+
}))
45+
46+
vi.mock('@nextcloud/router', () => ({
47+
generateUrl: (path: string) => path,
48+
}))
49+
50+
vi.mock('@nextcloud/vue/components/NcButton', () => ({
51+
default: {
52+
name: 'NcButton',
53+
inheritAttrs: false,
54+
render(h) {
55+
return h('button', {
56+
attrs: this.$attrs,
57+
on: {
58+
click: (event: MouseEvent) => this.$emit('click', event),
59+
},
60+
}, this.$slots.icon)
61+
},
62+
},
63+
}))
64+
65+
vi.mock('@nextcloud/vue/components/NcListItem', () => ({
66+
default: {
67+
name: 'NcListItem',
68+
render(h) {
69+
return h('li', [
70+
this.$slots.subname,
71+
this.$slots['extra-actions'],
72+
this.$slots.indicator,
73+
])
74+
},
75+
},
76+
}))
77+
78+
vi.mock('@nextcloud/vue/components/NcLoadingIcon', () => ({
79+
default: {
80+
name: 'NcLoadingIcon',
81+
render(h) {
82+
return h('span')
83+
},
84+
},
85+
}))
86+
87+
vi.mock('@nextcloud/vue/functions/dialog', () => ({
88+
spawnDialog: vi.fn(),
89+
}))
90+
91+
vi.mock('../../components/AccountMenu/AccountQRLoginDialog.vue', () => ({
92+
default: {
93+
name: 'AccountQRLoginDialog',
94+
render(h) {
95+
return h('div')
96+
},
97+
},
98+
}))
99+
100+
vi.mock('vue-material-design-icons/QrcodeScan.vue', () => ({
101+
default: {
102+
name: 'IconQrcodeScan',
103+
render(h) {
104+
return h('span')
105+
},
106+
},
107+
}))
108+
109+
describe('core: AccountMenuProfileEntry', () => {
110+
beforeEach(() => {
111+
vi.resetModules()
112+
capabilities.getCapabilities.mockReturnValue({
113+
core: {
114+
'can-create-app-token': true,
115+
},
116+
})
117+
})
118+
119+
it('labels the QR code button for assistive technologies', async () => {
120+
const AccountMenuProfileEntry = (await import('../../components/AccountMenu/AccountMenuProfileEntry.vue')).default
121+
const wrapper = mount(AccountMenuProfileEntry, {
122+
propsData: {
123+
id: 'profile',
124+
name: 'Profile',
125+
href: '/settings/user',
126+
active: false,
127+
},
128+
})
129+
130+
expect(wrapper.get('button').attributes('aria-label')).toBe('Show QR code for mobile app login')
131+
})
132+
})

core/src/tests/components/AppMenu.spec.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,13 @@ async function openPopover(wrapper: ReturnType<typeof mount>) {
109109
}
110110

111111
describe('core: AppMenu', () => {
112+
it('labels the app menu trigger buttons for assistive technologies', () => {
113+
const wrapper = mount(AppMenu, { attachTo: document.body })
114+
115+
expect(wrapper.get('.app-menu__waffle').attributes('aria-label')).toBe('Open apps menu')
116+
expect(wrapper.get('.app-menu__current-app').attributes('aria-label')).toBe('Open apps menu, currently in Files')
117+
})
118+
112119
it('renders one AppItem per app in the list, plus the "App store" tile for non-admins', async () => {
113120
const wrapper = mount(AppMenu, { attachTo: document.body })
114121
await openPopover(wrapper)

0 commit comments

Comments
 (0)