Testing
July 17, 2026 · View on GitHub
Test stack
Vitestdrives unit and integration coverage.jsdomis used for DOM-bound module and Svelte component tests.Playwrightruns end-to-end tests against the built Chrome extension.Selenium WebDriverwith BiDi runs end-to-end tests against the built Firefox extension.Playwrightalso runs Android WebView simulator parity tests against the builtdist-android/bundle.Gradleruns Kotlin unit tests and APK assembly checks for the Android shell.- Chrome extension APIs are mocked centrally in tests/mocks/chrome.js.
Commands
npm run build:chrome
npm run build:firefox
npm run build:android
npm run test:unit
npm run test:e2e
npm run test:e2e:firefox
npm run test:e2e:android
npm run android:test
npm run android:assemble:debug
npm run test:ci:web
npm run test:ci:android
npm run test
npm run test:ci
Useful variants:
npm run test:watchruns Vitest in watch mode.npm run test:uiopens the Vitest UI.npm run test:androidbuildsdist-android/and runs the Android simulator Playwright suite.npm run test:ci:webmirrors the web GitHub Actions job (Chrome + Firefox).npm run test:ci:androidmirrors the Android GitHub Actions job.npm run test:ciruns both CI-equivalent jobs locally.
Suite layout
- Unit tests live next to the source file when the module is mostly pure.
- Integration tests live under
tests/integration/. - Chrome E2E tests live under
tests/e2e/. - Firefox E2E tests live under
tests/e2e-firefox/. - Android simulator E2E tests live under
tests/e2e-android/. - Shared DOM helpers live under
tests/helpers/. - Shared response payloads live in
tests/e2e/fixtures/payloads.js.
Vitest conventions
- Keep tests independent. Reset mutable state in
beforeEach. - Use the AAA pattern.
- Prefer
vi.mock(...)over source edits when isolating dependencies. - For Svelte 5 components, mount via
mount()through tests/helpers/svelte.js. - If a test touches browser-extension APIs, extend the shared chrome mock instead of creating one-off mocks.
Playwright workflow (Chrome-target Chromium)
- Build the extension first with
npm run build:chrome. - Playwright loads the exact
dist-chrome/build as an unpacked extension in its bundled Chromium usingchannel: "chromium". - All HTTP(S) requests are intercepted via a catch-all route and resolved through the shared
tests/e2e/fixtures/fixture-resolver.js. Unmatched external URLs receive a terminal 500 response and fail the test. - The E2E suite then drives the real content script, sidebar injectors, and UI overlay behavior.
This lane is intentionally named Chrome-target Chromium, not branded Chrome Stable. Current Chrome and Edge releases removed the command-line flags Playwright needs to side-load unpacked extensions, so Playwright requires its bundled Chromium for extension automation. Release verification for a store-installed build in branded Chrome remains a manual check; the same production source is exercised automatically here and independently in Firefox.
Selenium WebDriver workflow (Firefox)
- Build both extensions with
npm run build. - Selenium Manager auto-provisions GeckoDriver. Set
FIREFOX_BINto an absolute path to override. Relative paths or command names (e.g.firefox) are rejected with an actionable error before driver construction. - Firefox launches with BiDi enabled and the unsigned
dist-firefox/extension installed temporarily viainstallAddon(path, true). - Catch-all HTTP(S) BiDi interception plus
network.provideResponseresolves every external request through the same sharedtests/e2e/fixtures/fixture-resolver.jsas Chromium. Unknown external URLs receive a terminal 500 response and mark the fixture failed. - Extension bootstrap is verified via WebDriver waits for
#bds-toggleand.bds-plus-btn(no fixed sleeps). npm run test:e2e:firefoxinvokes Vitest withvitest.firefox.config.js, running tests fromtests/e2e-firefox/.- In CI, Firefox runs headless. Selenium caches at
~/.cache/selenium— cache this directory for faster runs. The CI job uses${{ steps.setup-firefox.outputs.firefox-path }}for the absolute binary path. - Test results are written to
test-results-firefox/on failure.
Firefox contracts
- Boot: Extension boots and exposes
#bds-toggle,.bds-plus-btn. Driver health checked before each test. - Storage quiescence (#108):
waitForStartupfirst awaits background config/status and content locale persistence. A fresh probe must then remain attotal === 0,remoteConfig === 0for an observed quiet window. Correlated debug writes await storage completion. One uniquereplaceRemoteproduces exactly one total/remote event; a reordered repeat produces none; a second quiet window proves stability. - Performance (#105): 200/2000 message baselines with
waitForAllMessagesProcessed. All 5 samples at each scale required viaappendAndMeasureProcessing. Each < 2000ms, median ratio ≤ 2.5×, absolute increase ≤ 750ms. - Host wrapper:
.bds-download-cardis a descendant of.bds-host-wrapper(wrapper.contains(card)). Wrapper has nonzero width and height, display is notcontents.
Chrome-target Chromium contracts (matching)
The Chromium Playwright lane runs the identical performance (#105), startup/storage-quiescence (#108), and exact host-ownership contracts using the same shared fixture resolver. Its fixture teardown fails the test if any external request was unmatched.
Android simulator
- Build the Android bundle first with
npm run build:android. - Playwright loads the same mock DeepSeek fixture in a mobile Chromium context.
tests/e2e-android/helpers/android.jsinstalls a JS mock ofwindow.AndroidBridgebefore the app bundle runs.- The suite then verifies Android-specific platform gating, download routing, and storage persistence without needing a device farm.
Adding tests
- For pure functions, add co-located
*.test.jsfiles. - For content-script modules with side effects, prefer integration tests under
tests/integration/and mock the smallest stable boundary. - For new UI components, render them through the shared Svelte helper and assert only on public DOM behavior.
- For new extension flows, add them to the mock DeepSeek fixture only if the real selector contract requires it.
- For shared response payloads (pricing, GitHub ZIP, etc.), add them to
tests/e2e/fixtures/payloads.jsso both Chrome and Firefox lanes consume the same data.
CI
- GitHub Actions runs three jobs: Web / Extension (Chrome + unit), Firefox E2E, and Android.
- The web lane builds
dist-chrome/, runsnpm run test:unit, then runsnpm run test:e2e(Chrome-target Playwright Chromium). - The Firefox lane builds
dist-firefox/onubuntu-latestwith Node 20, installs Firefox Stable viabrowser-actions/setup-firefox@v1, caches~/.cache/selenium, runsnpm run test:e2e:firefox, and uploadstest-results-firefox/on failure. - The Android lane builds
dist-android/, assembles the debug APK, runsnpm run test:e2e:android, then runsnpm run android:test. - Coverage, Playwright reports, Android artifacts, and Firefox test results are uploaded on every CI run.
npm testbuilds (pretest) before running the unit + Chrome + Firefox suites — works from a clean checkout with no pre-existingdist-chrome/ordist-firefox/.