Bug 40180: (bug 40118 follow-up) Add Cypress tests

This patch heavily reuses what has been introduced by the previous
patches in the tree, and add a insertSampleHold task to be reusable
easily in other tests.

Test plan:
All Cypress tests must pass

Signed-off-by: Owen Leonard <oleonard@myacpl.org>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
(cherry picked from commit af311b5395)
Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
This commit is contained in:
Jonathan Druart 2025-06-17 16:09:59 +02:00 committed by Paul Derscheid
parent 604ede20d9
commit f66133bcd5
No known key found for this signature in database
GPG Key ID: C25F9FD44CED595B
5 changed files with 323 additions and 77 deletions

View File

@ -24,7 +24,7 @@ describe("catalogue/detail/holdings_table with items", () => {
});
afterEach(function () {
cy.task("deleteSampleObjects", this.objects);
cy.task("deleteSampleObjects", [this.objects]);
cy.set_syspref(
"AlwaysShowHoldingsTableFilters",
this.syspref_AlwaysShowHoldingsTableFilters
@ -279,7 +279,7 @@ describe("catalogue/detail/holdings_table without items", () => {
});
afterEach(function () {
cy.task("deleteSampleObjects", this.objects);
cy.task("deleteSampleObjects", [this.objects]);
cy.set_syspref(
"AlwaysShowHoldingsTableFilters",
this.syspref_AlwaysShowHoldingsTableFilters

View File

@ -0,0 +1,101 @@
describe("circ/pendingreserves/holdst", () => {
const table_id = "holdst";
beforeEach(() => {
cy.login();
cy.title().should("eq", "Koha staff interface");
cy.task("insertSampleBiblio", {
item_count: 2,
options: { different_libraries: true },
}).then(objects_biblio_1 => {
cy.wrap(objects_biblio_1).as("objects_biblio_1");
const commonLibraryId = objects_biblio_1.items[0].home_library_id;
cy.task("insertSampleHold", {
biblio: objects_biblio_1.biblio,
library_id: commonLibraryId,
}).then(objects_hold_1 => {
cy.wrap(objects_hold_1).as("objects_hold_1");
});
cy.task("insertSampleBiblio", { item_count: 1 }).then(
objects_biblio_2 => {
cy.wrap(objects_biblio_2).as("objects_biblio_2");
cy.task("insertSampleHold", {
biblio: objects_biblio_2.biblio,
library_id: objects_biblio_2.items[0].home_library_id,
}).then(objects_hold_2 => {
cy.wrap(objects_hold_2).as("objects_hold_2");
});
cy.task("query", {
sql: "UPDATE items SET homebranch=?, holdingbranch=? WHERE itemnumber=?",
values: [
commonLibraryId,
commonLibraryId,
objects_biblio_2.items[0].item_id,
],
});
}
);
});
});
afterEach(function () {
cy.task("deleteSampleObjects", [
this.objects_hold_1,
this.objects_hold_2,
this.objects_biblio_1,
this.objects_biblio_2,
]);
});
it("Should render library filters", function () {
cy.visit(
"/cgi-bin/koha/circ/pendingreserves.pl?from=2000-01-01&to=2999-12-31&run_report=Submit"
);
cy.get(`#${table_id} thead tr:eq(1) th:eq(5)`).should(
"have.text",
"Libraries"
);
// select has library names in value and text, this table is not using server-side processing
let libraries = this.objects_biblio_1.libraries
.map(library => library.name)
.sort();
cy.get(`#${table_id} thead tr:eq(1) th:eq(5) select`)
.children()
.should("have.length", 3)
.then(options => {
expect(options.eq(0).val()).to.eq("");
expect(options.eq(1).val()).to.eq(libraries[0]);
expect(options.eq(2).val()).to.eq(libraries[1]);
});
});
it("Should filter table on library", function () {
cy.visit(
"/cgi-bin/koha/circ/pendingreserves.pl?from=2000-01-01&to=2999-12-31&run_report=Submit"
);
cy.get(`#${table_id} thead tr:eq(1) th:eq(5) select`).select(
this.objects_biblio_1.libraries[0].name,
{ force: true }
);
cy.get(`#${table_id} tbody tr:eq(0) td:eq(4)`).should(
"contain",
this.objects_biblio_1.biblio.title
);
cy.get(`#${table_id} tbody tr:eq(1) td:eq(4)`).should(
"contain",
this.objects_biblio_2.biblio.title
);
cy.get(`#${table_id} tbody tr`).should("have.length", 2);
cy.get(`#${table_id} thead tr:eq(1) th:eq(5) select`).select(
this.objects_biblio_1.libraries[1].name
);
cy.get(`#${table_id} tbody tr:eq(0) td:eq(4)`).should(
"contain",
this.objects_biblio_1.biblio.title
);
cy.get(`#${table_id} tbody tr`).should("have.length", 1);
});
});

View File

@ -1,11 +1,13 @@
const { query } = require("./../../plugins/db.js");
describe("insertSampleBiblio", () => {
it("should generate library and item type", () => {
it("insertSampleBiblio - should generate library and item type", () => {
cy.task("insertSampleBiblio", { item_count: 3 }).then(objects => {
const biblio_id = objects.biblio.biblio_id;
expect(typeof objects.biblio.biblio_id).to.be.equal("number");
expect(typeof objects.biblio.title).to.be.equal("string");
expect(typeof objects.biblio.author).to.be.equal("string");
expect(typeof biblio_id).to.be.equal("number");
const biblio_id = objects.biblio.biblio_id;
cy.task("query", {
sql: "SELECT COUNT(*) as count FROM biblio WHERE biblionumber=?",
@ -30,7 +32,7 @@ describe("insertSampleBiblio", () => {
cy.task("query", {
sql: "SELECT COUNT(*) as count FROM branches WHERE branchcode=?",
values: [objects.library.library_id],
values: [objects.libraries[0].library_id],
}).then(result => {
expect(result[0].count).to.be.equal(1);
});
@ -60,7 +62,7 @@ describe("insertSampleBiblio", () => {
cy.task("query", {
sql: "SELECT COUNT(*) as count FROM branches WHERE branchcode=?",
values: [objects.library.library_id],
values: [objects.libraries[0].library_id],
}).then(result => {
expect(result[0].count).to.be.equal(0);
});

View File

@ -4,6 +4,7 @@ const { buildSampleObject, buildSampleObjects } = require("./mockData.js");
const {
insertSampleBiblio,
insertSampleHold,
insertObject,
deleteSampleObjects,
} = require("./insertData.js");
@ -36,11 +37,14 @@ module.exports = (on, config) => {
},
buildSampleObject,
buildSampleObjects,
insertSampleBiblio({ item_count }) {
return insertSampleBiblio(item_count, baseUrl, authHeader);
insertSampleBiblio(args) {
return insertSampleBiblio({ ...args, baseUrl, authHeader });
},
insertObject({ type, object }) {
return insertObject(type, object, baseUrl, authHeader);
insertSampleHold(args) {
return insertSampleHold({ ...args, baseUrl, authHeader });
},
insertObject(args) {
return insertObject({ ...args, baseUrl, authHeader });
},
deleteSampleObjects,
query,

View File

@ -3,27 +3,22 @@ const { query } = require("./db.js");
const { apiGet, apiPost } = require("./api-client.js");
const insertSampleBiblio = async (item_count, baseUrl, authHeader) => {
let generated_objects = {};
const objects = [{ object: "library" }, { object: "item_type" }];
for (const { object } of objects) {
const attributes = await buildSampleObject({ object });
generated_objects[object] = attributes;
}
const library = await insertObject(
"library",
generated_objects["library"],
const insertSampleBiblio = async ({
item_count,
options,
baseUrl,
authHeader,
}) => {
const generatedItemType = await buildSampleObject({ object: "item_type" });
const item_type = await insertObject({
type: "item_type",
object: generatedItemType,
baseUrl,
authHeader
);
const item_type = await insertObject(
"item_type",
generated_objects["item_type"],
baseUrl,
authHeader
);
authHeader,
});
let title = "Some boring read";
let author = "Some boring author";
let biblio = {
leader: " nam a22 7a 4500",
fields: [
@ -32,14 +27,14 @@ const insertSampleBiblio = async (item_count, baseUrl, authHeader) => {
245: {
ind1: "",
ind2: "",
subfields: [{ a: "Some boring read" }],
subfields: [{ a: title }],
},
},
{
100: {
ind1: "",
ind2: "",
subfields: [{ c: "Some boring author" }],
subfields: [{ c: author }],
},
},
{
@ -66,6 +61,8 @@ const insertSampleBiblio = async (item_count, baseUrl, authHeader) => {
// We might need to refine that in the future
biblio = {
biblio_id,
title,
author,
};
let items = buildSampleObjects({
@ -80,9 +77,8 @@ const insertSampleBiblio = async (item_count, baseUrl, authHeader) => {
restricted_status: 0,
new_status: null,
issues: 0,
checked_out_date: null,
item_type_id: item_type.item_type_id,
home_library_id: library.library_id,
holding_library_id: library.library_id,
},
});
items = items.map(
@ -123,7 +119,37 @@ const insertSampleBiblio = async (item_count, baseUrl, authHeader) => {
}) => rest
);
let createdItems = [];
let libraries = [];
let commonLibrary;
if (!options || !options.different_libraries) {
const generatedLibrary = await buildSampleObject({ object: "library" });
commonLibrary = await insertObject({
type: "library",
object: generatedLibrary,
baseUrl,
authHeader,
});
libraries.push(commonLibrary);
}
for (const item of items) {
if (options?.different_libraries) {
const generatedLibrary = await buildSampleObject({
object: "library",
});
const library = await insertObject({
type: "library",
object: generatedLibrary,
baseUrl,
authHeader,
});
libraries.push(library);
item.home_library_id = library.library_id;
item.holding_library_id = library.library_id;
} else {
item.home_library_id = commonLibrary.library_id;
item.holding_library_id = commonLibrary.library_id;
}
await apiPost({
endpoint: `/api/v1/biblios/${biblio_id}/items`,
body: item,
@ -131,51 +157,128 @@ const insertSampleBiblio = async (item_count, baseUrl, authHeader) => {
authHeader,
}).then(i => createdItems.push(i));
}
return { biblio, items: createdItems, library, item_type };
return { biblio, items: createdItems, libraries, item_type };
};
const deleteSampleObjects = async objects => {
const deletionOrder = ["items", "biblio", "library", "item_type"];
const insertSampleHold = async ({
item,
biblio,
library_id,
baseUrl,
authHeader,
}) => {
library_id ||= item.home_library_id;
const generatedPatron = await buildSampleObject({
object: "patron",
values: { library_id, incorrect_address: null, patron_card_lost: null },
});
const patron = await insertObject({
type: "patron",
object: generatedPatron,
baseUrl,
authHeader,
});
const generatedHold = buildSampleObject({
object: "hold",
values: {
patron_id: patron.patron_id,
biblio_id: item?.biblio_id || biblio.biblio_id,
pickup_library_id: library_id,
item_id: item?.item_id || null,
},
});
const hold = await insertObject({
type: "hold",
object: generatedHold,
baseUrl,
authHeader,
});
return { hold, patron };
};
const deleteSampleObjects = async allObjects => {
if (!Array.isArray(allObjects)) {
allObjects = [allObjects];
}
const deletionOrder = [
"hold",
"patron",
"item",
"items",
"biblio",
"library",
"libraries",
"item_type",
];
for (const type of deletionOrder) {
if (!objects.hasOwnProperty(type)) {
continue;
}
if (Array.isArray(objects[type]) && objects[type].length == 0) {
// Empty array
continue;
}
switch (type) {
case "biblio":
await query({
sql: "DELETE FROM biblio WHERE biblionumber=?",
values: [objects[type].biblio_id],
});
break;
case "items":
let item_ids = objects[type].map(i => i.item_id);
await query({
sql: `DELETE FROM items WHERE itemnumber IN (${item_ids.map(() => "?").join(",")})`,
values: item_ids,
});
break;
case "item":
await query({
sql: "DELETE FROM items WHERE itemnumber = ?",
values: [objects[type].item_id],
});
break;
case "library":
await query({
sql: "DELETE FROM branches WHERE branchcode = ?",
values: [objects[type].library_id],
});
break;
case "item_type":
await query({
sql: "DELETE FROM itemtypes WHERE itemtype = ?",
values: [objects[type].item_type_id],
});
break;
for (const objects of allObjects) {
if (!objects.hasOwnProperty(type)) {
continue;
}
if (Array.isArray(objects[type]) && objects[type].length == 0) {
// Empty array
continue;
}
switch (type) {
case "biblio":
await query({
sql: "DELETE FROM biblio WHERE biblionumber=?",
values: [objects[type].biblio_id],
});
break;
case "items":
let item_ids = objects[type].map(i => i.item_id);
await query({
sql: `DELETE FROM items WHERE itemnumber IN (${item_ids.map(() => "?").join(",")})`,
values: item_ids,
});
break;
case "item":
await query({
sql: "DELETE FROM items WHERE itemnumber = ?",
values: [objects[type].item_id],
});
break;
case "library":
await query({
sql: "DELETE FROM branches WHERE branchcode = ?",
values: [objects[type].library_id],
});
break;
case "libraries":
let library_ids = objects[type].map(i => i.library_id);
await query({
sql: `DELETE FROM branches WHERE branchcode IN (${library_ids.map(() => "?").join(",")})`,
values: library_ids,
});
break;
case "hold":
await query({
sql: "DELETE FROM reserves WHERE reserve_id = ?",
values: [objects[type].hold_id],
});
break;
case "item_type":
await query({
sql: "DELETE FROM itemtypes WHERE itemtype = ?",
values: [objects[type].item_type_id],
});
break;
case "patron":
await query({
sql: "DELETE FROM borrowers WHERE borrowernumber = ?",
values: [objects[type].patron_id],
});
break;
default:
console.log(
`Not implemented yet: cannot deleted object '${type}'`
);
}
}
}
return true;
@ -199,7 +302,7 @@ const insertLibrary = async (library, baseUrl, authHeader) => {
});
};
const insertObject = async (type, object, baseUrl, authHeader) => {
const insertObject = async ({ type, object, baseUrl, authHeader }) => {
if (type == "patron") {
await query({
sql: "SELECT COUNT(*) AS count FROM branches WHERE branchcode = ?",
@ -274,6 +377,41 @@ const insertObject = async (type, object, baseUrl, authHeader) => {
});
})
.then(item_types => item_types[0]);
} else if (type == "hold") {
const {
hold_id,
deleted_biblio_id,
item_group_id,
desk_id,
cancellation_date,
cancellation_reason,
notes,
priority,
status,
timestamp,
waiting_date,
expiration_date,
lowest_priority,
suspended,
suspended_until,
non_priority,
item_type,
item_level,
cancellation_requested,
biblio,
deleted_biblio,
item,
pickup_library,
hold_date,
...hold
} = object;
return apiPost({
endpoint: `/api/v1/holds`,
body: hold,
baseUrl,
authHeader,
});
} else {
return false;
}
@ -283,6 +421,7 @@ const insertObject = async (type, object, baseUrl, authHeader) => {
module.exports = {
insertSampleBiblio,
insertSampleHold,
insertObject,
deleteSampleObjects,
};