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

(-)a/installer/data/mysql/mandatory/userpermissions.sql (-1 lines)
Lines 107-113 INSERT INTO permissions (module_bit, code, description) VALUES Link Here
107
   (13, 'edit_calendar', 'Define days when the library is closed'),
107
   (13, 'edit_calendar', 'Define days when the library is closed'),
108
   (13, 'moderate_comments', 'Moderate patron comments'),
108
   (13, 'moderate_comments', 'Moderate patron comments'),
109
   (13, 'edit_notices', 'Define notices'),
109
   (13, 'edit_notices', 'Define notices'),
110
   (13, 'edit_notice_status_triggers', 'Set notice/status triggers for overdue items'),
111
   (13, 'edit_quotes', 'Edit quotes for quote-of-the-day feature'),
110
   (13, 'edit_quotes', 'Edit quotes for quote-of-the-day feature'),
112
   (13, 'view_system_logs', 'Browse the system logs'),
111
   (13, 'view_system_logs', 'Browse the system logs'),
113
   (13, 'inventory', 'Perform inventory (stocktaking) of your catalog'),
112
   (13, 'inventory', 'Perform inventory (stocktaking) of your catalog'),
(-)a/t/Koha/Auth/Permissions.t (+6 lines)
Lines 20-27 use Modern::Perl; Link Here
20
use Test::NoWarnings;
20
use Test::NoWarnings;
21
use Test::More tests => 4;
21
use Test::More tests => 4;
22
22
23
use C4::Context;
24
23
use_ok('Koha::Auth::Permissions');
25
use_ok('Koha::Auth::Permissions');
24
26
27
# Remove the deprecated edit_notice_status_triggers permission (bug 10190).
28
# It may still be present in DBs that pre-date the atomic update.
29
C4::Context->dbh->do(q|DELETE FROM permissions WHERE code = 'edit_notice_status_triggers'|);
30
25
subtest 'normal staff user test' => sub {
31
subtest 'normal staff user test' => sub {
26
32
27
    plan tests => 1;
33
    plan tests => 1;
(-)a/t/cypress/integration/Admin/CirculationTriggers_spec.ts (-654 / +218 lines)
Lines 8-27 const dates = { Link Here
8
    tomorrow_us: dayjs().add(1, "day").format("MM/DD/YYYY"),
8
    tomorrow_us: dayjs().add(1, "day").format("MM/DD/YYYY"),
9
};
9
};
10
10
11
// Helper function to generate mock circulation rule
11
// Generates a rule set in the format the API actually returns:
12
const getMockCirculationRule = (
12
// { context: { library_id, patron_category_id, item_type_id }, overdue_N_delay, ... }
13
const getMockRuleSet = (
13
    library_id: string = "*",
14
    library_id: string = "*",
14
    patron_category_id: string = "*",
15
    patron_category_id: string = "*",
15
    item_type_id: string = "*",
16
    item_type_id: string = "*",
16
    triggerNum: number = 1
17
    triggerNum: number = 1,
18
    delay: string = "7"
17
) => {
19
) => {
18
    return {
20
    return {
19
        id: Math.floor(Math.random() * 1000),
21
        context: { library_id, patron_category_id, item_type_id },
20
        rule_name: `overdue_${triggerNum}_delay`,
22
        [`overdue_${triggerNum}_delay`]: delay,
21
        rule_value: "7",
23
        [`overdue_${triggerNum}_notice`]: null,
22
        branchcode: library_id === "*" ? null : library_id,
24
        [`overdue_${triggerNum}_restrict`]: null,
23
        categorycode: patron_category_id === "*" ? null : patron_category_id,
25
        [`overdue_${triggerNum}_mtt`]: null,
24
        itemtype: item_type_id === "*" ? null : item_type_id,
25
    };
26
    };
26
};
27
};
27
28
Lines 61-67 const getMockPatronCategories = () => { Link Here
61
            description: "Student patron",
62
            description: "Student patron",
62
        },
63
        },
63
        {
64
        {
64
            patron_category_id: "ST",
65
            patron_category_id: "SF",
65
            name: "Staff",
66
            name: "Staff",
66
            description: "Staff member",
67
            description: "Staff member",
67
        },
68
        },
Lines 127-139 describe("Circulation Triggers - Breadcrumbs", () => { Link Here
127
        cy.get("#breadcrumbs > ol > li:nth-child(3)").contains(
128
        cy.get("#breadcrumbs > ol > li:nth-child(3)").contains(
128
            "Circulation triggers"
129
            "Circulation triggers"
129
        );
130
        );
130
        cy.get(".current").contains("Home");
131
    });
131
    });
