Skip to content

Commit d2cb067

Browse files
Merge pull request #101 from elek-io/e2e-tests-continued
Added Playwright E2E setup
2 parents 3c4a5c3 + de46666 commit d2cb067

20 files changed

Lines changed: 486 additions & 39 deletions

.changeset/brisk-otters-verify.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@elek-io/client': patch
3+
---
4+
5+
Add Playwright E2E tests that build the unpacked app and run against it in CI, across the same platforms as Core, with an isolated Electron userData directory and Core 0.21's ELEK_IO_DATA_DIR for test data. Modernize the toolchain to Electron 43 with electron-vite 5 on the Node 24 runtime, TypeScript 6, eslint 10, pnpm 11 supply-chain policies, and refreshed dependencies including lucide-react 1.x and react-day-picker 10. CD now publishes build artifacts to a draft GitHub Release.

.github/workflows/cd.yml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,13 @@ jobs:
1919
hasChangesets: ${{ steps.changesets.outputs.hasChangesets }}
2020
steps:
2121
- name: Check out code
22-
uses: actions/checkout@v4
22+
uses: actions/checkout@v7
2323
with:
2424
fetch-depth: 1
2525
- name: Install pnpm
26-
uses: pnpm/action-setup@v4
26+
uses: pnpm/action-setup@v6
2727
- name: Setup Node.js environment
28-
uses: actions/setup-node@v4
28+
uses: actions/setup-node@v6
2929
with:
3030
node-version-file: '.node-version'
3131
cache: 'pnpm'
@@ -56,13 +56,13 @@ jobs:
5656
runs-on: ${{ matrix.platform }}
5757
steps:
5858
- name: Check out code
59-
uses: actions/checkout@v4
59+
uses: actions/checkout@v7
6060
with:
6161
fetch-depth: 1
6262
- name: Install pnpm
63-
uses: pnpm/action-setup@v4
63+
uses: pnpm/action-setup@v6
6464
- name: Setup Node.js environment
65-
uses: actions/setup-node@v4
65+
uses: actions/setup-node@v6
6666
with:
6767
node-version-file: '.node-version'
6868
cache: 'pnpm'

.github/workflows/ci.yml

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@ jobs:
1515
runs-on: ${{ matrix.platform }}
1616
steps:
1717
- name: Check out code
18-
uses: actions/checkout@v4
18+
uses: actions/checkout@v7
1919
with:
2020
fetch-depth: 1
2121
- name: Install pnpm
22-
uses: pnpm/action-setup@v4
22+
uses: pnpm/action-setup@v6
2323
- name: Setup Node.js environment
24-
uses: actions/setup-node@v4
24+
uses: actions/setup-node@v6
2525
with:
2626
node-version-file: '.node-version'
2727
cache: 'pnpm'
@@ -35,12 +35,41 @@ jobs:
3535
# uses: github/codeql-action/upload-sarif@v1
3636
# with:
3737
# sarif_file: ../electronegativity_results
38+
# Full package build (not build:unpack), so PR CI also verifies that
39+
# electron-builder can still produce the installers. The E2E tests
40+
# only use the unpacked directory this step produces alongside them
3841
- name: Build
3942
run: pnpm run build
43+
# Invoke the runner directly, since "pnpm run test" would rebuild
44+
# the app that the build step above already built
45+
- name: Run E2E tests (Linux)
46+
if: runner.os == 'Linux'
47+
run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" pnpm exec playwright test
48+
- name: Run E2E tests (macOS/Windows)
49+
if: runner.os != 'Linux'
50+
run: pnpm exec playwright test
51+
- name: Upload Playwright report
52+
uses: actions/upload-artifact@v7
53+
if: ${{ !cancelled() }}
54+
with:
55+
name: playwright-report-${{ matrix.platform }}
56+
path: playwright-report/
57+
retention-days: 30
58+
# The HTML report above embeds the trace, but not each failed test's
59+
# Core data directory (the git repos and files Core wrote). preserveOutput
60+
# in the Playwright config keeps only failed tests' output, so this ships
61+
# just those data dirs
62+
- name: Upload test results
63+
uses: actions/upload-artifact@v7
64+
if: failure()
65+
with:
66+
name: test-results-${{ matrix.platform }}
67+
path: test-results/
68+
retention-days: 7
4069
# Uncomment if necessary
4170
# For example to inspect the build applications
4271
- name: Upload artifact
43-
uses: actions/upload-artifact@v4
72+
uses: actions/upload-artifact@v7
4473
with:
4574
name: artifact_${{ matrix.platform }}
4675
path: |

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,7 @@ dist/
9797

