Improved testing flow
This commit is contained in:
15
README.md
15
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 <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
29
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[@]}"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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">‹ Back</button>
|
||||
<button id="btn-mode-next" class="primary">Continue ›</button>
|
||||
<button id="btn-mode-next" class="primary" disabled>Continue ›</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:',
|
||||
|
||||
Reference in New Issue
Block a user