Skip to content
Closed
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
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,32 @@
# authn.io ChangeLog

## 7.4.4 - 2026-06-dd

### Fixed
- Fix the root cause of the first party dialog's horizontal overflow
instead of clipping it. The dialog header keeps its full-bleed
edge-to-edge border (a negative side-margin that cancels the panel
padding, with no overflow). The overflow came from the in-flow body
dividers, which reuse the same negative side-margin but sit inside a
wrapper narrower than the panel, so the margin pushed past its edge.
Neutralize the side margins on those body dividers only — they read as
inset rules with no overhang — and remove the temporary
`overflow-x: hidden` workaround. Adds a layout test asserting the
header border bleeds edge-to-edge so the bleed cannot regress silently.
- Target the wallet hint list by its own class for the 1p flex sizing,
rather than the generic `.wrm-flex-column-stretch` (which upstream
also applies to a wrapper around the integrated list), so the sizing
and scrolling apply only to the intended element.

### 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; `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.

## 7.4.3 - 2026-06-12

### 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
64 changes: 64 additions & 0 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
60 changes: 60 additions & 0 deletions playwright.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*!
* New BSD License (3-clause)
* Copyright (c) 2026, Digital Bazaar, Inc.
* All rights reserved.
*/
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
}
});
71 changes: 71 additions & 0 deletions test/e2e/build-gallery-index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*!
* New BSD License (3-clause)
* Copyright (c) 2026, Digital Bazaar, Inc.
* All rights reserved.
*/
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