Fix report pagination and rebuild saved plan navigation
Test and deploy / test-and-deploy (push) Successful in 1m43s
Test and deploy / test-and-deploy (push) Successful in 1m43s
This commit is contained in:
1 parent
824a964f9d
commit
5f3a015ea2
20 files changed
+1480
-809
No files matched your search
+251
-87
@@ -3,93 +3,257 @@ 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: unit toggles preserve milestone inputs; HTML download and print share the report",
|
||||
{ skip: process.env.RUN_BROWSER_TESTS !== "1" },
|
||||
async (t) => {
|
||||
const { chromium } = await import("playwright");
|
||||
const { app } = 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 signup = await page.request.post(base + "/api/auth/signup", {
|
||||
data: {
|
||||
email: "[email protected]",
|
||||
password: "browser regression password",
|
||||
},
|
||||
});
|
||||
assert.equal(signup.status(), 201);
|
||||
await page.goto(base + "/app");
|
||||
for (const [k, v] of Object.entries({
|
||||
0.1: "Burundi Kayanza Masha",
|
||||
0.4: "500",
|
||||
1.1: "Bourbon",
|
||||
1.4: "8:45",
|
||||
4.2: "12",
|
||||
4.3: "1:38",
|
||||
4.4: "1:15",
|
||||
5.2: "761.4",
|
||||
"preheat.tempC": "350",
|
||||
"preheat.minutes": "20",
|
||||
"temps.fc.tempC": "392",
|
||||
}))
|
||||
await page.locator(`#plan-form input[name="${k}"]`).fill(v);
|
||||
const temp = page.locator('#plan-form input[name="temps.fc.tempC"]');
|
||||
for (const [unit, value] of [
|
||||
["C", "200"],
|
||||
["F", "392"],
|
||||
]) {
|
||||
await page.evaluate((unit) => {
|
||||
const input = document.querySelector(
|
||||
`#temp-unit-toggle input[value="${unit}"]`,
|
||||
);
|
||||
input.checked = true;
|
||||
input.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
}, unit);
|
||||
assert.equal(await temp.inputValue(), value);
|
||||
assert.equal(
|
||||
await page.locator('#sec-machine input[name$=".tempC"]').count(),
|
||||
6,
|
||||
);
|
||||
}
|
||||
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 pending = page.waitForEvent("download");
|
||||
await page.getByRole("button", { name: "Download HTML" }).click();
|
||||
const download = await pending;
|
||||
assert.match(download.suggestedFilename(), /Burundi.*\.html$/);
|
||||
const html = await readFile(await download.path(), "utf8");
|
||||
assert.ok(html.includes("440 g"));
|
||||
assert.ok(html.includes("350°F"));
|
||||
assert.ok(html.includes("Development · 1:15"));
|
||||
assert.ok(!html.includes("°C"));
|
||||
await page.emulateMedia({ media: "print" });
|
||||
await page.evaluate(() => window.dispatchEvent(new Event("beforeprint")));
|
||||
assert.equal(await page.locator("#report-print").isVisible(), true);
|
||||
assert.equal(await page.locator("#print-sheet").isVisible(), false);
|
||||
assert.equal(await page.locator("#report-print .sheet").count(), 2);
|
||||
assert.equal(await page.locator("#report-print svg").count(), 1);
|
||||
await page.emulateMedia({ media: "screen" });
|
||||
await page.evaluate(() => window.dispatchEvent(new Event("afterprint")));
|
||||
await page.goto(base + "/roasters");
|
||||
assert.deepEqual(errors, []);
|
||||
},
|
||||
"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, []);
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user