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.
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
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");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,373 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
const makeCustomer = (overrides: Partial<Record<string, any>> = {}, 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<Record<string, any>> = {}, 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<string, any>[];
|
||||
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");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user