aboutsummaryrefslogtreecommitdiffstats
path: root/apps/files/src/composables/useHotKeys.spec.ts
diff options
context:
space:
mode:
Diffstat (limited to 'apps/files/src/composables/useHotKeys.spec.ts')
-rw-r--r--apps/files/src/composables/useHotKeys.spec.ts213
1 files changed, 213 insertions, 0 deletions
diff --git a/apps/files/src/composables/useHotKeys.spec.ts b/apps/files/src/composables/useHotKeys.spec.ts
new file mode 100644
index 00000000000..9c001e8b5ff
--- /dev/null
+++ b/apps/files/src/composables/useHotKeys.spec.ts
@@ -0,0 +1,213 @@
+/*
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import type { Location } from 'vue-router'
+
+import { File, Folder, Permission, View } from '@nextcloud/files'
+import { enableAutoDestroy, mount } from '@vue/test-utils'
+import { describe, it, vi, expect, beforeEach, afterEach } from 'vitest'
+import { defineComponent, nextTick } from 'vue'
+import axios from '@nextcloud/axios'
+
+import { getPinia } from '../store/index.ts'
+import { useActiveStore } from '../store/active.ts'
+import { useFilesStore } from '../store/files'
+
+import { action as deleteAction } from '../actions/deleteAction.ts'
+import { action as favoriteAction } from '../actions/favoriteAction.ts'
+import { action as renameAction } from '../actions/renameAction.ts'
+import { action as sidebarAction } from '../actions/sidebarAction.ts'
+import { useHotKeys } from './useHotKeys.ts'
+import { useUserConfigStore } from '../store/userconfig.ts'
+
+// this is the mocked current route
+const route = vi.hoisted(() => ({
+ name: 'test',
+ params: {
+ fileId: 123,
+ },
+ query: {
+ openFile: 'false',
+ dir: '/parent/dir',
+ },
+}))
+
+// mocked router
+const router = vi.hoisted(() => ({
+ push: vi.fn<(route: Location) => void>(),
+}))
+
+vi.mock('../actions/sidebarAction.ts', { spy: true })
+vi.mock('../actions/deleteAction.ts', { spy: true })
+vi.mock('../actions/favoriteAction.ts', { spy: true })
+vi.mock('../actions/renameAction.ts', { spy: true })
+
+vi.mock('vue-router/composables', () => ({
+ useRoute: vi.fn(() => route),
+ useRouter: vi.fn(() => router),
+}))
+
+let file: File
+const view = {
+ id: 'files',
+ name: 'Files',
+} as View
+
+const TestComponent = defineComponent({
+ name: 'test',
+ setup() {
+ useHotKeys()
+ },
+ template: '<div />',
+})
+
+describe('HotKeysService testing', () => {
+ const activeStore = useActiveStore(getPinia())
+
+ let initialState: HTMLInputElement
+
+ enableAutoDestroy(afterEach)
+
+ afterEach(() => {
+ document.body.removeChild(initialState)
+ })
+
+ beforeEach(() => {
+ // Make sure the router is reset before each test
+ router.push.mockClear()
+
+ // Make sure the file is reset before each test
+ file = new File({
+ id: 2,
+ source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
+ owner: 'admin',
+ mime: 'text/plain',
+ permissions: Permission.ALL,
+ })
+
+ const root = new Folder({ owner: 'test', source: 'https://cloud.domain.com/remote.php/dav/files/admin/', id: 1, permissions: Permission.CREATE })
+ const files = useFilesStore(getPinia())
+ files.setRoot({ service: 'files', root })
+
+ // Setting the view first as it reset the active node
+ activeStore.activeView = view
+ activeStore.activeNode = file
+
+ window.OCA = { Files: { Sidebar: { open: () => {}, setActiveTab: () => {} } } }
+ initialState = document.createElement('input')
+ initialState.setAttribute('type', 'hidden')
+ initialState.setAttribute('id', 'initial-state-files_trashbin-config')
+ initialState.setAttribute('value', btoa(JSON.stringify({
+ allow_delete: true,
+ })))
+ document.body.appendChild(initialState)
+
+ mount(TestComponent)
+ })
+
+ it('Pressing d should open the sidebar once', () => {
+ dispatchEvent({ key: 'd', code: 'KeyD' })
+
+ // Modifier keys should not trigger the action
+ dispatchEvent({ key: 'd', code: 'KeyD', ctrlKey: true })
+ dispatchEvent({ key: 'd', code: 'KeyD', altKey: true })
+ dispatchEvent({ key: 'd', code: 'KeyD', shiftKey: true })
+ dispatchEvent({ key: 'd', code: 'KeyD', metaKey: true })
+
+ expect(sidebarAction.enabled).toHaveReturnedWith(true)
+ expect(sidebarAction.exec).toHaveBeenCalledOnce()
+ })
+
+ it('Pressing F2 should rename the file', () => {
+ dispatchEvent({ key: 'F2', code: 'F2' })
+
+ // Modifier keys should not trigger the action
+ dispatchEvent({ key: 'F2', code: 'F2', ctrlKey: true })
+ dispatchEvent({ key: 'F2', code: 'F2', altKey: true })
+ dispatchEvent({ key: 'F2', code: 'F2', shiftKey: true })
+ dispatchEvent({ key: 'F2', code: 'F2', metaKey: true })
+
+ expect(renameAction.enabled).toHaveReturnedWith(true)
+ expect(renameAction.exec).toHaveBeenCalledOnce()
+ })
+
+ it('Pressing s should toggle favorite', () => {
+ vi.spyOn(axios, 'post').mockImplementationOnce(() => Promise.resolve())
+ dispatchEvent({ key: 's', code: 'KeyS' })
+
+ // Modifier keys should not trigger the action
+ dispatchEvent({ key: 's', code: 'KeyS', ctrlKey: true })
+ dispatchEvent({ key: 's', code: 'KeyS', altKey: true })
+ dispatchEvent({ key: 's', code: 'KeyS', shiftKey: true })
+ dispatchEvent({ key: 's', code: 'KeyS', metaKey: true })
+
+ expect(favoriteAction.enabled).toHaveReturnedWith(true)
+ expect(favoriteAction.exec).toHaveBeenCalledOnce()
+ })
+
+ it('Pressing Delete should delete the file', async () => {
+ // @ts-expect-error unit testing
+ vi.spyOn(deleteAction._action, 'exec').mockResolvedValue(() => true)
+
+ dispatchEvent({ key: 'Delete', code: 'Delete' })
+
+ // Modifier keys should not trigger the action
+ dispatchEvent({ key: 'Delete', code: 'Delete', ctrlKey: true })
+ dispatchEvent({ key: 'Delete', code: 'Delete', altKey: true })
+ dispatchEvent({ key: 'Delete', code: 'Delete', shiftKey: true })
+ dispatchEvent({ key: 'Delete', code: 'Delete', metaKey: true })
+
+ expect(deleteAction.enabled).toHaveReturnedWith(true)
+ expect(deleteAction.exec).toHaveBeenCalledOnce()
+ })
+
+ it('Pressing alt+up should go to parent directory', () => {
+ expect(router.push).toHaveBeenCalledTimes(0)
+ dispatchEvent({ key: 'ArrowUp', code: 'ArrowUp', altKey: true })
+
+ expect(router.push).toHaveBeenCalledOnce()
+ expect(router.push.mock.calls[0][0].query?.dir).toBe('/parent')
+ })
+
+ it('Pressing v should toggle grid view', async () => {
+ vi.spyOn(axios, 'put').mockImplementationOnce(() => Promise.resolve())
+
+ const userConfigStore = useUserConfigStore(getPinia())
+ userConfigStore.userConfig.grid_view = false
+ expect(userConfigStore.userConfig.grid_view).toBe(false)
+
+ dispatchEvent({ key: 'v', code: 'KeyV' })
+ expect(userConfigStore.userConfig.grid_view).toBe(true)
+ })
+
+ it.each([
+ ['ctrlKey'],
+ ['altKey'],
+ // those meta keys are still triggering...
+ // ['shiftKey'],
+ // ['metaKey']
+ ])('Pressing v with modifier key %s should not toggle grid view', async (modifier: string) => {
+ vi.spyOn(axios, 'put').mockImplementationOnce(() => Promise.resolve())
+
+ const userConfigStore = useUserConfigStore(getPinia())
+ userConfigStore.userConfig.grid_view = false
+ expect(userConfigStore.userConfig.grid_view).toBe(false)
+
+ dispatchEvent(new KeyboardEvent('keydown', { key: 'v', code: 'KeyV', [modifier]: true }))
+
+ await nextTick()
+
+ expect(userConfigStore.userConfig.grid_view).toBe(false)
+ })
+})
+
+/**
+ * Helper to dispatch the correct event.
+ *
+ * @param init - KeyboardEvent options
+ */
+function dispatchEvent(init: KeyboardEventInit) {
+ document.body.dispatchEvent(new KeyboardEvent('keydown', { ...init, bubbles: true }))
+}