From dfe13b093d07c0cb5e7b390421ceb79b6853846e Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Wed, 25 Mar 2026 18:59:47 +0100 Subject: [PATCH] Improved testing flow --- README.md | 15 +++++++++++++++ test.sh | 29 ++++++++++++++++++++++++++++- tests/e2e/integration.spec.js | 25 +++++++++++++++++-------- tests/e2e/playwright.config.js | 2 +- tests/e2e/screenshots.mjs | 20 ++++++++------------ web/src/index.html | 6 +++--- web/src/js/app.js | 12 ++++++++++-- web/src/js/nav.js | 3 ++- web/src/js/strings.js | 2 +- 9 files changed, 85 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index a9fb804..3108a54 100644 --- a/README.md +++ b/README.md @@ -232,6 +232,21 @@ Run all tests (WASM integration + E2E): This builds the web app, compiles the WASM binary, runs the WASM integration tests, and then runs the full E2E suite. On first run it will prompt to download test assets (~190 MB total) to `tests/cached_assets/`. Tests that require missing assets are skipped. +Available flags: + +- `--headed` — run with a visible browser window (also sets `SLOW_MO=1000` for 1s delay between actions) +- `--test ` — filter E2E tests by name (maps to Playwright `--grep`) + +Examples: + +```bash +./test.sh --headed +./test.sh --test "NickelMenu" +./test.sh --headed --test "back navigation" +``` + +Additional Playwright arguments can be appended after the flags. + ### E2E tests (Playwright) The E2E tests cover all major user flows: diff --git a/test.sh b/test.sh index 98a9dd7..d6ccb0a 100755 --- a/test.sh +++ b/test.sh @@ -1,6 +1,33 @@ #!/bin/bash set -euo pipefail +# Parse flags +HEADED="" +GREP="" +EXTRA_ARGS=() +while [[ $# -gt 0 ]]; do + case "$1" in + --headed) + HEADED="--headed" + export SLOW_MO=1000 + shift + ;; + --test) + GREP="--grep" + shift + ;; + *) + if [[ "$GREP" == "--grep" ]]; then + GREP="--grep $1" + shift + else + EXTRA_ARGS+=("$1") + shift + fi + ;; + esac +done + SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" CACHED_ASSETS="$SCRIPT_DIR/tests/cached_assets" @@ -67,4 +94,4 @@ if [ ! -d "node_modules" ]; then npx playwright install --with-deps fi -npm test +npx playwright test $HEADED $GREP "${EXTRA_ARGS[@]}" diff --git a/tests/e2e/integration.spec.js b/tests/e2e/integration.spec.js index c71cdbb..1d6a190 100644 --- a/tests/e2e/integration.spec.js +++ b/tests/e2e/integration.spec.js @@ -22,8 +22,8 @@ test.describe('NickelMenu', () => { await goToManualMode(page); - // Mode selection: NickelMenu should be pre-selected (checked in HTML) - await expect(page.locator('input[name="mode"][value="nickelmenu"]')).toBeChecked(); + // Select NickelMenu and continue + await page.click('input[name="mode"][value="nickelmenu"]'); await page.click('#btn-mode-next'); // NickelMenu configure step @@ -104,8 +104,8 @@ test.describe('NickelMenu', () => { await goToManualMode(page); - // Mode selection - await expect(page.locator('input[name="mode"][value="nickelmenu"]')).toBeChecked(); + // Select NickelMenu and continue + await page.click('input[name="mode"][value="nickelmenu"]'); await page.click('#btn-mode-next'); // NickelMenu configure step — select "Install NickelMenu with preset" @@ -158,6 +158,7 @@ test.describe('NickelMenu', () => { await connectMockDevice(page, { hasNickelMenu: false }); await page.click('#btn-device-next'); + await page.click('input[name="mode"][value="nickelmenu"]'); await page.click('#btn-mode-next'); // Select "Install NickelMenu with preset" @@ -193,6 +194,7 @@ test.describe('NickelMenu', () => { test.skip(!hasNickelMenuAssets(), 'NickelMenu assets not found in webroot'); await goToManualMode(page); + await page.click('input[name="mode"][value="nickelmenu"]'); await page.click('#btn-mode-next'); await expect(page.locator('#step-nickelmenu')).not.toBeHidden(); @@ -228,6 +230,7 @@ test.describe('NickelMenu', () => { test.skip(!hasNickelMenuAssets(), 'NickelMenu assets not found in webroot'); await goToManualMode(page); + await page.click('input[name="mode"][value="nickelmenu"]'); await page.click('#btn-mode-next'); await expect(page.locator('#step-nickelmenu')).not.toBeHidden(); @@ -246,8 +249,8 @@ test.describe('NickelMenu', () => { await page.click('#btn-device-next'); await expect(page.locator('#step-mode')).not.toBeHidden(); - // NickelMenu is pre-selected - await expect(page.locator('input[name="mode"][value="nickelmenu"]')).toBeChecked(); + // Select NickelMenu and continue + await page.click('input[name="mode"][value="nickelmenu"]'); await page.click('#btn-mode-next'); // NickelMenu configure step @@ -317,8 +320,9 @@ test.describe('NickelMenu', () => { await connectMockDevice(page, { hasNickelMenu: false }); - // Continue to mode selection + // Continue to mode selection → select NickelMenu await page.click('#btn-device-next'); + await page.click('input[name="mode"][value="nickelmenu"]'); await page.click('#btn-mode-next'); // NickelMenu configure step @@ -349,8 +353,9 @@ test.describe('NickelMenu', () => { await connectMockDevice(page, { hasNickelMenu: true }); - // Continue to mode selection + // Continue to mode selection → select NickelMenu await page.click('#btn-device-next'); + await page.click('input[name="mode"][value="nickelmenu"]'); await page.click('#btn-mode-next'); // NickelMenu configure step @@ -404,6 +409,7 @@ test.describe('NickelMenu', () => { }); await page.click('#btn-device-next'); + await page.click('input[name="mode"][value="nickelmenu"]'); await page.click('#btn-mode-next'); // Select remove @@ -455,6 +461,7 @@ test.describe('NickelMenu', () => { }); await page.click('#btn-device-next'); + await page.click('input[name="mode"][value="nickelmenu"]'); await page.click('#btn-mode-next'); // NickelMenu configure step @@ -493,6 +500,7 @@ test.describe('NickelMenu', () => { test.skip(!hasReaderlyAssets(), 'Readerly assets not found (run readerly/setup.sh)'); await goToManualMode(page); + await page.click('input[name="mode"][value="nickelmenu"]'); await page.click('#btn-mode-next'); // Select preset → features @@ -542,6 +550,7 @@ test.describe('NickelMenu', () => { test.skip(!hasReaderlyAssets(), 'Readerly assets not found (run readerly/setup.sh)'); await goToManualMode(page); + await page.click('input[name="mode"][value="nickelmenu"]'); await page.click('#btn-mode-next'); // Preset path: enable some features diff --git a/tests/e2e/playwright.config.js b/tests/e2e/playwright.config.js index 72056d0..e33f8de 100644 --- a/tests/e2e/playwright.config.js +++ b/tests/e2e/playwright.config.js @@ -18,7 +18,7 @@ module.exports = defineConfig({ actionTimeout: 10_000, launchOptions: { args: ['--disable-dev-shm-usage'], - serialMo: parseInt(process.env.SLOW_MO || '0', 10), + slowMo: parseInt(process.env.SLOW_MO || '0', 10), }, }, webServer: { diff --git a/tests/e2e/screenshots.mjs b/tests/e2e/screenshots.mjs index cd970a9..7d2afbb 100644 --- a/tests/e2e/screenshots.mjs +++ b/tests/e2e/screenshots.mjs @@ -41,10 +41,10 @@ test('manual nickelmenu', async ({ page }, testInfo) => { // Click "Build downloadable archive" to enter manual mode await page.click('#btn-manual'); await expect(page.locator('#step-mode')).not.toBeHidden(); - await shot(page, dir, '01-mode-selection', testInfo); - // Select NickelMenu → config + // Select NickelMenu, screenshot, then proceed await page.click('input[name="mode"][value="nickelmenu"]'); + await shot(page, dir, '01-mode-selection', testInfo); await page.click('#btn-mode-next'); await expect(page.locator('#step-nickelmenu')).not.toBeHidden(); await shot(page, dir, '02-nickelmenu-config', testInfo); @@ -90,10 +90,10 @@ test('manual patches', async ({ page }, testInfo) => { // Click "Build downloadable archive" to enter manual mode await page.click('#btn-manual'); await expect(page.locator('#step-mode')).not.toBeHidden(); - await shot(page, dir, '01-mode-selection', testInfo); - // Select Patches → version selection + // Select Patches, then screenshot mode selection before proceeding await page.click('input[name="mode"][value="patches"]'); + await shot(page, dir, '01-mode-selection', testInfo); await page.click('#btn-mode-next'); await expect(page.locator('#step-manual-version')).not.toBeHidden(); await shot(page, dir, '02-version-selection', testInfo); @@ -160,13 +160,11 @@ test('connected nickelmenu', async ({ page }, testInfo) => { await expect(page.locator('#step-device')).not.toBeHidden(); await shot(page, dir, '03-device', testInfo); - // Mode selection + // Mode selection — select NickelMenu, screenshot, then proceed await page.click('#btn-device-next'); await expect(page.locator('#step-mode')).not.toBeHidden(); - await shot(page, dir, '04-mode-selection', testInfo); - - // NickelMenu config await page.click('input[name="mode"][value="nickelmenu"]'); + await shot(page, dir, '04-mode-selection', testInfo); await page.click('#btn-mode-next'); await expect(page.locator('#step-nickelmenu')).not.toBeHidden(); await shot(page, dir, '05-nickelmenu-config', testInfo); @@ -222,13 +220,11 @@ test('connected patches', async ({ page }, testInfo) => { await expect(page.locator('#step-device')).not.toBeHidden(); await shot(page, dir, '03-device', testInfo); - // Mode selection + // Mode selection — select Patches, screenshot, then proceed await page.click('#btn-device-next'); await expect(page.locator('#step-mode')).not.toBeHidden(); - await shot(page, dir, '04-mode-selection', testInfo); - - // Patches config await page.click('input[name="mode"][value="patches"]'); + await shot(page, dir, '04-mode-selection', testInfo); await page.click('#btn-mode-next'); await expect(page.locator('#step-patches')).not.toBeHidden(); await shot(page, dir, '05-patches-config', testInfo); diff --git a/web/src/index.html b/web/src/index.html index b5eb1a6..052d71b 100644 --- a/web/src/index.html +++ b/web/src/index.html @@ -193,8 +193,8 @@ diff --git a/web/src/js/app.js b/web/src/js/app.js index 7f2a53f..62fb934 100644 --- a/web/src/js/app.js +++ b/web/src/js/app.js @@ -25,7 +25,7 @@ import { KoboPatchRunner } from './services/patch-runner.js'; import { NickelMenuInstaller, ALL_FEATURES } from '../nickelmenu/installer.js'; import { TL } from './strings.js'; import { isEnabled as analyticsEnabled, track } from './analytics.js'; -import { $, $q, populateSelect } from './dom.js'; +import { $, $q, $qa, populateSelect } from './dom.js'; import { showStep, setNavLabels, setNavStep, hideNav, showNav, stepHistory, setupCardRadios } from './nav.js'; import { initNickelMenu } from './flows/nickelmenu-flow.js'; import { initPatchesFlow } from './flows/patches-flow.js'; @@ -131,7 +131,7 @@ const nm = initNickelMenu(state); const patches = initPatchesFlow(state); // Wire up card-radio interactivity for mode selection and NM option cards. -setupCardRadios(stepMode, 'selection-card--selected'); +setupCardRadios(stepMode, 'selection-card--selected', () => { btnModeNext.disabled = false; }); setupCardRadios($('step-nickelmenu'), 'selection-card--selected'); // ============================================================================= @@ -181,6 +181,14 @@ state.showError = showError; function goToModeSelection() { nm.resetNickelMenuState(); + btnModeNext.disabled = true; + + // Clear any previous mode selection so the user must pick again. + for (const radio of $qa('input[name="mode"]', stepMode)) { + radio.checked = false; + radio.closest('.selection-card')?.classList.remove('selection-card--selected'); + } + const patchesRadio = $q('input[value="patches"]', stepMode); const patchesCard = patchesRadio.closest('.selection-card'); const autoModeNoPatchesAvailable = !state.manualMode && (!state.patchesLoaded || !state.firmwareURL); diff --git a/web/src/js/nav.js b/web/src/js/nav.js index 3901b7b..4553e0d 100644 --- a/web/src/js/nav.js +++ b/web/src/js/nav.js @@ -99,7 +99,7 @@ export function showNav() { * When a radio inside a