Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions tests/e2e/it_aggregates_subagents.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { expect, test } from "@playwright/test";

test.describe("subagent aggregation", () => {
test("child session is not rendered as a separate session card", async ({
page,
}) => {
await page.goto("/");

// The child session has title "@explore subagent" - it should NOT appear as its own card
const cards = page.locator(".session-card");
const count = await cards.count();

// We seeded 2 root sessions + 1 child. Only 2 cards should render.
expect(count).toBe(2);

// Verify the child title does not appear as a standalone card title
await expect(
page.locator(".session-title", { hasText: "@explore subagent" }),
).not.toBeVisible();
});

test("parent session card shows agent row from subagent", async ({
page,
}) => {
await page.goto("/");

const parentCard = page.locator(".session-card", {
hasText: "E2E Session",
});

// The child session title "@explore subagent" should produce an "explore" agent badge
await expect(
parentCard.locator(".agent-badge", { hasText: "explore" }),
).toBeVisible();
});

test("parent session tokens include child session tokens", async ({
page,
}) => {
await page.goto("/");

const parentCard = page.locator(".session-card", {
hasText: "E2E Session",
});
const tokenIn = parentCard.locator(".session-tokens .token-in");
await expect(tokenIn).toBeVisible();
const text = await tokenIn.textContent();

// Parent input=2000, cache=1000 + child input=400, cache=200
// renderTokens shows totalIn = input+cache = 2400+1200 = 3600 => "3.6k in"
expect(text).toContain("3.6k");
});
});
31 changes: 31 additions & 0 deletions tests/e2e/it_applies_recency_styling.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { expect, test } from "@playwright/test";

test.describe("session recency styling", () => {
test("active session (last seen < 5 min ago) has active class", async ({
page,
}) => {
await page.goto("/");

// session-e2e-1 was last seen 2 min ago
const activeCard = page.locator(".session-card", {
hasText: "E2E Session",
});
await expect(activeCard).toBeVisible();
await expect(activeCard).toHaveClass(/session-card--active/);
});

test("old session (last seen > 24h ago) has no recency class", async ({
page,
}) => {
await page.goto("/");

// session-e2e-2 was last seen 25 hours ago (> 24h = no class applied)
const oldCard = page.locator(".session-card", { hasText: "Other Session" });
await expect(oldCard).toBeVisible();

const classList = await oldCard.getAttribute("class");
expect(classList).not.toMatch(
/session-card--(active|recent|idle|stale|old)/,
);
});
});
37 changes: 37 additions & 0 deletions tests/e2e/it_auto_refreshes.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { expect, test } from "@playwright/test";

test.describe("auto-refresh", () => {
test("page contains auto-refresh indicator", async ({ page }) => {
await page.goto("/");

await expect(page.locator(".refresh-badge")).toBeVisible();
await expect(page.getByText("auto-refresh 5s")).toBeVisible();
});

test("auto-refresh fetches /api/stats periodically", async ({ page }) => {
await page.goto("/");

// Wait for an auto-refresh fetch to /api/stats
const response = await page.waitForResponse(
(resp) => resp.url().includes("/api/stats") && resp.status() === 200,
{ timeout: 10_000 },
);

expect(response.ok()).toBe(true);
});

test("refresh timing indicator updates after refresh", async ({ page }) => {
await page.goto("/");

// Wait for a refresh cycle
await page.waitForResponse((resp) => resp.url().includes("/api/stats"), {
timeout: 10_000,
});

const timing = page.locator("#refresh-timing");
await expect(timing).not.toBeEmpty();

const text = await timing.textContent();
expect(text).toMatch(/took \d+ms/);
});
});
73 changes: 73 additions & 0 deletions tests/e2e/it_filters_by_directory.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { expect, test } from "@playwright/test";

