1
0

Improved testing flow

This commit is contained in:
2026-03-25 18:59:47 +01:00
parent 0566d9db10
commit dfe13b093d
9 changed files with 85 additions and 29 deletions

View File

@@ -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 <pattern>` — 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:

29
test.sh
View File

@@ -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[@]}"

View File

@@ -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

View File

@@ -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: {

View File

@@ -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);

View File

@@ -193,8 +193,8 @@
<section id="step-mode" class="step" hidden>
<p>What would you like to do?</p>
<div class="selection-cards" role="radiogroup" aria-label="Mode selection">
<label class="selection-card selection-card--selected selection-card--recommended">
<input type="radio" name="mode" value="nickelmenu" checked>
<label class="selection-card selection-card--recommended">
<input type="radio" name="mode" value="nickelmenu">
<div class="selection-card-body">
<div class="selection-card-title">Install or remove NickelMenu</div>
<div class="recommended-label">Recommended</div>
@@ -212,7 +212,7 @@
<p id="mode-patches-hint" class="fallback-hint" hidden>Custom patches are not available for your software version. You can still install NickelMenu and choose what you want to do with your Kobo.</p>
<div class="step-actions">
<button id="btn-mode-back" class="secondary">&#x2039; Back</button>
<button id="btn-mode-next" class="primary">Continue &#x203A;</button>
<button id="btn-mode-next" class="primary" disabled>Continue &#x203A;</button>
</div>
</section>

View File

@@ -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);

View File

@@ -99,7 +99,7 @@ export function showNav() {
* When a radio inside a <label> is checked, the label gets `selectedClass`;
* all sibling labels lose it.
*/
export function setupCardRadios(container, selectedClass) {
export function setupCardRadios(container, selectedClass, onChange) {
const labels = $qa('label', container);
for (const label of labels) {
const radio = $q('input[type="radio"]', label);
@@ -109,6 +109,7 @@ export function setupCardRadios(container, selectedClass) {
if ($q('input[type="radio"]', l)) l.classList.remove(selectedClass);
}
if (radio.checked) label.classList.add(selectedClass);
if (onChange) onChange(radio);
});
}
}

View File

@@ -17,7 +17,7 @@ export const TL = {
STATUS: {
DEVICE_RECOGNIZED: 'Your device has been recognized. You can continue to the next step!',
NM_REMOVED_ON_REBOOT: 'NickelMenu will be removed on next reboot.',
NM_INSTALLED: 'NickelMenu has been prepared for your Kobo. To complete the installation, follow the instructions below.',
NM_INSTALLED: 'NickelMenu has been installed on your Kobo. To complete the installation, follow the instructions below.',
NM_DOWNLOAD_READY: 'Your NickelMenu package is ready to download. After downloading, a list of installation steps will be displayed.',
NM_WILL_BE_REMOVED: 'NickelMenu will be updated and marked for removal. It will uninstall itself when your Kobo reboots.',
NM_WILL_BE_INSTALLED: 'The following will be installed on your Kobo:',