mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
fix(e2e): stop mock servers from outliving Playwright runs (#1710)
* fix(e2e): stop mock servers from outliving Playwright runs Playwright stops a webServer with a SIGKILL to the process group it spawned, but `nx run-commands` starts its command in a detached process group of its own. Launching the Xtream/Stalker mocks through `pnpm nx run *-mock-server:serve` therefore left the tsx server running (reparented to PID 1) and holding its port after every run, so the next run failed with "…/health is already used" or silently reused a stale server. Every Playwright config now starts the mocks as a single `node --import tsx apps/<mock>/src/main.ts` process with TSX_TSCONFIG_PATH=tsconfig.base.json, which stays in Playwright's group. A project-config spec guards all playwright*.config.ts files against regressing to the Nx launch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(e2e): read sidebar categories atomically; tighten mock launch guard - category-management: readVisibleSidebarCategoryNames read items one by one; when Save removed an item between isVisible() and textContent(), textContent() auto-waited for the gone label through the whole 15 s poll, so expect.poll never retried (ubuntu shard 1 failed 3/3 while the UI already showed "No categories available"). Take one snapshot with filter({ visible: true }).evaluateAll() instead. - project-config.spec: pin which Playwright configs start which mock, reject any Nx form that mentions a mock server, and fail when a new config starts a mock without being listed (the old count check passed vacuously on zero matches). - docs: state which configs start which mock instead of "every config starts both". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * revert(e2e): leave the sidebar category read race to #1728 #1728 fixes the same readVisibleSidebarCategoryNames race with a shared helper; keeping a second copy here would only conflict. This PR stays about mock-server lifecycle. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: 4gray <fourgray@proton.me> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
7 files changed
+139
-15
No files matched your search
@@ -46,15 +46,27 @@ export default defineConfig({
|
||||
/* Video on failure */
|
||||
video: 'on-first-retry',
|
||||
},
|
||||
/* The mocks run as one node process, not through `nx run …:serve`: Nx
|
||||
* starts its command in a detached process group, so Playwright's
|
||||
* process-group kill missed it and the server kept its port after the run.
|
||||
* See docs/architecture/xtream-mock-server.md#playwright-integration. */
|
||||
webServer: [
|
||||
{
|
||||
command: 'pnpm nx run stalker-mock-server:serve',
|
||||
command: 'node --import tsx apps/stalker-mock-server/src/main.ts',
|
||||
env: {
|
||||
NODE_ENV: 'development',
|
||||
TSX_TSCONFIG_PATH: 'tsconfig.base.json',
|
||||
},
|
||||
url: `http://localhost:${process.env['MOCK_PORT'] ?? '3210'}/health`,
|
||||
reuseExistingServer: !process.env['CI'],
|
||||
cwd: workspaceRoot,
|
||||
},
|
||||
{
|
||||
command: 'pnpm nx run xtream-mock-server:serve',
|
||||
command: 'node --import tsx apps/xtream-mock-server/src/main.ts',
|
||||
env: {
|
||||
NODE_ENV: 'development',
|
||||
TSX_TSCONFIG_PATH: 'tsconfig.base.json',
|
||||
},
|
||||
url: `http://localhost:${process.env['XTREAM_MOCK_PORT'] ?? '3211'}/health`,
|
||||
reuseExistingServer: !process.env['CI'],
|
||||
cwd: workspaceRoot,
|
||||
|
||||
@@ -6,9 +6,11 @@ import { defineConfig } from '@playwright/test';
|
||||
* worker, no retries: every journey spawns its own Electron processes and
|
||||
* writes one summary per run. The Xtream mock serves both the M3U playlist
|
||||
* and the portal on a dedicated loopback port so a normal E2E server on
|
||||
* 3211 cannot be reused by accident. Locally a server left behind by an
|
||||
* earlier run on that port is reused (its fixtures are deterministic); CI
|
||||
* always starts its own.
|
||||
* 3211 cannot be reused by accident. The mock runs as one node process
|
||||
* rather than through `nx run …:serve`, whose detached process group outlived
|
||||
* the run and kept the port. Locally a server already on the port (e.g. one
|
||||
* started by hand) is reused, since its fixtures are deterministic; CI always
|
||||
* starts its own.
|
||||
*/
|
||||
const xtreamMockPort =
|
||||
process.env['IPTVNATOR_JOURNEY_XTREAM_MOCK_PORT'] ?? '3231';
|
||||
@@ -24,11 +26,13 @@ export default defineConfig({
|
||||
testIdAttribute: 'data-test-id',
|
||||
},
|
||||
webServer: {
|
||||
command: 'pnpm nx run xtream-mock-server:serve',
|
||||
command: 'node --import tsx apps/xtream-mock-server/src/main.ts',
|
||||
cwd: workspaceRoot,
|
||||
env: {
|
||||
HOST: '127.0.0.1',
|
||||
NODE_ENV: 'development',
|
||||
PORT: xtreamMockPort,
|
||||
TSX_TSCONFIG_PATH: 'tsconfig.base.json',
|
||||
},
|
||||
reuseExistingServer: !process.env['CI'],
|
||||
url: `http://127.0.0.1:${xtreamMockPort}/health`,
|
||||
|
||||
@@ -19,13 +19,16 @@ export default defineConfig({
|
||||
testIdAttribute: 'data-test-id',
|
||||
},
|
||||
webServer: {
|
||||
command: 'pnpm nx run xtream-mock-server:serve',
|
||||
// One node process (not `nx run`) so the run's kill reaches it.
|
||||
command: 'node --import tsx apps/xtream-mock-server/src/main.ts',
|
||||
cwd: workspaceRoot,
|
||||
env: {
|
||||
HOST: '127.0.0.1',
|
||||
IPTVNATOR_XTREAM_MOCK_CONTROL: '1',
|
||||
IPTVNATOR_XTREAM_MOCK_CONTROL_TOKEN: controlToken,
|
||||
NODE_ENV: 'development',
|
||||
PORT: '3221',
|
||||
TSX_TSCONFIG_PATH: 'tsconfig.base.json',
|
||||
},
|
||||
reuseExistingServer: false,
|
||||
url: 'http://127.0.0.1:3221/health',
|
||||
|
||||
@@ -66,14 +66,25 @@ export default defineConfig({
|
||||
reuseExistingServer: reuseExistingWebServer,
|
||||
cwd: workspaceRoot,
|
||||
},
|
||||
/* The mocks run as one node process, not through `nx run …:serve`:
|
||||
* Nx starts its command in a detached process group, so Playwright's
|
||||
* process-group kill missed it and the server kept its port. */
|
||||
{
|
||||
command: 'pnpm nx run stalker-mock-server:serve',
|
||||
command: 'node --import tsx apps/stalker-mock-server/src/main.ts',
|
||||
env: {
|
||||
NODE_ENV: 'development',
|
||||
TSX_TSCONFIG_PATH: 'tsconfig.base.json',
|
||||
},
|
||||
url: `http://localhost:${process.env['MOCK_PORT'] ?? '3210'}/health`,
|
||||
reuseExistingServer: !process.env['CI'],
|
||||
cwd: workspaceRoot,
|
||||
},
|
||||
{
|
||||
command: 'pnpm nx run xtream-mock-server:serve',
|
||||
command: 'node --import tsx apps/xtream-mock-server/src/main.ts',
|
||||
env: {
|
||||
NODE_ENV: 'development',
|
||||
TSX_TSCONFIG_PATH: 'tsconfig.base.json',
|
||||
},
|
||||
url: `http://localhost:${process.env['XTREAM_MOCK_PORT'] ?? '3211'}/health`,
|
||||
reuseExistingServer: !process.env['CI'],
|
||||
cwd: workspaceRoot,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { readdirSync, readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
|
||||
describe('Xtream mock Nx serve environment', () => {
|
||||
@@ -26,3 +26,61 @@ describe('Xtream mock Nx serve environment', () => {
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe('Playwright mock-server launch', () => {
|
||||
const appsDirectory = join(process.cwd(), 'apps');
|
||||
const expectedMockLaunches: Record<string, string[]> = {
|
||||
'apps/electron-backend-e2e/playwright.config.ts': ['stalker', 'xtream'],
|
||||
'apps/electron-backend-e2e/playwright.journeys.config.ts': ['xtream'],
|
||||
'apps/electron-backend-e2e/playwright.xtream-performance.config.ts': [
|
||||
'xtream',
|
||||
],
|
||||
'apps/web-e2e/playwright.config.ts': ['stalker', 'xtream'],
|
||||
};
|
||||
const readSource = (configPath: string) =>
|
||||
readFileSync(join(process.cwd(), configPath), 'utf8');
|
||||
|
||||
it('knows every Playwright config that starts a mock server', () => {
|
||||
const configsUsingMocks = readdirSync(appsDirectory, {
|
||||
withFileTypes: true,
|
||||
})
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.flatMap((entry) =>
|
||||
readdirSync(join(appsDirectory, entry.name))
|
||||
.filter((name) =>
|
||||
/^playwright(\..+)?\.config\.ts$/.test(name)
|
||||
)
|
||||
.map((name) => `apps/${entry.name}/${name}`)
|
||||
)
|
||||
.filter((configPath) =>
|
||||
readSource(configPath).includes('-mock-server')
|
||||
);
|
||||
|
||||
expect(configsUsingMocks.sort()).toEqual(
|
||||
Object.keys(expectedMockLaunches).sort()
|
||||
);
|
||||
});
|
||||
|
||||
// Nx starts a run-commands child in its own detached process group, and
|
||||
// Playwright stops a webServer with a process-group SIGKILL, so a mock
|
||||
// launched through Nx outlived the run and kept its port.
|
||||
it.each(Object.entries(expectedMockLaunches))(
|
||||
'starts each mock in %s as a single node process',
|
||||
(configPath, mocks) => {
|
||||
const source = readSource(configPath);
|
||||
|
||||
expect(source).not.toMatch(/\bnx\b[^'"`\n]*mock-server/);
|
||||
for (const mock of mocks) {
|
||||
expect(source).toContain(
|
||||
`'node --import tsx apps/${mock}-mock-server/src/main.ts'`
|
||||
);
|
||||
}
|
||||
expect(
|
||||
source.match(/node --import tsx apps\/[\w-]+-mock-server\//g)
|
||||
).toHaveLength(mocks.length);
|
||||
expect(
|
||||
source.match(/TSX_TSCONFIG_PATH: 'tsconfig\.base\.json'/g)
|
||||
).toHaveLength(mocks.length);
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -364,7 +364,11 @@ form is the one specs should use.
|
||||
webServer: [
|
||||
{ command: webServerCommand /* web:serve */, url: baseURL },
|
||||
{
|
||||
command: 'pnpm nx run stalker-mock-server:serve',
|
||||
command: 'node --import tsx apps/stalker-mock-server/src/main.ts',
|
||||
env: {
|
||||
NODE_ENV: 'development',
|
||||
TSX_TSCONFIG_PATH: 'tsconfig.base.json',
|
||||
},
|
||||
url: `http://localhost:${process.env['MOCK_PORT'] ?? '3210'}/health`,
|
||||
reuseExistingServer: !process.env['CI'],
|
||||
},
|
||||
@@ -374,12 +378,20 @@ webServer: [
|
||||
|
||||
Playwright waits for every server to be healthy before starting tests. If one is already running (e.g. in local dev), it reuses the existing instance.
|
||||
|
||||
The mock runs as a single `node` process instead of `pnpm nx run
|
||||
stalker-mock-server:serve`, because Nx starts its command in a detached
|
||||
process group that Playwright's process-group kill never reached, so the server
|
||||
outlived the run. The
|
||||
[Xtream mock's Playwright section](xtream-mock-server.md#playwright-integration)
|
||||
has the details.
|
||||
|
||||
**`MOCK_PORT` relocates the whole run.** Playwright's health-check URL and the
|
||||
`MOCK_SERVER` constants in the specs read it, and `main.ts` resolves the
|
||||
server's port as `PORT`, then `MOCK_PORT`, then `3210` — so
|
||||
`MOCK_PORT=3310 pnpm exec playwright test …` starts the Nx-managed mock on
|
||||
`MOCK_PORT=3310 pnpm exec playwright test …` starts the mock on
|
||||
3310 and points every spec at it, which is how two worktrees run E2E side by
|
||||
side when one already holds 3210. This only works because the `serve` and
|
||||
side when one already holds 3210. A hand-started Nx mock honours the same
|
||||
knobs because the `serve` and
|
||||
`serve-with-watch` targets no longer pin `PORT` in `project.json`: an `env`
|
||||
entry in `nx:run-commands` overrides the shell (`{...process.env, ...env}`),
|
||||
so a pinned value silently discarded every override. `PORT=3310 pnpm nx run
|
||||
|
||||
@@ -550,13 +550,37 @@ The mock server is listed as a third `webServer` entry:
|
||||
|
||||
```typescript
|
||||
{
|
||||
command: 'pnpm nx run xtream-mock-server:serve',
|
||||
url: 'http://localhost:3211/health',
|
||||
command: 'node --import tsx apps/xtream-mock-server/src/main.ts',
|
||||
env: {
|
||||
NODE_ENV: 'development',
|
||||
TSX_TSCONFIG_PATH: 'tsconfig.base.json',
|
||||
},
|
||||
url: `http://localhost:${process.env['XTREAM_MOCK_PORT'] ?? '3211'}/health`,
|
||||
reuseExistingServer: !process.env['CI'],
|
||||
cwd: workspaceRoot,
|
||||
}
|
||||
```
|
||||
|
||||
Every mock-server `webServer` entry uses this launch form, never
|
||||
`pnpm nx run …:serve`. The web and Electron E2E configs
|
||||
(`apps/web-e2e/playwright.config.ts`,
|
||||
`apps/electron-backend-e2e/playwright.config.ts`) start both the Stalker and
|
||||
the Xtream mock. The journeys and Xtream benchmark configs start only the
|
||||
Xtream mock, on their own loopback ports. The packaged and other performance
|
||||
configs start no mock.
|
||||
Playwright stops a `webServer` by sending `SIGKILL` to the process group it
|
||||
spawned (`taskkill /T /F` on Windows), and Nx `run-commands` starts its command
|
||||
in a detached process group of its own. The kill therefore reached only
|
||||
`pnpm`/`nx`, and the `tsx` server was reparented and kept the port, which made
|
||||
the next run fail with "…/health is already used" or, with
|
||||
`reuseExistingServer`, silently reuse a stale server. `node --import tsx` keeps
|
||||
the server a single process in Playwright's group. `TSX_TSCONFIG_PATH` stands in
|
||||
for the serve target's `--tsconfig` flag and is required for the `@iptvnator/*`
|
||||
path aliases. `project-config.spec.ts` pins which configs start which mock,
|
||||
and fails if any of them launches a mock through Nx or a new config starts one
|
||||
without being listed there. The `serve` targets remain the entry point
|
||||
for starting a mock by hand.
|
||||
|
||||
### Request Interception
|
||||
|
||||
The Angular PWA calls `localhost:3000/xtream?...`. Playwright intercepts these:
|
||||
|
||||
Reference in new issue
Block a user