fix(e2e): stop the web backend from outliving Playwright runs (#1747)

This commit is contained in:
4gray authored and GitHub committed 2026-09-29 19:34:15 +02:00
1 parent d60c80746b
commit 4e17fbed4e
6 files changed
+107 -11

No files matched your search

@@ -0,0 +1,63 @@
import { readdirSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
describe('Playwright web-backend launch', () => {
const appsDirectory = join(process.cwd(), 'apps');
const launchCommand = 'node --import tsx apps/web-backend/src/main.ts';
const configsStartingBackend = ['apps/web-e2e/playwright.config.ts'];
const readSource = (path: string) =>
readFileSync(join(process.cwd(), path), 'utf8');
const serveEnv = (
JSON.parse(readSource('apps/web-backend/project.json')) as {
targets: { serve: { options: { env: Record<string, string> } } };
}
).targets.serve.options.env;
it('knows every Playwright config that starts the web backend', () => {
const configs = 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('web-backend')
);
expect(configs.sort()).toEqual(configsStartingBackend);
});
// Nx starts a run-commands child in its own detached process group, and
// Playwright stops a webServer with a process-group SIGKILL, so a backend
// launched through Nx outlived the run and kept port 3333.
it.each(configsStartingBackend)(
'starts the backend in %s as a single node process',
(configPath) => {
const source = readSource(configPath);
const start = source.indexOf(`'${launchCommand}'`);
expect(source).not.toMatch(/\bnx\b[^'"`\n]*web-backend/);
expect(start).toBeGreaterThan(-1);
expect(
source.indexOf(launchCommand, start + launchCommand.length)
).toBe(-1);
// WEB_BACKEND_PORT relocates the run; its default is the target's.
const { PORT: port, ...fixedEnv } = serveEnv;
const entry = source.slice(start, source.indexOf('url:', start));
for (const [name, value] of Object.entries(fixedEnv)) {
expect(entry).toContain(`${name}: '${value}'`);
}
expect(source).toContain(
`process.env['WEB_BACKEND_PORT'] ?? '${port}'`
);
expect(entry).toContain('PORT: webBackendPort,');
expect(source).toContain(
'url: `http://localhost:${webBackendPort}/health`'
);
}
);
});
+14 -3
View File
@@ -15,6 +15,7 @@ const webServerCommand =
? `pnpm nx run web:serve-static --port=${staticPwaPort}`
: 'pnpm nx run web:serve';
const reuseExistingWebServer = isStaticPwaE2E ? false : !process.env['CI'];
const webBackendPort = process.env['WEB_BACKEND_PORT'] ?? '3333';
/**
* Read environment variables from file.
@@ -57,7 +58,8 @@ export default defineConfig({
* both mock servers honour them as a fallback for PORT (their serve
* targets no longer pin PORT, so an explicit shell value reaches the
* process). That is what lets two worktrees run E2E side by side when one
* already holds 3210/3211.
* already holds 3210/3211. WEB_BACKEND_PORT does the same for the web
* backend and self-hosted.e2e.ts, which is the only spec that calls it.
*/
webServer: [
{
@@ -89,9 +91,18 @@ export default defineConfig({
reuseExistingServer: !process.env['CI'],
cwd: workspaceRoot,
},
/* Same single-process launch for the backend; `env` mirrors the
* `web-backend:serve` target, which stays the manual entry point. */
{
command: 'pnpm nx run web-backend:serve',
url: 'http://localhost:3333/health',
command: 'node --import tsx apps/web-backend/src/main.ts',
env: {
PORT: webBackendPort,
CLIENT_URL: 'http://localhost:4200',
BACKEND_URL: '/api',
IPTVNATOR_PROXY_ALLOW_PRIVATE_NETWORKS: '1',
TSX_TSCONFIG_PATH: 'tsconfig.base.json',
},
url: `http://localhost:${webBackendPort}/health`,
reuseExistingServer: !process.env['CI'],
cwd: workspaceRoot,
},
+2 -1
View File
@@ -4,7 +4,8 @@ import { openSourceEditor, sourceRowByTitle } from './sources-pwa.helpers';
import { postWithRetry, setInputValue } from './e2e-helpers';
import { expect, test } from './fixtures';
const WEB_BACKEND_URL = 'http://localhost:3333';
const WEB_BACKEND_PORT = process.env['WEB_BACKEND_PORT'] ?? '3333';
const WEB_BACKEND_URL = `http://localhost:${WEB_BACKEND_PORT}`;
const XTREAM_MOCK_PORT = process.env['XTREAM_MOCK_PORT'] ?? '3211';
const STALKER_MOCK_PORT = process.env['MOCK_PORT'] ?? '3210';
const XTREAM_MOCK_SERVER = `http://localhost:${XTREAM_MOCK_PORT}`;
@@ -70,17 +70,20 @@ describe('Playwright mock-server launch', () => {
const source = readSource(configPath);
expect(source).not.toMatch(/\bnx\b[^'"`\n]*mock-server/);
// Other single-process webServers (web-e2e's web-backend) also set
// TSX_TSCONFIG_PATH, so check it inside each mock's own entry.
for (const mock of mocks) {
expect(source).toContain(
`'node --import tsx apps/${mock}-mock-server/src/main.ts'`
);
const launch = `'node --import tsx apps/${mock}-mock-server/src/main.ts'`;
const start = source.indexOf(launch);
expect(start).toBeGreaterThan(-1);
expect(
source.slice(start, source.indexOf('url:', start))
).toContain("TSX_TSCONFIG_PATH: 'tsconfig.base.json'");
}
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);
}
);
});
+17
View File
@@ -73,6 +73,23 @@ The current self-hosted PWA uses these `apps/web-backend` routes:
- `GET /xtream?targetId=<id>&username=<u>&password=<p>&action=<action>`
- `GET /stalker?targetId=<id>&macAddress=<mac>&action=<action>`
`pnpm nx run web-backend:serve` starts the backend by hand on port 3333 with
`TSX_TSCONFIG_PATH=tsconfig.base.json`, which `tsx` needs for the
`@iptvnator/*` path aliases. `apps/web-e2e/playwright.config.ts` does not use
that target: it launches `node --import tsx apps/web-backend/src/main.ts` with
an `env` that mirrors the target's. Nx `run-commands` starts its command in a
detached process group, and Playwright stops a `webServer` with a
process-group `SIGKILL` (`taskkill /T /F` on Windows). The kill therefore
reached only `pnpm`/`nx`, and the orphaned server kept port 3333 after the run.
`apps/web-backend/src/app/project-config.spec.ts` fails if a Playwright config
launches the backend through Nx or its `env` drifts from the `serve` target.
`WEB_BACKEND_PORT` (default 3333) relocates the E2E backend and the
`self-hosted.e2e.ts` URLs, like `MOCK_PORT` does for the mocks. Use it when
another worktree holds 3333. The `web:serve` entry can stay on Nx:
`@angular/build:dev-server` runs inside the Nx process, so the group kill
stops it. The mock servers follow the same
rule; see [Xtream mock Playwright integration](xtream-mock-server.md#playwright-integration).
The PWA continues to use `PwaService`; only the backend base URL is resolved at
runtime. Electron routes remain owned by the Electron backend and preload
bridge.
+2 -1
View File
@@ -579,7 +579,8 @@ 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.
for starting a mock by hand. The web-e2e `web-backend` entry uses the same
launch form; see [PWA web backend](pwa-self-hosted.md#web-backend).
### Request Interception