Skip to content

Commit 6dc3b2b

Browse files
authored
Merge pull request #7003 from LibreSign/backport/7002/stable33
[stable33] fix: vue3 component api migration
2 parents 7c7b9cd + 0d18e83 commit 6dc3b2b

12 files changed

Lines changed: 627 additions & 17 deletions

src/views/Documents/IdDocsValidation.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
</template>
1313
<NcActionInput v-model="filters.owner"
1414
:label="t('libresign', 'Owner')"
15-
@update:value="onFilterChange">
15+
@update:modelValue="onFilterChange">
1616
<template #icon>
1717
<NcIconSvgWrapper :path="mdiAccount" :size="20" />
1818
</template>
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+
})

src/views/FilesList/FileEntry/FileEntryCheckbox.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
<NcLoadingIcon v-if="isLoading" :name="loadingLabel" />
99
<NcCheckboxRadioSwitch v-else
1010
:aria-label="ariaLabel"
11-
:checked="isSelected"
12-
@update:checked="onSelectionChange" />
11+
:model-value="isSelected"
12+
@update:modelValue="onSelectionChange" />
1313
</td>
1414
</template>
1515

0 commit comments

Comments
 (0)