Skip to content

Commit b63aca7

Browse files
committed
test: move integration testing of hotkeys to Cypress
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
1 parent 24b3059 commit b63aca7

File tree

3 files changed

+139
-55
lines changed

3 files changed

+139
-55
lines changed

apps/files/src/composables/useHotKeys.spec.ts

Lines changed: 25 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,14 @@
44
*/
55

66
import type { View } from '@nextcloud/files'
7-
import type { Mock } from 'vitest'
87
import type { Location } from 'vue-router'
98

109
import axios from '@nextcloud/axios'
11-
import { File, Folder, Permission } from '@nextcloud/files'
10+
import { File, Folder, Permission, registerFileAction } from '@nextcloud/files'
1211
import { enableAutoDestroy, mount } from '@vue/test-utils'
13-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
12+
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
1413
import { defineComponent, nextTick } from 'vue'
1514
import { action as deleteAction } from '../actions/deleteAction.ts'
16-
import { action as favoriteAction } from '../actions/favoriteAction.ts'
17-
import { action as renameAction } from '../actions/renameAction.ts'
18-
import { action as sidebarAction } from '../actions/sidebarAction.ts'
1915
import { useActiveStore } from '../store/active.ts'
2016
import { useFilesStore } from '../store/files.ts'
2117
import { getPinia } from '../store/index.ts'
@@ -63,10 +59,23 @@ const TestComponent = defineComponent({
6359
template: '<div />',
6460
})
6561

62+
beforeAll(() => {
63+
// @ts-expect-error mocking for tests
64+
window.OCP ??= {}
65+
// @ts-expect-error mocking for tests
66+
window.OCP.Files ??= {}
67+
// @ts-expect-error mocking for tests
68+
window.OCP.Files.Router ??= {
69+
...router,
70+
goToRoute: vi.fn(),
71+
}
72+
})
73+
6674
describe('HotKeysService testing', () => {
6775
const activeStore = useActiveStore(getPinia())
6876

6977
let initialState: HTMLInputElement
78+
let component: ReturnType<typeof mount>
7079

7180
enableAutoDestroy(afterEach)
7281

@@ -114,54 +123,15 @@ describe('HotKeysService testing', () => {
114123
})))
115124
document.body.appendChild(initialState)
116125

117-
mount(TestComponent)
118-
})
119-
120-
it('Pressing d should open the sidebar once', () => {
121-
dispatchEvent({ key: 'd', code: 'KeyD' })
122-
123-
// Modifier keys should not trigger the action
124-
dispatchEvent({ key: 'd', code: 'KeyD', ctrlKey: true })
125-
dispatchEvent({ key: 'd', code: 'KeyD', altKey: true })
126-
dispatchEvent({ key: 'd', code: 'KeyD', shiftKey: true })
127-
dispatchEvent({ key: 'd', code: 'KeyD', metaKey: true })
128-
129-
expect(sidebarAction.enabled).toHaveReturnedWith(true)
130-
expect(sidebarAction.exec).toHaveBeenCalledOnce()
131-
})
132-
133-
it('Pressing F2 should rename the file', () => {
134-
dispatchEvent({ key: 'F2', code: 'F2' })
135-
136-
// Modifier keys should not trigger the action
137-
dispatchEvent({ key: 'F2', code: 'F2', ctrlKey: true })
138-
dispatchEvent({ key: 'F2', code: 'F2', altKey: true })
139-
dispatchEvent({ key: 'F2', code: 'F2', shiftKey: true })
140-
dispatchEvent({ key: 'F2', code: 'F2', metaKey: true })
141-
142-
expect(renameAction.enabled).toHaveReturnedWith(true)
143-
expect(renameAction.exec).toHaveBeenCalledOnce()
126+
component = mount(TestComponent)
144127
})
145128

