triage.test.ts

8.1 KB · raw

1// Covers §23–§24's triage: tab counts, awaiting reply, repo/type/word filters, hiding one2// follow-up, and pausing new issues per repo.34import { beforeAll, describe, expect, test } from "bun:test";5import { addEntry, fileIssue, setClosed } from "../src/issues/store.ts";6import { Browser, type Instance, instance } from "./browser.ts";7import { makeRepos } from "./fixture.ts";89describe("issue triage (§23, §24)", () => {10	let inst: Instance;11	let operator: Browser;12	let followUp: number;13	let token: string;14	const list = async (query = "") => (await operator.get(`/admin/issues${query}`)).text();15	const text = async (path: string) => (await inst.app.request(path)).text();16	const file = (repo: string, title: string, body: string, type: "bug" | "idea" | "question") =>17		fileIssue(inst.kepr, {18			repo,19			title,20			body,21			type,22			version: "",23			reporterName: "",24			contactEmail: "",25			ipHash: "x",26			filedAt: 1,27		});28	const id = (repo: string, n: number) =>29		(30			inst.kepr31				.query<{ id: number }, [string, number]>(32					"SELECT id FROM issues WHERE repo = ? AND number = ?",33				)34				.get(repo, n) as { id: number }35		).id;3637	beforeAll(async () => {38		const repos = await makeRepos(["app", "tools"]);39		inst = await instance(repos.bucketDir);40		operator = new Browser(inst.app);41		await operator.signIn();42		for (const repo of ["app", "tools"]) {43			await operator.post(`/admin/repos/${repo}`, { public: "1" });44		}45		file("app", "Crash on start", "A segfault in main", "bug");46		file("app", "Dark mode", "Please", "idea");47		token = file("app", "How to install", "Which command?", "question").token;48		file("tools", "Tools broken", "Nothing works", "bug");49		addEntry(inst.kepr, id("app", 2), "operator", "Planned.", 2);50		addEntry(inst.kepr, id("app", 3), "operator", "Use the script.", 2);51		followUp = addEntry(inst.kepr, id("app", 3), "reporter", "Buy cheap pills here", 3);52		setClosed(inst.kepr, id("tools", 1), 4);53	});5455	test("every tab carries its count", async () => {56		const page = await list();57		expect(page).toMatch(/>\s*open <span class="muted">3<\/span>/);58		expect(page).toMatch(/>\s*awaiting reply <span class="muted">2<\/span>/);59		expect(page).toMatch(/>\s*closed <span class="muted">1<\/span>/);60		expect(page).toMatch(/>\s*hidden <span class="muted">0<\/span>/);61	});6263	test("awaiting reply is the open issues where the reporter spoke last", async () => {64		const page = await list("?filter=awaiting");65		expect(page).toContain('href="/admin/issues/app/1"');66		expect(page).toContain('href="/admin/issues/app/3"');67		expect(page).not.toContain('href="/admin/issues/app/2"');68		const rows = (await list()).split('class="issue-row"');69		const row = (n: number) => rows.find((r) => r.includes(`/admin/issues/app/${n}"`)) ?? "";70		expect(row(1)).toContain("awaiting reply");71		expect(row(2)).not.toContain("awaiting reply");72	});7374	test("narrows by repo, type and words in the title or text, keeping the form filled", async () => {75		const closedTools = await list("?filter=closed&repo=tools");76		expect(closedTools).toContain('href="/admin/issues/tools/1"');77		expect(closedTools).toContain('<option value="tools" selected>');78		expect(await list("?repo=tools")).not.toContain('href="/admin/issues/app/1"');79		const ideas = await list("?type=idea");80		expect(ideas).toContain('href="/admin/issues/app/2"');81		expect(ideas).not.toContain('href="/admin/issues/app/1"');82		const words = await list("?q=SEGFAULT");83		expect(words).toContain('href="/admin/issues/app/1"');84		expect(words).not.toContain('href="/admin/issues/app/2"');85		expect(words).toContain('value="SEGFAULT"');86		expect(words).toMatch(/>\s*open <span class="muted">1<\/span>/);87		// An unknown repo or type narrows nothing rather than failing.88		expect(await list("?repo=nowhere&type=nonsense")).toContain('href="/admin/issues/app/2"');89	});9091	test("hiding one follow-up removes it publicly, keeps it marked in the console", async () => {92		const act = (fields: Record<string, string>) => operator.post("/admin/issues/app/3", fields);93		expect((await act({ action: "hide_entry", entry: String(followUp) })).status).toBe(303);94		expect(await text("/app/issues/3")).not.toContain("cheap pills");95		expect(await text("/app/issues/3")).toContain("Use the script.");96		const console = await (await operator.get("/admin/issues/app/3")).text();97		expect(console).toContain("cheap pills");98		expect(console).toMatch(/hidden[\s\S]*?cheap pills/);99		// The hidden follow-up no longer counts as the reporter speaking last.100		expect(await list("?filter=awaiting")).not.toContain('href="/admin/issues/app/3"');101		expect((await act({ action: "unhide_entry", entry: String(followUp) })).status).toBe(303);102		expect(await text("/app/issues/3")).toContain("cheap pills");103		const other = operator.post("/admin/issues/app/1", {104			action: "hide_entry",105			entry: String(followUp),106		});107		expect((await other).status).toBe(404);108		expect((await act({ action: "hide_entry", entry: "x" })).status).toBe(404);109	});110111	test("pausing new issues stops filing but keeps the list and follow-ups", async () => {112		const pause = await operator.post("/admin/repos/app", {113			public: "1",114			issues_paused: "1",115		});116		expect(pause.status).toBe(303);117		expect(await (await operator.get("/admin/repos?repo=app")).text()).toMatch(118			/name="issues_paused" value="1" checked/,119		);120		const form = await inst.app.request("/app/issues/new");121		expect(form.status).toBe(200);122		const formText = await form.text();123		expect(formText).toContain("app is not taking new issues right now.");124		expect(formText).not.toContain('name="title"');125		const filed = await inst.app.request("/app/issues/new", {126			method: "POST",127			body: new URLSearchParams({ title: "t", body: "b", type: "bug" }),128		});129		expect(filed.status).toBe(403);130		expect(await text("/app/issues")).not.toContain("report an issue →");131		expect(await text("/app/issues")).toContain('href="/app/issues/1"');132		expect(await text(`/app/issues/3?token=${token}`)).toContain('name="body"');133		await operator.post("/admin/repos/app", { public: "1" });134		expect(await text("/app/issues/new")).toContain('name="title"');135	});136137	test("ticked issues are closed, reopened, hidden and unhidden together", async () => {138		const ticked = [id("app", 1), id("app", 2)];139		const bulk = (action: string, issues = ticked) => {140			const form = new FormData();141			form.set("action", action);142			form.set("filter", "closed");143			for (const issue of issues) form.append("issue", String(issue));144			return operator.post("/admin/issues", form);145		};146		const status = () =>147			inst.kepr148				.query("SELECT status, hidden FROM issues WHERE id IN (?, ?) ORDER BY id")149				.all(...ticked);150		expect(await list()).toContain(`name="issue" value="${ticked[0]}"`);151		const closed = await bulk("close");152		expect(closed.status).toBe(303);153		expect(closed.headers.get("location")).toBe("/admin/issues?filter=closed");154		expect(status()).toEqual([155			{ status: "closed", hidden: 0 },156			{ status: "closed", hidden: 0 },157		]);158		await bulk("hide");159		expect(status().map((row) => (row as { hidden: number }).hidden)).toEqual([1, 1]);160		await bulk("unhide");161		await bulk("reopen");162		expect(status()).toEqual([163			{ status: "open", hidden: 0 },164			{ status: "open", hidden: 0 },165		]);166		expect((await bulk("close", [999_999])).status).toBe(404);167		expect((await bulk("explode")).status).toBe(404);168	});169170	test("blocking a reporter lists the block under the tabs until it is lifted", async () => {171		expect(await list()).not.toContain("blocked reporters");172		const blocked = await operator.post("/admin/issues/app/2", { action: "block" });173		expect(blocked.status).toBe(303);174		const issue = await (await operator.get("/admin/issues/app/2")).text();175		expect(issue).toContain("reporter blocked");176		const page = await list();177		expect(page).toContain("blocked reporters");178		expect(page).toContain('href="/admin/issues/app/2"');179		const block = /name="block" value="(\d+)"/.exec(page)?.[1] ?? "";180		expect(block).not.toBe("");181		expect((await operator.post("/admin/issues", { action: "unblock", block: "999" })).status).toBe(182			404,183		);184		expect((await operator.post("/admin/issues", { action: "unblock", block })).status).toBe(303);185		expect(await list()).not.toContain("blocked reporters");186	});187});