Skip to content

Commit 21c6de7

Browse files
committed
test: add headless Playwright E2E coverage for the standalone WebUI
- add fixture-based and emulator-backed Playwright suites - add standalone server and emulator harness helpers - support DATA_DIR isolation and dynamic emulator ports - fix AD5X and legacy emulator compatibility issues - document the new test workflow and runtime data path behavior
1 parent 47184d2 commit 21c6de7

39 files changed

Lines changed: 3897 additions & 102 deletions

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
- Playwright E2E testing framework with dual configuration:
13+
- Fixture-based E2E tests (`e2e/`) for fast WebUI validation with a stub HTTP+WebSocket server
14+
- Emulator-backed E2E tests (`e2e-emulator/`) for full lifecycle testing with `flashforge-emulator-v2` printer emulator
15+
- Fixture specs: WebUI smoke tests (asset versioning, context switching) and authentication flow (login, token persistence, logout)
16+
- Emulator specs: direct connection for all 5 printer models, network discovery flow, multi-printer context switching
17+
- Test helpers: emulator harness, standalone server harness, lifecycle runner, scenario definitions, WebUI page object model
18+
- `tsconfig.e2e.json` for E2E TypeScript type checking
19+
- Comprehensive npm test scripts for all test suites:
20+
- `test:e2e`, `test:e2e:smoke`, `test:e2e:auth` for fixture E2E subsets
21+
- `test:e2e:emulator`, `test:e2e:emulator:direct`, `test:e2e:emulator:discovery`, `test:e2e:emulator:multi` for emulator E2E subsets
22+
- `test:e2e:all` for all Playwright suites and `test:all` for Jest + Playwright combined
23+
- `test:e2e:install` for Chromium browser installation
24+
- `@playwright/test` (^1.58.2) as dev dependency for E2E browser automation
1225
- go2rtc-based camera streaming with:
1326
- bundled platform binaries in `resources/bin/`
1427
- `scripts/download-go2rtc.cjs` postinstall download flow
@@ -27,6 +40,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2740

2841
### Changed
2942

43+
- `type-check` script now runs both `type-check:app` and `type-check:e2e` for full TypeScript validation
3044
- Camera streaming migrated from the legacy proxy/RTSP stack to go2rtc-managed per-context streams
3145
- Frontend camera playback now uses the bundled `video-rtc` player instead of the previous streaming path
3246
- Backend build pipeline now bundles `src/index.ts` before pkg packaging, while leaving runtime packages external for compatibility

CLAUDE.md

