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

(-)a/t/cypress/integration/Admin/CirculationTriggers_spec.ts (-1 / +1259 lines)
Line 0 Link Here
0
- 
1
import { mount } from "@cypress/vue";
2
const dayjs = require("dayjs");
3
4
const dates = {
5
    today_iso: dayjs().format("YYYY-MM-DD"),
6
    today_us: dayjs().format("MM/DD/YYYY"),
7
    tomorrow_iso: dayjs().add(1, "day").format("YYYY-MM-DD"),
8
    tomorrow_us: dayjs().add(1, "day").format("MM/DD/YYYY"),
9
};
10
11
// Helper function to generate mock circulation rule
12
const getMockCirculationRule = (
13
    library_id: string = "*",
14
    patron_category_id: string = "*",
15
    item_type_id: string = "*",
16
    triggerNum: number = 1
17
) => {
18
    return {
19
        id: Math.floor(Math.random() * 1000),
20
        rule_name: `overdue_${triggerNum}_delay`,
21
        rule_value: "7",
22
        branchcode: library_id === "*" ? null : library_id,
23
        categorycode: patron_category_id === "*" ? null : patron_category_id,
24
        itemtype: item_type_id === "*" ? null : item_type_id,
25
    };
26
};
27
28
const getMockLibraries = () => {
29
    return [
30
        {
31
            library_id: "CPL",
32
            name: "Centerville",
33
            address1: "123 Main St",
34
            city: "Centerville",
35
        },
36
        {
37
            library_id: "MPL",
38
            name: "Midway",
39
            address1: "456 Broad St",
40
            city: "Midway",
41
        },
42
        {
43
            library_id: "FPL",
44
            name: "Fairview",
45
            address1: "789 Park Ave",
46
            city: "Fairview",
47
        },
48
    ];
49
};
50
51
const getMockPatronCategories = () => {
52
    return [
53
        {
54
            patron_category_id: "PT",
55
            name: "Patron",
56
            description: "Regular patron",
57
        },
58
        {
59
            patron_category_id: "ST",
60
            name: "Student",
61
            description: "Student patron",
62
        },
63
        {
64
            patron_category_id: "ST",
65
            name: "Staff",
66
            description: "Staff member",
67
        },
68
    ];
69
};
70
71
const getMockItemTypes = () => {
72
    return [
73
        {
74
            item_type_id: "BK",
75
            description: "Books",
76
        },
77
        {
78
            item_type_id: "DVD",
79
            description: "DVDs",
80
        },
81
        {
82
            item_type_id: "MAG",
83
            description: "Magazines",
84
        },
85
    ];
86
};
87
88
const getMockLetters = () => {
89
    return [
90
        {
91
            code: "ODUE1",
92
            name: "First Overdue Notice",
93
            branchcode: null,
94
            module: "circulation",
95
        },
96
        {
97
            code: "ODUE2",
98
            name: "Second Overdue Notice",
99
            branchcode: null,
100
            module: "circulation",
101
        },
102
        {
103
            code: "ODUE3",
104
            name: "Third Overdue Notice",
105
            branchcode: null,
106
            module: "circulation",
107
        },
108
        {
109
            code: "CPL_ODUE",
110
            name: "Centerville Overdue",
111
            branchcode: "CPL",
112
            module: "circulation",
113
        },
114
    ];
115
};
116
117
describe("Circulation Triggers - Breadcrumbs", () => {
118
    beforeEach(() => {
119
        cy.login();
120
        cy.title().should("eq", "Koha staff interface");
121
    });
122
123
    it("Should display correct breadcrumbs", () => {
124
        cy.visit("/cgi-bin/koha/admin/admin-home.pl");
125
        cy.contains("Circulation triggers").click();
126
        cy.get("#breadcrumbs").contains("Administration");
127
        cy.get("#breadcrumbs > ol > li:nth-child(3)").contains(
128
            "Circulation triggers"
129
        );
130
        cy.get(".current").contains("Home");
131
    });
132
133
    it("Should have breadcrumb link from add form", () => {
134
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
135
        cy.contains("Add new trigger").click();
136
        cy.get(".current").contains("Add new trigger");
137
        cy.get("#breadcrumbs")
138
            .contains("Circulation triggers")
139
            .should("have.attr", "href")
140
            .and("equal", "/cgi-bin/koha/admin/circulation_triggers");
141
    });
142
});
143
144
describe("Circulation Triggers - Initial Load", () => {
145
    beforeEach(() => {
146
        cy.login();
147
        cy.title().should("eq", "Koha staff interface");
148
149
        // Intercept API calls for initial data
150
        cy.intercept("GET", "/api/v1/libraries*", {
151
            statusCode: 200,
152
            body: getMockLibraries(),
153
        }).as("get-libraries");
154
155
        cy.intercept("GET", "/api/v1/patron_categories*", {
156
            statusCode: 200,
157
            body: getMockPatronCategories(),
158
        }).as("get-patron-categories");
159
160
        cy.intercept("GET", "/api/v1/item_types*", {
161
            statusCode: 200,
162
            body: getMockItemTypes(),
163
        }).as("get-item-types");
164
165
        cy.intercept("GET", "/api/v1/circulation_rules*", {
166
            statusCode: 200,
167
            body: [
168
                getMockCirculationRule("*", "*", "*", 1),
169
                getMockCirculationRule("*", "*", "*", 2),
170
                getMockCirculationRule("CPL", "PT", "BK", 1),
171
            ],
172
            headers: {
173
                "X-Base-Total-Count": "3",
174
                "X-Total-Count": "3",
175
            },
176
        }).as("get-rules");
177
    });
178
179
    it("Should successfully load the component and display initial elements", () => {
180
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
181
182
        // Check main heading
183
        cy.get("h1").should("contain", "Circulation triggers");
184
185
        // Check for the rules precedence information
186
        cy.get(".page-section").should(
187
            "contain",
188
            "Rules are applied from most specific to less specific"
189
        );
190
191
        // Check that filters are displayed
192
        cy.get("#library_select").should("exist");
193
        cy.get("#patron_category_select").should("exist");
194
        cy.get("#item_type_select").should("exist");
195
196
        // Check toolbar button exists
197
        cy.contains("Add new trigger").should("exist");
198
    });
199
200
    it("Should display trigger tabs", () => {
201
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
202
        cy.wait("@get-rules");
203
204
        // Check that tabs are displayed
205
        cy.get("#circ_triggers_tabs").should("exist");
206
        cy.get(".nav-link").contains("Trigger 1").should("exist");
207
        cy.get(".nav-link").contains("Trigger 2").should("exist");
208
    });
209
210
    it("Should handle API errors gracefully", () => {
211
        cy.intercept("GET", "/api/v1/circulation_rules*", {
212
            statusCode: 500,
213
        }).as("get-rules-error");
214
215
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
216
        cy.wait("@get-rules-error");
217
218
        // Component should handle error gracefully
219
        // (specific error handling depends on implementation)
220
    });
221
});
222
223
describe("Circulation Triggers - Filtering", () => {
224
    beforeEach(() => {
225
        cy.login();
226
        cy.title().should("eq", "Koha staff interface");
227
228
        cy.intercept("GET", "/api/v1/libraries*", {
229
            statusCode: 200,
230
            body: getMockLibraries(),
231
        });
232
233
        cy.intercept("GET", "/api/v1/patron_categories*", {
234
            statusCode: 200,
235
            body: getMockPatronCategories(),
236
        });
237
238
        cy.intercept("GET", "/api/v1/item_types*", {
239
            statusCode: 200,
240
            body: getMockItemTypes(),
241
        });
242
243
        cy.intercept("GET", "/api/v1/circulation_rules*", {
244
            statusCode: 200,
245
            body: [
246
                getMockCirculationRule("*", "*", "*", 1),
247
                getMockCirculationRule("CPL", "*", "*", 1),
248
                getMockCirculationRule("CPL", "PT", "*", 1),
249
                getMockCirculationRule("CPL", "PT", "BK", 1),
250
                getMockCirculationRule("MPL", "ST", "DVD", 1),
251
            ],
252
            headers: {
253
                "X-Base-Total-Count": "5",
254
                "X-Total-Count": "5",
255
            },
256
        }).as("get-rules");
257
    });
258
259
    it("Should filter by library", () => {
260
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
261
        cy.wait("@get-rules");
262
263
        // Select a specific library
264
        cy.get("#library_select .vs__search").type("Centerville{enter}", {
265
            force: true,
266
        });
267
        cy.get("#library_select .vs__selected").contains("Centerville");
268
269
        // Rules should be filtered to show only Centerville rules
270
    });
271
272
    it("Should filter by patron category", () => {
273
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
274
        cy.wait("@get-rules");
275
276
        // Select a specific patron category
277
        cy.get("#patron_category_select .vs__search").type("Patron{enter}", {
278
            force: true,
279
        });
280
        cy.get("#patron_category_select .vs__selected").contains("Patron");
281
282
        // Rules should be filtered
283
    });
284
285
    it("Should filter by item type", () => {
286
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
287
        cy.wait("@get-rules");
288
289
        // Select a specific item type
290
        cy.get("#item_type_select .vs__search").type("Books{enter}", {
291
            force: true,
292
        });
293
        cy.get("#item_type_select .vs__selected").contains("Books");
294
295
        // Rules should be filtered
296
    });
297
298
    it("Should filter by multiple criteria", () => {
299
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
300
        cy.wait("@get-rules");
301
302
        // Select library
303
        cy.get("#library_select .vs__search").type("Centerville{enter}", {
304
            force: true,
305
        });
306
307
        // Select patron category
308
        cy.get("#patron_category_select .vs__search").type("Patron{enter}", {
309
            force: true,
310
        });
311
312
        // Select item type
313
        cy.get("#item_type_select .vs__search").type("Books{enter}", {
314
            force: true,
315
        });
316
317
        // Should show only rules matching all three criteria
318
    });
319
320
    it("Should toggle between explicit and all applicable rules", () => {
321
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
322
        cy.wait("@get-rules");
323
324
        // Initially should show "all applied rules"
325
        cy.get("#filter-rules").should("exist");
326
327
        // Change to "explicitly set rules"
328
        cy.get("#filter-rules .vs__search").click();
329
        cy.get("#filter-rules .vs__dropdown-menu")
330
            .contains("explictly set rules")
331
            .click({ force: true });
332
333
        // Should show fewer rules (only explicitly set ones)
334
    });
335
});
336
337
describe("Circulation Triggers - Tab Navigation", () => {
338
    beforeEach(() => {
339
        cy.login();
340
        cy.title().should("eq", "Koha staff interface");
341
342
        cy.intercept("GET", "/api/v1/**", {
343
            statusCode: 200,
344
            body: [],
345
        });
346
347
        cy.intercept("GET", "/api/v1/circulation_rules*", {
348
            statusCode: 200,
349
            body: [
350
                getMockCirculationRule("*", "*", "*", 1),
351
                getMockCirculationRule("*", "*", "*", 2),
352
                getMockCirculationRule("*", "*", "*", 3),
353
            ],
354
            headers: {
355
                "X-Base-Total-Count": "3",
356
                "X-Total-Count": "3",
357
            },
358
        }).as("get-rules");
359
    });
360
361
    it("Should switch between trigger tabs", () => {
362
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
363
        cy.wait("@get-rules");
364
365
        // Check Trigger 1 is initially active
366
        cy.get(".nav-link")
367
            .contains("Trigger 1")
368
            .should("have.class", "active");
369
370
        // Click on Trigger 2
371
        cy.get(".nav-link").contains("Trigger 2").click();
372
        cy.get(".nav-link")
373
            .contains("Trigger 2")
374
            .should("have.class", "active");
375
376
        // Check that Trigger 2 content is displayed
377
        cy.get(".tab-pane.active").should("exist");
378
379
        // Click on Trigger 3
380
        cy.get(".nav-link").contains("Trigger 3").click();
381
        cy.get(".nav-link")
382
            .contains("Trigger 3")
383
            .should("have.class", "active");
384
    });
385
});
386
387
describe("Circulation Triggers - Add New Trigger", () => {
388
    beforeEach(() => {
389
        cy.login();
390
        cy.title().should("eq", "Koha staff interface");
391
392
        cy.intercept("GET", "/api/v1/libraries*", {
393
            statusCode: 200,
394
            body: getMockLibraries(),
395
        });
396
397
        cy.intercept("GET", "/api/v1/patron_categories*", {
398
            statusCode: 200,
399
            body: getMockPatronCategories(),
400
        });
401
402
        cy.intercept("GET", "/api/v1/item_types*", {
403
            statusCode: 200,
404
            body: getMockItemTypes(),
405
        });
406
407
        cy.intercept("GET", "/api/v1/circulation_rules*", {
408
            statusCode: 200,
409
            body: [getMockCirculationRule("CPL", "PT", "BK", 1)],
410
            headers: {
411
                "X-Base-Total-Count": "1",
412
                "X-Total-Count": "1",
413
            },
414
        }).as("get-rules");
415
    });
416
417
    it("Should open add trigger modal", () => {
418
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
419
420
        cy.get(".modal-dialog").should("not.exist");
421
        cy.contains("Add new trigger").click();
422
423
        // Modal should open
424
        cy.get(".modal-dialog").should("be.visible");
425
        cy.get(".modal-title").should(
426
            "contain",
427
            "Circulation Trigger Configuration"
428
        );
429
    });
430
431
    it("Should require context selection", () => {
432
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
433
        cy.contains("Add new trigger").click();
434
435
        // Should show context selection fields
436
        cy.get("#library_id").should("exist");
437
        cy.get("#patron_category_id").should("exist");
438
        cy.get("#item_type_id").should("exist");
439
440
        // All fields should be required
441
        cy.get("input[required]").should("have.length.at.least", 3);
442
    });
443
444
    it("Should proceed through add trigger workflow", () => {
445
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
446
        cy.contains("Add new trigger").click();
447
448
        // Step 1: Select context
449
        cy.get("#library_id .vs__search").type("Centerville{enter}", {
450
            force: true,
451
        });
452
        cy.get("#patron_category_id .vs__search").type("Patron{enter}", {
453
            force: true,
454
        });
455
        cy.get("#item_type_id .vs__search").type("Books{enter}", {
456
            force: true,
457
        });
458
459
        // Confirm context
460
        cy.contains("Confirm context").click();
461
462
        // Step 2: Should show existing triggers for context
463
        cy.get(".modal-dialog").should("contain", "Existing triggers");
464
465
        // Step 3: Fill in trigger details
466
        cy.get("#overdue_delay").should("exist");
467
        cy.get("#overdue_delay").type("14");
468
469
        // Set restrict checkouts
470
        cy.get("#restricts-yes").check();
471
472
        // Select letter template
473
        cy.get("#letter_code").should("exist");
474
    });
475
476
    it("Should validate delay constraints", () => {
477
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
478
        cy.contains("Add new trigger").click();
479
480
        // Complete context selection
481
        cy.get("#library_id .vs__search").type("Centerville{enter}", {
482
            force: true,
483
        });
484
        cy.get("#patron_category_id .vs__search").type("Patron{enter}", {
485
            force: true,
486
        });
487
        cy.get("#item_type_id .vs__search").type("Books{enter}", {
488
            force: true,
489
        });
490
        cy.contains("Confirm context").click();
491
492
        // Try to enter invalid delay (should have min/max constraints)
493
        cy.get("#overdue_delay").should("have.attr", "min");
494
        cy.get("#overdue_delay").should("have.attr", "max");
495
    });
496
497
    it("Should successfully submit new trigger", () => {
498
        cy.intercept("PUT", "/api/v1/circulation_rules", {
499
            statusCode: 200,
500
            body: getMockCirculationRule("CPL", "PT", "BK", 2),
501
        }).as("create-rule");
502
503
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
504
        cy.contains("Add new trigger").click();
505
506
        // Complete form
507
        cy.get("#library_id .vs__search").type("Centerville{enter}", {
508
            force: true,
509
        });
510
        cy.get("#patron_category_id .vs__search").type("Patron{enter}", {
511
            force: true,
512
        });
513
        cy.get("#item_type_id .vs__search").type("Books{enter}", {
514
            force: true,
515
        });
516
        cy.contains("Confirm context").click();
517
518
        cy.get("#overdue_delay").type("14");
519
        cy.get("#restricts-yes").check();
520
521
        // Submit form
522
        cy.get("form").submit();
523
524
        cy.wait("@create-rule");
525
526
        // Should return to list view
527
        cy.get(".modal-dialog").should("not.exist");
528
529
        // Should show success message
530
        cy.get(".alert-info").should("contain", "updated");
531
    });
532
533
    it("Should handle submission errors", () => {
534
        cy.intercept("PUT", "/api/v1/circulation_rules", {
535
            statusCode: 500,
536
        }).as("create-rule-error");
537
538
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
539
        cy.contains("Add new trigger").click();
540
541
        // Complete and submit form
542
        cy.get("#library_id .vs__search").type("Centerville{enter}", {
543
            force: true,
544
        });
545
        cy.get("#patron_category_id .vs__search").type("Patron{enter}", {
546
            force: true,
547
        });
548
        cy.get("#item_type_id .vs__search").type("Books{enter}", {
549
            force: true,
550
        });
551
        cy.contains("Confirm context").click();
552
553
        cy.get("#overdue_delay").type("14");
554
        cy.get("form").submit();
555
556
        cy.wait("@create-rule-error");
557
558
        // Should show error message
559
        cy.get(".alert-warning").should("contain", "went wrong");
560
    });
561
562
    it("Should allow canceling add operation", () => {
563
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
564
        cy.contains("Add new trigger").click();
565
566
        cy.get(".modal-dialog").should("be.visible");
567
568
        // Click cancel button
569
        cy.contains("Cancel").click();
570
571
        // Modal should close
572
        cy.get(".modal-dialog").should("not.exist");
573
574
        // Should return to list view
575
        cy.get("h1").should("contain", "Circulation triggers");
576
    });
577
578
    it("Should show placeholder values for fallback rules", () => {
579
        cy.intercept("GET", "/api/v1/circulation_rules*", {
580
            statusCode: 200,
581
            body: [
582
                {
583
                    ...getMockCirculationRule("*", "*", "*", 1),
584
                    rule_value: "7", // Default delay
585
                },
586
                {
587
                    ...getMockCirculationRule("*", "*", "*", 1),
588
                    rule_name: "overdue_1_restrict",
589
                    rule_value: "1", // Default restrict
590
                },
591
            ],
592
            headers: {
593
                "X-Base-Total-Count": "2",
594
                "X-Total-Count": "2",
595
            },
596
        });
597
598
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
599
        cy.contains("Add new trigger").click();
600
601
        // Complete context
602
        cy.get("#library_id .vs__search").type("Centerville{enter}", {
603
            force: true,
604
        });
605
        cy.get("#patron_category_id .vs__search").type("Patron{enter}", {
606
            force: true,
607
        });
608
        cy.get("#item_type_id .vs__search").type("Books{enter}", {
609
            force: true,
610
        });
611
        cy.contains("Confirm context").click();
612
613
        // Delay field should show fallback value as placeholder
614
        cy.get("#overdue_delay").should("have.attr", "placeholder", "7");
615
616
        // Restrict checkouts should show fallback value
617
        cy.contains("Fallback to default").should("exist");
618
        cy.contains("(Yes)").should("exist"); // Showing the fallback value
619
    });
620
});
621
622
describe("Circulation Triggers - Edit Existing Trigger", () => {
623
    beforeEach(() => {
624
        cy.login();
625
        cy.title().should("eq", "Koha staff interface");
626
627
        cy.intercept("GET", "/api/v1/**", {
628
            statusCode: 200,
629
            body: [],
630
        });
631
632
        cy.intercept("GET", "/api/v1/circulation_rules*", {
633
            statusCode: 200,
634
            body: [
635
                {
636
                    ...getMockCirculationRule("CPL", "PT", "BK", 1),
637
                    rule_value: "7",
638
                },
639
                {
640
                    ...getMockCirculationRule("CPL", "PT", "BK", 1),
641
                    rule_name: "overdue_1_restrict",
642
                    rule_value: "1",
643
                },
644
                {
645
                    ...getMockCirculationRule("CPL", "PT", "BK", 1),
646
                    rule_name: "overdue_1_letter",
647
                    rule_value: "ODUE1",
648
                },
649
            ],
650
            headers: {
651
                "X-Base-Total-Count": "3",
652
                "X-Total-Count": "3",
653
            },
654
        }).as("get-rules");
655
    });
656
657
    it("Should open edit modal from table", () => {
658
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
659
        cy.wait("@get-rules");
660
661
        // Click edit button/icon in table
662
        cy.get("table").contains("Edit").click();
663
664
        // Modal should open
665
        cy.get(".modal-dialog").should("be.visible");
666
    });
667
668
    it("Should pre-populate form with existing values", () => {
669
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
670
        cy.wait("@get-rules");
671
672
        cy.get("table").contains("Edit").click();
673
674
        // Context should be pre-selected and disabled
675
        cy.get("#library_id").should("be.disabled");
676
        cy.get("#patron_category_id").should("be.disabled");
677
        cy.get("#item_type_id").should("be.disabled");
678
679
        // Delay should be pre-filled
680
        cy.get("#overdue_delay").should("have.value", "7");
681
682
        // Restrict should be pre-selected
683
        cy.get("#restricts-yes").should("be.checked");
684
685
        // Letter should be pre-selected
686
        cy.get("#letter_code .vs__selected").should("contain", "ODUE1");
687
    });
688
689
    it("Should allow modifying trigger values within constraints", () => {
690
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
691
        cy.wait("@get-rules");
692
693
        cy.get("table").contains("Edit").click();
694
695
        // Change delay
696
        cy.get("#overdue_delay").clear().type("10");
697
698
        // Change restrict
699
        cy.get("#restricts-no").check();
700
701
        // Values should update
702
        cy.get("#overdue_delay").should("have.value", "10");
703
        cy.get("#restricts-no").should("be.checked");
704
    });
705
706
    it("Should use increment/decrement buttons for delay", () => {
707
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
708
        cy.wait("@get-rules");
709
710
        cy.get("table").contains("Edit").click();
711
712
        const initialDelay = 7;
713
714
        // Click increment
715
        cy.get(".increment-btn").click();
716
        cy.get("#overdue_delay").should("have.value", String(initialDelay + 1));
717
718
        // Click decrement
719
        cy.get(".decrement-btn").click();
720
        cy.get("#overdue_delay").should("have.value", String(initialDelay));
721
    });
722
723
    it("Should allow clearing values to use fallback", () => {
724
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
725
        cy.wait("@get-rules");
726
727
        cy.get("table").contains("Edit").click();
728
729
        // Clear delay using clear button
730
        cy.get(".clear-btn").click();
731
        cy.get("#overdue_delay").should("have.value", "");
732
733
        // Select fallback for restrict
734
        cy.get("#restricts-fallback").check();
735
736
        // Should show what fallback value will be used
737
        cy.get("#overdue_delay").should("have.attr", "placeholder");
738
    });
739
740
    it("Should successfully update trigger", () => {
741
        cy.intercept("PUT", "/api/v1/circulation_rules", {
742
            statusCode: 200,
743
            body: {
744
                ...getMockCirculationRule("CPL", "PT", "BK", 1),
745
                rule_value: "10",
746
            },
747
        }).as("update-rule");
748
749
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
750
        cy.wait("@get-rules");
751
752
        cy.get("table").contains("Edit").click();
753
754
        cy.get("#overdue_delay").clear().type("10");
755
        cy.get("form").submit();
756
757
        cy.wait("@update-rule");
758
759
        // Modal should close
760
        cy.get(".modal-dialog").should("not.exist");
761
762
        // Should show success message
763
        cy.get(".alert-info").should("exist");
764
    });
765
766
    it("Should handle update errors", () => {
767
        cy.intercept("PUT", "/api/v1/circulation_rules", {
768
            statusCode: 500,
769
        }).as("update-rule-error");
770
771
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
772
        cy.wait("@get-rules");
773
774
        cy.get("table").contains("Edit").click();
775
        cy.get("#overdue_delay").clear().type("10");
776
        cy.get("form").submit();
777
778
        cy.wait("@update-rule-error");
779
780
        // Should show error message
781
        cy.get(".alert-warning").should("contain", "went wrong");
782
    });
783
});
784
785
describe("Circulation Triggers - Delete Trigger", () => {
786
    beforeEach(() => {
787
        cy.login();
788
        cy.title().should("eq", "Koha staff interface");
789
790
        cy.intercept("GET", "/api/v1/**", {
791
            statusCode: 200,
792
            body: [],
793
        });
794
795
        cy.intercept("GET", "/api/v1/circulation_rules*", {
796
            statusCode: 200,
797
            body: [
798
                getMockCirculationRule("CPL", "PT", "BK", 1),
799
                getMockCirculationRule("CPL", "PT", "BK", 2),
800
                getMockCirculationRule("CPL", "PT", "BK", 3),
801
            ],
802
            headers: {
803
                "X-Base-Total-Count": "3",
804
                "X-Total-Count": "3",
805
            },
806
        }).as("get-rules");
807
    });
808
809
    it("Should only show delete button for last trigger", () => {
810
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
811
        cy.wait("@get-rules");
812
813
        // Navigate to Trigger 3 tab (last one)
814
        cy.get(".nav-link").contains("Trigger 3").click();
815
816
        // Delete button should exist
817
        cy.contains("Delete").should("exist");
818
819
        // Navigate to Trigger 1 tab
820
        cy.get(".nav-link").contains("Trigger 1").click();
821
822
        // Delete button should not exist
823
        cy.contains("Delete").should("not.exist");
824
    });
825
826
    it("Should show confirmation dialog for delete", () => {
827
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
828
        cy.wait("@get-rules");
829
830
        cy.get(".nav-link").contains("Trigger 3").click();
831
        cy.contains("Delete").click();
832
833
        // Confirmation modal should open
834
        cy.get(".modal-dialog").should("be.visible");
835
        cy.get(".modal-title").should("contain", "Delete");
836
837
        // Should show which trigger will be deleted
838
        cy.get(".modal-body").should("contain", "Trigger 3");
839
    });
840
841
    it("Should successfully delete trigger", () => {
842
        cy.intercept("PUT", "/api/v1/circulation_rules", {
843
            statusCode: 200,
844
            body: null,
845
        }).as("delete-rule");
846
847
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
848
        cy.wait("@get-rules");
849
850
        cy.get(".nav-link").contains("Trigger 3").click();
851
        cy.contains("Delete").click();
852
853
        // Confirm deletion
854
        cy.contains("Confirm").click();
855
856
        cy.wait("@delete-rule");
857
858
        // Should return to list
859
        cy.get(".modal-dialog").should("not.exist");
860
861
        // Success message should appear
862
        cy.get(".alert-info").should("exist");
863
    });
864
865
    it("Should handle delete errors", () => {
866
        cy.intercept("PUT", "/api/v1/circulation_rules", {
867
            statusCode: 500,
868
        }).as("delete-rule-error");
869
870
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
871
        cy.wait("@get-rules");
872
873
        cy.get(".nav-link").contains("Trigger 3").click();
874
        cy.contains("Delete").click();
875
        cy.contains("Confirm").click();
876
877
        cy.wait("@delete-rule-error");
878
879
        // Error message should appear
880
        cy.get(".alert-warning").should("contain", "went wrong");
881
    });
882
883
    it("Should allow canceling delete operation", () => {
884
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
885
        cy.wait("@get-rules");
886
887
        cy.get(".nav-link").contains("Trigger 3").click();
888
        cy.contains("Delete").click();
889
890
        cy.get(".modal-dialog").should("be.visible");
891
892
        // Click cancel
893
        cy.contains("Cancel").click();
894
895
        // Modal should close
896
        cy.get(".modal-dialog").should("not.exist");
897
898
        // Should still be on Trigger 3 tab
899
        cy.get(".nav-link")
900
            .contains("Trigger 3")
901
            .should("have.class", "active");
902
    });
903
});
904
905
describe("Circulation Triggers - Reset Rule Set", () => {
906
    beforeEach(() => {
907
        cy.login();
908
        cy.title().should("eq", "Koha staff interface");
909
910
        cy.intercept("GET", "/api/v1/**", {
911
            statusCode: 200,
912
            body: [],
913
        });
914
915
        cy.intercept("GET", "/api/v1/circulation_rules*", {
916
            statusCode: 200,
917
            body: [
918
                getMockCirculationRule("*", "*", "*", 1),
919
                getMockCirculationRule("CPL", "PT", "BK", 1),
920
                getMockCirculationRule("MPL", "PT", "BK", 1),
921
            ],
922
            headers: {
923
                "X-Base-Total-Count": "3",
924
                "X-Total-Count": "3",
925
            },
926
        }).as("get-rules");
927
    });
928
929
    it("Should show reset button for explicit rules", () => {
930
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
931
        cy.wait("@get-rules");
932
933
        // Look for reset button in rules table
934
        cy.get("table").contains("Reset").should("exist");
935
    });
936
937
    it("Should show confirmation dialog for reset", () => {
938
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
939
        cy.wait("@get-rules");
940
941
        cy.contains("Reset").click();
942
943
        // Confirmation modal should open
944
        cy.get(".modal-dialog").should("be.visible");
945
        cy.get(".modal-title").should("contain", "Reset");
946
947
        // Should explain what will happen
948
        cy.get(".modal-body").should("contain", "rule set will be removed");
949
    });
950
951
    it("Should successfully reset rule set", () => {
952
        cy.intercept("PUT", "/api/v1/circulation_rules", {
953
            statusCode: 200,
954
            body: null,
955
        }).as("reset-rules");
956
957
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
958
        cy.wait("@get-rules");
959
960
        cy.contains("Reset").click();
961
        cy.contains("Confirm").click();
962
963
        cy.wait("@reset-rules");
964
965
        // Should return to list
966
        cy.get(".modal-dialog").should("not.exist");
967
968
        // Success message
969
        cy.get(".alert-info").should("exist");
970
    });
971
972
    it("Should not allow reset if it's the only rule set", () => {
973
        cy.intercept("GET", "/api/v1/circulation_rules*", {
974
            statusCode: 200,
975
            body: [getMockCirculationRule("CPL", "PT", "BK", 1)],
976
            headers: {
977
                "X-Base-Total-Count": "1",
978
                "X-Total-Count": "1",
979
            },
980
        });
981
982
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
983
984
        // Reset button should not be available
985
        cy.contains("Reset").should("not.exist");
986
    });
987
});
988
989
describe("Circulation Triggers - Letter Template Filtering", () => {
990
    beforeEach(() => {
991
        cy.login();
992
        cy.title().should("eq", "Koha staff interface");
993
994
        cy.intercept("GET", "/api/v1/**", {
995
            statusCode: 200,
996
            body: [],
997
        });
998
    });
999
1000
    it("Should filter letter templates by library", () => {
1001
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
1002
        cy.contains("Add new trigger").click();
1003
1004
        // Select Centerville library
1005
        cy.get("#library_id .vs__search").type("Centerville{enter}", {
1006
            force: true,
1007
        });
1008
        cy.get("#patron_category_id .vs__search").type("Patron{enter}", {
1009
            force: true,
1010
        });
1011
        cy.get("#item_type_id .vs__search").type("Books{enter}", {
1012
            force: true,
1013
        });
1014
        cy.contains("Confirm context").click();
1015
1016
        // Letter dropdown should include Centerville-specific and default templates
1017
        cy.get("#letter_code .vs__search").click();
1018
        cy.get("#letter_code .vs__dropdown-menu").should("contain", "ODUE1");
1019
        cy.get("#letter_code .vs__dropdown-menu").should("contain", "CPL_ODUE");
1020
1021
        // Should NOT include templates for other libraries
1022
        cy.get("#letter_code .vs__dropdown-menu").should(
1023
            "not.contain",
1024
            "MPL_ODUE"
1025
        );
1026
    });
1027
1028
    it("Should include No letter option", () => {
1029
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
1030
        cy.contains("Add new trigger").click();
1031
1032
        cy.get("#library_id .vs__search").type("Centerville{enter}", {
1033
            force: true,
1034
        });
1035
        cy.get("#patron_category_id .vs__search").type("Patron{enter}", {
1036
            force: true,
1037
        });
1038
        cy.get("#item_type_id .vs__search").type("Books{enter}", {
1039
            force: true,
1040
        });
1041
        cy.contains("Confirm context").click();
1042
1043
        // Should have "No letter" option
1044
        cy.get("#letter_code .vs__search").click();
1045
        cy.get("#letter_code .vs__dropdown-menu").should(
1046
            "contain",
1047
            "No letter"
1048
        );
1049
    });
1050
});
1051
1052
describe("Circulation Triggers - Loading States", () => {
1053
    beforeEach(() => {
1054
        cy.login();
1055
        cy.title().should("eq", "Koha staff interface");
1056
    });
1057
1058
    it("Should show loading messages during initialization", () => {
1059
        cy.intercept("GET", "/api/v1/circulation_rules*", req => {
1060
            // Delay response to see loading state
1061
            req.reply({
1062
                delay: 1000,
1063
                statusCode: 200,
1064
                body: [],
1065
            });
1066
        });
1067
1068
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
1069
1070
        // Should show loading indicator or message
1071
        cy.contains("Loading").should("exist");
1072
    });
1073
1074
    it("Should show specific loading messages in modals", () => {
1075
        cy.intercept("GET", "/api/v1/**", {
1076
            statusCode: 200,
1077
            body: [],
1078
        });
1079
1080
        cy.intercept("GET", "/api/v1/circulation_rules*", req => {
1081
            req.reply({
1082
                delay: 500,
1083
                statusCode: 200,
1084
                body: [],
1085
            });
1086
        });
1087
1088
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
1089
        cy.contains("Add new trigger").click();
1090
1091
        // Should show context-specific loading message
1092
        cy.contains("Loading").should("exist");
1093
    });
1094
});
1095
1096
describe("Circulation Triggers - Empty States", () => {
1097
    beforeEach(() => {
1098
        cy.login();
1099
        cy.title().should("eq", "Koha staff interface");
1100
1101
        cy.intercept("GET", "/api/v1/**", {
1102
            statusCode: 200,
1103
            body: [],
1104
        });
1105
1106
        cy.intercept("GET", "/api/v1/circulation_rules*", {
1107
            statusCode: 200,
1108
            body: [],
1109
            headers: {
1110
                "X-Base-Total-Count": "0",
1111
                "X-Total-Count": "0",
1112
            },
1113
        });
1114
    });
1115
1116
    it("Should handle no rules gracefully", () => {
1117
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
1118
1119
        // Should show message about no rules or empty state
1120
        cy.get("body").should("exist"); // Component should still render
1121
    });
1122
});
1123
1124
describe("Circulation Triggers - Complex Scenarios", () => {
1125
    beforeEach(() => {
1126
        cy.login();
1127
        cy.title().should("eq", "Koha staff interface");
1128
1129
        cy.intercept("GET", "/api/v1/libraries*", {
1130
            statusCode: 200,
1131
            body: getMockLibraries(),
1132
        });
1133
1134
        cy.intercept("GET", "/api/v1/patron_categories*", {
1135
            statusCode: 200,
1136
            body: getMockPatronCategories(),
1137
        });
1138
1139
        cy.intercept("GET", "/api/v1/item_types*", {
1140
            statusCode: 200,
1141
            body: getMockItemTypes(),
1142
        });
1143
1144
        cy.intercept("GET", "/api/v1/circulation_rules*", {
1145
            statusCode: 200,
1146
            body: [
1147
                getMockCirculationRule("*", "*", "*", 1),
1148
                getMockCirculationRule("*", "*", "*", 2),
1149
                getMockCirculationRule("*", "*", "*", 3),
1150
                getMockCirculationRule("CPL", "*", "*", 1),
1151
                getMockCirculationRule("CPL", "PT", "*", 1),
1152
                getMockCirculationRule("CPL", "PT", "BK", 1),
1153
            ],
1154
            headers: {
1155
                "X-Base-Total-Count": "6",
1156
                "X-Total-Count": "6",
1157
            },
1158
        }).as("get-rules");
1159
    });
1160
1161
    it("Should handle multiple triggers for same context", () => {
1162
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
1163
        cy.wait("@get-rules");
1164
1165
        // Should show all three triggers
1166
        cy.get(".nav-link").contains("Trigger 1").should("exist");
1167
        cy.get(".nav-link").contains("Trigger 2").should("exist");
1168
        cy.get(".nav-link").contains("Trigger 3").should("exist");
1169
    });
1170
1171
    it("Should show inheritance hierarchy correctly", () => {
1172
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
1173
        cy.wait("@get-rules");
1174
1175
        // Select specific context
1176
        cy.get("#library_select .vs__search").type("Centerville{enter}", {
1177
            force: true,
1178
        });
1179
        cy.get("#patron_category_select .vs__search").type("Patron{enter}", {
1180
            force: true,
1181
        });
1182
        cy.get("#item_type_select .vs__search").type("Books{enter}", {
1183
            force: true,
1184
        });
1185
1186
        // Should show most specific rule
1187
        // And indicate inherited values in italic/bold
1188
    });
1189
1190
    it("Should allow rapid successive operations", () => {
1191
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
1192
        cy.wait("@get-rules");
1193
1194
        // Quickly change filters
1195
        cy.get("#library_select .vs__search").type("Centerville{enter}", {
1196
            force: true,
1197
        });
1198
        cy.get("#patron_category_select .vs__search").type("Patron{enter}", {
1199
            force: true,
1200
        });
1201
1202
        // Switch tabs
1203
        cy.get(".nav-link").contains("Trigger 2").click();
1204
        cy.get(".nav-link").contains("Trigger 1").click();
1205
1206
        // Toggle filter
1207
        cy.get("#filter-rules .vs__search").click();
1208
        cy.get("#filter-rules .vs__dropdown-menu")
1209
            .contains("explictly set rules")
1210
            .click({ force: true });
1211
1212
        // Should handle all operations without errors
1213
        cy.get("body").should("exist");
1214
    });
1215
});
1216
1217
describe("Circulation Triggers - Browser Navigation", () => {
1218
    beforeEach(() => {
1219
        cy.login();
1220
        cy.title().should("eq", "Koha staff interface");
1221
1222
        cy.intercept("GET", "/api/v1/**", {
1223
            statusCode: 200,
1224
            body: [],
1225
        });
1226
1227
        cy.intercept("GET", "/api/v1/circulation_rules*", {
1228
            statusCode: 200,
1229
            body: [getMockCirculationRule("CPL", "PT", "BK", 1)],
1230
            headers: {
1231
                "X-Base-Total-Count": "1",
1232
                "X-Total-Count": "1",
1233
            },
1234
        });
1235
    });
1236
1237
    it("Should handle browser back button", () => {
1238
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
1239
        cy.contains("Add new trigger").click();
1240
1241
        cy.get(".modal-dialog").should("be.visible");
1242
1243
        // Use browser back
1244
        cy.go("back");
1245
1246
        // Modal should close
1247
        cy.get(".modal-dialog").should("not.exist");
1248
    });
1249
1250
    it("Should handle browser forward button", () => {
1251
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
1252
        cy.contains("Add new trigger").click();
1253
        cy.go("back");
1254
        cy.go("forward");
1255
1256
        // Modal should reopen
1257
        cy.get(".modal-dialog").should("be.visible");
1258
    });
1259
});

Return to bug 10190