Files
roast_command_center/test/report-browser.test.js
T
snowspeeder 5f3a015ea2
Test and deploy / test-and-deploy (push) Successful in 1m43s
Fix report pagination and rebuild saved plan navigation
2026-09-05 15:47:44 -04:00

260 lines
8.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import test from "node:test";
import assert from "node:assert/strict";
import { once } from "node:events";
import { readFile } from "node:fs/promises";
import { setup } from "./helpers.js";
import { blankPlan } from "../shared/fields.js";
import { renderReportHtml } from "../shared/report.js";
const browserOptions = { skip: process.env.RUN_BROWSER_TESTS !== "1" };
async function session(t) {
const { chromium } = await import("playwright");
const { app, db } = await setup();
const server = app.listen(0, "127.0.0.1");
await once(server, "listening");
t.after(() => new Promise((r) => server.close(r)));
const browser = await chromium.launch({
headless: true,
args: ["--no-sandbox"],
});
t.after(() => browser.close());
const page = await browser.newPage({
viewport: { width: 1440, height: 1100 },
});
const errors = [];
page.on("pageerror", (e) => errors.push(e.message));
const base = `http://127.0.0.1:${server.address().port}`;
const res = await page.request.post(base + "/api/auth/signup", {
data: {
email: "[email protected]",
password: "browser regression password",
},
});
assert.equal(res.status(), 201);
const { csrfToken } = await res.json();
const create = async (plan, name = "") =>
(
await (
await page.request.post(base + "/api/plans", {
headers: { "x-csrf-token": csrfToken },
data: { plan, name },
})
).json()
).plan;
return { page, browser, base, create, errors, db, csrfToken };
}
function warningPlan() {
const p = blankPlan();
Object.assign(p.fields, {
0.1: "Burundi Kayanza Masha — Anaerobic Natural Bourbon",
0.4: "500",
1.1: "Bourbon",
1.4: "8:45",
3.1: "anaerobic",
4.1: "light",
4.2: "12",
4.3: "1:38",
3.2: "-0:30",
1.7: "-0:05",
5.2: "76.14",
});
return p;
}
test(
"browser: warning-rich reports print on two actual PDF pages without a footer-only page",
browserOptions,
async (t) => {
const { PDFDocument } = await import("pdf-lib");
const { page, base, create, errors } = await session(t);
const plan = warningPlan();
const saved = await create(plan);
await page.goto(base + "/app?plan=" + saved.id);
await page
.getByRole("button", { name: "Preview report", exact: true })
.waitFor();
assert.equal(await page.locator("#sec-machine").count(), 0);
assert.equal(await page.locator("#sec-roastlog").count(), 0);
assert.equal(
await page.locator('#plan-form input[name^="planActual."]').count(),
0,
);
await page
.getByRole("button", { name: "Preview report", exact: true })
.click();
await page
.frameLocator('iframe[title="Printable roast report"]')
.getByRole("heading", { name: "Milestone plan" })
.waitFor();
await page.getByRole("button", { name: "Close preview" }).click();
const downloading = page.waitForEvent("download");
await page.getByRole("button", { name: "Download HTML" }).click();
const download = await downloading;
const html = await readFile(await download.path(), "utf8");
assert.ok(html.includes("440 g"));
assert.ok(html.includes("761.4"));
assert.ok(html.includes("Development · 1:15"));
assert.ok(!html.includes("°C"));
let pdf = await page.pdf({
preferCSSPageSize: true,
printBackground: true,
});
assert.equal(
(await PDFDocument.load(pdf)).getPageCount(),
2,
"in-app print must not orphan the footer",
);
for (const [label, content, options] of [
["downloaded HTML", html, { preferCSSPageSize: true }],
[
"110% print scaling",
renderReportHtml({
...plan,
fields: { ...plan.fields, 5.2: "761.4", 4.4: "1:15" },
}),
{ preferCSSPageSize: true, scale: 1.1 },
],
["A4 paper", html, { format: "A4", preferCSSPageSize: false }],
]) {
await page.setContent(content);
pdf = await page.pdf({ ...options, printBackground: true });
assert.equal((await PDFDocument.load(pdf)).getPageCount(), 2, label);
}
assert.deepEqual(errors, []);
},
);
test(
"browser: searchable plan table opens the selected plan and preserves edits across switches",
browserOptions,
async (t) => {
const { page, base, create, errors, db } = await session(t);
const first = warningPlan();
const a = await create(first, "Fruit plan");
const b = await create(
{
...blankPlan(),
fields: {
...blankPlan().fields,
0.1: "Espresso coffee",
0.4: "300",
1.4: "8:00",
4.3: "2:00",
},
},
"Espresso plan",
);
await page.goto(base + "/app?plan=" + a.id);
await page.locator('#plan-form input[name="0.4"]').fill("520");
await page.getByRole("link", { name: "▤ Your plans", exact: true }).click();
await page.waitForURL("**/plans*");
await page.locator("#plans-body .plan-title-link").first().waitFor();
await page.locator('#plan-filters input[name="q"]').fill("Espresso");
await page
.getByRole("link", { name: "Espresso plan", exact: true })
.click();
await page.waitForURL("**/app?plan=" + b.id);
await page.locator('#plan-form input[name="0.1"]').waitFor();
assert.equal(
await page.locator('#plan-form input[name="0.1"]').inputValue(),
"Espresso coffee",
);
assert.equal(
await page.locator("#header-coffee-name").textContent(),
"Espresso plan",
);
await page.getByRole("link", { name: "▤ Your plans", exact: true }).click();
await page.waitForURL("**/plans*");
await page.getByRole("link", { name: "Fruit plan", exact: true }).click();
await page.waitForURL("**/app?plan=" + a.id);
await page.locator('#plan-form input[name="0.4"]').waitFor();
assert.equal(
await page.locator('#plan-form input[name="0.4"]').inputValue(),
"520",
);
const owner = (
await db.query("SELECT user_id FROM roast_plans WHERE id=$1", [a.id])
).rows[0].user_id;
for (let i = 0; i < 28; i++)
await db.query(
"INSERT INTO roast_plans(user_id,plan,name) VALUES($1,$2,$3)",
[owner, blankPlan(), "Library " + i],
);
await page.goto(base + "/plans");
await page
.locator("#plans-status")
.filter({ hasText: "of 30 plans" })
.waitFor();
assert.equal(
await page.locator("#plans-body .plan-title-link").count(),
25,
);
await page.getByRole("button", { name: "Next", exact: true }).click();
await page.locator("#plans-status").filter({ hasText: "26–30" }).waitFor();
assert.equal(await page.locator("#plans-body .plan-title-link").count(), 5);
assert.deepEqual(errors, []);
},
);
test(
"browser: Plan vs Actual is editable on finished roasts and survives reload",
browserOptions,
async (t) => {
const { page, base, create, db, errors } = await session(t);
const plan = await create(warningPlan(), "Fruit plan");
const owner = (
await db.query("SELECT user_id FROM roast_plans WHERE id=$1", [plan.id])
).rows[0].user_id;
const parsed = {
roast: { title: "Finished fruit roast" },
curve: [
{ t: 0, bt: 200 },
{ t: 60, bt: 90 },
{ t: 450, bt: 190 },
{ t: 525, bt: 200 },
],
milestones: [
{ key: "fc", label: "First crack", timeS: 450, tempC: 190 },
{ key: "drop", label: "Drop", timeS: 525, tempC: 200 },
],
derived: { firstCrackS: 450, dropS: 525, developmentS: 75 },
};
const row = (
await db.query(
"INSERT INTO actual_roasts(user_id,roast_plan_id,filename,original_content,parsed,evaluation_status) VALUES($1,$2,'fruit.alog','{}',$3,'failed') RETURNING id",
[owner, plan.id, parsed],
)
).rows[0];
await page.goto(base + "/roasts?roast=" + row.id);
await page
.getByRole("heading", { name: "Plan vs Actual", exact: true })
.waitFor();
const time = page.getByRole("textbox", {
name: "First crack actual time",
exact: true,
});
assert.equal(await time.inputValue(), "7:30");
await time.fill("7:40");
await page
.getByRole("textbox", { name: "First crack notes", exact: true })
.fill("Audible");
await page.getByRole("button", { name: "Save observations" }).click();
await page
.locator("#comparison-status")
.filter({ hasText: "Observations saved" })
.waitFor();
await page.reload();
await page
.getByRole("textbox", { name: "First crack actual time", exact: true })
.waitFor();
assert.equal(await time.inputValue(), "7:40");
assert.equal(
await page
.getByRole("textbox", { name: "First crack notes", exact: true })
.inputValue(),
"Audible",
);
assert.deepEqual(errors, []);
},
);