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:
parent
b6aa20e52b
commit
026e373f52
@ -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);
|
||||||
|
});
|
||||||
@ -38,6 +38,7 @@ export interface LineEditorProps<TLine> {
|
|||||||
description?: React.ReactNode;
|
description?: React.ReactNode;
|
||||||
lines: TLine[];
|
lines: TLine[];
|
||||||
getLineKey: (line: TLine, index: number) => React.Key;
|
getLineKey: (line: TLine, index: number) => React.Key;
|
||||||
|
getLineTestId?: (line: TLine, index: number) => string;
|
||||||
|
|
||||||
columns: LineEditorColumn<TLine>[];
|
columns: LineEditorColumn<TLine>[];
|
||||||
|
|
||||||
@ -64,6 +65,10 @@ export interface LineEditorProps<TLine> {
|
|||||||
moveDownLabel: string;
|
moveDownLabel: string;
|
||||||
removeLabel: string;
|
removeLabel: string;
|
||||||
|
|
||||||
|
addAtEndTestId?: string;
|
||||||
|
getLineActionsTestId?: (line: TLine, index: number) => string;
|
||||||
|
getLineRemoveTestId?: (line: TLine, index: number) => string;
|
||||||
|
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
@ -73,6 +78,7 @@ export const LineEditor = <TLine,>({
|
|||||||
description,
|
description,
|
||||||
lines,
|
lines,
|
||||||
getLineKey,
|
getLineKey,
|
||||||
|
getLineTestId,
|
||||||
|
|
||||||
columns,
|
columns,
|
||||||
|
|
||||||
@ -99,6 +105,10 @@ export const LineEditor = <TLine,>({
|
|||||||
moveDownLabel,
|
moveDownLabel,
|
||||||
removeLabel,
|
removeLabel,
|
||||||
|
|
||||||
|
addAtEndTestId,
|
||||||
|
getLineActionsTestId,
|
||||||
|
getLineRemoveTestId,
|
||||||
|
|
||||||
disabled = false,
|
disabled = false,
|
||||||
|
|
||||||
className,
|
className,
|
||||||
@ -124,6 +134,7 @@ export const LineEditor = <TLine,>({
|
|||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
data-testid={addAtEndTestId}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onClick={onAddAtEnd}
|
onClick={onAddAtEnd}
|
||||||
size="sm"
|
size="sm"
|
||||||
@ -171,6 +182,7 @@ export const LineEditor = <TLine,>({
|
|||||||
"group",
|
"group",
|
||||||
hasLineError && "bg-destructive/5 hover:bg-destructive/5"
|
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">
|
<TableCell className="text-muted-foreground text-sm font-medium align-top pt-4">
|
||||||
{index + 1}
|
{index + 1}
|
||||||
@ -189,6 +201,7 @@ export const LineEditor = <TLine,>({
|
|||||||
<Button
|
<Button
|
||||||
aria-label={actionsLabel}
|
aria-label={actionsLabel}
|
||||||
className="size-8"
|
className="size-8"
|
||||||
|
data-testid={getLineActionsTestId?.(line, index)}
|
||||||
size="icon"
|
size="icon"
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@ -235,6 +248,7 @@ export const LineEditor = <TLine,>({
|
|||||||
|
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="text-destructive focus:text-destructive"
|
className="text-destructive focus:text-destructive"
|
||||||
|
data-testid={getLineRemoveTestId?.(line, index)}
|
||||||
onClick={() => onRemove(index)}
|
onClick={() => onRemove(index)}
|
||||||
>
|
>
|
||||||
<Trash2 className="mr-2 h-4 w-4" />
|
<Trash2 className="mr-2 h-4 w-4" />
|
||||||
|
|||||||
@ -260,6 +260,7 @@ export const ProformaLineEditor = ({
|
|||||||
<LineEditor
|
<LineEditor
|
||||||
actionsLabel={t("common.actions", "Acciones")}
|
actionsLabel={t("common.actions", "Acciones")}
|
||||||
addAtEndLabel={t("common.add_at_end", "Al final")}
|
addAtEndLabel={t("common.add_at_end", "Al final")}
|
||||||
|
addAtEndTestId="proforma-add-item"
|
||||||
addAtStartLabel={t("common.add_at_start", "Al inicio")}
|
addAtStartLabel={t("common.add_at_start", "Al inicio")}
|
||||||
className={className}
|
className={className}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
@ -269,11 +270,14 @@ export const ProformaLineEditor = ({
|
|||||||
)}
|
)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
duplicateLabel={t("common.duplicate", "Duplicar")}
|
duplicateLabel={t("common.duplicate", "Duplicar")}
|
||||||
|
getLineActionsTestId={(field) => `proforma-item-actions-${field.fieldId}`}
|
||||||
getLineErrorMessage={(_, index) => {
|
getLineErrorMessage={(_, index) => {
|
||||||
const message = getItemErrorMessage(index);
|
const message = getItemErrorMessage(index);
|
||||||
return message ? t(message, message) : undefined;
|
return message ? t(message, message) : undefined;
|
||||||
}}
|
}}
|
||||||
getLineKey={(field) => field.fieldId}
|
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")}
|
insertAfterLabel={t("common.insert_after", "Insertar después")}
|
||||||
insertBeforeLabel={t("common.insert_before", "Insertar antes")}
|
insertBeforeLabel={t("common.insert_before", "Insertar antes")}
|
||||||
lines={fields}
|
lines={fields}
|
||||||
|
|||||||
@ -50,7 +50,7 @@ test("conecta la alta rápida directa y la alta rápida desde selector con callb
|
|||||||
namedExports: {
|
namedExports: {
|
||||||
CancelActionButton: () => null,
|
CancelActionButton: () => null,
|
||||||
PercentageField: () => null,
|
PercentageField: () => null,
|
||||||
RdxEditorStickyFooter: () => null,
|
RdxEditorActionBar: () => null,
|
||||||
RdxPageContainer: ({ children }: { children: ReactNode }) => children,
|
RdxPageContainer: ({ children }: { children: ReactNode }) => children,
|
||||||
RdxPageFrame: ({
|
RdxPageFrame: ({
|
||||||
children,
|
children,
|
||||||
|
|||||||
@ -83,7 +83,7 @@ test("RdxFormGrid mantiene la retícula base y combina className", () => {
|
|||||||
assert.match(html, /custom-grid/);
|
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(
|
const html = renderToStaticMarkup(
|
||||||
<RdxEditorActionBar
|
<RdxEditorActionBar
|
||||||
className="custom-sticky"
|
className="custom-sticky"
|
||||||
@ -104,7 +104,7 @@ test("RdxEditorStickyFooter renderiza regiones presentes sin wrappers vacíos",
|
|||||||
assert.ok(html.indexOf("Cancel") < html.indexOf("Save"));
|
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(
|
const html = renderToStaticMarkup(
|
||||||
<RdxEditorActionBar primary={<button type="submit">Save</button>} />
|
<RdxEditorActionBar primary={<button type="submit">Save</button>} />
|
||||||
);
|
);
|
||||||
|
|||||||
127
tests/e2e/customer-invoices/proformas/proforma-items.spec.ts
Normal file
127
tests/e2e/customer-invoices/proformas/proforma-items.spec.ts
Normal 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);
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user