Lines changed: 64 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -55,14 +55,43 @@ npm run format # Preview Biome formatting changes
5555
npm run format:fix # Apply Biome formatting changes
5656
npm run check # Run Biome check (lint + format combined)
5757
npm run check:fix # Auto-fix Biome check issues
58-
npm run type-check # TypeScript type checking without emit
58+
npm run type-check # TypeScript type checking (app + e2e)
59+
npm run type-check:app # Type check main application only
60+
npm run type-check:e2e # Type check e2e tests only (tsconfig.e2e.json)
5961
npm run docs:check # Validate @fileoverview coverage in source files
6062
npm run docs:check:debug # Debug fileoverview validation output
61-
npm test # Run Jest tests
62-
npm run test:watch # Run tests in watch mode
63-
npm run test:coverage # Run tests with coverage
64-
npm run test:verbose # Run tests with verbose output
6563
npm run clean # Remove dist directory
64+
npm run download:go2rtc # Manually download go2rtc binary
65+
```
66+
67+
### Testing
68+
```bash
69+
# Jest app tests
70+
npm test # Run all Jest tests
71+
npm run test:watch # Jest watch mode
72+
npm run test:coverage # Jest with coverage
73+
npm run test:verbose # Jest verbose output
74+
75+
# Playwright fixture E2E (fast, stub server)
76+
npm run test:e2e:install # Install Chromium for Playwright
77+
npm run test:e2e # Run all fixture E2E specs
78+
npm run test:e2e:smoke # Smoke tests only
79+
npm run test:e2e:auth # Auth tests only
80+
81+
# Playwright emulator E2E (full server + emulator, workers=1)
82+
npm run test:e2e:emulator # Run all emulator E2E specs
83+
npm run test:e2e:emulator:direct # Direct connection spec
84+
npm run test:e2e:emulator:discovery # Discovery spec
85+
npm run test:e2e:emulator:multi # Multi-printer spec
86+
87+
# Combined
88+
npm run test:e2e:all # All Playwright suites (fixture + emulator)
89+
npm run test:all # Everything (Jest + all Playwright)
90+
91+
# Passthrough: append extra args after --
92+
# npm run test:e2e -- --grep "login"
93+
# npm run test:e2e:emulator:direct -- --grep "connect"
94+
# npm test -- --testPathPattern=Config
6695
```
6796

6897
## Runtime Modes
@@ -147,12 +176,20 @@ src/index.ts # Entry point - initializes all singletons, conn
147176
- `DiscordNotificationService` - Discord webhook notifications for print events and periodic status updates
148177
- `SavedPrinterService` - Persistent printer storage in `data/printer_details.json`
149178
- `SpoolmanIntegrationService` / `SpoolmanService` - Spoolman connectivity and synchronization
179+
- `PrinterDiscoveryService` - Network printer discovery protocol
180+
- `ConnectionEstablishmentService` - Connection establishment flow orchestration
181+
- `ConnectionStateManager` - Tracks connection state per context
182+
- `EnvironmentService` - Package detection, path resolution, environment state
183+
- `PrinterPollingService` - Per-context status polling (used by `MultiContextPollingCoordinator`)
184+
- `AutoConnectService` - Auto-reconnect logic for saved printers
185+
- `PrinterDataTransformer` - Normalizes raw printer data to unified status format
186+
- `ThumbnailRequestQueue` - Queued thumbnail fetching for print files
150187

151188
**WebUI**:
152189
- `WebUIManager` - Express HTTP server and static file serving
153190
- `WebSocketManager` - Real-time bidirectional communication with the frontend
154191
- `AuthManager` - Optional password authentication
155-
- API routes organized by feature (printer-control, job, camera, spoolman, theme, discovery, etc.)
192+
- API routes organized by feature (context, printer-control, printer-status, printer-detection, printer-management, job, camera, temperature, filtration, spoolman, theme, discovery)
156193
- Frontend uses GridStack for dashboard layout and the bundled `video-rtc` player for go2rtc-backed camera streaming
157194

158195
### Data Directory
@@ -224,7 +261,8 @@ Default config values live in `src/types/config.ts` and are loaded through `Conf
224261
- TypeScript 5.7
225262
- Biome 2 for linting and formatting
226263
- `esbuild` for backend bundling
227-
- `jest`, `ts-jest`, `supertest` for testing
264+
- `jest`, `ts-jest`, `supertest` for unit/integration testing
265+
- `@playwright/test` for E2E browser testing
228266
- `concurrently` for parallel build tasks
229267
- `nodemon` for dev server reloads
230268
- `tsx` for build scripts
@@ -335,15 +373,32 @@ class Service extends EventEmitter<EventMap> {
335373

336374
## Testing Notes
337375

338-
Core functionality has been tested and verified:
376+
**Jest unit/integration tests** (`src/`):
377+
- ConfigManager, EnvironmentService, DiscordNotificationService, error utilities, WebUIManager integration
378+
379+
**Playwright fixture E2E** (`e2e/`):
380+
- Fast headless tests against the built WebUI served by a stub HTTP+WebSocket server
381+
- Specs: smoke (asset versioning, context switching), auth (login, token persistence, logout)
382+
- Global setup runs `npm run build` before the suite
383+
- Config: `playwright.config.ts` (30s timeout, single worker)
384+
385+
**Playwright emulator E2E** (`e2e-emulator/`):
386+
- Full integration tests using the standalone server and `flashforge-emulator-v2` printer emulator
387+
- Specs: direct connection (all 5 printer models), discovery flow, multi-printer context switching
388+
- Helpers: emulator harness, standalone server harness, lifecycle runner, scenario definitions, page object model
389+
- Config: `playwright.emulator.config.ts` (180s timeout, single worker)
390+
- Requires `flashforge-emulator-v2` cloned at `../flashforge-emulator-v2` (or `FF_EMULATOR_ROOT` env var)
391+
392+
**Verified and tested**:
339393
- Multi-printer context switching
340394
- Spoolman integration
341395
- Platform-specific binary builds (Linux ARM, Linux x64, Windows, macOS)
342396
- WebUI authentication
343397
- Static file serving in packaged binaries
344398
- Discord notification service behavior
399+
- Direct connection, discovery, and multi-printer E2E workflows via Playwright
345400

346-
Areas for continued testing:
401+
**Areas for continued testing**:
347402
- go2rtc binary download and startup across all supported platforms
348403
- Temperature anomaly detection edge cases
349404
- Wrapped build flows and packaging verification

README.md

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ Or if accessing from another device on your network:
145145
http://<server-ip>:3000
146146
```
147147

148-
**Default Login:** The default password is `changeme`. You should change this in `data/config.json` or via the `--webui-password` flag.
148+
**Default Login:** The default password is `changeme`. You should change this in the active config file (by default `data/config.json`, or the directory pointed to by `DATA_DIR`) or via the `--webui-password` flag.
149149

150150
<div align="center">
151151
<h2>Command Line Options</h2>
@@ -168,7 +168,18 @@ http://<server-ip>:3000
168168
<h2>Configuration</h2>
169169
</div>
170170

171-
The application automatically creates a configuration file at `data/config.json` on first run.
171+
The application automatically creates a configuration file in the active data directory on first run. By default this is `data/config.json`.
172+
173+
By default, runtime data is stored in `./data` under the working directory. Set `DATA_DIR` to move configuration, logs, and saved printer state elsewhere.
174+
175+
```bash
176+
# Linux/macOS
177+
DATA_DIR=/path/to/flashforge-data npm start
178+
179+
# PowerShell
180+
$env:DATA_DIR = 'C:\FlashForgeWebUI\data'
181+
npm start
182+
```
172183

173184
<div align="center">
174185

@@ -183,6 +194,53 @@ The application automatically creates a configuration file at `data/config.json`
183194

184195
</div>
185196

197+
<div align="center">
198+
<h2>Testing</h2>
199+
</div>
200+
201+
```bash
202+
# Jest app tests
203+
npm test
204+
205+
# TypeScript checks
206+
npm run type-check
207+
208+
# Playwright browser E2E (fixture/stub server)
209+
npm run test:e2e:install
210+
npm run test:e2e
211+
npm run test:e2e:smoke
212+
npm run test:e2e:auth
213+
214+
# Playwright emulator-backed E2E (headless, single worker)
215+
npm run test:e2e:emulator
216+
npm run test:e2e:emulator:direct
217+
npm run test:e2e:emulator:discovery
218+
npm run test:e2e:emulator:multi
219+
220+
# Combined entry points
221+
npm run test:e2e:all
222+
npm run test:all
223+
```
224+
225+
The fixture Playwright suite in `e2e/` runs against a built standalone WebUI with a local stub server.
226+
227+
The emulator-backed suite in `e2e-emulator/` runs headless Chromium against the real standalone server plus `flashforge-emulator-v2`. Clone the emulator repo next to this one at `../flashforge-emulator-v2`, or point `FF_EMULATOR_ROOT` at it explicitly.
228+
229+
```bash
230+
# Linux/macOS
231+
FF_EMULATOR_ROOT=../flashforge-emulator-v2 npm run test:e2e:emulator
232+
233+
# PowerShell
234+
$env:FF_EMULATOR_ROOT = 'C:\Users\coper\Documents\GitHub\flashforge-emulator-v2'
235+
npm run test:e2e:emulator
236+
```
237+
238+
All Playwright scripts support npm passthrough arguments:
239+
240+
```bash
241+
npm run test:e2e:emulator:direct -- --grep "Adventurer 3"
242+
```
243+
186244
<div align="center">
187245
<h2>Building from Source</h2>
188246
</div>
@@ -207,7 +265,7 @@ npm run build:mac-arm # macOS ARM (Apple Silicon)
207265
| --- | --- |
208266
| **"Cannot GET /" or blank page when accessing WebUI** | If running from source: Make sure you ran `npm run build` before `npm start`<br>If using a pre-1.0.2 binary: Update to version 1.0.2 or later (fixes static file serving bug) |
209267
| **"Permission denied" when running binary** | Run `chmod +x flashforge-webui-linux-*` to make executable |
210-
| **Port already in use** | Change the port in `data/config.json` or use `--webui-port=3001` |
268+
| **Port already in use** | Change the port in the active config file (default `data/config.json`, or the directory pointed to by `DATA_DIR`) or use `--webui-port=3001` |
211269
| **Cannot connect to printer** | Ensure your printer is on the same network as the device running WebUI<br>Check that the printer's IP address is correct<br>For legacy printers, ensure TCP port 8899 is accessible |
212270
| **Selecting the correct binary for your platform** | Windows: `flashforge-webui-win-x64.exe`<br>macOS Intel: `flashforge-webui-macos-x64`<br>macOS Apple Silicon: `flashforge-webui-macos-arm64`<br>Linux x64: `flashforge-webui-linux-x64`<br>Raspberry Pi (64-bit OS): `flashforge-webui-linux-arm64`<br>Raspberry Pi (32-bit OS): `flashforge-webui-linux-armv7`<br>Check your architecture with `uname -m` (x86_64 = x64, aarch64 = ARM64, armv7l = ARMv7) |
213271

e2e-emulator/direct.spec.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/**
2+
* @fileoverview Headless direct-connection emulator lifecycle coverage for the standalone WebUI.
3+
*/
4+
5+
import { test } from '@playwright/test';
6+
import {
7+
AD5X_MULTI_COLOR_MAPPINGS,
8+
AD5X_SCENARIO,
9+
ALL_LIFECYCLE_SCENARIOS,
10+
createForceLegacySeededPrinter,
11+
FORCE_LEGACY_DIRECT_SCENARIOS,
12+
} from './helpers/scenarios';
13+
import { runSingleModelFlow } from './helpers/lifecycle-runner';
14+
15+
test.describe('standalone emulator direct flows', () => {
16+
for (const scenario of ALL_LIFECYCLE_SCENARIOS) {
17+
test(`direct ${scenario.label}: connect + lifecycle + controls`, async ({ page }) => {
18+
test.slow();
19+
await runSingleModelFlow({
20+
page,
21+
scenario,
22+
connectionMode: 'direct',
23+
});
24+
});
25+
}
26+
27+
test('direct AD5X: multi-color start + lifecycle + controls', async ({ page }) => {
28+
test.slow();
29+
await runSingleModelFlow({
30+
page,
31+
scenario: AD5X_SCENARIO,
32+
connectionMode: 'direct',
33+
fileName: 'e2e-ad5x-direct-multicolor.3mf',
34+
seedFile: {
35+
gcodeToolCnt: AD5X_MULTI_COLOR_MAPPINGS.length,
36+
useMatlStation: true,
37+
materialMappings: AD5X_MULTI_COLOR_MAPPINGS,
38+
},
39+
expectMaterialMatching: true,
40+
materialSlotAssignments: AD5X_MULTI_COLOR_MAPPINGS.map((mapping) => ({
41+
toolId: mapping.toolId,
42+
slotId: mapping.slotId,
43+
})),
44+
});
45+
});
46+
47+
for (const scenario of FORCE_LEGACY_DIRECT_SCENARIOS) {
48+
test(`direct ${scenario.label}: reconnect saved + lifecycle + controls`, async ({ page }) => {
49+
test.slow();
50+
await runSingleModelFlow({
51+
page,
52+
scenario,
53+
connectionMode: 'saved',
54+
seededPrinters: [
55+
createForceLegacySeededPrinter({
56+
scenario,
57+
ipAddress: '127.0.0.1',
58+
commandPort: 8899,
59+
httpPort: 8898,
60+
}),
61+
],
62+
});
63+
});
64+
}
65+
});

e2e-emulator/discovery.spec.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/**
2+
* @fileoverview Headless discovery-based emulator lifecycle coverage for the standalone WebUI.
3+
*/
4+
5+
import { test } from '@playwright/test';
6+
import {
7+
AD5X_MULTI_COLOR_MAPPINGS,
8+
AD5X_SCENARIO,
9+
ALL_LIFECYCLE_SCENARIOS,
10+
} from './helpers/scenarios';
11+
import { runSingleModelFlow } from './helpers/lifecycle-runner';
12+
13+
test.describe('standalone emulator discovery flows', () => {
14+
for (const scenario of ALL_LIFECYCLE_SCENARIOS) {
15+
test(`discovery ${scenario.label}: connect + lifecycle + controls`, async ({ page }) => {
16+
test.slow();
17+
await runSingleModelFlow({
18+
page,
19+
scenario,
20+
connectionMode: 'discovery',
21+
});
22+
});
23+
}
24+
25+
test('discovery AD5X: multi-color start + lifecycle + controls', async ({ page }) => {
26+
test.slow();
27+
await runSingleModelFlow({
28+
page,
29+
scenario: AD5X_SCENARIO,
30+
connectionMode: 'discovery',
31+
fileName: 'e2e-ad5x-discovery-multicolor.3mf',
32+
seedFile: {
33+
gcodeToolCnt: AD5X_MULTI_COLOR_MAPPINGS.length,
34+
useMatlStation: true,
35+
materialMappings: AD5X_MULTI_COLOR_MAPPINGS,
36+
},
37+
expectMaterialMatching: true,
38+
materialSlotAssignments: AD5X_MULTI_COLOR_MAPPINGS.map((mapping) => ({
39+
toolId: mapping.toolId,
40+
slotId: mapping.slotId,
41+
})),
42+
});
43+
});
44+
});

0 commit comments

Comments
 (0)