View | Details | Raw Unified | Return to bug 37829
Collapse All | Expand All

(-)a/package.json (+11 lines)
Lines 52-57 Link Here
52
    "js:build:analyze": "rspack build --mode development --analyze",
52
    "js:build:analyze": "rspack build --mode development --analyze",
53
    "js:watch": "rspack build --mode development --watch",
53
    "js:watch": "rspack build --mode development --watch",
54
    "js:build:prod": "rspack build --mode production",
54
    "js:build:prod": "rspack build --mode production",
55
    "js:test:unit": "mocha t/mocha/unit/*.test.mjs",
55
    "build": "yarn css:build && yarn js:build && yarn api:bundle",
56
    "build": "yarn css:build && yarn js:build && yarn api:bundle",
56
    "build:prod": "yarn css:build:prod && yarn js:build:prod && yarn api:bundle",
57
    "build:prod": "yarn css:build:prod && yarn js:build:prod && yarn api:bundle",
57
    "cypress": "cypress",
58
    "cypress": "cypress",
Lines 73-80 Link Here
73
    "@koha-community/prettier-plugin-template-toolkit": "^2.0.2",
74
    "@koha-community/prettier-plugin-template-toolkit": "^2.0.2",
74
    "@rspack/cli": "^1.0.1",
75
    "@rspack/cli": "^1.0.1",
75
    "@rspack/core": "^1.0.1",
76
    "@rspack/core": "^1.0.1",
77
    "@types/chai": "^5.2.1",
78
    "@types/jsdom": "^21.1.7",
79
    "@types/mocha": "^10.0.10",
80
    "@types/rewire": "^2.5.30",
76
    "@vue/compiler-sfc": "^3.2.31",
81
    "@vue/compiler-sfc": "^3.2.31",
77
    "browserify": "^17.0.0",
82
    "browserify": "^17.0.0",
83
    "chai": "^5.2.0",
84
    "chai-sinon": "^2.8.1",
78
    "clean-webpack-plugin": "^4.0.0",
85
    "clean-webpack-plugin": "^4.0.0",
79
    "eslint": "^9.12.0",
86
    "eslint": "^9.12.0",
80
    "eslint-config-prettier": "^9.1.0",
87
    "eslint-config-prettier": "^9.1.0",
Lines 83-92 Link Here
83
    "globals": "^16.0.0",
90
    "globals": "^16.0.0",
84
    "gulp-tap": "^1.0.1",
91
    "gulp-tap": "^1.0.1",
85
    "html-webpack-plugin": "^5.5.0",
92
    "html-webpack-plugin": "^5.5.0",
93
    "jsdom": "^26.1.0",
94
    "mocha": "^11.1.0",
86
    "node-sass-tilde-importer": "^1.0.2",
95
    "node-sass-tilde-importer": "^1.0.2",
87
    "postcss": "^8.4.14",
96
    "postcss": "^8.4.14",
88
    "postcss-selector-parser": "^6.0.10",
97
    "postcss-selector-parser": "^6.0.10",
89
    "prettier": "^3.4.2",
98
    "prettier": "^3.4.2",
99
    "rewire": "^7.0.0",
100
    "sinon": "^20.0.0",
90
    "stylelint": "^14.9.1",
101
    "stylelint": "^14.9.1",
91
    "stylelint-config-standard-scss": "^5.0.0",
102
    "stylelint-config-standard-scss": "^5.0.0",
92
    "stylelint-order": "^5.0.0",
103
    "stylelint-order": "^5.0.0",