9898
# Sentry Config File
9999
.env.sentry-build-plugin
100+
101+
# Playwright
102+
/test-results/
103+
/playwright-report/

AGENTS.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ Two rules follow:
3030
- `pnpm check-types` - type-check both processes with tsc, no emit (`check-types:node` and `check-types:web` cover one process each)
3131
- `pnpm lint` - run eslint
3232
- `pnpm format` - format with prettier
33+
- `pnpm test` - build the unpacked app, then run the Playwright E2E tests against it (`test:ui`, `test:debug` and `test:report` variants exist, `pnpm exec playwright test` skips the rebuild)
3334

3435
## Conventions
3536

@@ -39,5 +40,6 @@ Two rules follow:
3940

4041
## Testing notes
4142

42-
- The client has no automated test runner configured yet, so there are no existing tests to follow. Until one is set up, verify a change by running the app with `pnpm dev` telling the user to exercise the affected flows, and make sure `pnpm check-types` and `pnpm lint` pass before considering it done.
43+
- Playwright E2E tests live in `tests/` and run against the packaged app. See [`contributing/testing.md`](./contributing/testing.md) for how the fixtures, isolation and CI work. Run `pnpm test` to verify a change end to end, and make sure `pnpm check-types` and `pnpm lint` pass before considering it done.
44+
- There is no unit test runner. Verify behavior without E2E coverage by running the app with `pnpm dev` and telling the user to exercise the affected flows.
4345
- All Core file and git operations run in the main process and are reached from the renderer only through `window.ipc`. Exercise them through the running app, not in isolation.

contributing/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ Then proceed to the specific topics based on your interests or contribution goal
2525
- **[Breadcrumb Navigation](./renderer/breadcrumb-navigation.md)** - Route-based breadcrumb system for hierarchical navigation
2626
- **[Theming and Styling](./renderer/theming-and-styling.md)** - shadcn/ui components, Tailwind v4 CSS configuration, and dark mode
2727
- **[Internationalization](./renderer/internationalization.md)** - translations, datetime locales, and multi-language form state
28+
- **[E2E Testing](./testing.md)** - Playwright tests against the packaged app, fixtures, isolation, and CI
2829
- **[Releasing](./releasing.md)** - versioning with changesets and how CD publishes draft GitHub Releases
2930

3031
For a running list of things Core supports but the client cannot use yet, see **[Not Yet Implemented](./not-yet-implemented.md)**. Add to it whenever you find such a gap.

