212 lines
9.6 KiB
TypeScript
212 lines
9.6 KiB
TypeScript
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);
|
|
});
|
|
});
|