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-static --port=${staticPwaPort}`
: 'pnpm nx run web:serve'; : 'pnpm nx run web:serve';
const reuseExistingWebServer = isStaticPwaE2E ? false : !process.env['CI']; const reuseExistingWebServer = isStaticPwaE2E ? false : !process.env['CI'];
const webBackendPort = process.env['WEB_BACKEND_PORT'] ?? '3333';
/** /**
* Read environment variables from file. * Read environment variables from file.
@@ -57,7 +58,8 @@ export default defineConfig({
* both mock servers honour them as a fallback for PORT (their serve * both mock servers honour them as a fallback for PORT (their serve
* targets no longer pin PORT, so an explicit shell value reaches the * 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 * 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: [ webServer: [
{ {
@@ -89,9 +91,18 @@ export default defineConfig({
reuseExistingServer: !process.env['CI'], reuseExistingServer: !process.env['CI'],
cwd: workspaceRoot, 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', command: 'node --import tsx apps/web-backend/src/main.ts',
url: 'http://localhost:3333/health', 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'], reuseExistingServer: !process.env['CI'],
cwd: workspaceRoot, cwd: workspaceRoot,
}, },
+2 -1
View File
@@ -4,7 +4,8 @@ import { openSourceEditor, sourceRowByTitle } from './sources-pwa.helpers';
import { postWithRetry, setInputValue } from './e2e-helpers'; import { postWithRetry, setInputValue } from './e2e-helpers';
import { expect, test } from './fixtures'; 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 XTREAM_MOCK_PORT = process.env['XTREAM_MOCK_PORT'] ?? '3211';
const STALKER_MOCK_PORT = process.env['MOCK_PORT'] ?? '3210'; const STALKER_MOCK_PORT = process.env['MOCK_PORT'] ?? '3210';
const XTREAM_MOCK_SERVER = `http://localhost:${XTREAM_MOCK_PORT}`; const XTREAM_MOCK_SERVER = `http://localhost:${XTREAM_MOCK_PORT}`;
@@ -70,17 +70,20 @@ describe('Playwright mock-server launch', () => {
const source = readSource(configPath); const source = readSource(configPath);
expect(source).not.toMatch(/\bnx\b[^'"`\n]*mock-server/); 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) { for (const mock of mocks) {
expect(source).toContain( const launch = `'node --import tsx apps/${mock}-mock-server/src/main.ts'`;
`'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( expect(
source.match(/node --import tsx apps\/[\w-]+-mock-server\//g) source.match(/node --import tsx apps\/[\w-]+-mock-server\//g)
).toHaveLength(mocks.length); ).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 /xtream?targetId=<id>&username=<u>&password=<p>&action=<action>`
- `GET /stalker?targetId=<id>&macAddress=<mac>&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 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 runtime. Electron routes remain owned by the Electron backend and preload
bridge. 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, 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 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 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 ### Request Interception