Files
diegosouzapw--omniroute/tests/e2e/group-b-activity-feed.spec.ts
2026-07-13 13:39:12 +08:00

97 lines
3.5 KiB
TypeScript

/**
* Group B — Activity Feed E2E spec.
*
* Validates that the new /dashboard/activity page (Group B, plan 16 F4) renders
* correctly: header visible, timeline container present, and the page responds
* with a 200 (not a redirect or error).
*
* Backend is mocked so this spec does not require a running upstream.
*/
import { test, expect } from "@playwright/test";
import { gotoDashboardRoute } from "./helpers/dashboardAuth";
test.describe("Group B — Activity Feed", () => {
test.beforeEach(async ({ page }) => {
// Mock the audit-log endpoint used by the Activity feed
await page.route("**/api/compliance/audit-log**", async (route) => {
const url = new URL(route.request().url());
const level = url.searchParams.get("level");
if (level === "high") {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
entries: [
{
id: "1",
action: "provider.added",
actor: "admin",
target: "codex",
severity: "info",
timestamp: new Date().toISOString(),
metadata: {},
},
],
total: 1,
}),
});
} else {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ entries: [], total: 0 }),
});
}
});
});
test("activity page exists and returns 200", async ({ page }) => {
const response = await page.goto("http://localhost:20128/dashboard/activity", {
waitUntil: "domcontentloaded",
});
// After login redirect the page should settle on activity or login
expect(response?.status()).not.toBe(404);
expect(response?.status()).not.toBe(500);
});
test("activity page renders header and timeline container", async ({ page }) => {
await gotoDashboardRoute(page, "/dashboard/activity");
// The page header should be visible (h1 or title element)
const heading = page
.locator("h1, [data-testid='activity-title']")
.first();
await expect(heading).toBeVisible({ timeout: 15000 });
// ActivityFeed renders <div role="status"> (empty state) or a
// <div class="divide-y..."> with a nested <ul class="divide-y..."> (entries).
// Match those feed-specific shapes (plus the legacy testids). Deliberately
// NOT matching a generic container like `div.rounded-xl`, which exists on
// many pages (incl. the /login card) and would let the test pass even when
// the dashboard redirected to login without rendering the feed.
const feedContainer = page.locator(
"[data-testid='activity-feed'], [data-testid='activity-empty-state']," +
" .activity-feed, [role='status'], [role='list'], ul.divide-y, div.divide-y"
);
await expect(feedContainer.first()).toBeVisible({ timeout: 15000 });
});
test("activity page does not show raw error stack traces", async ({ page }) => {
// Simulate a backend error to ensure error sanitization (Hard Rule #12)
await page.route("**/api/compliance/audit-log**", async (route) => {
await route.fulfill({
status: 500,
contentType: "application/json",
body: JSON.stringify({ error: { message: "Internal error" } }),
});
});
await gotoDashboardRoute(page, "/dashboard/activity");
const pageContent = await page.content();
// Stack traces should never appear in the UI (Hard Rule #12)
expect(pageContent).not.toMatch(/\s+at\s+\//);
});
});