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