From 4fee2c494477105b8e3b65b6591805b680e8bee8 Mon Sep 17 00:00:00 2001 From: AmirReza Jamali Date: Sun, 26 Apr 2026 10:13:02 +0330 Subject: [PATCH] chore: update CI configuration and Dockerfile for pnpm compatibility - Adjusted CI configuration to ensure compatibility with pnpm. - Made necessary updates to the Dockerfile for seamless integration with pnpm. - Refined installation instructions in README.md to align with the latest changes. --- .../e2e/customers/customers-create-edit.cy.ts | 171 ++++++++ .../e2e/customers/customers-management.cy.ts | 373 ++++++++++++++++++ 2 files changed, 544 insertions(+) create mode 100644 cypress/e2e/customers/customers-create-edit.cy.ts create mode 100644 cypress/e2e/customers/customers-management.cy.ts diff --git a/cypress/e2e/customers/customers-create-edit.cy.ts b/cypress/e2e/customers/customers-create-edit.cy.ts new file mode 100644 index 0000000..1784a55 --- /dev/null +++ b/cypress/e2e/customers/customers-create-edit.cy.ts @@ -0,0 +1,171 @@ +/// + +describe("customers management - create and edit", () => { + beforeEach(() => { + cy.setAuthCookies(); + }); + + it("validates required fields on create", () => { + cy.intercept("GET", "**/api/proxy/companies**", { + statusCode: 200, + body: { success: true, message: "ok", data: { companies: [] } }, + }).as("getCompanies"); + + cy.visit("/customers/create"); + cy.wait("@getCompanies"); + + cy.get('button[type="submit"]').click(); + cy.contains("This field is required").should("be.visible"); + }); + + it("validates invalid email on create", () => { + cy.intercept("GET", "**/api/proxy/companies**", { + statusCode: 200, + body: { success: true, message: "ok", data: { companies: [] } }, + }).as("getCompanies"); + + cy.visit("/customers/create"); + cy.wait("@getCompanies"); + + cy.get('input[name="username"]').type("qauser"); + cy.get('input[name="email"]').type("not-an-email"); + cy.get('select[name="role"]').select("admin"); + cy.get('input[name="password"]').type("Secret123!"); + cy.get('select[name="type"]').select("individual"); + cy.get('button[type="submit"]').click(); + + cy.contains("Email address is invalid").should("be.visible"); + }); + + it("creates customer and sends expected payload", () => { + cy.intercept("GET", "**/api/proxy/companies**", (req) => { + req.reply({ + statusCode: 200, + body: { + success: true, + message: "ok", + data: { + companies: [ + { id: "comp-1", name: "Acme" }, + { id: "comp-2", name: "Globex" }, + ], + }, + }, + }); + }).as("getCompanies"); + + cy.intercept("POST", "**/api/proxy/customers", (req) => { + expect(req.body).to.include({ + username: "newcustomer", + email: "newcustomer@example.com", + role: "admin", + type: "company", + }); + req.reply({ + statusCode: 201, + body: { success: true, message: "Customer created successfully", data: {} }, + }); + }).as("createCustomer"); + + cy.intercept("GET", "**/api/proxy/customers**", { + statusCode: 200, + body: { + success: true, + message: "ok", + data: { customers: [] }, + meta: { offset: 0, limit: 10, total: 0, pages: 1, page: 1 }, + }, + }).as("getCustomersAfterCreate"); + + cy.visit("/customers/create"); + cy.wait("@getCompanies"); + + cy.get('input[name="username"]').type("newcustomer"); + cy.get('input[name="email"]').type("newcustomer@example.com"); + cy.get('select[name="role"]').select("admin"); + cy.get('input[name="password"]').type("Secret123!"); + cy.get('select[name="type"]').select("company"); + cy.get('button[type="submit"]').click(); + + cy.wait("@createCustomer"); + cy.location("pathname").should("eq", "/customers"); + cy.wait("@getCustomersAfterCreate"); + }); + + it("loads edit page with customer details and updates customer", () => { + const customerId = "507f1f77bcf86cd799439155"; + const customerDetails = { + id: customerId, + username: "existingcustomer", + email: "existing@example.com", + full_name: "Existing Customer", + phone: "1234567890", + role: "analyst", + type: "individual", + companies: [{ id: "comp-2", name: "Globex" }], + status: "active", + created_at: 1710001000, + updated_at: 1710001500, + }; + + cy.intercept("GET", "**/api/proxy/companies**", { + statusCode: 200, + body: { + success: true, + message: "ok", + data: { + companies: [ + { id: "comp-1", name: "Acme" }, + { id: "comp-2", name: "Globex" }, + ], + }, + }, + }).as("getCompanies"); + + cy.intercept("GET", `**/api/proxy/customers/${customerId}`, { + statusCode: 200, + body: { success: true, message: "ok", data: customerDetails }, + }).as("getCustomerDetails"); + + cy.intercept("PUT", `**/api/proxy/customers/${customerId}`, (req) => { + expect(req.body).to.include({ + username: "existingcustomer", + email: "updated@example.com", + role: "admin", + type: "company", + }); + req.reply({ + statusCode: 200, + body: { success: true, message: "Customer updated successfully", data: {} }, + }); + }).as("updateCustomer"); + + cy.intercept("GET", "**/api/proxy/customers**", { + statusCode: 200, + body: { + success: true, + message: "ok", + data: { customers: [] }, + meta: { offset: 0, limit: 10, total: 0, pages: 1, page: 1 }, + }, + }).as("getCustomersAfterUpdate"); + + cy.visit(`/customers/edit/${customerId}`); + cy.wait("@getCustomerDetails"); + cy.wait("@getCompanies"); + + cy.get('input[name="username"]').should("have.value", "existingcustomer"); + cy.get('input[name="email"]').should("have.value", "existing@example.com"); + cy.get('select[name="role"]').should("have.value", "analyst"); + cy.get('select[name="type"]').should("have.value", "individual"); + + cy.get('input[name="email"]').clear().type("updated@example.com"); + cy.get('select[name="role"]').select("admin"); + cy.get('select[name="type"]').select("company"); + cy.get('button[type="submit"]').click(); + + cy.wait("@updateCustomer"); + cy.location("pathname").should("eq", "/customers"); + cy.wait("@getCustomersAfterUpdate"); + }); +}); diff --git a/cypress/e2e/customers/customers-management.cy.ts b/cypress/e2e/customers/customers-management.cy.ts new file mode 100644 index 0000000..38bc9fe --- /dev/null +++ b/cypress/e2e/customers/customers-management.cy.ts @@ -0,0 +1,373 @@ +/// + +const makeCustomer = (overrides: Partial> = {}, index = 1) => ({ + id: `507f1f77bcf86cd7994390${String(index).padStart(2, "0")}`, + full_name: `Customer ${index}`, + email: `customer${index}@example.com`, + username: `customer${index}`, + phone: "1234567890", + type: "individual", + status: "active", + role: "admin", + companies: [], + created_at: 1710000000 + index, + updated_at: 1710000500 + index, + ...overrides, +}); + +const makeCompany = (overrides: Partial> = {}, index = 1) => ({ + id: `company-${index}`, + name: `Company ${index}`, + email: `company${index}@example.com`, + phone: "1234567890", + website: "https://example.com", + type: "vendor", + description: "Test company", + founded_year: 2020, + employee_count: 25, + annual_revenue: 1000000, + currency: "USD", + industry: "Technology", + language: "en", + tax_id: `TAX-${index}`, + registration_number: `REG-${index}`, + timezone: "UTC", + address: { + street: "Main St", + city: "Tehran", + state: "Tehran", + postal_code: "12345", + country: "IR", + }, + contact_person: { + full_name: "Jane Doe", + first_name: "Jane", + last_name: "Doe", + email: "jane.doe@example.com", + phone: "1234567890", + mobile: "1234567890", + position: "Manager", + is_primary: true, + }, + tags: { + keywords: [], + categories: [], + specializations: [], + certifications: [], + cpv_codes: [], + }, + ...overrides, +}); + +const customersResponse = ({ + customers, + offset = 0, + limit = 10, + total = customers.length, + pages = Math.max(1, Math.ceil(total / limit)), +}: { + customers: Record[]; + offset?: number; + limit?: number; + total?: number; + pages?: number; +}) => ({ + success: true, + message: "ok", + data: { customers }, + meta: { offset, limit, total, pages, page: Math.floor(offset / limit) + 1 }, +}); + +describe("customers management - list, actions, filters, feedback", () => { + beforeEach(() => { + cy.setAuthCookies(); + }); + + it("renders table columns and row data from API", () => { + const customers = [ + makeCustomer( + { + full_name: "John Carter", + email: "john.carter@example.com", + companies: [{ id: "c1", name: "Acme" }], + }, + 1, + ), + makeCustomer({ status: "inactive", role: "analyst" }, 2), + ]; + + cy.intercept("GET", "**/api/proxy/customers**", { + statusCode: 200, + body: customersResponse({ customers }), + }).as("getCustomers"); + + cy.visit("/customers"); + cy.wait("@getCustomers"); + + [ + "row", + "full name", + "email", + "created at", + "type", + "status", + "company", + "role", + "actions", + ].forEach((header) => cy.contains("th", new RegExp(`^${header}$`, "i")).should("be.visible")); + + cy.contains("John Carter").should("be.visible"); + cy.contains("john.carter@example.com").should("be.visible"); + cy.contains("Acme").should("be.visible"); + }); + + it("shows empty state when API returns no customers", () => { + cy.intercept("GET", "**/api/proxy/customers**", { + statusCode: 200, + body: customersResponse({ customers: [], total: 0 }), + }).as("getCustomersEmpty"); + + cy.visit("/customers"); + cy.wait("@getCustomersEmpty"); + + cy.getByCy("empty-list-message").should("contain", "No Customers were found"); + }); + + it("shows loading skeleton while list request is in flight", () => { + cy.intercept("GET", "**/api/proxy/customers**", (req) => { + req.reply({ + delay: 1200, + statusCode: 200, + body: customersResponse({ customers: [makeCustomer()] }), + }); + }).as("getCustomersSlow"); + + cy.visit("/customers"); + cy.getByCy("table-skeleton").should("be.visible"); + cy.wait("@getCustomersSlow"); + cy.getByCy("table-skeleton").should("not.exist"); + }); + + it("paginates with correct offset when moving to next page", () => { + const pageOne = Array.from({ length: 10 }).map((_, i) => makeCustomer({}, i + 1)); + const pageTwo = [makeCustomer({ id: "507f1f77bcf86cd799439099", full_name: "Page 2 Customer" }, 99)]; + + cy.intercept("GET", "**/api/proxy/customers**", (req) => { + const offset = Number(req.query.offset ?? 0); + if (offset === 10) { + req.alias = "getCustomersPage2"; + req.reply({ + statusCode: 200, + body: customersResponse({ + customers: pageTwo, + offset: 10, + limit: 10, + total: 11, + pages: 2, + }), + }); + return; + } + req.alias = "getCustomersPage1"; + req.reply({ + statusCode: 200, + body: customersResponse({ + customers: pageOne, + offset: 0, + limit: 10, + total: 11, + pages: 2, + }), + }); + }); + + cy.visit("/customers"); + cy.wait("@getCustomersPage1"); + cy.contains("Page 2 Customer").should("not.exist"); + cy.contains("›").click(); + cy.wait("@getCustomersPage2") + .its("request.query.offset") + .should("eq", "10"); + cy.contains("Page 2 Customer").should("be.visible"); + }); + + it("deletes customer with confirm modal and sends correct request", () => { + const customer = makeCustomer({ id: "507f1f77bcf86cd799439066", full_name: "Delete Me" }, 66); + + cy.intercept("GET", "**/api/proxy/customers**", { + statusCode: 200, + body: customersResponse({ customers: [customer] }), + }).as("getCustomers"); + + cy.intercept("DELETE", `**/api/proxy/customers/${customer.id}`, { + statusCode: 200, + body: { success: true, data: { message: "Customer deleted successfully" } }, + }).as("deleteCustomer"); + + cy.visit("/customers"); + cy.wait("@getCustomers"); + cy.contains("Delete Me").should("be.visible"); + + cy.get('button[data-tooltip-id="delete"]').first().click(); + cy.getByCy("confirmation-modal").should("be.visible"); + cy.getByCy("confirmation-modal-cancel").click(); + cy.getByCy("confirmation-modal").should("not.exist"); + + cy.intercept("GET", "**/api/proxy/customers**", { + statusCode: 200, + body: customersResponse({ customers: [], total: 0 }), + }).as("getCustomersAfterDelete"); + + cy.get('button[data-tooltip-id="delete"]').first().click(); + cy.getByCy("confirmation-modal-confirm").click(); + cy.wait("@deleteCustomer"); + cy.wait("@getCustomersAfterDelete"); + cy.getByCy("empty-list-message").should("contain", "No Customers were found"); + }); + + it("assigns customer to company with correct payload", () => { + const customer = makeCustomer( + { id: "507f1f77bcf86cd799439033", companies: [{ id: "comp-2", name: "Global LLC" }] }, + 33, + ); + const companies = [ + makeCompany({ id: "comp-1", name: "Acme Corp" }, 1), + makeCompany({ id: "comp-2", name: "Global LLC" }, 2), + ]; + + cy.intercept("GET", "**/api/proxy/customers**", { + statusCode: 200, + body: customersResponse({ customers: [customer] }), + }).as("getCustomers"); + + cy.intercept("GET", "**/api/proxy/companies**", { + statusCode: 200, + body: { + success: true, + message: "ok", + data: { companies }, + meta: { offset: 0, limit: 10, total: companies.length }, + }, + }).as("getCompanies"); + + cy.intercept("POST", `**/api/proxy/customers/${customer.id}/companies`, (req) => { + expect(req.body).to.deep.equal({ company_ids: ["comp-2"] }); + req.reply({ + statusCode: 200, + body: { success: true, message: "Selected Companies were assigned", data: {} }, + }); + }).as("assignCompany"); + + cy.visit("/customers"); + cy.wait("@getCustomers"); + cy.get('button[data-tooltip-id="assign-company"]').first().click(); + cy.wait("@getCompanies"); + cy.getByCy("modal-confirm-button").click(); + cy.wait("@assignCompany"); + }); + + it("surfaces assign-company API failures (404)", () => { + const customer = makeCustomer( + { id: "507f1f77bcf86cd799439034", companies: [{ id: "comp-1", name: "Acme Corp" }] }, + 34, + ); + + cy.intercept("GET", "**/api/proxy/customers**", { + statusCode: 200, + body: customersResponse({ customers: [customer] }), + }).as("getCustomers"); + + cy.intercept("GET", "**/api/proxy/companies**", { + statusCode: 200, + body: { + success: true, + message: "ok", + data: { companies: [makeCompany({ id: "comp-1", name: "Acme Corp" }, 1)] }, + }, + }).as("getCompanies"); + + cy.intercept("POST", `**/api/proxy/customers/${customer.id}/companies`, { + statusCode: 404, + body: { error: { message: "Route not found" } }, + }).as("assignCompany404"); + + cy.visit("/customers"); + cy.wait("@getCustomers"); + cy.get('button[data-tooltip-id="assign-company"]').first().click(); + cy.wait("@getCompanies"); + cy.getByCy("modal-confirm-button").click(); + cy.wait("@assignCompany404").its("response.statusCode").should("eq", 404); + cy.getByCy("modal-content").should("be.visible"); + }); + + it("applies status/role/type filters and keeps close action side-effect free", () => { + const unfiltered = [makeCustomer({ full_name: "Unfiltered Customer" }, 11)]; + const filtered = [makeCustomer({ full_name: "Filtered Customer", status: "inactive" }, 12)]; + + cy.intercept("GET", "**/api/proxy/customers**", (req) => { + const isFiltered = + req.query.status === "inactive" && + req.query.role === "analyst" && + req.query.type === "company"; + req.alias = isFiltered ? "getCustomersFiltered" : "getCustomersDefault"; + req.reply({ + statusCode: 200, + body: customersResponse({ + customers: isFiltered ? filtered : unfiltered, + total: 1, + }), + }); + }); + + cy.visit("/customers"); + cy.wait("@getCustomersDefault"); + + cy.contains("button", "Filter").click(); + cy.get('select[name="status"]').select("inactive"); + cy.get('select[name="role"]').select("analyst"); + cy.get('select[name="type"]').select("company"); + cy.contains("button", "Search").click(); + + cy.wait("@getCustomersFiltered"); + cy.contains("Filtered Customer").should("be.visible"); + + cy.contains("button", "Filter").click(); + cy.contains("button", "Close").click(); + cy.get("@getCustomersFiltered.all").should("have.length", 1); + }); + + it("navigates to feedback page and loads customer feedback list", () => { + const customer = makeCustomer({ id: "507f1f77bcf86cd799439044", full_name: "Feedback Target" }, 44); + + cy.intercept("GET", "**/api/proxy/customers**", { + statusCode: 200, + body: customersResponse({ customers: [customer] }), + }).as("getCustomers"); + + cy.intercept("GET", "**/api/proxy/feedback**", (req) => { + expect(req.query.customer_id).to.eq(customer.id); + req.reply({ + statusCode: 200, + body: { + success: true, + message: "ok", + data: [ + { + id: "fb-1", + feedback_type: "bug", + created_at: 1710001000, + updated_at: 1710001200, + }, + ], + }, + }); + }).as("getCustomerFeedback"); + + cy.visit("/customers"); + cy.wait("@getCustomers"); + cy.get('button[data-tooltip-id="feedback"]').first().click(); + cy.location("pathname").should("eq", `/customers/feedback/${customer.id}`); + cy.wait("@getCustomerFeedback"); + cy.contains("feedback type").should("be.visible"); + }); +});