204 lines
8.1 KiB
TypeScript
204 lines
8.1 KiB
TypeScript
import { expect, type APIRequestContext, type Locator, type Page } from "@playwright/test";
|
|
import { dcUser } from "./helpers";
|
|
|
|
// Helpers for the phase 4 warehouse scenarios: packets made through the API, and the board locators.
|
|
// The labels are the exact texts of the phase 4 design contract (Frontend (C)). The frontend matches them.
|
|
|
|
export type Json = Record<string, any>;
|
|
|
|
export const L = {
|
|
pageHead: "Warehouse",
|
|
eyebrow: "Parts pull",
|
|
colIncoming: "Waiting to pull",
|
|
colPulling: "Being pulled",
|
|
colHold: "On hold",
|
|
colPulled: "Pulled, ready for production",
|
|
colReleased: "Released to shop (3 days)",
|
|
pillWaiting: "WH · waiting",
|
|
pillPulling: "WH · being pulled",
|
|
pillHold: "WH · on hold",
|
|
pillPulled: "WH · pulled",
|
|
startPulling: "Start pulling",
|
|
hold: "Hold",
|
|
markPulled: "Mark pulled",
|
|
resumePulling: "Resume pulling",
|
|
backToPulling: "Back to pulling",
|
|
releaseCard: "Release…",
|
|
putOnHold: "Put on hold",
|
|
cancel: "Cancel",
|
|
dropReleasedToast: "Open the packet and use Release to supervisor. It checks heats and shorts first.",
|
|
sendBack: "Send back to Clerical",
|
|
releaseToSupervisor: "Release to supervisor",
|
|
materialsTab: "Materials",
|
|
heatPlaceholder: "Scan / type heat",
|
|
recordShort: "Record short",
|
|
shortButton: "Short",
|
|
line: (n: number, what: "pulled" | "issued" | "location" | "heat" | "N/A" | "MTR file") => `Line ${n} ${what}`,
|
|
qtyShort: "Qty short",
|
|
reason: "Reason",
|
|
heatForPulled: "Heat no. for the pulled pieces",
|
|
holdSheet: "Put on hold",
|
|
holdNote: /^Note/,
|
|
recordShortSheet: "Record short",
|
|
receiveSheet: "Receive short part",
|
|
notAllPulled: "Not everything is pulled yet",
|
|
saveShort: "Save short",
|
|
receive: "Receive",
|
|
qtyReceived: "Qty received",
|
|
heat: "Heat",
|
|
note: "Note",
|
|
openShorts: "Open shorts",
|
|
noShorts: "No open shorts. When a part isn't on the shelf, mark it Short on the pull and it lands here.",
|
|
readyToAssign: "Ready to assign",
|
|
msgPull: "Pull every material line or mark it short first.",
|
|
msgHeat: "Scan or type the heat number of every pulled line first. Use N/A for a part with no traceability.",
|
|
} as const;
|
|
|
|
export function writeHeaders(page: Page): Record<string, string> {
|
|
const base = new URL(page.url()).origin;
|
|
return { Origin: base, "X-Requested-With": "PacketStation" };
|
|
}
|
|
|
|
export async function getJson(req: APIRequestContext, url: string): Promise<Json> {
|
|
const r = await req.get(url);
|
|
expect(r.status(), `${url}: ${await r.text()}`).toBe(200);
|
|
return (await r.json()) as Json;
|
|
}
|
|
|
|
async function sendJson(page: Page, method: "post" | "patch", url: string, data: Json, status = 200): Promise<Json> {
|
|
const r = await page.request[method](url, { headers: writeHeaders(page), data });
|
|
expect(r.status(), `${method} ${url}: ${await r.text()}`).toBe(status);
|
|
return (await r.json()) as Json;
|
|
}
|
|
|
|
/** A minimal valid one-page PDF, built from raw bytes with a correct xref table. */
|
|
/** A one-page PDF. A `label` goes into a comment line, so the file has its own sha256 (the server dedupes by it). */
|
|
export function onePagePdf(label?: string): Buffer {
|
|
const content = "0 0 m 612 792 l S";
|
|
const objects = [
|
|
"<< /Type /Catalog /Pages 2 0 R >>",
|
|
"<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
|
|
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 4 0 R /Resources << >> >>",
|
|
`<< /Length ${content.length} >>\nstream\n${content}\nendstream`,
|
|
];
|
|
let out = label ? `%PDF-1.4\n%${label.replace(/[\r\n]/g, " ")}\n` : "%PDF-1.4\n";
|
|
const offsets: number[] = [];
|
|
objects.forEach((body, i) => {
|
|
offsets.push(out.length);
|
|
out += `${i + 1} 0 obj\n${body}\nendobj\n`;
|
|
});
|
|
const xref = out.length;
|
|
out += `xref\n0 ${objects.length + 1}\n0000000000 65535 f \n`;
|
|
out += offsets.map((o) => `${String(o).padStart(10, "0")} 00000 n \n`).join("");
|
|
out += `trailer\n<< /Size ${objects.length + 1} /Root 1 0 R >>\nstartxref\n${xref}\n%%EOF\n`;
|
|
return Buffer.from(out, "latin1");
|
|
}
|
|
|
|
/** A 1x1 PNG. */
|
|
export function tinyPng(): Buffer {
|
|
return Buffer.from(
|
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==",
|
|
"base64",
|
|
);
|
|
}
|
|
|
|
export interface MadePacket {
|
|
id: number;
|
|
title: string;
|
|
unitNumber: string;
|
|
}
|
|
|
|
/**
|
|
* As the signed-in Clerical user: a project with one unit, a hand draft with BOM lines 1 to 3, a drawing
|
|
* attached with a password signature, the heat map answered, sent to the warehouse.
|
|
*/
|
|
export async function makeWarehousePacket(page: Page, projectNumber: string, title: string): Promise<MadePacket> {
|
|
const projects = (await getJson(page.request, "/api/projects?status=all")) as unknown as Json[];
|
|
let project = projects.find((p) => p.number === projectNumber);
|
|
if (!project) {
|
|
project = await sendJson(page, "post", "/api/projects", { number: projectNumber, name: "CI warehouse" }, 201);
|
|
}
|
|
const units = (await sendJson(page, "post", `/api/projects/${project.id}/units`, {}, 201)) as unknown as Json[];
|
|
const unit = units[0];
|
|
const draft = await sendJson(
|
|
page,
|
|
"post",
|
|
"/api/packets",
|
|
{
|
|
unit_id: unit.id,
|
|
dept_code: "03",
|
|
op: "WELD-PART",
|
|
title,
|
|
customer: "CI Customer",
|
|
drawing: `D-${projectNumber}`,
|
|
rev: "A",
|
|
work_orders: [],
|
|
bom: [
|
|
{ item: 1, qty: 1, part_id: "PIPE-4", description: "4in pipe SCH40" },
|
|
{ item: 2, qty: 2, part_id: "FLG-4", description: "4in flange" },
|
|
{ item: 3, qty: 1, part_id: "STUD-58", description: "5/8 stud bolt" },
|
|
],
|
|
},
|
|
201,
|
|
);
|
|
const up = await page.request.post("/api/assets", {
|
|
headers: writeHeaders(page),
|
|
multipart: {
|
|
file: { name: `${projectNumber}.pdf`, mimeType: "application/pdf", buffer: onePagePdf() },
|
|
purpose: "drawing",
|
|
},
|
|
});
|
|
expect(up.status(), await up.text()).toBe(201);
|
|
const asset = (await up.json()) as Json;
|
|
await sendJson(page, "post", `/api/packets/${draft.id}/pages`, {
|
|
asset_id: asset.id,
|
|
type: "drawing",
|
|
sign: { factor: "password", secret: dcUser("clerk.clara").password },
|
|
});
|
|
await sendJson(page, "patch", `/api/packets/${draft.id}`, { set: { heat_map: true }, base: {} });
|
|
const pkt = await getJson(page.request, `/api/packets/${draft.id}`);
|
|
const sent = await sendJson(page, "post", `/api/packets/${draft.id}/transitions`, {
|
|
action: "send_to_warehouse",
|
|
expect: { status: pkt.status, dept: pkt.dept.code },
|
|
});
|
|
expect(sent.status).toBe("warehouse");
|
|
return { id: Number(draft.id), title, unitNumber: String(unit.number ?? pkt.unit?.number ?? "") };
|
|
}
|
|
|
|
/** A board column by its title: a region named by the title, or a block that has it as a heading. */
|
|
export function column(page: Page, title: string): Locator {
|
|
return page
|
|
.getByRole("region", { name: title, exact: true })
|
|
.or(page.locator("section, [data-whcol]").filter({ has: page.getByRole("heading", { name: title, exact: true }) }))
|
|
.first();
|
|
}
|
|
|
|
/** A packet card, found by its unique title, anywhere under `scope`. */
|
|
export function card(scope: Page | Locator, title: string): Locator {
|
|
// Warehouse cards are role=group named "{job}, {stage name}"; the released column shows plain cards.
|
|
return scope
|
|
.getByRole("group")
|
|
.filter({ hasText: title })
|
|
.or(scope.locator(".card").filter({ hasText: title }))
|
|
.first();
|
|
}
|
|
|
|
/** Drag with the pointer from the middle of `from` to the middle of `to`, in steps (a real drag, not a click). */
|
|
export async function drag(page: Page, from: Locator, to: Locator): Promise<void> {
|
|
await from.scrollIntoViewIfNeeded();
|
|
const a = await from.boundingBox();
|
|
const b = await to.boundingBox();
|
|
if (!a || !b) throw new Error("drag: element not visible");
|
|
// Grab the card on its title line, away from its buttons.
|
|
await page.mouse.move(a.x + a.width / 2, a.y + 12);
|
|
await page.mouse.down();
|
|
await page.mouse.move(a.x + a.width / 2 + 15, a.y + 20, { steps: 4 });
|
|
await page.mouse.move(b.x + b.width / 2, b.y + Math.min(b.height / 2, 120), { steps: 16 });
|
|
await page.mouse.up();
|
|
}
|
|
|
|
export async function whStage(page: Page, id: number): Promise<string | null> {
|
|
const p = await getJson(page.request, `/api/packets/${id}`);
|
|
return p.wh ? String(p.wh.stage) : null;
|
|
}
|