From 6286ac738ff496df97ad2e6674c47c62f40845ce Mon Sep 17 00:00:00 2001 From: Martin Renvoize Date: Wed, 25 Feb 2026 14:45:24 +0000 Subject: [PATCH] Bug 10190: fix failing Permissions.t and overhaul CirculationTriggers Cypress spec Permissions.t: - Remove edit_notice_status_triggers from userpermissions.sql so fresh installs no longer seed this deprecated permission (existing installs are handled by the atomic update) - Add a defensive DELETE in the test to cover DBs that pre-date the atomic update CirculationTriggers_spec.ts: - Fix getMockCirculationRule: the helper was returning the old {rule_name, rule_value, branchcode, ...} format; update to the current API format {context: {...}, overdue_N_delay: ..., ...} - Remove assertions for UI elements that do not exist (success/error flash messages, "Existing triggers" heading, "rule set will be removed" text) - Fix reset confirmation text to match actual component output - Fix duplicate patron_category_id "ST" in mock patron categories - Remove aspirational tests that checked non-implemented behaviour - Add describe block for the no-default-triggers warning (no rules anywhere, rules only at specific libraries, library link switches the filter) - Add regression test for NaN trigger number when adding first rule for a library --- .../data/mysql/mandatory/userpermissions.sql | 1 - t/Koha/Auth/Permissions.t | 6 + .../Admin/CirculationTriggers_spec.ts | 871 +++++------------- 3 files changed, 224 insertions(+), 654 deletions(-) diff --git a/installer/data/mysql/mandatory/userpermissions.sql b/installer/data/mysql/mandatory/userpermissions.sql index 10b8607c47d..d10cd4cbea0 100644 --- a/installer/data/mysql/mandatory/userpermissions.sql +++ b/installer/data/mysql/mandatory/userpermissions.sql @@ -107,7 +107,6 @@ INSERT INTO permissions (module_bit, code, description) VALUES (13, 'edit_calendar', 'Define days when the library is closed'), (13, 'moderate_comments', 'Moderate patron comments'), (13, 'edit_notices', 'Define notices'), - (13, 'edit_notice_status_triggers', 'Set notice/status triggers for overdue items'), (13, 'edit_quotes', 'Edit quotes for quote-of-the-day feature'), (13, 'view_system_logs', 'Browse the system logs'), (13, 'inventory', 'Perform inventory (stocktaking) of your catalog'), diff --git a/t/Koha/Auth/Permissions.t b/t/Koha/Auth/Permissions.t index 5b740193d95..6619625d392 100755 --- a/t/Koha/Auth/Permissions.t +++ b/t/Koha/Auth/Permissions.t @@ -20,8 +20,14 @@ use Modern::Perl; use Test::NoWarnings; use Test::More tests => 4; +use C4::Context; + use_ok('Koha::Auth::Permissions'); +# Remove the deprecated edit_notice_status_triggers permission (bug 10190). +# It may still be present in DBs that pre-date the atomic update. +C4::Context->dbh->do(q|DELETE FROM permissions WHERE code = 'edit_notice_status_triggers'|); + subtest 'normal staff user test' => sub { plan tests => 1; diff --git a/t/cypress/integration/Admin/CirculationTriggers_spec.ts b/t/cypress/integration/Admin/CirculationTriggers_spec.ts index 77e30485edb..ae78f7f89c8 100644 --- a/t/cypress/integration/Admin/CirculationTriggers_spec.ts +++ b/t/cypress/integration/Admin/CirculationTriggers_spec.ts @@ -8,20 +8,21 @@ const dates = { tomorrow_us: dayjs().add(1, "day").format("MM/DD/YYYY"), }; -// Helper function to generate mock circulation rule -const getMockCirculationRule = ( +// Generates a rule set in the format the API actually returns: +// { context: { library_id, patron_category_id, item_type_id }, overdue_N_delay, ... } +const getMockRuleSet = ( library_id: string = "*", patron_category_id: string = "*", item_type_id: string = "*", - triggerNum: number = 1 + triggerNum: number = 1, + delay: string = "7" ) => { return { - id: Math.floor(Math.random() * 1000), - rule_name: `overdue_${triggerNum}_delay`, - rule_value: "7", - branchcode: library_id === "*" ? null : library_id, - categorycode: patron_category_id === "*" ? null : patron_category_id, - itemtype: item_type_id === "*" ? null : item_type_id, + context: { library_id, patron_category_id, item_type_id }, + [`overdue_${triggerNum}_delay`]: delay, + [`overdue_${triggerNum}_notice`]: null, + [`overdue_${triggerNum}_restrict`]: null, + [`overdue_${triggerNum}_mtt`]: null, }; }; @@ -61,7 +62,7 @@ const getMockPatronCategories = () => { description: "Student patron", }, { - patron_category_id: "ST", + patron_category_id: "SF", name: "Staff", description: "Staff member", }, @@ -127,13 +128,11 @@ describe("Circulation Triggers - Breadcrumbs", () => { cy.get("#breadcrumbs > ol > li:nth-child(3)").contains( "Circulation triggers" ); - cy.get(".current").contains("Home"); }); it("Should have breadcrumb link from add form", () => { cy.visit("/cgi-bin/koha/admin/circulation_triggers"); cy.contains("Add new trigger").click(); - cy.get(".current").contains("Add new trigger"); cy.get("#breadcrumbs") .contains("Circulation triggers") .should("have.attr", "href") @@ -146,7 +145,6 @@ describe("Circulation Triggers - Initial Load", () => { cy.login(); cy.title().should("eq", "Koha staff interface"); - // Intercept API calls for initial data cy.intercept("GET", "/api/v1/libraries*", { statusCode: 200, body: getMockLibraries(), @@ -165,9 +163,9 @@ describe("Circulation Triggers - Initial Load", () => { cy.intercept("GET", "/api/v1/circulation_rules*", { statusCode: 200, body: [ - getMockCirculationRule("*", "*", "*", 1), - getMockCirculationRule("*", "*", "*", 2), - getMockCirculationRule("CPL", "PT", "BK", 1), + getMockRuleSet("*", "*", "*", 1), + getMockRuleSet("*", "*", "*", 2), + getMockRuleSet("CPL", "PT", "BK", 1), ], headers: { "X-Base-Total-Count": "3", @@ -179,29 +177,21 @@ describe("Circulation Triggers - Initial Load", () => { it("Should successfully load the component and display initial elements", () => { cy.visit("/cgi-bin/koha/admin/circulation_triggers"); - // Check main heading cy.get("h1").should("contain", "Circulation triggers"); - - // Check for the rules precedence information cy.get(".page-section").should( "contain", "Rules are applied from most specific to less specific" ); - - // Check that filters are displayed cy.get("#library_select").should("exist"); cy.get("#patron_category_select").should("exist"); cy.get("#item_type_select").should("exist"); - - // Check toolbar button exists cy.contains("Add new trigger").should("exist"); }); - it("Should display trigger tabs", () => { + it("Should display trigger tabs when rules exist", () => { cy.visit("/cgi-bin/koha/admin/circulation_triggers"); cy.wait("@get-rules"); - // Check that tabs are displayed cy.get("#circ_triggers_tabs").should("exist"); cy.get(".nav-link").contains("Trigger 1").should("exist"); cy.get(".nav-link").contains("Trigger 2").should("exist"); @@ -215,8 +205,140 @@ describe("Circulation Triggers - Initial Load", () => { cy.visit("/cgi-bin/koha/admin/circulation_triggers"); cy.wait("@get-rules-error"); - // Component should handle error gracefully - // (specific error handling depends on implementation) + // Component should still render without crashing + cy.get("h1").should("contain", "Circulation triggers"); + }); +}); + +describe("Circulation Triggers - No default rules warning", () => { + beforeEach(() => { + cy.login(); + cy.title().should("eq", "Koha staff interface"); + + cy.intercept("GET", "/api/v1/libraries*", { + statusCode: 200, + body: getMockLibraries(), + }).as("get-libraries"); + + cy.intercept("GET", "/api/v1/patron_categories*", { + statusCode: 200, + body: getMockPatronCategories(), + }).as("get-patron-categories"); + + cy.intercept("GET", "/api/v1/item_types*", { + statusCode: 200, + body: getMockItemTypes(), + }).as("get-item-types"); + }); + + it("Should show warning when no default rules exist but library-specific rules do", () => { + // First intercept: default-library fetch returns empty + // Second intercept: all-rules fetch (for getLibrariesWithRules) returns CPL rules + cy.intercept("GET", "/api/v1/circulation_rules*", req => { + const url = new URL(req.url); + if (url.searchParams.get("library_id") === "*") { + req.reply({ + statusCode: 200, + body: [], + headers: { + "X-Base-Total-Count": "0", + "X-Total-Count": "0", + }, + }); + } else { + req.reply({ + statusCode: 200, + body: [getMockRuleSet("CPL", "*", "*", 1)], + headers: { + "X-Base-Total-Count": "1", + "X-Total-Count": "1", + }, + }); + } + }).as("get-rules"); + + cy.visit("/cgi-bin/koha/admin/circulation_triggers"); + cy.wait("@get-rules"); + + cy.get(".alert-warning").should( + "contain", + "No default overdue triggers are defined" + ); + cy.get(".alert-warning").should( + "contain", + "The following libraries have library-specific triggers defined" + ); + cy.get(".alert-warning").contains("Centerville").should("exist"); + }); + + it("Should show get-started message when no rules exist anywhere", () => { + cy.intercept("GET", "/api/v1/circulation_rules*", { + statusCode: 200, + body: [], + headers: { "X-Base-Total-Count": "0", "X-Total-Count": "0" }, + }).as("get-rules"); + + cy.visit("/cgi-bin/koha/admin/circulation_triggers"); + cy.wait("@get-rules"); + + cy.get(".alert-warning").should( + "contain", + "No default overdue triggers are defined" + ); + cy.get(".alert-warning").should( + "contain", + "Select add new trigger above to get started" + ); + }); + + it("Should not show warning when default rules exist", () => { + cy.intercept("GET", "/api/v1/circulation_rules*", { + statusCode: 200, + body: [getMockRuleSet("*", "*", "*", 1)], + headers: { "X-Base-Total-Count": "1", "X-Total-Count": "1" }, + }).as("get-rules"); + + cy.visit("/cgi-bin/koha/admin/circulation_triggers"); + cy.wait("@get-rules"); + + cy.get(".alert-warning").should("not.exist"); + }); + + it("Should switch to library view when clicking a library link in the warning", () => { + cy.intercept("GET", "/api/v1/circulation_rules*", req => { + const url = new URL(req.url); + if (url.searchParams.get("library_id") === "*") { + req.reply({ + statusCode: 200, + body: [], + headers: { + "X-Base-Total-Count": "0", + "X-Total-Count": "0", + }, + }); + } else { + req.reply({ + statusCode: 200, + body: [getMockRuleSet("CPL", "*", "*", 1)], + headers: { + "X-Base-Total-Count": "1", + "X-Total-Count": "1", + }, + }); + } + }).as("get-rules"); + + cy.visit("/cgi-bin/koha/admin/circulation_triggers"); + cy.wait("@get-rules"); + + cy.get(".alert-warning").contains("Centerville").click(); + + // The library filter should now show Centerville and the warning should be gone + cy.get("#library_select .vs__selected").should( + "contain", + "Centerville" + ); + cy.get(".alert-warning").should("not.exist"); }); }); @@ -243,11 +365,11 @@ describe("Circulation Triggers - Filtering", () => { cy.intercept("GET", "/api/v1/circulation_rules*", { statusCode: 200, body: [ - getMockCirculationRule("*", "*", "*", 1), - getMockCirculationRule("CPL", "*", "*", 1), - getMockCirculationRule("CPL", "PT", "*", 1), - getMockCirculationRule("CPL", "PT", "BK", 1), - getMockCirculationRule("MPL", "ST", "DVD", 1), + getMockRuleSet("*", "*", "*", 1), + getMockRuleSet("CPL", "*", "*", 1), + getMockRuleSet("CPL", "PT", "*", 1), + getMockRuleSet("CPL", "PT", "BK", 1), + getMockRuleSet("MPL", "ST", "DVD", 1), ], headers: { "X-Base-Total-Count": "5", @@ -260,77 +382,48 @@ describe("Circulation Triggers - Filtering", () => { cy.visit("/cgi-bin/koha/admin/circulation_triggers"); cy.wait("@get-rules"); - // Select a specific library cy.get("#library_select .vs__search").type("Centerville{enter}", { force: true, }); - cy.get("#library_select .vs__selected").contains("Centerville"); - - // Rules should be filtered to show only Centerville rules + cy.get("#library_select .vs__selected").should( + "contain", + "Centerville" + ); }); it("Should filter by patron category", () => { cy.visit("/cgi-bin/koha/admin/circulation_triggers"); cy.wait("@get-rules"); - // Select a specific patron category cy.get("#patron_category_select .vs__search").type("Patron{enter}", { force: true, }); - cy.get("#patron_category_select .vs__selected").contains("Patron"); - - // Rules should be filtered + cy.get("#patron_category_select .vs__selected").should( + "contain", + "Patron" + ); }); it("Should filter by item type", () => { cy.visit("/cgi-bin/koha/admin/circulation_triggers"); cy.wait("@get-rules"); - // Select a specific item type cy.get("#item_type_select .vs__search").type("Books{enter}", { force: true, }); - cy.get("#item_type_select .vs__selected").contains("Books"); - - // Rules should be filtered - }); - - it("Should filter by multiple criteria", () => { - cy.visit("/cgi-bin/koha/admin/circulation_triggers"); - cy.wait("@get-rules"); - - // Select library - cy.get("#library_select .vs__search").type("Centerville{enter}", { - force: true, - }); - - // Select patron category - cy.get("#patron_category_select .vs__search").type("Patron{enter}", { - force: true, - }); - - // Select item type - cy.get("#item_type_select .vs__search").type("Books{enter}", { - force: true, - }); - - // Should show only rules matching all three criteria + cy.get("#item_type_select .vs__selected").should("contain", "Books"); }); it("Should toggle between explicit and all applicable rules", () => { cy.visit("/cgi-bin/koha/admin/circulation_triggers"); cy.wait("@get-rules"); - // Initially should show "all applied rules" cy.get("#filter-rules").should("exist"); - // Change to "explicitly set rules" cy.get("#filter-rules .vs__search").click(); cy.get("#filter-rules .vs__dropdown-menu") .contains("explicitly set rules") .click({ force: true }); - - // Should show fewer rules (only explicitly set ones) }); }); @@ -347,9 +440,9 @@ describe("Circulation Triggers - Tab Navigation", () => { cy.intercept("GET", "/api/v1/circulation_rules*", { statusCode: 200, body: [ - getMockCirculationRule("*", "*", "*", 1), - getMockCirculationRule("*", "*", "*", 2), - getMockCirculationRule("*", "*", "*", 3), + getMockRuleSet("*", "*", "*", 1), + getMockRuleSet("*", "*", "*", 2), + getMockRuleSet("*", "*", "*", 3), ], headers: { "X-Base-Total-Count": "3", @@ -362,21 +455,17 @@ describe("Circulation Triggers - Tab Navigation", () => { cy.visit("/cgi-bin/koha/admin/circulation_triggers"); cy.wait("@get-rules"); - // Check Trigger 1 is initially active cy.get(".nav-link") .contains("Trigger 1") .should("have.class", "active"); - // Click on Trigger 2 cy.get(".nav-link").contains("Trigger 2").click(); cy.get(".nav-link") .contains("Trigger 2") .should("have.class", "active"); - // Check that Trigger 2 content is displayed cy.get(".tab-pane.active").should("exist"); - // Click on Trigger 3 cy.get(".nav-link").contains("Trigger 3").click(); cy.get(".nav-link") .contains("Trigger 3") @@ -406,7 +495,7 @@ describe("Circulation Triggers - Add New Trigger", () => { cy.intercept("GET", "/api/v1/circulation_rules*", { statusCode: 200, - body: [getMockCirculationRule("CPL", "PT", "BK", 1)], + body: [getMockRuleSet("*", "*", "*", 1, "7")], headers: { "X-Base-Total-Count": "1", "X-Total-Count": "1", @@ -420,7 +509,6 @@ describe("Circulation Triggers - Add New Trigger", () => { cy.get(".modal-dialog").should("not.exist"); cy.contains("Add new trigger").click(); - // Modal should open cy.get(".modal-dialog").should("be.visible"); cy.get(".modal-title").should( "contain", @@ -428,117 +516,27 @@ describe("Circulation Triggers - Add New Trigger", () => { ); }); - it("Should require context selection", () => { + it("Should require context selection fields", () => { cy.visit("/cgi-bin/koha/admin/circulation_triggers"); cy.contains("Add new trigger").click(); - // Should show context selection fields cy.get("#library_id").should("exist"); cy.get("#patron_category_id").should("exist"); cy.get("#item_type_id").should("exist"); - - // All fields should be required - cy.get("input[required]").should("have.length.at.least", 3); - }); - - it("Should proceed through add trigger workflow", () => { - cy.visit("/cgi-bin/koha/admin/circulation_triggers"); - cy.contains("Add new trigger").click(); - - // Step 1: Select context - cy.get("#library_id .vs__search").type("Centerville{enter}", { - force: true, - }); - cy.get("#patron_category_id .vs__search").type("Patron{enter}", { - force: true, - }); - cy.get("#item_type_id .vs__search").type("Books{enter}", { - force: true, - }); - - // Confirm context - cy.contains("Confirm context").click(); - - // Step 2: Should show existing triggers for context - cy.get(".modal-dialog").should("contain", "Existing triggers"); - - // Step 3: Fill in trigger details - cy.get("#overdue_delay").should("exist"); - cy.get("#overdue_delay").type("14"); - - // Set restrict checkouts - cy.get("#restricts-yes").check(); - - // Select letter template - cy.get("#letter_code").should("exist"); - }); - - it("Should validate delay constraints", () => { - cy.visit("/cgi-bin/koha/admin/circulation_triggers"); - cy.contains("Add new trigger").click(); - - // Complete context selection - cy.get("#library_id .vs__search").type("Centerville{enter}", { - force: true, - }); - cy.get("#patron_category_id .vs__search").type("Patron{enter}", { - force: true, - }); - cy.get("#item_type_id .vs__search").type("Books{enter}", { - force: true, - }); - cy.contains("Confirm context").click(); - - // Try to enter invalid delay (should have min/max constraints) - cy.get("#overdue_delay").should("have.attr", "min"); - cy.get("#overdue_delay").should("have.attr", "max"); }); - it("Should successfully submit new trigger", () => { - cy.intercept("PUT", "/api/v1/circulation_rules", { + it("Should show trigger number 1 when adding first trigger for a library with no existing rules", () => { + // Regression test for bug where triggerNumber was NaN when no rules + // existed for a library (triggerCounts[library_id] was undefined). + cy.intercept("GET", "/api/v1/circulation_rules*", { statusCode: 200, - body: getMockCirculationRule("CPL", "PT", "BK", 2), - }).as("create-rule"); - - cy.visit("/cgi-bin/koha/admin/circulation_triggers"); - cy.contains("Add new trigger").click(); - - // Complete form - cy.get("#library_id .vs__search").type("Centerville{enter}", { - force: true, - }); - cy.get("#patron_category_id .vs__search").type("Patron{enter}", { - force: true, - }); - cy.get("#item_type_id .vs__search").type("Books{enter}", { - force: true, + body: [], + headers: { "X-Base-Total-Count": "0", "X-Total-Count": "0" }, }); - cy.contains("Confirm context").click(); - - cy.get("#overdue_delay").type("14"); - cy.get("#restricts-yes").check(); - - // Submit form - cy.get("form").submit(); - - cy.wait("@create-rule"); - - // Should return to list view - cy.get(".modal-dialog").should("not.exist"); - - // Should show success message - cy.get(".alert-info").should("contain", "updated"); - }); - - it("Should handle submission errors", () => { - cy.intercept("PUT", "/api/v1/circulation_rules", { - statusCode: 500, - }).as("create-rule-error"); cy.visit("/cgi-bin/koha/admin/circulation_triggers"); cy.contains("Add new trigger").click(); - // Complete and submit form cy.get("#library_id .vs__search").type("Centerville{enter}", { force: true, }); @@ -550,13 +548,10 @@ describe("Circulation Triggers - Add New Trigger", () => { }); cy.contains("Confirm context").click(); - cy.get("#overdue_delay").type("14"); - cy.get("form").submit(); - - cy.wait("@create-rule-error"); - - // Should show error message - cy.get(".alert-warning").should("contain", "went wrong"); + // trigger number should be 1, not NaN + cy.get("legend").should("contain", "Add new trigger 1"); + cy.get("#overdue_delay").should("exist"); + cy.get("#overdue_delay").should("not.have.attr", "name", /NaN/); }); it("Should allow canceling add operation", () => { @@ -565,221 +560,11 @@ describe("Circulation Triggers - Add New Trigger", () => { cy.get(".modal-dialog").should("be.visible"); - // Click cancel button cy.contains("Cancel").click(); - // Modal should close cy.get(".modal-dialog").should("not.exist"); - - // Should return to list view cy.get("h1").should("contain", "Circulation triggers"); }); - - it("Should show placeholder values for fallback rules", () => { - cy.intercept("GET", "/api/v1/circulation_rules*", { - statusCode: 200, - body: [ - { - ...getMockCirculationRule("*", "*", "*", 1), - rule_value: "7", // Default delay - }, - { - ...getMockCirculationRule("*", "*", "*", 1), - rule_name: "overdue_1_restrict", - rule_value: "1", // Default restrict - }, - ], - headers: { - "X-Base-Total-Count": "2", - "X-Total-Count": "2", - }, - }); - - cy.visit("/cgi-bin/koha/admin/circulation_triggers"); - cy.contains("Add new trigger").click(); - - // Complete context - cy.get("#library_id .vs__search").type("Centerville{enter}", { - force: true, - }); - cy.get("#patron_category_id .vs__search").type("Patron{enter}", { - force: true, - }); - cy.get("#item_type_id .vs__search").type("Books{enter}", { - force: true, - }); - cy.contains("Confirm context").click(); - - // Delay field should show fallback value as placeholder - cy.get("#overdue_delay").should("have.attr", "placeholder", "7"); - - // Restrict checkouts should show fallback value - cy.contains("Fallback to default").should("exist"); - cy.contains("(Yes)").should("exist"); // Showing the fallback value - }); -}); - -describe("Circulation Triggers - Edit Existing Trigger", () => { - beforeEach(() => { - cy.login(); - cy.title().should("eq", "Koha staff interface"); - - cy.intercept("GET", "/api/v1/**", { - statusCode: 200, - body: [], - }); - - cy.intercept("GET", "/api/v1/circulation_rules*", { - statusCode: 200, - body: [ - { - ...getMockCirculationRule("CPL", "PT", "BK", 1), - rule_value: "7", - }, - { - ...getMockCirculationRule("CPL", "PT", "BK", 1), - rule_name: "overdue_1_restrict", - rule_value: "1", - }, - { - ...getMockCirculationRule("CPL", "PT", "BK", 1), - rule_name: "overdue_1_letter", - rule_value: "ODUE1", - }, - ], - headers: { - "X-Base-Total-Count": "3", - "X-Total-Count": "3", - }, - }).as("get-rules"); - }); - - it("Should open edit modal from table", () => { - cy.visit("/cgi-bin/koha/admin/circulation_triggers"); - cy.wait("@get-rules"); - - // Click edit button/icon in table - cy.get("table").contains("Edit").click(); - - // Modal should open - cy.get(".modal-dialog").should("be.visible"); - }); - - it("Should pre-populate form with existing values", () => { - cy.visit("/cgi-bin/koha/admin/circulation_triggers"); - cy.wait("@get-rules"); - - cy.get("table").contains("Edit").click(); - - // Context should be pre-selected and disabled - cy.get("#library_id").should("be.disabled"); - cy.get("#patron_category_id").should("be.disabled"); - cy.get("#item_type_id").should("be.disabled"); - - // Delay should be pre-filled - cy.get("#overdue_delay").should("have.value", "7"); - - // Restrict should be pre-selected - cy.get("#restricts-yes").should("be.checked"); - - // Letter should be pre-selected - cy.get("#letter_code .vs__selected").should("contain", "ODUE1"); - }); - - it("Should allow modifying trigger values within constraints", () => { - cy.visit("/cgi-bin/koha/admin/circulation_triggers"); - cy.wait("@get-rules"); - - cy.get("table").contains("Edit").click(); - - // Change delay - cy.get("#overdue_delay").clear().type("10"); - - // Change restrict - cy.get("#restricts-no").check(); - - // Values should update - cy.get("#overdue_delay").should("have.value", "10"); - cy.get("#restricts-no").should("be.checked"); - }); - - it("Should use increment/decrement buttons for delay", () => { - cy.visit("/cgi-bin/koha/admin/circulation_triggers"); - cy.wait("@get-rules"); - - cy.get("table").contains("Edit").click(); - - const initialDelay = 7; - - // Click increment - cy.get(".increment-btn").click(); - cy.get("#overdue_delay").should("have.value", String(initialDelay + 1)); - - // Click decrement - cy.get(".decrement-btn").click(); - cy.get("#overdue_delay").should("have.value", String(initialDelay)); - }); - - it("Should allow clearing values to use fallback", () => { - cy.visit("/cgi-bin/koha/admin/circulation_triggers"); - cy.wait("@get-rules"); - - cy.get("table").contains("Edit").click(); - - // Clear delay using clear button - cy.get(".clear-btn").click(); - cy.get("#overdue_delay").should("have.value", ""); - - // Select fallback for restrict - cy.get("#restricts-fallback").check(); - - // Should show what fallback value will be used - cy.get("#overdue_delay").should("have.attr", "placeholder"); - }); - - it("Should successfully update trigger", () => { - cy.intercept("PUT", "/api/v1/circulation_rules", { - statusCode: 200, - body: { - ...getMockCirculationRule("CPL", "PT", "BK", 1), - rule_value: "10", - }, - }).as("update-rule"); - - cy.visit("/cgi-bin/koha/admin/circulation_triggers"); - cy.wait("@get-rules"); - - cy.get("table").contains("Edit").click(); - - cy.get("#overdue_delay").clear().type("10"); - cy.get("form").submit(); - - cy.wait("@update-rule"); - - // Modal should close - cy.get(".modal-dialog").should("not.exist"); - - // Should show success message - cy.get(".alert-info").should("exist"); - }); - - it("Should handle update errors", () => { - cy.intercept("PUT", "/api/v1/circulation_rules", { - statusCode: 500, - }).as("update-rule-error"); - - cy.visit("/cgi-bin/koha/admin/circulation_triggers"); - cy.wait("@get-rules"); - - cy.get("table").contains("Edit").click(); - cy.get("#overdue_delay").clear().type("10"); - cy.get("form").submit(); - - cy.wait("@update-rule-error"); - - // Should show error message - cy.get(".alert-warning").should("contain", "went wrong"); - }); }); describe("Circulation Triggers - Delete Trigger", () => { @@ -795,9 +580,9 @@ describe("Circulation Triggers - Delete Trigger", () => { cy.intercept("GET", "/api/v1/circulation_rules*", { statusCode: 200, body: [ - getMockCirculationRule("CPL", "PT", "BK", 1), - getMockCirculationRule("CPL", "PT", "BK", 2), - getMockCirculationRule("CPL", "PT", "BK", 3), + getMockRuleSet("*", "*", "*", 1), + getMockRuleSet("*", "*", "*", 2), + getMockRuleSet("*", "*", "*", 3), ], headers: { "X-Base-Total-Count": "3", @@ -806,20 +591,14 @@ describe("Circulation Triggers - Delete Trigger", () => { }).as("get-rules"); }); - it("Should only show delete button for last trigger", () => { + it("Should only show delete button for last trigger tab", () => { cy.visit("/cgi-bin/koha/admin/circulation_triggers"); cy.wait("@get-rules"); - // Navigate to Trigger 3 tab (last one) cy.get(".nav-link").contains("Trigger 3").click(); - - // Delete button should exist cy.contains("Delete").should("exist"); - // Navigate to Trigger 1 tab cy.get(".nav-link").contains("Trigger 1").click(); - - // Delete button should not exist cy.contains("Delete").should("not.exist"); }); @@ -830,54 +609,9 @@ describe("Circulation Triggers - Delete Trigger", () => { cy.get(".nav-link").contains("Trigger 3").click(); cy.contains("Delete").click(); - // Confirmation modal should open cy.get(".modal-dialog").should("be.visible"); cy.get(".modal-title").should("contain", "Delete"); - - // Should show which trigger will be deleted - cy.get(".modal-body").should("contain", "Trigger 3"); - }); - - it("Should successfully delete trigger", () => { - cy.intercept("PUT", "/api/v1/circulation_rules", { - statusCode: 200, - body: null, - }).as("delete-rule"); - - cy.visit("/cgi-bin/koha/admin/circulation_triggers"); - cy.wait("@get-rules"); - - cy.get(".nav-link").contains("Trigger 3").click(); - cy.contains("Delete").click(); - - // Confirm deletion - cy.contains("Confirm").click(); - - cy.wait("@delete-rule"); - - // Should return to list - cy.get(".modal-dialog").should("not.exist"); - - // Success message should appear - cy.get(".alert-info").should("exist"); - }); - - it("Should handle delete errors", () => { - cy.intercept("PUT", "/api/v1/circulation_rules", { - statusCode: 500, - }).as("delete-rule-error"); - - cy.visit("/cgi-bin/koha/admin/circulation_triggers"); - cy.wait("@get-rules"); - - cy.get(".nav-link").contains("Trigger 3").click(); - cy.contains("Delete").click(); - cy.contains("Confirm").click(); - - cy.wait("@delete-rule-error"); - - // Error message should appear - cy.get(".alert-warning").should("contain", "went wrong"); + cy.get(".modal-body").should("contain", "3"); }); it("Should allow canceling delete operation", () => { @@ -889,13 +623,9 @@ describe("Circulation Triggers - Delete Trigger", () => { cy.get(".modal-dialog").should("be.visible"); - // Click cancel cy.contains("Cancel").click(); - // Modal should close cy.get(".modal-dialog").should("not.exist"); - - // Should still be on Trigger 3 tab cy.get(".nav-link") .contains("Trigger 3") .should("have.class", "active"); @@ -915,9 +645,9 @@ describe("Circulation Triggers - Reset Rule Set", () => { cy.intercept("GET", "/api/v1/circulation_rules*", { statusCode: 200, body: [ - getMockCirculationRule("*", "*", "*", 1), - getMockCirculationRule("CPL", "PT", "BK", 1), - getMockCirculationRule("MPL", "PT", "BK", 1), + getMockRuleSet("*", "*", "*", 1), + getMockRuleSet("CPL", "PT", "BK", 1), + getMockRuleSet("MPL", "PT", "BK", 1), ], headers: { "X-Base-Total-Count": "3", @@ -926,11 +656,10 @@ describe("Circulation Triggers - Reset Rule Set", () => { }).as("get-rules"); }); - it("Should show reset button for explicit rules", () => { + it("Should show Reset button for explicit rule sets in the table", () => { cy.visit("/cgi-bin/koha/admin/circulation_triggers"); cy.wait("@get-rules"); - // Look for reset button in rules table cy.get("table").contains("Reset").should("exist"); }); @@ -938,158 +667,28 @@ describe("Circulation Triggers - Reset Rule Set", () => { cy.visit("/cgi-bin/koha/admin/circulation_triggers"); cy.wait("@get-rules"); - cy.contains("Reset").click(); + cy.get("table").contains("Reset").click(); - // Confirmation modal should open cy.get(".modal-dialog").should("be.visible"); - cy.get(".modal-title").should("contain", "Reset"); - - // Should explain what will happen - cy.get(".modal-body").should("contain", "rule set will be removed"); - }); - - it("Should successfully reset rule set", () => { - cy.intercept("PUT", "/api/v1/circulation_rules", { - statusCode: 200, - body: null, - }).as("reset-rules"); - - cy.visit("/cgi-bin/koha/admin/circulation_triggers"); - cy.wait("@get-rules"); - - cy.contains("Reset").click(); - cy.contains("Confirm").click(); - - cy.wait("@reset-rules"); - - // Should return to list - cy.get(".modal-dialog").should("not.exist"); - - // Success message - cy.get(".alert-info").should("exist"); - }); - - it("Should not allow reset if it's the only rule set", () => { - cy.intercept("GET", "/api/v1/circulation_rules*", { - statusCode: 200, - body: [getMockCirculationRule("CPL", "PT", "BK", 1)], - headers: { - "X-Base-Total-Count": "1", - "X-Total-Count": "1", - }, - }); - - cy.visit("/cgi-bin/koha/admin/circulation_triggers"); - - // Reset button should not be available - cy.contains("Reset").should("not.exist"); - }); -}); - -describe("Circulation Triggers - Letter Template Filtering", () => { - beforeEach(() => { - cy.login(); - cy.title().should("eq", "Koha staff interface"); - - cy.intercept("GET", "/api/v1/**", { - statusCode: 200, - body: [], - }); - }); - - it("Should filter letter templates by library", () => { - cy.visit("/cgi-bin/koha/admin/circulation_triggers"); - cy.contains("Add new trigger").click(); - - // Select Centerville library - cy.get("#library_id .vs__search").type("Centerville{enter}", { - force: true, - }); - cy.get("#patron_category_id .vs__search").type("Patron{enter}", { - force: true, - }); - cy.get("#item_type_id .vs__search").type("Books{enter}", { - force: true, - }); - cy.contains("Confirm context").click(); - - // Letter dropdown should include Centerville-specific and default templates - cy.get("#letter_code .vs__search").click(); - cy.get("#letter_code .vs__dropdown-menu").should("contain", "ODUE1"); - cy.get("#letter_code .vs__dropdown-menu").should("contain", "CPL_ODUE"); - - // Should NOT include templates for other libraries - cy.get("#letter_code .vs__dropdown-menu").should( - "not.contain", - "MPL_ODUE" + cy.get(".modal-title").should( + "contain", + "Confirm circulation rule set reset" ); - }); - - it("Should include No letter option", () => { - cy.visit("/cgi-bin/koha/admin/circulation_triggers"); - cy.contains("Add new trigger").click(); - - cy.get("#library_id .vs__search").type("Centerville{enter}", { - force: true, - }); - cy.get("#patron_category_id .vs__search").type("Patron{enter}", { - force: true, - }); - cy.get("#item_type_id .vs__search").type("Books{enter}", { - force: true, - }); - cy.contains("Confirm context").click(); - - // Should have "No letter" option - cy.get("#letter_code .vs__search").click(); - cy.get("#letter_code .vs__dropdown-menu").should( + cy.get(".modal-body").should( "contain", - "No letter" + "Resetting this rule set for the chosen context" ); }); -}); - -describe("Circulation Triggers - Loading States", () => { - beforeEach(() => { - cy.login(); - cy.title().should("eq", "Koha staff interface"); - }); - - it("Should show loading messages during initialization", () => { - cy.intercept("GET", "/api/v1/circulation_rules*", req => { - // Delay response to see loading state - req.reply({ - delay: 1000, - statusCode: 200, - body: [], - }); - }); + it("Should allow canceling reset operation", () => { cy.visit("/cgi-bin/koha/admin/circulation_triggers"); + cy.wait("@get-rules"); - // Should show loading indicator or message - cy.contains("Loading").should("exist"); - }); - - it("Should show specific loading messages in modals", () => { - cy.intercept("GET", "/api/v1/**", { - statusCode: 200, - body: [], - }); - - cy.intercept("GET", "/api/v1/circulation_rules*", req => { - req.reply({ - delay: 500, - statusCode: 200, - body: [], - }); - }); - - cy.visit("/cgi-bin/koha/admin/circulation_triggers"); - cy.contains("Add new trigger").click(); + cy.get("table").contains("Reset").click(); + cy.get(".modal-dialog").should("be.visible"); - // Should show context-specific loading message - cy.contains("Loading").should("exist"); + cy.contains("Cancel").click(); + cy.get(".modal-dialog").should("not.exist"); }); }); @@ -1113,11 +712,14 @@ describe("Circulation Triggers - Empty States", () => { }); }); - it("Should handle no rules gracefully", () => { + it("Should render without crashing when no rules exist", () => { cy.visit("/cgi-bin/koha/admin/circulation_triggers"); - // Should show message about no rules or empty state - cy.get("body").should("exist"); // Component should still render + cy.get("h1").should("contain", "Circulation triggers"); + cy.get(".alert-warning").should( + "contain", + "No default overdue triggers" + ); }); }); @@ -1144,12 +746,12 @@ describe("Circulation Triggers - Complex Scenarios", () => { cy.intercept("GET", "/api/v1/circulation_rules*", { statusCode: 200, body: [ - getMockCirculationRule("*", "*", "*", 1), - getMockCirculationRule("*", "*", "*", 2), - getMockCirculationRule("*", "*", "*", 3), - getMockCirculationRule("CPL", "*", "*", 1), - getMockCirculationRule("CPL", "PT", "*", 1), - getMockCirculationRule("CPL", "PT", "BK", 1), + getMockRuleSet("*", "*", "*", 1), + getMockRuleSet("*", "*", "*", 2), + getMockRuleSet("*", "*", "*", 3), + getMockRuleSet("CPL", "*", "*", 1), + getMockRuleSet("CPL", "PT", "*", 1), + getMockRuleSet("CPL", "PT", "BK", 1), ], headers: { "X-Base-Total-Count": "6", @@ -1158,58 +760,24 @@ describe("Circulation Triggers - Complex Scenarios", () => { }).as("get-rules"); }); - it("Should handle multiple triggers for same context", () => { + it("Should show all three trigger tabs for default library", () => { cy.visit("/cgi-bin/koha/admin/circulation_triggers"); cy.wait("@get-rules"); - // Should show all three triggers cy.get(".nav-link").contains("Trigger 1").should("exist"); cy.get(".nav-link").contains("Trigger 2").should("exist"); cy.get(".nav-link").contains("Trigger 3").should("exist"); }); - it("Should show inheritance hierarchy correctly", () => { + it("Should handle rapid tab switching without errors", () => { cy.visit("/cgi-bin/koha/admin/circulation_triggers"); cy.wait("@get-rules"); - // Select specific context - cy.get("#library_select .vs__search").type("Centerville{enter}", { - force: true, - }); - cy.get("#patron_category_select .vs__search").type("Patron{enter}", { - force: true, - }); - cy.get("#item_type_select .vs__search").type("Books{enter}", { - force: true, - }); - - // Should show most specific rule - // And indicate inherited values in italic/bold - }); - - it("Should allow rapid successive operations", () => { - cy.visit("/cgi-bin/koha/admin/circulation_triggers"); - cy.wait("@get-rules"); - - // Quickly change filters - cy.get("#library_select .vs__search").type("Centerville{enter}", { - force: true, - }); - cy.get("#patron_category_select .vs__search").type("Patron{enter}", { - force: true, - }); - - // Switch tabs cy.get(".nav-link").contains("Trigger 2").click(); cy.get(".nav-link").contains("Trigger 1").click(); + cy.get(".nav-link").contains("Trigger 3").click(); - // Toggle filter - cy.get("#filter-rules .vs__search").click(); - cy.get("#filter-rules .vs__dropdown-menu") - .contains("explicitly set rules") - .click({ force: true }); - - // Should handle all operations without errors + cy.get(".tab-pane.active").should("exist"); cy.get("body").should("exist"); }); }); @@ -1226,7 +794,7 @@ describe("Circulation Triggers - Browser Navigation", () => { cy.intercept("GET", "/api/v1/circulation_rules*", { statusCode: 200, - body: [getMockCirculationRule("CPL", "PT", "BK", 1)], + body: [getMockRuleSet("*", "*", "*", 1)], headers: { "X-Base-Total-Count": "1", "X-Total-Count": "1", @@ -1234,26 +802,23 @@ describe("Circulation Triggers - Browser Navigation", () => { }); }); - it("Should handle browser back button", () => { + it("Should close modal on browser back button", () => { cy.visit("/cgi-bin/koha/admin/circulation_triggers"); cy.contains("Add new trigger").click(); cy.get(".modal-dialog").should("be.visible"); - // Use browser back cy.go("back"); - // Modal should close cy.get(".modal-dialog").should("not.exist"); }); - it("Should handle browser forward button", () => { + it("Should reopen modal on browser forward button", () => { cy.visit("/cgi-bin/koha/admin/circulation_triggers"); cy.contains("Add new trigger").click(); cy.go("back"); cy.go("forward"); - // Modal should reopen cy.get(".modal-dialog").should("be.visible"); }); }); -- 2.53.0