@finsys/core 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,583 +0,0 @@
1
- /*
2
- * Copyright 2025 Sisters Inspire Sdn Bhd
3
- *
4
- * Licensed under the Apache License, Version 2.0 (the "License");
5
- * you may not use this file except in compliance with the License.
6
- * You may obtain a copy of the License at
7
- *
8
- * http://www.apache.org/licenses/LICENSE-2.0
9
- *
10
- * Unless required by applicable law or agreed to in writing, software
11
- * distributed under the License is distributed on an "AS IS" BASIS,
12
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
- * See the License for the specific language governing permissions and
14
- * limitations under the License.
15
- */
16
- import { describe, it, expect } from 'vitest';
17
- import { generateRHFSchema, getStepSchema, getStepDefaultValues } from './rhf-generator.js';
18
- describe('rhf-generator', () => {
19
- const mockConfig = {
20
- displayName: 'Test Loan',
21
- categories: [{ id: 'general', name: 'General' }],
22
- fields: {
23
- carPrice: {
24
- type: 'text',
25
- inputType: 'number',
26
- displayName: 'Car Price',
27
- category: 'general',
28
- min: 1000,
29
- max: 500000
30
- },
31
- fullName: {
32
- type: 'text',
33
- displayName: 'Full Name',
34
- category: 'general',
35
- required: true,
36
- validators: [
37
- { type: 'regex', regex: '^[A-Za-z ]+$', text: 'Alpha only' }
38
- ]
39
- }
40
- },
41
- pages: [
42
- {
43
- id: 'step1',
44
- title: 'Step 1',
45
- fields: ['carPrice', 'fullName']
46
- }
47
- ]
48
- };
49
- it('should generate required schemas by default', () => {
50
- const { baseSchema } = generateRHFSchema(mockConfig);
51
- const shape = baseSchema.shape;
52
- // carPrice is required by default (even if not explicitly required: true in config)
53
- const result = shape.carPrice.safeParse(undefined);
54
- expect(result.success).toBe(false);
55
- });
56
- it('should generate required schemas when required is true', () => {
57
- const { baseSchema } = generateRHFSchema(mockConfig);
58
- const shape = baseSchema.shape;
59
- // fullName is required
60
- const result = shape.fullName.safeParse('');
61
- expect(result.success).toBe(false);
62
- });
63
- it('should apply numeric constraints (min/max) even to fields', () => {
64
- const { baseSchema } = generateRHFSchema(mockConfig);
65
- const shape = baseSchema.shape;
66
- // Below min (1000) should fail if provided
67
- const lowPrice = shape.carPrice.safeParse(500);
68
- expect(lowPrice.success).toBe(false);
69
- // Valid price should pass
70
- const validPrice = shape.carPrice.safeParse(5000);
71
- expect(validPrice.success).toBe(true);
72
- });
73
- it('should apply regex validation correctly', () => {
74
- const { baseSchema } = generateRHFSchema(mockConfig);
75
- const shape = baseSchema.shape;
76
- // Invalid format (numbers)
77
- const invalid = shape.fullName.safeParse('John123');
78
- expect(invalid.success).toBe(false);
79
- // Valid format
80
- const valid = shape.fullName.safeParse('John Doe');
81
- expect(valid.success).toBe(true);
82
- });
83
- it('should correctly build multi-step wizard categories', () => {
84
- const multiPageConfig = {
85
- displayName: 'Multi Page Test',
86
- categories: [{ id: 'general', name: 'General' }],
87
- fields: {
88
- carPrice: {
89
- type: 'text',
90
- inputType: 'number',
91
- displayName: 'Car Price',
92
- category: 'general'
93
- },
94
- fullName: {
95
- type: 'text',
96
- displayName: 'Full Name',
97
- category: 'general',
98
- required: true
99
- }
100
- },
101
- pages: [
102
- { id: 'page1', title: 'First', fields: ['carPrice'] },
103
- { id: 'page2', title: 'Second', fields: ['fullName'] }
104
- ]
105
- };
106
- const { steps } = generateRHFSchema(multiPageConfig);
107
- expect(steps).toHaveLength(2);
108
- expect(steps[0].title).toBe('First');
109
- expect(steps[0].fields[0].name).toBe('carPrice');
110
- expect(steps[1].title).toBe('Second');
111
- expect(steps[1].fields[0].name).toBe('fullName');
112
- });
113
- it('should handle field references with overrides', () => {
114
- const configWithOverrides = {
115
- displayName: 'Override Test',
116
- categories: [{ id: 'general', name: 'General' }],
117
- fields: {
118
- carPrice: {
119
- type: 'text',
120
- inputType: 'number',
121
- displayName: 'Car Price',
122
- category: 'general',
123
- min: 1000
124
- }
125
- },
126
- pages: [
127
- {
128
- id: 'page1',
129
- title: 'Test',
130
- fields: [
131
- { ref: 'carPrice', displayName: 'Vehicle Price (Override)', min: 5000 }
132
- ]
133
- }
134
- ]
135
- };
136
- const { fields } = generateRHFSchema(configWithOverrides);
137
- expect(fields[0].displayName).toBe('Vehicle Price (Override)');
138
- expect(fields[0].min).toBe(5000);
139
- });
140
- it('should normalize required and isRequired properties', () => {
141
- const { fields } = generateRHFSchema(mockConfig);
142
- // fullName has required: true, should also have isRequired: true
143
- const fullNameField = fields.find((f) => f.name === 'fullName');
144
- expect(fullNameField?.required).toBe(true);
145
- expect(fullNameField?.isRequired).toBe(true);
146
- // carPrice is also required by default in the new logic
147
- const carPriceField = fields.find((f) => f.name === 'carPrice');
148
- expect(carPriceField?.required).toBe(true);
149
- expect(carPriceField?.isRequired).toBe(true);
150
- });
151
- describe('Visibility-Aware Validation (superRefine)', () => {
152
- it('should NOT require a field if it is hidden via visibleIf', () => {
153
- const config = {
154
- displayName: 'Conditional Test',
155
- categories: [{ id: 'general', name: 'General' }],
156
- fields: {
157
- showField: { type: 'boolean', displayName: 'Show Secret', category: 'general' },
158
- secretField: {
159
- type: 'text',
160
- displayName: 'Secret',
161
- category: 'general',
162
- required: true,
163
- visibleIf: '{showField} = true'
164
- }
165
- },
166
- pages: [{ id: 'p1', title: 'P1', fields: ['showField', 'secretField'] }]
167
- };
168
- const { zodSchema } = generateRHFSchema(config);
169
- // Scenario A: showField is false, secretField is hidden. Validation should PASS even if empty.
170
- const passResult = zodSchema.safeParse({ showField: false, secretField: '' });
171
- expect(passResult.success).toBe(true);
172
- // Scenario B: showField is true, secretField is visible. Validation should FAIL if empty.
173
- const failResult = zodSchema.safeParse({ showField: true, secretField: '' });
174
- expect(failResult.success).toBe(false);
175
- if (!failResult.success) {
176
- expect(failResult.error.issues[0].message).toBe('This field is required');
177
- }
178
- });
179
- it('should NOT require a field if explicitly visible is false', () => {
180
- const config = {
181
- displayName: 'Hidden Test',
182
- categories: [{ id: 'general', name: 'General' }],
183
- fields: {
184
- hiddenField: { type: 'text', displayName: 'Hidden', category: 'general', required: true, visible: false }
185
- },
186
- pages: [{ id: 'p1', title: 'P1', fields: ['hiddenField'] }]
187
- };
188
- const { zodSchema } = generateRHFSchema(config);
189
- // hiddenField is required but visible: false. Should pass.
190
- const result = zodSchema.safeParse({ hiddenField: '' });
191
- expect(result.success).toBe(true);
192
- });
193
- });
194
- describe('Validation Message Precedence', () => {
195
- it('should skip regex/email validation for empty strings to favor "required" message', () => {
196
- const config = {
197
- displayName: 'Precedence Test',
198
- categories: [{ id: 'general', name: 'General' }],
199
- fields: {
200
- email: {
201
- type: 'text',
202
- name: 'email',
203
- displayName: 'Email',
204
- category: 'general',
205
- required: true,
206
- validators: [{ type: 'email', text: 'Bad email' }]
207
- }
208
- },
209
- pages: [{ id: 'p1', title: 'P1', fields: ['email'] }]
210
- };
211
- const { zodSchema } = generateRHFSchema(config);
212
- // 1. Empty string -> should give "This field is required" (from baseSchema.min(1) or superRefine)
213
- // Actually, since it's required, we expect an error.
214
- const emptyResult = zodSchema.safeParse({ email: '' });
215
- expect(emptyResult.success).toBe(false);
216
- if (!emptyResult.success) {
217
- // ZodError structure check
218
- const issues = emptyResult.error.issues;
219
- expect(issues[0].message).toBe('This field is required');
220
- }
221
- // 2. Invalid format but not empty -> should give "Bad email"
222
- const invalidResult = zodSchema.safeParse({ email: 'not-an-email' });
223
- expect(invalidResult.success).toBe(false);
224
- if (!invalidResult.success) {
225
- const issues = invalidResult.error.issues;
226
- expect(issues[0].message).toBe('Bad email');
227
- }
228
- });
229
- });
230
- describe('Field type handling', () => {
231
- it('should handle file fields with array schema and empty array default', () => {
232
- const config = {
233
- displayName: 'File Test',
234
- categories: [{ id: 'general', name: 'General' }],
235
- fields: {
236
- document: { type: 'file', displayName: 'Upload Document', category: 'general' }
237
- },
238
- pages: [{ id: 'p1', title: 'P1', fields: ['document'] }]
239
- };
240
- const { defaultValues, baseSchema } = generateRHFSchema(config);
241
- expect(defaultValues.document).toEqual([]);
242
- // File fields are required by default, so empty array fails base schema
243
- const emptyResult = baseSchema.shape.document.safeParse([]);
244
- expect(emptyResult.success).toBe(false);
245
- // With content, it passes
246
- const withFile = baseSchema.shape.document.safeParse([{ name: 'file.pdf' }]);
247
- expect(withFile.success).toBe(true);
248
- });
249
- it('should handle boolean fields with false default', () => {
250
- const config = {
251
- displayName: 'Boolean Test',
252
- categories: [{ id: 'general', name: 'General' }],
253
- fields: {
254
- agree: { type: 'boolean', displayName: 'Agree', category: 'general', required: false }
255
- },
256
- pages: [{ id: 'p1', title: 'P1', fields: ['agree'] }]
257
- };
258
- const { defaultValues } = generateRHFSchema(config);
259
- expect(defaultValues.agree).toBe(false);
260
- });
261
- it('should handle boolean fields with custom default', () => {
262
- const config = {
263
- displayName: 'Boolean Default Test',
264
- categories: [{ id: 'general', name: 'General' }],
265
- fields: {
266
- agree: { type: 'boolean', displayName: 'Agree', category: 'general', defaultValue: true }
267
- },
268
- pages: [{ id: 'p1', title: 'P1', fields: ['agree'] }]
269
- };
270
- const { defaultValues } = generateRHFSchema(config);
271
- expect(defaultValues.agree).toBe(true);
272
- });
273
- it('should handle checkbox fields with array schema and empty array default', () => {
274
- const config = {
275
- displayName: 'Checkbox Test',
276
- categories: [{ id: 'general', name: 'General' }],
277
- fields: {
278
- interests: {
279
- type: 'checkbox',
280
- displayName: 'Interests',
281
- category: 'general',
282
- choices: [
283
- { value: 'a', text: 'A' },
284
- { value: 'b', text: 'B' }
285
- ]
286
- }
287
- },
288
- pages: [{ id: 'p1', title: 'P1', fields: ['interests'] }]
289
- };
290
- const { defaultValues, baseSchema } = generateRHFSchema(config);
291
- expect(defaultValues.interests).toEqual([]);
292
- const result = baseSchema.shape.interests.safeParse(['a']);
293
- expect(result.success).toBe(true);
294
- });
295
- it('should handle html display fields as optional', () => {
296
- const config = {
297
- displayName: 'HTML Test',
298
- categories: [{ id: 'general', name: 'General' }],
299
- fields: {
300
- notice: {
301
- type: 'html',
302
- displayName: 'Notice',
303
- category: 'general',
304
- html: '<p>Important notice</p>',
305
- required: true // even if required is true, html should be optional
306
- }
307
- },
308
- pages: [{ id: 'p1', title: 'P1', fields: ['notice'] }]
309
- };
310
- const { defaultValues, zodSchema } = generateRHFSchema(config);
311
- expect(defaultValues.notice).toBe('');
312
- // html fields should not cause validation failure
313
- const result = zodSchema.safeParse({ notice: '' });
314
- expect(result.success).toBe(true);
315
- });
316
- it('should set numeric field default to empty string when no defaultValue specified', () => {
317
- const config = {
318
- displayName: 'Numeric Default Test',
319
- categories: [{ id: 'general', name: 'General' }],
320
- fields: {
321
- amount: { type: 'text', inputType: 'number', displayName: 'Amount', category: 'general' }
322
- },
323
- pages: [{ id: 'p1', title: 'P1', fields: ['amount'] }]
324
- };
325
- const { defaultValues } = generateRHFSchema(config);
326
- // HTML number inputs return "" when empty; z.coerce.number() handles this correctly
327
- expect(defaultValues.amount).toBe("");
328
- });
329
- it('should set numeric field default to the provided value', () => {
330
- const config = {
331
- displayName: 'Numeric Default Test',
332
- categories: [{ id: 'general', name: 'General' }],
333
- fields: {
334
- amount: {
335
- type: 'text', inputType: 'number', displayName: 'Amount',
336
- category: 'general', defaultValue: 5000
337
- }
338
- },
339
- pages: [{ id: 'p1', title: 'P1', fields: ['amount'] }]
340
- };
341
- const { defaultValues } = generateRHFSchema(config);
342
- expect(defaultValues.amount).toBe(5000);
343
- });
344
- it('should handle explicitly optional fields', () => {
345
- const config = {
346
- displayName: 'Optional Test',
347
- categories: [{ id: 'general', name: 'General' }],
348
- fields: {
349
- nickname: { type: 'text', displayName: 'Nickname', category: 'general', required: false }
350
- },
351
- pages: [{ id: 'p1', title: 'P1', fields: ['nickname'] }]
352
- };
353
- const { baseSchema } = generateRHFSchema(config);
354
- // Optional text field should accept empty string
355
- const result = baseSchema.shape.nickname.safeParse('');
356
- expect(result.success).toBe(true);
357
- });
358
- it('should use first element when defaultValue is an array for text fields', () => {
359
- const config = {
360
- displayName: 'Array Default Test',
361
- categories: [{ id: 'general', name: 'General' }],
362
- fields: {
363
- country: {
364
- type: 'dropdown',
365
- displayName: 'Country',
366
- category: 'general',
367
- defaultValue: ['US', 'UK']
368
- }
369
- },
370
- pages: [{ id: 'p1', title: 'P1', fields: ['country'] }]
371
- };
372
- const { defaultValues } = generateRHFSchema(config);
373
- expect(defaultValues.country).toBe('US');
374
- });
375
- });
376
- describe('Validators on conditional fields', () => {
377
- it('should apply regex validators to fields with visibleIf', () => {
378
- const config = {
379
- displayName: 'IC Validation Test',
380
- categories: [{ id: 'customer', name: 'Customer' }],
381
- fields: {
382
- idType: {
383
- type: 'dropdown',
384
- displayName: 'ID Type',
385
- category: 'customer',
386
- choices: [
387
- { value: 'MK', text: 'MyKad' },
388
- { value: 'MI', text: 'Military' }
389
- ]
390
- },
391
- idNumber_mk: {
392
- type: 'text',
393
- displayName: 'IC Number',
394
- category: 'customer',
395
- required: true,
396
- visibleIf: "{idType} == 'MK'",
397
- maxLength: 12,
398
- validators: [
399
- { type: 'regex', regex: '^\\d{12}$', text: 'Please enter exactly 12 digit numbers' }
400
- ]
401
- }
402
- },
403
- pages: [{ id: 'p1', title: 'Info', fields: ['idType', 'idNumber_mk'] }]
404
- };
405
- const { zodSchema } = generateRHFSchema(config);
406
- // When visible (idType=MK) and valid IC number -> pass
407
- const validResult = zodSchema.safeParse({ idType: 'MK', idNumber_mk: '700514065413' });
408
- expect(validResult.success).toBe(true);
409
- // When visible (idType=MK) and invalid IC number -> fail with regex message
410
- const invalidResult = zodSchema.safeParse({ idType: 'MK', idNumber_mk: 'abc123' });
411
- expect(invalidResult.success).toBe(false);
412
- if (!invalidResult.success) {
413
- const icIssue = invalidResult.error.issues.find(i => i.path.includes('idNumber_mk'));
414
- expect(icIssue?.message).toBe('Please enter exactly 12 digit numbers');
415
- }
416
- // When hidden (idType=MI) and empty -> pass (not validated)
417
- const hiddenResult = zodSchema.safeParse({ idType: 'MI', idNumber_mk: '' });
418
- expect(hiddenResult.success).toBe(true);
419
- });
420
- it('should apply email validators to fields with visibleIf', () => {
421
- const config = {
422
- displayName: 'Conditional Email Test',
423
- categories: [{ id: 'general', name: 'General' }],
424
- fields: {
425
- showEmail: { type: 'boolean', displayName: 'Show Email', category: 'general' },
426
- contactEmail: {
427
- type: 'text',
428
- inputType: 'email',
429
- displayName: 'Contact Email',
430
- category: 'general',
431
- required: true,
432
- visibleIf: '{showEmail} = true',
433
- validators: [{ type: 'email', text: 'Invalid email format' }]
434
- }
435
- },
436
- pages: [{ id: 'p1', title: 'Test', fields: ['showEmail', 'contactEmail'] }]
437
- };
438
- const { zodSchema } = generateRHFSchema(config);
439
- // Visible + invalid email -> fail
440
- const invalidResult = zodSchema.safeParse({ showEmail: true, contactEmail: 'not-email' });
441
- expect(invalidResult.success).toBe(false);
442
- if (!invalidResult.success) {
443
- const emailIssue = invalidResult.error.issues.find(i => i.path.includes('contactEmail'));
444
- expect(emailIssue?.message).toBe('Invalid email format');
445
- }
446
- // Visible + valid email -> pass
447
- const validResult = zodSchema.safeParse({ showEmail: true, contactEmail: 'test@example.com' });
448
- expect(validResult.success).toBe(true);
449
- });
450
- });
451
- describe('Error handling', () => {
452
- it('should throw when config has no pages', () => {
453
- const config = {
454
- displayName: 'No Pages',
455
- categories: [{ id: 'general', name: 'General' }],
456
- fields: { name: { type: 'text' } },
457
- pages: []
458
- };
459
- expect(() => generateRHFSchema(config)).toThrow("v2.0.0 templates must include a 'pages' configuration.");
460
- });
461
- });
462
- describe('getStepSchema', () => {
463
- it('should extract schema for a specific step from ZodEffects', () => {
464
- const multiPageConfig = {
465
- displayName: 'Step Schema Test',
466
- categories: [{ id: 'general', name: 'General' }],
467
- fields: {
468
- name: { type: 'text', displayName: 'Name', category: 'general' },
469
- age: { type: 'text', inputType: 'number', displayName: 'Age', category: 'general', min: 0 }
470
- },
471
- pages: [
472
- { id: 'p1', title: 'Personal', fields: ['name'] },
473
- { id: 'p2', title: 'Details', fields: ['age'] }
474
- ]
475
- };
476
- const result = generateRHFSchema(multiPageConfig);
477
- const step1Schema = getStepSchema(result.steps[0], result.zodSchema);
478
- // Step 1 only has "name"
479
- const step1Pass = step1Schema.safeParse({ name: 'John' });
480
- expect(step1Pass.success).toBe(true);
481
- // Step 1 should not care about "age"
482
- const step1NoAge = step1Schema.safeParse({ name: 'John' });
483
- expect(step1NoAge.success).toBe(true);
484
- });
485
- it('should accept baseSchema directly', () => {
486
- const config = {
487
- displayName: 'Step Schema Direct',
488
- categories: [{ id: 'general', name: 'General' }],
489
- fields: {
490
- name: { type: 'text', displayName: 'Name', category: 'general' }
491
- },
492
- pages: [{ id: 'p1', title: 'Test', fields: ['name'] }]
493
- };
494
- const result = generateRHFSchema(config);
495
- const stepSchema = getStepSchema(result.steps[0], result.baseSchema);
496
- const parsed = stepSchema.safeParse({ name: 'Alice' });
497
- expect(parsed.success).toBe(true);
498
- });
499
- it('should accept { baseSchema } wrapper object', () => {
500
- const config = {
501
- displayName: 'Step Schema Wrapper',
502
- categories: [{ id: 'general', name: 'General' }],
503
- fields: {
504
- name: { type: 'text', displayName: 'Name', category: 'general' }
505
- },
506
- pages: [{ id: 'p1', title: 'Test', fields: ['name'] }]
507
- };
508
- const result = generateRHFSchema(config);
509
- const stepSchema = getStepSchema(result.steps[0], { baseSchema: result.baseSchema });
510
- const parsed = stepSchema.safeParse({ name: 'Bob' });
511
- expect(parsed.success).toBe(true);
512
- });
513
- });
514
- describe('getStepDefaultValues', () => {
515
- it('should extract defaults for fields in a specific step', () => {
516
- const config = {
517
- displayName: 'Step Defaults Test',
518
- categories: [{ id: 'general', name: 'General' }],
519
- fields: {
520
- name: { type: 'text', displayName: 'Name', category: 'general', defaultValue: 'John' },
521
- age: { type: 'text', inputType: 'number', displayName: 'Age', category: 'general', defaultValue: 25 }
522
- },
523
- pages: [
524
- { id: 'p1', title: 'Personal', fields: ['name'] },
525
- { id: 'p2', title: 'Details', fields: ['age'] }
526
- ]
527
- };
528
- const result = generateRHFSchema(config);
529
- const step1Defaults = getStepDefaultValues(result.steps[0], result.defaultValues);
530
- expect(step1Defaults).toEqual({ name: 'John' });
531
- expect(step1Defaults).not.toHaveProperty('age');
532
- const step2Defaults = getStepDefaultValues(result.steps[1], result.defaultValues);
533
- expect(step2Defaults).toEqual({ age: 25 });
534
- expect(step2Defaults).not.toHaveProperty('name');
535
- });
536
- it('should return empty object for step with no matching defaults', () => {
537
- const config = {
538
- displayName: 'Empty Defaults Test',
539
- categories: [{ id: 'general', name: 'General' }],
540
- fields: {
541
- name: { type: 'text', displayName: 'Name', category: 'general' }
542
- },
543
- pages: [{ id: 'p1', title: 'Test', fields: ['name'] }]
544
- };
545
- const result = generateRHFSchema(config);
546
- // Pass empty allDefaults
547
- const stepDefaults = getStepDefaultValues(result.steps[0], {});
548
- expect(stepDefaults).toEqual({});
549
- });
550
- });
551
- describe('Dynamic titles', () => {
552
- it('should apply dynamic bank statement titles', () => {
553
- const config = {
554
- displayName: 'Bank Statement Test',
555
- categories: [{ id: 'docs', name: 'Documents' }],
556
- fields: {
557
- bank_statement_t1: { type: 'file', displayName: 'Statement', category: 'docs' },
558
- bank_statement_t2: { type: 'file', displayName: 'Statement', category: 'docs' }
559
- },
560
- pages: [{ id: 'p1', title: 'Docs', fields: ['bank_statement_t1', 'bank_statement_t2'] }]
561
- };
562
- const { fields, steps } = generateRHFSchema(config);
563
- // Both mergedFields and step fields should have dynamic titles
564
- expect(fields[0].displayName).toMatch(/^Bank Statement \(\w+\)$/);
565
- expect(fields[1].displayName).toMatch(/^Bank Statement \(\w+\)$/);
566
- expect(fields[0].displayName).not.toBe(fields[1].displayName);
567
- expect(steps[0].fields[0].displayName).toMatch(/^Bank Statement \(\w+\)$/);
568
- });
569
- it('should apply dynamic financials title', () => {
570
- const config = {
571
- displayName: 'Financials Test',
572
- categories: [{ id: 'docs', name: 'Documents' }],
573
- fields: {
574
- financials: { type: 'file', displayName: 'Financial Statement', category: 'docs' }
575
- },
576
- pages: [{ id: 'p1', title: 'Docs', fields: ['financials'] }]
577
- };
578
- const { fields } = generateRHFSchema(config);
579
- const expectedYear = (new Date().getFullYear() - 1).toString();
580
- expect(fields[0].displayName).toBe(`Audited Financial Statement (${expectedYear})`);
581
- });
582
- });
583
- });