-
-
Notifications
You must be signed in to change notification settings - Fork 111
Expand file tree
/
Copy pathValidation.spec.ts
More file actions
769 lines (665 loc) · 25 KB
/
Validation.spec.ts
File metadata and controls
769 lines (665 loc) · 25 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
/*
* SPDX-FileCopyrightText: 2026 LibreSign contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { afterEach, describe, expect, it, beforeEach, vi } from 'vitest'
import { shallowMount } from '@vue/test-utils'
import axios from '@nextcloud/axios'
import { getCapabilities } from '@nextcloud/capabilities'
import JSConfetti from 'js-confetti'
import Validation from '../../views/Validation.vue'
// Mock async components to prevent defineAsyncComponent from triggering
// pending Vite dev-server fetches that outlive the worker and cause
// "Closing rpc while fetch was pending" errors in Vitest.
vi.mock('../../components/validation/EnvelopeValidation.vue', () => ({ default: { template: '<div />' } }))
vi.mock('../../components/validation/FileValidation.vue', () => ({ default: { template: '<div />' } }))
vi.mock('../../components/validation/SigningProgress.vue', () => ({ default: { template: '<div />' } }))
// Mock js-confetti
vi.mock('js-confetti', () => ({
default: vi.fn(),
}))
// Mock @nextcloud packages
vi.mock('@nextcloud/axios', () => ({
default: {
get: vi.fn(),
post: vi.fn(),
put: vi.fn(),
delete: vi.fn(),
},
}))
vi.mock('@nextcloud/files', () => ({
formatFileSize: vi.fn((size) => `${size} B`),
}))
vi.mock('@nextcloud/auth', () => ({
getCurrentUser: vi.fn(() => ({
uid: 'test-user',
displayName: 'Test User',
})),
getRequestToken: vi.fn(() => 'test-csrf-token'),
}))
vi.mock('@nextcloud/logger', () => ({
getLogger: vi.fn(() => ({
error: vi.fn(),
warn: vi.fn(),
info: vi.fn(),
debug: vi.fn(),
})),
getLoggerBuilder: vi.fn(() => ({
setApp: vi.fn().mockReturnThis(),
detectUser: vi.fn().mockReturnThis(),
build: vi.fn(() => ({
error: vi.fn(),
warn: vi.fn(),
info: vi.fn(),
debug: vi.fn(),
})),
})),
}))
vi.mock('@nextcloud/l10n', () => ({
translate: vi.fn((app: string, text: string, vars?: Record<string, string>) => {
if (vars) {
return text.replace(/{(\w+)}/g, (_m: string, key: string) => vars[key as keyof typeof vars] || key)
}
return text
}),
translatePlural: vi.fn((app: string, singular: string, plural: string, count: number, vars?: Record<string, string>) => {
const template = count === 1 ? singular : plural
if (vars) {
return template.replace(/{(\w+)}/g, (_m: string, key: string) => vars[key as keyof typeof vars] || key)
}
return template
}),
t: vi.fn((app: string, text: string, vars?: Record<string, string>) => {
if (vars) {
return text.replace(/{(\w+)}/g, (_m: string, key: string) => vars[key as keyof typeof vars] || key)
}
return text
}),
n: vi.fn((app: string, singular: string, plural: string, count: number, vars?: Record<string, string>) => {
const template = count === 1 ? singular : plural
if (vars) {
return template.replace(/{(\w+)}/g, (_m: string, key: string) => vars[key as keyof typeof vars] || key)
}
return template
}),
getLanguage: vi.fn(() => 'en'),
getLocale: vi.fn(() => 'en'),
isRTL: vi.fn(() => false),
}))
// Mock router
const mockRoute = {
params: {},
query: {},
}
const mockRouter = {
push: vi.fn(),
}
// Mock capabilities - show-confetti enabled by default so existing tests pass
vi.mock('@nextcloud/capabilities', () => ({
getCapabilities: vi.fn(() => ({
libresign: {
config: {
'show-confetti': true,
},
},
})),
}))
// Mock initial state
vi.mock('@nextcloud/initial-state', () => ({
loadState: vi.fn((app, key, defaultValue) => defaultValue),
}))
// Mock router module
vi.mock('@nextcloud/router', () => ({
generateUrl: vi.fn((path) => path),
generateOcsUrl: vi.fn((path) => path),
}))
// Mock stores
vi.mock('../../store/sign.js', () => ({
useSignStore: vi.fn(() => ({
document: {},
})),
}))
vi.mock('../../store/sidebar.js', () => ({
useSidebarStore: vi.fn(() => ({
hideSidebar: vi.fn(),
})),
}))
// Mock utils
vi.mock('../../utils/viewer.js', () => ({
openDocument: vi.fn(),
}))
vi.mock('../../utils/fileStatus.js', () => ({
getStatusLabel: vi.fn((status) => `Status: ${status}`),
}))
describe('Validation.vue - Business Logic', () => {
let wrapper!: ReturnType<typeof shallowMount>
let mockAddConfetti: ReturnType<typeof vi.fn>
beforeEach(() => {
mockAddConfetti = vi.fn()
// Must use `function` syntax so vitest accepts it as a valid constructor mock
vi.mocked(JSConfetti).mockImplementation(function() {
return { addConfetti: mockAddConfetti }
} as unknown as typeof JSConfetti)
wrapper = shallowMount(Validation, {
mocks: {
$route: mockRoute,
$router: mockRouter,
},
stubs: {
NcActionButton: true,
NcActions: true,
NcAvatar: true,
NcButton: true,
NcDialog: true,
NcIconSvgWrapper: true,
NcListItem: true,
NcLoadingIcon: true,
NcNoteCard: true,
NcRichText: true,
NcTextField: true,
},
})
})
afterEach(() => {
wrapper.unmount()
})
describe('canValidate computed property', () => {
it('returns false when uuidToValidate is empty', () => {
wrapper.setData({ uuidToValidate: '' })
expect(wrapper.vm.canValidate).toBe(false)
})
it('accepts numeric IDs', () => {
wrapper.setData({ uuidToValidate: '12345' })
expect(wrapper.vm.canValidate).toBe(true)
})
it('accepts valid UUID format', () => {
wrapper.setData({ uuidToValidate: '550e8400-e29b-41d4-a716-446655440000' })
expect(wrapper.vm.canValidate).toBe(true)
})
it('rejects invalid UUID format', () => {
wrapper.setData({ uuidToValidate: 'invalid-uuid-format' })
expect(wrapper.vm.canValidate).toBe(false)
})
it('rejects UUID with wrong version (not v4)', () => {
wrapper.setData({ uuidToValidate: '550e8400-e29b-31d4-a716-446655440000' })
expect(wrapper.vm.canValidate).toBe(false)
})
it('rejects UUID with wrong variant', () => {
wrapper.setData({ uuidToValidate: '550e8400-e29b-41d4-1716-446655440000' })
expect(wrapper.vm.canValidate).toBe(false)
})
})
describe('helperTextValidation computed property', () => {
it('shows error message for invalid UUID', () => {
wrapper.setData({ uuidToValidate: 'invalid' })
expect(wrapper.vm.helperTextValidation).toBe('Invalid UUID')
})
it('returns empty string for valid UUID', () => {
wrapper.setData({ uuidToValidate: '550e8400-e29b-41d4-a716-446655440000' })
expect(wrapper.vm.helperTextValidation).toBe('')
})
it('returns empty string when uuidToValidate is empty', () => {
wrapper.setData({ uuidToValidate: '' })
expect(wrapper.vm.helperTextValidation).toBe('')
})
})
describe('isEnvelope computed property', () => {
it('returns true when nodeType is envelope', () => {
wrapper.setData({
document: { nodeType: 'envelope' },
})
expect(wrapper.vm.isEnvelope).toBe(true)
})
it('returns true when document has files array', () => {
wrapper.setData({
document: { files: [{ id: 1 }] },
})
expect(wrapper.vm.isEnvelope).toBe(true)
})
it('returns false when files array is empty', () => {
wrapper.setData({
document: { files: [] },
})
expect(wrapper.vm.isEnvelope).toBe(false)
})
it('returns false for regular document', () => {
wrapper.setData({
document: { nodeType: 'file' },
})
expect(wrapper.vm.isEnvelope).toBe(false)
})
})
describe('async signing rendering', () => {
it('does not render Promise text in async signing mode', async () => {
await wrapper.setData({
isAsyncSigning: true,
})
expect(wrapper.html()).not.toContain('[object Promise]')
})
})
describe('getValidityStatus method', () => {
it('returns unknown when valid_to is missing', () => {
const signer = {}
expect(wrapper.vm.getValidityStatus(signer)).toBe('unknown')
})
it('returns expired when certificate has expired', () => {
const pastDate = new Date()
pastDate.setFullYear(pastDate.getFullYear() - 1)
const signer = { valid_to: pastDate.toISOString() }
expect(wrapper.vm.getValidityStatus(signer)).toBe('expired')
})
it('returns expiring when certificate expires within 30 days', () => {
const soonDate = new Date()
soonDate.setDate(soonDate.getDate() + 15) // 15 days from now
const signer = { valid_to: soonDate.toISOString() }
expect(wrapper.vm.getValidityStatus(signer)).toBe('expiring')
})
it('returns valid when certificate expires in more than 30 days', () => {
const futureDate = new Date()
futureDate.setDate(futureDate.getDate() + 60) // 60 days from now
const signer = { valid_to: futureDate.toISOString() }
expect(wrapper.vm.getValidityStatus(signer)).toBe('valid')
})
it('returns expired for certificate expiring today', () => {
const today = new Date()
today.setHours(0, 0, 0, 0)
const signer = { valid_to: today.toISOString() }
expect(wrapper.vm.getValidityStatus(signer)).toBe('expired')
})
})
describe('getValidityStatusAtSigning method', () => {
it('returns unknown when signed date is missing', () => {
const signer = {
valid_from: '2024-01-01T00:00:00Z',
valid_to: '2025-01-01T00:00:00Z',
}
expect(wrapper.vm.getValidityStatusAtSigning(signer)).toBe('unknown')
})
it('returns unknown when valid_from is missing', () => {
const signer = {
signed: '2024-06-01T00:00:00Z',
valid_to: '2025-01-01T00:00:00Z',
}
expect(wrapper.vm.getValidityStatusAtSigning(signer)).toBe('unknown')
})
it('returns unknown when valid_to is missing', () => {
const signer = {
signed: '2024-06-01T00:00:00Z',
valid_from: '2024-01-01T00:00:00Z',
}
expect(wrapper.vm.getValidityStatusAtSigning(signer)).toBe('unknown')
})
it('returns valid when signed within validity period', () => {
const signer = {
signed: '2024-06-01T12:00:00Z',
valid_from: '2024-01-01T00:00:00Z',
valid_to: '2025-01-01T00:00:00Z',
}
expect(wrapper.vm.getValidityStatusAtSigning(signer)).toBe('valid')
})
it('returns expired when signed before valid_from', () => {
const signer = {
signed: '2023-12-31T23:59:59Z',
valid_from: '2024-01-01T00:00:00Z',
valid_to: '2025-01-01T00:00:00Z',
}
expect(wrapper.vm.getValidityStatusAtSigning(signer)).toBe('expired')
})
it('returns expired when signed after valid_to', () => {
const signer = {
signed: '2025-01-02T00:00:00Z',
valid_from: '2024-01-01T00:00:00Z',
valid_to: '2025-01-01T00:00:00Z',
}
expect(wrapper.vm.getValidityStatusAtSigning(signer)).toBe('expired')
})
it('returns valid when signed at exact valid_from time', () => {
const timestamp = '2024-01-01T00:00:00Z'
const signer = {
signed: timestamp,
valid_from: timestamp,
valid_to: '2025-01-01T00:00:00Z',
}
expect(wrapper.vm.getValidityStatusAtSigning(signer)).toBe('valid')
})
it('returns valid when signed at exact valid_to time', () => {
const timestamp = '2025-01-01T00:00:00Z'
const signer = {
signed: timestamp,
valid_from: '2024-01-01T00:00:00Z',
valid_to: timestamp,
}
expect(wrapper.vm.getValidityStatusAtSigning(signer)).toBe('valid')
})
})
describe('hasValidationIssues method', () => {
it('returns true when signature validation failed', () => {
const signer = {
signature_validation: { id: 2, label: 'Invalid' },
}
expect(wrapper.vm.hasValidationIssues(signer)).toBe(true)
})
it('returns true when certificate validation failed', () => {
const signer = {
signature_validation: { id: 1, label: 'Valid' },
certificate_validation: { id: 2, label: 'Untrusted' },
}
expect(wrapper.vm.hasValidationIssues(signer)).toBe(true)
})
it('returns true when certificate is revoked', () => {
const signer = {
signature_validation: { id: 1, label: 'Valid' },
certificate_validation: { id: 1, label: 'Trusted' },
crl_validation: 'revoked',
}
expect(wrapper.vm.hasValidationIssues(signer)).toBe(true)
})
it('returns true when certificate was invalid at signing time', () => {
const signer = {
signature_validation: { id: 1, label: 'Valid' },
certificate_validation: { id: 1, label: 'Trusted' },
signed: '2023-12-31T23:59:59Z',
valid_from: '2024-01-01T00:00:00Z',
valid_to: '2025-01-01T00:00:00Z',
}
expect(wrapper.vm.hasValidationIssues(signer)).toBe(true)
})
it('returns true when certificate is currently expired', () => {
const pastDate = new Date()
pastDate.setFullYear(pastDate.getFullYear() - 1)
const signer = {
signature_validation: { id: 1, label: 'Valid' },
certificate_validation: { id: 1, label: 'Trusted' },
valid_to: pastDate.toISOString(),
}
expect(wrapper.vm.hasValidationIssues(signer)).toBe(true)
})
it('returns true when certificate is expiring soon', () => {
const soonDate = new Date()
soonDate.setDate(soonDate.getDate() + 15)
const signer = {
signature_validation: { id: 1, label: 'Valid' },
certificate_validation: { id: 1, label: 'Trusted' },
valid_to: soonDate.toISOString(),
}
expect(wrapper.vm.hasValidationIssues(signer)).toBe(true)
})
it('returns false when all validations pass', () => {
const futureDate = new Date()
futureDate.setDate(futureDate.getDate() + 60)
const signer = {
signature_validation: { id: 1, label: 'Valid' },
certificate_validation: { id: 1, label: 'Trusted' },
crl_validation: 'valid',
valid_to: futureDate.toISOString(),
}
expect(wrapper.vm.hasValidationIssues(signer)).toBe(false)
})
})
describe('camelCaseToTitleCase method', () => {
it('converts camelCase to Title Case', () => {
expect(wrapper.vm.camelCaseToTitleCase('camelCase')).toBe('Camel Case')
})
it('converts PascalCase to Title Case', () => {
expect(wrapper.vm.camelCaseToTitleCase('PascalCase')).toBe('Pascal Case')
})
it('handles multiple capital letters', () => {
expect(wrapper.vm.camelCaseToTitleCase('XMLHttpRequest')).toBe('XML Http Request')
})
it('capitalizes first letter of already spaced text', () => {
expect(wrapper.vm.camelCaseToTitleCase('already spaced')).toBe('Already spaced')
})
it('handles single word', () => {
expect(wrapper.vm.camelCaseToTitleCase('word')).toBe('Word')
})
it('handles empty string', () => {
expect(wrapper.vm.camelCaseToTitleCase('')).toBe('')
})
})
describe('getName method', () => {
it('returns displayName when available', () => {
const signer = { displayName: 'John Doe', email: '[email protected]' }
expect(wrapper.vm.getName(signer)).toBe('John Doe')
})
it('returns email when displayName is not available', () => {
const signer = { email: '[email protected]' }
expect(wrapper.vm.getName(signer)).toBe('[email protected]')
})
it('returns signature validation label when neither displayName nor email available', () => {
const signer = { signature_validation: { label: 'Certificate CN' } }
expect(wrapper.vm.getName(signer)).toBe('Certificate CN')
})
it('returns Unknown when no identification available', () => {
const signer = {}
expect(wrapper.vm.getName(signer)).toBe('Unknown')
})
})
describe('EXPIRATION_WARNING_DAYS constant', () => {
it('is set to 30 days by default', () => {
expect(wrapper.vm.EXPIRATION_WARNING_DAYS).toBe(30)
})
})
// Vue Router 5 only preserves params that are part of the route path.
// Routes like /f/validation/:uuid only have :uuid in the path.
// State flags (isAfterSigned, isAsync) must travel via history.state,
// not via route params — otherwise Vue Router 5 silently drops them.
describe('isAfterSigned computed property - reads from history.state', () => {
afterEach(() => {
history.replaceState({}, '')
})
it('returns true when history.state.isAfterSigned is true', () => {
history.pushState({ isAfterSigned: true }, '')
expect(wrapper.vm.isAfterSigned).toBe(true)
})
it('returns false when history.state.isAfterSigned is false', () => {
history.pushState({ isAfterSigned: false }, '')
expect(wrapper.vm.isAfterSigned).toBe(false)
})
it('falls back to shouldFireAsyncConfetti when history.state has no isAfterSigned', async () => {
history.pushState({}, '')
await wrapper.setData({ shouldFireAsyncConfetti: true })
expect(wrapper.vm.isAfterSigned).toBe(true)
})
it('returns false when history state has no isAfterSigned and shouldFireAsyncConfetti is false', () => {
history.pushState({}, '')
expect(wrapper.vm.isAfterSigned).toBe(false)
})
})
// Vue Router 5 drops non-path params on navigation. isAsync must travel
// via history.state so it survives the push from the signing page.
describe('created() - async signing activation from history.state', () => {
const UUID = '550e8400-e29b-41d4-a716-446655440000'
let stateGetter: ReturnType<typeof vi.spyOn>
let localWrapper: ReturnType<typeof shallowMount> | null = null
beforeEach(() => {
// Prevent the validate() floating Promise from crashing on
// the undefined-return of the axios.get mock
vi.mocked(axios.get).mockResolvedValue({ data: { ocs: { data: {} } } })
})
afterEach(() => {
localWrapper?.unmount()
localWrapper = null
stateGetter?.mockRestore()
vi.mocked(axios.get).mockReset()
})
// REGRESSION TEST: before the fix, Validation.vue checked $route.params.isAsync.
// Vue Router 5 silently drops params not in the route path, so that check
// was always false — confetti never fired.
// After the fix, isAsync is read from history.state (passed via router's `state:`).
// This test verifies the OLD trigger (route params) no longer activates async signing.
it('does NOT set isAsyncSigning via $route.params (Vue Router 5 drops non-path params)', () => {
stateGetter = vi.spyOn(window.history, 'state', 'get').mockReturnValue({} as any)
localWrapper = shallowMount(Validation, {
mocks: {
$route: { params: { uuid: UUID, isAsync: true }, query: {} },
$router: { ...mockRouter, replace: vi.fn() },
},
})
// $route.params.isAsync is true in the mock, BUT the fixed code no longer reads
// from params — it reads from history.state (which is empty here).
expect(localWrapper.vm.isAsyncSigning).toBe(false)
expect(localWrapper.vm.shouldFireAsyncConfetti).toBe(false)
})
it('does not set isAsyncSigning when history.state has no isAsync flag', () => {
stateGetter = vi.spyOn(window.history, 'state', 'get').mockReturnValue({} as any)
localWrapper = shallowMount(Validation, {
mocks: {
$route: { params: { uuid: UUID }, query: {} },
$router: { ...mockRouter, replace: vi.fn() },
},
})
expect(localWrapper.vm.isAsyncSigning).toBe(false)
expect(localWrapper.vm.shouldFireAsyncConfetti).toBe(false)
})
})
describe('handleValidationSuccess - confetti behavior', () => {
// FILE_STATUS.SIGNED = 3
const SIGNED_STATUS = 3
it('fires confetti when document is signed and isAfterSigned returns true', () => {
// Spy on the computed getter to simulate the route-param path
// (Vue 3 mocked $route.params lacks reactivity in test env — covered separately)
vi.spyOn(wrapper.vm, 'isAfterSigned', 'get').mockReturnValue(true)
wrapper.vm.handleValidationSuccess({ status: SIGNED_STATUS, signers: [] })
expect(mockAddConfetti).toHaveBeenCalledOnce()
})
it('fires confetti when document is signed and shouldFireAsyncConfetti is true', async () => {
await wrapper.setData({ shouldFireAsyncConfetti: true })
wrapper.vm.handleValidationSuccess({ status: SIGNED_STATUS, signers: [] })
expect(mockAddConfetti).toHaveBeenCalledOnce()
})
it('fires confetti when all files in envelope are signed and shouldFireAsyncConfetti is true', async () => {
await wrapper.setData({ shouldFireAsyncConfetti: true })
wrapper.vm.handleValidationSuccess({
status: 0,
files: [
{ status: SIGNED_STATUS },
{ status: SIGNED_STATUS },
],
signers: [],
})
expect(mockAddConfetti).toHaveBeenCalledOnce()
})
it('fires confetti when current signer is signed and shouldFireAsyncConfetti is true', async () => {
await wrapper.setData({ shouldFireAsyncConfetti: true })
// SIGN_REQUEST_STATUS.SIGNED = 2
wrapper.vm.handleValidationSuccess({
status: 0,
signers: [{ me: true, status: 2 }],
})
expect(mockAddConfetti).toHaveBeenCalledOnce()
})
it('does not fire confetti when document is not signed even if isAfterSigned is true', () => {
const lw = shallowMount(Validation, {
mocks: {
$route: { params: { isAfterSigned: true }, query: {} },
$router: mockRouter,
},
})
lw.vm.handleValidationSuccess({ status: 1, signers: [] })
expect(mockAddConfetti).not.toHaveBeenCalled()
lw.unmount()
})
it('does not fire confetti when document is signed but neither isAfterSigned nor shouldFireAsyncConfetti is true', () => {
wrapper.vm.handleValidationSuccess({ status: SIGNED_STATUS, signers: [] })
expect(mockAddConfetti).not.toHaveBeenCalled()
})
it('resets shouldFireAsyncConfetti to false after firing confetti', async () => {
await wrapper.setData({ shouldFireAsyncConfetti: true })
wrapper.vm.handleValidationSuccess({ status: SIGNED_STATUS, signers: [] })
expect(wrapper.vm.shouldFireAsyncConfetti).toBe(false)
})
it('does not reset shouldFireAsyncConfetti when confetti is not fired', async () => {
await wrapper.setData({ shouldFireAsyncConfetti: true })
// document not signed → confetti won't fire
wrapper.vm.handleValidationSuccess({ status: 1, signers: [] })
expect(wrapper.vm.shouldFireAsyncConfetti).toBe(true)
})
it('does not fire confetti when isActiveView is false', async () => {
await wrapper.setData({ shouldFireAsyncConfetti: true, isActiveView: false })
wrapper.vm.handleValidationSuccess({ status: SIGNED_STATUS, signers: [] })
expect(mockAddConfetti).not.toHaveBeenCalled()
})
it('does not fire confetti when show-confetti capability is disabled', async () => {
vi.mocked(getCapabilities).mockReturnValueOnce({
libresign: {
config: {
'show-confetti': false,
},
},
} as ReturnType<typeof getCapabilities>)
vi.spyOn(wrapper.vm, 'isAfterSigned', 'get').mockReturnValue(true)
wrapper.vm.handleValidationSuccess({ status: SIGNED_STATUS, signers: [] })
expect(mockAddConfetti).not.toHaveBeenCalled()
})
})
describe('handleSigningComplete method', () => {
// FILE_STATUS.SIGNED = 3
const SIGNED_STATUS = 3
// SIGN_REQUEST_STATUS.SIGNED = 2
const SIGNER_SIGNED_STATUS = 2
it('sets isAsyncSigning to false when called', async () => {
await wrapper.setData({ isAsyncSigning: true })
vi.spyOn(wrapper.vm, 'refreshAfterAsyncSigning').mockResolvedValue(undefined)
wrapper.vm.handleSigningComplete(null)
expect(wrapper.vm.isAsyncSigning).toBe(false)
})
it('sets shouldFireAsyncConfetti to true when called', async () => {
vi.spyOn(wrapper.vm, 'refreshAfterAsyncSigning').mockResolvedValue(undefined)
wrapper.vm.handleSigningComplete(null)
expect(wrapper.vm.shouldFireAsyncConfetti).toBe(true)
})
it('does nothing when isActiveView is false', async () => {
await wrapper.setData({ isAsyncSigning: true, isActiveView: false })
wrapper.vm.handleSigningComplete(null)
expect(wrapper.vm.isAsyncSigning).toBe(true)
expect(wrapper.vm.shouldFireAsyncConfetti).toBe(false)
})
describe('RULE: when a file is returned directly by SigningProgress', () => {
it('fires confetti when the file has signed status', () => {
const signedFile = { status: SIGNED_STATUS, signers: [] }
wrapper.vm.handleSigningComplete(signedFile)
expect(mockAddConfetti).toHaveBeenCalledOnce()
})
it('fires confetti when the current signer is marked as signed', () => {
const fileWithSignedSigner = {
status: 1,
signers: [{ me: true, status: SIGNER_SIGNED_STATUS, signed: '2025-01-01T00:00:00Z' }],
}
wrapper.vm.handleSigningComplete(fileWithSignedSigner)
expect(mockAddConfetti).toHaveBeenCalledOnce()
})
it('does not fire confetti when the file is not yet signed and no signer is marked as signed', () => {
// This is a realistic scenario: SigningProgress emits 'completed'
// with a file object whose status is still pending/partial
const unsignedFile = { status: 1, signers: [{ me: true, status: 0 }] }
wrapper.vm.handleSigningComplete(unsignedFile)
expect(mockAddConfetti).not.toHaveBeenCalled()
})
})
describe('RULE: when SigningProgress emits completed without a file (async polling path)', () => {
it('fires confetti after polling returns a signed document', async () => {
await wrapper.setData({ uuidToValidate: '550e8400-e29b-41d4-a716-446655440000' })
// Simulate the validate call returning a signed document via handleValidationSuccess
vi.spyOn(wrapper.vm, 'validate').mockImplementation(async () => {
wrapper.vm.handleValidationSuccess({ status: SIGNED_STATUS, signers: [] })
})
wrapper.vm.handleSigningComplete(null)
// Wait for the async polling loop to run
await new Promise(resolve => setTimeout(resolve, 0))
expect(mockAddConfetti).toHaveBeenCalledOnce()
})
it('fires confetti after polling finds that the current signer is signed', async () => {
await wrapper.setData({ uuidToValidate: '550e8400-e29b-41d4-a716-446655440000' })
vi.spyOn(wrapper.vm, 'validate').mockImplementation(async () => {
wrapper.vm.handleValidationSuccess({
status: 1,
signers: [{ me: true, status: SIGNER_SIGNED_STATUS, signed: '2025-01-01T00:00:00Z' }],
})
})
wrapper.vm.handleSigningComplete(null)
await new Promise(resolve => setTimeout(resolve, 0))
expect(mockAddConfetti).toHaveBeenCalledOnce()
})
})
})
})