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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,12 @@ authn.localhost-access.log
authn.localhost-access1.log
bedrock
static/images/logo-*.png

# Playwright
/test-results
/playwright-report
/blob-report
/test/e2e/gallery

# local working notes / manual-test artifacts
scratchpad
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
# authn.io ChangeLog

## 7.5.0 - 2026-06-dd

### Added
- Add an automated visual/layout test suite for the first party wallet
chooser dialog. A dev-only `/test/wallet-chooser` harness route
renders the dialog with fake state across wallet counts and the
cross-device QR section (excluded from production builds);
`npm run test:e2e` asserts layout invariants on desktop Chromium,
WebKit, and Firefox plus emulated iPhone and Pixel phone sizes, and
`npm run gallery` produces a browsable screenshot gallery. No
production behavior or styles change. One overflow assertion is
skipped pending a later CSS rework.

## 7.4.4 - 2026-06-13

### Fixed
Expand Down
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,48 @@ Access the server at the following URL:

* https://authn.localhost:33443/

## Testing

The first party wallet chooser dialog has an automated visual/layout test
suite. It drives a dev-only harness route
(`/test/wallet-chooser?hints=N&qr=1`) that renders the dialog with fake state,
so the layout can be exercised across wallet counts (0, 1, 5, 15) and the
cross-device QR section without any CHAPI registration, wallets, or popups. The
harness route is excluded from production builds.

Install the browser engines once:

npx playwright install chromium webkit firefox

Run the layout regression suite:

npm run test:e2e

This asserts structural invariants — the expected wallets render, no horizontal
or window scrollbar appears, the header stays pinned while the list scrolls, and
the panel fills the popup — rather than comparing pixels. It starts the dev
server automatically and runs five projects: desktop Chromium, WebKit, and
Firefox at the 500px popup width, plus emulated **iPhone 15** and **Pixel 7**.
The phone projects matter because on a phone the popup is clamped to the screen
width and crosses the dialog's 430px "small screen" CSS breakpoint, exercising
layout branches the desktop width does not.

Run a single project with, e.g., `npm run test:e2e -- --project=iphone`. The
project names are `chromium`, `webkit`, `firefox`, `iphone`, and
`android-pixel`.

Generate a browsable screenshot gallery of every state (wallet count × theme ×
engine, collapsed and expanded), with an `index.html` contact sheet:

npm run gallery

Output is written to `test/e2e/gallery/` (git-ignored).

> **Brave:** the Chromium engine covers Brave's rendering (Brave is Chromium
> plus "shields", which do not affect the dialog CSS). Brave's storage/shields
> behavior is a CHAPI plumbing concern, verified in the manual mobile pass, not
> by this layout suite. Real mobile/Safari-on-iOS is likewise a manual pass.

## Production

