Bug 40170: Replace cypress-mysql with mysql2 - Cypress tests

This patch suggests to replace cypress-mysql with mysql2

cypress-mysql provides a custom Cypress command (cy.query) that run within the browser context.
This approach mixes test-side and server-side and is limited in flexibility and error handling.

mysql2 runs inside Cypress plugins (Node process) via cy.task calls, which are executed server-side (outside the browser)

This is needed for the follow-up bug reports where we need to request
the DB from other plugins

Test plan:
Run `yarn install` to install the mysql2
All the Cypress tests modified by this patch must still pass
The new test t/cypress/integration/t/db.ts must also pass

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
(cherry picked from commit 79060544a4)
Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
This commit is contained in:
Jonathan Druart 2025-06-12 15:42:17 +02:00 committed by Paul Derscheid
parent 61d0294e62
commit 226f50d801
No known key found for this signature in database
GPG Key ID: C25F9FD44CED595B
10 changed files with 107 additions and 81 deletions

View File

@ -15,13 +15,5 @@ export default defineConfig({
baseUrl: "http://localhost:8081",
specPattern: "t/cypress/integration/**/*.*",
supportFile: "t/cypress/support/e2e.js",
env: {
db: {
host: "db",
user: "koha_kohadev",
password: "password",
database: "koha_kohadev",
},
},
},
});

View File

@ -19,7 +19,6 @@
"bootstrap": "^5.3.3",
"css-loader": "^6.6.0",
"cypress": "^12.17.2",
"cypress-mysql": "^1.0.0",
"datatables.net-buttons": "^2.3.4",
"datatables.net-vue3": "^2.0.0",
"gulp": "^4.0.2",
@ -82,6 +81,7 @@
"globals": "^16.0.0",
"gulp-tap": "^1.0.1",
"html-webpack-plugin": "^5.5.0",
"mysql2": "^3.14.1",
"node-sass-tilde-importer": "^1.0.2",
"postcss": "^8.4.14",
"postcss-selector-parser": "^6.0.10",

View File

