66import type { IToken } from '../store/authtoken.ts'
77
88import { createTestingPinia } from '@pinia/testing'
9+ import { getByRole } from '@testing-library/vue'
910import { enableAutoUnmount , mount } from '@vue/test-utils'
1011import { afterEach , beforeEach , describe , expect , it , vi } from 'vitest'
1112import { nextTick } from 'vue'
1213
13- // AuthToken.vue reads window.OC.theme.productName at module evaluation time.
14- // vi.hoisted runs before imports, so this guarantees the property is set on
15- // the existing jsdom window before the SFC is first parsed.
16- vi . hoisted ( ( ) => {
17- ( window as unknown as { OC : { theme : { productName : string } } } ) . OC . theme = { productName : 'Nextcloud' }
18- } )
19-
2014// Mock @nextcloud /dialogs so the wipe action's showConfirmation call resolves
2115// synchronously in tests. Hoisted so it's installed before AuthToken.vue imports.
2216const showConfirmationMock = vi . hoisted ( ( ) => vi . fn ( ) )
@@ -31,6 +25,9 @@ import AuthTokenSetupDialog from './AuthTokenSetupDialog.vue'
3125import { TokenType , useAuthTokenStore } from '../store/authtoken.ts'
3226import { detect } from '../utils/userAgentDetect.ts'
3327
28+ // AuthToken.vue reads window.OC.theme.productName, which the test window lacks
29+ ; ( window as unknown as { OC : { theme : { productName : string } } } ) . OC . theme = { productName : 'Nextcloud' }
30+
3431enableAutoUnmount ( afterEach )
3532
3633function makeToken ( overrides : Partial < IToken > = { } ) : IToken {
@@ -46,13 +43,11 @@ function makeToken(overrides: Partial<IToken> = {}): IToken {
4643 }
4744}
4845
49- function mountAuthToken ( token : IToken ) {
46+ function mountAuthToken ( token : IToken , { stubs = { } , attachTo } : { stubs ?: Record < string , object | boolean > , attachTo ?: HTMLElement } = { } ) {
5047 return mount ( AuthToken , {
5148 props : { token } ,
49+ attachTo,
5250 global : {
53- mocks : {
54- t : ( _ : string , text : string ) => text ,
55- } ,
5651 stubs : {
5752 NcActions : true ,
5853 NcActionButton : true ,
@@ -61,6 +56,7 @@ function mountAuthToken(token: IToken) {
6156 NcDateTime : true ,
6257 NcIconSvgWrapper : true ,
6358 NcTextField : true ,
59+ ...stubs ,
6460 } ,
6561 plugins : [ createTestingPinia ( {
6662 createSpy : vi . fn ,
@@ -74,9 +70,6 @@ function mountDeleteDialog(token: IToken, open = true) {
7470 return mount ( AuthTokenDeleteDialog , {
7571 props : { token, open } ,
7672 global : {
77- mocks : {
78- t : ( _ : string , text : string ) => text ,
79- } ,
8073 stubs : {
8174 NcDialog : { template : '<div><slot /></div>' } ,
8275 } ,
@@ -153,32 +146,17 @@ describe('AuthToken revoke flow', () => {
153146
154147describe ( 'AuthToken rename focus' , ( ) => {
155148 function mountRenamable ( token : IToken ) {
156- return mount ( AuthToken , {
157- props : { token } ,
149+ return mountAuthToken ( token , {
158150 attachTo : document . body ,
159- global : {
160- mocks : {
161- t : ( _ : string , text : string ) => text ,
162- } ,
163- stubs : {
164- NcActions : { template : '<div><button>Device settings</button><slot /></div>' } ,
165- NcActionButton : true ,
166- NcActionCheckbox : true ,
167- NcButton : true ,
168- NcDateTime : true ,
169- NcIconSvgWrapper : true ,
170- NcTextField : { template : '<input>' , methods : { select ( ) { } } } ,
171- } ,
172- plugins : [ createTestingPinia ( {
173- createSpy : vi . fn ,
174- initialState : { 'auth-token' : { tokens : [ token ] } } ,
175- } ) ] ,
151+ stubs : {
152+ NcActions : { template : '<div><button>Device settings</button><slot /></div>' } ,
153+ NcTextField : { template : '<input>' , methods : { select ( ) { } } } ,
176154 } ,
177155 } )
178156 }
179157
180158 function actionsButton ( wrapper : ReturnType < typeof mountRenamable > ) {
181- return wrapper . findAll ( 'button' ) . find ( ( button ) => button . text ( ) === 'Device settings' ) ! . element
159+ return getByRole ( wrapper . element , 'button' , { name : 'Device settings' } )
182160 }
183161
184162 it ( 'returns focus to the actions button after cancelling with Escape' , async ( ) => {
@@ -190,7 +168,7 @@ describe('AuthToken rename focus', () => {
190168 await nextTick ( )
191169
192170 expect ( wrapper . find ( 'form' ) . exists ( ) ) . toBe ( false )
193- expect ( document . activeElement ) . toBe ( actionsButton ( wrapper ) )
171+ expect ( actionsButton ( wrapper ) ) . toHaveFocus ( )
194172 } )
195173
196174 it ( 'returns focus to the actions button after saving the new name' , async ( ) => {
@@ -204,56 +182,41 @@ describe('AuthToken rename focus', () => {
204182 await nextTick ( )
205183
206184 expect ( store . renameToken ) . toHaveBeenCalledWith ( token , token . name )
207- expect ( document . activeElement ) . toBe ( actionsButton ( wrapper ) )
185+ expect ( actionsButton ( wrapper ) ) . toHaveFocus ( )
208186 } )
209187
210- // Renaming always asks for the password, and that dialog takes focus until it closes.
211188 it ( 'returns focus to the actions button once the password confirmation closes' , async ( ) => {
212189 const token = makeToken ( )
213190 const wrapper = mountRenamable ( token )
214191 const store = useAuthTokenStore ( )
215192 const dialogField = document . createElement ( 'input' )
216193 document . body . appendChild ( dialogField )
217- let closeDialog = ( ) => { }
194+ const dialog = Promise . withResolvers < void > ( )
218195 vi . mocked ( store . renameToken ) . mockImplementation ( async ( ) => {
219196 await new Promise ( ( resolve ) => setTimeout ( resolve ) )
220197 dialogField . focus ( )
221- await new Promise < void > ( ( resolve ) => {
222- closeDialog = resolve
223- } )
198+ await dialog . promise
224199 dialogField . remove ( )
225200 return true
226201 } )
227202
228203 ; ( wrapper . vm as unknown as { startRename : ( ) => void } ) . startRename ( )
229204 await nextTick ( )
230205 await wrapper . find ( 'form' ) . trigger ( 'submit' )
231- await vi . waitFor ( ( ) => expect ( document . activeElement ) . toBe ( dialogField ) )
232- closeDialog ( )
206+ await vi . waitFor ( ( ) => expect ( dialogField ) . toHaveFocus ( ) , { interval : 1 } )
207+ dialog . resolve ( )
233208
234- await vi . waitFor ( ( ) => expect ( document . activeElement ) . toBe ( actionsButton ( wrapper ) ) )
209+ await vi . waitFor ( ( ) => expect ( actionsButton ( wrapper ) ) . toHaveFocus ( ) , { interval : 1 } )
235210 } )
236211} )
237212
238213describe ( 'AuthToken action labels' , ( ) => {
239214 it ( 'labels each action with its own text' , ( ) => {
240- const token = makeToken ( )
241- const wrapper = mount ( AuthToken , {
242- props : { token } ,
243- global : {
244- mocks : {
245- t : ( _ : string , text : string ) => text ,
246- } ,
247- stubs : {
248- NcActions : { template : '<ul><slot /></ul>' } ,
249- NcActionCheckbox : true ,
250- NcDateTime : true ,
251- NcIconSvgWrapper : true ,
252- } ,
253- plugins : [ createTestingPinia ( {
254- createSpy : vi . fn ,
255- initialState : { 'auth-token' : { tokens : [ token ] } } ,
256- } ) ] ,
215+ const wrapper = mountAuthToken ( makeToken ( ) , {
216+ stubs : {
217+ NcActions : { template : '<ul><slot /></ul>' } ,
218+ NcActionButton : false ,
219+ NcButton : false ,
257220 } ,
258221 } )
259222
@@ -263,26 +226,22 @@ describe('AuthToken action labels', () => {
263226} )
264227
265228describe ( 'AuthTokenSetupDialog QR code' , ( ) => {
266- // The login name and password are shown as text, so the QR code adds nothing for assistive technology
229+ // The credentials are shown as text, so the QR code is redundant for screen readers
267230 it ( 'hides the QR code from assistive technology' , async ( ) => {
268231 const wrapper = mount ( AuthTokenSetupDialog , {
269232 props : { token : { token : 'app-password' , loginName : 'admin' , deviceToken : makeToken ( ) } } ,
270233 global : {
271- mocks : {
272- t : ( _ : string , text : string ) => text ,
273- } ,
274234 stubs : {
275235 NcDialog : { template : '<div><slot /></div>' } ,
276236 NcIconSvgWrapper : true ,
277237 } ,
278238 } ,
279239 } )
280240
281- await wrapper . findAll ( 'button' ) . find ( ( button ) => button . text ( ) === 'Show QR code for mobile apps' ) ! . trigger ( 'click' )
241+ getByRole ( wrapper . element , 'button' , { name : 'Show QR code for mobile apps' } ) . click ( )
242+ await nextTick ( )
282243
283- const qrCode = wrapper . find ( 'canvas' )
284- expect ( qrCode . exists ( ) ) . toBe ( true )
285- expect ( qrCode . attributes ( 'aria-hidden' ) ) . toBe ( 'true' )
244+ expect ( wrapper . find ( 'canvas' ) . element ) . toHaveAttribute ( 'aria-hidden' , 'true' )
286245 } )
287246} )
288247
0 commit comments