From b0c6ca0e2b5a66f7cc01394f201e7851bcdb0cf2 Mon Sep 17 00:00:00 2001 From: Jonathan Druart Date: Tue, 15 Mar 2022 12:16:45 +0100 Subject: [PATCH] Bug 32030: ERM - Add "integration" tests using Cypress We are mocking the REST API routes responses here, we could do better, but it's a nice first step. To run the tests: From the host (ie. *not* inside ktd): `yarn run cypress open` Signed-off-by: Jonathan Field Signed-off-by: Martin Renvoize Signed-off-by: Kyle M Hall Signed-off-by: Tomas Cohen Arazi --- cypress.json | 3 + cypress/integration/Agreements_spec.ts | 266 ++++++++++++++++++ cypress/plugins/index.js | 13 + cypress/support/commands.js | 33 +++ cypress/support/index.js | 20 ++ .../vue/components/ERM/AgreementPeriods.vue | 5 + package.json | 5 + tsconfig.json | 3 +- webpack.config.js | 6 +- 9 files changed, 351 insertions(+), 3 deletions(-) create mode 100644 cypress.json create mode 100644 cypress/integration/Agreements_spec.ts create mode 100644 cypress/plugins/index.js create mode 100644 cypress/support/commands.js create mode 100644 cypress/support/index.js diff --git a/cypress.json b/cypress.json new file mode 100644 index 0000000000..41f5e30897 --- /dev/null +++ b/cypress.json @@ -0,0 +1,3 @@ +{ + "baseUrl": "http://kohadev-intra.mydnsname.org:8081" +} \ No newline at end of file diff --git a/cypress/integration/Agreements_spec.ts b/cypress/integration/Agreements_spec.ts new file mode 100644 index 0000000000..d50203d120 --- /dev/null +++ b/cypress/integration/Agreements_spec.ts @@ -0,0 +1,266 @@ +import { mount } from "@cypress/vue"; +const dayjs = require("dayjs"); /* Cannot use our calendar JS code, it's in an include file (!) + Also note that moment.js is deprecated */ + +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"), +}; +function get_agreement() { + return { + agreement_id: 1, + closure_reason: "", + description: "my first agreement", + is_perpetual: false, + license_info: "", + name: "agreement 1", + renewal_priority: "", + status: "active", + vendor_id: null, + periods: [ + { + started_on: dates["today_iso"], + ended_on: dates["tomorrow_iso"], + cancellation_deadline: null, + notes: null, + }, + { + started_on: dates["today_iso"], + ended_on: null, + cancellation_deadline: dates["tomorrow_iso"], + notes: "this is a note", + }, + ], + user_roles: [], + }; +} + +describe("Agreement CRUD operations", () => { + beforeEach(() => { + cy.login("koha", "koha"); + cy.title().should("eq", "Koha staff interface"); + }); + + it("List agreements", () => { + // GET agreements returns 500 + cy.intercept("GET", "/api/v1/erm/agreements*", { + statusCode: 500, + error: "Something went wrong", + }); + cy.visit("/cgi-bin/koha/erm/agreements.pl"); + cy.get("#agreements").contains("Something went wrong"); + + // GET agreements returns empty list + cy.intercept("GET", "/api/v1/erm/agreements*", []); + cy.visit("/cgi-bin/koha/erm/agreements.pl"); + cy.get("#agreements").contains("There are no agreements defined."); + + // GET agreements returns something + let agreement = get_agreement(); + let agreements = [agreement]; + + cy.intercept("GET", "/api/v1/erm/agreements*", { + statusCode: 200, + body: agreements, + headers: { + "X-Base-Total-Count": "1", + "X-Total-Count": "1", + }, + }); + cy.intercept("GET", "/api/v1/erm/agreements/*", agreement); + cy.visit("/cgi-bin/koha/erm/agreements.pl"); + cy.get("#agreements").contains("Showing 1 to 1 of 1 entries"); + }); + + it ("Add agreement", () => { + // Click the button in the toolbar + cy.visit("/cgi-bin/koha/erm/agreements.pl"); + cy.contains("New agreement").click(); + cy.get("#agreements h2").contains("New agreement"); + + // Fill in the form for normal attributes + let agreement = get_agreement(); + + cy.get("#agreements").contains("Submit").click(); + cy.get("input:invalid,textarea:invalid,select:invalid").should("have.length", 3); + cy.get("#agreement_name").type(agreement.name); + cy.get("#agreement_description").type(agreement.description); + cy.get("#agreements").contains("Submit").click(); + cy.get("input:invalid,textarea:invalid,select:invalid").should("have.length", 1); // name, description, status + cy.get("#agreement_status").select(agreement.status); + + cy.contains("Add new period").click(); + cy.get("#agreements").contains("Submit").click(); + cy.get("input:invalid,textarea:invalid,select:invalid").should("have.length", 1); // Start date + + // Add new periods + cy.contains("Add new period").click(); + cy.contains("Add new period").click(); + cy.get("#agreement_periods > fieldset").should("have.length", 3); + + cy.get("#agreement_period_1").contains("Remove this period").click(); + + cy.get("#agreement_periods > fieldset").should("have.length", 2); + cy.get("#agreement_period_0"); + cy.get("#agreement_period_1"); + + // Selecting the flatpickr values is a bit tedious here... + // We have 3 date inputs per period + cy.get("#ended_on_0").click(); + // Second flatpickr => ended_on for the first period + cy.get(".flatpickr-calendar") + .eq(1) + .find("span.today") + .click({ force: true }); // select today. No idea why we should force, but there is a random failure otherwise + + cy.get("#started_on_0").click(); + cy.get(".flatpickr-calendar") + .eq(0) + .find("span.today") + .next("span") + .click(); // select tomorrow + + cy.get("#ended_on_0").should("have.value", ""); // Has been reset correctly + + cy.get("#started_on_0").click(); + cy.get(".flatpickr-calendar").eq(0).find("span.today").click(); // select today + cy.get("#ended_on_0").click({ force: true }); // No idea why we should force, but there is a random failure otherwise + cy.get(".flatpickr-calendar") + .eq(1) + .find("span.today") + .next("span") + .click(); // select tomorrow + + // Second period + cy.get("#started_on_1").click({ force: true }); + cy.get(".flatpickr-calendar").eq(3).find("span.today").click(); // select today + cy.get("#cancellation_deadline_1").click(); + cy.get(".flatpickr-calendar") + .eq(5) + .find("span.today") + .next("span") + .click(); // select tomorrow + cy.get("#notes_1").type("this is a note"); + + // TODO Add a new user + // How to test a new window with cypresS? + //cy.contains("Add new user").click(); + //cy.contains("Select user").click(); + + // Submit the form, get 500 + cy.intercept("POST", "/api/v1/erm/agreements", { + statusCode: 500, + error: "Something went wrong", + }); + cy.get("#agreements").contains("Submit").click(); + cy.get("#agreements").contains( + "Something went wrong: Internal Server Error" + ); + + // Submit the form, success! + cy.intercept("POST", "/api/v1/erm/agreements", { + statusCode: 201, + body: agreement, + }); + cy.get("#agreements").contains("Submit").click(); + cy.get("#agreements").contains("Agreement created"); + }); + + it ("Edit agreement", () => { + let agreement = get_agreement(); + let agreements = [agreement]; + // Click the 'Edit' button from the list + cy.intercept("GET", "/api/v1/erm/agreements*", { + statusCode: 200, + body: agreements, + headers: { + "X-Base-Total-Count": "1", + "X-Total-Count": "1", + }, + }); + cy.intercept("GET", "/api/v1/erm/agreements/*", agreement).as("get-agreement"); + cy.visit("/cgi-bin/koha/erm/agreements.pl"); + cy.get("#agreements table tbody tr:first").contains("Edit").click(); + cy.wait("@get-agreement"); + cy.wait(500); // Cypress is too fast! Vue hasn't populated the form yet! + cy.get("#agreements h2").contains("Edit agreement"); + + // Form has been correctly filled in + cy.get("#agreement_name").should("have.value", agreements[0].name); + cy.get("#agreement_description").should( + "have.value", + agreements[0].description + ); + cy.get("#agreement_status").should("have.value", agreement.status); + cy.get("#agreement_is_perpetual_no").should("be.checked"); + cy.get("#started_on_0").invoke("val").should("eq", dates["today_us"]); + cy.get("#ended_on_0").invoke("val").should("eq", dates["tomorrow_us"]); + cy.get("#cancellation_deadline_0").invoke("val").should("eq", ""); + cy.get("#notes_0").should("have.value", ""); + cy.get("#started_on_1").invoke("val").should("eq", dates["today_us"]); + cy.get("#ended_on_1").invoke("val").should("eq", ""); + cy.get("#cancellation_deadline_1") + .invoke("val") + .should("eq", dates["tomorrow_us"]); + cy.get("#notes_1").should("have.value", "this is a note"); + + // Submit the form, get 500 + cy.intercept("PUT", "/api/v1/erm/agreements/*", { + statusCode: 500, + error: "Something went wrong", + }); + cy.get("#agreements").contains("Submit").click(); + cy.get("#agreements").contains( + "Something went wrong: Internal Server Error" + ); + + // Submit the form, success! + cy.intercept("PUT", "/api/v1/erm/agreements/*", { + statusCode: 200, + body: agreement, + }); + cy.get("#agreements").contains("Submit").click(); + cy.get("#agreements").contains("Agreement updated"); + }); + + it ("Delete agreement", () => { + let agreement = get_agreement(); + let agreements = [agreement]; + + // Click the 'Delete' button from the list + cy.intercept("GET", "/api/v1/erm/agreements*", { + statusCode: 200, + body: agreements, + headers: { + "X-Base-Total-Count": "1", + "X-Total-Count": "1", + }, + }); + cy.intercept("GET", "/api/v1/erm/agreements/*", agreement); + cy.visit("/cgi-bin/koha/erm/agreements.pl"); + + cy.get("#agreements table tbody tr:first").contains("Delete").click(); + cy.get("#agreements h2").contains("Delete agreement"); + cy.contains("Agreement name: " + agreement.name); + + // Submit the form, get 500 + cy.intercept("DELETE", "/api/v1/erm/agreements/*", { + statusCode: 500, + error: "Something went wrong", + }); + cy.contains("Yes, delete").click(); + cy.get("#agreements").contains( + "Something went wrong: Internal Server Error" + ); + + // Submit the form, success! + cy.intercept("DELETE", "/api/v1/erm/agreements/*", { + statusCode: 204, + body: null, + }); + cy.contains("Yes, delete").click(); + cy.get("#agreements").contains("Agreement deleted"); + }); +}); diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js new file mode 100644 index 0000000000..cf6b72f611 --- /dev/null +++ b/cypress/plugins/index.js @@ -0,0 +1,13 @@ +const { startDevServer } = require('@cypress/webpack-dev-server') +const webpackConfig = require('@vue/cli-service/webpack.config.js') + +module.exports = (on, config) => { + on('dev-server:start', options => + startDevServer({ + options, + webpackConfig + }) + ) + + return config +} diff --git a/cypress/support/commands.js b/cypress/support/commands.js new file mode 100644 index 0000000000..be71149f90 --- /dev/null +++ b/cypress/support/commands.js @@ -0,0 +1,33 @@ +// *********************************************** +// This example commands.js shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** +// +// +// -- This is a parent command -- +// Cypress.Commands.add('login', (email, password) => { ... }) +// +// +// -- This is a child command -- +// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) +// +// +// -- This is a dual command -- +// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) +// +// +// -- This will overwrite an existing command -- +// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) + + +Cypress.Commands.add('login', (username, password) => { + cy.visit('/cgi-bin/koha/mainpage.pl?logout.x=1') + cy.get("#userid").type(username) + cy.get("#password").type(password) + cy.get("#submit-button").click() +}) \ No newline at end of file diff --git a/cypress/support/index.js b/cypress/support/index.js new file mode 100644 index 0000000000..d68db96df2 --- /dev/null +++ b/cypress/support/index.js @@ -0,0 +1,20 @@ +// *********************************************************** +// This example support/index.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands' + +// Alternatively you can use CommonJS syntax: +// require('./commands') diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/AgreementPeriods.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/AgreementPeriods.vue index 9feea10e85..979528f172 100644 --- a/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/AgreementPeriods.vue +++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/AgreementPeriods.vue @@ -2,6 +2,7 @@
Periods
Start date: { p.started_on = $date(p.started_on) + p.ended_on = $date(p.ended_on) + p.cancellation_deadline = $date(p.cancellation_deadline) }) this.dates_fixed = 1 } diff --git a/package.json b/package.json index 209a2cbd44..b428a0d417 100644 --- a/package.json +++ b/package.json @@ -7,14 +7,18 @@ "test": "test" }, "dependencies": { + "@cypress/vue": "^3.1.1", + "@cypress/webpack-dev-server": "^1.8.3", "@fortawesome/fontawesome-svg-core": "^6.1.0", "@fortawesome/free-solid-svg-icons": "^6.0.0", "@fortawesome/vue-fontawesome": "^3.0.0-5", "@popperjs/core": "^2.11.2", + "@vue/cli-service": "^5.0.1", "babel-core": "^7.0.0-beta.3", "bootstrap": "^5.1.3", "bootstrap-vue-3": "^0.1.7", "css-loader": "^6.6.0", + "cypress": "^9.5.2", "gulp": "^4.0.2", "gulp-autoprefixer": "^4.0.0", "gulp-concat-po": "^1.0.0", @@ -28,6 +32,7 @@ "lodash": "^4.17.12", "merge-stream": "^2.0.0", "minimist": "^1.2.5", + "mysql": "^2.18.1", "style-loader": "^3.3.1", "vue": "^3.2.31", "vue-flatpickr-component": "^9" diff --git a/tsconfig.json b/tsconfig.json index 88ccffe748..298160923b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,4 +1,5 @@ { "compilerOptions": { - } + }, + "exclude": ["./cypress"] } diff --git a/webpack.config.js b/webpack.config.js index efa980de82..0c6183f87e 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -16,17 +16,19 @@ module.exports = { { test: /\.vue$/, loader: "vue-loader", + exclude: [path.resolve(__dirname, "cypress/")], }, { test: /\.ts$/, loader: 'ts-loader', options: { appendTsSuffixTo: [/\.vue$/] - } + }, + exclude: [path.resolve(__dirname, "cypress/")], }, { test: /\.css$/, - use: ['style-loader', 'css-loader'] + use: ['style-loader', 'css-loader'], } ], }, -- 2.39.5