diff --git a/modules/customers/src/web/list/ui/blocks/customers-grid/use-customer-grid-columns.tsx b/modules/customers/src/web/list/ui/blocks/customers-grid/use-customer-grid-columns.tsx index 4faf8437..ba39cd85 100644 --- a/modules/customers/src/web/list/ui/blocks/customers-grid/use-customer-grid-columns.tsx +++ b/modules/customers/src/web/list/ui/blocks/customers-grid/use-customer-grid-columns.tsx @@ -179,7 +179,7 @@ export function useCustomersGridColumns( primaryActions.push({ id: "edit", label: t("list.actions.edit"), - icon: , + icon: , onSelect: () => actionHandlers.onEditClick?.(customer), disabled: !canEdit, }); diff --git a/modules/customers/src/web/view/ui/pages/customer-view-page.tsx b/modules/customers/src/web/view/ui/pages/customer-view-page.tsx index e14dff99..7862900b 100644 --- a/modules/customers/src/web/view/ui/pages/customer-view-page.tsx +++ b/modules/customers/src/web/view/ui/pages/customer-view-page.tsx @@ -110,7 +110,9 @@ export const CustomerViewPage = () => {
{t("view.fields.name")}
-
{viewCtrl.customerData?.name}
+
+ {viewCtrl.customerData?.name} +
diff --git a/tests/e2e/customers/create-customer.spec.ts b/tests/e2e/customers/create-customer.spec.ts new file mode 100644 index 00000000..9871b70b --- /dev/null +++ b/tests/e2e/customers/create-customer.spec.ts @@ -0,0 +1,44 @@ +import { expect, test } from "@playwright/test"; + +import { loginViaUi } from "../support/login-via-ui"; + +import { createCustomerViaUi } from "./support/create-customer-via-ui"; + +test.beforeEach(async ({ page }) => { + await loginViaUi(page); +}); + +test("CUSTOMERS-CREATE-001: crea un cliente y conserva sus datos tras recargar", async ({ + page, +}, testInfo) => { + const customer = await createCustomerViaUi(page, testInfo); + + await page.reload(); + + await expect(page.locator('input[name="name"]')).toHaveValue(customer.name); + await expect(page.locator('input[name="primaryEmail"]')).toHaveValue(customer.email); +}); + +test("CUSTOMERS-CREATE-002: valida el nombre obligatorio sin enviar el alta", async ({ page }) => { + await page.goto("/customers/create"); + const name = page.locator('input[name="name"]'); + await expect(name).toBeVisible(); + await expect(name).toHaveValue(""); + + let creationRequests = 0; + page.on("request", (request) => { + if (request.method() === "POST" && /\/customers\/?$/.test(new URL(request.url()).pathname)) { + creationRequests += 1; + } + }); + + await page.getByTestId("submit-button").click(); + + await expect(name).toHaveAttribute("aria-invalid", "true"); + await expect( + page.locator('[data-slot="field"]').filter({ has: name }).locator('[data-slot="field-error"]') + ).toBeVisible(); + await expect(page).toHaveURL(/\/customers\/create$/); + // El error visible marca el fin de la validación; sin POST no puede existir un alta. + expect(creationRequests).toBe(0); +}); diff --git a/tests/e2e/customers/customer-navigation.spec.ts b/tests/e2e/customers/customer-navigation.spec.ts new file mode 100644 index 00000000..3e9130c0 --- /dev/null +++ b/tests/e2e/customers/customer-navigation.spec.ts @@ -0,0 +1,33 @@ +import { expect, test } from "@playwright/test"; + +import { loginViaUi } from "../support/login-via-ui"; + +import { createCustomerViaUi } from "./support/create-customer-via-ui"; + +test("CUSTOMERS-NAVIGATION-001: cancelar edición conserva el contexto del listado", async ({ + page, +}, testInfo) => { + await loginViaUi(page); + const customer = await createCustomerViaUi(page, testInfo); + const listPath = "/customers?page=1&pageSize=25&sortField=name&sortDirection=desc"; + + await page.goto(listPath); + const editAction = page.getByTestId(`customer-edit-${customer.id}`); + await expect(editAction).toBeVisible(); + await editAction.click(); + + await expect(page).toHaveURL( + (url) => + url.pathname === `/customers/${customer.id}/edit` && + url.searchParams.get("returnTo") === listPath + ); + await expect(page.locator('input[name="name"]')).toHaveValue(customer.name); + + // Recargar demuestra que returnTo no depende de estado de navegación en memoria. + await page.reload(); + await expect(page.locator('input[name="name"]')).toHaveValue(customer.name); + await page.getByTestId("cancel-button").click(); + + await expect(page).toHaveURL((url) => `${url.pathname}${url.search}` === listPath); + await expect(editAction).toBeVisible(); +}); diff --git a/tests/e2e/customers/support/create-customer-via-ui.ts b/tests/e2e/customers/support/create-customer-via-ui.ts new file mode 100644 index 00000000..b59c264c --- /dev/null +++ b/tests/e2e/customers/support/create-customer-via-ui.ts @@ -0,0 +1,27 @@ +import { randomUUID } from "node:crypto"; + +import type { Page, TestInfo } from "@playwright/test"; +import { expect } from "@playwright/test"; + +export async function createCustomerViaUi(page: Page, testInfo: TestInfo) { + const suffix = `${Date.now()}-${testInfo.workerIndex}-${testInfo.retry}-${randomUUID().slice(0, 8)}`; + const name = `zzzz E2E customer ${suffix}`; + const email = `e2e-${suffix}@example.invalid`; + + await page.goto("/customers/create"); + await page.locator('input[name="name"]').fill(name); + await page.locator('input[name="primaryEmail"]').fill(email); + await page.getByTestId("submit-button").click(); + + // La navegación solo ocurre tras la respuesta real de creación. + await expect(page).toHaveURL(/\/customers\/[^/]+\/edit\?returnTo=/); + await expect(page.locator('input[name="name"]')).toHaveValue(name); + await expect(page.locator('input[name="primaryEmail"]')).toHaveValue(email); + + const editUrl = new URL(page.url()); + const id = editUrl.pathname.split("/")[2]; + if (!id) { + throw new Error("Missing customer ID in edit URL"); + } + return { id, name, email }; +} diff --git a/tests/e2e/customers/update-customer.spec.ts b/tests/e2e/customers/update-customer.spec.ts new file mode 100644 index 00000000..ff133090 --- /dev/null +++ b/tests/e2e/customers/update-customer.spec.ts @@ -0,0 +1,30 @@ +import { expect, test } from "@playwright/test"; + +import { loginViaUi } from "../support/login-via-ui"; + +import { createCustomerViaUi } from "./support/create-customer-via-ui"; + +test("CUSTOMERS-UPDATE-001: modifica un cliente propio y conserva el cambio tras recargar", async ({ + page, +}, testInfo) => { + await loginViaUi(page); + const customer = await createCustomerViaUi(page, testInfo); + const updatedName = `${customer.name} updated`; + + await page.locator('input[name="name"]').fill(updatedName); + const responsePromise = page.waitForResponse( + (response) => + response.request().method() === "PUT" && + new URL(response.url()).pathname.endsWith(`/customers/${customer.id}`) + ); + await page.getByTestId("submit-button").click(); + const response = await responsePromise; + expect(response.status()).toBe(200); + + await expect(page).toHaveURL((url) => url.pathname === `/customers/${customer.id}/`); + await expect(page.locator("#customer-view-name")).toHaveText(updatedName); + + await page.reload(); + + await expect(page.locator("#customer-view-name")).toHaveText(updatedName); +}); diff --git a/tests/e2e/identity/login.spec.ts b/tests/e2e/identity/login.spec.ts index 6235d667..89172602 100644 --- a/tests/e2e/identity/login.spec.ts +++ b/tests/e2e/identity/login.spec.ts @@ -1,6 +1,6 @@ import { expect, test } from "@playwright/test"; -import { loginViaUi } from "./support/login-via-ui"; +import { loginViaUi } from "../support/login-via-ui"; test("IDENTITY-LOGIN-001: mantiene la sesión después de iniciar sesión y recargar", async ({ page, diff --git a/tests/e2e/identity/logout.spec.ts b/tests/e2e/identity/logout.spec.ts index b7fd37ff..fdbe4b87 100644 --- a/tests/e2e/identity/logout.spec.ts +++ b/tests/e2e/identity/logout.spec.ts @@ -1,6 +1,6 @@ import { expect, test } from "@playwright/test"; -import { loginViaUi } from "./support/login-via-ui"; +import { loginViaUi } from "../support/login-via-ui"; test("IDENTITY-LOGOUT-001: cierra la sesión y bloquea el acceso posterior a rutas privadas", async ({ page, diff --git a/tests/e2e/identity/support/login-via-ui.ts b/tests/e2e/support/login-via-ui.ts similarity index 72% rename from tests/e2e/identity/support/login-via-ui.ts rename to tests/e2e/support/login-via-ui.ts index 49fec401..2d4302ff 100644 --- a/tests/e2e/identity/support/login-via-ui.ts +++ b/tests/e2e/support/login-via-ui.ts @@ -1,13 +1,12 @@ -import { type Page, expect } from "@playwright/test"; +import type { Page } from "@playwright/test"; +import { expect } from "@playwright/test"; export async function loginViaUi(page: Page): Promise { const email = process.env.E2E_USER_EMAIL; const password = process.env.E2E_USER_PASSWORD; if (!(email && password)) { - throw new Error( - "Identity E2E requires E2E_USER_EMAIL and E2E_USER_PASSWORD in apps/server/.env.e2e." - ); + throw new Error("E2E requires E2E_USER_EMAIL and E2E_USER_PASSWORD in apps/server/.env.e2e."); } await page.goto("/login");