initialize

This commit is contained in:
2026-03-26 03:49:05 +03:00
parent e9211a4aba
commit 5dc54ffd00
146 changed files with 12078 additions and 146 deletions
@@ -0,0 +1,293 @@
describe("Customer Form – Integration Tests", () => {
beforeEach(() => {
cy.login()
cy.fixture("customers").then((data) => {
cy.intercept("GET", "**/api/referral-sources", {
statusCode: 200,
body: data.referral_sources,
}).as("getReferralSources")
cy.intercept("GET", "**/api/payment-terms", {
statusCode: 200,
body: data.payment_terms,
}).as("getPaymentTerms")
cy.intercept("GET", "**/api/countries", {
statusCode: 200,
body: data.countries,
}).as("getCountries")
cy.intercept("GET", "**/api/states", {
statusCode: 200,
body: data.states,
}).as("getStates")
cy.intercept("GET", "**/api/customers*", {
statusCode: 200,
body: { success: true, data: { data: [], pagination: { total: 0 } } },
}).as("getCustomers")
})
cy.visit("/sales/customers")
cy.contains("button", "Create Customer").click()
cy.get("[role='dialog']").should("be.visible")
})
// ── Form interaction flow ──
describe("Field interactions", () => {
it("should clear a text field after typing", () => {
cy.get("[role='dialog']").within(() => {
cy.get("input[name='first_name']")
.type("John")
.should("have.value", "John")
.clear()
.should("have.value", "")
})
})
it("should handle special characters in text inputs", () => {
cy.get("[role='dialog']").within(() => {
cy.get("input[name='first_name']").type("José-María").should("have.value", "José-María")
cy.get("input[name='last_name']").type("O'Brien").should("have.value", "O'Brien")
cy.get("input[name='company_name']").type("Smith & Co.").should("have.value", "Smith & Co.")
})
})
it("should accept various email formats", () => {
cy.get("[role='dialog']").within(() => {
cy.get("input[name='first_name']").type("Test")
cy.get("input[name='last_name']").type("User")
// Valid email should not show error
cy.get("input[name='email']").type("user+tag@sub.domain.com")
cy.contains("button", "Create Customer").click()
cy.contains("Enter a valid email address").should("not.exist")
})
})
it("should handle phone number input", () => {
cy.get("[role='dialog']").within(() => {
cy.get("input[name='phone']")
.type("0501234567")
.should("have.value", "0501234567")
cy.get("input[name='alternate_phone']")
.type("+971501234567")
.should("have.value", "+971501234567")
})
})
})
// ── Async select integration ──
describe("Async select fields", () => {
it("should show loading state while fetching referral sources", () => {
cy.intercept("GET", "**/api/referral-sources", {
statusCode: 200,
body: { success: true, data: { data: [{ id: 1, name: "Google" }] } },
delay: 2000,
}).as("slowReferralSources")
// Reload to get the delayed intercept
cy.visit("/sales/customers")
cy.contains("button", "Create Customer").click()
cy.get("[role='dialog']").should("be.visible")
cy.get("[role='dialog']").within(() => {
cy.contains("label", "Referral Source").parent().find("input").click()
})
// The component should show a loading spinner
cy.get("[role='listbox']").should("be.visible")
})
it("should filter options by text input in combobox", () => {
cy.wait("@getReferralSources")
cy.get("[role='dialog']").within(() => {
cy.contains("label", "Referral Source").parent().find("input").click().type("Goo")
})
// Should show Google, shouldn't show Friend Referral
cy.get("[role='option']").contains("Google").should("exist")
})
it("should show empty state when no options match", () => {
cy.wait("@getCountries")
cy.get("[role='dialog']").within(() => {
cy.contains("label", "Country").parent().find("input").click().type("zzzzz")
})
cy.contains("No results found").should("be.visible")
})
it("should select a payment term from the combobox", () => {
cy.wait("@getPaymentTerms")
cy.get("[role='dialog']").within(() => {
cy.contains("label", "Payment Terms").parent().find("input").click()
})
cy.get("[role='option']").contains("Net 30").click()
})
it("should select a state from the combobox", () => {
cy.wait("@getStates")
cy.get("[role='dialog']").within(() => {
cy.contains("label", "State").parent().find("input").click()
})
cy.get("[role='option']").contains("Dubai").click()
})
})
// ── Validation edge cases ──
describe("Validation edge cases", () => {
it("should validate only on submit (not on blur)", () => {
cy.get("[role='dialog']").within(() => {
// Focus and blur first_name without typing
cy.get("input[name='first_name']").focus().blur()
// Error should NOT appear yet (react-hook-form validates on submit by default)
cy.contains("First name is required").should("not.exist")
})
})
it("should clear validation errors when user corrects input", () => {
cy.get("[role='dialog']").within(() => {
// Trigger validation
cy.contains("button", "Create Customer").click()
cy.contains("First name is required").should("be.visible")
// Fix the error
cy.get("input[name='first_name']").type("John")
cy.contains("First name is required").should("not.exist")
})
})
it("should trim whitespace-only inputs and still require first_name", () => {
cy.get("[role='dialog']").within(() => {
cy.get("input[name='first_name']").type(" ")
cy.get("input[name='last_name']").type("Doe")
cy.contains("button", "Create Customer").click()
})
})
it("should allow submission with only required fields", () => {
cy.fixture("customers").then((data) => {
cy.intercept("POST", "**/api/customers", {
statusCode: 201,
body: data.customer_created,
}).as("createCustomer")
})
cy.get("[role='dialog']").within(() => {
cy.get("input[name='first_name']").type("Jane")
cy.get("input[name='last_name']").type("Smith")
cy.contains("button", "Create Customer").click()
})
cy.wait("@createCustomer").its("request.body").should((body) => {
expect(body.first_name).to.eq("Jane")
expect(body.last_name).to.eq("Smith")
// Optional fields should be empty or undefined
expect(body.company_name).to.satisfy(
(v: unknown) => v === "" || v === undefined || v === null,
)
})
})
})
// ── API error scenarios ──
describe("API error handling", () => {
it("should handle network error gracefully", () => {
cy.intercept("POST", "**/api/customers", { forceNetworkError: true }).as(
"networkError",
)
cy.get("[role='dialog']").within(() => {
cy.get("input[name='first_name']").type("John")
cy.get("input[name='last_name']").type("Doe")
cy.contains("button", "Create Customer").click()
})
cy.wait("@networkError")
cy.get("[role='dialog']").within(() => {
cy.contains("Failed to create customer").should("be.visible")
})
})
it("should handle 500 server error", () => {
cy.intercept("POST", "**/api/customers", {
statusCode: 500,
body: { success: false, message: "Internal server error" },
}).as("serverError")
cy.get("[role='dialog']").within(() => {
cy.get("input[name='first_name']").type("John")
cy.get("input[name='last_name']").type("Doe")
cy.contains("button", "Create Customer").click()
})
cy.wait("@serverError")
cy.get("[role='dialog']").within(() => {
cy.contains("Failed to create customer").should("be.visible")
})
})
it("should handle 422 validation error from server", () => {
cy.intercept("POST", "**/api/customers", {
statusCode: 422,
body: {
success: false,
message: "The email has already been taken.",
errors: { email: ["The email has already been taken."] },
},
}).as("validationError")
cy.get("[role='dialog']").within(() => {
cy.get("input[name='first_name']").type("John")
cy.get("input[name='last_name']").type("Doe")
cy.get("input[name='email']").type("existing@example.com")
cy.contains("button", "Create Customer").click()
})
cy.wait("@validationError")
cy.get("[role='dialog']").within(() => {
cy.contains("Failed to create customer").should("be.visible")
})
})
it("should re-enable submit button after a failed request", () => {
cy.intercept("POST", "**/api/customers", {
statusCode: 422,
body: {
success: false,
message: "Validation failed",
errors: {},
},
}).as("failedRequest")
cy.get("[role='dialog']").within(() => {
cy.get("input[name='first_name']").type("John")
cy.get("input[name='last_name']").type("Doe")
cy.contains("button", "Create Customer").click()
})
cy.wait("@failedRequest")
cy.get("[role='dialog']").within(() => {
cy.contains("button", "Create Customer").should("not.be.disabled")
})
})
})
})
+347
View File
@@ -0,0 +1,347 @@
describe("Customer Form", () => {
beforeEach(() => {
// Authenticate via API and set cookies
cy.login()
// Intercept lookup APIs with fixture data
cy.fixture("customers").then((data) => {
cy.intercept("GET", "**/api/referral-sources", {
statusCode: 200,
body: data.referral_sources,
}).as("getReferralSources")
cy.intercept("GET", "**/api/payment-terms", {
statusCode: 200,
body: data.payment_terms,
}).as("getPaymentTerms")
cy.intercept("GET", "**/api/countries", {
statusCode: 200,
body: data.countries,
}).as("getCountries")
cy.intercept("GET", "**/api/states", {
statusCode: 200,
body: data.states,
}).as("getStates")
// Intercept customer list (GET) for the data table
cy.intercept("GET", "**/api/customers*", {
statusCode: 200,
body: { success: true, data: { data: [], pagination: { total: 0 } } },
}).as("getCustomers")
})
cy.visit("/sales/customers")
})
function openCustomerDialog() {
cy.contains("button", "Create Customer").click()
cy.get("[role='dialog']").should("be.visible")
}
// ── Rendering ──
it("should open the create customer dialog", () => {
openCustomerDialog()
cy.get("[role='dialog']").within(() => {
cy.contains("Create Customer").should("exist")
})
})
it("should display all form fields", () => {
openCustomerDialog()
cy.get("[role='dialog']").within(() => {
// Text fields
cy.get("input[name='first_name']").should("exist")
cy.get("input[name='last_name']").should("exist")
cy.get("input[name='company_name']").should("exist")
cy.get("input[name='email']").should("exist")
cy.get("input[name='phone']").should("exist")
cy.get("input[name='alternate_phone']").should("exist")
cy.get("input[name='address_line_1']").should("exist")
cy.get("input[name='address_line_2']").should("exist")
cy.get("input[name='city']").should("exist")
cy.get("input[name='zip_code']").should("exist")
// Labels
cy.contains("label", "First Name").should("exist")
cy.contains("label", "Last Name").should("exist")
cy.contains("label", "Email").should("exist")
cy.contains("label", "Salutation").should("exist")
cy.contains("label", "Customer Type").should("exist")
cy.contains("label", "Referral Source").should("exist")
cy.contains("label", "Payment Terms").should("exist")
cy.contains("label", "Country").should("exist")
cy.contains("label", "State").should("exist")
})
})
// ── Validation ──
it("should show validation errors for required fields", () => {
openCustomerDialog()
cy.get("[role='dialog']").within(() => {
cy.contains("button", "Create Customer").click()
// first_name and last_name are required
cy.contains("First name is required").should("be.visible")
cy.contains("Last name is required").should("be.visible")
})
})
it("should show email validation error for invalid email", () => {
openCustomerDialog()
cy.get("[role='dialog']").within(() => {
cy.get("input[name='email']").type("not-an-email")
cy.contains("button", "Create Customer").click()
cy.contains("Enter a valid email address").should("be.visible")
})
})
it("should not show email error when email is empty", () => {
openCustomerDialog()
cy.get("[role='dialog']").within(() => {
cy.get("input[name='first_name']").type("John")
cy.get("input[name='last_name']").type("Doe")
cy.contains("button", "Create Customer").click()
cy.contains("Enter a valid email address").should("not.exist")
})
})
// ── Text input ──
it("should fill in text fields", () => {
openCustomerDialog()
cy.get("[role='dialog']").within(() => {
cy.get("input[name='first_name']").type("John").should("have.value", "John")
cy.get("input[name='last_name']").type("Doe").should("have.value", "Doe")
cy.get("input[name='company_name']").type("Acme Corp").should("have.value", "Acme Corp")
cy.get("input[name='email']").type("john@example.com").should("have.value", "john@example.com")
cy.get("input[name='phone']").type("0501234567").should("have.value", "0501234567")
cy.get("input[name='address_line_1']").type("123 Main St").should("have.value", "123 Main St")
cy.get("input[name='city']").type("Dubai").should("have.value", "Dubai")
cy.get("input[name='zip_code']").type("00000").should("have.value", "00000")
})
})
// ── Select fields ──
it("should select a salutation from the dropdown", () => {
openCustomerDialog()
cy.get("[role='dialog']").within(() => {
// Click the Salutation select trigger
cy.contains("label", "Salutation")
.parent()
.find("[role='combobox'], button[data-slot='select-trigger']")
.click()
})
// Select option from the popover (may render outside the dialog)
cy.get("[role='option'], [role='listbox'] [data-value='Mr']")
.contains("Mr")
.click()
})
it("should select a customer type", () => {
openCustomerDialog()
cy.get("[role='dialog']").within(() => {
cy.contains("label", "Customer Type")
.parent()
.find("[role='combobox'], button[data-slot='select-trigger']")
.click()
})
cy.get("[role='option']").contains("Individual").click()
})
// ── Async select (Combobox) fields ──
it("should load and select a referral source", () => {
openCustomerDialog()
cy.wait("@getReferralSources")
cy.get("[role='dialog']").within(() => {
cy.contains("label", "Referral Source")
.parent()
.find("input")
.click()
.type("Google")
})
cy.get("[role='option']").contains("Google").click()
})
it("should load and select a country", () => {
openCustomerDialog()
cy.wait("@getCountries")
cy.get("[role='dialog']").within(() => {
cy.contains("label", "Country")
.parent()
.find("input")
.click()
.type("United")
})
cy.get("[role='option']").contains("United Arab Emirates").click()
})
// ── Successful submission ──
it("should submit the form successfully with required fields", () => {
cy.fixture("customers").then((data) => {
cy.intercept("POST", "**/api/customers", {
statusCode: 201,
body: data.customer_created,
}).as("createCustomer")
})
openCustomerDialog()
cy.get("[role='dialog']").within(() => {
cy.get("input[name='first_name']").type("John")
cy.get("input[name='last_name']").type("Doe")
cy.contains("button", "Create Customer").click()
})
cy.wait("@createCustomer").its("request.body").should((body) => {
expect(body.first_name).to.eq("John")
expect(body.last_name).to.eq("Doe")
})
})
it("should submit a fully filled form", () => {
cy.fixture("customers").then((data) => {
cy.intercept("POST", "**/api/customers", {
statusCode: 201,
body: data.customer_created,
}).as("createCustomer")
})
openCustomerDialog()
// Wait for async data
cy.wait("@getReferralSources")
cy.wait("@getPaymentTerms")
cy.wait("@getCountries")
cy.wait("@getStates")
cy.get("[role='dialog']").within(() => {
// Text fields
cy.get("input[name='first_name']").type("John")
cy.get("input[name='last_name']").type("Doe")
cy.get("input[name='company_name']").type("Doe Holdings")
cy.get("input[name='email']").type("john@example.com")
cy.get("input[name='phone']").type("0501234567")
cy.get("input[name='alternate_phone']").type("0551234567")
cy.get("input[name='address_line_1']").type("Street 10")
cy.get("input[name='address_line_2']").type("Near Central Plaza")
cy.get("input[name='city']").type("Dubai")
cy.get("input[name='zip_code']").type("00000")
// Submit
cy.contains("button", "Create Customer").click()
})
cy.wait("@createCustomer").its("request.body").should((body) => {
expect(body.first_name).to.eq("John")
expect(body.last_name).to.eq("Doe")
expect(body.company_name).to.eq("Doe Holdings")
expect(body.email).to.eq("john@example.com")
expect(body.phone).to.eq("0501234567")
})
})
// ── Error handling ──
it("should display API error on submission failure", () => {
cy.intercept("POST", "**/api/customers", {
statusCode: 422,
body: {
success: false,
message: "The given data was invalid.",
errors: { email: ["The email has already been taken."] },
},
}).as("createCustomerFail")
openCustomerDialog()
cy.get("[role='dialog']").within(() => {
cy.get("input[name='first_name']").type("John")
cy.get("input[name='last_name']").type("Doe")
cy.get("input[name='email']").type("john@example.com")
cy.contains("button", "Create Customer").click()
})
cy.wait("@createCustomerFail")
cy.get("[role='dialog']").within(() => {
cy.contains("Failed to create customer").should("be.visible")
})
})
it("should show loading state while submitting", () => {
cy.intercept("POST", "**/api/customers", {
statusCode: 201,
body: { success: true, data: { id: 1 } },
delay: 1000,
}).as("createCustomerSlow")
openCustomerDialog()
cy.get("[role='dialog']").within(() => {
cy.get("input[name='first_name']").type("John")
cy.get("input[name='last_name']").type("Doe")
cy.contains("button", "Create Customer").click()
// Button should show loading text and be disabled
cy.contains("button", "Creating...").should("be.visible").and("be.disabled")
})
})
// ── Form reset after success ──
it("should reset the form after successful submission", () => {
cy.fixture("customers").then((data) => {
cy.intercept("POST", "**/api/customers", {
statusCode: 201,
body: data.customer_created,
}).as("createCustomer")
})
openCustomerDialog()
cy.get("[role='dialog']").within(() => {
cy.get("input[name='first_name']").type("John")
cy.get("input[name='last_name']").type("Doe")
cy.contains("button", "Create Customer").click()
})
cy.wait("@createCustomer")
// After success, re-open the dialog and verify fields are empty
openCustomerDialog()
cy.get("[role='dialog']").within(() => {
cy.get("input[name='first_name']").should("have.value", "")
cy.get("input[name='last_name']").should("have.value", "")
})
})
})
+47
View File
@@ -0,0 +1,47 @@
{
"referral_sources": {
"success": true,
"data": {
"data": [
{ "id": 1, "name": "Google" },
{ "id": 2, "name": "Friend Referral" },
{ "id": 3, "name": "Social Media" }
]
}
},
"payment_terms": {
"success": true,
"data": {
"data": [
{ "id": 1, "name": "Net 30" },
{ "id": 2, "name": "Net 60" },
{ "id": 3, "name": "Due on Receipt" }
]
}
},
"countries": {
"success": true,
"data": [
{ "id": 1, "name": "United Arab Emirates" },
{ "id": 2, "name": "Saudi Arabia" },
{ "id": 3, "name": "United States" }
]
},
"states": {
"success": true,
"data": [
{ "id": 1, "name": "Dubai" },
{ "id": 2, "name": "Abu Dhabi" },
{ "id": 3, "name": "Sharjah" }
]
},
"customer_created": {
"success": true,
"data": {
"id": 101,
"first_name": "John",
"last_name": "Doe",
"email": "john@example.com"
}
}
}
+33
View File
@@ -0,0 +1,33 @@
/// <reference types="cypress" />
declare global {
namespace Cypress {
interface Chainable {
/**
* Log in via the API and set auth cookies so the app
* recognises the user as authenticated.
*/
login(email?: string, password?: string): Chainable<void>
}
}
}
Cypress.Commands.add("login", (email?: string, password?: string) => {
const userEmail = email ?? Cypress.env("TEST_USER_EMAIL") ?? "admin@admin.com"
const userPassword = password ?? Cypress.env("TEST_USER_PASSWORD") ?? "12345678"
cy.request({
method: "POST",
url: `${Cypress.env("API_URL") ?? "http://localhost:8000"}/api/login`,
body: { email: userEmail, password: userPassword },
}).then((response) => {
const { token, user } = response.body
cy.setCookie("auth_token", token, { path: "/" })
cy.setCookie("auth_user", encodeURIComponent(JSON.stringify(user)), {
path: "/",
})
})
})
export {}
+1
View File
@@ -0,0 +1 @@
import "./commands"
+15
View File
@@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["ES2017", "DOM"],
"types": ["cypress"],
"moduleResolution": "bundler",
"module": "ESNext",
"strict": true,
"baseUrl": ".",
"paths": {
"@/*": ["../../*"]
}
},
"include": ["**/*.ts", "../support/**/*.ts"]
}