Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
43c727b
refactor(vue3): migrate RequestSignatureTab to script setup ts
vitormattos Mar 6, 2026
e4b740b
test(vue3): adapt RequestSignatureTab spec for script setup
vitormattos Mar 6, 2026
90e621c
test(vue3): add CertificateCustonOptions coverage
vitormattos Mar 6, 2026
af17fc1
refactor(vue3): migrate CertificateCustonOptions to script setup ts
vitormattos Mar 6, 2026
9958030
test(vue3): adapt Validation tests for script setup
vitormattos Mar 6, 2026
913d1aa
refactor(vue3): migrate Validation to script setup ts
vitormattos Mar 6, 2026
67a4a88
test(vue3): add PdfEditor Signature coverage
vitormattos Mar 6, 2026
ec9ad92
refactor(vue3): migrate PdfEditor Signature to script setup ts
vitormattos Mar 6, 2026
1bfaffe
fix(vue3): resolve Validation dynamic component render
vitormattos Mar 6, 2026
35fac9f
test(vue3): add settings Validation coverage
vitormattos Mar 6, 2026
1f69cb8
refactor(vue3): migrate settings Validation to script setup ts
vitormattos Mar 6, 2026
94146a6
refactor(vue3): migrate PdfEditor to script setup ts
vitormattos Mar 6, 2026
964b32d
refactor(vue3): migrate FileUpload to script setup ts
vitormattos Mar 6, 2026
86eb62c
test(vue3): normalize FileUpload spec newline
vitormattos Mar 6, 2026
78cfc94
test(vue3): harden SigningProgress l10n mock
vitormattos Mar 6, 2026
4890192
refactor(vue3): migrate TSA settings to script setup ts
vitormattos Mar 6, 2026
d0e54bc
fix(vue3): guard TSA error mappings
vitormattos Mar 6, 2026
ac61edf
refactor(vue3): migrate OpenSSL root certificate settings to script s…
vitormattos Mar 6, 2026
493a3bb
refactor(vue3): migrate CFSSL root certificate settings to script set…
vitormattos Mar 6, 2026
027e780
test: normalize Validation spec newline
vitormattos Mar 6, 2026
ce6b7ab
refactor(vue3): migrate SignatureStamp to script setup ts
vitormattos Mar 6, 2026
38fb2e3
test(vue3): add SignatureStamp view coverage
vitormattos Mar 6, 2026
3096930
refactor(vue3): migrate CrlManagement to script setup ts
vitormattos Mar 6, 2026
0d6301c
test(vue3): add CrlManagement view coverage
vitormattos Mar 6, 2026
0ec2552
fix(vue3): preserve modal URL contract in RequestSignatureTab
vitormattos Mar 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
537 changes: 309 additions & 228 deletions src/components/Draw/FileUpload.vue

Large diffs are not rendered by default.

582 changes: 331 additions & 251 deletions src/components/PdfEditor/PdfEditor.vue

Large diffs are not rendered by default.

488 changes: 264 additions & 224 deletions src/components/PdfEditor/Signature.vue

Large diffs are not rendered by default.

1,465 changes: 708 additions & 757 deletions src/components/RightSidebar/RequestSignatureTab.vue

Large diffs are not rendered by default.

