From 54e0393252d9334f1074869affcab1f24c113109 Mon Sep 17 00:00:00 2001 From: ColumbusLabs <287001685+ColumbusLabs@users.noreply.github.com> Date: Wed, 22 Jul 2026 15:25:43 -0400 Subject: [PATCH 1/2] test: migrate FilesList header specs to testing Pinia Signed-off-by: ColumbusLabs <287001685+ColumbusLabs@users.noreply.github.com> --- .../FilesList/FilesListTableHeader.spec.ts | 107 +++++++++++------- .../FilesListTableHeaderButton.spec.ts | 79 ++++++++----- 2 files changed, 113 insertions(+), 73 deletions(-) diff --git a/src/tests/views/FilesList/FilesListTableHeader.spec.ts b/src/tests/views/FilesList/FilesListTableHeader.spec.ts index d195b28a60..53d7fb9575 100644 --- a/src/tests/views/FilesList/FilesListTableHeader.spec.ts +++ b/src/tests/views/FilesList/FilesListTableHeader.spec.ts @@ -3,13 +3,13 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { createTestingPinia } from '@pinia/testing' import { mount } from '@vue/test-utils' -import { createPinia, setActivePinia } from 'pinia' +import { setActivePinia } from 'pinia' +import { beforeEach, describe, expect, it, vi } from 'vitest' import FilesListTableHeader from '../../../views/FilesList/FilesListTableHeader.vue' -import { useFilesStore } from '../../../store/files.js' -import { useFilesSortingStore } from '../../../store/filesSorting.js' + import { useSelectionStore } from '../../../store/selection.js' type Column = { @@ -94,22 +94,54 @@ const NcCheckboxRadioSwitchStub = { template: '', } -function createWrapper(filesCount = 2, options: { resetFiles?: boolean, canRequestSign?: boolean } = {}) { - const filesStore = useFilesStore() - filesStore.ordered = Array.from({ length: filesCount }, (_, index) => index + 1) as typeof filesStore.ordered - if (options.resetFiles !== false) { - filesStore.files = Object.fromEntries(Array.from({ length: filesCount }, (_, index) => { - const id = index + 1 - return [id, { id }] - })) as typeof filesStore.files - } - filesStore.canRequestSign = options.canRequestSign ?? true +type FilesState = Record + +type HeaderState = { + canRequestSign?: boolean + files?: FilesState + selected?: number[] + sortingMode?: string + sortingDirection?: string +} + +/** + * Mount the table header with isolated store state. + * + * @param filesCount Number of files represented in the test. + * @param state Initial state for the files, selection, and sorting stores. + */ +function createWrapper(filesCount = 2, state: HeaderState = {}) { + const ordered = Array.from({ length: filesCount }, (_, index) => index + 1) + const files = state.files ?? Object.fromEntries(ordered.map(id => [id, { id }])) + const pinia = createTestingPinia({ + createSpy: vi.fn, + stubActions: false, + initialState: { + files: { + ordered, + files, + canRequestSign: state.canRequestSign ?? true, + }, + selection: { + selected: state.selected ?? [], + }, + filesSorting: { + ...(state.sortingMode !== undefined && { sortingMode: state.sortingMode }), + ...(state.sortingDirection !== undefined && { sortingDirection: state.sortingDirection }), + }, + }, + }) + setActivePinia(pinia) return mount(FilesListTableHeader, { props: { nodes: Array.from({ length: filesCount }, (_, index) => ({ id: index + 1, basename: `file${index + 1}.pdf` })), }, global: { + plugins: [pinia], stubs: { NcCheckboxRadioSwitch: NcCheckboxRadioSwitchStub, FilesListTableHeaderButton: { @@ -123,7 +155,6 @@ function createWrapper(filesCount = 2, options: { resetFiles?: boolean, canReque describe('FilesListTableHeader.vue', () => { beforeEach(() => { - setActivePinia(createPinia()) vi.clearAllMocks() }) @@ -245,13 +276,13 @@ describe('FilesListTableHeader.vue', () => { }) it('selects only deletable files when using select all', async () => { - const filesStore = useFilesStore() - filesStore.files = { - 1: { id: 1 }, - 2: { id: 2, requested_by: { userId: 'someone-else' } }, - 3: { id: 3 }, - } - const wrapper = createWrapper(3, { resetFiles: false }) + const wrapper = createWrapper(3, { + files: { + 1: { id: 1 }, + 2: { id: 2, requested_by: { userId: 'someone-else' } }, + 3: { id: 3 }, + }, + }) const stub = wrapper.findComponent(NcCheckboxRadioSwitchStub) const selectionStore = useSelectionStore() @@ -261,20 +292,14 @@ describe('FilesListTableHeader.vue', () => { }) it('sets modelValue to true when all files are selected', async () => { - const wrapper = createWrapper(2) - const selectionStore = useSelectionStore() - - selectionStore.set([1, 2]) + const wrapper = createWrapper(2, { selected: [1, 2] }) await wrapper.vm.$nextTick() expect(wrapper.findComponent(NcCheckboxRadioSwitchStub).props('modelValue')).toBe(true) }) it('sets indeterminate when only some files are selected', async () => { - const wrapper = createWrapper(2) - const selectionStore = useSelectionStore() - - selectionStore.set([1]) + const wrapper = createWrapper(2, { selected: [1] }) await wrapper.vm.$nextTick() const stub = wrapper.findComponent(NcCheckboxRadioSwitchStub) @@ -299,24 +324,22 @@ describe('FilesListTableHeader.vue', () => { expect(vm.ariaSortForMode('actions', false)).toBeNull() }) - it('returns descending when the active mode is descending', async () => { - const wrapper = createWrapper() + it('returns descending when the active mode is descending', () => { + const wrapper = createWrapper(2, { + sortingMode: 'created_at', + sortingDirection: 'desc', + }) const vm = wrapper.vm as FilesListTableHeaderVm - const sortingStore = useFilesSortingStore() - sortingStore.sortingMode = 'created_at' - sortingStore.sortingDirection = 'desc' - await wrapper.vm.$nextTick() expect(vm.ariaSortForMode('created_at')).toBe('descending') }) - it('returns ascending when the active mode is ascending', async () => { - const wrapper = createWrapper() + it('returns ascending when the active mode is ascending', () => { + const wrapper = createWrapper(2, { + sortingMode: 'status', + sortingDirection: 'asc', + }) const vm = wrapper.vm as FilesListTableHeaderVm - const sortingStore = useFilesSortingStore() - sortingStore.sortingMode = 'status' - sortingStore.sortingDirection = 'asc' - await wrapper.vm.$nextTick() expect(vm.ariaSortForMode('status')).toBe('ascending') }) diff --git a/src/tests/views/FilesList/FilesListTableHeaderButton.spec.ts b/src/tests/views/FilesList/FilesListTableHeaderButton.spec.ts index b9c82973c4..bef2f55146 100644 --- a/src/tests/views/FilesList/FilesListTableHeaderButton.spec.ts +++ b/src/tests/views/FilesList/FilesListTableHeaderButton.spec.ts @@ -3,11 +3,13 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { createTestingPinia } from '@pinia/testing' import { mount } from '@vue/test-utils' -import { createPinia, setActivePinia } from 'pinia' +import { setActivePinia } from 'pinia' +import { beforeEach, describe, expect, it, vi } from 'vitest' import FilesListTableHeaderButton from '../../../views/FilesList/FilesListTableHeaderButton.vue' + import { useFilesSortingStore } from '../../../store/filesSorting.js' vi.mock('@nextcloud/event-bus', () => ({ @@ -57,10 +59,31 @@ const NcButtonStub = { template: '', } -function createWrapper(mode = 'size', name = 'Size') { +type FilesSortingState = { + sortingMode?: string + sortingDirection?: string +} + +/** + * Mount the header button with isolated sorting-store state. + * + * @param mode Sort key represented by the button. + * @param name Visible label for the button. + * @param sortingState Initial state for the sorting store. + */ +function createWrapper(mode = 'size', name = 'Size', sortingState: FilesSortingState = {}) { + const pinia = createTestingPinia({ + createSpy: vi.fn, + initialState: { + filesSorting: sortingState, + }, + }) + setActivePinia(pinia) + return mount(FilesListTableHeaderButton, { props: { name, mode }, global: { + plugins: [pinia], stubs: { NcButton: NcButtonStub, NcIconSvgWrapper: true, @@ -71,7 +94,6 @@ function createWrapper(mode = 'size', name = 'Size') { describe('FilesListTableHeaderButton.vue', () => { beforeEach(() => { - setActivePinia(createPinia()) vi.clearAllMocks() }) @@ -82,11 +104,10 @@ describe('FilesListTableHeaderButton.vue', () => { }) it('computes descending state when the same column is sorted descending', () => { - const sortingStore = useFilesSortingStore() - sortingStore.sortingMode = 'size' - sortingStore.sortingDirection = 'desc' - - const wrapper = createWrapper('size') + const wrapper = createWrapper('size', 'Size', { + sortingMode: 'size', + sortingDirection: 'desc', + }) expect(wrapper.vm.isAscending).toBe(false) }) @@ -97,31 +118,29 @@ describe('FilesListTableHeaderButton.vue', () => { }) it('triggers the store sort toggle for the current mode', async () => { - const sortingStore = useFilesSortingStore() - const spy = vi.spyOn(sortingStore, 'toggleSortBy') const wrapper = createWrapper('size') + const sortingStore = useFilesSortingStore() await wrapper.find('button').trigger('click') - expect(spy).toHaveBeenCalledWith('size') + expect(sortingStore.toggleSortBy).toHaveBeenCalledWith('size') }) describe('Vue 3 sorting interactions', () => { it('is true when the column is active and direction is asc', () => { - const sortingStore = useFilesSortingStore() - sortingStore.sortingMode = 'created_at' - sortingStore.sortingDirection = 'asc' - - const wrapper = createWrapper('created_at', 'Created at') + const wrapper = createWrapper('created_at', 'Created at', { + sortingMode: 'created_at', + sortingDirection: 'asc', + }) expect(wrapper.vm.isAscending).toBe(true) }) it('reacts when direction changes from asc to desc', async () => { + const wrapper = createWrapper('created_at', 'Created at', { + sortingMode: 'created_at', + sortingDirection: 'asc', + }) const sortingStore = useFilesSortingStore() - sortingStore.sortingMode = 'created_at' - sortingStore.sortingDirection = 'asc' - - const wrapper = createWrapper('created_at', 'Created at') expect(wrapper.vm.isAscending).toBe(true) sortingStore.sortingDirection = 'desc' @@ -131,10 +150,9 @@ describe('FilesListTableHeaderButton.vue', () => { }) it('adds the active class when the column is the active sort mode', () => { - const sortingStore = useFilesSortingStore() - sortingStore.sortingMode = 'created_at' - - const wrapper = createWrapper('created_at', 'Created at') + const wrapper = createWrapper('created_at', 'Created at', { + sortingMode: 'created_at', + }) expect(wrapper.find('button').classes()).toContain('files-list__column-sort-button--active') }) @@ -145,10 +163,10 @@ describe('FilesListTableHeaderButton.vue', () => { }) it('removes the active class when the active mode changes', async () => { + const wrapper = createWrapper('created_at', 'Created at', { + sortingMode: 'created_at', + }) const sortingStore = useFilesSortingStore() - sortingStore.sortingMode = 'created_at' - - const wrapper = createWrapper('created_at', 'Created at') expect(wrapper.find('button').classes()).toContain('files-list__column-sort-button--active') sortingStore.sortingMode = 'status' @@ -158,13 +176,12 @@ describe('FilesListTableHeaderButton.vue', () => { }) it('delegates click to toggleSortBy for the provided mode', async () => { - const sortingStore = useFilesSortingStore() - const spy = vi.spyOn(sortingStore, 'toggleSortBy') const wrapper = createWrapper('created_at', 'Created at') + const sortingStore = useFilesSortingStore() await wrapper.find('button').trigger('click') - expect(spy).toHaveBeenCalledWith('created_at') + expect(sortingStore.toggleSortBy).toHaveBeenCalledWith('created_at') }) }) }) From bca74aae74ce8e119dccc4cce3eb51267a9c93c7 Mon Sep 17 00:00:00 2001 From: ColumbusLabs <287001685+ColumbusLabs@users.noreply.github.com> Date: Thu, 23 Jul 2026 15:10:06 -0400 Subject: [PATCH 2/2] test: narrow FilesList Pinia action stubbing Signed-off-by: ColumbusLabs <287001685+ColumbusLabs@users.noreply.github.com> --- .../FilesList/FilesListTableHeader.spec.ts | 88 ++++++++----------- .../FilesListTableHeaderButton.spec.ts | 16 +--- 2 files changed, 37 insertions(+), 67 deletions(-) diff --git a/src/tests/views/FilesList/FilesListTableHeader.spec.ts b/src/tests/views/FilesList/FilesListTableHeader.spec.ts index 53d7fb9575..79cabb6f73 100644 --- a/src/tests/views/FilesList/FilesListTableHeader.spec.ts +++ b/src/tests/views/FilesList/FilesListTableHeader.spec.ts @@ -5,11 +5,12 @@ import { createTestingPinia } from '@pinia/testing' import { mount } from '@vue/test-utils' -import { setActivePinia } from 'pinia' import { beforeEach, describe, expect, it, vi } from 'vitest' import FilesListTableHeader from '../../../views/FilesList/FilesListTableHeader.vue' +import { useFilesStore } from '../../../store/files.js' +import { useFilesSortingStore } from '../../../store/filesSorting.js' import { useSelectionStore } from '../../../store/selection.js' type Column = { @@ -94,47 +95,22 @@ const NcCheckboxRadioSwitchStub = { template: '', } -type FilesState = Record - -type HeaderState = { - canRequestSign?: boolean - files?: FilesState - selected?: number[] - sortingMode?: string - sortingDirection?: string -} - -/** - * Mount the table header with isolated store state. - * - * @param filesCount Number of files represented in the test. - * @param state Initial state for the files, selection, and sorting stores. - */ -function createWrapper(filesCount = 2, state: HeaderState = {}) { +function createWrapper(filesCount = 2, options: { canRequestSign?: boolean } = {}) { const ordered = Array.from({ length: filesCount }, (_, index) => index + 1) - const files = state.files ?? Object.fromEntries(ordered.map(id => [id, { id }])) const pinia = createTestingPinia({ createSpy: vi.fn, - stubActions: false, + stubActions: (action, store) => !( + (store.$id === 'files' && action === 'canDelete') + || (store.$id === 'selection' && ['set', 'reset'].includes(action)) + ), initialState: { files: { ordered, - files, - canRequestSign: state.canRequestSign ?? true, - }, - selection: { - selected: state.selected ?? [], - }, - filesSorting: { - ...(state.sortingMode !== undefined && { sortingMode: state.sortingMode }), - ...(state.sortingDirection !== undefined && { sortingDirection: state.sortingDirection }), + files: Object.fromEntries(ordered.map(id => [id, { id }])), + canRequestSign: options.canRequestSign ?? true, }, }, }) - setActivePinia(pinia) return mount(FilesListTableHeader, { props: { @@ -276,13 +252,13 @@ describe('FilesListTableHeader.vue', () => { }) it('selects only deletable files when using select all', async () => { - const wrapper = createWrapper(3, { - files: { - 1: { id: 1 }, - 2: { id: 2, requested_by: { userId: 'someone-else' } }, - 3: { id: 3 }, - }, - }) + const wrapper = createWrapper(3) + const filesStore = useFilesStore() + filesStore.files = { + 1: { id: 1 }, + 2: { id: 2, requested_by: { userId: 'someone-else' } }, + 3: { id: 3 }, + } const stub = wrapper.findComponent(NcCheckboxRadioSwitchStub) const selectionStore = useSelectionStore() @@ -292,14 +268,20 @@ describe('FilesListTableHeader.vue', () => { }) it('sets modelValue to true when all files are selected', async () => { - const wrapper = createWrapper(2, { selected: [1, 2] }) + const wrapper = createWrapper(2) + const selectionStore = useSelectionStore() + + selectionStore.set([1, 2]) await wrapper.vm.$nextTick() expect(wrapper.findComponent(NcCheckboxRadioSwitchStub).props('modelValue')).toBe(true) }) it('sets indeterminate when only some files are selected', async () => { - const wrapper = createWrapper(2, { selected: [1] }) + const wrapper = createWrapper(2) + const selectionStore = useSelectionStore() + + selectionStore.set([1]) await wrapper.vm.$nextTick() const stub = wrapper.findComponent(NcCheckboxRadioSwitchStub) @@ -324,22 +306,24 @@ describe('FilesListTableHeader.vue', () => { expect(vm.ariaSortForMode('actions', false)).toBeNull() }) - it('returns descending when the active mode is descending', () => { - const wrapper = createWrapper(2, { - sortingMode: 'created_at', - sortingDirection: 'desc', - }) + it('returns descending when the active mode is descending', async () => { + const wrapper = createWrapper() const vm = wrapper.vm as FilesListTableHeaderVm + const sortingStore = useFilesSortingStore() + sortingStore.sortingMode = 'created_at' + sortingStore.sortingDirection = 'desc' + await wrapper.vm.$nextTick() expect(vm.ariaSortForMode('created_at')).toBe('descending') }) - it('returns ascending when the active mode is ascending', () => { - const wrapper = createWrapper(2, { - sortingMode: 'status', - sortingDirection: 'asc', - }) + it('returns ascending when the active mode is ascending', async () => { + const wrapper = createWrapper() const vm = wrapper.vm as FilesListTableHeaderVm + const sortingStore = useFilesSortingStore() + sortingStore.sortingMode = 'status' + sortingStore.sortingDirection = 'asc' + await wrapper.vm.$nextTick() expect(vm.ariaSortForMode('status')).toBe('ascending') }) diff --git a/src/tests/views/FilesList/FilesListTableHeaderButton.spec.ts b/src/tests/views/FilesList/FilesListTableHeaderButton.spec.ts index bef2f55146..c9e6479f9d 100644 --- a/src/tests/views/FilesList/FilesListTableHeaderButton.spec.ts +++ b/src/tests/views/FilesList/FilesListTableHeaderButton.spec.ts @@ -5,7 +5,6 @@ import { createTestingPinia } from '@pinia/testing' import { mount } from '@vue/test-utils' -import { setActivePinia } from 'pinia' import { beforeEach, describe, expect, it, vi } from 'vitest' import FilesListTableHeaderButton from '../../../views/FilesList/FilesListTableHeaderButton.vue' @@ -59,26 +58,13 @@ const NcButtonStub = { template: '', } -type FilesSortingState = { - sortingMode?: string - sortingDirection?: string -} - -/** - * Mount the header button with isolated sorting-store state. - * - * @param mode Sort key represented by the button. - * @param name Visible label for the button. - * @param sortingState Initial state for the sorting store. - */ -function createWrapper(mode = 'size', name = 'Size', sortingState: FilesSortingState = {}) { +function createWrapper(mode = 'size', name = 'Size', sortingState: { sortingMode?: string, sortingDirection?: string } = {}) { const pinia = createTestingPinia({ createSpy: vi.fn, initialState: { filesSorting: sortingState, }, }) - setActivePinia(pinia) return mount(FilesListTableHeaderButton, { props: { name, mode },