test.describe("directory filter", () => {
test("renders directory filter dropdown", async ({ page }) => {
await page.goto("/");

const dropdown = page.locator("#dir-filter");
await expect(dropdown).toBeVisible();
});

test("dropdown has 'All directories' as default option", async ({ page }) => {
await page.goto("/");

const defaultOption = page.locator("#dir-filter option[value='']");
await expect(defaultOption).toHaveText("All directories");
});

test("dropdown lists seeded directories", async ({ page }) => {
await page.goto("/");

const dropdown = page.locator("#dir-filter");
const options = dropdown.locator("option");
const count = await options.count();

// "All directories" + at least 2 seeded dirs
expect(count).toBeGreaterThanOrEqual(3);

const texts = await options.allTextContents();
expect(texts).toContain("/tmp/e2e-project");
expect(texts).toContain("/tmp/e2e-other");
});

test("selecting a directory filters sessions", async ({ page }) => {
await page.goto("/");

// Initially both sessions visible
await expect(
page.locator(".session-card", { hasText: "E2E Session" }),
).toBeVisible();
await expect(
page.locator(".session-card", { hasText: "Other Session" }),
).toBeVisible();

// Select /tmp/e2e-other
await page.locator("#dir-filter").selectOption("/tmp/e2e-other");

// Wait for refresh to apply the filter
await page.waitForResponse((resp) =>
resp.url().includes("/api/stats?dir="),
);

// Only "Other Session" should be visible
await expect(
page.locator(".session-card", { hasText: "Other Session" }),
).toBeVisible();
await expect(
page.locator(".session-card", { hasText: "E2E Session" }),
).not.toBeVisible();
});

test("page loaded with ?dir= query param filters sessions", async ({
page,
}) => {
await page.goto("/?dir=/tmp/e2e-other");

await expect(
page.locator(".session-card", { hasText: "Other Session" }),
).toBeVisible();
await expect(
page.locator(".session-card", { hasText: "E2E Session" }),
).not.toBeVisible();
});
});
75 changes: 75 additions & 0 deletions tests/e2e/it_renders_daily_chart.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { expect, test } from "@playwright/test";

test.describe("daily chart", () => {
test("renders daily token usage chart with title", async ({ page }) => {
await page.goto("/");

const chart = page.locator(".daily-chart").first();
await expect(chart).toBeVisible();
await expect(
chart.locator(".chart-title", {
hasText: "Daily Token Usage (last 60 days)",
}),
).toBeVisible();
});

test("renders bar columns in chart container", async ({ page }) => {
await page.goto("/");

const container = page
.locator(".daily-chart")
.first()
.locator(".chart-container");
await expect(container).toBeVisible();

const cols = container.locator(".chart-col");
// 60 days of bars
await expect(cols).toHaveCount(60);
});

test("renders 5-day moving average SVG polyline", async ({ page }) => {
await page.goto("/");

const svg = page
.locator(".daily-chart")
.first()
.locator("svg.chart-avg-line");
await expect(svg).toBeVisible();

const polyline = svg.locator("polyline");
await expect(polyline).toHaveCount(1);

const points = await polyline.getAttribute("points");
expect(points).toBeTruthy();
expect(points!.split(" ").length).toBe(60);
});

test("renders chart legend with daily tokens and 5-day avg", async ({
page,
}) => {
await page.goto("/");

const legend = page
.locator(".daily-chart")
.first()
.locator(".chart-legend");
await expect(
legend.locator(".legend-item", { hasText: "Daily tokens" }),
).toBeVisible();
await expect(
legend.locator(".legend-item", { hasText: "5-day avg" }),
).toBeVisible();
});

test("shows non-zero bars for seeded days", async ({ page }) => {
await page.goto("/");

// At least some bars should have height > 0 (non-empty style)
const barsWithHeight = page
.locator(".daily-chart")
.first()
.locator(".chart-bar[style*='height']");
const count = await barsWithHeight.count();
expect(count).toBeGreaterThan(0);
});
});
48 changes: 48 additions & 0 deletions tests/e2e/it_renders_model_chart.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { expect, test } from "@playwright/test";

test.describe("model chart", () => {
test("renders model chart with title", async ({ page }) => {
await page.goto("/");

// The model chart is the second .daily-chart
const modelChart = page.locator(".daily-chart").nth(1);
await expect(modelChart).toBeVisible();
await expect(
modelChart.locator(".chart-title", {
hasText: "Daily Token Usage by Model",
}),
).toBeVisible();
});

test("renders 60 bar columns", async ({ page }) => {
await page.goto("/");

const modelChart = page.locator(".daily-chart").nth(1);
const cols = modelChart.locator(".chart-container .chart-col");
await expect(cols).toHaveCount(60);
});

test("renders stacked bar segments for days with data", async ({ page }) => {
await page.goto("/");

const modelChart = page.locator(".daily-chart").nth(1);
const segments = modelChart.locator(".model-bar-seg");
const count = await segments.count();
expect(count).toBeGreaterThan(0);
});

test("renders legend with seeded model names", async ({ page }) => {
await page.goto("/");

const modelChart = page.locator(".daily-chart").nth(1);
const legend = modelChart.locator(".chart-legend");

// We seeded gpt-5.3-codex and claude-sonnet-4
await expect(
legend.locator(".legend-item", { hasText: "gpt-5.3-codex" }),
).toBeVisible();
await expect(
legend.locator(".legend-item", { hasText: "claude-sonnet-4" }),
).toBeVisible();
});
});
Loading