feat(tests): agregar pruebas E2E para la gestión de líneas en proformas, incluyendo adición, edición y eliminación

This commit is contained in:
David Arranz 2026-09-14 19:49:19 +02:00
parent b6aa20e52b
commit 026e373f52
6 changed files with 190 additions and 3 deletions

View File

@ -0,0 +1,42 @@
import assert from "node:assert/strict";
import test from "node:test";
import { renderToStaticMarkup } from "react-dom/server";
import { LineEditor } from "./line-editor";
test("asocia el selector de añadir línea al botón de añadir al final", () => {
const html = renderToStaticMarkup(
<LineEditor
actionsLabel="Acciones"
addAtEndLabel="Añadir al final"
addAtEndTestId="proforma-add-item"
addAtStartLabel="Añadir al inicio"
columns={[]}
duplicateLabel="Duplicar"
getLineKey={(line) => line}
insertAfterLabel="Insertar después"
insertBeforeLabel="Insertar antes"
lines={[] as string[]}
moveDownLabel="Mover abajo"
moveUpLabel="Mover arriba"
onAddAfter={() => undefined}
onAddAtEnd={() => undefined}
onAddAtStart={() => undefined}
onAddBefore={() => undefined}
onDuplicate={() => undefined}
onMoveDown={() => undefined}
onMoveUp={() => undefined}
onRemove={() => undefined}
removeLabel="Eliminar"
title="Líneas"
/>
);
const addAtStartLabelIndex = html.indexOf("Añadir al inicio");
const addAtEndTestIdIndex = html.indexOf('data-testid="proforma-add-item"');
const addAtEndLabelIndex = html.indexOf("Añadir al final");
assert.ok(addAtStartLabelIndex < addAtEndTestIdIndex);
assert.ok(addAtEndTestIdIndex < addAtEndLabelIndex);
});

View File

