Skip to content

Commit 0d18e83

Browse files
test(SigningMode): add Vue 3 API regression tests for async-mode and worker switches
Add unit tests to guard against regressions to the Vue 2 → Vue 3 NcCheckboxRadioSwitch API migration in SigningMode.vue. The NcCheckboxRadioSwitch stub enforces the Vue 3 contract (:model-value / @update:modelValue). Tests verify: - modelValue prop is passed (not the legacy :checked) - async switch starts unchecked (loadState returns 'sync' by default) - update:modelValue correctly sets asyncEnabled and externalWorkerEnabled - worker-type switch is only visible when asyncEnabled is true - axios.post is called after a toggle change Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
1 parent 3b2ce85 commit 0d18e83

1 file changed

Lines changed: 187 additions & 0 deletions

File tree

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
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+
* SigningMode uses two NcCheckboxRadioSwitch (type="switch") components
9+
* that were broken because they used ` :checked` / `@update:checked`
10+
* instead of `:model-value` / `@update:modelValue`.
11+
*
12+
* The stub enforces the Vue 3 modelValue contract. If someone reverts to
13+
* `:checked` the stub will not reflect state and onToggle handlers won't fire.
14+
*/
15+
16+
import { beforeEach, describe, expect, it, vi } from 'vitest'
17+
import { mount } from '@vue/test-utils'
18+
import SigningMode from './SigningMode.vue'
19+
20+
// ---------------------------------------------------------------------------
21+
// Mocks
22+
// ---------------------------------------------------------------------------
23+
24+
vi.mock('@nextcloud/initial-state', () => ({
25+
loadState: vi.fn((app, key, defaultValue) => defaultValue),
26+
}))
27+
28+
vi.mock('@nextcloud/axios', () => ({
29+
default: {
30+
post: vi.fn(() => Promise.resolve({ data: {} })),
31+
},
32+
}))
33+
34+
vi.mock('@nextcloud/router', () => ({
35+
generateOcsUrl: vi.fn((path) => `/ocs/v2.php${path}`),
36+
}))
37+
38+
// ---------------------------------------------------------------------------
39+
// NcCheckboxRadioSwitch stub — Vue 3 modelValue API
40+
//
41+
// Critical: this stub only accepts `modelValue`, mirroring the real
42+
// component's Vue 3 contract. Tests that assert modelValue state or
43+
// trigger update:modelValue would fail if the parent component were
44+
// to bind the old `:checked` prop.
45+
// ---------------------------------------------------------------------------
46+
const NcCheckboxRadioSwitchStub = {
47+
name: 'NcCheckboxRadioSwitch',
48+
props: {
49+
modelValue: {
50+
type: Boolean,
51+
default: false,
52+
},
53+
disabled: Boolean,
54+
type: String,
55+
},
56+
emits: ['update:modelValue'],
57+
template: '<button role="switch" :aria-checked="String(modelValue)" :disabled="disabled || undefined" @click="$emit(\'update:modelValue\', !modelValue)"><slot /></button>',
58+
}
59+
60+
const stubComponents = {
61+
NcCheckboxRadioSwitch: NcCheckboxRadioSwitchStub,
62+
NcSettingsSection: { template: '<div><slot /></div>' },
63+
NcNoteCard: { template: '<div />' },
64+
NcLoadingIcon: { template: '<span />' },
65+
NcSavingIndicatorIcon: { template: '<span />' },
66+
NcTextField: { template: '<input />' },
67+
}
68+
69+
// ---------------------------------------------------------------------------
70+
// Helpers
71+
// ---------------------------------------------------------------------------
72+
73+
const createWrapper = () => mount(SigningMode, { global: { stubs: stubComponents } })
74+
75+
// ---------------------------------------------------------------------------
76+
// Tests
77+
// ---------------------------------------------------------------------------
78+
79+
describe('SigningMode', () => {
80+
beforeEach(() => {
81+
vi.clearAllMocks()
82+
})
83+
84+
describe('RULE: async-mode switch binds via modelValue (Vue 3 API)', () => {
85+
it('stub receives modelValue prop (not legacy checked prop)', () => {
86+
const wrapper = createWrapper()
87+
const switches = wrapper.findAllComponents(NcCheckboxRadioSwitchStub)
88+
89+
// The first switch is the async-mode toggle
90+
expect(switches[0].props('modelValue')).toBeDefined()
91+
expect(typeof switches[0].props('modelValue')).toBe('boolean')
92+
})
93+
94+
it('async switch modelValue is false by default (loadState returns default sync)', () => {
95+
const wrapper = createWrapper()
96+
const asyncSwitch = wrapper.findAllComponents(NcCheckboxRadioSwitchStub)[0]
97+
98+
expect(asyncSwitch.props('modelValue')).toBe(false)
99+
})
100+
})
101+
102+
describe('RULE: onToggleChange called via update:modelValue (Vue 3 API)', () => {
103+
it('sets asyncEnabled to true when update:modelValue = true emitted', async () => {
104+
const wrapper = createWrapper()
105+
const asyncSwitch = wrapper.findAllComponents(NcCheckboxRadioSwitchStub)[0]
106+
107+
await asyncSwitch.vm.$emit('update:modelValue', true)
108+
109+
const vm = wrapper.vm as InstanceType<typeof SigningMode> & { asyncEnabled: boolean }
110+
expect(vm.asyncEnabled).toBe(true)
111+
})
112+
113+
it('sets asyncEnabled to false when update:modelValue = false emitted', async () => {
114+
const wrapper = createWrapper()
115+
const vm = wrapper.vm as InstanceType<typeof SigningMode> & { asyncEnabled: boolean }
116+
vm.asyncEnabled = true
117+
await wrapper.vm.$nextTick()
118+
119+
const asyncSwitch = wrapper.findAllComponents(NcCheckboxRadioSwitchStub)[0]
120+
await asyncSwitch.vm.$emit('update:modelValue', false)
121+
122+
expect(vm.asyncEnabled).toBe(false)
123+
})
124+
125+
it('handler IS called via update:modelValue (Vue 2 regression guard: old @update:checked would not fire)', async () => {
126+
// Before the fix, @update:checked bound to the switch. Vue 3 NcCheckboxRadioSwitch
127+
// emits update:modelValue, not update:checked, so the handler was silent.
128+
// This test proves update:modelValue correctly reaches onToggleChange by
129+
// verifying the observable side-effect: asyncEnabled changes.
130+
const wrapper = createWrapper()
131+
const asyncSwitch = wrapper.findAllComponents(NcCheckboxRadioSwitchStub)[0]
132+
133+
await asyncSwitch.vm.$emit('update:modelValue', true)
134+
135+
const vm = wrapper.vm as InstanceType<typeof SigningMode> & { asyncEnabled: boolean }
136+
expect(vm.asyncEnabled).toBe(true)
137+
})
138+
})
139+
140+
describe('RULE: worker-type switch visible and working only when asyncEnabled', () => {
141+
it('worker-type switch is not rendered when asyncEnabled is false', () => {
142+
const wrapper = createWrapper()
143+
const vm = wrapper.vm as InstanceType<typeof SigningMode> & { asyncEnabled: boolean }
144+
145+
expect(vm.asyncEnabled).toBe(false)
146+
// Only the one async switch should be present when async is disabled
147+
const switches = wrapper.findAllComponents(NcCheckboxRadioSwitchStub)
148+
expect(switches.length).toBe(1)
149+
})
150+
151+
it('worker-type switch is rendered when asyncEnabled is true', async () => {
152+
const wrapper = createWrapper()
153+
const vm = wrapper.vm as InstanceType<typeof SigningMode> & { asyncEnabled: boolean }
154+
vm.asyncEnabled = true
155+
await wrapper.vm.$nextTick()
156+
157+
const switches = wrapper.findAllComponents(NcCheckboxRadioSwitchStub)
158+
expect(switches.length).toBeGreaterThanOrEqual(2)
159+
})
160+
161+
it('onWorkerTypeChange is triggered via update:modelValue from worker switch', async () => {
162+
const wrapper = createWrapper()
163+
const vm = wrapper.vm as InstanceType<typeof SigningMode> & { asyncEnabled: boolean, externalWorkerEnabled: boolean }
164+
vm.asyncEnabled = true
165+
await wrapper.vm.$nextTick()
166+
167+
const workerSwitch = wrapper.findAllComponents(NcCheckboxRadioSwitchStub)[1]
168+
await workerSwitch.vm.$emit('update:modelValue', true)
169+
170+
expect(vm.externalWorkerEnabled).toBe(true)
171+
})
172+
})
173+
174+
describe('RULE: saveConfig called after toggle change', () => {
175+
it('calls axios.post when onToggleChange fires', async () => {
176+
const { default: axios } = await import('@nextcloud/axios')
177+
const wrapper = createWrapper()
178+
const asyncSwitch = wrapper.findAllComponents(NcCheckboxRadioSwitchStub)[0]
179+
180+
await asyncSwitch.vm.$emit('update:modelValue', true)
181+
// Wait for any pending microtasks
182+
await wrapper.vm.$nextTick()
183+
184+
expect(axios.post).toHaveBeenCalled()
185+
})
186+
})
187+
})

0 commit comments

Comments
 (0)