e2e/assembly.spec.ts
Rama main · réplica de lectura del repositorio original.
import { openFileDetails, openProject, openProjectSection } from "./workspace-controls";
import { expect, test } from "@playwright/test";
import { readFile } from "node:fs/promises";
import AxeBuilder from "@axe-core/playwright";
test("assembly notes remain usable when graphics are unavailable and survive retry", async ({ page }) => {
const errors: string[] = [];
page.on("pageerror", error => errors.push(error.message));
await page.addInitScript(() => {
const original = HTMLCanvasElement.prototype.getContext;
HTMLCanvasElement.prototype.getContext = function (...args: Parameters<typeof original>) {
if (/^webgl2?$|^experimental-webgl$/u.test(args[0]) && !Reflect.get(window, "testGraphicsAvailable")) return null;
return original.apply(this, args);
} as typeof original;
});
await page.setViewportSize({ width: 390, height: 844 });
await page.goto("/"); await page.getByLabel("Workspace password").fill("demo-password-please-change");
await page.getByRole("button", { name: "Sign in", exact: true }).click();
await openProject(page, "Synthetic H2D desk lamp");
page.on("console", message => {
if (message.type() === "error" && !/^(?:THREE\.WebGLRenderer: )+Error creating WebGL context\.$/u.test(message.text())) errors.push(message.text());
});
await page.getByRole("tab", { name: /^Files/u }).click();
await page.getByLabel("Choose files to upload").setInputFiles({ name: "synthetic-no-graphics.step", mimeType: "model/step", buffer: await readFile("packages/artifacts/testfiles/synthetic-assembly.step") });
await page.getByRole("button", { name: "Add 1 file", exact: true }).click();
await expect(page.getByRole("button", { name: "Download synthetic-no-graphics.step", exact: true })).toBeVisible();
await openProjectSection(page, "Assembly");
await page.getByLabel("synthetic-no-graphics.step", { exact: true }).check();
await page.getByRole("button", { name: "Open assembly", exact: true }).click();
await expect(page.getByRole("alert")).toContainText("3D viewing is unavailable in this browser");
await expect(page.locator(".assembly-canvas canvas")).toHaveCount(0);
await page.getByRole("button", { name: "Lid", exact: true }).click();
await page.getByRole("button", { name: "Edit assembly", exact: true }).click();
await page.getByLabel("Fixing notes").fill("Keep this draft while restoring the model view.");
await page.getByRole("button", { name: "Front", exact: true }).click();
await page.getByRole("button", { name: "Exploded", exact: true }).click();
await page.getByRole("button", { name: "Retry 3D viewer", exact: true }).click();
await expect(page.getByRole("alert")).toContainText("3D viewing is unavailable in this browser");
await expect(page.getByLabel("Fixing notes")).toHaveValue("Keep this draft while restoring the model view.");
await page.evaluate(() => Reflect.set(window, "testGraphicsAvailable", true));
await page.getByRole("button", { name: "Retry 3D viewer", exact: true }).click();
await expect(page.locator(".assembly-canvas canvas")).toBeVisible();
await expect(page.getByRole("alert")).toHaveCount(0);
await expect(page.locator(".assembly-dimensions")).toContainText("40.0 × 30.0 × 17.0 mm");
await expect(page.getByLabel("Fixing notes")).toHaveValue("Keep this draft while restoring the model view.");
await expect(page.getByRole("button", { name: "Front", exact: true })).toHaveAttribute("aria-pressed", "true");
await expect(page.getByRole("button", { name: "Lid", exact: true })).toHaveAttribute("aria-pressed", "true");
await expect(page.getByRole("slider", { name: "Assembly separation" })).toHaveAttribute("aria-valuenow", "100");
await expect.poll(() => page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(390);
expect(errors).toEqual([]);
});
test("assembly explorer imports CAD, edits a guide, saves and reopens on desktop and mobile", async ({ page }) => {
await page.setViewportSize({ width: 1440, height: 1000 });
await page.goto("/"); await page.getByLabel("Workspace password").fill("demo-password-please-change"); await page.getByRole("button", { name: "Sign in", exact: true }).click();
await openProject(page, "Synthetic H2D desk lamp"); await page.getByRole("tab", { name: /^Files/u }).click();
await page.getByLabel("Choose files to upload").setInputFiles({ name: "synthetic-assembly.step", mimeType: "model/step", buffer: await readFile("packages/artifacts/testfiles/synthetic-assembly.step") });
await page.getByRole("button", { name: "Add 1 file", exact: true }).click(); await expect(page.getByRole("button", { name: "Download synthetic-assembly.step", exact: true })).toBeVisible();
await openProjectSection(page, "Assembly");
await page.getByLabel("synthetic-assembly.step", { exact: true }).check(); await page.getByRole("button", { name: "Open assembly", exact: true }).click();
await expect(page.locator(".assembly-canvas canvas")).toBeVisible(); await expect(page.locator(".assembly-dimensions")).toContainText("40.0 × 30.0 × 17.0 mm");
await page.getByRole("button", { name: "Exploded", exact: true }).click(); await expect(page.getByRole("slider", { name: "Assembly separation" })).toHaveAttribute("aria-valuenow", "100");
await page.getByRole("button", { name: "Lid", exact: true }).click();
await page.getByRole("button", { name: "Edit assembly", exact: true }).click();
await page.getByLabel("Material", { exact: true }).fill("Synthetic cream PLA");
await page.getByLabel("Fixing notes").fill("Seat the lid after checking the connector clearance.");
// Losing GPU resources must not require navigation or discard this draft.
await page.locator(".assembly-canvas canvas").evaluate(canvas => canvas.dispatchEvent(new Event("webglcontextlost", { cancelable: true })));
await expect(page.getByRole("alert")).toContainText("graphics connection was lost");
await expect(page.getByLabel("Fixing notes")).toHaveValue("Seat the lid after checking the connector clearance.");
await page.getByRole("button", { name: "Retry 3D viewer", exact: true }).click();
await expect(page.locator(".assembly-canvas canvas")).toBeVisible();
await expect(page.getByRole("alert")).toHaveCount(0);
await expect(page.getByLabel("Fixing notes")).toHaveValue("Seat the lid after checking the connector clearance.");
await page.getByRole("button", { name: "Placement and separation", exact: true }).click();
await page.getByLabel("Separation (mm) Z", { exact: true }).fill("45");
await page.getByRole("button", { name: "Isolate part", exact: true }).click(); await expect(page.getByLabel("Show Base", { exact: true })).not.toBeChecked();
await page.getByRole("button", { name: "Show all parts", exact: true }).click();
// LAN HTTP does not expose randomUUID; both local identities must still work.
await page.evaluate(() => Reflect.deleteProperty(Crypto.prototype, "randomUUID"));
await page.getByRole("button", { name: "Lid", exact: true }).click();
await page.getByRole("button", { name: "Add another placement", exact: true }).click();
await expect(page.getByLabel("Name", { exact: true })).toHaveValue("Lid copy");
await page.getByRole("button", { name: "Build order", exact: true }).click(); await page.getByRole("button", { name: "Add build step", exact: true }).click();
await page.getByLabel("Step title").fill("Fit the enclosure"); await page.getByLabel("Instructions", { exact: true }).fill("Check both parts before fastening.");
await page.getByRole("button", { name: "Save assembly", exact: true }).click(); await expect(page.getByText(/Saved version 1/u)).toBeVisible();
await page.reload(); await expect(page.getByLabel("Build views").getByRole("button", { name: "Assembly", exact: true })).toHaveAttribute("aria-pressed", "true");
await expect(page.locator(".assembly-canvas canvas")).toBeVisible(); await page.getByRole("button", { name: "Lid", exact: true }).click(); await expect(page.getByText("Synthetic cream PLA", { exact: true })).toBeVisible();
await page.getByRole("button", { name: "Build order", exact: true }).click(); await page.getByRole("button", { name: /Fit the enclosure/u }).click();
await expect(page.getByRole("slider", { name: "Assembly separation" })).toHaveAttribute("aria-valuenow", "55");
await page.getByRole("button", { name: "Parts & details", exact: true }).click(); await page.getByRole("button", { name: "Show all", exact: true }).click(); await page.getByRole("button", { name: "Exploded", exact: true }).click();
await page.screenshot({ path: "test-results/assembly-desktop.png", fullPage: true });
const a11y = await new AxeBuilder({ page }).include(".assembly-workspace").withTags(["wcag2a", "wcag2aa", "wcag21aa"]).analyze(); expect(a11y.violations).toEqual([]);
await page.setViewportSize({ width: 390, height: 844 }); await expect(page.locator(".assembly-canvas canvas")).toBeVisible();
await expect.poll(() => page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(390);
await page.screenshot({ path: "test-results/assembly-mobile.png", fullPage: true });
await page.getByRole("button", { name: "Edit assembly", exact: true }).click(); await page.getByLabel("Assembly name", { exact: true }).fill("Unsaved assembly");
await page.getByRole("tab", { name: /^Files/u }).click(); await expect(page.getByRole("alertdialog", { name: "Leave without saving?" })).toBeVisible();
});
test("a large assembly keeps selection controls reachable in light, dark and narrow views", async ({ page }) => {
await page.setViewportSize({ width: 1440, height: 1000 });
await page.goto("/"); await page.getByLabel("Workspace password").fill("demo-password-please-change"); await page.getByRole("button", { name: "Sign in", exact: true }).click();
await openProject(page, "Synthetic H2D desk lamp"); await page.getByRole("tab", { name: /^Files/u }).click();
await page.getByLabel("Choose files to upload").setInputFiles({ name: "synthetic-panels.step", mimeType: "model/step", buffer: await readFile("packages/artifacts/testfiles/synthetic-assembly.step") });
await page.getByRole("button", { name: "Add 1 file", exact: true }).click(); await expect(page.getByRole("button", { name: "Download synthetic-panels.step", exact: true })).toBeVisible();
await page.route("**/assembly", route => route.request().method() === "GET" ? route.fulfill({ json: { assembly: null, warnings: [] } }) : route.abort());
// Exercise a long list with synthetic repeated placements; never write the mocked draft.
await page.route("**/assembly/inspect", async route => {
const response = await route.fetch(), model = await response.json();
model.parts = Array.from({ length: 26 }, (_, i) => ({ ...model.parts[0], id: `panel-${i}`, name: `Panel ${i + 1}`, position: [i % 5 * 45, Math.floor(i / 5) * 35, 0] }));
await route.fulfill({ response, json: model });
});
await openProjectSection(page, "Assembly");
await page.getByRole("button", { name: "Select all files", exact: true }).click(); await page.getByRole("button", { name: "Open assembly", exact: true }).click();
await expect(page.locator(".assembly-canvas canvas")).toBeVisible();
await expect(page.getByLabel("Assembly name", { exact: true })).toHaveCount(0);
await expect(page.getByRole("button", { name: "Save assembly", exact: true })).toBeVisible();
const parts = page.getByRole("region", { name: "Model parts", exact: true });
// A long list scrolls within its own region, leaving room for the selected
// part's controls. The usable bound follows the layout, not an old pixel cap.
expect(await parts.evaluate(element => element.scrollHeight > element.clientHeight && ["auto", "scroll"].includes(getComputedStyle(element).overflowY))).toBe(true);
await expect(parts.getByRole("button", { name: "Panel 26", exact: true })).not.toBeInViewport();
const selectLastPart = async () => {
await page.getByLabel("Find a part", { exact: true }).fill("Panel 26");
await parts.getByRole("button", { name: "Panel 26", exact: true }).click();
await expect(page.getByRole("button", { name: "Isolate part", exact: true })).toBeInViewport({ ratio: 1 });
await expect(page.getByRole("button", { name: "Show all parts", exact: true })).toBeInViewport({ ratio: 1 });
await page.getByLabel("Find a part", { exact: true }).fill("");
await expect(parts.getByRole("button", { name: "Panel 26", exact: true })).toHaveAttribute("aria-pressed", "true");
await expect(page.getByRole("button", { name: "Isolate part", exact: true })).toBeInViewport({ ratio: 1 });
await expect(page.getByRole("button", { name: "Show all parts", exact: true })).toBeInViewport({ ratio: 1 });
};
await selectLastPart();
const stage = page.locator(".assembly-stage"); const light = await stage.evaluate(el => getComputedStyle(el).backgroundColor);
await page.evaluate(() => document.documentElement.dataset.theme = "dark");
await expect.poll(() => stage.evaluate(el => getComputedStyle(el).backgroundColor)).not.toBe(light);
const darkA11y = await new AxeBuilder({ page }).include(".assembly-workspace").withTags(["wcag2a", "wcag2aa", "wcag21aa"]).analyze(); expect(darkA11y.violations).toEqual([]);
await page.screenshot({ path: "test-results/assembly-large-dark.png", fullPage: true });
await page.evaluate(() => document.documentElement.dataset.theme = "light");
await page.setViewportSize({ width: 390, height: 844 });
// ResizeObserver fits the WebGL canvas on the next layout pass.
await expect.poll(() => page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(390);
await page.getByRole("slider", { name: "Assembly separation" }).focus(); await page.keyboard.press("ArrowRight"); await expect(page.getByRole("slider", { name: "Assembly separation" })).toHaveAttribute("aria-valuenow", "1");
await selectLastPart();
await page.screenshot({ path: "test-results/assembly-large-mobile.png", fullPage: true });
});
test("the downloadable GLB showcase uploads with its standard browser media type", async ({ page }) => {
await page.goto("/"); await page.getByLabel("Workspace password").fill("demo-password-please-change");
await page.getByRole("button", { name: "Sign in", exact: true }).click();
await openProject(page, "Synthetic H2D desk lamp"); await page.getByRole("tab", { name: /^Files/u }).click();
const bytes = await readFile("docs/assets/showcase/synthetic-enclosure.glb");
await page.getByLabel("Choose files to upload").setInputFiles({ name: "synthetic-enclosure.glb", mimeType: "model/gltf-binary", buffer: bytes });
await page.getByRole("button", { name: "Add 1 file", exact: true }).click();
const details = await openFileDetails(page, "synthetic-enclosure.glb");
const download = details.getByRole("button", { name: "Download file", exact: true });
await expect(download).toBeVisible();
const pending = page.waitForEvent("download"); await download.click();
const stream = await (await pending).createReadStream(); const chunks: Buffer[] = [];
for await (const chunk of stream) chunks.push(Buffer.from(chunk));
expect(Buffer.concat(chunks)).toEqual(bytes);
});