feat(e2e): agregar pruebas E2E para la creación y actualización de clientes, y mejorar la navegación

This commit is contained in:
David Arranz 2026-09-10 17:32:09 +02:00
parent 04babcc14b
commit 322df28591
9 changed files with 143 additions and 8 deletions

View File

@ -179,7 +179,7 @@ export function useCustomersGridColumns(
primaryActions.push({
id: "edit",
label: t("list.actions.edit"),
icon: <PencilIcon className="size-4" />,
icon: <PencilIcon className="size-4" data-testid={`customer-edit-${customer.id}`} />,
onSelect: () => actionHandlers.onEditClick?.(customer),
disabled: !canEdit,
});

View File

@ -110,7 +110,9 @@ export const CustomerViewPage = () => {
<CardContent className="space-y-4">
<div>
<dt className="text-sm font-medium text-muted-foreground">{t("view.fields.name")}</dt>
<dd className="mt-1 text-base text-foreground">{viewCtrl.customerData?.name}</dd>
<dd className="mt-1 text-base text-foreground" id="customer-view-name">
{viewCtrl.customerData?.name}
</dd>
</div>
<div>
<dt className="text-sm font-medium text-muted-foreground">

View File

@ -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);
});

View File

@ -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();
});

View File

@ -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 };
}

View File

@ -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);
});

View File

@ -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,

View File

@ -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,

View File

@ -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<void> {
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");