Bug 32030: ERM - Add "integration" tests using Cypress
authorJonathan Druart <jonathan.druart@bugs.koha-community.org>
Tue, 15 Mar 2022 11:16:45 +0000 (12:16 +0100)
committerTomas Cohen Arazi <tomascohen@theke.io>
Tue, 8 Nov 2022 12:43:39 +0000 (09:43 -0300)
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 <jonathan.field@ptfs-europe.com>
Signed-off-by: Martin Renvoize <martin.renvoize@ptfs-europe.com>
Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>
cypress.json [new file with mode: 0644]
cypress/integration/Agreements_spec.ts [new file with mode: 0644]
cypress/plugins/index.js [new file with mode: 0644]
cypress/support/commands.js [new file with mode: 0644]
cypress/support/index.js [new file with mode: 0644]
koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/AgreementPeriods.vue
package.json
tsconfig.json
webpack.config.js

diff --git a/cypress.json b/cypress.json
new file mode 100644 (file)
index 0000000..41f5e30
--- /dev/null
@@ -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 (file)
index 0000000..d50203d
--- /dev/null
@@ -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 (file)
index 0000000..cf6b72f
--- /dev/null
@@ -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 (file)
index 0000000..be71149
--- /dev/null
@@ -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 (file)
index 0000000..d68db96
--- /dev/null
@@ -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')
index 9feea10..979528f 100644 (file)
@@ -2,6 +2,7 @@
     <fieldset class="rows" id="agreement_periods">
         <legend>Periods</legend>
         <fieldset
+            :id="`agreement_period_${counter}`"
             class="rows"
             v-for="(period, counter) in periods"
             v-bind:key="counter"
@@ -18,6 +19,7 @@
                         >Start date:
                     </label>
                     <flat-pickr
+                        :id="`started_on_${counter}`"
                         v-model="period.started_on"
                         required
                         :config="fp_config"
@@ -46,6 +48,7 @@
                 <li>
                     <label :for="`notes_${counter}`">Notes: </label>
                     <input
+                        :id="`notes_${counter}`"
                         type="text"
                         class="notes"
                         :name="`notes_${counter}`"
@@ -74,6 +77,8 @@ export default {
         if (!this.dates_fixed) {
             this.periods.forEach(p => {
                 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
         }
index 209a2cb..b428a0d 100644 (file)
@@ -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"
index 88ccffe..2981609 100644 (file)
@@ -1,4 +1,5 @@
 {
   "compilerOptions": {
-  }
+  },
+  "exclude": ["./cypress"]
 }
index efa980d..0c6183f 100644 (file)
@@ -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'],
       }
     ],
   },