Skip to content

Commit d2f5a99

Browse files
test(FileEntryCheckbox): add Vue 3 API regression tests
Add unit tests to guard against regressions to the Vue 2 → Vue 3 NcCheckboxRadioSwitch API migration in FileEntryCheckbox.vue. The NcCheckboxRadioSwitch stub enforces the Vue 3 contract (:model-value / @update:modelValue). Tests verify: - modelValue prop is passed correctly (not the legacy :checked) - selecting/deselecting a file updates the selection store - update:modelValue reaches onSelectionChange (regression guard for the old @update:checked that was silently ignored in Vue 3) - loading state renders the spinner instead of the checkbox Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
1 parent a917100 commit d2f5a99

1 file changed

Lines changed: 201 additions & 0 deletions

File tree

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
/**
7+
* Regression tests for Vue 2 → Vue 3 migration:
8+
* NcCheckboxRadioSwitch changed from :checked / @update:checked
9+
* to :model-value / @update:modelValue.
10+
*
11+
* These tests ensure the checkbox selection wires up correctly via
12+
* the Vue 3 modelValue API. If someone reverts to the old Vue 2 API
13+
* the interaction assertions will fail.
14+
*/
15+
16+
import { beforeEach, describe, expect, it, vi } from 'vitest'
17+
import { mount } from '@vue/test-utils'
18+
import { setActivePinia, createPinia } from 'pinia'
19+
import FileEntryCheckbox from './FileEntryCheckbox.vue'
20+
21+
// ---------------------------------------------------------------------------
22+
// Mocks required by the component and its store dependencies
23+
// ---------------------------------------------------------------------------
24+
25+
vi.mock('@nextcloud/event-bus', () => ({
26+
emit: vi.fn(),
27+
subscribe: vi.fn(),
28+
}))
29+
30+
vi.mock('@nextcloud/logger', () => ({
31+
getLogger: vi.fn(() => ({
32+
error: vi.fn(),
33+
warn: vi.fn(),
34+
info: vi.fn(),
35+
debug: vi.fn(),
36+
})),
37+
getLoggerBuilder: vi.fn(() => ({
38+
setApp: vi.fn().mockReturnThis(),
39+
detectUser: vi.fn().mockReturnThis(),
40+
build: vi.fn(() => ({
41+
error: vi.fn(),
42+
warn: vi.fn(),
43+
info: vi.fn(),
44+
debug: vi.fn(),
45+
})),
46+
})),
47+
}))
48+
49+
// ---------------------------------------------------------------------------
50+
// NcCheckboxRadioSwitch stub — Vue 3 modelValue API
51+
//
52+
// The stub intentionally mirrors the real component's Vue 3 contract:
53+
// - prop: modelValue (was `checked` in Vue 2)
54+
// - emit: update:modelValue (was `update:checked` in Vue 2)
55+
//
56+
// If FileEntryCheckbox.vue were to use the old :checked / @update:checked
57+
// the stub would never receive the correct prop and the emit would never
58+
// reach the handler – tests would fail and catch the regression.
59+
// ---------------------------------------------------------------------------
60+
const NcCheckboxRadioSwitchStub = {
61+
name: 'NcCheckboxRadioSwitch',
62+
props: {
63+
modelValue: {
64+
type: Boolean,
65+
default: false,
66+
},
67+
ariaLabel: String,
68+
},
69+
emits: ['update:modelValue'],
70+
template: '<input type="checkbox" :checked="modelValue" :aria-label="ariaLabel" @change="$emit(\'update:modelValue\', $event.target.checked)" />',
71+
}
72+
73+
const NcLoadingIconStub = {
74+
name: 'NcLoadingIcon',
75+
props: ['name'],
76+
template: '<span class="loading-icon" />',
77+
}
78+
79+
// ---------------------------------------------------------------------------
80+
// Helpers
81+
// ---------------------------------------------------------------------------
82+
83+
const createSource = (id = 1, basename = 'test.pdf') => ({ id, basename })
84+
85+
const createWrapper = (sourceOverrides = {}, storeState: { selected?: number[] } = {}) => {
86+
return mount(FileEntryCheckbox, {
87+
props: {
88+
source: createSource(1, 'test.pdf'),
89+
...sourceOverrides,
90+
},
91+
global: {
92+
stubs: {
93+
NcCheckboxRadioSwitch: NcCheckboxRadioSwitchStub,
94+
NcLoadingIcon: NcLoadingIconStub,
95+
},
96+
},
97+
})
98+
}
99+
100+
// ---------------------------------------------------------------------------
101+
// Tests
102+
// ---------------------------------------------------------------------------
103+
104+
describe('FileEntryCheckbox', () => {
105+
beforeEach(() => {
106+
setActivePinia(createPinia())
107+
vi.clearAllMocks()
108+
})
109+
110+
describe('RULE: checkbox reflects selection state via modelValue (Vue 3 API)', () => {
111+
it('renders unchecked when file is not selected', () => {
112+
const wrapper = createWrapper()
113+
const checkbox = wrapper.find('input[type="checkbox"]')
114+
115+
expect(checkbox.exists()).toBe(true)
116+
expect((checkbox.element as HTMLInputElement).checked).toBe(false)
117+
})
118+
119+
it('stub receives modelValue prop (not legacy checked prop)', () => {
120+
const wrapper = createWrapper()
121+
const stub = wrapper.findComponent(NcCheckboxRadioSwitchStub)
122+
123+
// The component must bind using :model-value, not :checked.
124+
// If :checked were used, the stub would never receive modelValue.
125+
expect(stub.props('modelValue')).toBe(false)
126+
})
127+
})
128+
129+
describe('RULE: clicking checkbox updates selection via update:modelValue (Vue 3 API)', () => {
130+
it('calls onSelectionChange when checkbox emits update:modelValue = true', async () => {
131+
const wrapper = createWrapper()
132+
const stub = wrapper.findComponent(NcCheckboxRadioSwitchStub)
133+
134+
// Simulate the component emitting the Vue 3 event
135+
await stub.vm.$emit('update:modelValue', true)
136+
137+
// After selecting, the selectionStore should contain this file's id
138+
const vm = wrapper.vm as InstanceType<typeof FileEntryCheckbox> & { selectionStore: { selected: number[] } }
139+
expect(vm.selectionStore.selected).toContain(1)
140+
})
141+
142+
it('removes file from selection when checkbox emits update:modelValue = false', async () => {
143+
const wrapper = createWrapper()
144+
const stub = wrapper.findComponent(NcCheckboxRadioSwitchStub)
145+
146+
// First select
147+
await stub.vm.$emit('update:modelValue', true)
148+
// Then deselect
149+
await stub.vm.$emit('update:modelValue', false)
150+
151+
const vm = wrapper.vm as InstanceType<typeof FileEntryCheckbox> & { selectionStore: { selected: number[] } }
152+
expect(vm.selectionStore.selected).not.toContain(1)
153+
})
154+
155+
it('handler IS called via update:modelValue (Vue 2 regression guard: old @update:checked would not fire)', async () => {
156+
// Before the fix, @update:checked was used. Vue 3 NcCheckboxRadioSwitch emits
157+
// update:modelValue, not update:checked, so the handler was never called.
158+
// This test proves update:modelValue correctly reaches onSelectionChange by
159+
// verifying the observable side-effect: file appears in selectionStore.
160+
const wrapper = createWrapper()
161+
const stub = wrapper.findComponent(NcCheckboxRadioSwitchStub)
162+
163+
await stub.vm.$emit('update:modelValue', true)
164+
165+
const vm = wrapper.vm as InstanceType<typeof FileEntryCheckbox> & { selectionStore: { selected: number[] } }
166+
expect(vm.selectionStore.selected).toContain(1)
167+
})
168+
})
169+
170+
describe('RULE: loading state shows spinner instead of checkbox', () => {
171+
it('shows loading icon when isLoading is true', () => {
172+
const wrapper = mount(FileEntryCheckbox, {
173+
props: { source: createSource(), isLoading: true },
174+
global: {
175+
stubs: {
176+
NcCheckboxRadioSwitch: NcCheckboxRadioSwitchStub,
177+
NcLoadingIcon: NcLoadingIconStub,
178+
},
179+
},
180+
})
181+
182+
expect(wrapper.find('.loading-icon').exists()).toBe(true)
183+
expect(wrapper.findComponent(NcCheckboxRadioSwitchStub).exists()).toBe(false)
184+
})
185+
186+
it('shows checkbox when isLoading is false', () => {
187+
const wrapper = mount(FileEntryCheckbox, {
188+
props: { source: createSource(), isLoading: false },
189+
global: {
190+
stubs: {
191+
NcCheckboxRadioSwitch: NcCheckboxRadioSwitchStub,
192+
NcLoadingIcon: NcLoadingIconStub,
193+
},
194+
},
195+
})
196+
197+
expect(wrapper.find('.loading-icon').exists()).toBe(false)
198+
expect(wrapper.findComponent(NcCheckboxRadioSwitchStub).exists()).toBe(true)
199+
})
200+
})
201+
})

0 commit comments

Comments
 (0)