@ -5,7 +5,7 @@ const branchname = "test_branchname";
function cleanup() {
const sql = "DELETE FROM branches WHERE branchcode=?";
cy.query(sql, branchcode);
cy.task("query", { sql, values: [branchcode] });
}
describe("CSRF", () => {
@ -32,10 +32,10 @@ describe("CSRF", () => {
.find(".alert")
.contains(/No CSRF token passed for POST/);
cy.query(
"SELECT COUNT(*) as count FROM branches WHERE branchcode=?",
branchcode
).then(result => {
cy.task("query", {
sql: "SELECT COUNT(*) as count FROM branches WHERE branchcode=?",
values: [branchcode],
}).then(result => {
expect(result[0].count).to.equal(0);
});
});
@ -53,10 +53,10 @@ describe("CSRF", () => {
.find(".alert")
.contains(/Wrong CSRF token/);
cy.query(
"SELECT COUNT(*) as count FROM branches WHERE branchcode=?",
branchcode
).then(result => {
cy.task("query", {
sql: "SELECT COUNT(*) as count FROM branches WHERE branchcode=?",
values: [branchcode],
}).then(result => {
expect(result[0].count).to.equal(0);
});
});
@ -89,10 +89,10 @@ describe("CSRF", () => {
// We do not want Wrong CSRF token here
cy.get(".message").should("not.exist");
cy.query(
"SELECT COUNT(*) as count FROM branches WHERE branchcode=?",
branchcode
).then(result => {
cy.task("query", {
sql: "SELECT COUNT(*) as count FROM branches WHERE branchcode=?",
values: [branchcode],
}).then(result => {
expect(result[0].count).to.equal(0);
});
});
@ -112,19 +112,19 @@ describe("CSRF", () => {
cy.get("select[name='libraries_length']").select("-1");
cy.get("td").contains(branchcode);
cy.query(
"SELECT COUNT(*) as count FROM branches WHERE branchcode=?",
branchcode
).then(result => {
cy.task("query", {
sql: "SELECT COUNT(*) as count FROM branches WHERE branchcode=?",
values: [branchcode],
}).then(result => {
expect(result[0].count).to.equal(1);
});
});
it("Delete without CSRF", () => {
cy.query("INSERT INTO branches(branchcode, branchname) VALUES (?, ?)", [
branchcode,
branchname,
]);
cy.task("query", {
sql: "INSERT INTO branches(branchcode, branchname) VALUES (?, ?)",
values: [branchcode, branchname],
});
cy.visit("/cgi-bin/koha/admin/branches.pl");
cy.get("select[name='libraries_length']").select("-1");
@ -141,19 +141,19 @@ describe("CSRF", () => {
.find(".alert")
.contains(/No CSRF token passed for POST/);
cy.query(
"SELECT COUNT(*) as count FROM branches WHERE branchcode=?",
branchcode
).then(result => {
cy.task("query", {
sql: "SELECT COUNT(*) as count FROM branches WHERE branchcode=?",
values: [branchcode],
}).then(result => {
expect(result[0].count).to.equal(1);
});
});
it("Delete", () => {
cy.query("INSERT INTO branches(branchcode, branchname) VALUES (?, ?)", [
branchcode,
branchname,
]);
cy.task("query", {
sql: "INSERT INTO branches(branchcode, branchname) VALUES (?, ?)",
values: [branchcode, branchname],
});
cy.visit("/cgi-bin/koha/admin/branches.pl");
cy.get("select[name='libraries_length']").select("-1");
@ -165,10 +165,10 @@ describe("CSRF", () => {
.find(".alert")
.contains(/Library deleted successfully/);
cy.query(
"SELECT COUNT(*) as count FROM branches WHERE branchcode=?",
branchcode
).then(result => {
cy.task("query", {
sql: "SELECT COUNT(*) as count FROM branches WHERE branchcode=?",
values: [branchcode],
}).then(result => {
expect(result[0].count).to.equal(0);
});
});

View File

@ -23,12 +23,14 @@ describe("Acquisitions menu", () => {
it("Should show/hide links based on permissions", () => {
cy.get(".sidebar_menu").should("be.visible");
cy.query(
"UPDATE borrowers SET flags=2052 WHERE borrowernumber=51"
).then(() => {
cy.task("query", {
sql: "UPDATE borrowers SET flags=2052 WHERE borrowernumber=51",
}).then(() => {
cy.reload(true);
cy.get(".sidebar_menu a").should("have.length", 8);
cy.query("UPDATE borrowers SET flags=1 WHERE borrowernumber=51");
cy.task("query", {
sql: "UPDATE borrowers SET flags=1 WHERE borrowernumber=51",
});
});
});
it("Should correctly apply the 'current' class", () => {

View File

@ -42,7 +42,9 @@ describe("catalogue/detail/holdings_table with items", () => {
cy.wrap(Promise.resolve())
.then(() => {
return queries.reduce((chain, { query, values }) => {
return chain.then(() => cy.query(query, values));
return chain.then(() =>
cy.task("query", { sql: query, values })
);
}, Promise.resolve());
})
.then(() => {
@ -102,9 +104,9 @@ describe("catalogue/detail/holdings_table with items", () => {
});
});
});
cy.query(
"SELECT value FROM systempreferences WHERE variable='AlwaysShowHoldingsTableFilters'"
).then(value => {
cy.task("query", {
sql: "SELECT value FROM systempreferences WHERE variable='AlwaysShowHoldingsTableFilters'",
}).then(value => {
cy.wrap(value).as("syspref_AlwaysShowHoldingsTableFilters");
});
});
@ -435,14 +437,16 @@ describe("catalogue/detail/holdings_table without items", () => {
const item_type = generated_objects["item_type"];
const queries = [
{
query: "INSERT INTO itemtypes(itemtype, description) VALUES (?, ?)",
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, { query, values }) => {
return chain.then(() => cy.query(query, values));
return queries.reduce((chain, { sql, values }) => {
return chain.then(() =>
cy.task("query", { sql, values })
);
}, Promise.resolve());
})
.then(() => {
@ -491,9 +495,9 @@ describe("catalogue/detail/holdings_table without items", () => {
});
});
});
cy.query(
"SELECT value FROM systempreferences WHERE variable='AlwaysShowHoldingsTableFilters'"
).then(value => {
cy.task("query", {
sql: "SELECT value FROM systempreferences WHERE variable='AlwaysShowHoldingsTableFilters'",
}).then(value => {
cy.wrap(value).as("syspref_AlwaysShowHoldingsTableFilters");
});
});

View File

@ -9,7 +9,7 @@ const patron_attr_type = "attribute_type4TEST";
function cleanup() {
const sql = "DELETE FROM borrower_attribute_types WHERE code=?";
cy.query(sql, patron_attr_type);
cy.task("query", { sql, values: [patron_attr_type] });
}
describe("ExtendedPatronAttributes", () => {
beforeEach(() => {
@ -19,9 +19,9 @@ describe("ExtendedPatronAttributes", () => {
cy.window().then(win => {
win.localStorage.clear();
});
cy.query(
"SELECT value FROM systempreferences WHERE variable='ExtendedPatronAttributes'"
).then(value => {
cy.task("query", {
sql: "SELECT value FROM systempreferences WHERE variable='ExtendedPatronAttributes'",
}).then(value => {
cy.wrap(value).as("syspref_ExtendedPatronAttributes");
});
});
@ -45,9 +45,9 @@ describe("ExtendedPatronAttributes", () => {
cy.get("#search_patron_filter").type("something");
cy.get("form.patron_search_form input[type='submit']").click();
cy.query(
"select count(*) as nb_searchable from borrower_attribute_types where staff_searchable=1"
).then(result => {
cy.task("query", {
sql: "select count(*) as nb_searchable from borrower_attribute_types where staff_searchable=1",
}).then(result => {
const has_searchable = result[0].nb_searchable;
cy.wait("@searchPatrons").then(interception => {
const q = interception.request.query.q;
@ -55,10 +55,10 @@ describe("ExtendedPatronAttributes", () => {
});
});
cy.query(
"INSERT INTO borrower_attribute_types(code, description, staff_searchable, searched_by_default) VALUES (?, 'only for tests', 1, 1)",
patron_attr_type
).then(() => {
cy.task("query", {
sql: "INSERT INTO borrower_attribute_types(code, description, staff_searchable, searched_by_default) VALUES (?, 'only for tests', 1, 1)",
values: [patron_attr_type],
}).then(() => {
cy.visit("/cgi-bin/koha/members/members-home.pl");
cy.get("#search_patron_filter").type("something");
@ -80,9 +80,9 @@ describe("ExtendedPatronAttributes", () => {
cy.get("#search_patron_filter").type("something");
cy.get("form.patron_search_form input[type='submit']").click();
cy.query(
"select count(*) as nb_searchable from borrower_attribute_types where staff_searchable=1 AND searched_by_default=1"
).then(result => {
cy.task("query", {
sql: "select count(*) as nb_searchable from borrower_attribute_types where staff_searchable=1 AND searched_by_default=1",
}).then(result => {
const has_searchable = result[0].nb_searchable;
cy.wait("@searchPatrons").then(interception => {
const q = interception.request.query.q;
@ -94,10 +94,10 @@ describe("ExtendedPatronAttributes", () => {
});
});
cy.query(
"INSERT INTO borrower_attribute_types(code, description, staff_searchable, searched_by_default) VALUES (?, 'only for tests', 1, 1)",
patron_attr_type
).then(() => {
cy.task("query", {
sql: "INSERT INTO borrower_attribute_types(code, description, staff_searchable, searched_by_default) VALUES (?, 'only for tests', 1, 1)",
values: [patron_attr_type],
}).then(() => {
cy.visit("/cgi-bin/koha/members/members-home.pl");
cy.get("#search_patron_filter").type("something");

View File

@ -0,0 +1,15 @@
describe("DB tests", () => {
it("should be able to SELECT", () => {
cy.task("query", { sql: "SELECT count(*) FROM borrowers" }).then(
rows => {
expect(typeof rows.length).to.be.equal("number");
}
);
cy.task("query", {
sql: "SELECT count(*) FROM borrowers WHERE `surname` = ?",
values: ["john"],
}).then(rows => {
expect(typeof rows.length).to.be.equal("number");
});
});
});

17
t/cypress/plugins/db.js Normal file
View File

@ -0,0 +1,17 @@
const mysql = require("mysql2/promise");
const connectionConfig = {
host: "db",
user: "koha_kohadev",
password: "password",
database: "koha_kohadev",
};
async function query(sql, params = []) {
const connection = await mysql.createConnection(connectionConfig);
const [rows] = await connection.execute(sql, params);
await connection.end();
return rows;
}
module.exports = { query };

View File

@ -1,9 +1,9 @@
const { startDevServer } = require("@cypress/webpack-dev-server");
const mysql = require("cypress-mysql");
const { buildSampleObject, buildSampleObjects } = require("./mockData.js");
const { query } = require("./db.js");
module.exports = (on, config) => {
on("dev-server:start", options =>
startDevServer({
@ -11,11 +11,10 @@ module.exports = (on, config) => {
})
);
mysql.configurePlugin(on);
on("task", {
buildSampleObject,
buildSampleObjects,
query,
});
return config;
};

View File

@ -1874,9 +1874,6 @@ cy.getVendor = () => {
};
};
const mysql = require("cypress-mysql");
mysql.addCommands();
Cypress.Commands.add("set_syspref", (variable, value) => {
cy.window().then(win => {
const client = win.APIClient.sysprefs;