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:
AmirReza Jamali
2026-04-26 10:13:02 +03:30
parent 84d34866be
commit 4fee2c4944
2 changed files with 544 additions and 0 deletions
@@ -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");
});
});