@ -38,6 +38,7 @@ export interface LineEditorProps<TLine> {
description?: React.ReactNode;
lines: TLine[];
getLineKey: (line: TLine, index: number) => React.Key;
getLineTestId?: (line: TLine, index: number) => string;
columns: LineEditorColumn<TLine>[];
@ -64,6 +65,10 @@ export interface LineEditorProps<TLine> {
moveDownLabel: string;
removeLabel: string;
addAtEndTestId?: string;
getLineActionsTestId?: (line: TLine, index: number) => string;
getLineRemoveTestId?: (line: TLine, index: number) => string;
disabled?: boolean;
className?: string;
}
@ -73,6 +78,7 @@ export const LineEditor = <TLine,>({
description,
lines,
getLineKey,
getLineTestId,
columns,
@ -99,6 +105,10 @@ export const LineEditor = <TLine,>({
moveDownLabel,
removeLabel,
addAtEndTestId,
getLineActionsTestId,
getLineRemoveTestId,
disabled = false,
className,
@ -124,6 +134,7 @@ export const LineEditor = <TLine,>({
</Button>
<Button
data-testid={addAtEndTestId}
disabled={disabled}
onClick={onAddAtEnd}
size="sm"
@ -171,6 +182,7 @@ export const LineEditor = <TLine,>({
"group",
hasLineError && "bg-destructive/5 hover:bg-destructive/5"
)}
data-testid={getLineTestId?.(line, index)}
>
<TableCell className="text-muted-foreground text-sm font-medium align-top pt-4">
{index + 1}
@ -189,6 +201,7 @@ export const LineEditor = <TLine,>({
<Button
aria-label={actionsLabel}
className="size-8"
data-testid={getLineActionsTestId?.(line, index)}
size="icon"
type="button"
variant="ghost"
@ -235,6 +248,7 @@ export const LineEditor = <TLine,>({
<DropdownMenuItem
className="text-destructive focus:text-destructive"
data-testid={getLineRemoveTestId?.(line, index)}
onClick={() => onRemove(index)}
>
<Trash2 className="mr-2 h-4 w-4" />

View File

@ -260,6 +260,7 @@ export const ProformaLineEditor = ({
<LineEditor
actionsLabel={t("common.actions", "Acciones")}
addAtEndLabel={t("common.add_at_end", "Al final")}
addAtEndTestId="proforma-add-item"
addAtStartLabel={t("common.add_at_start", "Al inicio")}
className={className}
columns={columns}
@ -269,11 +270,14 @@ export const ProformaLineEditor = ({
)}
disabled={disabled}
duplicateLabel={t("common.duplicate", "Duplicar")}
getLineActionsTestId={(field) => `proforma-item-actions-${field.fieldId}`}
getLineErrorMessage={(_, index) => {
const message = getItemErrorMessage(index);
return message ? t(message, message) : undefined;
}}
getLineKey={(field) => field.fieldId}
getLineRemoveTestId={(field) => `proforma-item-delete-${field.fieldId}`}
getLineTestId={(field) => `proforma-item-row-${field.fieldId}`}
insertAfterLabel={t("common.insert_after", "Insertar después")}
insertBeforeLabel={t("common.insert_before", "Insertar antes")}
lines={fields}

View File

@ -50,7 +50,7 @@ test("conecta la alta rápida directa y la alta rápida desde selector con callb
namedExports: {
CancelActionButton: () => null,
PercentageField: () => null,
RdxEditorStickyFooter: () => null,
RdxEditorActionBar: () => null,
RdxPageContainer: ({ children }: { children: ReactNode }) => children,
RdxPageFrame: ({
children,

View File

@ -83,7 +83,7 @@ test("RdxFormGrid mantiene la retícula base y combina className", () => {
assert.match(html, /custom-grid/);
});
test("RdxEditorStickyFooter renderiza regiones presentes sin wrappers vacíos", () => {
test("RdxEditorActionBar renderiza regiones presentes sin wrappers vacíos", () => {
const html = renderToStaticMarkup(
<RdxEditorActionBar
className="custom-sticky"
@ -104,7 +104,7 @@ test("RdxEditorStickyFooter renderiza regiones presentes sin wrappers vacíos",
assert.ok(html.indexOf("Cancel") < html.indexOf("Save"));
});
test("RdxEditorStickyFooter omite regiones opcionales vacías", () => {
test("RdxEditorActionBar omite regiones opcionales vacías", () => {
const html = renderToStaticMarkup(
<RdxEditorActionBar primary={<button type="submit">Save</button>} />
);

View File

@ -0,0 +1,127 @@
import type { Page, TestInfo } from "@playwright/test";
import { expect, test } from "@playwright/test";
import { loginViaUi } from "../../support/login-via-ui";
import { createProformaViaUi } from "./support/create-proforma-via-ui";
const itemRows = (page: Page) => page.getByTestId(/^proforma-item-row-/);
const saveProforma = async (page: Page, id: string) => {
const responsePromise = page.waitForResponse(
(response) =>
response.request().method() === "PUT" &&
new URL(response.url()).pathname.endsWith(`/proformas/${id}`)
);
await page.getByTestId("proforma-update-submit").click();
expect((await responsePromise).status()).toBe(200);
};
const addItem = async (page: Page, description: string) => {
await page.getByTestId("proforma-add-item").click();
const row = itemRows(page).last();
await expect(row).toBeVisible();
const descriptionInput = row.locator('[name$=".description"]');
await descriptionInput.fill(description);
};
const expectItemDescriptions = async (page: Page, descriptions: string[]) => {
const rows = itemRows(page);
await expect(rows).toHaveCount(descriptions.length);
for (const [index, description] of descriptions.entries()) {
await expect(rows.nth(index).locator('[name$=".description"]')).toHaveValue(description);
}
};
const createProformaWithItemsViaUi = async (
page: Page,
testInfo: TestInfo,
descriptions: string[]
) => {
const proforma = await createProformaViaUi(page, testInfo);
for (const description of descriptions) {
await addItem(page, description);
}
await saveProforma(page, proforma.id);
return proforma;
};
test("CUSTOMER-INVOICES-PROFORMA-ITEMS-001: añade una línea a un draft vacío y conserva sus datos", async ({
page,
}, testInfo) => {
await loginViaUi(page);
const proforma = await createProformaViaUi(page, testInfo);
const description = `E2E ITEM ADD ${proforma.token}`;
await addItem(page, description);
await saveProforma(page, proforma.id);
await expectItemDescriptions(page, [description]);
await page.reload();
await expectItemDescriptions(page, [description]);
});
test("CUSTOMER-INVOICES-PROFORMA-ITEMS-002: edita una línea persistida sin duplicarla", async ({
page,
}, testInfo) => {
await loginViaUi(page);
const original = `E2E ITEM ORIGINAL ${testInfo.testId}`;
const proforma = await createProformaWithItemsViaUi(page, testInfo, [original]);
const updated = `E2E ITEM UPDATED ${proforma.token}`;
const row = itemRows(page).first();
await row.locator('[name$=".description"]').fill(updated);
await saveProforma(page, proforma.id);
await expectItemDescriptions(page, [updated]);
await page.reload();
await expectItemDescriptions(page, [updated]);
});
test("CUSTOMER-INVOICES-PROFORMA-ITEMS-003: elimina la última línea y conserva el draft vacío", async ({
page,
}, testInfo) => {
await loginViaUi(page);
const proforma = await createProformaWithItemsViaUi(page, testInfo, [
`E2E ITEM DELETE ${testInfo.testId}`,
]);
const row = itemRows(page).first();
await row.getByTestId(/^proforma-item-actions-/).click();
await page.getByTestId(/^proforma-item-delete-/).click();
await expect(itemRows(page)).toHaveCount(0);
const requestPromise = page.waitForRequest(
(request) =>
request.method() === "PUT" &&
new URL(request.url()).pathname.endsWith(`/proformas/${proforma.id}`)
);
await saveProforma(page, proforma.id);
expect((await requestPromise).postDataJSON()).toMatchObject({ items: [] });
await page.reload();
await expect(itemRows(page)).toHaveCount(0);
await expect(page.locator('input[name="reference"]')).toBeVisible();
});
test("CUSTOMER-INVOICES-PROFORMA-ITEMS-004: conserva varias líneas y su orden tras recargar", async ({
page,
}, testInfo) => {
await loginViaUi(page);
const proforma = await createProformaViaUi(page, testInfo);
const descriptions = [
`E2E ITEM AAA ${proforma.token}`,
`E2E ITEM BBB ${proforma.token}`,
`E2E ITEM CCC ${proforma.token}`,
];
for (const description of descriptions) {
await addItem(page, description);
}
await saveProforma(page, proforma.id);
await expectItemDescriptions(page, descriptions);
await page.reload();
await expectItemDescriptions(page, descriptions);
});