Skip to content

Commit 3ed6b33

Browse files
djscruggsclaude
andcommitted
Add wallet chooser screenshot gallery and document testing.
Add `npm run gallery`, which screenshots the first party wallet chooser across wallet counts, themes, and engines (collapsed and expanded) via the harness route, then builds an `index.html` contact sheet for quick visual review. Output lands in the git-ignored `test/e2e/gallery/`. Document both the regression suite and the gallery in the README, including the Brave/mobile coverage caveats. Also git-ignore the local `scratchpad/` working directory. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 826bd2c commit 3ed6b33

5 files changed

Lines changed: 169 additions & 1 deletion

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,6 @@ static/images/logo-*.png
2424
/playwright-report
2525
/blob-report
2626
/test/e2e/gallery
27+
28+
# local working notes / manual-test artifacts
29+
scratchpad

README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,40 @@ Access the server at the following URL:
8383

8484
* https://authn.localhost:33443/
8585

86+
## Testing
87+
88+
The first party wallet chooser dialog has an automated visual/layout test
89+
suite. It drives a dev-only harness route
90+
(`/test/wallet-chooser?hints=N&qr=1`) that renders the dialog with fake state,
91+
so the layout can be exercised across wallet counts (0, 1, 5, 15) and the
92+
cross-device QR section without any CHAPI registration, wallets, or popups. The
93+
harness route is excluded from production builds.
94+
95+
Install the browser engines once:
96+
97+
npx playwright install chromium webkit firefox
98+
99+
Run the layout regression suite (Chromium, WebKit, Firefox):
100+
101+
npm run test:e2e
102+
103+
This asserts structural invariants — the expected wallets render, no horizontal
104+
or window scrollbar appears, the header stays pinned while the list scrolls, and
105+
the panel fills the popup — rather than comparing pixels. It starts the dev
106+
server automatically.
107+
108+
Generate a browsable screenshot gallery of every state (wallet count × theme ×
109+
engine, collapsed and expanded), with an `index.html` contact sheet:
110+
111+
npm run gallery
112+
113+
Output is written to `test/e2e/gallery/` (git-ignored).
114+
115+
> **Brave:** the Chromium engine covers Brave's rendering (Brave is Chromium
116+
> plus "shields", which do not affect the dialog CSS). Brave's storage/shields
117+
> behavior is a CHAPI plumbing concern, verified in the manual mobile pass, not
118+
> by this layout suite. Real mobile/Safari-on-iOS is likewise a manual pass.
119+
86120
## Production
87121

