/// 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(); // Route transition can be delayed in headless runs. cy.location("pathname", { timeout: 15000 }).should( "eq", `/customers/feedback/${customer.id}`, ); cy.wait("@getCustomerFeedback"); cy.contains("feedback type").should("be.visible"); }); });