Skip to content

Commit 1dd7f52

Browse files
test(vue3): add SignatureStamp view coverage
Cover preview visibility, clipboard actions, template persistence, background upload flow and reset helpers for the migrated SignatureStamp view. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
1 parent f5d6228 commit 1dd7f52

1 file changed

Lines changed: 322 additions & 0 deletions

File tree

Lines changed: 322 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,322 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2026 LibreSign contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import { beforeEach, describe, expect, it, vi } from 'vitest'
7+
import { flushPromises, mount } from '@vue/test-utils'
8+
9+
import SignatureStamp from '../../../views/Settings/SignatureStamp.vue'
10+
11+
const axiosGetMock = vi.fn()
12+
const axiosPostMock = vi.fn()
13+
const axiosPatchMock = vi.fn()
14+
const axiosDeleteMock = vi.fn()
15+
const clipboardWriteTextMock = vi.fn()
16+
const subscribeMock = vi.fn()
17+
const unsubscribeMock = vi.fn()
18+
19+
let stateOverrides: Record<string, unknown> = {}
20+
21+
vi.mock('debounce', () => ({
22+
default: vi.fn((fn: (...args: unknown[]) => unknown) => fn),
23+
}))
24+
25+
vi.mock('@nextcloud/l10n', () => ({
26+
isRTL: vi.fn(() => false),
27+
t: vi.fn((_app: string, text: string) => text),
28+
}))
29+
30+
vi.mock('@nextcloud/auth', () => ({
31+
getCurrentUser: vi.fn(() => ({ displayName: 'Jane Doe' })),
32+
}))
33+
34+
vi.mock('@nextcloud/axios', () => ({
35+
default: {
36+
get: vi.fn((...args: unknown[]) => axiosGetMock(...args)),
37+
post: vi.fn((...args: unknown[]) => axiosPostMock(...args)),
38+
patch: vi.fn((...args: unknown[]) => axiosPatchMock(...args)),
39+
delete: vi.fn((...args: unknown[]) => axiosDeleteMock(...args)),
40+
},
41+
}))
42+
43+
vi.mock('@nextcloud/event-bus', () => ({
44+
subscribe: vi.fn((...args: unknown[]) => subscribeMock(...args)),
45+
unsubscribe: vi.fn((...args: unknown[]) => unsubscribeMock(...args)),
46+
}))
47+
48+
vi.mock('@nextcloud/initial-state', () => ({
49+
loadState: vi.fn((_app: string, key: string, defaultValue?: unknown) => {
50+
const defaults: Record<string, unknown> = {
51+
signature_background_type: 'default',
52+
default_signature_text_template: 'Signed by {{ signerName }}',
53+
default_template_font_size: 10,
54+
default_signature_font_size: 18,
55+
default_signature_width: 180,
56+
default_signature_height: 90,
57+
signature_text_template: 'Signed by {{ signerName }}',
58+
signature_width: 180,
59+
signature_height: 90,
60+
signature_font_size: 18,
61+
template_font_size: 10,
62+
signature_preview_zoom_level: 100,
63+
signature_render_mode: 'GRAPHIC_AND_DESCRIPTION',
64+
signature_text_template_error: '',
65+
signature_text_parsed: '<p>Signed by Jane Doe</p>',
66+
signature_available_variables: {
67+
'{{ signerName }}': 'Signer name',
68+
},
69+
}
70+
71+
if (key in stateOverrides) {
72+
return stateOverrides[key]
73+
}
74+
75+
return key in defaults ? defaults[key] : defaultValue
76+
}),
77+
}))
78+
79+
vi.mock('@nextcloud/router', () => ({
80+
generateOcsUrl: vi.fn((path: string) => `/ocs/v2.php${path}`),
81+
}))
82+
83+
vi.mock('@nextcloud/vue/composables/useIsDarkTheme', () => ({
84+
useIsDarkTheme: vi.fn(() => false),
85+
}))
86+
87+
vi.mock('@nextcloud/vue/directives/Linkify', () => ({
88+
default: {},
89+
}))
90+
91+
vi.mock('../../../components/CodeEditor.vue', () => ({
92+
default: {
93+
name: 'CodeEditor',
94+
props: ['modelValue', 'label', 'placeholder'],
95+
emits: ['update:modelValue'],
96+
template: '<textarea class="code-editor-stub" @input="$emit(\'update:modelValue\', $event.target.value)" />',
97+
},
98+
}))
99+
100+
vi.mock('@nextcloud/vue/components/NcButton', () => ({
101+
default: {
102+
name: 'NcButton',
103+
emits: ['click'],
104+
template: '<button class="nc-button-stub" @click="$emit(\'click\')"><slot /><slot name="icon" /></button>',
105+
},
106+
}))
107+
108+
vi.mock('@nextcloud/vue/components/NcCheckboxRadioSwitch', () => ({
109+
default: {
110+
name: 'NcCheckboxRadioSwitch',
111+
props: ['modelValue', 'value'],
112+
emits: ['update:modelValue'],
113+
template: '<div class="nc-checkbox-radio-switch-stub"><slot /></div>',
114+
},
115+
}))
116+
117+
vi.mock('@nextcloud/vue/components/NcDialog', () => ({
118+
default: {
119+
name: 'NcDialog',
120+
props: ['open', 'name'],
121+
emits: ['update:open'],
122+
template: '<div class="nc-dialog-stub"><slot /></div>',
123+
},
124+
}))
125+
126+
vi.mock('@nextcloud/vue/components/NcFormBoxButton', () => ({
127+
default: {
128+
name: 'NcFormBoxButton',
129+
emits: ['click'],
130+
template: '<button class="nc-form-box-button-stub" @click="$emit(\'click\')"><slot /><slot name="icon" /><slot name="description" /></button>',
131+
},
132+
}))
133+
134+
vi.mock('@nextcloud/vue/components/NcIconSvgWrapper', () => ({
135+
default: {
136+
name: 'NcIconSvgWrapper',
137+
template: '<i class="nc-icon-svg-wrapper-stub" />',
138+
},
139+
}))
140+
141+
vi.mock('@nextcloud/vue/components/NcLoadingIcon', () => ({
142+
default: {
143+
name: 'NcLoadingIcon',
144+
template: '<span class="nc-loading-icon-stub" />',
145+
},
146+
}))
147+
148+
vi.mock('@nextcloud/vue/components/NcNoteCard', () => ({
149+
default: {
150+
name: 'NcNoteCard',
151+
template: '<div class="nc-note-card-stub"><slot /></div>',
152+
},
153+
}))
154+
155+
vi.mock('@nextcloud/vue/components/NcSettingsSection', () => ({
156+
default: {
157+
name: 'NcSettingsSection',
158+
template: '<section class="nc-settings-section-stub"><slot /></section>',
159+
},
160+
}))
161+
162+
vi.mock('@nextcloud/vue/components/NcTextField', () => ({
163+
default: {
164+
name: 'NcTextField',
165+
props: ['modelValue', 'label'],
166+
emits: ['update:modelValue', 'keydown.enter', 'blur'],
167+
template: '<input class="nc-text-field-stub" />',
168+
},
169+
}))
170+
171+
describe('SignatureStamp.vue', () => {
172+
const appConfigMock = {
173+
setValue: vi.fn(),
174+
}
175+
176+
const createWrapper = () => mount(SignatureStamp)
177+
178+
beforeEach(() => {
179+
stateOverrides = {}
180+
axiosGetMock.mockReset()
181+
axiosPostMock.mockReset()
182+
axiosPatchMock.mockReset()
183+
axiosDeleteMock.mockReset()
184+
clipboardWriteTextMock.mockReset()
185+
subscribeMock.mockReset()
186+
unsubscribeMock.mockReset()
187+
appConfigMock.setValue.mockReset()
188+
189+
axiosGetMock.mockResolvedValue({
190+
data: {
191+
ocs: {
192+
data: {
193+
signature_available_variables: { '{{ signerName }}': 'Signer name' },
194+
default_signature_text_template: 'Updated default',
195+
},
196+
},
197+
},
198+
})
199+
200+
axiosPostMock.mockResolvedValue({
201+
data: {
202+
ocs: {
203+
data: {
204+
parsed: '<p>Updated</p>',
205+
templateFontSize: 12,
206+
signatureFontSize: 20,
207+
},
208+
},
209+
},
210+
})
211+
212+
axiosPatchMock.mockResolvedValue({ data: { ocs: { data: {} } } })
213+
axiosDeleteMock.mockResolvedValue({ data: { ocs: { data: {} } } })
214+
215+
vi.stubGlobal('OCP', { AppConfig: appConfigMock })
216+
vi.stubGlobal('navigator', {
217+
clipboard: {
218+
writeText: clipboardWriteTextMock,
219+
},
220+
})
221+
})
222+
223+
it('loads preview state from initial settings', () => {
224+
const wrapper = createWrapper()
225+
226+
expect(wrapper.vm.renderMode).toBe('GRAPHIC_AND_DESCRIPTION')
227+
expect(wrapper.vm.backgroundUrl).toBe('/ocs/v2.php/apps/libresign/api/v1/admin/signature-background')
228+
expect(wrapper.vm.displayPreview).toBe(true)
229+
expect(wrapper.vm.signatureImageUrl).toContain('text=Signature%20image%20here')
230+
})
231+
232+
it('hides preview when background is deleted and description text is empty', () => {
233+
stateOverrides = {
234+
signature_background_type: 'deleted',
235+
signature_render_mode: 'DESCRIPTION_ONLY',
236+
signature_text_parsed: '',
237+
}
238+
239+
const wrapper = createWrapper()
240+
241+
expect(wrapper.vm.displayPreview).toBe(false)
242+
})
243+
244+
it('copies available variables to the clipboard and marks them as copied', async () => {
245+
vi.useFakeTimers()
246+
const wrapper = createWrapper()
247+
248+
wrapper.vm.copyToClipboard('{{ signerName }}')
249+
250+
expect(clipboardWriteTextMock).toHaveBeenCalledWith('{{ signerName }}')
251+
expect(wrapper.vm.isCopied('{{ signerName }}')).toBe(true)
252+
253+
vi.advanceTimersByTime(2000)
254+
expect(wrapper.vm.copiedVariable).toBeNull()
255+
vi.useRealTimers()
256+
})
257+
258+
it('saves the template and synchronizes normalized font sizes from the API response', async () => {
259+
const wrapper = createWrapper()
260+
wrapper.vm.signatureTextTemplate = 'Updated template'
261+
wrapper.vm.templateFontSize = 11
262+
wrapper.vm.signatureFontSize = 19
263+
264+
await wrapper.vm.saveTemplate()
265+
await flushPromises()
266+
267+
expect(axiosPostMock).toHaveBeenCalledWith('/ocs/v2.php/apps/libresign/api/v1/admin/signature-text', {
268+
template: 'Updated template',
269+
templateFontSize: 11,
270+
signatureFontSize: 19,
271+
signatureWidth: 180,
272+
signatureHeight: 90,
273+
renderMode: 'GRAPHIC_AND_DESCRIPTION',
274+
})
275+
expect(wrapper.vm.parsed).toBe('<p>Updated</p>')
276+
expect(wrapper.vm.templateFontSize).toBe(12)
277+
expect(wrapper.vm.signatureFontSize).toBe(20)
278+
expect(wrapper.vm.templateSaved).toBe(true)
279+
})
280+
281+
it('uploads and removes the background image', async () => {
282+
vi.spyOn(Date, 'now').mockReturnValue(123456)
283+
const wrapper = createWrapper()
284+
const event = {
285+
target: {
286+
files: [new File(['png'], 'signature.png', { type: 'image/png' })],
287+
},
288+
}
289+
290+
await wrapper.vm.onChangeBackground(event)
291+
await flushPromises()
292+
293+
expect(axiosPostMock).toHaveBeenCalled()
294+
expect(wrapper.vm.backgroundType).toBe('custom')
295+
expect(wrapper.vm.backgroundUrl).toContain('?t=123456')
296+
297+
await wrapper.vm.removeBackground()
298+
await flushPromises()
299+
300+
expect(axiosDeleteMock).toHaveBeenCalledWith('/ocs/v2.php/apps/libresign/api/v1/admin/signature-background', {
301+
setting: undefined,
302+
value: 'backgroundColor',
303+
})
304+
expect(wrapper.vm.backgroundType).toBe('deleted')
305+
expect(wrapper.vm.backgroundUrl).toBe('')
306+
})
307+
308+
it('resets render mode and preview dimensions back to defaults', async () => {
309+
const wrapper = createWrapper()
310+
wrapper.vm.renderMode = 'GRAPHIC_ONLY'
311+
wrapper.vm.signatureWidth = 250
312+
wrapper.vm.signatureHeight = 130
313+
314+
await wrapper.vm.resetRenderMode()
315+
await wrapper.vm.resetSignatureWidth()
316+
await wrapper.vm.resetSignatureHeight()
317+
318+
expect(wrapper.vm.renderMode).toBe('GRAPHIC_AND_DESCRIPTION')
319+
expect(wrapper.vm.signatureWidth).toBe(180)
320+
expect(wrapper.vm.signatureHeight).toBe(90)
321+
})
322+
})

0 commit comments

Comments
 (0)