Files
wehub-resource-sync f99010fae1
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Windows) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:36 +08:00

1692 lines
55 KiB
TypeScript
Raw Permalink 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, expect, Page, Locator } from '@playwright/test';
import { join } from 'path';
const DIR = process.env.SCREENSHOT_DIR || join(
__dirname, '..', '..', 'assets', 'generated', 'screenshots'
);
const FULL = { width: 1440, height: 900 };
// PG serve instance for pg-sync screenshots (machine labels, etc.)
const PG_BASE_URL = process.env.PG_BASE_URL || '';
async function snap(page: Page, name: string) {
await page.screenshot({
path: join(DIR, `${name}.png`),
type: 'png',
});
}
async function snapEl(loc: Locator, name: string) {
await loc.screenshot({
path: join(DIR, `${name}.png`),
type: 'png',
});
}
async function waitForApp(page: Page) {
await page.goto('/');
await page.waitForSelector('.session-item', {
timeout: 15_000,
});
// Let analytics charts render
await page.waitForTimeout(3000);
}
async function setDateRange1Y(page: Page) {
const btn = page.locator('.preset-btn', { hasText: '1y' });
if (await btn.count() > 0) {
await btn.click();
await page.waitForTimeout(3000);
}
}
async function selectFirstSession(page: Page) {
const items = page.locator('.session-item');
await items.first().click();
await page.waitForSelector('.message', { timeout: 10_000 });
await page.waitForTimeout(1000);
}
// Find a session likely to have thinking/tool content
// by looking for ones with higher message counts
async function selectRichSession(page: Page) {
const items = page.locator('.session-item');
const count = await items.count();
// Try the 3rd session (index 2) for variety, fall back to first
const idx = Math.min(2, count - 1);
await items.nth(idx).click();
await page.waitForSelector('.message', { timeout: 10_000 });
await page.waitForTimeout(1000);
}
// ── Dashboard / Analytics ───────────────────────────────
test.describe('Dashboard', () => {
test.beforeEach(async ({ page }) => {
await page.setViewportSize(FULL);
await waitForApp(page);
await setDateRange1Y(page);
});
test('full dashboard', async ({ page }) => {
await snap(page, 'dashboard');
});
test('summary cards', async ({ page }) => {
const el = page.locator('.summary-cards');
if (await el.count() > 0) {
await snapEl(el, 'summary-cards');
}
});
test('date range and toolbar', async ({ page }) => {
const el = page.locator('.analytics-toolbar');
if (await el.count() > 0) {
await snapEl(el, 'date-range');
}
});
test('activity heatmap', async ({ page }) => {
const heatmap = page.locator('.heatmap-container');
if (await heatmap.count() > 0) {
// Wait for SVG cells to render
await page.waitForSelector('.heatmap-cell', {
timeout: 10_000,
});
await page.waitForTimeout(500);
await snapEl(heatmap, 'heatmap');
}
});
test('heatmap click-to-filter', async ({ page }) => {
// Click a cell with data (has .clickable class)
const clickable = page.locator('.heatmap-cell.clickable');
if (await clickable.count() > 0) {
await clickable.first().click();
await page.waitForTimeout(2000);
await snap(page, 'heatmap-filtered');
// Click again to deselect
const selected = page.locator('.heatmap-cell.selected');
if (await selected.count() > 0) {
await selected.first().click();
await page.waitForTimeout(500);
}
}
});
test('hour of week heatmap', async ({ page }) => {
const panels = page.locator('.chart-panel');
const count = await panels.count();
for (let i = 0; i < count; i++) {
const text = await panels.nth(i).textContent();
if (text && (text.includes('Hour') || text.includes('Week'))) {
await snapEl(panels.nth(i), 'hour-of-week');
break;
}
}
});
test('activity timeline', async ({ page }) => {
const timeline = page.locator('.timeline-container');
if (await timeline.count() > 0) {
await timeline.scrollIntoViewIfNeeded();
await page.waitForSelector('.timeline-svg', {
timeout: 10_000,
});
await page.waitForTimeout(500);
// Capture the parent chart-panel that wraps the timeline
const panel = page.locator(
'.chart-panel:has(.timeline-container)'
);
if (await panel.count() > 0) {
await snapEl(panel, 'activity-timeline');
}
}
});
test('top sessions', async ({ page }) => {
// Scroll down to find top sessions
const content = page.locator('.analytics-content');
await content.evaluate(
(el) => el.scrollTo(0, el.scrollHeight / 3)
);
await page.waitForTimeout(500);
const panels = page.locator('.chart-panel');
const count = await panels.count();
for (let i = 0; i < count; i++) {
const text = await panels.nth(i).textContent();
if (text && text.includes('Top')) {
await panels.nth(i).scrollIntoViewIfNeeded();
await page.waitForTimeout(300);
await snapEl(panels.nth(i), 'top-sessions');
break;
}
}
});
test('project breakdown', async ({ page }) => {
const content = page.locator('.analytics-content');
await content.evaluate(
(el) => el.scrollTo(0, el.scrollHeight / 3)
);
await page.waitForTimeout(500);
const panels = page.locator('.chart-panel');
const count = await panels.count();
for (let i = 0; i < count; i++) {
const text = await panels.nth(i).textContent();
if (text && text.includes('Project')) {
await panels.nth(i).scrollIntoViewIfNeeded();
await page.waitForTimeout(300);
await snapEl(panels.nth(i), 'project-breakdown');
break;
}
}
});
test('session shape', async ({ page }) => {
const content = page.locator('.analytics-content');
await content.evaluate(
(el) => el.scrollTo(0, el.scrollHeight / 2)
);
await page.waitForTimeout(500);
const panels = page.locator('.chart-panel');
const count = await panels.count();
for (let i = 0; i < count; i++) {
const text = await panels.nth(i).textContent();
if (text && (text.includes('Shape') || text.includes('Distribution'))) {
await panels.nth(i).scrollIntoViewIfNeeded();
await page.waitForTimeout(300);
await snapEl(panels.nth(i), 'session-shape');
break;
}
}
});
test('tool usage', async ({ page }) => {
const content = page.locator('.analytics-content');
await content.evaluate(
(el) => el.scrollTo(0, (el.scrollHeight * 2) / 3)
);
await page.waitForTimeout(500);
const panels = page.locator('.chart-panel');
const count = await panels.count();
for (let i = 0; i < count; i++) {
const text = await panels.nth(i).textContent();
if (text && text.includes('Tool')) {
await panels.nth(i).scrollIntoViewIfNeeded();
await page.waitForTimeout(300);
await snapEl(panels.nth(i), 'tool-usage');
break;
}
}
});
test('top skills', async ({ page }) => {
const panel = page.locator('.chart-panel:has(.skills-container)');
await expect(panel).toBeVisible({ timeout: 10_000 });
await panel.scrollIntoViewIfNeeded();
await page.waitForTimeout(500);
await snapEl(panel, 'top-skills');
});
test('velocity metrics', async ({ page }) => {
const content = page.locator('.analytics-content');
await content.evaluate(
(el) => el.scrollTo(0, (el.scrollHeight * 3) / 4)
);
await page.waitForTimeout(500);
const panels = page.locator('.chart-panel');
const count = await panels.count();
for (let i = 0; i < count; i++) {
const text = await panels.nth(i).textContent();
if (text && text.includes('Velocity')) {
await panels.nth(i).scrollIntoViewIfNeeded();
await page.waitForTimeout(300);
await snapEl(panels.nth(i), 'velocity');
break;
}
}
});
test('agent comparison', async ({ page }) => {
const content = page.locator('.analytics-content');
await content.evaluate(
(el) => el.scrollTo(0, el.scrollHeight)
);
await page.waitForTimeout(500);
const panels = page.locator('.chart-panel');
const count = await panels.count();
for (let i = 0; i < count; i++) {
const text = await panels.nth(i).textContent();
if (text && text.includes('Comparison')) {
await panels.nth(i).scrollIntoViewIfNeeded();
await page.waitForTimeout(300);
await snapEl(panels.nth(i), 'agent-comparison');
break;
}
}
});
test('dashboard model filter', async ({ page }) => {
// The dashboard toolbar Model dropdown reuses the shared
// FilterDropdown (label "Model") in include mode. Open it and
// include the first listed model so the screenshot shows the open
// panel, the resulting "Model: <name>" trigger label, and the
// active-filter chip the ActiveFilters row renders beneath it.
const modelDropdown = page.locator(
'.analytics-toolbar .kit-filter-dropdown',
{ hasText: 'Model' }
);
const trigger = modelDropdown.locator('.kit-filter-dropdown__btn');
await expect(trigger).toBeVisible({ timeout: 5_000 });
await trigger.click();
await modelDropdown.locator('.kit-filter-dropdown__panel').waitFor(
{ state: 'visible', timeout: 5_000 }
);
// In include mode the first dropdown item is the "All models"
// reset row; the actual models follow. Include the first real
// model if one exists so a filter chip appears.
const rows = modelDropdown.locator('.kit-filter-dropdown__item');
if (await rows.count() > 1) {
await rows.nth(1).click();
await page.waitForTimeout(1500);
}
await snap(page, 'analytics-model-filter');
// Clean up: clicking the "All models" row clears the filter.
if (await rows.count() > 0) {
await rows.first().click();
}
await page.keyboard.press('Escape');
});
});
// ── Activity dashboard ───────────────────────────────────
test.describe('Activity dashboard', () => {
async function navigateToActivity(page: Page, path = '/activity') {
await page.goto(path);
await page.waitForSelector('.activity-page', { timeout: 10_000 });
await expect(
page.locator('.activity-page .summary-cards .card').first()
).toBeVisible({ timeout: 15_000 });
await expect(
page.locator('.activity-page .timeline-svg')
).toBeVisible({ timeout: 15_000 });
await page.waitForTimeout(2000);
}
test.beforeEach(async ({ page }) => {
await page.setViewportSize(FULL);
await waitForApp(page);
});
test('daily activity page', async ({ page }) => {
await navigateToActivity(page);
await snap(page, 'activity-page');
});
test('weekly activity page', async ({ page }) => {
await navigateToActivity(page, '/activity?preset=week');
await snap(page, 'activity-week');
});
test('activity concurrency chart', async ({ page }) => {
await navigateToActivity(page, '/activity?preset=week');
const panel = page.locator('.activity-page .chart-panel:has(.timeline)');
await expect(panel).toBeVisible({ timeout: 5_000 });
await panel.scrollIntoViewIfNeeded();
await page.waitForTimeout(500);
await snapEl(panel, 'activity-concurrency');
});
test('activity sessions table', async ({ page }) => {
await navigateToActivity(page, '/activity?preset=week');
const panel = page.locator(
'.activity-page .chart-panel:has(.sessions-table)'
);
await expect(panel).toBeVisible({ timeout: 5_000 });
await panel.scrollIntoViewIfNeeded();
await page.waitForTimeout(500);
await snapEl(panel, 'activity-sessions');
});
test('activity breakdowns', async ({ page }) => {
await navigateToActivity(page, '/activity?preset=week');
const panel = page.locator('.activity-page .chart-panel:has(.breakdowns)');
await expect(panel).toBeVisible({ timeout: 5_000 });
await panel.scrollIntoViewIfNeeded();
await page.waitForTimeout(500);
await snapEl(panel, 'activity-breakdowns');
});
test('activity insight', async ({ page }) => {
await navigateToActivity(page, '/activity?preset=week');
const panel = page.locator(
'.activity-page .chart-panel:has(.activity-insight)'
);
await expect(panel).toBeVisible({ timeout: 10_000 });
await panel.scrollIntoViewIfNeeded();
await page.waitForTimeout(500);
await snapEl(panel, 'activity-insight');
});
});
// ── Session browser ─────────────────────────────────────
test.describe('Session browser', () => {
test.beforeEach(async ({ page }) => {
await page.setViewportSize(FULL);
await waitForApp(page);
});
test('session list', async ({ page }) => {
const sidebar = page.locator('.sidebar');
if (await sidebar.count() > 0) {
await snapEl(sidebar, 'session-list');
}
});
test('project filter', async ({ page }) => {
// The project filter is a typeahead in the header. Open it and pick the
// first real project (index 0 is the "All Projects" option).
const trigger = page.locator(
'.project-picker .kit-typeahead__trigger'
).first();
await trigger.click();
await page.waitForSelector(
'.project-picker .kit-typeahead__option',
{ timeout: 5_000 }
);
const options = page.locator(
'.project-picker .kit-typeahead__option'
);
if (await options.count() > 1) {
await options.nth(1).click();
await page.waitForTimeout(1000);
await snap(page, 'session-filtered');
}
});
test('session filter dropdown', async ({ page }) => {
const filterBtn = page.locator('button.filter-btn');
if (await filterBtn.count() > 0) {
await filterBtn.click();
await page.waitForTimeout(300);
await snap(page, 'session-filters');
}
});
test('session filters active', async ({ page }) => {
const filterBtn = page.locator('button.filter-btn');
if (await filterBtn.count() > 0) {
await filterBtn.click();
await page.waitForTimeout(300);
// Select "Min Prompts 10" filter
const minPrompts = page.getByRole('button', {
name: '10',
exact: true,
});
if (await minPrompts.count() > 0) {
await minPrompts.click();
await page.waitForTimeout(300);
}
// Select an agent filter
const agentBtn = page.locator(
'button.agent-filter-btn'
).first();
if (await agentBtn.count() > 0) {
await agentBtn.click();
await page.waitForTimeout(300);
}
await snap(page, 'session-filters-active');
// Clean up
const clear = page.locator('button.clear-filters-btn');
if (await clear.count() > 0) await clear.click();
}
});
test('starred session', async ({ page }) => {
// Star the first session using 's' key
await selectFirstSession(page);
await page.keyboard.press('s');
await page.waitForTimeout(500);
const sidebar = page.locator('.sidebar');
if (await sidebar.count() > 0) {
await snapEl(sidebar, 'starred-session');
}
// Unstar to clean up
await page.keyboard.press('s');
});
test('group by agent', async ({ page }) => {
// Find and click the group-by-agent toggle
const groupBtn = page.locator(
'button[title*="group"], button[title*="Group"]'
);
if (await groupBtn.count() > 0) {
await groupBtn.click();
await page.waitForTimeout(500);
// Expand the first group
const groupHeader = page.locator(
'.agent-group-header'
).first();
if (await groupHeader.count() > 0) {
await groupHeader.click();
await page.waitForTimeout(300);
}
const sidebar = page.locator('.sidebar');
await snapEl(sidebar, 'group-by-agent');
// Toggle off to clean up
await groupBtn.click();
}
});
});
// ── Message viewer ──────────────────────────────────────
test.describe('Message viewer', () => {
test.beforeEach(async ({ page }) => {
await page.setViewportSize(FULL);
await waitForApp(page);
});
async function findVisibleToolBlock(page: Page): Promise<Locator> {
let tool = page.locator('.tool-block').first();
if (await tool.count() > 0 && await tool.isVisible()) {
return tool;
}
const rows = page.locator('.message, .virtual-row');
const count = await rows.count();
for (let i = 0; i < Math.min(count, 60); i++) {
await rows.nth(i).scrollIntoViewIfNeeded();
await page.waitForTimeout(150);
tool = page.locator('.tool-block').first();
if (await tool.count() > 0 && await tool.isVisible()) {
return tool;
}
}
throw new Error('no visible .tool-block found in selected session');
}
test('full message view', async ({ page }) => {
await selectRichSession(page);
await snap(page, 'message-viewer');
});
test('thinking blocks', async ({ page }) => {
// Sessions with thinking blocks are rare and never recent, so run.sh
// resolves one session id and passes it in; navigate straight there.
// Fall back to a rich session when the id is absent (e.g. local runs).
const thinkingId = process.env.SCREENSHOT_THINKING_SESSION_ID;
if (thinkingId) {
await page.goto('/sessions/' + encodeURIComponent(thinkingId));
await page.waitForSelector('.message', { timeout: 15_000 });
await page.waitForTimeout(500);
} else {
await selectRichSession(page);
await page.waitForTimeout(500);
}
// Surface a thinking block, scrolling the virtualized transcript if needed.
let thinking = page.locator('.thinking-block').first();
if (await thinking.count() === 0) {
const rows = page.locator('.message, .virtual-row');
const rowCount = await rows.count();
for (let i = 0; i < Math.min(rowCount, 60); i++) {
await rows.nth(i).scrollIntoViewIfNeeded();
await page.waitForTimeout(100);
if (await page.locator('.thinking-block').count() > 0) break;
}
thinking = page.locator('.thinking-block').first();
}
if (await thinking.count() > 0) {
// Thinking blocks are collapsed by default; expand so the snap shows
// the reasoning text and not just the header strip.
const header = thinking.locator('.thinking-header');
if (await header.count() > 0) {
await header.click();
await page.waitForTimeout(300);
}
await thinking.scrollIntoViewIfNeeded();
await page.waitForTimeout(200);
await snapEl(thinking, 'thinking-blocks');
}
});
test('tool blocks', async ({ page }) => {
await selectRichSession(page);
const tool = page.locator('.tool-block').first();
if (await tool.isVisible()) {
// Expand it
const header = tool.locator('.tool-header');
if (await header.count() > 0) {
await header.click();
await page.waitForTimeout(300);
}
await snapEl(tool, 'tool-blocks');
} else {
// Scroll to find one
const messages = page.locator('.message');
const count = await messages.count();
for (let i = 0; i < Math.min(count, 20); i++) {
await messages.nth(i).scrollIntoViewIfNeeded();
await page.waitForTimeout(200);
const tb = page.locator('.tool-block').first();
if (await tb.isVisible()) {
const hdr = tb.locator('.tool-header');
if (await hdr.count() > 0) await hdr.click();
await page.waitForTimeout(300);
await snapEl(tb, 'tool-blocks');
break;
}
}
}
});
test('copy buttons on tool block', async ({ page }) => {
await selectRichSession(page);
const tool = await findVisibleToolBlock(page);
const header = tool.locator('.tool-header');
const isExpanded = (await tool.locator('.tool-chevron.open').count()) > 0;
if (!isExpanded && (await header.count()) > 0) {
await header.click();
await page.waitForTimeout(300);
}
if ((await tool.locator('.tool-copy').count()) === 0) {
throw new Error('visible .tool-block has no copy buttons');
}
await tool.hover();
await page.waitForTimeout(600);
await snapEl(tool, 'tool-block-copy-btn');
});
test('tool call groups', async ({ page }) => {
await selectRichSession(page);
const group = page.locator('.tool-group').first();
if (await group.isVisible()) {
await snapEl(group, 'tool-groups');
} else {
const messages = page.locator('.virtual-row');
const count = await messages.count();
for (let i = 0; i < Math.min(count, 30); i++) {
await messages.nth(i).scrollIntoViewIfNeeded();
await page.waitForTimeout(150);
const tg = page.locator('.tool-group').first();
if (await tg.isVisible()) {
await snapEl(tg, 'tool-groups');
break;
}
}
}
});
test('compact layout', async ({ page }) => {
await selectRichSession(page);
// Press 'l' to cycle to compact layout
await page.keyboard.press('l');
await page.waitForTimeout(500);
// Verify we're in compact layout
const list = page.locator('.layout-compact');
if (await list.count() > 0) {
await snap(page, 'layout-compact');
}
// Cycle back to default
await page.keyboard.press('l');
await page.keyboard.press('l');
});
test('stream layout', async ({ page }) => {
await selectRichSession(page);
// Press 'l' twice to reach stream layout
await page.keyboard.press('l');
await page.keyboard.press('l');
await page.waitForTimeout(500);
const list = page.locator('.layout-stream');
if (await list.count() > 0) {
await snap(page, 'layout-stream');
}
// Cycle back to default
await page.keyboard.press('l');
});
test('block-type filter dropdown', async ({ page }) => {
await selectRichSession(page);
// Click the block-type filter button
const filterBtn = page.locator(
'button[title="Filter block types"]'
);
if (await filterBtn.count() > 0) {
await filterBtn.click();
await page.waitForTimeout(300);
const dropdown = page.locator('.block-filter-dropdown');
if (await dropdown.count() > 0) {
await snapEl(dropdown, 'block-filter');
}
// Close by clicking elsewhere
await page.keyboard.press('Escape');
}
});
test('copy button on message', async ({ page }) => {
await selectRichSession(page);
// Hover over a message to reveal the copy button
const message = page.locator('.message').first();
await expect(message).toBeVisible({ timeout: 5_000 });
await message.hover();
await page.waitForTimeout(300);
const copyBtn = message.locator('.message-header .kit-copy-btn').first();
await expect(copyBtn).toBeVisible({ timeout: 5_000 });
await snapEl(message, 'message-copy-btn');
});
test('copy button on code block', async ({ page }) => {
// Walk sessions until we find one with at least one fenced
// code block. We scan more aggressively than selectRichSession
// because most sessions have only inline code or no code at
// all, and we need a `.code-block` (CodeBlock.svelte:34) for
// this test to be meaningful.
const items = page.locator('.session-item');
const total = await items.count();
const max = Math.min(40, total);
let found = false;
for (let i = 0; i < max; i++) {
await items.nth(i).click();
await page.waitForSelector('.message', { timeout: 10_000 });
await page.waitForTimeout(400);
if (await page.locator('.code-block').first().count() > 0) {
found = true;
break;
}
}
if (!found) {
test.skip(true,
`No .code-block found in the first ${max} sessions`);
}
// Capture just the top of a code block where the copy
// button lives (CopyButton is absolutely positioned at
// top:6px, right:6px inside .code-block). We clip the
// page screenshot to a small region around that corner
// so multi-page diffs don't produce a 4000px tall image.
const codeBlock = page.locator('.code-block').first();
await codeBlock.evaluate((el) => {
el.scrollIntoView({ block: 'start' });
});
await page.waitForTimeout(200);
await codeBlock.hover();
// Hover transition: .code-copy fades to opacity:1; give
// the transition time to settle before capturing.
await page.waitForTimeout(600);
const box = await codeBlock.boundingBox();
if (!box) {
throw new Error('code-block has no bounding box');
}
await page.screenshot({
path: join(DIR, 'code-block-copy-btn.png'),
type: 'png',
clip: {
x: Math.max(0, box.x),
y: Math.max(0, box.y),
width: Math.min(box.width, 1440 - box.x),
height: Math.min(box.height, 140),
},
});
});
});
// ── Command palette & search ────────────────────────────
test.describe('Command palette', () => {
test.beforeEach(async ({ page }) => {
await page.setViewportSize(FULL);
await waitForApp(page);
});
test('recent sessions', async ({ page }) => {
await page.keyboard.press('Control+k');
await page.waitForSelector('.palette-overlay', {
timeout: 5_000,
});
await page.waitForTimeout(500);
await snap(page, 'command-palette');
});
test('search results', async ({ page }) => {
await page.keyboard.press('Control+k');
await page.waitForSelector('.palette-overlay', {
timeout: 5_000,
});
const input = page.locator('.palette-input');
await input.fill('implement');
await page.waitForTimeout(1500);
await snap(page, 'search-results');
});
});
// ── Modals ──────────────────────────────────────────────
test.describe('Modals', () => {
test.beforeEach(async ({ page }) => {
await page.setViewportSize(FULL);
await waitForApp(page);
});
test('shortcuts modal', async ({ page }) => {
await page.locator(
'button[aria-label="Keyboard shortcuts"]'
).click();
const dialog = page.getByRole('dialog', {
name: 'Keyboard Shortcuts',
});
await expect(dialog).toBeVisible({ timeout: 5_000 });
await page.waitForTimeout(300);
await snapEl(dialog, 'shortcuts-modal');
});
test('resync modal', async ({ page }) => {
// Resync now lives in Settings: open it, then trigger the modal.
await page.goto('/settings');
await page.waitForSelector('.settings-page', { timeout: 5_000 });
await page.waitForSelector('.resync-btn', { timeout: 5_000 });
await page.locator('.resync-btn').click();
const dialog = page.getByRole('dialog', { name: 'Full Resync' });
await expect(dialog).toBeVisible({ timeout: 5_000 });
await page.waitForTimeout(300);
await snapEl(dialog, 'resync-modal');
await page.keyboard.press('Escape');
});
test('publish modal', async ({ page }) => {
await selectFirstSession(page);
await page.locator(
'button[aria-label="Publish to Gist"]'
).click();
await page.locator('.export-dropdown .overflow-item', {
hasText: 'Publish public Gist',
}).click();
const dialog = page.getByRole('dialog', {
name: 'Publish to public GitHub Gist',
});
await expect(dialog).toBeVisible({ timeout: 5_000 });
// With no token, the modal settles on the setup view (token input); wait
// for it so the snap is the stable setup state, not the initial spinner.
await page
.locator('.token-input')
.waitFor({ state: 'visible', timeout: 6_000 })
.catch(() => {});
await snapEl(dialog, 'publish-modal');
await page.keyboard.press('Escape');
});
});
// ── Insights ────────────────────────────────────────────
test.describe('Insights', () => {
test.beforeEach(async ({ page }) => {
await page.setViewportSize(FULL);
await waitForApp(page);
});
async function navigateToInsights(page: Page) {
await page.goto('/insights');
await page.waitForSelector('.insights-page', {
timeout: 10_000,
});
await page.waitForTimeout(1000);
}
test('full insights page', async ({ page }) => {
await navigateToInsights(page);
// Select the first completed insight (weekly analysis)
const rows = page.locator('.insight-row');
if (await rows.count() > 0) {
await rows.first().click();
await page.waitForTimeout(500);
}
await snap(page, 'insights');
});
test('insight content', async ({ page }) => {
await navigateToInsights(page);
// Select the first generated insight and snap its rendered detail.
// extract-db.sh seeds the archive with safe, synthetic reports.
const items = page.locator('.generated-list button');
await items.first().waitFor({ state: 'visible', timeout: 10_000 });
await items.first().click();
await page.waitForSelector('.generated-detail .markdown-body', {
timeout: 10_000,
});
await page.waitForTimeout(400);
await snapEl(page.locator('.generated-detail'), 'insight-content');
});
test('session insight action in header', async ({ page }) => {
await selectFirstSession(page);
const breadcrumb = page.locator('.session-breadcrumb').first();
await expect(breadcrumb).toBeVisible({ timeout: 5_000 });
const insightButton = breadcrumb.locator(
'button.insight-btn[aria-label="Agent Analysis"]'
);
await expect(insightButton).toBeVisible({ timeout: 5_000 });
await insightButton.hover();
await page.waitForTimeout(300);
await snapEl(breadcrumb, 'session-insight-action');
});
});
// ── Themes ──────────────────────────────────────────────
test.describe('Themes', () => {
test('dark theme session view', async ({ page }) => {
await page.setViewportSize(FULL);
await waitForApp(page);
await selectRichSession(page);
// Ensure dark mode
const isDark = await page.evaluate(
() => document.documentElement.classList.contains('dark')
);
if (!isDark) {
// Find and click theme toggle
const btns = page.locator('.header-btn');
const count = await btns.count();
for (let i = 0; i < count; i++) {
const title = await btns.nth(i).getAttribute('title');
const aria = await btns.nth(i).getAttribute('aria-label');
const text = (title || '') + (aria || '');
if (text.toLowerCase().includes('theme')) {
await btns.nth(i).click();
await page.waitForTimeout(500);
break;
}
}
}
await snap(page, 'theme-dark');
});
test('light theme session view', async ({ page }) => {
await page.setViewportSize(FULL);
await waitForApp(page);
await selectRichSession(page);
// Switch to light mode
const isDark = await page.evaluate(
() => document.documentElement.classList.contains('dark')
);
if (isDark) {
const btns = page.locator('.header-btn');
const count = await btns.count();
for (let i = 0; i < count; i++) {
const title = await btns.nth(i).getAttribute('title');
const aria = await btns.nth(i).getAttribute('aria-label');
const text = (title || '') + (aria || '');
if (text.toLowerCase().includes('theme')) {
await btns.nth(i).click();
await page.waitForTimeout(500);
break;
}
}
}
await snap(page, 'theme-light');
});
});
// ── Settings page ────────────────────────────────────────
test.describe('Settings', () => {
test.beforeEach(async ({ page }) => {
await page.setViewportSize(FULL);
await waitForApp(page);
});
async function openSettings(page: Page) {
const settingsBtn = page.locator(
'button[title*="Settings"], button[title*="settings"], ' +
'a[href*="settings"], .nav-btn:has-text("Settings")'
);
await expect(settingsBtn.first()).toBeVisible({
timeout: 5_000,
});
await settingsBtn.first().click();
const settingsPage = page.locator(
'.settings-page, .settings-container'
);
await expect(settingsPage).toBeVisible({ timeout: 5_000 });
await page.waitForTimeout(500);
}
test('settings page', async ({ page }) => {
await openSettings(page);
await snap(page, 'settings');
});
test('settings remote access section', async ({ page }) => {
await openSettings(page);
// Find the settings-section that contains "Remote Access"
const remoteSection = page.locator(
'.settings-section:has(.section-title:text("Remote Access"))'
);
await expect(remoteSection).toBeVisible({ timeout: 5_000 });
await remoteSection.scrollIntoViewIfNeeded();
await page.waitForTimeout(500);
await snapEl(remoteSection, 'settings-remote');
});
test('worktree project mappings section', async ({ page }) => {
await openSettings(page);
// SettingsSection renders a heading with the title prop;
// match the section that contains the "Worktree mappings"
// header text, the same pattern used for Remote Access above.
const worktreeSection = page.locator(
'.settings-section:has-text("Worktree mappings")'
);
await expect(worktreeSection.first()).toBeVisible({
timeout: 5_000,
});
await worktreeSection.first().scrollIntoViewIfNeeded();
await page.waitForTimeout(500);
await snapEl(worktreeSection.first(), 'worktree-mappings');
});
});
// ── About dialog ─────────────────────────────────────────
test.describe('About', () => {
test.beforeEach(async ({ page }) => {
await page.setViewportSize(FULL);
await waitForApp(page);
});
test('about dialog', async ({ page }) => {
const versionEl = page.locator('button.version');
await expect(versionEl.first()).toBeVisible({
timeout: 5_000,
});
await versionEl.first().click();
const dialog = page.getByRole('dialog', { name: 'AgentsView' });
await expect(dialog).toBeVisible({ timeout: 5_000 });
await snapEl(dialog, 'about-dialog');
await page.keyboard.press('Escape');
});
});
// ── In-session search ────────────────────────────────────
test.describe('In-session search', () => {
test.beforeEach(async ({ page }) => {
await page.setViewportSize(FULL);
await waitForApp(page);
await selectRichSession(page);
});
test('search bar with matches', async ({ page }) => {
await page.locator('.find-btn').click();
await page.waitForSelector('.kit-find-bar', { timeout: 5_000 });
await page.waitForTimeout(300);
// Type a common word to get matches
const input = page.locator('.kit-find-bar__input');
await input.fill('the');
await page.waitForTimeout(1000);
await snap(page, 'in-session-search');
// Close search
await page.keyboard.press('Escape');
});
});
// ── Token usage ──────────────────────────────────────────
test.describe('Token usage', () => {
test.beforeEach(async ({ page }) => {
await page.setViewportSize(FULL);
await waitForApp(page);
});
test('token usage in session header', async ({ page }) => {
await selectRichSession(page);
await page.waitForTimeout(500);
// Token badge lives in SessionBreadcrumb
const badge = page.locator('.token-badge');
await expect(badge.first()).toBeVisible({
timeout: 5_000,
});
// Capture the parent breadcrumb row for context
const breadcrumb = page.locator(
'.session-breadcrumb, .breadcrumb'
);
if (await breadcrumb.count() > 0) {
await snapEl(breadcrumb.first(), 'token-usage');
} else {
await snapEl(badge.first(), 'token-usage');
}
});
});
// ── Sub-agent tree ───────────────────────────────────────
test.describe('Sub-agent tree', () => {
test.beforeEach(async ({ page }) => {
await page.setViewportSize(FULL);
await waitForApp(page);
});
test('collapsible sub-agent tree', async ({ page }) => {
// The sidebar uses virtual scrolling, so only the visible
// window of sessions is in the DOM. The first session with
// a subagent chain may not be in the initial viewport, so
// scroll the list until a `.tree-toggle` button appears.
const treeToggle = page.locator('.tree-toggle');
const scrollContainer = page.locator('.session-list-scroll');
let found = false;
for (let i = 0; i < 30; i++) {
if (await treeToggle.first().isVisible().catch(() => false)) {
found = true;
break;
}
await scrollContainer.evaluate(
(el) => { el.scrollTop += 400; }
);
await page.waitForTimeout(150);
}
expect(found).toBe(true);
await treeToggle.first().click();
await page.waitForTimeout(500);
const sidebar = page.locator('.sidebar');
await snapEl(sidebar, 'subagent-tree');
});
});
// ── Focused transcript mode ─────────────────────────────
test.describe('Follow latest', () => {
test.beforeEach(async ({ page }) => {
await page.setViewportSize(FULL);
await waitForApp(page);
await selectRichSession(page);
});
test('follow latest toggle active', async ({ page }) => {
const toggle = page.locator(
'button[aria-label="Follow latest messages"]'
);
await expect(toggle).toBeVisible({ timeout: 5_000 });
await toggle.click();
// The button toggles its `class:active` and `aria-pressed`;
// wait for the state to be reflected in the rendered DOM.
await expect(toggle).toHaveAttribute('aria-pressed', 'true');
await page.waitForTimeout(500);
// The toggle is a 14×14 icon inside .header-btn — snapping the
// whole 1440px header bar drowns it visually. Crop to a tight
// ~240×56 box centered on the button so the active state and
// its immediate neighbors (sort/layout) are both visible.
const box = await toggle.boundingBox();
if (!box) {
throw new Error('follow-latest toggle has no bounding box');
}
const padX = 100;
const padY = 14;
await page.screenshot({
path: join(DIR, 'follow-latest-toggle.png'),
type: 'png',
clip: {
x: Math.max(0, box.x - padX),
y: Math.max(0, box.y - padY),
width: box.width + padX * 2,
height: box.height + padY * 2,
},
});
// Reset state
await toggle.click();
});
});
test.describe('Focused transcript mode', () => {
test.beforeEach(async ({ page }) => {
await page.setViewportSize(FULL);
await waitForApp(page);
await selectRichSession(page);
});
test('focused transcript view', async ({ page }) => {
// Click the "Focused" pill in the transcript strip
const focusedPill = page.locator(
'button[aria-label="Focused transcript mode"]'
);
await expect(focusedPill).toBeVisible({ timeout: 5_000 });
await focusedPill.click();
await page.waitForTimeout(1000);
await snap(page, 'focused-transcript');
// Toggle back to normal mode
const normalPill = page.locator(
'button[aria-label="Normal transcript mode"]'
);
await normalPill.click();
});
});
// ── Machine labels (pg sync) ────────────────────────────
test.describe('Machine labels', () => {
test('machine labels on session items', async ({ page }) => {
test.skip(!PG_BASE_URL, 'PG_BASE_URL not set');
await page.setViewportSize(FULL);
await page.goto(PG_BASE_URL);
await page.waitForSelector('.session-item', {
timeout: 15_000,
});
await page.waitForTimeout(2000);
const machineTag = page.locator(
'.machine-tag, .machine-label'
);
await expect(machineTag.first()).toBeVisible({
timeout: 5_000,
});
const sidebar = page.locator('.sidebar');
await snapEl(sidebar, 'machine-labels');
});
});
// ── Search grouping and sort ────────────────────────────
test.describe('Search grouping', () => {
test.beforeEach(async ({ page }) => {
await page.setViewportSize(FULL);
await waitForApp(page);
});
test('grouped search results with sort toggle', async ({ page }) => {
await page.keyboard.press('Control+k');
await page.waitForSelector('.palette-overlay', {
timeout: 5_000,
});
const input = page.locator('.palette-input');
await input.fill('implement');
await page.waitForTimeout(1500);
// Assert grouped results rendered (each result shows a
// session name via .item-name, indicating per-session grouping)
const results = page.locator('.palette-results .palette-item');
await expect(results.first()).toBeVisible({
timeout: 5_000,
});
const sessionName = results.first().locator('.item-name');
await expect(sessionName).toBeVisible();
const sortBtns = page.locator('.sort-btn');
await expect(sortBtns.first()).toBeVisible({
timeout: 5_000,
});
// Verify relevance is the default active sort
const relevanceBtn = page.locator('.sort-btn.active');
await expect(relevanceBtn).toHaveText('Relevance');
// Toggle to recency and verify it becomes active
const recencyBtn = page.locator('.sort-btn', {
hasText: 'Recency',
});
await recencyBtn.click();
await page.waitForTimeout(500);
await expect(recencyBtn).toHaveClass(/active/);
await snap(page, 'search-grouped');
});
});
// ── Model info in session header ────────────────────────
test.describe('Model info', () => {
test.beforeEach(async ({ page }) => {
await page.setViewportSize(FULL);
await waitForApp(page);
});
test('model badge is visible in session header', async ({ page }) => {
await selectRichSession(page);
await page.waitForTimeout(500);
// Assert the model badge renders — no separate screenshot
// since it shares the breadcrumb with the token-usage shot.
const modelBadge = page.locator('.model-badge');
await expect(modelBadge.first()).toBeVisible({
timeout: 5_000,
});
});
});
// ── Import conversations ────────────────────────────────
test.describe('Import conversations', () => {
test.beforeEach(async ({ page }) => {
await page.setViewportSize(FULL);
await waitForApp(page);
});
test('import button in header', async ({ page }) => {
const importBtn = page.locator(
'button[title="Import conversations"]'
);
await expect(importBtn).toBeVisible({ timeout: 5_000 });
// Capture a region around the button for context — snap
// the header-right section that contains it.
const headerRight = page.locator('.header-right');
if (await headerRight.count() > 0) {
await snapEl(headerRight, 'import-button');
} else {
await snapEl(importBtn, 'import-button');
}
});
test('import modal claude-ai', async ({ page }) => {
const importBtn = page.locator(
'button[title="Import conversations"]'
);
await expect(importBtn).toBeVisible({ timeout: 5_000 });
await importBtn.click();
const modal = page.locator('div[role="dialog"]');
await expect(modal).toBeVisible({ timeout: 5_000 });
await page.waitForTimeout(300);
// Claude.ai is the default provider
await snapEl(modal, 'import-modal-claude');
await page.keyboard.press('Escape');
});
test('import modal chatgpt', async ({ page }) => {
const importBtn = page.locator(
'button[title="Import conversations"]'
);
await expect(importBtn).toBeVisible({ timeout: 5_000 });
await importBtn.click();
const modal = page.locator('div[role="dialog"]');
await expect(modal).toBeVisible({ timeout: 5_000 });
await page.waitForTimeout(300);
// Select ChatGPT provider via its label
await modal.getByText('ChatGPT').click();
await page.waitForTimeout(300);
await snapEl(modal, 'import-modal-chatgpt');
await page.keyboard.press('Escape');
});
});
// ── Session Vital Signs ─────────────────────────────────
test.describe('Session Vital Signs', () => {
test.beforeEach(async ({ page }) => {
await page.setViewportSize(FULL);
await waitForApp(page);
});
// Walk the sidebar until we find a session whose vitals panel
// actually has content (a populated `Calls` list). Sessions
// without tool calls render an empty-ish panel that makes a dull
// screenshot, so we prefer one that exercises the layout.
async function selectSessionWithCalls(page: Page) {
const items = page.locator('.session-item');
const total = await items.count();
const limit = Math.min(total, 25);
const vitalsBtn = page.locator(
'button[aria-label="Show session analysis"], button[aria-label="Hide session analysis"]',
);
for (let i = 0; i < limit; i++) {
await items.nth(i).click();
await page.waitForSelector('.message', { timeout: 10_000 });
await page.waitForTimeout(400);
if (!(await vitalsBtn.count()) || !(await vitalsBtn.isVisible())) {
continue;
}
const aside = page.locator('aside.vitals');
const isOpen = (await aside.count()) > 0;
if (!isOpen) {
await vitalsBtn.click();
await page.waitForTimeout(600);
}
const callRows = page.locator('aside.vitals .call');
if ((await callRows.count()) >= 3) {
return;
}
// Close before trying the next session.
if (!isOpen) {
await vitalsBtn.click();
await page.waitForTimeout(150);
}
}
throw new Error(
`no session with a populated vitals panel found in first ${limit} items`,
);
}
test('session with vital signs', async ({ page }) => {
await selectSessionWithCalls(page);
const aside = page.locator('aside.vitals');
await expect(aside).toBeVisible({ timeout: 5_000 });
await snap(page, 'session-vital-signs');
});
test('vital signs panel detail', async ({ page }) => {
await selectSessionWithCalls(page);
const aside = page.locator('aside.vitals');
await expect(aside).toBeVisible({ timeout: 5_000 });
await snapEl(aside, 'vital-signs-panel');
});
});
// ── Trends ──────────────────────────────────────────────
test.describe('Trends', () => {
test.beforeEach(async ({ page }) => {
await page.setViewportSize(FULL);
await waitForApp(page);
});
test('trends page', async ({ page }) => {
await page.goto('/trends');
await page.waitForSelector('.trends-page', { timeout: 10_000 });
// The default-terms request resolves async; wait for both the
// chart svg lines and the term-table rows so the page is fully
// populated before we snap.
await page.waitForSelector('.term-table-wrap tbody tr', {
timeout: 15_000,
});
await page.waitForTimeout(1500);
await snap(page, 'trends');
});
});
// ── Session intelligence ────────────────────────────────
test.describe('Session intelligence', () => {
test.beforeEach(async ({ page }) => {
await page.setViewportSize(FULL);
await waitForApp(page);
});
// Walks through the sidebar until it finds a session whose
// header renders a `.grade-badge`. Sessions without scored
// signals don't render the badge at all, so we can't rely on
// `selectRichSession` here.
async function selectGradedSession(page: Page) {
const items = page.locator('.session-item');
const total = await items.count();
const limit = Math.min(total, 25);
for (let i = 0; i < limit; i++) {
await items.nth(i).click();
await page.waitForSelector('.message', { timeout: 10_000 });
await page.waitForTimeout(400);
const badge = page.locator('.grade-badge');
if (
(await badge.count()) > 0 &&
(await badge.first().isVisible())
) {
return;
}
}
throw new Error(
`no session with a .grade-badge found in first ${limit} items`,
);
}
// Same as `selectGradedSession` but requires the opened signal
// panel to render `.penalty` chips. Panels with no penalties are
// valid but make a duller screenshot, so we prefer a session
// whose panel actually exercises the penalty-chip layout. Falls
// back to any graded session if no session in the scan window
// has penalties.
async function openSignalPanelWithPenalties(page: Page) {
const items = page.locator('.session-item');
const total = await items.count();
const limit = Math.min(total, 30);
let firstGradedIdx = -1;
for (let i = 0; i < limit; i++) {
await items.nth(i).click();
await page.waitForSelector('.message', { timeout: 10_000 });
await page.waitForTimeout(300);
const badge = page.locator('.grade-badge').first();
if (!(await badge.count()) || !(await badge.isVisible())) {
continue;
}
if (firstGradedIdx < 0) firstGradedIdx = i;
await badge.click();
const panel = page.locator('.signal-panel');
await expect(panel).toBeVisible({ timeout: 3_000 });
await page.waitForTimeout(200);
if ((await panel.locator('.penalty').count()) > 0) {
return;
}
await badge.click(); // close and move on
await page.waitForTimeout(100);
}
if (firstGradedIdx < 0) {
throw new Error('no graded session found in scan window');
}
// Fall back to the first graded session we saw.
await items.nth(firstGradedIdx).click();
await page.waitForSelector('.message', { timeout: 10_000 });
await page.waitForTimeout(300);
await page.locator('.grade-badge').first().click();
await expect(page.locator('.signal-panel')).toBeVisible({
timeout: 3_000,
});
await page.waitForTimeout(200);
}
test('grade badge in session header', async ({ page }) => {
await selectGradedSession(page);
// Capture the breadcrumb row so the badge is shown in context
// alongside the session title and action buttons.
const breadcrumb = page.locator('.session-breadcrumb').first();
await snapEl(breadcrumb, 'grade-badge');
});
test('signal panel dropdown', async ({ page }) => {
await openSignalPanelWithPenalties(page);
const panel = page.locator('.signal-panel');
await snapEl(panel, 'signal-panel');
// Toggle off to leave the UI clean for later tests.
await page.locator('.grade-badge').first().click();
});
});
// ── Dashboard session health section ────────────────────
test.describe('Dashboard session health', () => {
test.beforeEach(async ({ page }) => {
await page.setViewportSize(FULL);
await waitForApp(page);
await setDateRange1Y(page);
});
test('session health section', async ({ page }) => {
const section = page.locator('.health-section');
await expect(section).toBeVisible({ timeout: 10_000 });
await section.scrollIntoViewIfNeeded();
await page.waitForTimeout(800);
await snapEl(section, 'session-health');
});
});
// ── Usage dashboard (token usage & cost) ────────────────
test.describe('Usage dashboard', () => {
async function navigateToUsage(page: Page) {
await page.goto('/usage');
await page.waitForSelector('.usage-page', { timeout: 10_000 });
// Wait for summary + charts to finish loading
await expect(
page.locator('.usage-page .summary-cards .card').first()
).toBeVisible({ timeout: 10_000 });
// Give SVG charts time to render
await page.waitForTimeout(2000);
}
test.beforeEach(async ({ page }) => {
await page.setViewportSize(FULL);
await waitForApp(page);
await navigateToUsage(page);
});
test('full usage page', async ({ page }) => {
await snap(page, 'usage-page');
});
test('usage summary cards', async ({ page }) => {
const cards = page.locator('.usage-page .summary-cards');
await snapEl(cards, 'usage-summary-cards');
});
test('usage toolbar with filters', async ({ page }) => {
const toolbar = page.locator('.usage-toolbar');
await snapEl(toolbar, 'usage-toolbar');
});
test('cost over time chart', async ({ page }) => {
const panel = page.locator(
'.usage-page .chart-panel:has(.chart-title:text("Cost Over Time"))'
);
await expect(panel).toBeVisible({ timeout: 5_000 });
await panel.scrollIntoViewIfNeeded();
await page.waitForTimeout(500);
await snapEl(panel, 'usage-cost-trend');
});
test('attribution treemap', async ({ page }) => {
const panel = page.locator('.attribution-panel');
await expect(panel).toBeVisible({ timeout: 5_000 });
await panel.scrollIntoViewIfNeeded();
await page.waitForTimeout(500);
// Treemap is the default view
await snapEl(panel, 'usage-attribution');
});
test('top sessions by cost', async ({ page }) => {
const panel = page.locator(
'.chart-panel:has(.top-sessions-container)'
);
await expect(panel).toBeVisible({ timeout: 5_000 });
await panel.scrollIntoViewIfNeeded();
await page.waitForTimeout(500);
await snapEl(panel, 'usage-top-sessions');
});
test('cache efficiency panel', async ({ page }) => {
const panel = page.locator('.chart-panel:has(.cache-panel)');
await expect(panel).toBeVisible({ timeout: 5_000 });
await panel.scrollIntoViewIfNeeded();
await page.waitForTimeout(500);
await snapEl(panel, 'usage-cache-efficiency');
});
test('model filter dropdown open', async ({ page }) => {
// Click the Model filter trigger in the toolbar
const modelDropdown = page.locator(
'.usage-toolbar .kit-filter-dropdown',
{ hasText: 'Model' }
);
const trigger = modelDropdown.locator('.kit-filter-dropdown__btn');
await expect(trigger).toBeVisible({ timeout: 5_000 });
await trigger.click();
await modelDropdown.locator('.kit-filter-dropdown__panel').waitFor(
{ state: 'visible', timeout: 5_000 }
);
await page.waitForTimeout(300);
await snap(page, 'usage-filter-dropdown');
await page.keyboard.press('Escape');
});
});
// ── Recent Edits ────────────────────────────────────────
test.describe('Recent Edits', () => {
test.beforeEach(async ({ page }) => {
await page.setViewportSize(FULL);
await waitForApp(page);
});
test('recent edits feed', async ({ page }) => {
// Recent Edits is a top-level route reached from the More menu.
// Navigate directly, then expand the first file row so the
// screenshot shows both the grouped feed and a file's edits.
await page.goto('/recent-edits');
await page.waitForSelector('.recent-edits-page', {
timeout: 10_000,
});
await page.waitForSelector('.re-file', { timeout: 10_000 });
const firstRow = page.locator('.re-file-row').first();
await firstRow.click();
await page.waitForSelector('.re-edits', { timeout: 5_000 });
await page.waitForTimeout(500);
await snap(page, 'recent-edits');
});
});