Full instructions for running this code in production are beyond the scope of
Expand Down
68 changes: 66 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
},
"scripts": {
"start": "node authn.localhost.js",
"lint": "eslint"
"lint": "eslint",
"test:e2e": "playwright test wallet-chooser",
"gallery": "playwright test gallery && node test/e2e/build-gallery-index.js"
},
"repository": {
"type": "git",
Expand Down Expand Up @@ -37,6 +39,7 @@
},
"devDependencies": {
"@digitalbazaar/eslint-config": "^8.0.1",
"@playwright/test": "^1.60.0",
"eslint": "^9.39.4"
},
"engines": {
Expand Down
59 changes: 59 additions & 0 deletions playwright.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*!
* New BSD License (3-clause)
* Copyright (c) 2026, Digital Bazaar, Inc.
*/
import {defineConfig, devices} from '@playwright/test';

const BASE_URL = 'https://authn.localhost:33443';

export default defineConfig({
testDir: './test/e2e',
// the dialog suite is layout-only and deterministic; fail fast in CI
forbidOnly: !!process.env.CI,
retries: 0,
reporter: 'list',
use: {
baseURL: BASE_URL,
// the dev server uses a self-signed localhost certificate
ignoreHTTPSErrors: true,
screenshot: 'off',
trace: 'on-first-retry'
},
// the wallet chooser popups are 500px wide; emulate that as the default
// viewport so layout matches the real popup
projects: [
{
name: 'chromium',
use: {...devices['Desktop Chrome'], viewport: {width: 500, height: 640}}
},
{
name: 'webkit',
use: {...devices['Desktop Safari'], viewport: {width: 500, height: 640}}
},
{
name: 'firefox',
use: {...devices['Desktop Firefox'], viewport: {width: 500, height: 640}}
},
// phone-sized projects: on a phone the popup is clamped to the screen
// width (narrower than the 500px desktop popup) and crosses the
// dialog's 430px "small screen" CSS breakpoint, so these exercise
// layout branches the desktop projects do not. Device descriptors also
// set a touch-capable, mobile-UA context.
{
name: 'iphone',
use: {...devices['iPhone 15']}
},
{
name: 'android-pixel',
use: {...devices['Pixel 7']}
}
],
// start the authn.io dev server automatically; reuse one already running
webServer: {
command: 'node authn.localhost.js',
url: `${BASE_URL}/test/wallet-chooser?hints=1`,
ignoreHTTPSErrors: true,
reuseExistingServer: true,
timeout: 120 * 1000
}
});
70 changes: 70 additions & 0 deletions test/e2e/build-gallery-index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*!
* New BSD License (3-clause)
* Copyright (c) 2026, Digital Bazaar, Inc.
*/
import {fileURLToPath} from 'node:url';
import fs from 'node:fs/promises';
import path from 'node:path';

/* Builds an `index.html` contact sheet from the PNGs the gallery spec wrote
to `test/e2e/gallery/<engine>/<theme>/`. Run after `playwright test gallery`
(the `gallery` npm script chains them). Filesystem-driven so it sees every
engine's output regardless of which Playwright worker produced it. */

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const GALLERY_DIR = path.join(__dirname, 'gallery');

async function findPngs(dir, base = dir) {
const out = [];
let entries;
try {
entries = await fs.readdir(dir, {withFileTypes: true});
} catch {
return out;
}
for(const entry of entries) {
const full = path.join(dir, entry.name);
if(entry.isDirectory()) {
out.push(...await findPngs(full, base));
} else if(entry.name.endsWith('.png')) {
out.push(path.relative(base, full));
}
}
return out;
}

const pngs = (await findPngs(GALLERY_DIR)).sort((a, b) => a.localeCompare(b));
if(pngs.length === 0) {
console.error('No gallery PNGs found. Run `playwright test gallery` first.');
process.exit(1);
}

const cards = pngs.map(rel => {
// rel = <engine>/<theme>/<state>.png
const [engine, theme, file] = rel.split(path.sep);
const label = file.replace(/\.png$/, '').replace(/-/g, ' ');
return `
<figure>
<img src="${rel}" alt="${label}" loading="lazy">
<figcaption>${engine} / ${theme} — ${label}</figcaption>
</figure>`;
}).join('');

const html = `<!doctype html>
<meta charset="utf-8">
<title>authn.io wallet chooser gallery</title>
<style>
body {font: 14px system-ui, sans-serif; margin: 24px; background: #fafafa;}
h1 {font-size: 18px;}
.grid {display: flex; flex-wrap: wrap; gap: 16px;}
figure {margin: 0; background: #fff; border: 1px solid #ddd;
border-radius: 6px; padding: 8px;}
img {display: block; width: 250px; height: auto; border: 1px solid #eee;}
figcaption {font-size: 12px; color: #444; padding-top: 6px; max-width: 250px;}
</style>
<h1>authn.io wallet chooser — ${pngs.length} shots</h1>
<div class="grid">${cards}</div>`;

const indexPath = path.join(GALLERY_DIR, 'index.html');
await fs.writeFile(indexPath, html);
console.log(`Gallery contact sheet: ${indexPath}`);
Loading