Files
storkit/frontend/tests/e2e/test-protection.spec.ts
Dave 2c3003d721 Story 28: Show remaining test TODOs in the UI
Add TodoPanel that displays unchecked acceptance criteria from current
story files. Backend parses `- [ ]` lines from markdown, frontend
shows them in a panel with refresh. Includes 4 Rust unit tests,
3 Vitest tests, 3 Playwright E2E tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 15:33:45 +00:00

251 lines
6.9 KiB
TypeScript

import { expect, test } from "@playwright/test";
import type {
AcceptanceResponse,
ReviewListResponse,
} from "../../src/api/workflow";
/**
* Helper: mock all API routes needed to render the Chat view.
*/
function mockChatApis(
page: import("@playwright/test").Page,
overrides: {
acceptance?: AcceptanceResponse;
reviewQueue?: ReviewListResponse;
} = {},
) {
const acceptance: AcceptanceResponse = overrides.acceptance ?? {
can_accept: false,
reasons: ["No test results recorded for the story."],
warning: null,
summary: { total: 0, passed: 0, failed: 0 },
missing_categories: ["unit", "integration"],
};
const reviewQueue: ReviewListResponse = overrides.reviewQueue ?? {
stories: [],
};
return Promise.all([
page.route("**/api/projects", (route) =>
route.fulfill({ json: ["/tmp/test-project"] }),
),
page.route("**/api/io/fs/home", (route) => route.fulfill({ json: "/tmp" })),
page.route("**/api/project", (route) => {
if (route.request().method() === "POST") {
return route.fulfill({ json: "/tmp/test-project" });
}
if (route.request().method() === "DELETE") {
return route.fulfill({ json: true });
}
return route.fulfill({ json: null });
}),
page.route("**/api/ollama/models**", (route) =>
route.fulfill({ json: ["llama3.1"] }),
),
page.route("**/api/anthropic/key/exists", (route) =>
route.fulfill({ json: false }),
),
page.route("**/api/anthropic/models", (route) =>
route.fulfill({ json: [] }),
),
page.route("**/api/model", (route) => {
if (route.request().method() === "POST") {
return route.fulfill({ json: true });
}
return route.fulfill({ json: null });
}),
page.route("**/api/workflow/acceptance", (route) => {
if (route.request().url().includes("/ensure")) return route.fallback();
return route.fulfill({ json: acceptance });
}),
page.route("**/api/workflow/review/all", (route) =>
route.fulfill({ json: reviewQueue }),
),
page.route("**/api/workflow/acceptance/ensure", (route) =>
route.fulfill({ json: true }),
),
page.route("**/api/io/fs/list/absolute**", (route) =>
route.fulfill({ json: [] }),
),
page.route("**/api/workflow/todos", (route) =>
route.fulfill({ json: { stories: [] } }),
),
]);
}
async function openProject(page: import("@playwright/test").Page) {
await page.goto("/");
await page.getByPlaceholder("/path/to/project").fill("/tmp/test-project");
await page.getByRole("button", { name: "Open Project" }).click();
await expect(page.getByText("Workflow Gates", { exact: true })).toBeVisible();
}
test.describe("Coverage threshold (AC1)", () => {
test("shows blocked status when coverage is below threshold", async ({
page,
}) => {
await mockChatApis(page, {
acceptance: {
can_accept: false,
reasons: ["Coverage below threshold (55.0% < 80.0%)."],
warning: null,
summary: { total: 3, passed: 3, failed: 0 },
missing_categories: [],
coverage_report: {
current_percent: 55.0,
threshold_percent: 80.0,
baseline_percent: null,
},
},
});
await openProject(page);
await expect(page.getByText("Blocked").first()).toBeVisible();
await expect(page.getByText(/Coverage: 55\.0%/)).toBeVisible();
await expect(page.getByText(/threshold: 80\.0%/)).toBeVisible();
await expect(
page.getByText("Coverage below threshold (55.0% < 80.0%)."),
).toBeVisible();
});
test("shows green coverage when above threshold", async ({ page }) => {
await mockChatApis(page, {
acceptance: {
can_accept: true,
reasons: [],
warning: null,
summary: { total: 5, passed: 5, failed: 0 },
missing_categories: [],
coverage_report: {
current_percent: 92.0,
threshold_percent: 80.0,
baseline_percent: 90.0,
},
},
});
await openProject(page);
await expect(page.getByText("Ready to accept")).toBeVisible();
await expect(page.getByText(/Coverage: 92\.0%/)).toBeVisible();
});
});
test.describe("Coverage regression reporting (AC2)", () => {
test("review panel shows coverage regression reason", async ({ page }) => {
await mockChatApis(page, {
reviewQueue: {
stories: [
{
story_id: "27_protect_tests_and_coverage",
can_accept: false,
reasons: ["Coverage regression: 90.0% → 82.0% (threshold: 80.0%)."],
warning: null,
summary: { total: 4, passed: 4, failed: 0 },
missing_categories: [],
coverage_report: {
current_percent: 82.0,
threshold_percent: 80.0,
baseline_percent: 90.0,
},
},
],
},
});
await openProject(page);
await expect(
page.getByText(/Coverage regression.*90\.0%.*82\.0%/),
).toBeVisible();
await expect(page.getByText(/Coverage: 82\.0%/)).toBeVisible();
const blockedButton = page.getByRole("button", { name: "Blocked" });
await expect(blockedButton).toBeDisabled();
});
test("blocked acceptance with coverage and test failures combined", async ({
page,
}) => {
await mockChatApis(page, {
acceptance: {
can_accept: false,
reasons: [
"1 test(s) are failing; acceptance is blocked.",
"Coverage below threshold (65.0% < 80.0%).",
],
warning: null,
summary: { total: 4, passed: 3, failed: 1 },
missing_categories: [],
coverage_report: {
current_percent: 65.0,
threshold_percent: 80.0,
baseline_percent: null,
},
},
});
await openProject(page);
await expect(page.getByText("Blocked").first()).toBeVisible();
await expect(
page.getByText("Coverage below threshold (65.0% < 80.0%)."),
).toBeVisible();
await expect(page.getByText(/Coverage: 65\.0%/)).toBeVisible();
await expect(page.getByText(/1 test\(s\) are failing/)).toBeVisible();
});
});
test.describe("Coverage collection (E2E)", () => {
test("collect coverage button triggers collection and displays result", async ({
page,
}) => {
await mockChatApis(page);
// Mock the collect coverage endpoint
await page.route("**/api/workflow/coverage/collect", (route) =>
route.fulfill({
json: {
current_percent: 85.0,
threshold_percent: 80.0,
baseline_percent: null,
},
}),
);
await openProject(page);
// Click the Collect Coverage button
const collectButton = page.getByRole("button", {
name: "Collect Coverage",
});
await expect(collectButton).toBeVisible();
// Override acceptance to return coverage data after collection
await page.unroute("**/api/workflow/acceptance");
await page.route("**/api/workflow/acceptance", (route) => {
if (route.request().url().includes("/ensure")) return route.fallback();
return route.fulfill({
json: {
can_accept: true,
reasons: [],
warning: null,
summary: { total: 5, passed: 5, failed: 0 },
missing_categories: [],
coverage_report: {
current_percent: 85.0,
threshold_percent: 80.0,
baseline_percent: null,
},
},
});
});
await collectButton.click();
await expect(page.getByText(/Coverage: 85\.0%/)).toBeVisible();
});
});