feat(package): add Cypress for end-to-end testing and enhance scripts
- Added Cypress as a dependency for end-to-end testing. - Introduced new npm scripts for running Cypress tests and starting the development server for testing. - Updated package-lock.json to reflect the new dependencies and their versions.
This commit is contained in:
@@ -0,0 +1,11 @@
|
|||||||
|
import { defineConfig } from "cypress";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
e2e: {
|
||||||
|
baseUrl: process.env.CYPRESS_BASE_URL || "http://localhost:3000",
|
||||||
|
supportFile: "cypress/support/e2e.ts",
|
||||||
|
specPattern: "cypress/e2e/**/*.cy.ts",
|
||||||
|
},
|
||||||
|
viewportWidth: 1440,
|
||||||
|
viewportHeight: 900,
|
||||||
|
});
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
describe("TM-389 - prevent duplicate create customer request", () => {
|
||||||
|
let createRequestCount = 0;
|
||||||
|
const fillRequiredFormFields = () => {
|
||||||
|
cy.get('input[name="username"]').type("tm389user");
|
||||||
|
cy.get('input[name="email"]').type("tm389@example.com");
|
||||||
|
cy.get('select[name="role"]').select("admin");
|
||||||
|
cy.get('input[name="password"]').type("Secret123!");
|
||||||
|
cy.get('select[name="type"]').select("individual");
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
createRequestCount = 0;
|
||||||
|
|
||||||
|
cy.intercept("GET", "**/api/proxy/companies**", {
|
||||||
|
statusCode: 200,
|
||||||
|
body: {
|
||||||
|
success: true,
|
||||||
|
message: "ok",
|
||||||
|
data: {
|
||||||
|
companies: [
|
||||||
|
{
|
||||||
|
id: "507f1f77bcf86cd799439011",
|
||||||
|
name: "Acme Company",
|
||||||
|
email: "acme@example.com",
|
||||||
|
phone: "1234567890",
|
||||||
|
website: "https://acme.example.com",
|
||||||
|
type: "private",
|
||||||
|
description: "Test company",
|
||||||
|
founded_year: 2020,
|
||||||
|
employee_count: 50,
|
||||||
|
annual_revenue: 100000,
|
||||||
|
currency: "USD",
|
||||||
|
industry: "Tech",
|
||||||
|
language: "en",
|
||||||
|
tax_id: "TAX-001",
|
||||||
|
registration_number: "REG-001",
|
||||||
|
timezone: "UTC",
|
||||||
|
address: {
|
||||||
|
street: "Street 1",
|
||||||
|
city: "City",
|
||||||
|
state: "State",
|
||||||
|
postal_code: "12345",
|
||||||
|
country: "Country",
|
||||||
|
},
|
||||||
|
contact_person: {
|
||||||
|
full_name: "John Doe",
|
||||||
|
first_name: "John",
|
||||||
|
last_name: "Doe",
|
||||||
|
email: "john@example.com",
|
||||||
|
phone: "1234567890",
|
||||||
|
mobile: "1234567890",
|
||||||
|
position: "Manager",
|
||||||
|
is_primary: true,
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
keywords: [],
|
||||||
|
categories: [],
|
||||||
|
specializations: [],
|
||||||
|
certifications: [],
|
||||||
|
cpv_codes: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
offset: 0,
|
||||||
|
limit: 10,
|
||||||
|
total: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).as("getCompanies");
|
||||||
|
|
||||||
|
cy.intercept("POST", "**/api/proxy/customers*", (req) => {
|
||||||
|
createRequestCount += 1;
|
||||||
|
req.reply({
|
||||||
|
delay: 1200,
|
||||||
|
statusCode: 201,
|
||||||
|
body: {
|
||||||
|
success: true,
|
||||||
|
message: "created",
|
||||||
|
data: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}).as("createCustomer");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sends only one create request even when submit is clicked repeatedly", () => {
|
||||||
|
cy.visit("/customers/create");
|
||||||
|
|
||||||
|
cy.wait("@getCompanies");
|
||||||
|
|
||||||
|
fillRequiredFormFields();
|
||||||
|
|
||||||
|
cy.get('button[type="submit"]').as("submitButton");
|
||||||
|
cy.get("@submitButton").should("not.be.disabled");
|
||||||
|
cy.get("@submitButton").click();
|
||||||
|
|
||||||
|
// Fire extra clicks while request is in-flight to simulate impatient taps.
|
||||||
|
cy.get("@submitButton").then(($button) => {
|
||||||
|
const button = $button.get(0) as HTMLButtonElement;
|
||||||
|
button.click();
|
||||||
|
button.click();
|
||||||
|
button.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.get('button[type="submit"]').should("be.disabled");
|
||||||
|
cy.wait("@createCustomer");
|
||||||
|
cy.get("@createCustomer.all").should("have.length", 1);
|
||||||
|
cy.then(() => {
|
||||||
|
expect(createRequestCount).to.eq(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not send another create request when submit is clicked again before response", () => {
|
||||||
|
cy.visit("/customers/create");
|
||||||
|
|
||||||
|
cy.wait("@getCompanies");
|
||||||
|
fillRequiredFormFields();
|
||||||
|
|
||||||
|
cy.get('button[type="submit"]').as("submitButton");
|
||||||
|
cy.get("@submitButton").click();
|
||||||
|
|
||||||
|
cy.get("@submitButton").then(($button) => {
|
||||||
|
const button = $button.get(0) as HTMLButtonElement;
|
||||||
|
button.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ensure we are still in-flight and no duplicate request was fired.
|
||||||
|
cy.wait(300);
|
||||||
|
cy.get("@createCustomer.all").should("have.length", 1);
|
||||||
|
cy.then(() => {
|
||||||
|
expect(createRequestCount).to.eq(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.wait("@createCustomer");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
const AUTH_COOKIE_KEYS = {
|
||||||
|
accessToken: "TENDER_PANEL_token",
|
||||||
|
refreshToken: "TENDER_PANEL_refresh_token",
|
||||||
|
user: "TENDER_PANEL_user",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
Cypress.Commands.add("loginAsAdmin", () => {
|
||||||
|
cy.session(
|
||||||
|
"admin-session",
|
||||||
|
() => {
|
||||||
|
cy.visit("/auth/sign-in");
|
||||||
|
|
||||||
|
cy.setCookie(AUTH_COOKIE_KEYS.accessToken, "e2e-access-token");
|
||||||
|
cy.setCookie(AUTH_COOKIE_KEYS.refreshToken, "e2e-refresh-token");
|
||||||
|
cy.setCookie(
|
||||||
|
AUTH_COOKIE_KEYS.user,
|
||||||
|
encodeURIComponent(
|
||||||
|
JSON.stringify({
|
||||||
|
id: "507f1f77bcf86cd799439012",
|
||||||
|
username: "e2e-admin",
|
||||||
|
email: "e2e-admin@example.com",
|
||||||
|
role: "admin",
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
validate: () => {
|
||||||
|
cy.getCookie(AUTH_COOKIE_KEYS.accessToken).should("exist");
|
||||||
|
cy.getCookie(AUTH_COOKIE_KEYS.user).should("exist");
|
||||||
|
},
|
||||||
|
cacheAcrossSpecs: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
// Central place for global Cypress hooks/commands.
|
||||||
|
import "./commands";
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
if (Cypress.env("AUTO_LOGIN") !== false) {
|
||||||
|
cy.loginAsAdmin();
|
||||||
|
}
|
||||||
|
});
|
||||||
Vendored
+11
@@ -0,0 +1,11 @@
|
|||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
namespace Cypress {
|
||||||
|
interface Chainable {
|
||||||
|
loginAsAdmin(): Chainable<void>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"lib": ["ES2020", "DOM"],
|
||||||
|
"types": ["cypress", "node"],
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"strict": true
|
||||||
|
},
|
||||||
|
"include": ["**/*.ts", "**/*.d.ts"]
|
||||||
|
}
|
||||||
Generated
+1875
-19
File diff suppressed because it is too large
Load Diff
+7
-1
@@ -4,9 +4,13 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
|
"dev:e2e": "next dev -p 3000",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint"
|
"lint": "next lint",
|
||||||
|
"cypress:open": "cypress open",
|
||||||
|
"cypress:run": "cypress run",
|
||||||
|
"test:e2e": "start-server-and-test dev:e2e http://localhost:3000 \"CYPRESS_BASE_URL=http://localhost:3000 cypress run\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/react-query": "^5.85.6",
|
"@tanstack/react-query": "^5.85.6",
|
||||||
@@ -44,11 +48,13 @@
|
|||||||
"@types/react": "19.0.8",
|
"@types/react": "19.0.8",
|
||||||
"@types/react-dom": "19.0.3",
|
"@types/react-dom": "19.0.3",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
|
"cypress": "^15.14.0",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "15.1.9",
|
"eslint-config-next": "15.1.9",
|
||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
"prettier": "^3.4.2",
|
"prettier": "^3.4.2",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||||
|
"start-server-and-test": "^2.1.2",
|
||||||
"tailwindcss": "^3.4.16",
|
"tailwindcss": "^3.4.16",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user