项目文件夹

文件
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 行
55 KiB
TypeScript

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
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');
});
});