146-
it('Pressing s should toggle favorite', () => {
147-
(favoriteAction.enabled as Mock).mockReturnValue(true);
148-
(favoriteAction.exec as Mock).mockImplementationOnce(() => Promise.resolve(null))
129+
// tests for register action handling
149130

150-
vi.spyOn(axios, 'post').mockImplementationOnce(() => Promise.resolve())
151-
dispatchEvent({ key: 's', code: 'KeyS' })
152-
153-
// Modifier keys should not trigger the action
154-
dispatchEvent({ key: 's', code: 'KeyS', ctrlKey: true })
155-
dispatchEvent({ key: 's', code: 'KeyS', altKey: true })
156-
dispatchEvent({ key: 's', code: 'KeyS', shiftKey: true })
157-
dispatchEvent({ key: 's', code: 'KeyS', metaKey: true })
158-
159-
expect(favoriteAction.exec).toHaveBeenCalledOnce()
160-
})
161-
162-
it('Pressing Delete should delete the file', async () => {
163-
// @ts-expect-error unit testing - private method access
164-
vi.spyOn(deleteAction._action, 'exec').mockResolvedValue(() => true)
131+
it('registeres actions', () => {
132+
component.destroy()
133+
registerFileAction(deleteAction)
134+
component = mount(TestComponent)
165135

166136
dispatchEvent({ key: 'Delete', code: 'Delete' })
167137

@@ -175,6 +145,8 @@ describe('HotKeysService testing', () => {
175145
expect(deleteAction.exec).toHaveBeenCalledOnce()
176146
})
177147

148+
// actions implemented by the composable
149+
178150
it('Pressing alt+up should go to parent directory', () => {
179151
expect(router.push).toHaveBeenCalledTimes(0)
180152
dispatchEvent({ key: 'ArrowUp', code: 'ArrowUp', altKey: true })
@@ -197,9 +169,8 @@ describe('HotKeysService testing', () => {
197169
it.each([
198170
['ctrlKey'],
199171
['altKey'],
200-
// those meta keys are still triggering...
201-
// ['shiftKey'],
202-
// ['metaKey']
172+
['shiftKey'],
173+
['metaKey'],
203174
])('Pressing v with modifier key %s should not toggle grid view', async (modifier: string) => {
204175
vi.spyOn(axios, 'put').mockImplementationOnce(() => Promise.resolve())
205176

cypress/e2e/files/FilesUtils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import type { User } from '@nextcloud/e2e-test-server/cypress'
77

88
const ACTION_COPY_MOVE = 'move-copy'
99

10-
export const getRowForFileId = (fileid: number) => cy.get(`[data-cy-files-list-row-fileid="${fileid}"]`)
10+
export const getRowForFileId = (fileid: string | number) => cy.get(`[data-cy-files-list-row-fileid="${fileid}"]`)
1111
export const getRowForFile = (filename: string) => cy.get(`[data-cy-files-list-row-name="${CSS.escape(filename)}"]`)
1212

1313
export const getActionsForFileId = (fileid: number) => getRowForFileId(fileid).find('[data-cy-files-list-row-actions]')

cypress/e2e/files/hotkeys.cy.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import { getRowForFileId } from './FilesUtils.ts'
7+
8+
describe('Files hotkey handling', () => {
9+
before(() => {
10+
cy.createRandomUser().then((user) => {
11+
cy.mkdir(user, '/abcd')
12+
cy.mkdir(user, '/zyx')
13+
cy.rm(user, '/welcome.txt')
14+
cy.login(user)
15+
})
16+
})
17+
18+
beforeEach(() => cy.visit('/apps/files'))
19+
20+
it('Pressing "arrow down" should go to first file', () => {
21+
cy.get('[data-cy-files-list]')
22+
.press(Cypress.Keyboard.Keys.DOWN)
23+
24+
cy.url()
25+
.should('match', /\/apps\/files\/files\/\d+/)
26+
.then((url) => new URL(url).pathname.split('/').at(-1))
27+
.then((fileId) => getRowForFileId(fileId)
28+
.should('exist')
29+
.and('have.attr', 'data-cy-files-list-row-name', 'abcd'))
30+
})
31+
32+
it('Pressing "arrow up" should go to first file', () => {
33+
cy.get('[data-cy-files-list]')
34+
.press(Cypress.Keyboard.Keys.UP)
35+
36+
cy.url()
37+
.should('match', /\/apps\/files\/files\/\d+/)
38+
.then((url) => new URL(url).pathname.split('/').at(-1))
39+
.then((fileId) => getRowForFileId(fileId)
40+
.should('exist')
41+
.and('have.attr', 'data-cy-files-list-row-name', 'zyx'))
42+
})
43+
44+
it('Pressing D should open the sidebar once', () => {
45+
activateFirstRow()
46+
cy.get('[data-cy-files-list]')
47+
.press('d')
48+
49+
cy.get('[data-cy-sidebar]')
50+
.should('exist')
51+
.and('be.visible')
52+
})
53+
54+
it('Pressing F2 should rename the file', () => {
55+
activateFirstRow()
56+
cy.get('[data-cy-files-list]')
57+
.should('exist')
58+
.then(($el) => {
59+
const el = $el.get(0)
60+
// manually dispatch as Cypress refuses to press F-keys for "security reasons"
61+
cy.log('Dispatching F2 keydown/keyup events')
62+
el.dispatchEvent(new KeyboardEvent('keydown', { key: 'F2', code: 'F2', bubbles: true }))
63+
el.dispatchEvent(new KeyboardEvent('keyup', { key: 'F2', code: 'F2', bubbles: true }))
64+
el.dispatchEvent(new KeyboardEvent('keypress', { key: 'F2', code: 'F2', bubbles: true }))
65+
})
66+
67+
cy.get('[data-cy-files-list-row-name]')
68+
.first()
69+
.findByRole('textbox', { name: /Folder name/ })
70+
.should('exist')
71+
})
72+
73+
it('Pressing S should toggle favorite', () => {
74+
activateFirstRow()
75+
cy.get('[data-cy-files-list]')
76+
.press('s')
77+
78+
cy.get('[data-cy-files-list-row-name]')
79+
.first()
80+
.as('firstRow')
81+
.findByRole('img', { name: /Favorite/ })
82+
.should('exist')
83+
84+
cy.get('[data-cy-files-list]')
85+
.press('s')
86+
87+
cy.get('@firstRow')
88+
.findByRole('img', { name: /Favorite/ })
89+
.should('not.exist')
90+
})
91+
92+
it('Pressing DELETE should delete the folder', () => {
93+
activateFirstRow()
94+
cy.get('td[data-cy-files-list-row-name]')
95+
.should('have.length', 2)
96+
97+
cy.get('[data-cy-files-list]')
98+
.press(Cypress.Keyboard.Keys.DELETE)
99+
100+
cy.get('td[data-cy-files-list-row-name]')
101+
.should('have.length', 1)
102+
})
103+
})
104+
105+
/**
106+
* Activates the first row in the files list by simulating a press of the down arrow key.
107+
*/
108+
function activateFirstRow() {
109+
cy.get('[data-cy-files-list]')
110+
.press(Cypress.Keyboard.Keys.DOWN)
111+
cy.url()
112+
.should('match', /\/apps\/files\/files\/\d+/)
113+
}

0 commit comments

Comments
 (0)