88122
Full instructions for running this code in production are beyond the scope of

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
"scripts": {
1010
"start": "node authn.localhost.js",
1111
"lint": "eslint",
12-
"test:e2e": "playwright test wallet-chooser"
12+
"test:e2e": "playwright test wallet-chooser",
13+
"gallery": "playwright test gallery && node test/e2e/build-gallery-index.js"
1314
},
1415
"repository": {
1516
"type": "git",

test/e2e/build-gallery-index.js

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*!
2+
* New BSD License (3-clause)
3+
* Copyright (c) 2026, Digital Bazaar, Inc.
4+
* All rights reserved.
5+
*/
6+
import {fileURLToPath} from 'node:url';
7+
import fs from 'node:fs/promises';
8+
import path from 'node:path';
9+
10+
/* Builds an `index.html` contact sheet from the PNGs the gallery spec wrote
11+
to `test/e2e/gallery/<engine>/<theme>/`. Run after `playwright test gallery`
12+
(the `gallery` npm script chains them). Filesystem-driven so it sees every
13+
engine's output regardless of which Playwright worker produced it. */
14+
15+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
16+
const GALLERY_DIR = path.join(__dirname, 'gallery');
17+
18+
async function findPngs(dir, base = dir) {
19+
const out = [];
20+
let entries;
21+
try {
22+
entries = await fs.readdir(dir, {withFileTypes: true});
23+
} catch {
24+
return out;
25+
}
26+
for(const entry of entries) {
27+
const full = path.join(dir, entry.name);
28+
if(entry.isDirectory()) {
29+
out.push(...await findPngs(full, base));
30+
} else if(entry.name.endsWith('.png')) {
31+
out.push(path.relative(base, full));
32+
}
33+
}
34+
return out;
35+
}
36+
37+
const pngs = (await findPngs(GALLERY_DIR)).sort((a, b) => a.localeCompare(b));
38+
if(pngs.length === 0) {
39+
console.error('No gallery PNGs found. Run `playwright test gallery` first.');
40+
process.exit(1);
41+
}
42+
43+
const cards = pngs.map(rel => {
44+
// rel = <engine>/<theme>/<state>.png
45+
const [engine, theme, file] = rel.split(path.sep);
46+
const label = file.replace(/\.png$/, '').replace(/-/g, ' ');
47+
return `
48+
<figure>
49+
<img src="${rel}" alt="${label}" loading="lazy">
50+
<figcaption>${engine} / ${theme}${label}</figcaption>
51+
</figure>`;
52+
}).join('');
53+
54+
const html = `<!doctype html>
55+
<meta charset="utf-8">
56+
<title>authn.io wallet chooser gallery</title>
57+
<style>
58+
body {font: 14px system-ui, sans-serif; margin: 24px; background: #fafafa;}
59+
h1 {font-size: 18px;}
60+
.grid {display: flex; flex-wrap: wrap; gap: 16px;}
61+
figure {margin: 0; background: #fff; border: 1px solid #ddd;
62+
border-radius: 6px; padding: 8px;}
63+
img {display: block; width: 250px; height: auto; border: 1px solid #eee;}
64+
figcaption {font-size: 12px; color: #444; padding-top: 6px; max-width: 250px;}
65+
</style>
66+
<h1>authn.io wallet chooser — ${pngs.length} shots</h1>
67+
<div class="grid">${cards}</div>`;
68+
69+
const indexPath = path.join(GALLERY_DIR, 'index.html');
70+
await fs.writeFile(indexPath, html);
71+
console.log(`Gallery contact sheet: ${indexPath}`);

test/e2e/gallery.spec.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*!
2+
* New BSD License (3-clause)
3+
* Copyright (c) 2026, Digital Bazaar, Inc.
4+
* All rights reserved.
5+
*/
6+
import {expect, test} from '@playwright/test';
7+
import {fileURLToPath} from 'node:url';
8+
import fs from 'node:fs/promises';
9+
import path from 'node:path';
10+
11+
/* Generates a labelled, human-viewable gallery of the first party wallet
12+
chooser dialog across wallet counts, the cross-device QR section, themes, and
13+
viewports — the screenshots we otherwise capture by hand for review. Output
14+
goes to `test/e2e/gallery/<engine>/<theme>/<state>.png` plus an `index.html`
15+
contact sheet. This is NOT a regression gate (see `wallet-chooser.spec.js` for
16+
that); run it with `npm run gallery`. */
17+
18+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
19+
const GALLERY_DIR = path.join(__dirname, 'gallery');
20+
21+
const STATES = [
22+
{name: 'hints-0-qr', query: 'hints=0&qr=1', label: '0 wallets + QR'},
23+
{name: 'hints-1-qr', query: 'hints=1&qr=1', label: '1 wallet + QR'},
24+
{name: 'hints-5-qr', query: 'hints=5&qr=1', label: '5 wallets + QR'},
25+
{name: 'hints-5-noqr', query: 'hints=5&qr=0', label: '5 wallets, no QR'},
26+
{name: 'hints-15-qr', query: 'hints=15&qr=1', label: '15 wallets + QR'}
27+
];
28+
const THEMES = ['light', 'dark'];
29+
30+
test.describe('gallery', () => {
31+
for(const theme of THEMES) {
32+
test.describe(theme, () => {
33+
test.use({colorScheme: theme});
34+
35+
for(const state of STATES) {
36+
test(`${state.label}`, async ({page}, testInfo) => {
37+
const engine = testInfo.project.name;
38+
await page.goto(`/test/wallet-chooser?${state.query}`);
39+
await page.locator('.wrm-modal-1p .wrm-modal-content').waitFor();
40+
41+
const dir = path.join(GALLERY_DIR, engine, theme);
42+
await fs.mkdir(dir, {recursive: true});
43+
44+
// collapsed (default) shot
45+
await page.screenshot({path: path.join(dir, `${state.name}.png`)});
46+
47+
// for states with the expander, also capture it expanded
48+
const toggle = page.locator('.cross-device-toggle');
49+
if(await toggle.count() > 0) {
50+
await toggle.click();
51+
await expect(page.locator('img[alt*="QR"]')).toBeVisible();
52+
await page.screenshot(
53+
{path: path.join(dir, `${state.name}-expanded.png`)});
54+
}
55+
});
56+
}
57+
});
58+
}
59+
});

0 commit comments

Comments
 (0)