From e6d2325b25f09d606e50f8116edd643fea5f9c50 Mon Sep 17 00:00:00 2001 From: Derek Scruggs Date: Sat, 13 Jun 2026 17:14:18 -0500 Subject: [PATCH 1/7] Add the 1p wallet chooser layout test suite and harness. Land the automated tooling on its own, with no style changes, so the dialog's CSS can be reworked later against code we own (after the components we use are pulled out of vue-web-request-mediator and that dependency is removed). - Add a dev-only `/test/wallet-chooser` harness route that mounts the presentational dialog with fake state across wallet counts and the cross-device QR section. Excluded from production builds. - Add a Playwright geometric-invariant suite (`npm run test:e2e`) on desktop Chromium, WebKit, and Firefox plus emulated iPhone and Pixel sizes: expected hints render, no scrollbar, header pinned while the list scrolls, panel fills the popup, header border bleeds edge to edge, expander/QR behave per wallet count. - Add a screenshot gallery (`npm run gallery`) that captures every state across themes and engines and builds a contact sheet. - Document both commands in the README. `web/app.less` is unchanged from main; no production behavior or styles change. The strict "no horizontal overflow" assertion is skipped: it fails only because the dialog still relies on the `overflow-x: hidden` band-aid to hide a real overflow, which is the deferred CSS work. The test stays in the suite as the guard that will prove the band-aid can be removed. Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitignore | 9 ++ CHANGELOG.md | 13 +++ README.md | 42 ++++++++ package-lock.json | 68 ++++++++++++- package.json | 7 +- playwright.config.js | 60 +++++++++++ test/e2e/build-gallery-index.js | 71 +++++++++++++ test/e2e/gallery.spec.js | 74 ++++++++++++++ test/e2e/wallet-chooser.spec.js | 165 +++++++++++++++++++++++++++++++ web/router.js | 11 ++- web/routes/TestWalletChooser.vue | 103 +++++++++++++++++++ 11 files changed, 618 insertions(+), 5 deletions(-) create mode 100644 playwright.config.js create mode 100644 test/e2e/build-gallery-index.js create mode 100644 test/e2e/gallery.spec.js create mode 100644 test/e2e/wallet-chooser.spec.js create mode 100644 web/routes/TestWalletChooser.vue diff --git a/.gitignore b/.gitignore index 37876f84..983fb768 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f177e01..4db3fc0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 30f8fcc4..06f244d0 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/package-lock.json b/package-lock.json index 5f51f662..19e04bc9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "authio", - "version": "7.4.4", + "version": "7.4.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "authio", - "version": "7.4.4", + "version": "7.4.3", "license": "SEE LICENSE IN LICENSE", "dependencies": { "@bedrock/config-yaml": "^4.3.3", @@ -30,6 +30,7 @@ }, "devDependencies": { "@digitalbazaar/eslint-config": "^8.0.1", + "@playwright/test": "^1.60.0", "eslint": "^9.39.4" }, "engines": { @@ -2624,6 +2625,22 @@ "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", "license": "MIT" }, + "node_modules/@playwright/test": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz", + "integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@sindresorhus/base62": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@sindresorhus/base62/-/base62-1.0.0.tgz", @@ -5195,6 +5212,21 @@ "node": ">= 0.6" } }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -6763,6 +6795,38 @@ "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", "license": "MIT" }, + "node_modules/playwright": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", + "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz", + "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/pluralize": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", diff --git a/package.json b/package.json index 8ec0b654..1a025b9d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "authio", - "version": "7.4.4", + "version": "7.4.3", "type": "module", "main": "./lib/index.js", "browser": { @@ -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", @@ -37,6 +39,7 @@ }, "devDependencies": { "@digitalbazaar/eslint-config": "^8.0.1", + "@playwright/test": "^1.60.0", "eslint": "^9.39.4" }, "engines": { diff --git a/playwright.config.js b/playwright.config.js new file mode 100644 index 00000000..3a05242b --- /dev/null +++ b/playwright.config.js @@ -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 + } +}); diff --git a/test/e2e/build-gallery-index.js b/test/e2e/build-gallery-index.js new file mode 100644 index 00000000..3262d947 --- /dev/null +++ b/test/e2e/build-gallery-index.js @@ -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///`. 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 = //.png + const [engine, theme, file] = rel.split(path.sep); + const label = file.replace(/\.png$/, '').replace(/-/g, ' '); + return ` +
+ ${label} +
${engine} / ${theme} — ${label}
+
`; +}).join(''); + +const html = ` + +authn.io wallet chooser gallery + +