234 changes: 234 additions & 0 deletions src/tests/components/Draw/FileUpload.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
/**
* SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { beforeEach, describe, expect, it, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import FileUpload from '../../../components/Draw/FileUpload.vue'

vi.mock('@nextcloud/l10n', () => ({
t: vi.fn((_app: string, text: string) => text),
}))

vi.mock('@nextcloud/capabilities', () => ({
getCapabilities: vi.fn(() => ({
libresign: {
config: {
'sign-elements': {
'signature-width': 700,
'signature-height': 200,
},
},
},
})),
}))

vi.mock('@nextcloud/vue/components/NcButton', () => ({
default: {
name: 'NcButton',
template: '<button @click="$emit(\'click\')"><slot /><slot name="icon" /></button>',
props: ['disabled', 'variant', 'wide', 'ariaLabel', 'title'],
emits: ['click'],
},
}))

vi.mock('@nextcloud/vue/components/NcDialog', () => ({
default: {
name: 'NcDialog',
template: '<div><slot /><slot name="actions" /></div>',
props: ['name', 'contentClasses'],
emits: ['closing'],
},
}))

vi.mock('@nextcloud/vue/components/NcTextField', () => ({
default: {
name: 'NcTextField',
template: '<input :value="modelValue" @input="$emit(\'update:modelValue\', $event.target.value)" />',
props: ['modelValue', 'label', 'disabled', 'type', 'min', 'max', 'step'],
emits: ['update:modelValue'],
},
}))

vi.mock('@nextcloud/vue/components/NcIconSvgWrapper', () => ({
default: {
name: 'NcIconSvgWrapper',
template: '<span class="icon-stub" />',
props: ['path', 'size'],
},
}))

vi.mock('vue-advanced-cropper', () => ({
Cropper: {
name: 'Cropper',
template: '<div class="cropper-stub" />',
props: ['src', 'defaultSize', 'stencilProps', 'imageRestriction'],
emits: ['change'],
methods: {
zoom: vi.fn(),
move: vi.fn(),
getResult: vi.fn(() => ({
visibleArea: { width: 200, height: 80, left: 0, top: 0 },
image: { width: 400, height: 160 },
})),
},
},
}))

describe('FileUpload.vue - Uploaded signature flow', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.stubGlobal('ResizeObserver', class {
observe = vi.fn()
disconnect = vi.fn()
})
})

function mountComponent() {
return mount(FileUpload)
}

it('initializes stencil dimensions from capabilities', () => {
const wrapper = mountComponent()

expect(wrapper.vm.stencilBaseWidth).toBe(700)
expect(wrapper.vm.stencilBaseHeight).toBe(200)
expect(wrapper.vm.defaultStencilSize).toEqual({ width: 700, height: 200 })
})

it('scales the default stencil size to fit the cropper container', async () => {
const wrapper = mountComponent()

wrapper.vm.containerWidth = 374
await wrapper.vm.$nextTick()

expect(wrapper.vm.defaultStencilSize).toEqual({ width: 350, height: 100 })
})

it('loads the selected file as a data URL', () => {
const wrapper = mountComponent()
const listeners = new Map<string, Array<() => void>>()

class FileReaderMock {
result: string | null = 'data:image/png;base64,loaded'

addEventListener(event: string, callback: () => void) {
listeners.set(event, [...(listeners.get(event) || []), callback])
}

readAsDataURL() {
for (const callback of listeners.get('load') || []) {
callback()
}
}
}

vi.stubGlobal('FileReader', FileReaderMock)

wrapper.vm.fileSelect({
target: {
files: [new File(['binary'], 'signature.png', { type: 'image/png' })],
},
} as unknown as Event)

expect(wrapper.vm.image).toBe('data:image/png;base64,loaded')
})

it('clamps zoom level from cropper results', () => {
const wrapper = mountComponent()

wrapper.vm.updateZoomLevelFromResult({
visibleArea: { width: 100, height: 80, left: 0, top: 0 },
image: { width: 900, height: 300 },
})

expect(wrapper.vm.zoomLevel).toBe(8)
})

it('zooms through the cropper instance and refreshes the zoom level', async () => {
const zoom = vi.fn()
const getResult = vi.fn(() => ({
visibleArea: { width: 200, height: 80, left: 0, top: 0 },
image: { width: 400, height: 160 },
}))
const wrapper = mountComponent()

wrapper.vm.cropper = { zoom, getResult }
wrapper.vm.zoomBy(1.25)
await wrapper.vm.$nextTick()

expect(zoom).toHaveBeenCalledWith(1.25)
expect(wrapper.vm.zoomLevel).toBe(2)
})

it('centers the image after fit-to-area completes', () => {
const move = vi.fn()
const wrapper = mountComponent()

wrapper.vm.cropper = { move }
wrapper.vm.pendingFitCenter = true
wrapper.vm.zoomLevel = 1

wrapper.vm.change({
canvas: {
toDataURL: () => 'data:image/png;base64,cropped',
},
visibleArea: { width: 100, height: 80, left: 0, top: 0 },
image: { width: 100, height: 200 },
})

expect(wrapper.vm.imageData).toBe('data:image/png;base64,cropped')
expect(move).toHaveBeenCalledWith(0, 60)
expect(wrapper.vm.pendingFitCenter).toBe(false)
})

it('emits save with the cropped image and closes the modal', () => {
const wrapper = mountComponent()

wrapper.vm.modal = true
wrapper.vm.imageData = 'data:image/png;base64,signed'
wrapper.vm.saveSignature()

expect(wrapper.vm.modal).toBe(false)
expect(wrapper.emitted('save')).toEqual([['data:image/png;base64,signed']])
})

it('opens and closes the confirmation dialog through actions', () => {
const wrapper = mountComponent()

wrapper.vm.confirmSave()
expect(wrapper.vm.modal).toBe(true)

wrapper.vm.cancel()
expect(wrapper.vm.modal).toBe(false)
})

it('emits close when the cancel action is requested', () => {
const wrapper = mountComponent()

wrapper.vm.close()

expect(wrapper.emitted('close')).toEqual([[]])
})

it('resets crop state when the image is cleared', async () => {
const disconnect = vi.fn()
const wrapper = mountComponent()

wrapper.vm.resizeObserver = { observe: vi.fn(), disconnect }
wrapper.vm.containerWidth = 480
wrapper.vm.zoomLevel = 2.4
wrapper.vm.pendingFitCenter = true
wrapper.vm.image = 'data:image/png;base64,existing'
await wrapper.vm.$nextTick()

wrapper.vm.image = ''
await wrapper.vm.$nextTick()

expect(disconnect).toHaveBeenCalled()
expect(wrapper.vm.containerWidth).toBe(0)
expect(wrapper.vm.zoomLevel).toBe(1)
expect(wrapper.vm.pendingFitCenter).toBe(false)
})
})
Loading
Loading