contributing/testing.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# E2E Testing
2+
3+
The client is tested end to end with [Playwright](https://playwright.dev/docs/api/class-electron) driving the packaged Electron app. Playwright labels its Electron support experimental, but it is the best fit for this project. It runs over the Chrome DevTools Protocol, Microsoft tests VS Code through the same path, and breakages with new Electron versions have historically been fixed within days. The main alternative, [WebdriverIO's electron service](https://webdriver.io/docs/desktop-testing/electron), is listed first in [Electron's testing docs](https://www.electronjs.org/docs/latest/tutorial/automated-testing) and would be the fallback if Playwright's Electron support ever goes away. Only the small launch surface in the fixture is Playwright-Electron specific, so the specs themselves would port.
4+
5+
There is no unit test setup yet. Everything below is about E2E tests.
6+
7+
## Running tests
8+
9+
Tests run against the unpacked build in `dist`. The test scripts build it first, so they always test the current code:
10+
11+
```sh
12+
pnpm test # build unpacked, then run all tests
13+
pnpm test:ui # build unpacked, then open Playwright's UI mode
14+
pnpm test:debug # build unpacked, then run with the Playwright inspector
15+
pnpm test:report # open the HTML report of the last run
16+
```
17+
18+
When iterating on test code only, skip the rebuild by invoking the runner directly:
19+
20+
```sh
21+
pnpm exec playwright test
22+
```
23+
24+
CI does the same, since its build step already produced the packaged app. Note that reruns inside UI or debug mode also do not rebuild, only the initial invocation does.
25+
26+
## Why the packaged build
27+
28+
Playwright's own Electron examples launch the source entry with the bundled dev binary, `electron.launch({ args: ['main.js'] })`. This project launches the packaged build instead. That is a deliberate choice. It runs what users actually run, so the tests cover the Content Security Policy, the custom `elek-io-local-file:` protocol and the electron-builder output layout, none of which a source launch exercises.
29+
30+
The cost is that tests only see a change after a rebuild, which is why the `pnpm test` scripts build first and CI builds before the suite. Iterating on test code alone with `pnpm exec playwright test` reuses the existing build (see [Running tests](#running-tests)).
31+
32+
Launching a packaged build shapes the fixture in two ways that differ from the Playwright examples:
33+
34+
- It finds the built executable with `parseElectronApp` from `electron-playwright-helpers` and passes it as `executablePath`, instead of relying on the dev binary in `node_modules`. This is the only reason that dependency is here.
35+
- It passes no main script argument. A packaged build bundles and loads its own entry, so the argument is unnecessary. It was also a suspect in a Windows launch hang, so it is left off on purpose.
36+
37+
## How the fixtures work
38+
39+
Specs live in `tests/specs` and import `test` from [`tests/fixtures/electronApp.ts`](../tests/fixtures/electronApp.ts) instead of `@playwright/test`. The fixtures provide:
40+
41+
- `electronApp` launches the unpacked build for the current platform from `dist`.
42+
- `mainWindow` is the app's first window as a Playwright `Page`.
43+
44+
The launch does a few important things:
45+
46+
- **Isolation**: each test gets its own empty data directories under its `test-results` output path, so tests never touch real user data and always start clean. Two stores are redirected: Core's project data via the `ELEK_IO_DATA_DIR` environment variable, which Core reads at startup (see Core's usage docs), and Electron's own userData (the Chromium profile, localStorage and caches) via Chromium's `--user-data-dir` launch switch, which Electron honors without any test-only code in the app. An earlier version redirected the OS home directory instead, but on Windows a redirected `USERPROFILE` stalls Electron's boot before Chromium starts, so these two targeted redirects are both cleaner and avoid that hang.
47+
- **`NODE_ENV=test`**: both the main process and the renderer disable Sentry when they see this, so test runs do not report errors, traces or replays. A disabled Sentry client also never sets up its integrations, so profiling and replay stay dormant.
48+
- **`ELECTRON_RUN_AS_NODE` is removed**: Electron based terminals like the one in VSCode set it, which would turn the launched app into a plain Node process and fail the launch with `Process failed to launch!`.
49+
50+
Playwright disables the Chromium sandbox on Linux itself (it adds `--no-sandbox` unless `chromiumSandbox: true`), so unpacked builds without the SUID sandbox helper run fine on GitHub's Ubuntu runners without the fixture passing anything.
51+
52+
When a test passes, the `mainWindow` fixture asserts that no console errors or warnings occurred and runs an axe accessibility scan, which does not assert on violations yet until the existing ones are resolved. The scan uses axe legacy mode, since otherwise axe opens a blank aggregation page via `context.newPage()`, which Electron does not support. Both checks are skipped when the test already failed, so they do not bury the real failure.
53+
54+
## Writing tests
55+
56+
Reusable page interactions belong in `tests/helpers` as plain functions that take the `Page` first. Prefer role and label based locators over CSS selectors, and auto-retrying assertions like `toHaveURL` over one-shot reads, since route redirects happen client side.
57+
58+
The `tests` folder is type-checked by `pnpm check-types:node` under the strictest settings. That config resolves modules with `nodenext`, so relative imports need explicit `.js` extensions, which Playwright resolves to the `.ts` files. `pnpm lint` also applies the type-aware ESLint rules to `tests`, so `no-floating-promises` flags an unawaited assertion, which type-checking alone does not catch.
59+
60+
## CI
61+
62+
CI builds and then runs the tests on the same four platforms as Core (Linux, Windows, macOS on Intel and ARM). Linux has no display server on the runners, so the tests run under `xvfb-run`. The Playwright HTML report is uploaded as an artifact for every run. When a test fails the raw `test-results` folder is uploaded too, which holds each failed test's Core data directory plus a runner-level trace on the retry (the step timeline, stdio and attachments). Playwright does not record screenshots or videos for Electron windows, nor DOM or network in the trace, since those come from its built-in browser fixtures, which these tests do not use.

eslint.config.mjs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@ export default [
1515
// Base config for all files
1616
...tseslint.configs.recommended,
1717

18-
// Type-checked rules for all source files
18+
// Type-checked rules for all source and test files. Covering tests means
19+
// no-floating-promises catches an unawaited Playwright assertion, the
20+
// classic E2E bug that tsc alone does not flag
1921
{
20-
files: ['src/**/*.{ts,tsx}'],
22+
files: ['src/**/*.{ts,tsx}', 'tests/**/*.ts'],
2123
...tseslint.configs.recommendedTypeChecked[0],
2224
languageOptions: {
2325
...tseslint.configs.recommendedTypeChecked[0].languageOptions,

package.json

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,11 @@
3232
"build:unpack": "pnpm run build --dir",
3333
"build:win": "pnpm run build --win",
3434
"build:mac": "pnpm run build --mac",
35-
"build:linux": "pnpm run build --linux"
35+
"build:linux": "pnpm run build --linux",
36+
"test": "pnpm run build:unpack && playwright test",
37+
"test:debug": "pnpm run build:unpack && playwright test --debug",
38+
"test:ui": "pnpm run build:unpack && playwright test --ui",
39+
"test:report": "playwright show-report"
3640
},
3741
"dependencies": {
3842
"@dnd-kit/core": "6.3.1",
@@ -41,7 +45,7 @@
4145
"@dnd-kit/utilities": "3.2.2",
4246
"@electron-toolkit/preload": "3.0.2",
4347
"@electron-toolkit/utils": "4.0.0",
44-
"@elek-io/core": "0.20.0",
48+
"@elek-io/core": "0.21.0",
4549
"@fontsource-variable/montserrat": "5.2.8",
4650
"@fontsource/roboto": "5.2.10",
4751
"@hookform/resolvers": "5.4.0",
@@ -83,11 +87,13 @@
8387
"zod": "4.4.3"
8488
},
8589
"devDependencies": {
90+
"@axe-core/playwright": "4.12.1",
8691
"@changesets/cli": "2.31.0",
8792
"@electron-toolkit/eslint-config-prettier": "3.0.0",
8893
"@electron-toolkit/eslint-config-ts": "3.1.0",
8994
"@eslint/js": "10.0.1",
9095
"@hookform/devtools": "4.4.0",
96+
"@playwright/test": "1.61.1",
9197
"@tailwindcss/vite": "4.3.2",
9298
"@tanstack/react-query-devtools": "5.101.2",
9399
"@tanstack/react-router-devtools": "1.167.0",
@@ -103,6 +109,7 @@
103109
"autoprefixer": "10.4.21",
104110
"electron": "43.0.0",
105111
"electron-builder": "26.15.6",
112+
"electron-playwright-helpers": "2.1.0",
106113
"electron-vite": "5.0.0",
107114
"eslint": "10.6.0",
108115
"eslint-plugin-jsx-a11y": "6.10.2",

playwright.config.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { defineConfig } from '@playwright/test';
2+
3+
/**
4+
* Playwright configuration for Electron E2E testing
5+
* @see https://playwright.dev/docs/test-configuration
6+
*/
7+
export default defineConfig({
8+
testDir: './tests/specs',
9+
// A single Electron app instance runs at a time
10+
workers: 1,
11+
reporter: 'html',
12+
// Matches Playwright's default. Pinned so the relationship to the fixture's
13+
// 20s firstWindow timeout, which must stay below it, stays visible
14+
timeout: 30000,
15+
16+
// Fail the build on CI if you accidentally left test.only in the source code.
17+
forbidOnly: !!process.env['CI'],
18+
19+
// Retry on CI only
20+
retries: process.env['CI'] ? 2 : 0,
21+
22+
// Keep only failed tests' output. Passed tests' output, including their
23+
// per-test Core data directory, is deleted so it does not pile up on disk
24+
// or in the CI failure artifact
25+
preserveOutput: 'failures-only',
26+
27+
use: {
28+
// Collect a trace when retrying a failed test. For Electron this is the
29+
// runner-level trace (step timeline, stdio, attachments) only, since
30+
// Playwright does not trace the Electron window's DOM, screencast or
31+
// network. Screenshot and video options are omitted because Playwright
32+
// only captures those for its built-in browser fixtures, which these
33+
// Electron tests do not use. @see https://playwright.dev/docs/trace-viewer
34+
trace: 'on-first-retry',
35+
},
36+
});

0 commit comments

Comments
 (0)