Bugzilla – Attachment 183372 Details for
Bug 40174
Add a way to cleanly insert data in DB from Cypress tests
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 40174: Allow proper E2E testing with Cypress
Bug-40174-Allow-proper-E2E-testing-with-Cypress.patch (text/plain), 49.30 KB, created by
Jonathan Druart
on 2025-06-19 10:50:58 UTC
(
hide
)
Description:
Bug 40174: Allow proper E2E testing with Cypress
Filename:
MIME Type:
Creator:
Jonathan Druart
Created:
2025-06-19 10:50:58 UTC
Size:
49.30 KB
patch
obsolete
>From 2d03c6140c965c76440bcd6dae5e118ece3a441b Mon Sep 17 00:00:00 2001 >From: Jonathan Druart <jonathan.druart@bugs.koha-community.org> >Date: Thu, 12 Jun 2025 13:22:47 +0200 >Subject: [PATCH] Bug 40174: Allow proper E2E testing with Cypress >MIME-Version: 1.0 >Content-Type: text/plain; charset=UTF-8 >Content-Transfer-Encoding: 8bit > >This is the first step toward implementing true end-to-end testing with >Cypress. > >Until now, we have been mocking responses using cy.intercept(), but this >approach can lead to confusion. In some cases, we have even had to mock >global JavaScript variables, which makes the code unnecessarily complex >and unconventional (e.g. win.categories_map). > >All the bug reports listed under this tree have helped lay the >groundwork for this patch. With it, we are able to build mock objects >(via plugin mockData) and insert them directly into the database >(insertData). Once the tests are complete, weâll restore the database to >its previous state by removing any data generated during the tests. > >The tests in KohaTable/Holdings_spec.ts have been updated to leverage >this new setup. > >Additionally, a caching mechanism has been added to prevent _id >attributes from being generated with the same values (a rare issue we >occasionally encountered). > >While this is still not a perfect solution, it introduces a solid >foundation for future improvements. > >Test plan: >All Cypress tests must pass >--- > .../integration/KohaTable/Holdings_spec.ts | 601 ++++++------------ > t/cypress/integration/t/insertData.ts | 76 +++ > t/cypress/plugins/index.js | 14 + > t/cypress/plugins/insertData.js | 304 +++++++++ > t/cypress/plugins/mockData.js | 65 +- > 5 files changed, 640 insertions(+), 420 deletions(-) > create mode 100644 t/cypress/integration/t/insertData.ts > create mode 100644 t/cypress/plugins/insertData.js > >diff --git a/t/cypress/integration/KohaTable/Holdings_spec.ts b/t/cypress/integration/KohaTable/Holdings_spec.ts >index 380ac9d2d8d..32fd124f74e 100644 >--- a/t/cypress/integration/KohaTable/Holdings_spec.ts >+++ b/t/cypress/integration/KohaTable/Holdings_spec.ts >@@ -1,5 +1,5 @@ > const RESTdefaultPageSize = "20"; // FIXME Mock this >-const baseTotalCount = "42"; >+const baseTotalCount = "21"; > > describe("catalogue/detail/holdings_table with items", () => { > const table_id = "holdings_table"; >@@ -10,108 +10,21 @@ describe("catalogue/detail/holdings_table with items", () => { > win.localStorage.clear(); > }); > >- // FIXME All the following code should not be reused as it >- // It must be moved to a Cypress command or task "buildSampleBiblio" or even "insertSampleBiblio" >- let generated_objects = {}; >- const objects = [{ object: "library" }, { object: "item_type" }]; >- cy.wrap(Promise.resolve()) >- .then(() => { >- return objects.reduce((chain, { object }) => { >- return chain.then(() => { >- return cy >- .task("buildSampleObject", { object }) >- .then(attributes => { >- generated_objects[object] = attributes; >- }); >- }); >- }, Promise.resolve()); >- }) >- .then(() => { >- const library = generated_objects["library"]; >- const item_type = generated_objects["item_type"]; >- const queries = [ >- { >- query: "INSERT INTO branches(branchcode, branchname) VALUES (?, ?)", >- values: [library.library_id, library.name], >- }, >- { >- query: "INSERT INTO itemtypes(itemtype, description) VALUES (?, ?)", >- values: [item_type.item_type_id, item_type.description], >- }, >- ]; >- cy.wrap(Promise.resolve()) >- .then(() => { >- return queries.reduce((chain, { query, values }) => { >- return chain.then(() => >- cy.task("query", { sql: query, values }) >- ); >- }, Promise.resolve()); >- }) >- .then(() => { >- let biblio = { >- leader: " nam a22 7a 4500", >- fields: [ >- { "005": "20250120101920.0" }, >- { >- "245": { >- ind1: "", >- ind2: "", >- subfields: [{ a: "Some boring read" }], >- }, >- }, >- { >- "100": { >- ind1: "", >- ind2: "", >- subfields: [ >- { c: "Some boring author" }, >- ], >- }, >- }, >- { >- "942": { >- ind1: "", >- ind2: "", >- subfields: [ >- { c: item_type.item_type_id }, >- ], >- }, >- }, >- ], >- }; >- cy.request({ >- method: "POST", >- url: "/api/v1/biblios", >- headers: { >- "Content-Type": "application/marc-in-json", >- "x-confirm-not-duplicate": 1, >- }, >- body: biblio, >- }).then(response => { >- const biblio_id = response.body.id; >- cy.wrap(biblio_id).as("biblio_id"); >- cy.request({ >- method: "POST", >- url: `/api/v1/biblios/${biblio_id}/items`, >- headers: { >- "Content-Type": "application/json", >- }, >- body: { >- home_library_id: library.library_id, >- holding_library_id: library.library_id, >- }, >- }); >- }); >- }); >- }); > cy.task("query", { > sql: "SELECT value FROM systempreferences WHERE variable='AlwaysShowHoldingsTableFilters'", > }).then(value => { > cy.wrap(value).as("syspref_AlwaysShowHoldingsTableFilters"); > }); >+ >+ cy.task("insertSampleBiblio", { item_count: baseTotalCount }).then( >+ objects => { >+ cy.wrap(objects).as("objects"); >+ } >+ ); > }); > > afterEach(function () { >+ cy.task("deleteSampleObjects", this.objects); > cy.set_syspref( > "AlwaysShowHoldingsTableFilters", > this.syspref_AlwaysShowHoldingsTableFilters >@@ -119,33 +32,9 @@ describe("catalogue/detail/holdings_table with items", () => { > }); > > it("Correctly init the table", function () { >- // Do not use `() => {` or this.biblio_id won't be retrieved >- const biblio_id = this.biblio_id; >- cy.task("buildSampleObjects", { >- object: "item", >- count: RESTdefaultPageSize, >- values: { >- biblio_id, >- checkout: null, >- transfer: null, >- lost_status: 0, >- withdrawn: 0, >- damaged_status: 0, >- not_for_loan_status: 0, >- course_item: null, >- cover_image_ids: [], >- _status: ["available"], >- }, >- }).then(items => { >- cy.intercept("get", `/api/v1/biblios/${biblio_id}/items*`, { >- statuscode: 200, >- body: items, >- headers: { >- "X-Base-Total-Count": baseTotalCount, >- "X-Total-Count": baseTotalCount, >- }, >- }); >- >+ // Do not use `() => {` or this.objets won't be retrieved >+ const biblio_id = this.objects.biblio.biblio_id; >+ cy.set_syspref("AlwaysShowHoldingsTableFilters", 1).then(() => { > cy.visit( > "/cgi-bin/koha/catalogue/detail.pl?biblionumber=" + biblio_id > ); >@@ -162,248 +51,209 @@ describe("catalogue/detail/holdings_table with items", () => { > }); > > it("Show filters", function () { >- // Do not use `() => {` or this.biblio_id won't be retrieved >- const biblio_id = this.biblio_id; >- cy.task("buildSampleObjects", { >- object: "item", >- count: RESTdefaultPageSize, >- values: { >- biblio_id, >- checkout: null, >- transfer: null, >- lost_status: 0, >- withdrawn: 0, >- damaged_status: 0, >- not_for_loan_status: 0, >- course_item: null, >- cover_image_ids: [], >- _status: ["available"], >- }, >- }).then(items => { >- cy.intercept("get", `/api/v1/biblios/${biblio_id}/items*`, { >- statuscode: 200, >- body: items, >- headers: { >- "X-Base-Total-Count": baseTotalCount, >- "X-Total-Count": baseTotalCount, >- }, >- }); >+ // Do not use `() => {` or this.objects won't be retrieved >+ const biblio_id = this.objects.biblio.biblio_id; > >- cy.set_syspref("AlwaysShowHoldingsTableFilters", 0).then(() => { >- cy.visit( >- "/cgi-bin/koha/catalogue/detail.pl?biblionumber=" + >- biblio_id >- ); >+ cy.set_syspref("AlwaysShowHoldingsTableFilters", 0).then(() => { >+ cy.visit( >+ "/cgi-bin/koha/catalogue/detail.pl?biblionumber=" + biblio_id >+ ); > >- // Hide the 'URL' column >- cy.mock_table_settings( >- { >- columns: { uri: { is_hidden: 1 } }, >- }, >- "items_table_settings.holdings" >- ); >+ // Hide the 'URL' column >+ cy.mock_table_settings( >+ { >+ columns: { uri: { is_hidden: 1 } }, >+ }, >+ "items_table_settings.holdings" >+ ); > >- cy.get("@columns").then(columns => { >- cy.get(`#${table_id}_wrapper tbody tr`).should( >- "have.length", >- RESTdefaultPageSize >- ); >+ cy.get("@columns").then(columns => { >+ cy.get(`#${table_id}_wrapper tbody tr`).should( >+ "have.length", >+ RESTdefaultPageSize >+ ); > >- // Filters are not displayed >- cy.get(`#${table_id} thead tr`).should("have.length", 1); >+ // Filters are not displayed >+ cy.get(`#${table_id} thead tr`).should("have.length", 1); > >- cy.get(`#${table_id} th`).contains("Status"); >- cy.get(`#${table_id} th`) >- .contains("URL") >- .should("not.exist"); >- cy.get(`#${table_id} th`) >- .contains("Course reserves") >- .should("not.exist"); >+ cy.get(`#${table_id} th`).contains("Status"); >+ cy.get(`#${table_id} th`).contains("URL").should("not.exist"); >+ cy.get(`#${table_id} th`) >+ .contains("Course reserves") >+ .should("not.exist"); > >- cy.get(`.${table_id}_table_controls .show_filters`).click(); >- cy.get(`#${table_id}_wrapper .dt-info`).contains( >- `Showing 1 to ${RESTdefaultPageSize} of ${baseTotalCount} entries` >- ); >- // Filters are displayed >- cy.get(`#${table_id} thead tr`).should("have.length", 2); >- >- cy.get(`#${table_id} th`).contains("Status"); >- cy.get(`#${table_id} th`) >- .contains("URL") >- .should("not.exist"); >- cy.get(`#${table_id} th`) >- .contains("Course reserves") >- .should("not.exist"); >- }); >+ cy.get(`.${table_id}_table_controls .show_filters`).click(); >+ cy.get(`#${table_id}_wrapper .dt-info`).contains( >+ `Showing 1 to ${RESTdefaultPageSize} of ${baseTotalCount} entries` >+ ); >+ // Filters are displayed >+ cy.get(`#${table_id} thead tr`).should("have.length", 2); >+ >+ cy.get(`#${table_id} th`).contains("Status"); >+ cy.get(`#${table_id} th`).contains("URL").should("not.exist"); >+ cy.get(`#${table_id} th`) >+ .contains("Course reserves") >+ .should("not.exist"); > }); >+ }); > >- cy.set_syspref("AlwaysShowHoldingsTableFilters", 1).then(() => { >- cy.visit( >- "/cgi-bin/koha/catalogue/detail.pl?biblionumber=" + >- biblio_id >- ); >+ cy.set_syspref("AlwaysShowHoldingsTableFilters", 1).then(() => { >+ cy.visit( >+ "/cgi-bin/koha/catalogue/detail.pl?biblionumber=" + biblio_id >+ ); > >- // Hide the 'URL' column >- cy.mock_table_settings( >- { >- columns: { uri: { is_hidden: 1 } }, >- }, >- "items_table_settings.holdings" >- ); >+ // Hide the 'URL' column >+ cy.mock_table_settings( >+ { >+ columns: { uri: { is_hidden: 1 } }, >+ }, >+ "items_table_settings.holdings" >+ ); > >- cy.get("@columns").then(columns => { >- cy.get(`#${table_id}_wrapper tbody tr`).should( >- "have.length", >- RESTdefaultPageSize >- ); >+ cy.get("@columns").then(columns => { >+ cy.get(`#${table_id}_wrapper tbody tr`).should( >+ "have.length", >+ RESTdefaultPageSize >+ ); > >- // Filters are displayed >- cy.get(`#${table_id} thead tr`).should("have.length", 2); >+ // Filters are displayed >+ cy.get(`#${table_id} thead tr`).should("have.length", 2); > >- cy.get(`.${table_id}_table_controls .hide_filters`).click(); >+ cy.get(`.${table_id}_table_controls .hide_filters`).click(); > >- // Filters are not displayed >- cy.get(`#${table_id} thead tr`).should("have.length", 1); >- }); >+ // Filters are not displayed >+ cy.get(`#${table_id} thead tr`).should("have.length", 1); > }); > }); > }); > > it("Filters by code and description", function () { >- // Do not use `() => {` or this.biblio_id won't be retrieved >- const biblio_id = this.biblio_id; >- cy.task("buildSampleObjects", { >- object: "item", >- count: RESTdefaultPageSize, >- values: { >- biblio_id, >- checkout: null, >- transfer: null, >- lost_status: 0, >- withdrawn: 0, >- damaged_status: 0, >- not_for_loan_status: 0, >- course_item: null, >- cover_image_ids: [], >- _status: ["available"], >- }, >- }).then(items => { >- cy.intercept("get", `/api/v1/biblios/${biblio_id}/items*`, { >- statuscode: 200, >- body: items, >- headers: { >- "X-Base-Total-Count": baseTotalCount, >- "X-Total-Count": baseTotalCount, >- }, >- }).as("searchItems"); >+ // Do not use `() => {` or this.objects won't be retrieved >+ const biblio_id = this.objects.biblio.biblio_id; > >- cy.visit( >- "/cgi-bin/koha/catalogue/detail.pl?biblionumber=" + biblio_id >- ); >+ cy.intercept("get", `/api/v1/biblios/${biblio_id}/items*`).as( >+ "searchItems" >+ ); > >- cy.window().then(win => { >- win.coded_values.library = new Map( >- items.map(i => [ >- i.home_library.name, >- i.home_library.library_id, >- ]) >- ); >- win.coded_values.item_type = new Map( >- items.map(i => [ >- i.item_type.description, >- i.item_type.item_type_id, >- ]) >- ); >- }); >- cy.wait("@searchItems"); >- >- let library_id = items[0].home_library.library_id; >- let library_name = items[0].home_library.name; >- cy.get(`#${table_id}_wrapper input.dt-input`).type(library_id); >- >- cy.wait("@searchItems").then(interception => { >- const q = interception.request.query.q; >- expect(q).to.match( >- new RegExp( >- `"me.home_library_id":{"like":"%${library_id}%"}` >- ) >- ); >- }); >+ cy.visit("/cgi-bin/koha/catalogue/detail.pl?biblionumber=" + biblio_id); >+ >+ cy.wait("@searchItems"); > >- cy.get(`#${table_id}_wrapper input.dt-input`).clear(); >- cy.wait("@searchItems"); >- cy.get(`#${table_id}_wrapper input.dt-input`).type(library_name); >+ cy.task("query", { >+ sql: "SELECT homebranch FROM items WHERE biblionumber=? LIMIT 1", >+ values: [biblio_id], >+ }).then(result => { >+ let library_id = result[0].homebranch; >+ cy.task("query", { >+ sql: "SELECT branchname FROM branches WHERE branchcode=?", >+ values: [library_id], >+ }).then(result => { >+ let library_name = result[0].branchname; >+ cy.get(`#${table_id}_wrapper input.dt-input`).type(library_id); >+ >+ cy.wait("@searchItems").then(interception => { >+ const q = interception.request.query.q; >+ expect(q).to.match( >+ new RegExp( >+ `"me.home_library_id":{"like":"%${library_id}%"}` >+ ) >+ ); >+ }); > >- cy.wait("@searchItems").then(interception => { >- const q = interception.request.query.q; >- expect(q).to.match( >- new RegExp(`"me.home_library_id":\\["${library_id}"\\]`) >+ cy.get(`#${table_id}_wrapper input.dt-input`).clear(); >+ cy.wait("@searchItems"); >+ cy.get(`#${table_id}_wrapper input.dt-input`).type( >+ library_name > ); >- }); > >- let item_type_id = items[0].item_type.item_type_id; >- let item_type_description = items[0].item_type.description; >- cy.get(`#${table_id}_wrapper input.dt-input`).clear(); >- cy.wait("@searchItems"); >- cy.get(`#${table_id}_wrapper input.dt-input`).type(item_type_id); >+ cy.wait("@searchItems").then(interception => { >+ const q = interception.request.query.q; >+ expect(q).to.match( >+ new RegExp(`"me.home_library_id":\\["${library_id}"\\]`) >+ ); >+ }); >+ }); >+ }); > >- cy.wait("@searchItems").then(interception => { >- const q = interception.request.query.q; >- expect(q).to.match( >- new RegExp(`"me.item_type_id":{"like":"%${item_type_id}%"}`) >+ cy.task("query", { >+ sql: "SELECT itype FROM items WHERE biblionumber=? LIMIT 1", >+ values: [biblio_id], >+ }).then(result => { >+ let item_type_id = result[0].itype; >+ cy.task("query", { >+ sql: "SELECT description FROM itemtypes WHERE itemtype=?", >+ values: [item_type_id], >+ }).then(result => { >+ let item_type_description = result[0].description; >+ >+ cy.get(`#${table_id}_wrapper input.dt-input`).clear(); >+ cy.wait("@searchItems"); >+ cy.get(`#${table_id}_wrapper input.dt-input`).type( >+ item_type_id > ); >- }); > >- cy.get(`#${table_id}_wrapper input.dt-input`).clear(); >- cy.wait("@searchItems"); >- cy.get(`#${table_id}_wrapper input.dt-input`).type( >- item_type_description >- ); >+ cy.wait("@searchItems").then(interception => { >+ const q = interception.request.query.q; >+ expect(q).to.match( >+ new RegExp( >+ `"me.item_type_id":{"like":"%${item_type_id}%"}` >+ ) >+ ); >+ }); > >- cy.wait("@searchItems").then(interception => { >- const q = interception.request.query.q; >- expect(q).to.match( >- new RegExp(`"me.item_type_id":\\["${item_type_id}"\\]`) >+ cy.get(`#${table_id}_wrapper input.dt-input`).clear(); >+ cy.wait("@searchItems"); >+ cy.get(`#${table_id}_wrapper input.dt-input`).type( >+ item_type_description > ); >- }); > >- cy.viewport(2999, 2999); >- cy.get(`#${table_id}_wrapper input.dt-input`).clear(); >- cy.wait("@searchItems"); >- // Show filters if not there already >- cy.get(`.${table_id}_table_controls .show_filters`) >- .then(link => { >- if (link.is(":visible")) { >- cy.wrap(link).click(); >- cy.wait("@searchItems"); >- } >- }) >- .then(() => { >- // Select first (non-empty) option >- cy.get( >- `#${table_id}_wrapper th#holdings_itype select` >- ).then(select => { >- const raw_value = select.find("option").eq(1).val(); >- expect(raw_value).to.match(/^\^/); >- expect(raw_value).to.match(/\$$/); >- item_type_id = raw_value.replace(/^\^|\$$/g, ""); // Remove ^ and $ >- }); >- cy.get( >- `#${table_id}_wrapper th#holdings_itype select option` >- ) >- .eq(1) >- .then(o => { >- cy.get( >- `#${table_id}_wrapper th#holdings_itype select` >- ).select(o.val(), { force: true }); >+ cy.wait("@searchItems").then(interception => { >+ const q = interception.request.query.q; >+ expect(q).to.match( >+ new RegExp(`"me.item_type_id":\\["${item_type_id}"\\]`) >+ ); >+ }); >+ >+ cy.viewport(2999, 2999); >+ cy.get(`#${table_id}_wrapper input.dt-input`).clear(); >+ cy.wait("@searchItems"); >+ // Show filters if not there already >+ cy.get(`.${table_id}_table_controls .show_filters`) >+ .then(link => { >+ if (link.is(":visible")) { >+ cy.wrap(link).click(); >+ cy.wait("@searchItems"); >+ } >+ }) >+ .then(() => { >+ // Select first (non-empty) option >+ cy.get( >+ `#${table_id}_wrapper th#holdings_itype select` >+ ).then(select => { >+ const raw_value = select.find("option").eq(1).val(); >+ expect(raw_value).to.match(/^\^/); >+ expect(raw_value).to.match(/\$$/); >+ item_type_id = raw_value.replace(/^\^|\$$/g, ""); // Remove ^ and $ >+ }); >+ cy.get( >+ `#${table_id}_wrapper th#holdings_itype select option` >+ ) >+ .eq(1) >+ .then(o => { >+ cy.get( >+ `#${table_id}_wrapper th#holdings_itype select` >+ ).select(o.val(), { force: true }); >+ }); >+ cy.wait("@searchItems").then(interception => { >+ const q = interception.request.query.q; >+ expect(q).to.match( >+ new RegExp( >+ `{"me.item_type_id":"${item_type_id}"}` >+ ) >+ ); > }); >- cy.wait("@searchItems").then(interception => { >- const q = interception.request.query.q; >- expect(q).to.match( >- new RegExp(`{"me.item_type_id":"${item_type_id}"}`) >- ); > }); >- }); >+ }); > }); > }); > }); >@@ -417,92 +267,19 @@ describe("catalogue/detail/holdings_table without items", () => { > win.localStorage.clear(); > }); > >- // FIXME All the following code should not be reused as it >- // It must be moved to a Cypress command or task "buildSampleBiblio" or even "insertSampleBiblio" >- let generated_objects = {}; >- const objects = [{ object: "library" }, { object: "item_type" }]; >- cy.wrap(Promise.resolve()) >- .then(() => { >- return objects.reduce((chain, { object }) => { >- return chain.then(() => { >- return cy >- .task("buildSampleObject", { object }) >- .then(attributes => { >- generated_objects[object] = attributes; >- }); >- }); >- }, Promise.resolve()); >- }) >- .then(() => { >- const item_type = generated_objects["item_type"]; >- const queries = [ >- { >- sql: "INSERT INTO itemtypes(itemtype, description) VALUES (?, ?)", >- values: [item_type.item_type_id, item_type.description], >- }, >- ]; >- cy.wrap(Promise.resolve()) >- .then(() => { >- return queries.reduce((chain, { sql, values }) => { >- return chain.then(() => >- cy.task("query", { sql, values }) >- ); >- }, Promise.resolve()); >- }) >- .then(() => { >- let biblio = { >- leader: " nam a22 7a 4500", >- fields: [ >- { "005": "20250120101920.0" }, >- { >- "245": { >- ind1: "", >- ind2: "", >- subfields: [{ a: "Some boring read" }], >- }, >- }, >- { >- "100": { >- ind1: "", >- ind2: "", >- subfields: [ >- { c: "Some boring author" }, >- ], >- }, >- }, >- { >- "942": { >- ind1: "", >- ind2: "", >- subfields: [ >- { c: item_type.item_type_id }, >- ], >- }, >- }, >- ], >- }; >- cy.request({ >- method: "POST", >- url: "/api/v1/biblios", >- headers: { >- "Content-Type": "application/marc-in-json", >- "x-confirm-not-duplicate": 1, >- }, >- body: biblio, >- }).then(response => { >- const biblio_id = response.body.id; >- cy.wrap(biblio_id).as("biblio_id"); >- }); >- }); >- }); > cy.task("query", { > sql: "SELECT value FROM systempreferences WHERE variable='AlwaysShowHoldingsTableFilters'", > }).then(value => { > cy.wrap(value).as("syspref_AlwaysShowHoldingsTableFilters"); > }); >+ >+ cy.task("insertSampleBiblio", { item_count: 0 }).then(objects => { >+ cy.wrap(objects).as("objects"); >+ }); > }); > > afterEach(function () { >+ cy.task("deleteSampleObjects", this.objects); > cy.set_syspref( > "AlwaysShowHoldingsTableFilters", > this.syspref_AlwaysShowHoldingsTableFilters >@@ -510,8 +287,8 @@ describe("catalogue/detail/holdings_table without items", () => { > }); > > it("Do not display the table", function () { >- // Do not use `() => {` or this.biblio_id won't be retrieved >- const biblio_id = this.biblio_id; >+ // Do not use `() => {` or this.objects won't be retrieved >+ const biblio_id = this.objects.biblio.biblio_id; > > cy.visit("/cgi-bin/koha/catalogue/detail.pl?biblionumber=" + biblio_id); > >diff --git a/t/cypress/integration/t/insertData.ts b/t/cypress/integration/t/insertData.ts >new file mode 100644 >index 00000000000..ff163c606a7 >--- /dev/null >+++ b/t/cypress/integration/t/insertData.ts >@@ -0,0 +1,76 @@ >+const { query } = require("./../../plugins/db.js"); >+ >+describe("insertSampleBiblio", () => { >+ it("should generate library and item type", () => { >+ cy.task("insertSampleBiblio", { item_count: 3 }).then(objects => { >+ const biblio_id = objects.biblio.biblio_id; >+ >+ expect(typeof biblio_id).to.be.equal("number"); >+ >+ cy.task("query", { >+ sql: "SELECT COUNT(*) as count FROM biblio WHERE biblionumber=?", >+ values: [biblio_id], >+ }).then(result => { >+ expect(result[0].count).to.be.equal(1); >+ }); >+ >+ cy.task("query", { >+ sql: "SELECT COUNT(*) as count FROM items WHERE biblionumber=?", >+ values: [biblio_id], >+ }).then(result => { >+ expect(result[0].count).to.be.equal(3); >+ }); >+ >+ cy.task("query", { >+ sql: "SELECT DISTINCT(itype) as count FROM items WHERE biblionumber=?", >+ values: [biblio_id], >+ }).then(result => { >+ expect(result.length).to.be.equal(1); >+ }); >+ >+ cy.task("query", { >+ sql: "SELECT COUNT(*) as count FROM branches WHERE branchcode=?", >+ values: [objects.library.library_id], >+ }).then(result => { >+ expect(result[0].count).to.be.equal(1); >+ }); >+ >+ cy.task("query", { >+ sql: "SELECT COUNT(*) as count FROM itemtypes WHERE itemtype=?", >+ values: [objects.item_type.item_type_id], >+ }).then(result => { >+ expect(result[0].count).to.be.equal(1); >+ }); >+ >+ cy.task("deleteSampleObjects", objects); >+ >+ cy.task("query", { >+ sql: "SELECT COUNT(*) as count FROM biblio WHERE biblionumber=?", >+ values: [biblio_id], >+ }).then(result => { >+ expect(result[0].count).to.be.equal(0); >+ }); >+ >+ cy.task("query", { >+ sql: "SELECT COUNT(*) as count FROM items WHERE biblionumber=?", >+ values: [biblio_id], >+ }).then(result => { >+ expect(result[0].count).to.be.equal(0); >+ }); >+ >+ cy.task("query", { >+ sql: "SELECT COUNT(*) as count FROM branches WHERE branchcode=?", >+ values: [objects.library.library_id], >+ }).then(result => { >+ expect(result[0].count).to.be.equal(0); >+ }); >+ >+ cy.task("query", { >+ sql: "SELECT COUNT(*) as count FROM itemtypes WHERE itemtype=?", >+ values: [objects.item_type.item_type_id], >+ }).then(result => { >+ expect(result[0].count).to.be.equal(0); >+ }); >+ }); >+ }); >+}); >diff --git a/t/cypress/plugins/index.js b/t/cypress/plugins/index.js >index 285351d906b..1045e9b6919 100644 >--- a/t/cypress/plugins/index.js >+++ b/t/cypress/plugins/index.js >@@ -2,11 +2,18 @@ const { startDevServer } = require("@cypress/webpack-dev-server"); > > const { buildSampleObject, buildSampleObjects } = require("./mockData.js"); > >+const { >+ insertSampleBiblio, >+ insertObject, >+ deleteSampleObjects, >+} = require("./insertData.js"); >+ > const { query } = require("./db.js"); > > const { apiGet, apiPost, apiPut, apiDelete } = require("./api-client.js"); > > module.exports = (on, config) => { >+ const baseUrl = config.baseUrl; > on("dev-server:start", options => > startDevServer({ > options, >@@ -16,6 +23,13 @@ module.exports = (on, config) => { > on("task", { > buildSampleObject, > buildSampleObjects, >+ insertSampleBiblio({ item_count }) { >+ return insertSampleBiblio(item_count, baseUrl); >+ }, >+ insertObject({ type, object }) { >+ return insertObject(type, object, baseUrl, authHeader); >+ }, >+ deleteSampleObjects, > query, > > apiGet(args) { >diff --git a/t/cypress/plugins/insertData.js b/t/cypress/plugins/insertData.js >new file mode 100644 >index 00000000000..a9d63de45f4 >--- /dev/null >+++ b/t/cypress/plugins/insertData.js >@@ -0,0 +1,304 @@ >+const { buildSampleObject, buildSampleObjects } = require("./mockData.js"); >+const { query } = require("./db.js"); >+ >+const { APIClient } = require("./dist/api-client.cjs.js"); >+const { Buffer } = require("buffer"); >+ >+const insertSampleBiblio = async (item_count, baseUrl) => { >+ let client = APIClient.default; >+ 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"], >+ baseUrl, >+ authHeader >+ ); >+ const item_type = await insertObject( >+ "item_type", >+ generated_objects["item_type"], >+ baseUrl, >+ authHeader >+ ); >+ >+ let biblio = { >+ leader: " nam a22 7a 4500", >+ fields: [ >+ { "005": "20250120101920.0" }, >+ { >+ 245: { >+ ind1: "", >+ ind2: "", >+ subfields: [{ a: "Some boring read" }], >+ }, >+ }, >+ { >+ 100: { >+ ind1: "", >+ ind2: "", >+ subfields: [{ c: "Some boring author" }], >+ }, >+ }, >+ { >+ 942: { >+ ind1: "", >+ ind2: "", >+ subfields: [{ c: item_type.item_type_id }], >+ }, >+ }, >+ ], >+ }; >+ const credentials = Buffer.from("koha:koha").toString("base64"); >+ let result = await client.koha.post({ >+ endpoint: `${baseUrl}/api/v1/biblios`, >+ headers: { >+ "Content-Type": "application/marc-in-json", >+ "x-confirm-not-duplicate": 1, >+ Authorization: "Basic " + credentials, >+ }, >+ body: biblio, >+ }); >+ const biblio_id = result.id; >+ // We do not have a route to get a biblio as it is stored in DB >+ // We might need to refine that in the future >+ biblio = { >+ biblio_id, >+ }; >+ >+ let items = buildSampleObjects({ >+ object: "item", >+ count: item_count, >+ values: { >+ biblio_id, >+ lost_status: 0, >+ withdrawn: 0, >+ damaged_status: 0, >+ not_for_loan_status: 0, >+ restricted_status: 0, >+ new_status: null, >+ issues: 0, >+ item_type_id: item_type.item_type_id, >+ home_library_id: library.library_id, >+ holding_library_id: library.library_id, >+ }, >+ }); >+ items = items.map( >+ ({ >+ item_id, >+ checkout, >+ transfer, >+ lost_date, >+ withdrawn_date, >+ damaged_date, >+ course_item, >+ _strings, >+ biblio, >+ bundle_host, >+ item_group_item, >+ recall, >+ return_claim, >+ return_claims, >+ serial_item, >+ first_hold, >+ checkouts_count, >+ renewals_count, >+ holds_count, >+ bundle_items_lost_count, >+ analytics_count, >+ effective_not_for_loan_status, >+ effective_item_type_id, >+ home_library, >+ holding_library, >+ bundle_items_not_lost_count, >+ item_type, >+ _status, >+ effective_bookable, >+ in_bundle, >+ cover_image_ids, >+ localuse, >+ ...rest >+ }) => rest >+ ); >+ let createdItems = []; >+ for (const item of items) { >+ await client.koha >+ .post({ >+ endpoint: `${baseUrl}/api/v1/biblios/${biblio_id}/items`, >+ body: item, >+ headers: { >+ "Content-Type": "application/json", >+ Authorization: "Basic " + credentials, >+ }, >+ }) >+ .then(i => createdItems.push(i)); >+ } >+ return { biblio, items: createdItems, library, item_type }; >+}; >+ >+const deleteSampleObjects = async objects => { >+ const deletionOrder = ["items", "biblio", "library", "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; >+ } >+ } >+ return true; >+}; >+ >+const insertLibrary = async (library, baseUrl, authHeader) => { >+ const { >+ pickup_items, >+ smtp_server, >+ cash_registers, >+ desks, >+ library_hours, >+ needs_override, >+ ...new_library >+ } = library; >+ let client = APIClient.default; >+ return client.koha.post({ >+ endpoint: `${baseUrl}/api/v1/libraries`, >+ body: new_library, >+ headers: { >+ "Content-Type": "application/json", >+ Authorization: authHeader, >+ }, >+ }); >+}; >+ >+const insertObject = async (type, object, baseUrl, authHeader) => { >+ let client = APIClient.default; >+ if (type == "patron") { >+ await query({ >+ sql: "SELECT COUNT(*) AS count FROM branches WHERE branchcode = ?", >+ values: [object.library_id], >+ }).then(result => { >+ if (!result[0].count) { >+ insertLibrary(object.library, baseUrl, authHeader); >+ } >+ }); >+ await query({ >+ sql: "SELECT COUNT(*) AS count FROM categories WHERE categorycode= ?", >+ values: [object.category_id], >+ }).then(result => { >+ if (!result[0].count) { >+ query({ >+ sql: "INSERT INTO categories(categorycode, description) VALUES (?, ?)", >+ values: [ >+ object.category_id, >+ `description for ${object.category_id}`, >+ ], >+ }); >+ } >+ }); >+ const { >+ _strings, >+ anonymized, >+ restricted, >+ expired, >+ extended_attributes, >+ library, >+ checkouts_count, >+ overdues_count, >+ account_balance, >+ lang, >+ login_attempts, >+ sms_provider_id, >+ ...patron >+ } = object; >+ >+ return client.koha.post({ >+ endpoint: `${baseUrl}/api/v1/patrons`, >+ body: patron, >+ headers: { >+ "Content-Type": "application/json", >+ Authorization: authHeader, >+ }, >+ }); >+ } else if (type == "library") { >+ const keysToKeep = ["library_id", "name"]; >+ const library = Object.fromEntries( >+ Object.entries(object).filter(([key]) => keysToKeep.includes(key)) >+ ); >+ return client.koha.post({ >+ endpoint: `${baseUrl}/api/v1/libraries`, >+ headers: { >+ "Content-Type": "application/json", >+ Authorization: authHeader, >+ }, >+ body: library, >+ }); >+ } else if (type == "item_type") { >+ const keysToKeep = ["item_type_id", "description"]; >+ const item_type = Object.fromEntries( >+ Object.entries(object).filter(([key]) => keysToKeep.includes(key)) >+ ); >+ return query({ >+ sql: "INSERT INTO itemtypes(itemtype, description) VALUES (?, ?)", >+ values: [item_type.item_type_id, item_type.description], >+ }) >+ .then(result => { >+ // FIXME We need /item_types/:item_type_id >+ return client.koha.get({ >+ endpoint: `${baseUrl}/api/v1/item_types?q={"item_type_id":"${item_type.item_type_id}"}`, >+ headers: { >+ "Content-Type": "application/json", >+ Authorization: authHeader, >+ }, >+ }); >+ }) >+ .then(item_types => item_types[0]); >+ } else { >+ return false; >+ } >+ >+ return true; >+}; >+ >+module.exports = { >+ insertSampleBiblio, >+ insertObject, >+ deleteSampleObjects, >+}; >diff --git a/t/cypress/plugins/mockData.js b/t/cypress/plugins/mockData.js >index c2025abeebf..5791c68270e 100644 >--- a/t/cypress/plugins/mockData.js >+++ b/t/cypress/plugins/mockData.js >@@ -1,19 +1,22 @@ > const { faker } = require("@faker-js/faker"); > const { readYamlFile } = require("./../plugins/readYamlFile.js"); >+const { query } = require("./db.js"); > const fs = require("fs"); > >+const generatedDataCache = new Set(); >+ > const generateMockData = (type, properties) => { > switch (type) { > case "string": > if (properties?.maxLength) { >- return faker.string.alpha({ >+ return (value = faker.string.alpha({ > length: { > min: properties.minLength || 1, > max: properties.maxLength, > }, >- }); >+ })); > } >- return faker.lorem.words(3); >+ return (value = faker.lorem.words(3)); > case "integer": > return faker.number.int(); > case "boolean": >@@ -56,18 +59,58 @@ const generateDataFromSchema = (properties, values = {}) => { > data = buildSampleObject({ object: "library" }); > fk_name = "library_id"; > break; >+ case "pickup_library": >+ data = buildSampleObject({ object: "library" }); >+ fk_name = "pickup_library_id"; >+ break; >+ case "library": >+ data = buildSampleObject({ object: "library" }); >+ fk_name = "library_id"; >+ break; > case "item_type": > data = buildSampleObject({ object: "item_type" }); > fk_name = "item_type_id"; > break; >+ case "item": >+ data = buildSampleObject({ object: "item" }); >+ fk_name = "item_id"; >+ break; > default: >- data = generateMockData(type, value); >+ try { >+ data = generateMockData(type, value); >+ } catch (e) { >+ throw new Error( >+ `Failed to generate data for (${key}): ${e}` >+ ); >+ } > } > if (typeof data === "object") { > ids[key] = data[fk_name]; > } > } else { >- data = generateMockData(type, value); >+ try { >+ if (key.match(/_id$/)) { >+ let attempts = 0; >+ >+ do { >+ data = generateMockData(type, value); >+ attempts++; >+ if (attempts > 10) { >+ throw new Error( >+ "Could not generate unique string after 10 attempts" >+ ); >+ } >+ } while (generatedDataCache.has(data)); >+ >+ generatedDataCache.add(data); >+ } else { >+ data = generateMockData(type, value); >+ } >+ } catch (e) { >+ throw new Error( >+ `Failed to generate data for ${key} (${type}): ${e}` >+ ); >+ } > } > mockData[key] = data; > } >@@ -93,9 +136,15 @@ const buildSampleObjects = ({ object, values, count = 1 }) => { > ); > } > const schema = readYamlFile(yamlPath); >- return Array.from({ length: count }, () => >- generateDataFromSchema(schema.properties, values) >- ); >+ let generatedObject; >+ try { >+ generatedObject = Array.from({ length: count }, () => >+ generateDataFromSchema(schema.properties, values) >+ ); >+ } catch (e) { >+ throw new Error(`Failed to generate data for object '${object}': ${e}`); >+ } >+ return generatedObject; > }; > > const buildSampleObject = ({ object, values = {} }) => { >-- >2.34.1
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Diff
|
Splinter Review
Attachments on
bug 40174
: 183372 |
183373
|
183374