1+ /*
2+ * SPDX-FileCopyrightText: 2026 LibreSign contributors
3+ * SPDX-License-Identifier: AGPL-3.0-or-later
4+ */
5+
6+ import { describe , expect , it } from 'vitest'
7+
8+ import {
9+ buildPdfEditorSignerPayload ,
10+ calculatePdfPlacement ,
11+ createPdfEditorObject ,
12+ createPdfObjectId ,
13+ findPdfObjectLocation ,
14+ getPdfEditorSignerId ,
15+ getPdfEditorSignerLabel ,
16+ resolvePdfEditorSignerChange ,
17+ } from '../../../components/PdfEditor/pdfEditorModel'
18+
19+ describe ( 'pdfEditorModel' , ( ) => {
20+ describe ( 'RULE: signer identity is derived from a single stable key' , ( ) => {
21+ it ( 'prefers signRequestId over email' , ( ) => {
22+ expect ( getPdfEditorSignerId ( { signRequestId : 15 , email : 'fallback@example.com' } ) ) . toBe ( '15' )
23+ } )
24+
25+ it ( 'falls back to email when signRequestId is absent' , ( ) => {
26+ expect ( getPdfEditorSignerId ( { email : 'fallback@example.com' } ) ) . toBe ( 'fallback@example.com' )
27+ } )
28+
29+ it ( 'returns empty string when signer has no identity' , ( ) => {
30+ expect ( getPdfEditorSignerId ( { } ) ) . toBe ( '' )
31+ } )
32+ } )
33+
34+ describe ( 'RULE: signer labels come from a single presentation rule' , ( ) => {
35+ it ( 'prefers displayName' , ( ) => {
36+ expect ( getPdfEditorSignerLabel ( { displayName : 'Ada' , email : 'ada@example.com' , signRequestId : 9 } ) ) . toBe ( 'Ada' )
37+ } )
38+
39+ it ( 'falls back to identity fields' , ( ) => {
40+ expect ( getPdfEditorSignerLabel ( { email : 'ada@example.com' } ) ) . toBe ( 'ada@example.com' )
41+ expect ( getPdfEditorSignerLabel ( { signRequestId : 9 } ) ) . toBe ( '9' )
42+ } )
43+ } )
44+
45+ describe ( 'RULE: signer payload cloning never mutates caller input' , ( ) => {
46+ it ( 'ensures a detached element object exists' , ( ) => {
47+ const source = { email : 'ada@example.com' }
48+
49+ const payload = buildPdfEditorSignerPayload ( source )
50+
51+ expect ( payload ) . toEqual ( { email : 'ada@example.com' , element : { } } )
52+ expect ( payload ) . not . toBe ( source )
53+ } )
54+
55+ it ( 'preserves placement data by value' , ( ) => {
56+ const source = {
57+ email : 'ada@example.com' ,
58+ element : {
59+ elementId : 9 ,
60+ coordinates : { page : 1 , left : 10 , top : 20 } ,
61+ } ,
62+ }
63+
64+ const payload = buildPdfEditorSignerPayload ( source )
65+
66+ expect ( payload . element ) . toEqual ( source . element )
67+ expect ( payload . element ) . not . toBe ( source . element )
68+ } )
69+ } )
70+
71+ describe ( 'RULE: object lookup is a pure document traversal' , ( ) => {
72+ it ( 'finds the object document and page indexes' , ( ) => {
73+ const location = findPdfObjectLocation ( [
74+ { allObjects : [ [ { id : 'obj-1' } ] , [ { id : 'obj-2' } ] ] } ,
75+ { allObjects : [ [ { id : 'obj-3' } ] ] } ,
76+ ] , 'obj-2' )
77+
78+ expect ( location ) . toEqual ( { docIndex : 0 , pageIndex : 1 } )
79+ } )
80+
81+ it ( 'returns null when the object does not exist' , ( ) => {
82+ expect ( findPdfObjectLocation ( [ { allObjects : [ [ { id : 'obj-1' } ] ] } ] , 'missing' ) ) . toBeNull ( )
83+ } )
84+ } )
85+
86+ describe ( 'RULE: signer replacement is resolved without component state mutation' , ( ) => {
87+ it ( 'returns the next signer and preserves current placement' , ( ) => {
88+ const result = resolvePdfEditorSignerChange ( {
89+ availableSigners : [
90+ { signRequestId : 1 , displayName : 'One' } ,
91+ { signRequestId : 2 , email : 'two@example.com' } ,
92+ ] ,
93+ selectedSigner : { signRequestId : 2 } ,
94+ object : {
95+ id : 'obj-1' ,
96+ signer : {
97+ signRequestId : 1 ,
98+ element : { elementId : 99 , coordinates : { page : 1 , left : 10 , top : 20 } } ,
99+ } ,
100+ } ,
101+ documents : [ { allObjects : [ [ { id : 'obj-1' } ] ] } ] ,
102+ } )
103+
104+ expect ( result ) . toEqual ( {
105+ docIndex : 0 ,
106+ signer : {
107+ signRequestId : 2 ,
108+ email : 'two@example.com' ,
109+ displayName : '2' ,
110+ element : { elementId : 99 , coordinates : { page : 1 , left : 10 , top : 20 } } ,
111+ } ,
112+ } )
113+ } )
114+
115+ it ( 'returns null when no target signer can be resolved' , ( ) => {
116+ const result = resolvePdfEditorSignerChange ( {
117+ availableSigners : [ { signRequestId : 1 , displayName : 'One' } ] ,
118+ selectedSigner : { signRequestId : 3 } ,
119+ object : { id : 'obj-1' } ,
120+ documents : [ ] ,
121+ } )
122+
123+ expect ( result ) . toBeNull ( )
124+ } )
125+ } )
126+
127+ describe ( 'RULE: placement calculation is isolated from component orchestration' , ( ) => {
128+ it ( 'uses left/top coordinates directly' , ( ) => {
129+ const placement = calculatePdfPlacement ( {
130+ signer : {
131+ element : {
132+ documentIndex : 1 ,
133+ coordinates : { page : 2 , left : 10 , top : 20 , width : 30 , height : 40 } ,
134+ } ,
135+ } ,
136+ defaultDocIndex : 0 ,
137+ pageHeight : 800 ,
138+ } )
139+
140+ expect ( placement ) . toEqual ( {
141+ docIndex : 1 ,
142+ pageIndex : 1 ,
143+ x : 10 ,
144+ y : 20 ,
145+ width : 30 ,
146+ height : 40 ,
147+ } )
148+ } )
149+
150+ it ( 'converts PDF coordinate boxes into canvas placement' , ( ) => {
151+ const placement = calculatePdfPlacement ( {
152+ signer : {
153+ element : {
154+ coordinates : { page : 1 , llx : 50 , lly : 100 , urx : 250 , ury : 700 } ,
155+ } ,
156+ } ,
157+ defaultDocIndex : 0 ,
158+ pageHeight : 841.89 ,
159+ } )
160+
161+ expect ( placement ) . toEqual ( {
162+ docIndex : 0 ,
163+ pageIndex : 0 ,
164+ x : 50 ,
165+ y : 141.89 ,
166+ width : 200 ,
167+ height : 600 ,
168+ } )
169+ } )
170+
171+ it ( 'returns null when page number is invalid' , ( ) => {
172+ expect ( calculatePdfPlacement ( { signer : { element : { coordinates : { left : 10 , top : 10 } } } , defaultDocIndex : 0 , pageHeight : 500 } ) ) . toBeNull ( )
173+ } )
174+ } )
175+
176+ describe ( 'RULE: object creation is deterministic except for id generation' , ( ) => {
177+ it ( 'builds a signature object from signer and placement' , ( ) => {
178+ const signer = { email : 'ada@example.com' }
179+ const object = createPdfEditorObject ( {
180+ signer,
181+ placement : { docIndex : 0 , pageIndex : 0 , x : 10 , y : 20 , width : 30 , height : 40 } ,
182+ objectId : 'obj-fixed' ,
183+ } )
184+
185+ expect ( object ) . toEqual ( {
186+ id : 'obj-fixed' ,
187+ type : 'signature' ,
188+ signer,
189+ x : 10 ,
190+ y : 20 ,
191+ width : 30 ,
192+ height : 40 ,
193+ } )
194+ } )
195+
196+ it ( 'creates ids with the expected prefix and random suffix' , ( ) => {
197+ expect ( createPdfObjectId ( ) ) . toMatch ( / ^ o b j - \d + - [ a - z 0 - 9 ] { 6 } $ / )
198+ } )
199+ } )
200+ } )
0 commit comments