Files
PacketStation/tests/e2e/tests/viewer.spec.ts
T

212 lines
9.6 KiB
TypeScript
Raw Normal View History

import { type CDPSession, type Locator, type Page } from "@playwright/test";
import { expect, test, signInPassword } from "./helpers";
import {
actor,
closeAll,
drawing,
drawingReady,
makeShopDraft,
packetTab,
pullAndRelease,
type Actor,
type ShopPacket,
} from "./shophelpers";
import { getJson, onePagePdf, tinyPng } from "./whhelpers";
// Phase 10, the old tfs, tpan2 and tmtr on the new UI. sup.wes opens the Heat map of a packet ready to assign: Full
// screen and back (button, Esc, leaving the tab); the drawing pans by mouse after re-renders by exactly the drag,
// so no listeners pile up; on a touch device four finger drags each move the view. wh.wade uploads a PDF and a PNG
// MTR on a warehouse packet and opens each in the MTR viewer (pdf.js on a canvas, no frame); "Open in new tab" serves
// the file with the PDF policy.
const RUN = Date.now().toString(36).toUpperCase().slice(-5);
const PROJECT = `V${RUN}`;
const VIEWER = "Heat map";
let ready: ShopPacket;
let atWh: ShopPacket;
function viewerBox(page: Page): Locator {
return page.getByRole("group", { name: VIEWER, exact: true });
}
/** The stage's translate as [x, y]. */
async function pan(page: Page): Promise<[number, number]> {
const t = await viewerBox(page)
.locator(".stage")
.evaluate((el) => (el as HTMLElement).style.transform);
const m = /translate\((-?[\d.]+)px,\s*(-?[\d.]+)px\)/.exec(t);
if (!m) throw new Error(`no translate in "${t}"`);
return [Number(m[1]), Number(m[2])];
}
async function openHeatMap(page: Page, id: number): Promise<void> {
await page.goto(`/packets/${id}`);
await packetTab(page, /^Heat map/).click();
await drawingReady(page, VIEWER);
}
async function canvasPoint(page: Page, fx: number, fy: number): Promise<{ x: number; y: number }> {
const c = drawing(page, VIEWER);
await c.scrollIntoViewIfNeeded();
const b = await c.boundingBox();
if (!b) throw new Error("drawing not visible");
return { x: Math.round(b.x + b.width * fx), y: Math.round(b.y + b.height * fy) };
}
async function touchDrag(cdp: CDPSession, from: { x: number; y: number }, dx: number, dy: number): Promise<void> {
await cdp.send("Input.dispatchTouchEvent", { type: "touchStart", touchPoints: [{ x: from.x, y: from.y }] });
for (let i = 1; i <= 8; i++) {
await cdp.send("Input.dispatchTouchEvent", {
type: "touchMove",
touchPoints: [{ x: from.x + (dx * i) / 8, y: from.y + (dy * i) / 8 }],
});
}
await cdp.send("Input.dispatchTouchEvent", { type: "touchEnd", touchPoints: [] });
}
test.describe.serial("drawing viewer and MTRs", () => {
test("setup: a packet ready to assign and one at the warehouse", async ({ browser }) => {
const clara = await actor(browser, "clerk.clara");
const wade = await actor(browser, "wh.wade");
ready = await makeShopDraft(clara, PROJECT, `Viewer spool ${RUN}`);
await pullAndRelease(wade, ready.id);
atWh = await makeShopDraft(clara, PROJECT, `MTR spool ${RUN}`);
await closeAll(clara, wade);
});
test("tfs: full screen from the viewer; Exit, Esc and leaving the tab end it", async ({ browser }) => {
const wes = await actor(browser, "sup.wes");
const page = wes.page;
await openHeatMap(page, ready.id);
const box = viewerBox(page);
const before = await box.boundingBox();
const body = page.locator("body");
await box.getByRole("button", { name: "Full screen", exact: true }).click();
await expect(body).toHaveClass(/\bfs-view\b/);
await expect(box.getByRole("button", { name: "Exit full screen", exact: true })).toBeVisible();
const big = await box.boundingBox();
expect(big && before && big.width * big.height > before.width * before.height).toBe(true);
await expect(page.locator('[data-ready="1"]').and(box)).toBeVisible();
await box.getByRole("button", { name: "Exit full screen", exact: true }).click();
await expect(body).not.toHaveClass(/\bfs-view\b/);
await box.getByRole("button", { name: "Full screen", exact: true }).click();
await expect(body).toHaveClass(/\bfs-view\b/);
await page.keyboard.press("Escape");
await expect(body).not.toHaveClass(/\bfs-view\b/);
await box.getByRole("button", { name: "Full screen", exact: true }).click();
await expect(body).toHaveClass(/\bfs-view\b/);
await packetTab(page, /^Materials/).click();
await expect(body).not.toHaveClass(/\bfs-view\b/);
await closeAll(wes);
});
test("tpan2: a mouse drag pans by exactly the drag after re-renders", async ({ browser }) => {
const wes = await actor(browser, "sup.wes");
const page = wes.page;
await openHeatMap(page, ready.id);
const box = viewerBox(page);
// Re-render the viewer a few times: zoom, fit, full screen in and out
for (let i = 0; i < 2; i++) {
await box.getByRole("button", { name: "Zoom in", exact: true }).click();
await box.getByRole("button", { name: "Fit drawing" }).click();
await box.getByRole("button", { name: "Full screen", exact: true }).click();
await box.getByRole("button", { name: "Exit full screen", exact: true }).click();
}
await box.getByRole("button", { name: "Zoom in", exact: true }).click();
for (const [dx, dy] of [
[60, 30],
[-40, 25],
[30, -50],
] as const) {
const p0 = await pan(page);
const from = await canvasPoint(page, 0.5, 0.5);
await page.mouse.move(from.x, from.y);
await page.mouse.down();
await page.mouse.move(from.x + dx / 2, from.y + dy / 2, { steps: 4 });
await page.mouse.move(from.x + dx, from.y + dy, { steps: 4 });
await page.mouse.up();
const p1 = await pan(page);
expect(Math.abs(p1[0] - p0[0] - dx)).toBeLessThanOrEqual(1);
expect(Math.abs(p1[1] - p0[1] - dy)).toBeLessThanOrEqual(1);
}
await closeAll(wes);
});
test("tpan2: on a touch device each finger drag moves the view", async ({ browser, browserName }) => {
test.skip(browserName !== "chromium", "touch is sent through the Chrome DevTools Protocol");
const ctx = await browser.newContext({ hasTouch: true, viewport: { width: 1024, height: 768 } });
const page = await ctx.newPage();
const wes: Actor = { sam: "sup.wes", ctx, page };
await signInPassword(page, "sup.wes");
await openHeatMap(page, ready.id);
await viewerBox(page).getByRole("button", { name: "Zoom in", exact: true }).click();
const cdp = await ctx.newCDPSession(page);
for (const [dx, dy] of [
[50, 0],
[0, 40],
[-30, -30],
[45, 20],
] as const) {
const p0 = await pan(page);
await touchDrag(cdp, await canvasPoint(page, 0.5, 0.5), dx, dy);
const p1 = await pan(page);
expect(Math.abs(p1[0] - p0[0] - dx)).toBeLessThanOrEqual(2);
expect(Math.abs(p1[1] - p0[1] - dy)).toBeLessThanOrEqual(2);
}
await closeAll(wes);
});
test("tmtr: a PDF and a PNG MTR, each shown in the MTR viewer and served for a new tab", async ({ browser }) => {
const wade = await actor(browser, "wh.wade");
const page = wade.page;
await page.goto(`/packets/${atWh.id}`);
await packetTab(page, /^Materials/).click();
const r1 = page.getByRole("row").filter({ hasText: "4in pipe SCH40" });
const r2 = page.getByRole("row").filter({ hasText: "4in flange" });
await page
.getByLabel("Line 1 MTR file", { exact: true })
.setInputFiles({ name: "mtr-1.pdf", mimeType: "application/pdf", buffer: onePagePdf(`MTR ${RUN}`) });
await expect(r1.getByRole("button", { name: "MTR", exact: true })).toBeVisible();
await page
.getByLabel("Line 2 MTR file", { exact: true })
.setInputFiles({ name: "mtr-2.png", mimeType: "image/png", buffer: tinyPng() });
await expect(r2.getByRole("button", { name: "MTR", exact: true })).toBeVisible();
const p = await getJson(page.request, `/api/packets/${atWh.id}`);
const bom = p.bom as Array<Record<string, unknown>>;
expect(bom.find((b) => b.item === 1)?.mtr_asset_id).toBeTruthy();
expect(bom.find((b) => b.item === 2)?.mtr_asset_id).toBeTruthy();
// The PDF: drawn by pdf.js in the sheet, no frame
await r1.getByRole("button", { name: "MTR", exact: true }).click();
let sheet = page.getByRole("dialog", { name: /^MTR · item 1/ });
await expect(sheet).toBeVisible();
await expect(sheet.getByRole("img", { name: /Drawing page 1/ }).first()).toBeVisible();
await expect(sheet.locator("iframe, object, embed")).toHaveCount(0);
const href = await sheet.getByRole("link", { name: "Open in new tab" }).first().getAttribute("href");
expect(href).toBeTruthy();
const pdf = await page.request.get(String(href));
expect(pdf.status()).toBe(200);
expect(pdf.headers()["content-type"]).toContain("application/pdf");
expect((await pdf.body()).subarray(0, 4).toString("latin1")).toBe("%PDF");
const pdfCsp = pdf.headers()["content-security-policy"] ?? "";
expect(pdfCsp).toContain("object-src 'self'");
expect(pdfCsp).toContain("frame-ancestors 'none'");
await sheet.getByRole("button", { name: "Close", exact: true }).click();
await expect(sheet).toBeHidden();
// The PNG: shown as an image
await r2.getByRole("button", { name: "MTR", exact: true }).click();
sheet = page.getByRole("dialog", { name: /^MTR · item 2/ });
const img = sheet.getByRole("img", { name: /^MTR · item 2/ });
await expect(img).toBeVisible();
await expect.poll(() => img.evaluate((el) => (el as HTMLImageElement).naturalWidth)).toBeGreaterThan(0);
const png = await page.request.get(String(await sheet.getByRole("link", { name: "Open in new tab" }).first().getAttribute("href")));
expect(png.status()).toBe(200);
expect(png.headers()["content-type"]).toContain("image/png");
await closeAll(wade);
});
});