Skip to content

Commit fd974f4

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

File tree

3 files changed

+134
-55
lines changed

3 files changed

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

0 commit comments

Comments
 (0)