Skip to content

Commit 6c27191

Browse files
authored
Merge pull request #7230 from LibreSign/backport/7224/stable33
[stable33] chore: migration to vue3
2 parents a95ae33 + 0bbb395 commit 6c27191

25 files changed

Lines changed: 6073 additions & 3583 deletions

src/components/Draw/FileUpload.vue

Lines changed: 309 additions & 228 deletions
Large diffs are not rendered by default.

src/components/PdfEditor/PdfEditor.vue

Lines changed: 331 additions & 251 deletions
Large diffs are not rendered by default.

src/components/PdfEditor/Signature.vue

Lines changed: 264 additions & 224 deletions
Large diffs are not rendered by default.

src/components/RightSidebar/RequestSignatureTab.vue

Lines changed: 708 additions & 757 deletions
Large diffs are not rendered by default.
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import { beforeEach, describe, expect, it, vi } from 'vitest'
7+
import { mount } from '@vue/test-utils'
8+
import FileUpload from '../../../components/Draw/FileUpload.vue'
9+
10+
vi.mock('@nextcloud/l10n', () => ({
11+
t: vi.fn((_app: string, text: string) => text),
12+
}))
13+
14+
vi.mock('@nextcloud/capabilities', () => ({
15+
getCapabilities: vi.fn(() => ({
16+
libresign: {
17+
config: {
18+
'sign-elements': {
19+
'signature-width': 700,
20+
'signature-height': 200,
21+
},
22+
},
23+
},
24+
})),
25+
}))
26+
27+
vi.mock('@nextcloud/vue/components/NcButton', () => ({
28+
default: {
29+
name: 'NcButton',
30+
template: '<button @click="$emit(\'click\')"><slot /><slot name="icon" /></button>',
31+
props: ['disabled', 'variant', 'wide', 'ariaLabel', 'title'],
32+
emits: ['click'],
33+
},
34+
}))
35+
36+
vi.mock('@nextcloud/vue/components/NcDialog', () => ({
37+
default: {
38+
name: 'NcDialog',
39+
template: '<div><slot /><slot name="actions" /></div>',
40+
props: ['name', 'contentClasses'],
41+
emits: ['closing'],
42+
},
43+
}))
44+
45+
vi.mock('@nextcloud/vue/components/NcTextField', () => ({
46+
default: {
47+
name: 'NcTextField',
48+
template: '<input :value="modelValue" @input="$emit(\'update:modelValue\', $event.target.value)" />',
49+
props: ['modelValue', 'label', 'disabled', 'type', 'min', 'max', 'step'],
50+
emits: ['update:modelValue'],
51+
},
52+
}))
53+
54+
vi.mock('@nextcloud/vue/components/NcIconSvgWrapper', () => ({
55+
default: {
56+
name: 'NcIconSvgWrapper',
57+
template: '<span class="icon-stub" />',
58+
props: ['path', 'size'],
59+
},
60+
}))
61+
62+
vi.mock('vue-advanced-cropper', () => ({
63+
Cropper: {
64+
name: 'Cropper',
65+
template: '<div class="cropper-stub" />',
66+
props: ['src', 'defaultSize', 'stencilProps', 'imageRestriction'],
67+
emits: ['change'],
68+
methods: {
69+
zoom: vi.fn(),
70+
move: vi.fn(),
71+
getResult: vi.fn(() => ({
72+
visibleArea: { width: 200, height: 80, left: 0, top: 0 },
73+
image: { width: 400, height: 160 },
74+
})),
75+
},
76+
},
77+
}))
78+
79+
describe('FileUpload.vue - Uploaded signature flow', () => {
80+
beforeEach(() => {
81+
vi.clearAllMocks()
82+
vi.stubGlobal('ResizeObserver', class {
83+
observe = vi.fn()
84+
disconnect = vi.fn()
85+
})
86+
})
87+
88+
function mountComponent() {
89+
return mount(FileUpload)
90+
}
91+
92+
it('initializes stencil dimensions from capabilities', () => {
93+
const wrapper = mountComponent()
94+
95+
expect(wrapper.vm.stencilBaseWidth).toBe(700)
96+
expect(wrapper.vm.stencilBaseHeight).toBe(200)
97+
expect(wrapper.vm.defaultStencilSize).toEqual({ width: 700, height: 200 })
98+
})
99+
100+
it('scales the default stencil size to fit the cropper container', async () => {
101+
const wrapper = mountComponent()
102+
103+
wrapper.vm.containerWidth = 374
104+
await wrapper.vm.$nextTick()
105+
106+
expect(wrapper.vm.defaultStencilSize).toEqual({ width: 350, height: 100 })
107+
})
108+
109+
it('loads the selected file as a data URL', () => {
110+
const wrapper = mountComponent()
111+
const listeners = new Map<string, Array<() => void>>()
112+
113+
class FileReaderMock {
114+
result: string | null = 'data:image/png;base64,loaded'
115+
116+
addEventListener(event: string, callback: () => void) {
117+
listeners.set(event, [...(listeners.get(event) || []), callback])
118+
}
119+
120+
readAsDataURL() {
121+
for (const callback of listeners.get('load') || []) {
122+
callback()
123+
}
124+
}
125+
}
126+
127+
vi.stubGlobal('FileReader', FileReaderMock)
128+
129+
wrapper.vm.fileSelect({
130+
target: {
131+
files: [new File(['binary'], 'signature.png', { type: 'image/png' })],
132+
},
133+
} as unknown as Event)
134+
135+
expect(wrapper.vm.image).toBe('data:image/png;base64,loaded')
136+
})
137+
138+
it('clamps zoom level from cropper results', () => {
139+
const wrapper = mountComponent()
140+
141+
wrapper.vm.updateZoomLevelFromResult({
142+
visibleArea: { width: 100, height: 80, left: 0, top: 0 },
143+
image: { width: 900, height: 300 },
144+
})
145+
146+
expect(wrapper.vm.zoomLevel).toBe(8)
147+
})
148+
149+
it('zooms through the cropper instance and refreshes the zoom level', async () => {
150+
const zoom = vi.fn()
151+
const getResult = vi.fn(() => ({
152+
visibleArea: { width: 200, height: 80, left: 0, top: 0 },
153+
image: { width: 400, height: 160 },
154+
}))
155+
const wrapper = mountComponent()
156+
157+
wrapper.vm.cropper = { zoom, getResult }
158+
wrapper.vm.zoomBy(1.25)
159+
await wrapper.vm.$nextTick()
160+
161+
expect(zoom).toHaveBeenCalledWith(1.25)
162+
expect(wrapper.vm.zoomLevel).toBe(2)
163+
})
164+
165+
it('centers the image after fit-to-area completes', () => {
166+
const move = vi.fn()
167+
const wrapper = mountComponent()
168+
169+
wrapper.vm.cropper = { move }
170+
wrapper.vm.pendingFitCenter = true
171+
wrapper.vm.zoomLevel = 1
172+
173+
wrapper.vm.change({
174+
canvas: {
175+
toDataURL: () => 'data:image/png;base64,cropped',
176+
},
177+
visibleArea: { width: 100, height: 80, left: 0, top: 0 },
178+
image: { width: 100, height: 200 },
179+
})
180+
181+
expect(wrapper.vm.imageData).toBe('data:image/png;base64,cropped')
182+
expect(move).toHaveBeenCalledWith(0, 60)
183+
expect(wrapper.vm.pendingFitCenter).toBe(false)
184+
})
185+
186+
it('emits save with the cropped image and closes the modal', () => {
187+
const wrapper = mountComponent()
188+
189+
wrapper.vm.modal = true
190+
wrapper.vm.imageData = 'data:image/png;base64,signed'
191+
wrapper.vm.saveSignature()
192+
193+
expect(wrapper.vm.modal).toBe(false)
194+
expect(wrapper.emitted('save')).toEqual([['data:image/png;base64,signed']])
195+
})
196+
197+
it('opens and closes the confirmation dialog through actions', () => {
198+
const wrapper = mountComponent()
199+
200+
wrapper.vm.confirmSave()
201+
expect(wrapper.vm.modal).toBe(true)
202+
203+
wrapper.vm.cancel()
204+
expect(wrapper.vm.modal).toBe(false)
205+
})
206+
207+
it('emits close when the cancel action is requested', () => {
208+
const wrapper = mountComponent()
209+
210+
wrapper.vm.close()
211+
212+
expect(wrapper.emitted('close')).toEqual([[]])
213+
})
214+
215+
it('resets crop state when the image is cleared', async () => {
216+
const disconnect = vi.fn()
217+
const wrapper = mountComponent()
218+
219+
wrapper.vm.resizeObserver = { observe: vi.fn(), disconnect }
220+
wrapper.vm.containerWidth = 480
221+
wrapper.vm.zoomLevel = 2.4
222+
wrapper.vm.pendingFitCenter = true
223+
wrapper.vm.image = 'data:image/png;base64,existing'
224+
await wrapper.vm.$nextTick()
225+
226+
wrapper.vm.image = ''
227+
await wrapper.vm.$nextTick()
228+
229+
expect(disconnect).toHaveBeenCalled()
230+
expect(wrapper.vm.containerWidth).toBe(0)
231+
expect(wrapper.vm.zoomLevel).toBe(1)
232+
expect(wrapper.vm.pendingFitCenter).toBe(false)
233+
})
234+
})

0 commit comments

Comments
 (0)