authn.io wallet chooser — ${pngs.length} shots

+
${cards}
`; + +const indexPath = path.join(GALLERY_DIR, 'index.html'); +await fs.writeFile(indexPath, html); +console.log(`Gallery contact sheet: ${indexPath}`); diff --git a/test/e2e/gallery.spec.js b/test/e2e/gallery.spec.js new file mode 100644 index 00000000..44265761 --- /dev/null +++ b/test/e2e/gallery.spec.js @@ -0,0 +1,74 @@ +/*! + * New BSD License (3-clause) + * Copyright (c) 2026, Digital Bazaar, Inc. + * All rights reserved. + */ +import {expect, test} from '@playwright/test'; +import {fileURLToPath} from 'node:url'; +import fs from 'node:fs/promises'; +import path from 'node:path'; + +/* Generates a labelled, human-viewable gallery of the first party wallet +chooser dialog across wallet counts, the cross-device QR section, themes, and +viewports — the screenshots we otherwise capture by hand for review. Output +goes to `test/e2e/gallery///.png` plus an `index.html` +contact sheet. This is NOT a regression gate (see `wallet-chooser.spec.js` for +that); run it with `npm run gallery`. */ + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const GALLERY_DIR = path.join(__dirname, 'gallery'); + +const STATES = [ + {name: 'hints-0-qr', query: 'hints=0&qr=1', label: '0 wallets + QR'}, + {name: 'hints-1-qr', query: 'hints=1&qr=1', label: '1 wallet + QR'}, + {name: 'hints-5-qr', query: 'hints=5&qr=1', label: '5 wallets + QR'}, + {name: 'hints-5-noqr', query: 'hints=5&qr=0', label: '5 wallets, no QR'}, + {name: 'hints-15-qr', query: 'hints=15&qr=1', label: '15 wallets + QR'} +]; +const THEMES = ['light', 'dark']; + +test.describe('gallery', () => { + for(const theme of THEMES) { + test.describe(theme, () => { + test.use({colorScheme: theme}); + + for(const state of STATES) { + test(`${state.label}`, async ({page}, testInfo) => { + const engine = testInfo.project.name; + await page.goto(`/test/wallet-chooser?${state.query}`); + await page.locator('.wrm-modal-1p .wrm-modal-content').waitFor(); + + const dir = path.join(GALLERY_DIR, engine, theme); + await fs.mkdir(dir, {recursive: true}); + + // collapsed (default) shot + await page.screenshot({path: path.join(dir, `${state.name}.png`)}); + + // for states with the expander, also capture it expanded + const toggle = page.locator('.cross-device-toggle'); + if(await toggle.count() > 0) { + await toggle.click(); + const qr = page.locator('img[alt*="QR"]'); + await expect(qr).toBeVisible(); + await page.screenshot( + {path: path.join(dir, `${state.name}-expanded.png`)}); + + // when the expanded QR sits below the fold (many wallets on a + // short/phone viewport), also capture a shot scrolled to the + // QR, so the gallery shows it is reachable + const qrInView = await qr.evaluate(el => { + const r = el.getBoundingClientRect(); + return r.bottom <= window.innerHeight && r.top >= 0; + }); + if(!qrInView) { + await qr.scrollIntoViewIfNeeded(); + await expect(qr).toBeInViewport(); + await page.screenshot( + {path: path.join(dir, `${state.name}-expanded-scrolled.png`)}); + } + } + }); + } + }); + } +}); diff --git a/test/e2e/wallet-chooser.spec.js b/test/e2e/wallet-chooser.spec.js new file mode 100644 index 00000000..5af90c18 --- /dev/null +++ b/test/e2e/wallet-chooser.spec.js @@ -0,0 +1,165 @@ +/*! + * New BSD License (3-clause) + * Copyright (c) 2026, Digital Bazaar, Inc. + * All rights reserved. + */ +import {expect, test} from '@playwright/test'; + +/* Geometric-invariant tests for the first party wallet chooser dialog, +exercised via the dev-only `/test/wallet-chooser` harness route. These assert +structural properties (no horizontal scroll, header pinned, panel fills the +popup) rather than pixels, so they catch the layout regressions seen in the +7.4.x series without screenshot-diff noise. */ + +// the states the dialog must handle, keyed for readable test titles +const STATES = [ + {name: '0 wallets + QR', query: 'hints=0&qr=1', hints: 0, qr: true}, + {name: '1 wallet + QR', query: 'hints=1&qr=1', hints: 1, qr: true}, + {name: '5 wallets + QR', query: 'hints=5&qr=1', hints: 5, qr: true}, + {name: '5 wallets, no QR', query: 'hints=5&qr=0', hints: 5, qr: false}, + {name: '15 wallets + QR', query: 'hints=15&qr=1', hints: 15, qr: true} +]; + +async function gotoChooser(page, query) { + await page.goto(`/test/wallet-chooser?${query}`); + // wait for the dialog content to render + await page.locator('.wrm-modal-1p .wrm-modal-content').waitFor(); +} + +for(const state of STATES) { + test.describe(state.name, () => { + test.beforeEach(async ({page}) => gotoChooser(page, state.query)); + + test('renders the expected number of wallet hints', async ({page}) => { + await expect(page.locator('.wrm-hint-list .wrm-selectable')) + .toHaveCount(state.hints); + }); + + test('shows no horizontal scrollbar', async ({page}) => { + // the 7.4.1 regression produced a horizontal scrollbar on the 1p + // content. Assert the document does not scroll horizontally. + const scrolls = await page.evaluate(() => + document.documentElement.scrollWidth > window.innerWidth); + expect(scrolls).toBe(false); + }); + + // Stricter than the scrollbar check: detects the horizontal OVERFLOW + // condition itself (an element whose content is wider than its box), + // not just the resulting scrollbar — so the `overflow-x: hidden` + // band-aid in the 1p dialog CSS cannot make it a false pass. + // + // SKIPPED for now: this currently fails because the dialog still + // relies on that band-aid to hide a real overflow (the headers and + // separators bleed their border edge-to-edge with a negative side + // margin that overhangs the border-box flex layout). The fix is + // deferred until the styles are reworked against code we own — after + // the components we use are pulled out of `vue-web-request-mediator` + // and that dependency is removed. Enable this test as part of that + // CSS work; it is the guard that proves the band-aid can be removed. + test.skip('has no horizontal overflow', async ({page}) => { + const offenders = await page.evaluate(() => { + const out = []; + for(const el of document.querySelectorAll('.wrm-modal-1p *')) { + // only inspect elements with a real content box. Inline + // elements (e.g. , ) report `clientWidth: 0` but + // a nonzero `scrollWidth`, which Firefox surfaces and Chromium + // does not; that difference is not overflow, so skip them. + const cs = getComputedStyle(el); + if(cs.display === 'inline' || el.clientWidth === 0) { + continue; + } + if(el.scrollWidth - el.clientWidth > 1) { + out.push(`${el.className || el.tagName} ` + + `(scrollW ${el.scrollWidth} > clientW ${el.clientWidth})`); + } + } + return out; + }); + expect(offenders, offenders.join('; ')).toEqual([]); + }); + + test('does not scroll the popup window', async ({page}) => { + // only an inner region may scroll; the window itself must not (that + // scrolled the header/Close button away and stacked scrollbars) + const windowScrolls = await page.evaluate(() => + document.documentElement.scrollHeight > window.innerHeight + 1); + expect(windowScrolls).toBe(false); + }); + + test('keeps the header pinned while the list scrolls', async ({page}) => { + const header = page.locator('.wrm-modal-1p .wrm-modal-content-header') + .first(); + const before = await header.boundingBox(); + // scroll the wallet list to its end, if it scrolls at all + await page.evaluate(() => { + const list = document.querySelector('.wrm-hint-list'); + if(list) { + list.scrollTop = list.scrollHeight; + } + }); + const after = await header.boundingBox(); + expect(after.y).toBeCloseTo(before.y, 0); + }); + + test('panel fills the popup width (no background bleed)', + async ({page}) => { + // the content panel must span the full popup width; gaps showed the + // page background as dark bands + const fills = await page.evaluate(() => { + const panel = document.querySelector( + '.wrm-modal-1p .wrm-modal-content'); + return Math.abs(panel.getBoundingClientRect().width - + window.innerWidth) <= 1; + }); + expect(fills).toBe(true); + }); + + test('header border bleeds edge to edge', async ({page}) => { + // the dialog header ("Choose a Wallet") is full-bleed by design: its + // bottom border spans the popup edge to edge, distinguishing it from + // an inset in-page separator. This guards that bleed directly, so a + // future CSS rework cannot silently shrink the border to the padded + // content width (a change the geometric-overflow checks would miss, + // since shrinking it removes no overflow). The header is the panel's + // direct-child header, not the empty separator div the body reuses + // the class for. + const flush = await page.evaluate(() => { + const header = document.querySelector( + '.wrm-modal-1p .wrm-modal-content > .wrm-modal-content-header'); + const panel = document.querySelector( + '.wrm-modal-1p .wrm-modal-content'); + const h = header.getBoundingClientRect(); + const p = panel.getBoundingClientRect(); + return Math.abs(h.left - p.left) <= 1 && + Math.abs(h.right - p.right) <= 1; + }); + expect(flush).toBe(true); + }); + + if(state.qr && state.hints > 0) { + test('shows the cross-device expander, collapsed', async ({page}) => { + await expect(page.locator('.cross-device-toggle')).toBeVisible(); + await expect(page.locator('img[alt*="QR"]')).toHaveCount(0); + }); + + test('expands the QR code when the prompt is clicked', async ({page}) => { + await page.locator('.cross-device-toggle').click(); + await expect(page.locator('img[alt*="QR"]')).toBeVisible(); + }); + } + + if(state.qr && state.hints === 0) { + test('shows the QR code immediately, no expander', async ({page}) => { + await expect(page.locator('.cross-device-toggle')).toHaveCount(0); + await expect(page.locator('img[alt*="QR"]')).toBeVisible(); + }); + } + + if(!state.qr) { + test('shows no cross-device section', async ({page}) => { + await expect(page.locator('.cross-device-toggle')).toHaveCount(0); + await expect(page.locator('img[alt*="QR"]')).toHaveCount(0); + }); + } + }); +} diff --git a/web/router.js b/web/router.js index 11a3deab..2ed0ed58 100644 --- a/web/router.js +++ b/web/router.js @@ -30,6 +30,15 @@ export async function createRouter() { /* webpackChunkName: "FirstPartyMediatorPage" */ './routes/FirstPartyMediatorPage.vue'), meta: {title: 'Allow Wallet'} - }] + }, + // dev-only harness for exercising the wallet chooser dialog layout + // without CHAPI; excluded from production builds + ...(process.env.NODE_ENV !== 'production' ? [{ + path: '/test/wallet-chooser', + component: () => import( + /* webpackChunkName: "TestWalletChooser" */ + './routes/TestWalletChooser.vue'), + meta: {title: 'Test Wallet Chooser'} + }] : [])] }); } diff --git a/web/routes/TestWalletChooser.vue b/web/routes/TestWalletChooser.vue new file mode 100644 index 00000000..46106dcf --- /dev/null +++ b/web/routes/TestWalletChooser.vue @@ -0,0 +1,103 @@ + + + + + From 3c96af4071c3b76312f8647563b43ebea40a016d Mon Sep 17 00:00:00 2001 From: Derek Scruggs Date: Sun, 14 Jun 2026 08:50:54 -0500 Subject: [PATCH 2/7] Mark the overflow test as fixme rather than skip. `test.fixme` reports the deferred "has no horizontal overflow" check as a known-broken test to re-enable, instead of a silent skip, so the CSS rework that removes the `overflow-x: hidden` band-aid is visible in the runner output as outstanding work. Co-Authored-By: Claude Opus 4.8 (1M context) --- test/e2e/wallet-chooser.spec.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/test/e2e/wallet-chooser.spec.js b/test/e2e/wallet-chooser.spec.js index 5af90c18..010c4ee6 100644 --- a/test/e2e/wallet-chooser.spec.js +++ b/test/e2e/wallet-chooser.spec.js @@ -48,15 +48,17 @@ for(const state of STATES) { // not just the resulting scrollbar — so the `overflow-x: hidden` // band-aid in the 1p dialog CSS cannot make it a false pass. // - // SKIPPED for now: this currently fails because the dialog still + // FIXME for now: this currently fails because the dialog still // relies on that band-aid to hide a real overflow (the headers and // separators bleed their border edge-to-edge with a negative side // margin that overhangs the border-box flex layout). The fix is // deferred until the styles are reworked against code we own — after // the components we use are pulled out of `vue-web-request-mediator` - // and that dependency is removed. Enable this test as part of that - // CSS work; it is the guard that proves the band-aid can be removed. - test.skip('has no horizontal overflow', async ({page}) => { + // and that dependency is removed. `fixme` (not `skip`) so the runner + // reports it as a known-broken test to re-enable, not a silent skip. + // Drop the `.fixme` as part of that CSS work; it is the guard that + // proves the band-aid can be removed. + test.fixme('has no horizontal overflow', async ({page}) => { const offenders = await page.evaluate(() => { const out = []; for(const el of document.querySelectorAll('.wrm-modal-1p *')) { From 3231cc58ec5afdaa1faa99df9641b7bcf45970c3 Mon Sep 17 00:00:00 2001 From: Derek Scruggs Date: Sun, 14 Jun 2026 08:53:44 -0500 Subject: [PATCH 3/7] Run the wallet chooser layout suite in CI. Add a `test-e2e` job as a sibling of the lint job (both run in parallel, no `needs:`). It installs the Playwright browsers, maps the `authn.localhost` server domain to loopback so the auto-started dev server is reachable, and runs `npm run test:e2e` across the desktop and emulated-phone projects. Set `permissions: {}` and rename the workflow to reflect that it now lints and tests. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/main.yml | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 24c24bde..6aaaeeac 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,7 +1,9 @@ -name: Lint CI +name: Lint and Test CI on: [push] +permissions: {} + jobs: lint: runs-on: ubuntu-latest @@ -18,3 +20,24 @@ jobs: - run: npm install - name: Run ESLint run: npm run lint + test-e2e: + runs-on: ubuntu-latest + timeout-minutes: 20 + strategy: + matrix: + node-version: [24.x] + steps: + - uses: actions/checkout@v6 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v6 + with: + node-version: ${{ matrix.node-version }} + - run: npm install + # the dev server binds the `authn.localhost` domain (config.server.domain); + # map it to loopback so Playwright's webServer can reach it in CI + - name: Map authn.localhost to loopback + run: echo "127.0.0.1 authn.localhost" | sudo tee -a /etc/hosts + - name: Install Playwright browsers + run: npx playwright install --with-deps chromium webkit firefox + - name: Run the wallet chooser layout suite + run: npm run test:e2e From 35d23fb7c73a1bac336ddeb99a378ca3d3f942ac Mon Sep 17 00:00:00 2001 From: Derek Scruggs Date: Sun, 14 Jun 2026 09:34:20 -0500 Subject: [PATCH 4/7] Add the github reporter to the e2e suite in CI. In CI, emit the `github` reporter alongside `list` so failed and skipped/fixme tests surface as inline GitHub annotations on the run and PR, instead of collapsing into the summary's skipped count. Keep plain `list` locally, where the workflow-command output is just noise. Co-Authored-By: Claude Opus 4.8 (1M context) --- playwright.config.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/playwright.config.js b/playwright.config.js index 3a05242b..2de7e1c8 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -12,7 +12,12 @@ export default defineConfig({ // the dialog suite is layout-only and deterministic; fail fast in CI forbidOnly: !!process.env.CI, retries: 0, - reporter: 'list', + // `list` for readable output everywhere; in CI also emit the `github` + // reporter, which surfaces failed and skipped/fixme tests as inline + // GitHub annotations on the run and PR (e.g. the deferred + // `has no horizontal overflow` checks show up rather than vanishing + // into the summary's skipped count) + reporter: process.env.CI ? [['list'], ['github']] : 'list', use: { baseURL: BASE_URL, // the dev server uses a self-signed localhost certificate From 862a5ca6b53d54f5d5ce0420a8e461f6f3d1997b Mon Sep 17 00:00:00 2001 From: Derek Scruggs Date: Sun, 14 Jun 2026 09:42:52 -0500 Subject: [PATCH 5/7] Drop the e2e CI job; keep the overflow test active and failing. Hold off on running the layout suite in CI for now. Revert the `test-e2e` workflow job and the CI-only `github` reporter, leaving the lint workflow as it was. Leave the "has no horizontal overflow" test active (not skipped or fixme) rather than green-by-deferral. It fails today against the band-aid CSS, so a local `npm run test:e2e` run shows it red as a standing reminder of the deferred CSS rework; since the suite is not in CI, the failure gates nothing. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/main.yml | 25 +------------------------ playwright.config.js | 7 +------ test/e2e/wallet-chooser.spec.js | 20 ++++++++++---------- 3 files changed, 12 insertions(+), 40 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6aaaeeac..24c24bde 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,9 +1,7 @@ -name: Lint and Test CI +name: Lint CI on: [push] -permissions: {} - jobs: lint: runs-on: ubuntu-latest @@ -20,24 +18,3 @@ jobs: - run: npm install - name: Run ESLint run: npm run lint - test-e2e: - runs-on: ubuntu-latest - timeout-minutes: 20 - strategy: - matrix: - node-version: [24.x] - steps: - - uses: actions/checkout@v6 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v6 - with: - node-version: ${{ matrix.node-version }} - - run: npm install - # the dev server binds the `authn.localhost` domain (config.server.domain); - # map it to loopback so Playwright's webServer can reach it in CI - - name: Map authn.localhost to loopback - run: echo "127.0.0.1 authn.localhost" | sudo tee -a /etc/hosts - - name: Install Playwright browsers - run: npx playwright install --with-deps chromium webkit firefox - - name: Run the wallet chooser layout suite - run: npm run test:e2e diff --git a/playwright.config.js b/playwright.config.js index 2de7e1c8..3a05242b 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -12,12 +12,7 @@ export default defineConfig({ // the dialog suite is layout-only and deterministic; fail fast in CI forbidOnly: !!process.env.CI, retries: 0, - // `list` for readable output everywhere; in CI also emit the `github` - // reporter, which surfaces failed and skipped/fixme tests as inline - // GitHub annotations on the run and PR (e.g. the deferred - // `has no horizontal overflow` checks show up rather than vanishing - // into the summary's skipped count) - reporter: process.env.CI ? [['list'], ['github']] : 'list', + reporter: 'list', use: { baseURL: BASE_URL, // the dev server uses a self-signed localhost certificate diff --git a/test/e2e/wallet-chooser.spec.js b/test/e2e/wallet-chooser.spec.js index 010c4ee6..a62b0ab4 100644 --- a/test/e2e/wallet-chooser.spec.js +++ b/test/e2e/wallet-chooser.spec.js @@ -48,17 +48,17 @@ for(const state of STATES) { // not just the resulting scrollbar — so the `overflow-x: hidden` // band-aid in the 1p dialog CSS cannot make it a false pass. // - // FIXME for now: this currently fails because the dialog still - // relies on that band-aid to hide a real overflow (the headers and - // separators bleed their border edge-to-edge with a negative side - // margin that overhangs the border-box flex layout). The fix is - // deferred until the styles are reworked against code we own — after + // KNOWN FAILING: this fails today because the dialog still relies on + // that band-aid to hide a real overflow (the headers and separators + // bleed their border edge-to-edge with a negative side margin that + // overhangs the border-box flex layout). It is left active, not + // skipped, so it shows up as a red test when the suite is run locally + // — a standing reminder of the deferred CSS work. The fix comes after // the components we use are pulled out of `vue-web-request-mediator` - // and that dependency is removed. `fixme` (not `skip`) so the runner - // reports it as a known-broken test to re-enable, not a silent skip. - // Drop the `.fixme` as part of that CSS work; it is the guard that - // proves the band-aid can be removed. - test.fixme('has no horizontal overflow', async ({page}) => { + // and that dependency is removed; this test then turns green and + // proves the band-aid can be removed. (CI does not run this suite + // yet, so this failure does not gate anything.) + test('has no horizontal overflow', async ({page}) => { const offenders = await page.evaluate(() => { const out = []; for(const el of document.querySelectorAll('.wrm-modal-1p *')) { From b53d2ca7d781ed9a5a441abf84c23bed0aec4acc Mon Sep 17 00:00:00 2001 From: Derek Scruggs Date: Sun, 14 Jun 2026 11:39:27 -0500 Subject: [PATCH 6/7] Fix the package version regressed to 7.4.3. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Assembling this branch pulled `package.json`/`package-lock.json` from an older harness branch cut before `main` reached 7.4.4, which dragged its stale `7.4.3` version back in — a backwards version bump. Set the version to 7.5.0 to match the CHANGELOG entry for the added tooling, and sync the lockfile. Co-Authored-By: Claude Opus 4.8 (1M context) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 19e04bc9..e58f2e25 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "authio", - "version": "7.4.3", + "version": "7.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "authio", - "version": "7.4.3", + "version": "7.5.0", "license": "SEE LICENSE IN LICENSE", "dependencies": { "@bedrock/config-yaml": "^4.3.3", diff --git a/package.json b/package.json index 1a025b9d..0828b43f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "authio", - "version": "7.4.3", + "version": "7.5.0", "type": "module", "main": "./lib/index.js", "browser": { From 2ab0af9dc2ddef7a6ecadf1f6cc04a7ac2ea3687 Mon Sep 17 00:00:00 2001 From: Dave Longley Date: Sun, 14 Jun 2026 14:08:39 -0400 Subject: [PATCH 7/7] Update file headers, fix version for automation. Co-authored-by: Dave Longley --- package.json | 2 +- playwright.config.js | 1 - test/e2e/build-gallery-index.js | 1 - test/e2e/gallery.spec.js | 1 - test/e2e/wallet-chooser.spec.js | 1 - web/router.js | 2 +- web/routes/TestWalletChooser.vue | 1 - 7 files changed, 2 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 0828b43f..7b1878bb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "authio", - "version": "7.5.0", + "version": "7.4.4", "type": "module", "main": "./lib/index.js", "browser": { diff --git a/playwright.config.js b/playwright.config.js index 3a05242b..34ffd700 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -1,7 +1,6 @@ /*! * New BSD License (3-clause) * Copyright (c) 2026, Digital Bazaar, Inc. - * All rights reserved. */ import {defineConfig, devices} from '@playwright/test'; diff --git a/test/e2e/build-gallery-index.js b/test/e2e/build-gallery-index.js index 3262d947..ff447d3a 100644 --- a/test/e2e/build-gallery-index.js +++ b/test/e2e/build-gallery-index.js @@ -1,7 +1,6 @@ /*! * 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'; diff --git a/test/e2e/gallery.spec.js b/test/e2e/gallery.spec.js index 44265761..7c8d40ff 100644 --- a/test/e2e/gallery.spec.js +++ b/test/e2e/gallery.spec.js @@ -1,7 +1,6 @@ /*! * New BSD License (3-clause) * Copyright (c) 2026, Digital Bazaar, Inc. - * All rights reserved. */ import {expect, test} from '@playwright/test'; import {fileURLToPath} from 'node:url'; diff --git a/test/e2e/wallet-chooser.spec.js b/test/e2e/wallet-chooser.spec.js index a62b0ab4..ede7795d 100644 --- a/test/e2e/wallet-chooser.spec.js +++ b/test/e2e/wallet-chooser.spec.js @@ -1,7 +1,6 @@ /*! * New BSD License (3-clause) * Copyright (c) 2026, Digital Bazaar, Inc. - * All rights reserved. */ import {expect, test} from '@playwright/test'; diff --git a/web/router.js b/web/router.js index 2ed0ed58..06a22301 100644 --- a/web/router.js +++ b/web/router.js @@ -1,5 +1,5 @@ /*! - * Copyright (c) 2023 Digital Bazaar, Inc. All rights reserved. + * Copyright (c) 2023-2026 Digital Bazaar, Inc. */ import {createRouter as _createRouter, createWebHistory} from 'vue-router'; diff --git a/web/routes/TestWalletChooser.vue b/web/routes/TestWalletChooser.vue index 46106dcf..03e279b3 100644 --- a/web/routes/TestWalletChooser.vue +++ b/web/routes/TestWalletChooser.vue @@ -23,7 +23,6 @@ /*! * New BSD License (3-clause) * Copyright (c) 2026, Digital Bazaar, Inc. - * All rights reserved. */ import {computed} from 'vue'; import MediatorWizard from '../components/MediatorWizard.vue';