Skip to content

Commit 5f035e5

Browse files
committed
Merge branch 'feat/8x1z-agent-screenshots'
2 parents cf0d4ab + ade965e commit 5f035e5

5 files changed

Lines changed: 108 additions & 0 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@ web/dist/
1717
/web/playwright-report/
1818
/nibs.exe
1919

20+
/web/screenshots/output/

CLAUDE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ All commands use [Task](https://taskfile.dev/) (`go-task/task`) as the task runn
1818
- `cd web && npx vitest run --reporter=agent` - Run web tests only
1919
- `task demo` - Serve the web UI with the sample-project fixture (temporary copy, safe to mutate)
2020
- `task demo:tui` - Run the TUI with the sample-project fixture
21+
- `task screenshots` - Capture web UI screenshots to `web/screenshots/output/` for visual verification
2122
- `task --list` - List all available tasks
2223

2324
## GraphQL Schema Changes
@@ -135,6 +136,7 @@ Before starting any new work, run `git fetch` (and `git -C .nibs fetch`) and che
135136
- Web test commands require `web/` as the working directory. If cwd has drifted, `cd` to the project root's `web/` directory first.
136137
- **Always use `--reporter=agent`** when running vitest — it keeps output concise. Never pipe vitest through grep; read the output once.
137138
- `task test` runs both Go and web tests. No need to run them separately unless debugging a specific failure.
139+
- **Visual verification of the web UI**: `task screenshots` captures PNGs of the key UI states (table at each view level, detail panel, editor modal, context menu) into `web/screenshots/output/` (gitignored), served from a temp copy of the sample fixture. Read the PNGs to *see* rendered changes — jsdom tests can't verify pixels. One-time setup: `cd web && npx playwright install chromium` (plain `install`, not `--with-deps` — that flag is apt-only and fails on Fedora). Extend `web/screenshots/capture.spec.ts` when new views or themes land.
138140
- **bits-ui timer flush**: `test-setup.ts` has an `afterAll` that waits 50ms so bits-ui's body-scroll-lock deferred cleanup (24ms setTimeout) fires while jsdom still exists. Without this, the timer fires after jsdom teardown causing a spurious "document is not defined" error.
139141

140142
## Architecture Reviews

Taskfile.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,13 @@ tasks:
7979
cmds:
8080
- npx playwright test
8181

82+
screenshots:
83+
desc: Capture web UI screenshots for visual verification (web/screenshots/output)
84+
deps: [web:build]
85+
dir: web
86+
cmds:
87+
- npx playwright test --config=playwright.screenshots.config.ts
88+
8289
demo:
8390
desc: Serve the web UI with the sample project fixture (temporary copy)
8491
# Task's built-in u-root mktemp defaults to /tmp (which doesn't exist on
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { defineConfig } from "@playwright/test";
2+
import { cpSync, mkdtempSync } from "node:fs";
3+
import { tmpdir } from "node:os";
4+
import { join, resolve } from "node:path";
5+
6+
// Screenshot captures run against a throwaway copy of the sample-project
7+
// fixture (same idea as `task demo`) so output is deterministic and never
8+
// touches real nibs data. The temp dir is left for the OS to clean up.
9+
const fixture = resolve(import.meta.dirname, "..", "testdata", "fixtures", "sample-project");
10+
const tmp = mkdtempSync(join(tmpdir(), "nibs-screenshots-"));
11+
cpSync(join(fixture, ".nibs"), join(tmp, ".nibs"), { recursive: true });
12+
cpSync(join(fixture, ".nibs.yml"), join(tmp, ".nibs.yml"));
13+
14+
export default defineConfig({
15+
testDir: "./screenshots",
16+
timeout: 30_000,
17+
retries: 0,
18+
use: {
19+
baseURL: "http://127.0.0.1:3132",
20+
headless: true,
21+
viewport: { width: 1440, height: 900 },
22+
},
23+
webServer: {
24+
command: `cd .. && go run . serve --port 3132 --no-open --nibs-path "${join(tmp, ".nibs")}"`,
25+
url: "http://127.0.0.1:3132",
26+
// Never reuse: a leftover server could be pointed at real data.
27+
reuseExistingServer: false,
28+
timeout: 30_000,
29+
},
30+
projects: [
31+
{
32+
name: "chromium",
33+
use: { browserName: "chromium" },
34+
},
35+
],
36+
});

web/screenshots/capture.spec.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { test, expect, type Page } from "@playwright/test";
2+
import { mkdirSync } from "node:fs";
3+
import { join } from "node:path";
4+
5+
// Captures PNGs of the key web UI states into web/screenshots/output/ so an
6+
// agent (or human) can visually verify UI changes. Run via `task screenshots`.
7+
//
8+
// When the theme engine lands (nibs-vmaq), extend this to loop the captures
9+
// once per theme. When the board view lands (nibs-sg09), add a capture for it.
10+
11+
const OUT = join(import.meta.dirname, "output");
12+
mkdirSync(OUT, { recursive: true });
13+
14+
const VIEW_LEVELS = ["milestones", "epics", "backlog"] as const;
15+
16+
async function openApp(page: Page, viewLevel: (typeof VIEW_LEVELS)[number] = "milestones") {
17+
await page.addInitScript(level => {
18+
localStorage.setItem(
19+
"nibs-filter-preferences",
20+
JSON.stringify({ filter: {}, viewLevel: level }),
21+
);
22+
}, viewLevel);
23+
await page.goto("/");
24+
await expect(page.locator("tr[data-nib-id]").first()).toBeVisible({ timeout: 10_000 });
25+
}
26+
27+
function shot(page: Page, name: string) {
28+
return page.screenshot({ path: join(OUT, `${name}.png`), animations: "disabled" });
29+
}
30+
31+
for (const level of VIEW_LEVELS) {
32+
test(`table — ${level} view level`, async ({ page }) => {
33+
await openApp(page, level);
34+
await shot(page, `table-${level}`);
35+
});
36+
}
37+
38+
test("detail panel", async ({ page }) => {
39+
await openApp(page);
40+
await page.locator("tr[data-nib-id]").first().locator('[data-action="title"]').click();
41+
await expect(page.locator('[data-testid="detail-panel"]')).toBeVisible({ timeout: 5_000 });
42+
await expect(page.locator('[data-testid="detail-loading"]')).toBeHidden({ timeout: 5_000 });
43+
await shot(page, "detail-panel");
44+
});
45+
46+
test("editor modal", async ({ page }) => {
47+
await openApp(page);
48+
await page.locator("tr[data-nib-id]").first().click({ button: "right" });
49+
await expect(page.locator('[data-testid="context-menu"]')).toBeVisible({ timeout: 3_000 });
50+
await page.locator('[data-testid="ctx-edit"]').click();
51+
await expect(page.locator('[data-testid="editor-modal"]')).toBeVisible({ timeout: 5_000 });
52+
// Wait for the CodeMirror editor to mount so the body area isn't blank.
53+
await expect(page.locator(".cm-content").first()).toBeVisible({ timeout: 5_000 });
54+
await shot(page, "editor-modal");
55+
});
56+
57+
test("context menu", async ({ page }) => {
58+
await openApp(page);
59+
await page.locator("tr[data-nib-id]").first().click({ button: "right" });
60+
await expect(page.locator('[data-testid="context-menu"]')).toBeVisible({ timeout: 3_000 });
61+
await shot(page, "context-menu");
62+
});

0 commit comments

Comments
 (0)