e2e/shopping-proposal.spec.ts
Rama main · réplica de lectura del repositorio original.
import { expect, test } from "@playwright/test";
import { randomUUID } from "node:crypto";
test("canonical proposal copy and download include hidden selected quotes and unresolved requirements", async ({ page }) => {
await page.addInitScript(() => {
Object.defineProperty(navigator, "clipboard", { configurable: true, value: { writeText: () => Promise.reject(new DOMException("Synthetic clipboard denial", "NotAllowedError")) } });
});
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 csrf = (await page.context().cookies()).find(cookie => cookie.name === "forge_csrf")!.value;
const headers = () => ({ "x-csrf-token": csrf, "idempotency-key": randomUUID() });
const post = async (path: string, data: object) => {
const response = await page.request.post(`/api/v1${path}`, { headers: headers(), data });
expect(response.ok(), await response.text()).toBe(true);
return (await response.json()).data;
};
const id = `proposal-${randomUUID()}`, revision = `${id}-r1`;
const root = `/projects/${id}/revisions/${revision}`;
await post("/projects/with-initial-revision", { project: { id, name: "Synthetic complete shopping proposal", status: "planned" }, revision: { id: revision, name: "Initial", status: "concept", fabricationRoute: "none" } });
const observedAt = new Date().toISOString();
for (const fixture of [
{ name: "Proposal visible connectors", currency: "EUR", packageQuantity: 4, priceMinor: 250, shippingMinor: 100, taxIncluded: "yes" },
{ name: "Proposal hidden spacers", currency: "USD", packageQuantity: 3, priceMinor: 90, taxIncluded: "unknown" },
{ name: "Proposal unresolved bracket" }
]) {
const line = await post(`/project-revisions/${revision}/bom`, { name: fixture.name, requiredQuantity: 7, unit: "each", role: "consumed", optional: false, constraints: {}, alternatives: [] });
if (!fixture.currency) continue;
const quote = await post(`${root}/requirement-offers`, { bomLineId: line.id, expectedBomLineVersion: line.version, supplier: `Synthetic ${fixture.currency} supplier`, title: `Recorded ${fixture.currency} package`, url: `https://supplier.example/${fixture.currency.toLowerCase()}`, packageQuantity: fixture.packageQuantity, packageUnit: "each", priceMinor: fixture.priceMinor, currency: fixture.currency, ...(fixture.shippingMinor === undefined ? {} : { shippingMinor: fixture.shippingMinor }), taxIncluded: fixture.taxIncluded, observedAt, validForDays: 30 });
const chosen = await page.request.put(`/api/v1${root}/offer-choice`, { headers: headers(), data: { bomLineId: line.id, offerId: quote.id, expectedVersion: 0, expectedBomLineVersion: line.version, confirmedFit: true } });
expect(chosen.ok(), await chosen.text()).toBe(true);
}
await page.goto(`/#/projects/${id}/offers`); await page.reload();
await page.getByLabel("Search quote requirements").fill("Proposal visible connectors");
await expect(page.locator(".sourcing-requirement")).toHaveCount(1);
await expect(page.locator(".sourcing-requirement")).toContainText("Proposal visible connectors");
await expect(page.locator(".sourcing-requirement")).not.toContainText("Proposal hidden spacers");
const reads: URL[] = [];
page.on("request", request => {
const url = new URL(request.url());
if (request.method() === "GET" && url.pathname.endsWith("/sourcing") && url.searchParams.get("limit") === "100") reads.push(url);
});
const copy = page.getByRole("button", { name: "Copy proposal", exact: true });
await expect(copy).toBeEnabled(); await copy.click();
const manual = page.getByRole("textbox", { name: "Shopping proposal text", exact: true });
await expect(manual).toBeVisible(); await expect(manual).toBeFocused();
await expect(page.getByText(/Clipboard access was unavailable/u)).toBeVisible();
const copied = await manual.inputValue();
const verify = (text: string) => {
expect(text).toContain("3 requirements in the full revision");
for (const name of ["Proposal visible connectors", "Proposal hidden spacers", "Proposal unresolved bracket"]) expect(text).toContain(name);
expect(text).toContain("Recorded EUR package"); expect(text).toContain("Recorded USD package");
expect(text).toContain("2 packs supply 8 each"); expect(text).toContain("3 packs supply 9 each");
expect(text).toMatch(/6\.00 EUR/u); expect(text).toMatch(/2\.70 USD/u);
expect(text).toContain("shipping incomplete"); expect(text).toContain("tax not fully confirmed");
expect(text).toContain("https://supplier.example/eur"); expect(text).toContain("https://supplier.example/usd");
expect(text).toContain(`Observed: ${observedAt}`); expect(text).toContain("No supplier observation selected.");
expect(text).toContain("Nothing is purchased, reserved, received or physically confirmed");
};
verify(copied);
expect(await manual.evaluate(element => { const field = element as HTMLTextAreaElement; return field.selectionStart === 0 && field.selectionEnd === field.value.length; })).toBe(true);
const downloading = page.waitForEvent("download");
await page.getByRole("button", { name: "Download proposal text", exact: true }).click();
const download = await downloading;
expect(download.suggestedFilename()).toMatch(/^shopping-proposal-\d{4}-\d{2}-\d{2}\.txt$/u);
const stream = await download.createReadStream(); expect(stream).not.toBeNull();
const chunks: Buffer[] = []; for await (const chunk of stream!) chunks.push(Buffer.from(chunk));
verify(Buffer.concat(chunks).toString("utf8"));
expect(reads).toHaveLength(4);
expect(reads.every(url => url.searchParams.get("filter") === "all" && !url.searchParams.has("query"))).toBe(true);
await expect(page.getByLabel("Search quote requirements")).toHaveValue("Proposal visible connectors");
await expect(page.locator(".sourcing-requirement")).toHaveCount(1);
});