diff --git a/README.md b/README.md index fd34d7a..223f9bc 100644 --- a/README.md +++ b/README.md @@ -50,18 +50,33 @@ web/ style.css js/ app.js # Orchestrator: shared state, device connection, mode selection, error/retry, dialogs - dom.js # Shared DOM helpers ($, $q, $qa, formatMB, populateSelect, triggerDownload) + dom.js # Shared DOM/utility helpers ($, $q, populateSelect, renderNmCheckboxList, populateList, fetchOrThrow, triggerDownload) nav.js # Step navigation, progress bar, step history, card radio interactivity - nickelmenu-flow.js # NickelMenu flow: config, features, review, install, done - patches-flow.js # Custom patches flow: configure, build, install/download - kobo-device.js # KoboModels, KoboDevice class - kobo-software-urls.js # Fetches download URLs from JSON, getSoftwareUrl, getDevicesForVersion - nickelmenu/ # NickelMenu feature modules + installer orchestrator - patch-ui.js # PatchUI: loads patches, parses YAML, renders toggle UI - patch-runner.js # KoboPatchRunner: spawns Web Worker per build - patch-worker.js # Web Worker: loads WASM, runs patchFirmware() strings.js # Localized UI strings + analytics.js # Privacy-focused analytics wrapper (Umami) + flows/ + nickelmenu-flow.js # NickelMenu flow: config, features, review, install, done + patches-flow.js # Custom patches flow: configure, build, install/download + services/ + kobo-device.js # KoboModels, KoboDevice class (File System Access API) + kobo-software-urls.js # Fetches download URLs from JSON, getSoftwareUrl, getDevicesForVersion + patch-runner.js # KoboPatchRunner: spawns Web Worker per build + ui/ + patch-ui.js # PatchUI: loads patches, parses YAML, renders toggle UI + workers/ + patch-worker.js # Web Worker: loads WASM, runs patchFirmware() wasm_exec.js # Go WASM runtime (copied from Go SDK by build.sh, gitignored) + nickelmenu/ + installer.js # NickelMenu installer orchestrator: collects files, writes to device or builds ZIP + features/ + helpers.js # Shared postProcess helpers (appendToNmConfig, prependToNmConfig) + custom-menu/ # Required preset menu items + readerly-fonts/ # Font installation + koreader/ # KOReader e-reader installation + simplify-tabs/ # Navigation tab configuration + hide-recommendations/ # Home screen recommendations toggle + hide-notices/ # Home screen notices toggle + screensaver/ # Screensaver image installation patches/ index.json # Available patch manifest downloads.json # Firmware download URLs by version/serial (may be auto-generated) @@ -75,7 +90,7 @@ web/ dist/ # Build output (gitignored, fully regenerable) bundle.js # esbuild output (minified, content-hashed) index.html # Generated with cache-busted references - css/ favicon/ patches/ nickelmenu/ readerly/ koreader/ wasm/ js/wasm_exec.js + css/ favicon/ patches/ nickelmenu/ readerly/ koreader/ wasm/ js/workers/ build.mjs # esbuild build script + asset copy package.json # esbuild, jszip @@ -106,7 +121,8 @@ tests/ paths.js # Test asset paths, expected checksums tar.js # Tar archive parser for output verification integration.spec.js # Playwright E2E tests - playwright.config.js + playwright.config.js # Parallel by default; serial when --headed or --slow + global-setup.js # Creates firmware symlink once before all tests run-e2e.sh # Root scripts @@ -164,13 +180,15 @@ This downloads the latest release directly into `web/dist/koreader/`, skipping t ## Building the frontend -The JS source lives in `web/src/js/` as ES modules, organized around the two main user flows: +The JS source lives in `web/src/js/` as ES modules, organized by role: - **`app.js`** — the orchestrator: creates shared state, handles device connection, mode selection, error recovery, and dialogs. Delegates to the two flow modules below. -- **`nickelmenu-flow.js`** — the entire NickelMenu path (config, features, review, install, done). -- **`patches-flow.js`** — the entire custom patches path (configure, build, install/download). +- **`flows/`** — the two main user journeys: `nickelmenu-flow.js` (install/configure/remove NickelMenu) and `patches-flow.js` (configure/build/install custom patches). +- **`services/`** — modules that wrap external APIs with no DOM dependencies: `kobo-device.js` (File System Access API), `kobo-software-urls.js` (firmware URL lookup), `patch-runner.js` (Web Worker manager). +- **`ui/`** — UI rendering: `patch-ui.js` (patch list rendering and toggle UI). +- **`workers/`** — Web Worker files (not bundled, loaded at runtime): `patch-worker.js` (loads WASM, runs patcher). +- **`dom.js`** — shared DOM/utility helpers (`$`, `$q`, `renderNmCheckboxList`, `populateList`, `fetchOrThrow`, etc.) used across modules. - **`nav.js`** — step navigation, progress bar, and step history (shared by both flows). -- **`dom.js`** — tiny DOM utility helpers (`$`, `$q`, `$qa`, etc.) used everywhere. Flow modules receive a shared `state` object by reference and call back into the orchestrator via `state.showError()` and `state.goToModeSelection()` when they need to cross module boundaries. esbuild bundles everything into a single `web/dist/bundle.js`. @@ -230,6 +248,8 @@ cd tests/e2e ./run-e2e.sh ``` +By default, tests run in parallel across 4 workers. When `--headed` or `--slow` is passed, tests run serially with a single worker so you can follow along in the browser. + To run with a visible browser window: ```bash diff --git a/tests/e2e/global-setup.js b/tests/e2e/global-setup.js new file mode 100644 index 0000000..f0cc3f3 --- /dev/null +++ b/tests/e2e/global-setup.js @@ -0,0 +1,8 @@ +const { setupFirmwareSymlink, cleanupFirmwareSymlink, hasFirmwareZip } = require('./helpers/assets'); + +module.exports = function globalSetup() { + if (hasFirmwareZip()) setupFirmwareSymlink(); + + // Return a teardown function (Playwright >= 1.30) + return () => cleanupFirmwareSymlink(); +}; diff --git a/tests/e2e/integration.spec.js b/tests/e2e/integration.spec.js index fc87719..c71cdbb 100644 --- a/tests/e2e/integration.spec.js +++ b/tests/e2e/integration.spec.js @@ -6,13 +6,10 @@ const zlib = require('zlib'); const JSZip = require('jszip'); const { FIRMWARE_PATH, EXPECTED_SHA1, ORIGINAL_TGZ_SHA1 } = require('./helpers/paths'); -const { hasNickelMenuAssets, hasKoreaderAssets, hasReaderlyAssets, hasFirmwareZip, setupFirmwareSymlink, cleanupFirmwareSymlink } = require('./helpers/assets'); +const { hasNickelMenuAssets, hasKoreaderAssets, hasReaderlyAssets, hasFirmwareZip } = require('./helpers/assets'); const { injectMockDevice, connectMockDevice, overrideFirmwareURLs, goToManualMode, readMockFile, mockPathExists, getWrittenFiles } = require('./helpers/mock-device'); const { parseTar } = require('./helpers/tar'); -test.afterEach(() => { - cleanupFirmwareSymlink(); -}); // ============================================================ // NickelMenu @@ -600,7 +597,7 @@ test.describe('Custom patches', () => { test('no device — full manual mode patching pipeline', async ({ page }) => { test.skip(!hasFirmwareZip(), `Firmware not found at ${FIRMWARE_PATH}`); - setupFirmwareSymlink(); + await goToManualMode(page); // Select "Custom Patches" mode @@ -684,7 +681,7 @@ test.describe('Custom patches', () => { test('no device — restore original firmware', async ({ page }) => { test.skip(!hasFirmwareZip(), `Firmware not found at ${FIRMWARE_PATH}`); - setupFirmwareSymlink(); + await goToManualMode(page); // Select "Custom Patches" mode @@ -815,7 +812,7 @@ test.describe('Custom patches', () => { test('with device — apply patches and verify checksums', async ({ page }) => { test.skip(!hasFirmwareZip(), `Firmware not found at ${FIRMWARE_PATH}`); - setupFirmwareSymlink(); + // Override firmware URLs BEFORE connecting so the app captures the local URL await connectMockDevice(page, { hasNickelMenu: false, overrideFirmware: true }); @@ -890,7 +887,7 @@ test.describe('Custom patches', () => { test('with device — restore original firmware', async ({ page }) => { test.skip(!hasFirmwareZip(), `Firmware not found at ${FIRMWARE_PATH}`); - setupFirmwareSymlink(); + // Override firmware URLs BEFORE connecting so the app captures the local URL await connectMockDevice(page, { hasNickelMenu: false, overrideFirmware: true }); @@ -932,7 +929,7 @@ test.describe('Custom patches', () => { test('with device — build failure shows Go Back and returns to patches', async ({ page }) => { test.skip(!hasFirmwareZip(), `Firmware not found at ${FIRMWARE_PATH}`); - setupFirmwareSymlink(); + await connectMockDevice(page, { hasNickelMenu: false, overrideFirmware: true }); // Select Custom Patches @@ -969,7 +966,7 @@ test.describe('Custom patches', () => { test('with device — real patch failure with Go Back (Allow rotation)', async ({ page }) => { test.skip(!hasFirmwareZip(), `Firmware not found at ${FIRMWARE_PATH}`); - setupFirmwareSymlink(); + await connectMockDevice(page, { hasNickelMenu: false, overrideFirmware: true }); // Select Custom Patches diff --git a/tests/e2e/playwright.config.js b/tests/e2e/playwright.config.js index f524752..72056d0 100644 --- a/tests/e2e/playwright.config.js +++ b/tests/e2e/playwright.config.js @@ -1,10 +1,15 @@ const { defineConfig } = require('@playwright/test'); +const serial = parseInt(process.env.SLOW_MO || '0', 10) > 0 || process.argv.includes('--headed'); + module.exports = defineConfig({ testDir: '.', testMatch: '*.spec.js', timeout: 300_000, retries: 0, + workers: serial ? 1 : 4, + fullyParallel: !serial, + globalSetup: './global-setup.js', expect: { timeout: 10_000, }, @@ -13,7 +18,7 @@ module.exports = defineConfig({ actionTimeout: 10_000, launchOptions: { args: ['--disable-dev-shm-usage'], - slowMo: parseInt(process.env.SLOW_MO || '0', 10), + serialMo: parseInt(process.env.SLOW_MO || '0', 10), }, }, webServer: {