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 },