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