Bug 10190: (follow-up): test: cypress tests update

New tests:
- regression: mtt cells render as .fallback (bold italics)
- bg-success-subtle highlight on completed steps in the add flow
- disabled-Delete states:
  - patron category filter set
  - item type filter set
  - default library, if any library has a trigger higher that default's highest
- redirect to admin home if not permitted to access the page
- in the edit/add stepper form
  - check row actions are enabled at the 'selectOrAdd' step
  - check row actions are disabled once add mode is entered
- check the action column is always available on the main page

Clean up / general updates:
- replace the /api/v1/** catch-all intercept with per-endpoint intercepts so @get-rules waits resolve reliably
- remove now-redundant cy.wait("@get-rules") calls
- drop unused mount / dayjs / dates imports
- ensure expected strings match UI where strings have changed

Assisted-by: Claude Opus 4.7 (Anthropic)

AI used to draft enhancements tests. Changes were reviewed, tweaked, and commited manually and gradually (with a pre-submission commit squash). Tests were run multiple times and adjusted iteratively to match code changes, cover regressions as those were found, and handle vue page load.

Sponsored-by: Glasgow Colleges Library Group <https://library.cityofglasgowcollege.ac.uk>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
This commit is contained in:
Chloe Zermatten 2026-05-26 15:44:17 +00:00 committed by Pedro Amorim
parent d6f596c514
commit 763dc46e31
No known key found for this signature in database
GPG Key ID: 06B75BA7F3894D55
1 changed files with 316 additions and 49 deletions

View File

@ -1,13 +1,3 @@
import { mount } from "@cypress/vue";
const dayjs = require("dayjs");
const dates = {
today_iso: dayjs().format("YYYY-MM-DD"),
today_us: dayjs().format("MM/DD/YYYY"),
tomorrow_iso: dayjs().add(1, "day").format("YYYY-MM-DD"),
tomorrow_us: dayjs().add(1, "day").format("MM/DD/YYYY"),
};
// 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 = (
@ -190,7 +180,6 @@ describe("Circulation Triggers - Initial Load", () => {
it("Should display trigger tabs when rules exist", () => {
cy.visit("/cgi-bin/koha/admin/circulation_triggers");
cy.wait("@get-rules");
cy.get("#circ-triggers-tabs").should("exist");
cy.get(".nav-link").contains("Trigger 1").should("exist");
@ -208,6 +197,23 @@ describe("Circulation Triggers - Initial Load", () => {
// Component should still render without crashing
cy.get("h1").should("contain", "Circulation triggers");
});
// regression check (mtt stopped reliably displaying fallback status)
it("Should render MTT cells with the fallback class when no explicit mtt rule exists", () => {
// mtt is one DB column shared by email/print/sms, so the three are
// atomic — you can't set sms while inheriting print/email. When no
// rule set provides an mtt value, all three cells render .fallback
// (bold italics) to signal inheritance.
cy.visit("/cgi-bin/koha/admin/circulation_triggers");
cy.get("#trigger-table-main tbody tr")
.first()
.within(() => {
cy.get("td").eq(5).find("span.fallback").should("exist");
cy.get("td").eq(6).find("span.fallback").should("exist");
cy.get("td").eq(7).find("span.fallback").should("exist");
});
});
});
describe("Circulation Triggers - No default rules warning", () => {
@ -258,7 +264,6 @@ describe("Circulation Triggers - No default rules warning", () => {
}).as("get-rules");
cy.visit("/cgi-bin/koha/admin/circulation_triggers");
cy.wait("@get-rules");
cy.get(".alert-warning").should(
"contain",
@ -279,7 +284,6 @@ describe("Circulation Triggers - No default rules warning", () => {
}).as("get-rules");
cy.visit("/cgi-bin/koha/admin/circulation_triggers");
cy.wait("@get-rules");
cy.get(".alert-warning").should(
"contain",
@ -299,7 +303,7 @@ describe("Circulation Triggers - No default rules warning", () => {
}).as("get-rules");
cy.visit("/cgi-bin/koha/admin/circulation_triggers");
cy.wait("@get-rules");
cy.get("#circ-triggers-tabs").should("exist");
cy.get(".alert-warning").should("not.exist");
});
@ -329,7 +333,6 @@ describe("Circulation Triggers - No default rules warning", () => {
}).as("get-rules");
cy.visit("/cgi-bin/koha/admin/circulation_triggers");
cy.wait("@get-rules");
cy.get(".alert-warning").contains("Centerville").click();
@ -380,7 +383,6 @@ describe("Circulation Triggers - Filtering", () => {
it("Should filter by library", () => {
cy.visit("/cgi-bin/koha/admin/circulation_triggers");
cy.wait("@get-rules");
cy.get("#library_select .vs__search").type("Centerville{enter}", {
force: true,
@ -393,7 +395,6 @@ describe("Circulation Triggers - Filtering", () => {
it("Should filter by patron category", () => {
cy.visit("/cgi-bin/koha/admin/circulation_triggers");
cy.wait("@get-rules");
cy.get("#patron_category_select .vs__search").type("Patron{enter}", {
force: true,
@ -406,7 +407,6 @@ describe("Circulation Triggers - Filtering", () => {
it("Should filter by item type", () => {
cy.visit("/cgi-bin/koha/admin/circulation_triggers");
cy.wait("@get-rules");
cy.get("#item_type_select .vs__search").type("Books{enter}", {
force: true,
@ -416,7 +416,6 @@ describe("Circulation Triggers - Filtering", () => {
it("Should toggle between explicit and all applicable rules", () => {
cy.visit("/cgi-bin/koha/admin/circulation_triggers");
cy.wait("@get-rules");
cy.get("#filter-rules").should("exist");
@ -432,7 +431,15 @@ describe("Circulation Triggers - Tab Navigation", () => {
cy.login();
cy.title().should("eq", "Koha staff interface");
cy.intercept("GET", "/api/v1/**", {
cy.intercept("GET", "/api/v1/libraries*", {
statusCode: 200,
body: [],
});
cy.intercept("GET", "/api/v1/patron_categories*", {
statusCode: 200,
body: [],
});
cy.intercept("GET", "/api/v1/item_types*", {
statusCode: 200,
body: [],
});
@ -453,7 +460,6 @@ describe("Circulation Triggers - Tab Navigation", () => {
it("Should switch between trigger tabs", () => {
cy.visit("/cgi-bin/koha/admin/circulation_triggers");
cy.wait("@get-rules");
cy.get(".nav-link")
.contains("Trigger 1")
@ -505,7 +511,6 @@ describe("Circulation Triggers - Add New Trigger", () => {
it("Should open add trigger modal", () => {
cy.visit("/cgi-bin/koha/admin/circulation_triggers");
cy.get(".modal-dialog").should("not.exist");
cy.contains("Add new trigger").click();
@ -528,12 +533,8 @@ describe("Circulation Triggers - Add New Trigger", () => {
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: [],
headers: { "X-Base-Total-Count": "0", "X-Total-Count": "0" },
});
// The default beforeEach intercept returns only a */*/* rule, so CPL
// has no library-specific rules — exact regression case.
cy.visit("/cgi-bin/koha/admin/circulation_triggers");
cy.contains("Add new trigger").click();
@ -548,13 +549,102 @@ describe("Circulation Triggers - Add New Trigger", () => {
});
cy.contains("Confirm context").click();
cy.get("#trigger-table-form td.actions button").contains("Add").click();
// 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", () => {
it("Should show enabled row actions in the trigger table at the selectOrAdd step", () => {
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();
cy.get("#trigger-table-form td.actions").should("exist");
cy.get("#trigger-table-form td.actions button").should("exist");
cy.get("#trigger-table-form td.actions button[disabled]").should(
"not.exist"
);
});
it("Should disable row actions once add mode is entered from the trigger table", () => {
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();
cy.get("#trigger-table-form td.actions button").contains("Add").click();
cy.get("#trigger-table-form td.actions button[disabled]").should(
"exist"
);
cy.get("#trigger-table-form td.actions button:not([disabled])").should(
"not.exist"
);
});
it("Should mark non-active steps with bg-success-subtle in the add flow", () => {
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();
// At selectOrAdd: the prior ConfirmContext step is highlighted,
// but the active TriggersTable section is not.
cy.contains("legend", "Selected trigger context")
.parents(".page-section")
.first()
.should("have.class", "bg-success-subtle");
cy.get("#trigger-table-form")
.parents(".page-section")
.first()
.should("not.have.class", "bg-success-subtle");
// Enter add mode: now both prior steps should be highlighted.
cy.get("#trigger-table-form td.actions button").contains("Add").click();
cy.get("#trigger-table-form")
.parents(".page-section")
.first()
.should("have.class", "bg-success-subtle");
});
it("Should show row actions in the trigger table on the main list", () => {
cy.visit("/cgi-bin/koha/admin/circulation_triggers");
cy.get("#trigger-table-main td.actions").should("exist");
});
it("Should allow cancelling add operation", () => {
cy.visit("/cgi-bin/koha/admin/circulation_triggers");
cy.contains("Add new trigger").click();
@ -572,7 +662,15 @@ describe("Circulation Triggers - Delete Trigger", () => {
cy.login();
cy.title().should("eq", "Koha staff interface");
cy.intercept("GET", "/api/v1/**", {
cy.intercept("GET", "/api/v1/libraries*", {
statusCode: 200,
body: [],
});
cy.intercept("GET", "/api/v1/patron_categories*", {
statusCode: 200,
body: [],
});
cy.intercept("GET", "/api/v1/item_types*", {
statusCode: 200,
body: [],
});
@ -593,7 +691,6 @@ describe("Circulation Triggers - Delete Trigger", () => {
it("Should only show delete button for last trigger tab", () => {
cy.visit("/cgi-bin/koha/admin/circulation_triggers");
cy.wait("@get-rules");
cy.get(".nav-link").contains("Trigger 3").click();
cy.contains("Delete").should("exist");
@ -604,19 +701,20 @@ describe("Circulation Triggers - Delete Trigger", () => {
it("Should show confirmation dialog for delete", () => {
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.get(".modal-dialog").should("be.visible");
cy.get(".modal-title").should("contain", "Delete");
cy.get(".modal-body").should("contain", "3");
cy.get(".modal-title").should(
"contain",
"Confirm deletion of Trigger 3"
);
cy.get(".modal-body").should("contain", "Rule sets to be deleted");
});
it("Should allow canceling delete operation", () => {
cy.visit("/cgi-bin/koha/admin/circulation_triggers");
cy.wait("@get-rules");
cy.get(".nav-link").contains("Trigger 3").click();
cy.contains("Delete").click();
@ -632,12 +730,155 @@ describe("Circulation Triggers - Delete Trigger", () => {
});
});
describe("Circulation Triggers - Delete Trigger Disabled States", () => {
beforeEach(() => {
cy.login();
cy.title().should("eq", "Koha staff interface");
cy.intercept("GET", "/api/v1/libraries*", {
statusCode: 200,
body: getMockLibraries(),
});
cy.intercept("GET", "/api/v1/patron_categories*", {
statusCode: 200,
body: getMockPatronCategories(),
});
cy.intercept("GET", "/api/v1/item_types*", {
statusCode: 200,
body: getMockItemTypes(),
});
});
// this is because we always delete a trigger for a library
// where a trigger is all the rulesets for the triggerNumber for the library
// this mirrors the create trigger pattern, which is also per library per triggerNumber
it("Should disable Delete with tooltip when a patron category filter is set", () => {
cy.intercept("GET", "/api/v1/circulation_rules*", {
statusCode: 200,
body: [
getMockRuleSet("*", "*", "*", 1),
getMockRuleSet("*", "*", "*", 2),
],
headers: {
"X-Base-Total-Count": "2",
"X-Total-Count": "2",
},
}).as("get-rules");
cy.visit("/cgi-bin/koha/admin/circulation_triggers");
cy.get(".nav-link").contains("Trigger 2").click();
cy.contains("Delete").should("not.have.attr", "disabled");
cy.get("#patron_category_select .vs__search").type("Patron{enter}", {
force: true,
});
cy.contains("Delete").should("have.attr", "disabled");
cy.contains("Delete")
.parent("span")
.should(
"have.attr",
"title",
"Delete is only available when no patron category or item type filter is set, as it removes all explicit rules for this trigger across the selected library."
);
});
it("Should disable Delete with tooltip when an item type filter is set", () => {
cy.intercept("GET", "/api/v1/circulation_rules*", {
statusCode: 200,
body: [
getMockRuleSet("*", "*", "*", 1),
getMockRuleSet("*", "*", "*", 2),
],
headers: {
"X-Base-Total-Count": "2",
"X-Total-Count": "2",
},
}).as("get-rules");
cy.visit("/cgi-bin/koha/admin/circulation_triggers");
cy.get(".nav-link").contains("Trigger 2").click();
cy.contains("Delete").should("not.have.attr", "disabled");
cy.get("#item_type_select .vs__search").type("Books{enter}", {
force: true,
});
cy.contains("Delete").should("have.attr", "disabled");
cy.contains("Delete")
.parent("span")
.should(
"have.attr",
"title",
"Delete is only available when no patron category or item type filter is set, as it removes all explicit rules for this trigger across the selected library."
);
});
it("Should disable Delete on default library when another library has higher triggers", () => {
// Default library has triggers 1 and 2; CPL has triggers up to 3.
// Deleting trigger 2 on the default library would orphan CPL trigger 3.
// Since we do not allow breaks in trigger sequentiality, this should result in the button being disabled
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: [
getMockRuleSet("*", "*", "*", 1),
getMockRuleSet("*", "*", "*", 2),
],
headers: {
"X-Base-Total-Count": "2",
"X-Total-Count": "2",
},
});
} else {
req.reply({
statusCode: 200,
body: [
getMockRuleSet("*", "*", "*", 1),
getMockRuleSet("*", "*", "*", 2),
getMockRuleSet("CPL", "*", "*", 1),
getMockRuleSet("CPL", "*", "*", 2),
getMockRuleSet("CPL", "*", "*", 3),
],
headers: {
"X-Base-Total-Count": "5",
"X-Total-Count": "5",
},
});
}
}).as("get-rules");
cy.visit("/cgi-bin/koha/admin/circulation_triggers");
cy.get(".nav-link").contains("Trigger 2").click();
cy.contains("Delete").should("have.attr", "disabled");
cy.contains("Delete")
.parent("span")
.should("have.attr", "title")
.and("contain", "Centerville")
.and("contain", "would be orphaned");
});
});
describe("Circulation Triggers - Reset Rule Set", () => {
beforeEach(() => {
cy.login();
cy.title().should("eq", "Koha staff interface");
cy.intercept("GET", "/api/v1/**", {
cy.intercept("GET", "/api/v1/libraries*", {
statusCode: 200,
body: [],
});
cy.intercept("GET", "/api/v1/patron_categories*", {
statusCode: 200,
body: [],
});
cy.intercept("GET", "/api/v1/item_types*", {
statusCode: 200,
body: [],
});
@ -658,14 +899,12 @@ describe("Circulation Triggers - Reset Rule Set", () => {
it("Should show Reset button for explicit rule sets in the table", () => {
cy.visit("/cgi-bin/koha/admin/circulation_triggers");
cy.wait("@get-rules");
cy.get("table").contains("Reset").should("exist");
});
it("Should show confirmation dialog for reset", () => {
cy.visit("/cgi-bin/koha/admin/circulation_triggers");
cy.wait("@get-rules");
cy.get("table").contains("Reset").click();
@ -674,15 +913,11 @@ describe("Circulation Triggers - Reset Rule Set", () => {
"contain",
"Confirm circulation rule set reset"
);
cy.get(".modal-body").should(
"contain",
"Resetting this rule set for the chosen context"
);
cy.get(".modal-body").should("contain", "Rule set selected for reset");
});
it("Should allow canceling reset operation", () => {
cy.visit("/cgi-bin/koha/admin/circulation_triggers");
cy.wait("@get-rules");
cy.get("table").contains("Reset").click();
cy.get(".modal-dialog").should("be.visible");
@ -697,7 +932,15 @@ describe("Circulation Triggers - Empty States", () => {
cy.login();
cy.title().should("eq", "Koha staff interface");
cy.intercept("GET", "/api/v1/**", {
cy.intercept("GET", "/api/v1/libraries*", {
statusCode: 200,
body: [],
});
cy.intercept("GET", "/api/v1/patron_categories*", {
statusCode: 200,
body: [],
});
cy.intercept("GET", "/api/v1/item_types*", {
statusCode: 200,
body: [],
});
@ -709,7 +952,7 @@ describe("Circulation Triggers - Empty States", () => {
"X-Base-Total-Count": "0",
"X-Total-Count": "0",
},
});
}).as("get-rules");
});
it("Should render without crashing when no rules exist", () => {
@ -762,7 +1005,6 @@ describe("Circulation Triggers - Complex Scenarios", () => {
it("Should show all three trigger tabs for default library", () => {
cy.visit("/cgi-bin/koha/admin/circulation_triggers");
cy.wait("@get-rules");
cy.get(".nav-link").contains("Trigger 1").should("exist");
cy.get(".nav-link").contains("Trigger 2").should("exist");
@ -771,7 +1013,6 @@ describe("Circulation Triggers - Complex Scenarios", () => {
it("Should handle rapid tab switching without errors", () => {
cy.visit("/cgi-bin/koha/admin/circulation_triggers");
cy.wait("@get-rules");
cy.get(".nav-link").contains("Trigger 2").click();
cy.get(".nav-link").contains("Trigger 1").click();
@ -787,7 +1028,15 @@ describe("Circulation Triggers - Browser Navigation", () => {
cy.login();
cy.title().should("eq", "Koha staff interface");
cy.intercept("GET", "/api/v1/**", {
cy.intercept("GET", "/api/v1/libraries*", {
statusCode: 200,
body: [],
});
cy.intercept("GET", "/api/v1/patron_categories*", {
statusCode: 200,
body: [],
});
cy.intercept("GET", "/api/v1/item_types*", {
statusCode: 200,
body: [],
});
@ -799,7 +1048,7 @@ describe("Circulation Triggers - Browser Navigation", () => {
"X-Base-Total-Count": "1",
"X-Total-Count": "1",
},
});
}).as("get-rules");
});
it("Should close modal on browser back button", () => {
@ -822,3 +1071,21 @@ describe("Circulation Triggers - Browser Navigation", () => {
cy.get(".modal-dialog").should("be.visible");
});
});
describe("Circulation Triggers - Permissions", () => {
beforeEach(() => {
cy.login();
cy.title().should("eq", "Koha staff interface");
});
it("Should redirect to admin home when config returns no valid permissions", () => {
cy.intercept("GET", "/api/v1/circulation_rules/config*", {
statusCode: 403,
}).as("get-config");
cy.visit("/cgi-bin/koha/admin/circulation_triggers");
cy.wait("@get-config");
cy.url().should("include", "admin-home");
});
});