Ctrl K
ComunidadGuías…

e2e/approved-part-recovery.spec.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import AxeBuilder from "@axe-core/playwright";
import { expect, test } from "@playwright/test";
import { randomUUID } from "node:crypto";
import { openProject } from "./workspace-controls";

for (const height of [844, 600]) test(`failed part save keeps its explanation and retry visible at 390x${height}`, async ({ page }, info) => {
  await page.setViewportSize({ width: 390, height });
  await page.emulateMedia({ reducedMotion: "reduce" });
  await page.goto("/");
  await page.getByLabel("Workspace password").fill("demo-password-please-change");
  await page.getByRole("button", { name: "Sign in", exact: true }).click();
  await expect(page.getByRole("heading", { name: "Projects", exact: true })).toBeVisible();

  const id = `synthetic-recovery-${randomUUID()}`;
  const revisionId = `${id}-r1`;
  const name = `Synthetic recovery project ${height}`;
  const csrf = (await page.context().cookies()).find(cookie => cookie.name === "forge_csrf")!.value;
  const created = await page.request.post("/api/v1/projects/with-initial-revision", {
    headers: { "x-csrf-token": csrf, "idempotency-key": randomUUID() },
    data: { project: { id, name, status: "planned" }, revision: { id: revisionId, name: "Initial", status: "concept", fabricationRoute: "none" } },
  });
  expect(created.ok(), await created.text()).toBe(true);
  await page.reload();
  await openProject(page, name, "Parts");
  await page.getByRole("button", { name: "Add first part", exact: true }).click();
  const dialog = page.getByRole("dialog", { name: "Add a part", exact: true });
  const partName = "Synthetic panel mounting clips";
  const note = "Check clip width against the panel before sourcing.";
  await dialog.getByLabel("Part name", { exact: true }).fill(partName);
  await dialog.getByLabel("Amount needed", { exact: true }).fill("4");
  await dialog.getByRole("textbox", { name: "Specification (optional)", exact: true }).fill(note);
  await dialog.getByRole("button", { name: "Alternatives and notes", exact: true }).click();
  await dialog.getByLabel("How will you use it?", { exact: true }).selectOption("reusable");
  await dialog.getByRole("checkbox", { name: "Mark as optional", exact: true }).check();
  const attempts: unknown[] = [];
  await page.route(`**/api/v1/project-revisions/${revisionId}/bom`, route => {
    if (route.request().method() !== "POST") return route.continue();
    attempts.push(route.request().postDataJSON());
    return route.fulfill({ status: 409, json: { error: { code: "version_conflict", message: "The project changed. Your part has not been saved." } } });
  });
  await dialog.getByRole("button", { name: "Add part", exact: true }).click();
  const alert = dialog.getByRole("alert");
  const retry = dialog.getByRole("button", { name: "Try saving again", exact: true });
  await expect(alert).toContainText("Couldn’t save. Your entries are kept.");
  await expect(alert).toContainText("The project changed. Your part has not been saved.");
  // These checks must run before any operation that might scroll the form.
  await expect(alert).toBeInViewport({ ratio: 1 });
  await expect(retry).toBeInViewport({ ratio: 1 });
  const geometry = await dialog.evaluate(element => {
    const bounds = element.getBoundingClientRect();
    const fields = element.querySelector(".add-part-fields")!.getBoundingClientRect();
    const recovery = element.querySelector(".add-part-recovery")!.getBoundingClientRect();
    const error = element.querySelector('[role="alert"]')!;
    return { x: bounds.x, y: bounds.y, right: bounds.right, bottom: bounds.bottom, fieldsBottom: fields.bottom, recoveryTop: recovery.top,
      errorFits: error.scrollHeight <= error.clientHeight + 1 && error.scrollWidth <= error.clientWidth + 1,
      horizontalOverflow: document.documentElement.scrollWidth > innerWidth };
  });
  expect(geometry).toMatchObject({ errorFits: true, horizontalOverflow: false });
  expect(geometry.x).toBeGreaterThanOrEqual(0);
  expect(geometry.y).toBeGreaterThanOrEqual(0);
  expect(geometry.right).toBeLessThanOrEqual(390);
  expect(geometry.bottom).toBeLessThanOrEqual(height);
  expect(geometry.fieldsBottom).toBeLessThanOrEqual(geometry.recoveryTop);
  expect((await retry.boundingBox())!.height).toBeGreaterThanOrEqual(44);
  await page.screenshot({ path: info.outputPath(`add-part-recovery-390x${height}.png`), animations: "disabled" });
  await expect(dialog.getByLabel("Part name", { exact: true })).toHaveValue(partName);
  await expect(dialog.getByLabel("Amount needed", { exact: true })).toHaveValue("4");
  await expect(dialog.getByLabel("Unit", { exact: true })).toHaveValue("each");
  await expect(dialog.getByRole("textbox", { name: "Specification (optional)", exact: true })).toHaveValue(note);
  await expect(dialog.getByLabel("How will you use it?", { exact: true })).toHaveValue("reusable");
  await expect(dialog.getByRole("checkbox", { name: "Mark as optional", exact: true })).toBeChecked();
  expect((await new AxeBuilder({ page }).include('[role="dialog"]').analyze()).violations).toEqual([]);
  await retry.click();
  await expect.poll(() => attempts.length).toBe(2);
  expect(attempts[1]).toEqual(attempts[0]);
  const lines = await page.request.get(`/api/v1/project-revisions/${revisionId}/bom`);
  expect(lines.ok()).toBe(true);
  expect(await lines.json()).toEqual([]);
});