Files
roast_command_center/test/report-browser.test.js
T
snowspeeder a72aec9f86
Test and deploy / test-and-deploy (push) Successful in 1m28s
Make plan deletion visible and reliable with in-page confirmation
2026-09-07 06:22:43 -04:00

764 lines
26 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, appOptions = {}) {
const { chromium } = await import("playwright");
const { app, db } = await setup({}, appOptions);
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",
);
const standalone = await page.context().browser().newPage();
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, []);
},
);
test(
"browser: Your Plans recovers a deleted remote draft without losing edits",
browserOptions,
async (t) => {
const { page, base, create, db, errors } = await session(t);
const saved = await create(warningPlan(), "Recovered fruit");
await page.goto(base + "/app?plan=" + saved.id);
await page.locator('#plan-form input[name="0.4"]').waitFor();
await db.query("DELETE FROM roast_plans WHERE id=$1", [saved.id]);
await page.locator('#plan-form input[name="0.4"]').fill("530");
await page.getByRole("link", { name: "← Your plans", exact: true }).click();
await page.waitForURL("**/plans*");
await page
.getByRole("link", { name: "Recovered fruit", exact: true })
.waitFor();
const rows = (await db.query("SELECT id,plan FROM roast_plans")).rows;
assert.equal(rows.length, 1);
assert.notEqual(rows[0].id, saved.id);
assert.equal(rows[0].plan.fields["0.4"], "530");
assert.deepEqual(errors, []);
},
);
test(
"browser: Your Plans saves to server when browser draft storage is full",
browserOptions,
async (t) => {
const { page, base, create, db, errors } = await session(t);
const saved = await create(warningPlan());
await page.goto(base + "/app?plan=" + saved.id);
await page.locator('#plan-form input[name="0.4"]').waitFor();
await page.evaluate(() => {
const original = Storage.prototype.setItem;
Storage.prototype.setItem = function (key, value) {
if (key.startsWith("roastPlannerPlan.v2:"))
throw new DOMException("Storage full", "QuotaExceededError");
return original.call(this, key, value);
};
});
await page.locator('#plan-form input[name="0.4"]').fill("540");
await page.getByRole("link", { name: "← Your plans", exact: true }).click();
await page.waitForURL("**/plans*");
assert.equal(
(await db.query("SELECT plan FROM roast_plans WHERE id=$1", [saved.id]))
.rows[0].plan.fields["0.4"],
"540",
);
assert.deepEqual(errors, []);
},
);
test(
"browser: rejected sync shows the cause and keeps the edited plan open",
browserOptions,
async (t) => {
const { page, base, create, db } = await session(t);
const saved = await create(warningPlan());
await page.goto(base + "/app?plan=" + saved.id);
await page.locator('#plan-form input[name="0.4"]').waitFor();
await page.route("**/api/plans/" + saved.id, async (route) =>
route.request().method() === "PUT"
? route.fulfill({
status: 403,
contentType: "application/json",
body: JSON.stringify({ ok: false, code: "csrf_failed" }),
})
: route.continue(),
);
await page.locator('#plan-form input[name="0.4"]').fill("550");
await page.getByRole("link", { name: "← Your plans", exact: true }).click();
await page.getByText(/Your sign-in session needs refreshing/).waitFor();
assert.ok(page.url().includes("/app?plan=" + saved.id));
assert.equal(
(await db.query("SELECT COUNT(*) AS n FROM roast_plans")).rows[0].n,
1,
);
const draft = await page.evaluate(() =>
JSON.parse(
localStorage.getItem(
Object.keys(localStorage).find(
(k) =>
k.startsWith("roastPlannerPlan.v2:") && !k.endsWith(":last-user"),
),
),
),
);
assert.equal(draft.plan.fields["0.4"], "550");
},
);
test(
"browser: Clone opens an independent plan whose roast level can change",
browserOptions,
async (t) => {
const { page, base, create, db, errors } = await session(t);
const original = await create(warningPlan(), "Fruit original");
await page.goto(base + "/plans");
await page
.getByRole("button", { name: "Clone Fruit original", exact: true })
.click();
await page.waitForURL("**/app?plan=*");
await page
.locator("#header-coffee-name")
.filter({ hasText: "Fruit original (copy)" })
.waitFor();
const id = new URL(page.url()).searchParams.get("plan");
assert.notEqual(id, original.id);
await page
.locator("#plan-form label")
.filter({ has: page.locator('input[name="4.1"][value="medium"]') })
.click();
await page.getByRole("link", { name: "← Your plans", exact: true }).click();
await page.waitForURL("**/plans*");
await page
.getByRole("link", { name: "Fruit original (copy)", exact: true })
.waitFor();
const rows = (await db.query("SELECT id,plan FROM roast_plans")).rows;
assert.equal(rows.length, 2);
assert.equal(
rows.find((r) => r.id === original.id).plan.fields["4.1"],
"light",
);
assert.equal(rows.find((r) => r.id === id).plan.fields["4.1"], "medium");
assert.deepEqual(errors, []);
},
);
test(
"browser: source URL persists from manual entry and prefill, with undo and cloning",
browserOptions,
async (t) => {
const { page, base, create, db, errors } = await session(t);
const original = await create(warningPlan(), "Source test");
await page.goto(base + "/app?plan=" + original.id);
await page
.locator("#header-coffee-name")
.filter({ hasText: "Source test" })
.waitFor();
const source = page.locator('#plan-form input[name="sourceUrl"]');
await source.fill("https://example.com/manual-coffee");
await page.getByRole("link", { name: "← Your plans", exact: true }).click();
await page.waitForURL("**/plans*");
await page.getByRole("link", { name: "Source test", exact: true }).click();
await page
.locator("#header-coffee-name")
.filter({ hasText: "Source test" })
.waitFor();
await source.waitFor();
assert.equal(
await source.inputValue(),
"https://example.com/manual-coffee",
);
await page.route("**/api/prefill", (route) =>
route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
ok: true,
fields: {},
source: { finalUrl: "https://example.com/new-coffee" },
warnings: [],
}),
}),
);
await page.locator("#nav-prefill").click();
await page.locator("#prefill-url").fill("https://example.com/new-coffee");
await page.locator("#prefill-go").click();
await page
.locator("#prefill-result")
.filter({ hasText: "Applied 0 fields" })
.waitFor();
assert.equal(await source.inputValue(), "https://example.com/new-coffee");
await page.locator("#prefill-undo").click();
assert.equal(
await source.inputValue(),
"https://example.com/manual-coffee",
);
await page.locator("#prefill-go").click();
await page
.locator("#prefill-result")
.filter({ hasText: "Applied 0 fields" })
.waitFor();
await page.keyboard.press("Escape");
await page.getByRole("link", { name: "← Your plans", exact: true }).click();
await page.waitForURL("**/plans*");
assert.equal(
(
await db.query("SELECT plan FROM roast_plans WHERE id=$1", [
original.id,
])
).rows[0].plan.sourceUrl,
"https://example.com/new-coffee",
);
await page
.getByRole("button", { name: "Clone Source test", exact: true })
.click();
await page.waitForURL("**/app?plan=*");
await page
.locator("#header-coffee-name")
.filter({ hasText: "Source test (copy)" })
.waitFor();
await source.waitFor();
assert.equal(await source.inputValue(), "https://example.com/new-coffee");
assert.deepEqual(errors, []);
},
);
test(
"browser: Bean Bro conversation persists; proposed changes require review and can be undone or cloned",
browserOptions,
async (t) => {
const { page, base, create, db, errors } = await session(t, {
runPlanChat: async () => ({
reply: "Try ten extra seconds and compare sweetness.",
model: "test:coach",
suggestions: [
{
fields: { 4.4: "1:25" },
reason: "Test development.",
expected: "More sweetness.",
evidenceRoastIds: [],
},
],
}),
});
const saved = await create(warningPlan(), "Bro experiment");
await page.goto(base + "/app?plan=" + saved.id);
await page
.getByRole("button", { name: "Review this plan", exact: true })
.click();
await page
.getByRole("button", { name: "Apply changes", exact: true })
.waitFor();
const override = page.locator('#plan-form [name="4.4"]');
assert.equal(await override.inputValue(), "");
await page
.getByRole("button", { name: "Apply changes", exact: true })
.click();
assert.equal(await override.inputValue(), "1:25");
await page
.getByRole("button", { name: "Undo changes", exact: true })
.click();
assert.equal(await override.inputValue(), "");
await page.getByRole("link", { name: "← Your plans", exact: true }).click();
await page.waitForURL("**/plans*");
await page
.getByRole("link", { name: "Bro experiment", exact: true })
.click();
await page
.locator("#chat-thread")
.filter({ hasText: "Try ten extra seconds" })
.waitFor();
await page
.getByRole("button", { name: "Create a variation", exact: true })
.click();
await page.waitForURL(
(url) =>
url.pathname === "/app" && url.searchParams.get("plan") !== saved.id,
);
await page
.locator("#header-coffee-name")
.filter({ hasText: "(copy)" })
.waitFor();
assert.equal(await override.inputValue(), "1:25");
const original = (
await db.query("SELECT plan FROM roast_plans WHERE id=$1", [saved.id])
).rows[0].plan;
assert.equal(original.fields["4.4"], "");
assert.equal(original.assistant.messages.length, 2);
assert.deepEqual(errors, []);
},
);
test(
"browser: Source URL has a full row and mobile Bean Bro is a dedicated tab",
browserOptions,
async (t) => {
const { page, base, create, errors } = await session(t);
const saved = await create(warningPlan());
await page.goto(base + "/app?plan=" + saved.id);
await page.locator(".planner-layout").waitFor();
const source = page.locator('#plan-form [name="sourceUrl"]'),
coffee = page.locator('#plan-form [name="0.1"]');
const desktop = await source.boundingBox(),
coffeeBox = await coffee.boundingBox();
assert.ok(desktop.width > 400);
assert.ok(coffeeBox.width > 500);
await page.setViewportSize({ width: 390, height: 844 });
await page.waitForTimeout(350);
await page
.getByRole("button", { name: "Ask Bean Bro", exact: true })
.filter({ visible: true })
.click();
assert.equal(await page.locator("#panel-chat").isVisible(), true);
assert.equal(await page.locator(".planner-main").isVisible(), false);
await page.getByRole("button", { name: "Plan", exact: true }).click();
await page.waitForTimeout(400);
assert.equal(await page.locator(".planner-main").isVisible(), true);
assert.ok((await source.boundingBox()).width > 280);
assert.equal(
await page.evaluate(() => document.body.scrollWidth <= innerWidth),
true,
);
assert.deepEqual(errors, []);
},
);
test(
"browser: roast-day events survive an offline reload",
browserOptions,
async (t) => {
const { page, base, create, errors } = await session(t);
const saved = await create(warningPlan());
await page.goto(base + "/app?plan=" + saved.id);
await page
.getByRole("button", { name: "Roast day", exact: true })
.waitFor();
await page.evaluate(() => navigator.serviceWorker.ready);
await page.waitForFunction(
() => navigator.serviceWorker.controller !== null,
);
await page.context().setOffline(true);
await page.getByRole("button", { name: "Roast day", exact: true }).click();
await page
.getByRole("button", { name: "Start roast", exact: true })
.click();
await page
.getByRole("spinbutton", {
name: "Charge actual Fahrenheit",
exact: true,
})
.fill("345");
await page
.locator(".roast-event")
.first()
.getByRole("button", { name: "Mark event", exact: true })
.click();
await page
.getByRole("button", { name: "Close roast day", exact: true })
.click();
await page.reload();
await page.getByRole("button", { name: "Roast day", exact: true }).click();
assert.equal(
await page
.getByRole("spinbutton", {
name: "Charge actual Fahrenheit",
exact: true,
})
.inputValue(),
"345",
);
await page.context().setOffline(false);
assert.deepEqual(errors, []);
},
);
test(
"browser: coffee history compares measured curves and quick tasting persists",
browserOptions,
async (t) => {
const { page, base, create, db, errors } = await session(t);
const saved = await create(warningPlan());
const owner = (
await db.query("SELECT user_id FROM roast_plans WHERE id=$1", [saved.id])
).rows[0].user_id;
let first;
for (let i = 1; i <= 2; i++) {
const r = (
await db.query(
"INSERT INTO actual_roasts(user_id,roast_plan_id,filename,original_content,parsed,evaluation_status) VALUES($1,$2,$3,'{}',$4,'failed') RETURNING id",
[
owner,
saved.id,
`test-${i}.alog`,
{
roast: { title: `Batch ${i}` },
curve: [
{ timeS: 0, tempC: 200 },
{ timeS: 60, tempC: 90 },
{ timeS: 500, tempC: 190 },
],
derived: { firstCrackS: 450, developmentS: 50, dropS: 500 },
},
],
)
).rows[0];
first ??= r.id;
}
await page.goto(base + "/roasts?roast=" + first);
await page
.getByRole("heading", { name: "Quick taste", exact: true })
.waitFor();
await page
.locator('#after-form [name="tasteBalance"]')
.selectOption("Balanced");
await page.locator('#after-form [name="sweetness"]').selectOption("4");
await page.locator('#after-form [name="favorite"]').selectOption("yes");
await page
.getByRole("button", { name: "Save after-roast", exact: true })
.click();
await page.waitForFunction(
() => document.querySelector("#after-save").disabled === false,
);
await page.goto(
base +
"/coffees?coffee=" +
encodeURIComponent(
"coffee:" + warningPlan().fields["0.1"].toLowerCase(),
),
);
await page.getByRole("heading", { name: "Your favorite batch" }).waitFor();
await page
.getByRole("checkbox", { name: "Compare Batch 1", exact: true })
.check();
await page
.getByRole("checkbox", { name: "Compare Batch 2", exact: true })
.check();
await page
.getByRole("button", { name: "Compare selected batches" })
.click();
await page
.getByRole("img", {
name: "Measured roast curves, Fahrenheit versus time",
})
.waitFor();
assert.equal(await page.locator(".comparison-chart polyline").count(), 2);
assert.deepEqual(errors, []);
},
);
test(
"browser: finishing manual roast day opens a tasting record",
browserOptions,
async (t) => {
const { page, base, create, errors } = await session(t);
const p = warningPlan();
p.roastDay = {
startedAt: Date.now() - 600000,
events: {
charge: { timeS: 0, tempF: "345" },
fc: { timeS: 500, tempF: "360" },
drop: { timeS: 580, tempF: "375" },
},
};
const saved = await create(p, "Manual fruit batch");
await page.goto(base + "/app?plan=" + saved.id);
await page.getByRole("button", { name: "Roast day", exact: true }).click();
await page
.getByRole("button", { name: "Finish & record tasting", exact: true })
.click();
await page.waitForURL("**/roasts?roast=*");
await page
.getByRole("heading", { name: "Quick taste", exact: true })
.waitFor();
await page.getByRole("link", { name: "Download manual record" }).waitFor();
assert.equal(
await page.locator("#detail-download-updated").isVisible(),
false,
);
assert.deepEqual(errors, []);
},
);
test("browser: deleting a plan confirms in-page, supports retry, and preserves roast records", browserOptions, async (t) => {
const { page, base, create, db, csrfToken, errors } = await session(t);
const plan = blankPlan();
const saved = await create(plan, "Burundi — previous light roast plan");
const keep = await create(plan, "Keep this plan");
await page.request.put(base + "/api/plans/" + saved.id, {
headers: { "x-csrf-token": csrfToken }, data: { name: "Burundi — previous light roast plan revised" },
});
const owner = (await db.query("SELECT user_id FROM roast_plans WHERE id=$1", [saved.id])).rows[0].user_id;
const roast = (await db.query("INSERT INTO actual_roasts(user_id,roast_plan_id,filename,original_content,parsed,plan_snapshot) VALUES($1,$2,'saved.alog','{}',$3,$4) RETURNING id", [owner,saved.id,{roast:{}},plan])).rows[0];
// Browser popup suppression must not turn Delete into a silent no-op.
await page.addInitScript(() => { window.confirm = () => false; });
await page.goto(base + "/plans");
const remove = page.getByRole("button", {name: "Delete Burundi — previous light roast plan revised", exact:true});
await remove.click();
const dialog = page.getByRole("dialog", {name:"Delete this plan?"});
await dialog.waitFor();
await page.screenshot({path:"/tmp/delete-plan-desktop.png"});
await page.setViewportSize({width:390,height:844});
await page.screenshot({path:"/tmp/delete-plan-mobile.png"});
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), true);
await dialog.getByRole("button",{name:"Cancel",exact:true}).click();
assert.equal((await page.request.get(base + "/api/plans/" + saved.id)).status(),200);
await remove.click();
await page.route("**/api/plans/" + saved.id, async (route) => {
if (route.request().method() === "DELETE") await route.fulfill({status:503,contentType:"application/json",body:JSON.stringify({error:"Temporary service failure"})});
else await route.continue();
});
await dialog.getByRole("button",{name:"Delete plan",exact:true}).click();
await page.getByRole("alert").filter({hasText:"Temporary service failure"}).waitFor();
assert.equal(await dialog.isVisible(),true);
await page.unroute("**/api/plans/" + saved.id);
await dialog.getByRole("button",{name:"Delete plan",exact:true}).click();
await dialog.waitFor({state:"hidden"});
await page.waitForFunction(() => document.getElementById("plans-status").textContent === "1–1 of 1 plans");
assert.equal((await page.request.get(base + "/api/plans/" + saved.id)).status(),404);
assert.equal((await page.request.get(base + "/api/plans/" + keep.id)).status(),200);
assert.equal((await db.query("SELECT * FROM plan_revisions WHERE plan_id=$1",[saved.id])).rows.length,0);
const remaining = (await db.query("SELECT * FROM actual_roasts WHERE id=$1",[roast.id])).rows[0];
assert.equal(remaining.roast_plan_id,null);
assert.deepEqual(remaining.plan_snapshot,plan);
assert.deepEqual(errors,[]);
});