132
132
133
    it("Should have breadcrumb link from add form", () => {
133
    it("Should have breadcrumb link from add form", () => {
134
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
134
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
135
        cy.contains("Add new trigger").click();
135
        cy.contains("Add new trigger").click();
136
        cy.get(".current").contains("Add new trigger");
137
        cy.get("#breadcrumbs")
136
        cy.get("#breadcrumbs")
138
            .contains("Circulation triggers")
137
            .contains("Circulation triggers")
139
            .should("have.attr", "href")
138
            .should("have.attr", "href")
Lines 146-152 describe("Circulation Triggers - Initial Load", () => { Link Here
146
        cy.login();
145
        cy.login();
147
        cy.title().should("eq", "Koha staff interface");
146
        cy.title().should("eq", "Koha staff interface");
148
147
149
        // Intercept API calls for initial data
150
        cy.intercept("GET", "/api/v1/libraries*", {
148
        cy.intercept("GET", "/api/v1/libraries*", {
151
            statusCode: 200,
149
            statusCode: 200,
152
            body: getMockLibraries(),
150
            body: getMockLibraries(),
Lines 165-173 describe("Circulation Triggers - Initial Load", () => { Link Here
165
        cy.intercept("GET", "/api/v1/circulation_rules*", {
163
        cy.intercept("GET", "/api/v1/circulation_rules*", {
166
            statusCode: 200,
164
            statusCode: 200,
167
            body: [
165
            body: [
168
                getMockCirculationRule("*", "*", "*", 1),
166
                getMockRuleSet("*", "*", "*", 1),
169
                getMockCirculationRule("*", "*", "*", 2),
167
                getMockRuleSet("*", "*", "*", 2),
170
                getMockCirculationRule("CPL", "PT", "BK", 1),
168
                getMockRuleSet("CPL", "PT", "BK", 1),
171
            ],
169
            ],
172
            headers: {
170
            headers: {
173
                "X-Base-Total-Count": "3",
171
                "X-Base-Total-Count": "3",
Lines 179-207 describe("Circulation Triggers - Initial Load", () => { Link Here
179
    it("Should successfully load the component and display initial elements", () => {
177
    it("Should successfully load the component and display initial elements", () => {
180
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
178
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
181
179
182
        // Check main heading
183
        cy.get("h1").should("contain", "Circulation triggers");
180
        cy.get("h1").should("contain", "Circulation triggers");
184
185
        // Check for the rules precedence information
186
        cy.get(".page-section").should(
181
        cy.get(".page-section").should(
187
            "contain",
182
            "contain",
188
            "Rules are applied from most specific to less specific"
183
            "Rules are applied from most specific to less specific"
189
        );
184
        );
190
191
        // Check that filters are displayed
192
        cy.get("#library_select").should("exist");
185
        cy.get("#library_select").should("exist");
193
        cy.get("#patron_category_select").should("exist");
186
        cy.get("#patron_category_select").should("exist");
194
        cy.get("#item_type_select").should("exist");
187
        cy.get("#item_type_select").should("exist");
195
196
        // Check toolbar button exists
197
        cy.contains("Add new trigger").should("exist");
188
        cy.contains("Add new trigger").should("exist");
198
    });
189
    });
199
190
200
    it("Should display trigger tabs", () => {
191
    it("Should display trigger tabs when rules exist", () => {
201
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
192
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
202
        cy.wait("@get-rules");
193
        cy.wait("@get-rules");
203
194
204
        // Check that tabs are displayed
205
        cy.get("#circ_triggers_tabs").should("exist");
195
        cy.get("#circ_triggers_tabs").should("exist");
206
        cy.get(".nav-link").contains("Trigger 1").should("exist");
196
        cy.get(".nav-link").contains("Trigger 1").should("exist");
207
        cy.get(".nav-link").contains("Trigger 2").should("exist");
197
        cy.get(".nav-link").contains("Trigger 2").should("exist");
Lines 215-222 describe("Circulation Triggers - Initial Load", () => { Link Here
215
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
205
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
216
        cy.wait("@get-rules-error");
206
        cy.wait("@get-rules-error");
217
207
218
        // Component should handle error gracefully
208
        // Component should still render without crashing
219
        // (specific error handling depends on implementation)
209
        cy.get("h1").should("contain", "Circulation triggers");
210
    });
211
});
212
213
describe("Circulation Triggers - No default rules warning", () => {
214
    beforeEach(() => {
215
        cy.login();
216
        cy.title().should("eq", "Koha staff interface");
217
218
        cy.intercept("GET", "/api/v1/libraries*", {
219
            statusCode: 200,
220
            body: getMockLibraries(),
221
        }).as("get-libraries");
222
223
        cy.intercept("GET", "/api/v1/patron_categories*", {
224
            statusCode: 200,
225
            body: getMockPatronCategories(),
226
        }).as("get-patron-categories");
227
228
        cy.intercept("GET", "/api/v1/item_types*", {
229
            statusCode: 200,
230
            body: getMockItemTypes(),
231
        }).as("get-item-types");
232
    });
233
234
    it("Should show warning when no default rules exist but library-specific rules do", () => {
235
        // First intercept: default-library fetch returns empty
236
        // Second intercept: all-rules fetch (for getLibrariesWithRules) returns CPL rules
237
        cy.intercept("GET", "/api/v1/circulation_rules*", req => {
238
            const url = new URL(req.url);
239
            if (url.searchParams.get("library_id") === "*") {
240
                req.reply({
241
                    statusCode: 200,
242
                    body: [],
243
                    headers: {
244
                        "X-Base-Total-Count": "0",
245
                        "X-Total-Count": "0",
246
                    },
247
                });
248
            } else {
249
                req.reply({
250
                    statusCode: 200,
251
                    body: [getMockRuleSet("CPL", "*", "*", 1)],
252
                    headers: {
253
                        "X-Base-Total-Count": "1",
254
                        "X-Total-Count": "1",
255
                    },
256
                });
257
            }
258
        }).as("get-rules");
259
260
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
261
        cy.wait("@get-rules");
262
263
        cy.get(".alert-warning").should(
264
            "contain",
265
            "No default overdue triggers are defined"
266
        );
267
        cy.get(".alert-warning").should(
268
            "contain",
269
            "The following libraries have library-specific triggers defined"
270
        );
271
        cy.get(".alert-warning").contains("Centerville").should("exist");
272
    });
273
274
    it("Should show get-started message when no rules exist anywhere", () => {
275
        cy.intercept("GET", "/api/v1/circulation_rules*", {
276
            statusCode: 200,
277
            body: [],
278
            headers: { "X-Base-Total-Count": "0", "X-Total-Count": "0" },
279
        }).as("get-rules");
280
281
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
282
        cy.wait("@get-rules");
283
284
        cy.get(".alert-warning").should(
285
            "contain",
286
            "No default overdue triggers are defined"
287
        );
288
        cy.get(".alert-warning").should(
289
            "contain",
290
            "Select add new trigger above to get started"
291
        );
292
    });
293
294
    it("Should not show warning when default rules exist", () => {
295
        cy.intercept("GET", "/api/v1/circulation_rules*", {
296
            statusCode: 200,
297
            body: [getMockRuleSet("*", "*", "*", 1)],
298
            headers: { "X-Base-Total-Count": "1", "X-Total-Count": "1" },
299
        }).as("get-rules");
300
301
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
302
        cy.wait("@get-rules");
303
304
        cy.get(".alert-warning").should("not.exist");
305
    });
306
307
    it("Should switch to library view when clicking a library link in the warning", () => {
308
        cy.intercept("GET", "/api/v1/circulation_rules*", req => {
309
            const url = new URL(req.url);
310
            if (url.searchParams.get("library_id") === "*") {
311
                req.reply({
312
                    statusCode: 200,
313
                    body: [],
314
                    headers: {
315
                        "X-Base-Total-Count": "0",
316
                        "X-Total-Count": "0",
317
                    },
318
                });
319
            } else {
320
                req.reply({
321
                    statusCode: 200,
322
                    body: [getMockRuleSet("CPL", "*", "*", 1)],
323
                    headers: {
324
                        "X-Base-Total-Count": "1",
325
                        "X-Total-Count": "1",
326
                    },
327
                });
328
            }
329
        }).as("get-rules");
330
331
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
332
        cy.wait("@get-rules");
333
334
        cy.get(".alert-warning").contains("Centerville").click();
335
336
        // The library filter should now show Centerville and the warning should be gone
337
        cy.get("#library_select .vs__selected").should(
338
            "contain",
339
            "Centerville"
340
        );
341
        cy.get(".alert-warning").should("not.exist");
220
    });
342
    });
221
});
343
});
222
344
Lines 243-253 describe("Circulation Triggers - Filtering", () => { Link Here
243
        cy.intercept("GET", "/api/v1/circulation_rules*", {
365
        cy.intercept("GET", "/api/v1/circulation_rules*", {
244
            statusCode: 200,
366
            statusCode: 200,
245
            body: [
367
            body: [
246
                getMockCirculationRule("*", "*", "*", 1),
368
                getMockRuleSet("*", "*", "*", 1),
247
                getMockCirculationRule("CPL", "*", "*", 1),
369
                getMockRuleSet("CPL", "*", "*", 1),
248
                getMockCirculationRule("CPL", "PT", "*", 1),
370
                getMockRuleSet("CPL", "PT", "*", 1),
249
                getMockCirculationRule("CPL", "PT", "BK", 1),
371
                getMockRuleSet("CPL", "PT", "BK", 1),
250
                getMockCirculationRule("MPL", "ST", "DVD", 1),
372
                getMockRuleSet("MPL", "ST", "DVD", 1),
251
            ],
373
            ],
252
            headers: {
374
            headers: {
253
                "X-Base-Total-Count": "5",
375
                "X-Base-Total-Count": "5",
Lines 260-336 describe("Circulation Triggers - Filtering", () => { Link Here
260
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
382
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
261
        cy.wait("@get-rules");
383
        cy.wait("@get-rules");
262
384
263
        // Select a specific library
264
        cy.get("#library_select .vs__search").type("Centerville{enter}", {
385
        cy.get("#library_select .vs__search").type("Centerville{enter}", {
265
            force: true,
386
            force: true,
266
        });
387
        });
267
        cy.get("#library_select .vs__selected").contains("Centerville");
388
        cy.get("#library_select .vs__selected").should(
268
389
            "contain",
269
        // Rules should be filtered to show only Centerville rules
390
            "Centerville"
391
        );
270
    });
392
    });
271
393
272
    it("Should filter by patron category", () => {
394
    it("Should filter by patron category", () => {
273
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
395
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
274
        cy.wait("@get-rules");
396
        cy.wait("@get-rules");
275
397
276
        // Select a specific patron category
277
        cy.get("#patron_category_select .vs__search").type("Patron{enter}", {
398
        cy.get("#patron_category_select .vs__search").type("Patron{enter}", {
278
            force: true,
399
            force: true,
279
        });
400
        });
280
        cy.get("#patron_category_select .vs__selected").contains("Patron");
401
        cy.get("#patron_category_select .vs__selected").should(
281
402
            "contain",
282
        // Rules should be filtered
403
            "Patron"
404
        );
283
    });
405
    });
284
406
285
    it("Should filter by item type", () => {
407
    it("Should filter by item type", () => {
286
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
408
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
287
        cy.wait("@get-rules");
409
        cy.wait("@get-rules");
288
410
289
        // Select a specific item type
290
        cy.get("#item_type_select .vs__search").type("Books{enter}", {
411
        cy.get("#item_type_select .vs__search").type("Books{enter}", {
291
            force: true,
412
            force: true,
292
        });
413
        });
293
        cy.get("#item_type_select .vs__selected").contains("Books");
414
        cy.get("#item_type_select .vs__selected").should("contain", "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
    });
415
    });
319
416
320
    it("Should toggle between explicit and all applicable rules", () => {
417
    it("Should toggle between explicit and all applicable rules", () => {
321
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
418
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
322
        cy.wait("@get-rules");
419
        cy.wait("@get-rules");
323
420
324
        // Initially should show "all applied rules"
325
        cy.get("#filter-rules").should("exist");
421
        cy.get("#filter-rules").should("exist");
326
422
327
        // Change to "explicitly set rules"
328
        cy.get("#filter-rules .vs__search").click();
423
        cy.get("#filter-rules .vs__search").click();
329
        cy.get("#filter-rules .vs__dropdown-menu")
424
        cy.get("#filter-rules .vs__dropdown-menu")
330
            .contains("explicitly set rules")
425
            .contains("explicitly set rules")
331
            .click({ force: true });
426
            .click({ force: true });
332
333
        // Should show fewer rules (only explicitly set ones)
334
    });
427
    });
335
});
428
});
336
429
Lines 347-355 describe("Circulation Triggers - Tab Navigation", () => { Link Here
347
        cy.intercept("GET", "/api/v1/circulation_rules*", {
440
        cy.intercept("GET", "/api/v1/circulation_rules*", {
348
            statusCode: 200,
441
            statusCode: 200,
349
            body: [
442
            body: [
350
                getMockCirculationRule("*", "*", "*", 1),
443
                getMockRuleSet("*", "*", "*", 1),
351
                getMockCirculationRule("*", "*", "*", 2),
444
                getMockRuleSet("*", "*", "*", 2),
352
                getMockCirculationRule("*", "*", "*", 3),
445
                getMockRuleSet("*", "*", "*", 3),
353
            ],
446
            ],
354
            headers: {
447
            headers: {
355
                "X-Base-Total-Count": "3",
448
                "X-Base-Total-Count": "3",
Lines 362-382 describe("Circulation Triggers - Tab Navigation", () => { Link Here
362
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
455
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
363
        cy.wait("@get-rules");
456
        cy.wait("@get-rules");
364
457
365
        // Check Trigger 1 is initially active
366
        cy.get(".nav-link")
458
        cy.get(".nav-link")
367
            .contains("Trigger 1")
459
            .contains("Trigger 1")
368
            .should("have.class", "active");
460
            .should("have.class", "active");
369
461
370
        // Click on Trigger 2
371
        cy.get(".nav-link").contains("Trigger 2").click();
462
        cy.get(".nav-link").contains("Trigger 2").click();
372
        cy.get(".nav-link")
463
        cy.get(".nav-link")
373
            .contains("Trigger 2")
464
            .contains("Trigger 2")
374
            .should("have.class", "active");
465
            .should("have.class", "active");
375
466
376
        // Check that Trigger 2 content is displayed
377
        cy.get(".tab-pane.active").should("exist");
467
        cy.get(".tab-pane.active").should("exist");
378
468
379
        // Click on Trigger 3
380
        cy.get(".nav-link").contains("Trigger 3").click();
469
        cy.get(".nav-link").contains("Trigger 3").click();
381
        cy.get(".nav-link")
470
        cy.get(".nav-link")
382
            .contains("Trigger 3")
471
            .contains("Trigger 3")
Lines 406-412 describe("Circulation Triggers - Add New Trigger", () => { Link Here
406
495
407
        cy.intercept("GET", "/api/v1/circulation_rules*", {
496
        cy.intercept("GET", "/api/v1/circulation_rules*", {
408
            statusCode: 200,
497
            statusCode: 200,
409
            body: [getMockCirculationRule("CPL", "PT", "BK", 1)],
498
            body: [getMockRuleSet("*", "*", "*", 1, "7")],
410
            headers: {
499
            headers: {
411
                "X-Base-Total-Count": "1",
500
                "X-Base-Total-Count": "1",
412
                "X-Total-Count": "1",
501
                "X-Total-Count": "1",
Lines 420-426 describe("Circulation Triggers - Add New Trigger", () => { Link Here
420
        cy.get(".modal-dialog").should("not.exist");
509
        cy.get(".modal-dialog").should("not.exist");
421
        cy.contains("Add new trigger").click();
510
        cy.contains("Add new trigger").click();
422
511
423
        // Modal should open
424
        cy.get(".modal-dialog").should("be.visible");
512
        cy.get(".modal-dialog").should("be.visible");
425
        cy.get(".modal-title").should(
513
        cy.get(".modal-title").should(
426
            "contain",
514
            "contain",
Lines 428-544 describe("Circulation Triggers - Add New Trigger", () => { Link Here
428
        );
516
        );
429
    });
517
    });
430
518
431
    it("Should require context selection", () => {
519
    it("Should require context selection fields", () => {
432
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
520
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
433
        cy.contains("Add new trigger").click();
521
        cy.contains("Add new trigger").click();
434
522
435
        // Should show context selection fields
436
        cy.get("#library_id").should("exist");
523
        cy.get("#library_id").should("exist");
437
        cy.get("#patron_category_id").should("exist");
524
        cy.get("#patron_category_id").should("exist");
438
        cy.get("#item_type_id").should("exist");
525
        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
    });
526
    });
496
527
497
    it("Should successfully submit new trigger", () => {
528
    it("Should show trigger number 1 when adding first trigger for a library with no existing rules", () => {
498
        cy.intercept("PUT", "/api/v1/circulation_rules", {
529
        // Regression test for bug where triggerNumber was NaN when no rules
530
        // existed for a library (triggerCounts[library_id] was undefined).
531
        cy.intercept("GET", "/api/v1/circulation_rules*", {
499
            statusCode: 200,
532
            statusCode: 200,
500
            body: getMockCirculationRule("CPL", "PT", "BK", 2),
533
            body: [],
501
        }).as("create-rule");
534
            headers: { "X-Base-Total-Count": "0", "X-Total-Count": "0" },
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
        });
535
        });
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
536
538
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
537
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
539
        cy.contains("Add new trigger").click();
538
        cy.contains("Add new trigger").click();
540
539
541
        // Complete and submit form
542
        cy.get("#library_id .vs__search").type("Centerville{enter}", {
540
        cy.get("#library_id .vs__search").type("Centerville{enter}", {
543
            force: true,
541
            force: true,
544
        });
542
        });
Lines 550-562 describe("Circulation Triggers - Add New Trigger", () => { Link Here
550
        });
548
        });
551
        cy.contains("Confirm context").click();
549
        cy.contains("Confirm context").click();
552
550
553
        cy.get("#overdue_delay").type("14");
551
        // trigger number should be 1, not NaN
554
        cy.get("form").submit();
552
        cy.get("legend").should("contain", "Add new trigger 1");
555
553
        cy.get("#overdue_delay").should("exist");
556
        cy.wait("@create-rule-error");
554
        cy.get("#overdue_delay").should("not.have.attr", "name", /NaN/);
557
558
        // Should show error message
559
        cy.get(".alert-warning").should("contain", "went wrong");
560
    });
555
    });
561
556
562
    it("Should allow canceling add operation", () => {
557
    it("Should allow canceling add operation", () => {
Lines 565-785 describe("Circulation Triggers - Add New Trigger", () => { Link Here
565
560
566
        cy.get(".modal-dialog").should("be.visible");
561
        cy.get(".modal-dialog").should("be.visible");
567
562
568
        // Click cancel button
569
        cy.contains("Cancel").click();
563
        cy.contains("Cancel").click();
570
564
571
        // Modal should close
572
        cy.get(".modal-dialog").should("not.exist");
565
        cy.get(".modal-dialog").should("not.exist");
573
574
        // Should return to list view
575
        cy.get("h1").should("contain", "Circulation triggers");
566
        cy.get("h1").should("contain", "Circulation triggers");
576
    });
567
    });
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
});
568
});
784
569
785
describe("Circulation Triggers - Delete Trigger", () => {
570
describe("Circulation Triggers - Delete Trigger", () => {
Lines 795-803 describe("Circulation Triggers - Delete Trigger", () => { Link Here
795
        cy.intercept("GET", "/api/v1/circulation_rules*", {
580
        cy.intercept("GET", "/api/v1/circulation_rules*", {
796
            statusCode: 200,
581
            statusCode: 200,
797
            body: [
582
            body: [
798
                getMockCirculationRule("CPL", "PT", "BK", 1),
583
                getMockRuleSet("*", "*", "*", 1),
799
                getMockCirculationRule("CPL", "PT", "BK", 2),
584
                getMockRuleSet("*", "*", "*", 2),
800
                getMockCirculationRule("CPL", "PT", "BK", 3),
585
                getMockRuleSet("*", "*", "*", 3),
801
            ],
586
            ],
802
            headers: {
587
            headers: {
803
                "X-Base-Total-Count": "3",
588
                "X-Base-Total-Count": "3",
Lines 806-825 describe("Circulation Triggers - Delete Trigger", () => { Link Here
806
        }).as("get-rules");
591
        }).as("get-rules");
807
    });
592
    });
808
593
809
    it("Should only show delete button for last trigger", () => {
594
    it("Should only show delete button for last trigger tab", () => {
810
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
595
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
811
        cy.wait("@get-rules");
596
        cy.wait("@get-rules");
812
597
813
        // Navigate to Trigger 3 tab (last one)
814
        cy.get(".nav-link").contains("Trigger 3").click();
598
        cy.get(".nav-link").contains("Trigger 3").click();
815
816
        // Delete button should exist
817
        cy.contains("Delete").should("exist");
599
        cy.contains("Delete").should("exist");
818
600
819
        // Navigate to Trigger 1 tab
820
        cy.get(".nav-link").contains("Trigger 1").click();
601
        cy.get(".nav-link").contains("Trigger 1").click();
821
822
        // Delete button should not exist
823
        cy.contains("Delete").should("not.exist");
602
        cy.contains("Delete").should("not.exist");
824
    });
603
    });
825
604
Lines 830-883 describe("Circulation Triggers - Delete Trigger", () => { Link Here
830
        cy.get(".nav-link").contains("Trigger 3").click();
609
        cy.get(".nav-link").contains("Trigger 3").click();
831
        cy.contains("Delete").click();
610
        cy.contains("Delete").click();
832
611
833
        // Confirmation modal should open
834
        cy.get(".modal-dialog").should("be.visible");
612
        cy.get(".modal-dialog").should("be.visible");
835
        cy.get(".modal-title").should("contain", "Delete");
613
        cy.get(".modal-title").should("contain", "Delete");
836
614
        cy.get(".modal-body").should("contain", "3");
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
    });
615
    });
882
616
883
    it("Should allow canceling delete operation", () => {
617
    it("Should allow canceling delete operation", () => {
Lines 889-901 describe("Circulation Triggers - Delete Trigger", () => { Link Here
889
623
890
        cy.get(".modal-dialog").should("be.visible");
624
        cy.get(".modal-dialog").should("be.visible");
891
625
892
        // Click cancel
893
        cy.contains("Cancel").click();
626
        cy.contains("Cancel").click();
894
627
895
        // Modal should close
896
        cy.get(".modal-dialog").should("not.exist");
628
        cy.get(".modal-dialog").should("not.exist");
897
898
        // Should still be on Trigger 3 tab
899
        cy.get(".nav-link")
629
        cy.get(".nav-link")
900
            .contains("Trigger 3")
630
            .contains("Trigger 3")
901
            .should("have.class", "active");
631
            .should("have.class", "active");
Lines 915-923 describe("Circulation Triggers - Reset Rule Set", () => { Link Here
915
        cy.intercept("GET", "/api/v1/circulation_rules*", {
645
        cy.intercept("GET", "/api/v1/circulation_rules*", {
916
            statusCode: 200,
646
            statusCode: 200,
917
            body: [
647
            body: [
918
                getMockCirculationRule("*", "*", "*", 1),
648
                getMockRuleSet("*", "*", "*", 1),
919
                getMockCirculationRule("CPL", "PT", "BK", 1),
649
                getMockRuleSet("CPL", "PT", "BK", 1),
920
                getMockCirculationRule("MPL", "PT", "BK", 1),
650
                getMockRuleSet("MPL", "PT", "BK", 1),
921
            ],
651
            ],
922
            headers: {
652
            headers: {
923
                "X-Base-Total-Count": "3",
653
                "X-Base-Total-Count": "3",
Lines 926-936 describe("Circulation Triggers - Reset Rule Set", () => { Link Here
926
        }).as("get-rules");
656
        }).as("get-rules");
927
    });
657
    });
928
658
929
    it("Should show reset button for explicit rules", () => {
659
    it("Should show Reset button for explicit rule sets in the table", () => {
930
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
660
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
931
        cy.wait("@get-rules");
661
        cy.wait("@get-rules");
932
662
933
        // Look for reset button in rules table
934
        cy.get("table").contains("Reset").should("exist");
663
        cy.get("table").contains("Reset").should("exist");
935
    });
664
    });
936
665
Lines 938-1095 describe("Circulation Triggers - Reset Rule Set", () => { Link Here
938
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
667
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
939
        cy.wait("@get-rules");
668
        cy.wait("@get-rules");
940
669
941
        cy.contains("Reset").click();
670
        cy.get("table").contains("Reset").click();
942
671
943
        // Confirmation modal should open
944
        cy.get(".modal-dialog").should("be.visible");
672
        cy.get(".modal-dialog").should("be.visible");
945
        cy.get(".modal-title").should("contain", "Reset");
673
        cy.get(".modal-title").should(
946
674
            "contain",
947
        // Should explain what will happen
675
            "Confirm circulation rule set reset"
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
        );
676
        );
1026
    });
677
        cy.get(".modal-body").should(
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",
678
            "contain",
1047
            "No letter"
679
            "Resetting this rule set for the chosen context"
1048
        );
680
        );
1049
    });
681
    });
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
682
683
    it("Should allow canceling reset operation", () => {
1068
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
684
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
685
        cy.wait("@get-rules");
1069
686
1070
        // Should show loading indicator or message
687
        cy.get("table").contains("Reset").click();
1071
        cy.contains("Loading").should("exist");
688
        cy.get(".modal-dialog").should("be.visible");
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
689
1091
        // Should show context-specific loading message
690
        cy.contains("Cancel").click();
1092
        cy.contains("Loading").should("exist");
691
        cy.get(".modal-dialog").should("not.exist");
1093
    });
692
    });
1094
});
693
});
1095
694
Lines 1113-1123 describe("Circulation Triggers - Empty States", () => { Link Here
1113
        });
712
        });
1114
    });
713
    });
1115
714
1116
    it("Should handle no rules gracefully", () => {
715
    it("Should render without crashing when no rules exist", () => {
1117
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
716
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
1118
717
1119
        // Should show message about no rules or empty state
718
        cy.get("h1").should("contain", "Circulation triggers");
1120
        cy.get("body").should("exist"); // Component should still render
719
        cy.get(".alert-warning").should(
720
            "contain",
721
            "No default overdue triggers"
722
        );
1121
    });
723
    });
1122
});
724
});
1123
725
Lines 1144-1155 describe("Circulation Triggers - Complex Scenarios", () => { Link Here
1144
        cy.intercept("GET", "/api/v1/circulation_rules*", {
746
        cy.intercept("GET", "/api/v1/circulation_rules*", {
1145
            statusCode: 200,
747
            statusCode: 200,
1146
            body: [
748
            body: [
1147
                getMockCirculationRule("*", "*", "*", 1),
749
                getMockRuleSet("*", "*", "*", 1),
1148
                getMockCirculationRule("*", "*", "*", 2),
750
                getMockRuleSet("*", "*", "*", 2),
1149
                getMockCirculationRule("*", "*", "*", 3),
751
                getMockRuleSet("*", "*", "*", 3),
1150
                getMockCirculationRule("CPL", "*", "*", 1),
752
                getMockRuleSet("CPL", "*", "*", 1),
1151
                getMockCirculationRule("CPL", "PT", "*", 1),
753
                getMockRuleSet("CPL", "PT", "*", 1),
1152
                getMockCirculationRule("CPL", "PT", "BK", 1),
754
                getMockRuleSet("CPL", "PT", "BK", 1),
1153
            ],
755
            ],
1154
            headers: {
756
            headers: {
1155
                "X-Base-Total-Count": "6",
757
                "X-Base-Total-Count": "6",
Lines 1158-1215 describe("Circulation Triggers - Complex Scenarios", () => { Link Here
1158
        }).as("get-rules");
760
        }).as("get-rules");
1159
    });
761
    });
1160
762
1161
    it("Should handle multiple triggers for same context", () => {
763
    it("Should show all three trigger tabs for default library", () => {
1162
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
764
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
1163
        cy.wait("@get-rules");
765
        cy.wait("@get-rules");
1164
766
1165
        // Should show all three triggers
1166
        cy.get(".nav-link").contains("Trigger 1").should("exist");
767
        cy.get(".nav-link").contains("Trigger 1").should("exist");
1167
        cy.get(".nav-link").contains("Trigger 2").should("exist");
768
        cy.get(".nav-link").contains("Trigger 2").should("exist");
1168
        cy.get(".nav-link").contains("Trigger 3").should("exist");
769
        cy.get(".nav-link").contains("Trigger 3").should("exist");
1169
    });
770
    });
1170
771
1171
    it("Should show inheritance hierarchy correctly", () => {
772
    it("Should handle rapid tab switching without errors", () => {
1172
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
773
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
1173
        cy.wait("@get-rules");
774
        cy.wait("@get-rules");
1174
775
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();
776
        cy.get(".nav-link").contains("Trigger 2").click();
1204
        cy.get(".nav-link").contains("Trigger 1").click();
777
        cy.get(".nav-link").contains("Trigger 1").click();
778
        cy.get(".nav-link").contains("Trigger 3").click();
1205
779
1206
        // Toggle filter
780
        cy.get(".tab-pane.active").should("exist");
1207
        cy.get("#filter-rules .vs__search").click();
1208
        cy.get("#filter-rules .vs__dropdown-menu")
1209
            .contains("explicitly set rules")
1210
            .click({ force: true });
1211
1212
        // Should handle all operations without errors
1213
        cy.get("body").should("exist");
781
        cy.get("body").should("exist");
1214
    });
782
    });
1215
});
783
});
Lines 1226-1232 describe("Circulation Triggers - Browser Navigation", () => { Link Here
1226
794
1227
        cy.intercept("GET", "/api/v1/circulation_rules*", {
795
        cy.intercept("GET", "/api/v1/circulation_rules*", {
1228
            statusCode: 200,
796
            statusCode: 200,
1229
            body: [getMockCirculationRule("CPL", "PT", "BK", 1)],
797
            body: [getMockRuleSet("*", "*", "*", 1)],
1230
            headers: {
798
            headers: {
1231
                "X-Base-Total-Count": "1",
799
                "X-Base-Total-Count": "1",
1232
                "X-Total-Count": "1",
800
                "X-Total-Count": "1",
Lines 1234-1259 describe("Circulation Triggers - Browser Navigation", () => { Link Here
1234
        });
802
        });
1235
    });
803
    });
1236
804
1237
    it("Should handle browser back button", () => {
805
    it("Should close modal on browser back button", () => {
1238
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
806
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
1239
        cy.contains("Add new trigger").click();
807
        cy.contains("Add new trigger").click();
1240
808
1241
        cy.get(".modal-dialog").should("be.visible");
809
        cy.get(".modal-dialog").should("be.visible");
1242
810
1243
        // Use browser back
1244
        cy.go("back");
811
        cy.go("back");
1245
812
1246
        // Modal should close
1247
        cy.get(".modal-dialog").should("not.exist");
813
        cy.get(".modal-dialog").should("not.exist");
1248
    });
814
    });
1249
815
1250
    it("Should handle browser forward button", () => {
816
    it("Should reopen modal on browser forward button", () => {
1251
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
817
        cy.visit("/cgi-bin/koha/admin/circulation_triggers");
1252
        cy.contains("Add new trigger").click();
818
        cy.contains("Add new trigger").click();
1253
        cy.go("back");
819
        cy.go("back");
1254
        cy.go("forward");
820
        cy.go("forward");
1255
821
1256
        // Modal should reopen
1257
        cy.get(".modal-dialog").should("be.visible");
822
        cy.get(".modal-dialog").should("be.visible");
1258
    });
823
    });
1259
});
824
});
1260
- 

Return to bug 10190