(-)a/t/mocha/unit/additional-fields.test.mjs (-1 / +924 lines)
Line 0 Link Here
0
- 
1
import mocha from "mocha";
2
import * as chai from "chai";
3
import sinonChai from "chai-sinon";
4
import jsdom from "jsdom";
5
import rewire from "rewire";
6
import sinon from "sinon";
7
const { JSDOM } = jsdom;
8
const { expect } = chai;
9
const { describe, it, beforeEach, afterEach } = mocha;
10
11
chai.use(sinonChai);
12
13
// Create a complete HTML document with our container
14
const dom = new JSDOM(`
15
    <!DOCTYPE html>
16
    <html>
17
        <body>
18
            <div id="extended_attributes"></div>
19
        </body>
20
    </html>
21
`);
22
23
const { window } = dom;
24
const { document } = window;
25
26
// Set up global variables
27
global.document = document;
28
global.window = window;
29
global.HTMLOListElement = window.HTMLOListElement;
30
global.HTMLInputElement = window.HTMLInputElement;
31
global.HTMLSelectElement = window.HTMLSelectElement;
32
33
// Add translation function
34
global.__ = str => str;
35
36
// Set up mock APIClient
37
global.window.APIClient = {
38
    additional_fields: {
39
        additional_fields: {
40
            getAll: resourceType => {
41
                if (!resourceType)
42
                    return Promise.reject(new Error("Resource type required"));
43
                return Promise.resolve([
44
                    {
45
                        extended_attribute_type_id: 1,
46
                        name: "Urgency",
47
                        authorised_value_category_name: "BOOKINGS_URGENCY",
48
                        repeatable: false,
49
                        resource_type: "booking",
50
                        marc_field: "",
51
                        marc_field_mode: "get",
52
                        searchable: false,
53
                    },
54
                    {
55
                        extended_attribute_type_id: 2,
56
                        name: "Note",
57
                        authorised_value_category_name: null,
58
                        repeatable: false,
59
                        resource_type: "booking",
60
                        marc_field: "",
61
                        marc_field_mode: "get",
62
                        searchable: false,
63
                    },
64
                ]);
65
            },
66
        },
67
    },
68
    authorised_values: {
69
        values: {
70
            getCategoriesWithValues: categories => {
71
                if (!categories || categories.length === 0) {
72
                    return Promise.resolve([]);
73
                }
74
                const categoryNames = JSON.parse(categories[0]);
75
                if (!categoryNames.length) {
76
                    return Promise.resolve([]);
77
                }
78
                return Promise.resolve(
79
                    categoryNames.map(category => ({
80
                        category_name: category,
81
                        authorised_values:
82
                            category === "BOOKINGS_URGENCY"
83
                                ? [
84
                                      { value: "HIGH", description: "High" },
85
                                      {
86
                                          value: "MEDIUM",
87
                                          description: "Medium",
88
                                      },
89
                                      { value: "LOW", description: "Low" },
90
                                  ]
91
                                : [
92
                                      { value: "1", description: "Yes" },
93
                                      { value: "0", description: "No" },
94
                                  ],
95
                    }))
96
                );
97
            },
98
        },
99
    },
100
};
101
102
// Mock console.error to silence error output
103
const originalConsoleError = console.error;
104
console.error = () => {};
105
106
// Restore console.error after tests
107
after(() => {
108
    console.error = originalConsoleError;
109
});
110
111
// Rewire the additional-fields.js file
112
const additionalFieldsModule = rewire(
113
    "./koha-tmpl/intranet-tmpl/prog/js/additional-fields.js"
114
);
115
116
// Get the result of the IIFE
117
const AdditionalFields = additionalFieldsModule.__get__("AdditionalFields");
118
119
describe("AdditionalFields", () => {
120
    let additionalFields;
121
    let container;
122
    let getAllStub;
123
    let getCategoriesWithValuesStub;
124
125
    beforeEach(() => {
126
        // Get the container
127
        container = document.getElementById("extended_attributes");
128
        if (!container) {
129
            throw new Error("Container not found");
130
        }
131
        container.innerHTML = "";
132
133
        // Set up mock APIClient
134
        window.APIClient = {
135
            additional_fields: {
136
                additional_fields: {
137
                    getAll: sinon.stub().resolves([
138
                        {
139
                            extended_attribute_type_id: 1,
140
                            name: "Urgency",
141
                            authorised_value_category_name: "BOOKINGS_URGENCY",
142
                            repeatable: false,
143
                            resource_type: "booking",
144
                            marc_field: "",
145
                            marc_field_mode: "get",
146
                            searchable: false,
147
                        },
148
                        {
149
                            extended_attribute_type_id: 2,
150
                            name: "Note",
151
                            authorised_value_category_name: null,
152
                            repeatable: false,
153
                            resource_type: "booking",
154
                            marc_field: "",
155
                            marc_field_mode: "get",
156
                            searchable: false,
157
                        },
158
                        {
159
                            extended_attribute_type_id: 3,
160
                            name: "Contact",
161
                            authorised_value_category_name: null,
162
                            repeatable: false,
163
                            resource_type: "booking",
164
                            marc_field: "",
165
                            marc_field_mode: "get",
166
                            searchable: false,
167
                        },
168
                        {
169
                            extended_attribute_type_id: 4,
170
                            name: "Test",
171
                            authorised_value_category_name: "YES_NO",
172
                            repeatable: true,
173
                            resource_type: "booking",
174
                            marc_field: "",
175
                            marc_field_mode: "get",
176
                            searchable: false,
177
                        },
178
                    ]),
179
                },
180
            },
181
            authorised_values: {
182
                values: {
183
                    getCategoriesWithValues: sinon.stub().resolves([
184
                        {
185
                            category_name: "BOOKINGS_URGENCY",
186
                            authorised_values: [
187
                                { value: "HIGH", description: "High" },
188
                                { value: "MEDIUM", description: "Medium" },
189
                                { value: "LOW", description: "Low" },
190
                            ],
191
                        },
192
                        {
193
                            category_name: "YES_NO",
194
                            authorised_values: [
195
                                { value: "1", description: "Yes" },
196
                                { value: "0", description: "No" },
197
                            ],
198
                        },
199
                    ]),
200
                },
201
            },
202
        };
203
204
        getAllStub =
205
            window.APIClient.additional_fields.additional_fields.getAll;
206
        getCategoriesWithValuesStub =
207
            window.APIClient.authorised_values.values.getCategoriesWithValues;
208
209
        // Rewire the additional-fields.js file
210
        const additionalFieldsModule = rewire(
211
            "./koha-tmpl/intranet-tmpl/prog/js/additional-fields.js"
212
        );
213
214
        // Get the result of the IIFE
215
        const AdditionalFields =
216
            additionalFieldsModule.__get__("AdditionalFields");
217
218
        additionalFields = AdditionalFields.init({
219
            containerId: "extended_attributes",
220
            resourceType: "booking",
221
            onFieldsChanged: () => {},
222
        });
223
    });
224
225
    afterEach(() => {
226
        // Restore container if it was removed
227
        if (!document.getElementById("extended_attributes")) {
228
            const container = document.createElement("div");
229
            container.id = "extended_attributes";
230
            document.body.appendChild(container);
231
        }
232
        // Reset stubs
233
        getAllStub.reset();
234
        getCategoriesWithValuesStub.reset();
235
    });
236
237
    describe("renderExtendedAttributes", () => {
238
        it("should render fields with provided authorized values", async () => {
239
            const types = [
240
                {
241
                    extended_attribute_type_id: 1,
242
                    name: "Test Attribute",
243
                    authorised_value_category_name: "test_category",
244
                    repeatable: false,
245
                    resource_type: "test-resource",
246
                    marc_field: "",
247
                    marc_field_mode: "",
248
                    searchable: false,
249
                },
250
            ];
251
252
            const values = [{ field_id: 1, value: "value1" }];
253
254
            const authorizedValues = {
255
                test_category: [
256
                    { value: "value1", description: "Value 1" },
257
                    { value: "value2", description: "Value 2" },
258
                ],
259
            };
260
261
            await additionalFields.renderExtendedAttributes(
262
                types,
263
                values,
264
                authorizedValues
265
            );
266
267
            const select = container.querySelector("select");
268
            expect(select).to.exist;
269
            expect(select.value).to.equal("value1");
270
            expect(select.options.length).to.equal(3); // Default option + 2 values
271
        });
272
273
        it("should handle repeatable fields", async () => {
274
            const types = [
275
                {
276
                    extended_attribute_type_id: 1,
277
                    name: "Test Field",
278
                    authorised_value_category_name: null,
279
                    repeatable: true,
280
                    resource_type: "test",
281
                    marc_field: "",
282
                    marc_field_mode: "",
283
                    searchable: false,
284
                },
285
            ];
286
287
            const values = [{ field_id: 1, value: "value1" }];
288
289
            await additionalFields.renderExtendedAttributes(types, values, {});
290
291
            const input = container.querySelector("input");
292
            expect(input).to.exist;
293
            expect(input.value).to.equal("value1");
294
295
            const addButton = container.querySelector(".add-repeatable");
296
            expect(addButton).to.exist;
297
298
            // Test adding a new field
299
            addButton.click();
300
            const inputs = container.querySelectorAll("input");
301
            expect(inputs).to.have.lengthOf(2);
302
            expect(inputs[1].value).to.equal("");
303
304
            // Test removing a field
305
            const removeButton = inputs[1]
306
                .closest(".d-flex")
307
                .querySelector(".remove-repeatable");
308
            removeButton.click();
309
            expect(container.querySelectorAll("input")).to.have.lengthOf(1);
310
        });
311
312
        it("should handle empty types array", async () => {
313
            await additionalFields.renderExtendedAttributes([], [], {});
314
            expect(container.innerHTML).to.equal("");
315
        });
316
317
        it("should handle null values", async () => {
318
            const types = [
319
                {
320
                    extended_attribute_type_id: 1,
321
                    name: "Test Field",
322
                    authorised_value_category_name: null,
323
                    repeatable: false,
324
                    resource_type: "test",
325
                    marc_field: "",
326
                    marc_field_mode: "",
327
                    searchable: false,
328
                },
329
            ];
330
331
            await additionalFields.renderExtendedAttributes(types, null, {});
332
            const input = container.querySelector("input");
333
            expect(input).to.exist;
334
            expect(input.value).to.equal("");
335
        });
336
337
        it("should handle multiple field types", async () => {
338
            const types = [
339
                {
340
                    extended_attribute_type_id: 1,
341
                    name: "Text Field",
342
                    authorised_value_category_name: null,
343
                    repeatable: false,
344
                    resource_type: "test",
345
                    marc_field: "",
346
                    marc_field_mode: "",
347
                    searchable: false,
348
                },
349
                {
350
                    extended_attribute_type_id: 2,
351
                    name: "Select Field",
352
                    authorised_value_category_name: "test_category",
353
                    repeatable: false,
354
                    resource_type: "test",
355
                    marc_field: "",
356
                    marc_field_mode: "",
357
                    searchable: false,
358
                },
359
                {
360
                    extended_attribute_type_id: 3,
361
                    name: "Repeatable Field",
362
                    authorised_value_category_name: null,
363
                    repeatable: true,
364
                    resource_type: "test",
365
                    marc_field: "",
366
                    marc_field_mode: "",
367
                    searchable: false,
368
                },
369
            ];
370
371
            const authorizedValues = {
372
                test_category: [
373
                    { value: "option1", description: "Option 1" },
374
                    { value: "option2", description: "Option 2" },
375
                ],
376
            };
377
378
            await additionalFields.renderExtendedAttributes(
379
                types,
380
                [],
381
                authorizedValues
382
            );
383
384
            const textInput = container.querySelector(
385
                "input[name='extended_attributes.1']"
386
            );
387
            expect(textInput).to.exist;
388
389
            const select = container.querySelector(
390
                "select[name='extended_attributes.2']"
391
            );
392
            expect(select).to.exist;
393
394
            const repeatableInput = container.querySelector(
395
                "input[name='extended_attributes.3[0]']"
396
            );
397
            expect(repeatableInput).to.exist;
398
        });
399
    });
400
401
    describe("setValues", () => {
402
        it("should set values for all field types", async () => {
403
            const types = [
404
                {
405
                    extended_attribute_type_id: 1,
406
                    name: "Text Field",
407
                    authorised_value_category_name: null,
408
                    repeatable: false,
409
                    resource_type: "test",
410
                    marc_field: "",
411
                    marc_field_mode: "",
412
                    searchable: false,
413
                },
414
                {
415
                    extended_attribute_type_id: 2,
416
                    name: "Select Field",
417
                    authorised_value_category_name: "test_category",
418
                    repeatable: false,
419
                    resource_type: "test",
420
                    marc_field: "",
421
                    marc_field_mode: "",
422
                    searchable: false,
423
                },
424
                {
425
                    extended_attribute_type_id: 3,
426
                    name: "Repeatable Field",
427
                    authorised_value_category_name: null,
428
                    repeatable: true,
429
                    resource_type: "test",
430
                    marc_field: "",
431
                    marc_field_mode: "",
432
                    searchable: false,
433
                },
434
            ];
435
436
            const values = [
437
                { field_id: 1, value: "text value" },
438
                { field_id: 2, value: "option1" },
439
                { field_id: 3, value: "repeat1" },
440
                { field_id: 3, value: "repeat2" },
441
            ];
442
443
            const authorizedValues = {
444
                test_category: [
445
                    { value: "option1", description: "Option 1" },
446
                    { value: "option2", description: "Option 2" },
447
                ],
448
            };
449
450
            await additionalFields.renderExtendedAttributes(
451
                types,
452
                [],
453
                authorizedValues
454
            );
455
            await additionalFields.setValues(values, authorizedValues);
456
457
            const textInput = container.querySelector(
458
                "input[name='extended_attributes.1']"
459
            );
460
            expect(textInput.value).to.equal("text value");
461
462
            const select = container.querySelector(
463
                "select[name='extended_attributes.2']"
464
            );
465
            expect(select.value).to.equal("option1");
466
467
            const repeatableInputs = container.querySelectorAll(
468
                "input[name^='extended_attributes.3']"
469
            );
470
            expect(repeatableInputs).to.have.lengthOf(2);
471
            expect(repeatableInputs[0].value).to.equal("repeat1");
472
            expect(repeatableInputs[1].value).to.equal("repeat2");
473
        });
474
475
        it("should handle empty values array", async () => {
476
            const types = [
477
                {
478
                    extended_attribute_type_id: 1,
479
                    name: "Test Field",
480
                    authorised_value_category_name: null,
481
                    repeatable: false,
482
                    resource_type: "test",
483
                    marc_field: "",
484
                    marc_field_mode: "",
485
                    searchable: false,
486
                },
487
            ];
488
489
            await additionalFields.renderExtendedAttributes(types, [], {});
490
            await additionalFields.setValues([], {});
491
492
            const input = container.querySelector("input");
493
            expect(input.value).to.equal("");
494
        });
495
496
        it("should handle null values", async () => {
497
            const types = [
498
                {
499
                    extended_attribute_type_id: 1,
500
                    name: "Test Field",
501
                    authorised_value_category_name: null,
502
                    repeatable: false,
503
                    resource_type: "test",
504
                    marc_field: "",
505
                    marc_field_mode: "",
506
                    searchable: false,
507
                },
508
            ];
509
510
            await additionalFields.renderExtendedAttributes(types, [], {});
511
            await additionalFields.setValues(null, {});
512
513
            const input = container.querySelector("input");
514
            expect(input.value).to.equal("");
515
        });
516
517
        it("should handle partial values", async () => {
518
            const types = [
519
                {
520
                    extended_attribute_type_id: 1,
521
                    name: "Field 1",
522
                    authorised_value_category_name: null,
523
                    repeatable: false,
524
                    resource_type: "test",
525
                    marc_field: "",
526
                    marc_field_mode: "",
527
                    searchable: false,
528
                },
529
                {
530
                    extended_attribute_type_id: 2,
531
                    name: "Field 2",
532
                    authorised_value_category_name: null,
533
                    repeatable: false,
534
                    resource_type: "test",
535
                    marc_field: "",
536
                    marc_field_mode: "",
537
                    searchable: false,
538
                },
539
            ];
540
541
            await additionalFields.renderExtendedAttributes(types, [], {});
542
            await additionalFields.setValues(
543
                [{ field_id: 1, value: "value1" }],
544
                {}
545
            );
546
547
            const input1 = container.querySelector(
548
                "input[name='extended_attributes.1']"
549
            );
550
            expect(input1.value).to.equal("value1");
551
552
            const input2 = container.querySelector(
553
                "input[name='extended_attributes.2']"
554
            );
555
            expect(input2.value).to.equal("");
556
        });
557
    });
558
559
    describe("getValues", () => {
560
        it("should return values from all field types", async () => {
561
            const types = [
562
                {
563
                    extended_attribute_type_id: 1,
564
                    name: "Text Field",
565
                    authorised_value_category_name: null,
566
                    repeatable: false,
567
                    resource_type: "test",
568
                    marc_field: "",
569
                    marc_field_mode: "",
570
                    searchable: false,
571
                },
572
                {
573
                    extended_attribute_type_id: 2,
574
                    name: "Select Field",
575
                    authorised_value_category_name: "test_category",
576
                    repeatable: false,
577
                    resource_type: "test",
578
                    marc_field: "",
579
                    marc_field_mode: "",
580
                    searchable: false,
581
                },
582
                {
583
                    extended_attribute_type_id: 3,
584
                    name: "Repeatable Field",
585
                    authorised_value_category_name: null,
586
                    repeatable: true,
587
                    resource_type: "test",
588
                    marc_field: "",
589
                    marc_field_mode: "",
590
                    searchable: false,
591
                },
592
            ];
593
594
            const values = [
595
                { field_id: 1, value: "text value" },
596
                { field_id: 2, value: "option1" },
597
                { field_id: 3, value: "repeat1" },
598
                { field_id: 3, value: "repeat2" },
599
            ];
600
601
            const authorizedValues = {
602
                test_category: [
603
                    { value: "option1", description: "Option 1" },
604
                    { value: "option2", description: "Option 2" },
605
                ],
606
            };
607
608
            await additionalFields.renderExtendedAttributes(
609
                types,
610
                [],
611
                authorizedValues
612
            );
613
            await additionalFields.setValues(values, authorizedValues);
614
615
            const result = additionalFields.getValues();
616
            expect(result).to.deep.equal(values);
617
        });
618
619
        it("should return empty array when container is not found", () => {
620
            container.remove();
621
            const result = additionalFields.getValues();
622
            expect(result).to.deep.equal([]);
623
        });
624
625
        it("should return empty array when no fields are present", async () => {
626
            await additionalFields.renderExtendedAttributes([], [], {});
627
            const result = additionalFields.getValues();
628
            expect(result).to.deep.equal([]);
629
        });
630
631
        it("should handle partially filled fields", async () => {
632
            const types = [
633
                {
634
                    extended_attribute_type_id: 1,
635
                    name: "Field 1",
636
                    authorised_value_category_name: null,
637
                    repeatable: false,
638
                    resource_type: "test",
639
                    marc_field: "",
640
                    marc_field_mode: "",
641
                    searchable: false,
642
                },
643
                {
644
                    extended_attribute_type_id: 2,
645
                    name: "Field 2",
646
                    authorised_value_category_name: null,
647
                    repeatable: false,
648
                    resource_type: "test",
649
                    marc_field: "",
650
                    marc_field_mode: "",
651
                    searchable: false,
652
                },
653
            ];
654
655
            await additionalFields.renderExtendedAttributes(types, [], {});
656
            const input1 = container.querySelector(
657
                "input[name='extended_attributes.1']"
658
            );
659
            input1.value = "value1";
660
661
            const result = additionalFields.getValues();
662
            expect(result).to.deep.equal([
663
                { field_id: 1, value: "value1" },
664
                { field_id: 2, value: "" },
665
            ]);
666
        });
667
    });
668
669
    describe("clear", () => {
670
        it("should clear all extended attribute values", async () => {
671
            const types = [
672
                {
673
                    extended_attribute_type_id: 1,
674
                    name: "Text Field",
675
                    authorised_value_category_name: null,
676
                    repeatable: false,
677
                    resource_type: "test",
678
                    marc_field: "",
679
                    marc_field_mode: "",
680
                    searchable: false,
681
                },
682
                {
683
                    extended_attribute_type_id: 2,
684
                    name: "Select Field",
685
                    authorised_value_category_name: "test_category",
686
                    repeatable: false,
687
                    resource_type: "test",
688
                    marc_field: "",
689
                    marc_field_mode: "",
690
                    searchable: false,
691
                },
692
            ];
693
694
            const values = [
695
                { field_id: 1, value: "text value" },
696
                { field_id: 2, value: "option1" },
697
            ];
698
699
            const authorizedValues = {
700
                test_category: [
701
                    { value: "option1", description: "Option 1" },
702
                    { value: "option2", description: "Option 2" },
703
                ],
704
            };
705
706
            await additionalFields.renderExtendedAttributes(
707
                types,
708
                [],
709
                authorizedValues
710
            );
711
            await additionalFields.setValues(values, authorizedValues);
712
            additionalFields.clear();
713
714
            const textInput = container.querySelector(
715
                "input[name='extended_attributes.1']"
716
            );
717
            expect(textInput.value).to.equal("");
718
719
            const select = container.querySelector(
720
                "select[name='extended_attributes.2']"
721
            );
722
            expect(select.value).to.equal("");
723
        });
724
    });
725
726
    describe("fetchExtendedAttributes", () => {
727
        it("should fetch extended attribute types", async () => {
728
            const types =
729
                await additionalFields.fetchExtendedAttributes("booking");
730
            expect(types).to.be.an("array");
731
            expect(types).to.have.lengthOf(4);
732
            expect(types[0]).to.have.property("name", "Urgency");
733
            expect(types[1]).to.have.property("name", "Note");
734
            expect(types[2]).to.have.property("name", "Contact");
735
            expect(types[3]).to.have.property("name", "Test");
736
        });
737
    });
738
739
    describe("renderExtendedAttributesValues", () => {
740
        it("should return empty array when no attributes are provided", () => {
741
            const result = AdditionalFields.renderExtendedAttributesValues(
742
                null,
743
                {},
744
                {},
745
                "test-id"
746
            );
747
            expect(result).to.deep.equal([""]);
748
        });
749
750
        it("should filter attributes by record ID", () => {
751
            const attributes = [
752
                { record_id: "test-id", field_id: 1, value: "value1" },
753
                { record_id: "other-id", field_id: 2, value: "value2" },
754
            ];
755
            const result = AdditionalFields.renderExtendedAttributesValues(
756
                attributes,
757
                {},
758
                {},
759
                "test-id"
760
            );
761
            expect(result).to.deep.equal(["1: value1"]);
762
        });
763
764
        it("should use type name when available", () => {
765
            const attributes = [
766
                { record_id: "test-id", field_id: 1, value: "value1" },
767
            ];
768
            const types = {
769
                1: { name: "Test Field" },
770
            };
771
            const result = AdditionalFields.renderExtendedAttributesValues(
772
                attributes,
773
                types,
774
                {},
775
                "test-id"
776
            );
777
            expect(result).to.deep.equal(["Test Field: value1"]);
778
        });
779
780
        it("should use field ID as fallback for name", () => {
781
            const attributes = [
782
                { record_id: "test-id", field_id: 1, value: "value1" },
783
            ];
784
            const result = AdditionalFields.renderExtendedAttributesValues(
785
                attributes,
786
                {},
787
                {},
788
                "test-id"
789
            );
790
            expect(result).to.deep.equal(["1: value1"]);
791
        });
792
793
        it("should use authorized value description when available", () => {
794
            const attributes = [
795
                { record_id: "test-id", field_id: 1, value: "value1" },
796
            ];
797
            const types = {
798
                1: {
799
                    name: "Test Field",
800
                    authorised_value_category_name: "test_category",
801
                },
802
            };
803
            const authorizedValues = {
804
                test_category: {
805
                    value1: "Value 1 Description",
806
                },
807
            };
808
            const result = AdditionalFields.renderExtendedAttributesValues(
809
                attributes,
810
                types,
811
                authorizedValues,
812
                "test-id"
813
            );
814
            expect(result).to.deep.equal(["Test Field: Value 1 Description"]);
815
        });
816
817
        it("should use raw value as fallback when authorized value not found", () => {
818
            const attributes = [
819
                { record_id: "test-id", field_id: 1, value: "value1" },
820
            ];
821
            const types = {
822
                1: {
823
                    name: "Test Field",
824
                    authorised_value_category_name: "test_category",
825
                },
826
            };
827
            const authorizedValues = {
828
                test_category: {
829
                    other_value: "Other Description",
830
                },
831
            };
832
            const result = AdditionalFields.renderExtendedAttributesValues(
833
                attributes,
834
                types,
835
                authorizedValues,
836
                "test-id"
837
            );
838
            expect(result).to.deep.equal(["Test Field: value1"]);
839
        });
840
841
        it("should handle multiple attributes", () => {
842
            const attributes = [
843
                { record_id: "test-id", field_id: 1, value: "value1" },
844
                { record_id: "test-id", field_id: 2, value: "value2" },
845
            ];
846
            const types = {
847
                1: { name: "Field 1" },
848
                2: { name: "Field 2" },
849
            };
850
            const result = AdditionalFields.renderExtendedAttributesValues(
851
                attributes,
852
                types,
853
                {},
854
                "test-id"
855
            );
856
            expect(result).to.deep.equal([
857
                "Field 1: value1",
858
                "Field 2: value2",
859
            ]);
860
        });
861
    });
862
863
    describe("fetchAndProcessExtendedAttributes", () => {
864
        it("should process extended attributes correctly", async () => {
865
            const result =
866
                await AdditionalFields.fetchAndProcessExtendedAttributes(
867
                    "booking"
868
                );
869
            expect(result).to.be.an("object");
870
            expect(result).to.have.property("1");
871
            expect(result["1"]).to.have.property("name", "Urgency");
872
            expect(result["1"]).to.have.property(
873
                "authorised_value_category_name",
874
                "BOOKINGS_URGENCY"
875
            );
876
        });
877
878
        it("should handle API errors gracefully", async () => {
879
            getAllStub.rejects(new Error("API Error"));
880
881
            const result =
882
                await AdditionalFields.fetchAndProcessExtendedAttributes(
883
                    "booking"
884
                );
885
            expect(result).to.deep.equal({});
886
        });
887
    });
888
889
    describe("fetchAndProcessAuthorizedValues", () => {
890
        it("should process authorized values correctly", async () => {
891
            const categories = ["BOOKINGS_URGENCY", "YES_NO"];
892
            const result =
893
                await AdditionalFields.fetchAndProcessAuthorizedValues(
894
                    categories
895
                );
896
897
            expect(result).to.be.an("object");
898
            expect(result).to.have.property("BOOKINGS_URGENCY");
899
            expect(result.BOOKINGS_URGENCY).to.have.property("HIGH", "High");
900
            expect(result.BOOKINGS_URGENCY).to.have.property(
901
                "MEDIUM",
902
                "Medium"
903
            );
904
            expect(result.BOOKINGS_URGENCY).to.have.property("LOW", "Low");
905
        });
906
907
        it("should handle empty categories array", async () => {
908
            getCategoriesWithValuesStub.resolves([]);
909
            const result =
910
                await AdditionalFields.fetchAndProcessAuthorizedValues([]);
911
            expect(result).to.deep.equal({});
912
        });
913
914
        it("should handle API errors gracefully", async () => {
915
            getCategoriesWithValuesStub.rejects(new Error("API Error"));
916
917
            const result =
918
                await AdditionalFields.fetchAndProcessAuthorizedValues([
919
                    "BOOKINGS_URGENCY",
920
                ]);
921
            expect(result).to.deep.equal({});
922
        });
923
    });
924
});

Return to bug 37829