mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 09:01:03 -08:00
fix(e2e): let web-e2e:e2e run outside CI (#1779)
This commit is contained in:
1 parent
6e229f85b6
commit
8b6fcf3560
9 files changed
+317
-4
No files matched your search
@@ -375,6 +375,12 @@ jobs:
|
|||||||
- name: Validate stylesheet Nx inputs
|
- name: Validate stylesheet Nx inputs
|
||||||
run: pnpm run styles:inputs:validate
|
run: pnpm run styles:inputs:validate
|
||||||
|
|
||||||
|
# Nx rejects a non-parallel task with continuous dependencies, and
|
||||||
|
# the Playwright plugin infers serve dependencies only when CI is
|
||||||
|
# unset, so this checks both the local and the CI inference.
|
||||||
|
- name: Validate Playwright task graphs
|
||||||
|
run: pnpm run e2e:task-graphs:validate
|
||||||
|
|
||||||
- name: Typecheck web and Electron entry points
|
- name: Typecheck web and Electron entry points
|
||||||
run: pnpm run typecheck:ci
|
run: pnpm run typecheck:ci
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,9 @@
|
|||||||
],
|
],
|
||||||
"// targets": "to see all targets run: nx show project web-e2e --web",
|
"// targets": "to see all targets run: nx show project web-e2e --web",
|
||||||
"targets": {
|
"targets": {
|
||||||
"e2e": {},
|
"e2e": {
|
||||||
|
"dependsOn": []
|
||||||
|
},
|
||||||
"lint": {
|
"lint": {
|
||||||
"executor": "@nx/eslint:lint"
|
"executor": "@nx/eslint:lint"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,8 +87,10 @@ launches the backend through Nx or its `env` drifts from the `serve` target.
|
|||||||
`self-hosted.e2e.ts` URLs, like `MOCK_PORT` does for the mocks. Use it when
|
`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:
|
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
|
`@angular/build:dev-server` runs inside the Nx process, so the group kill
|
||||||
stops it. The mock servers follow the same
|
stops it. Playwright launches that command itself even outside CI: the
|
||||||
rule; see [Xtream mock Playwright integration](xtream-mock-server.md#playwright-integration).
|
web-e2e targets carry no Nx `serve` dependency, because Nx refuses a
|
||||||
|
non-parallel task with a continuous dependency. 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
|
||||||
|
|||||||
@@ -197,6 +197,13 @@ and JSON summary output. CI uploads the merged Tier A report to Codecov with the
|
|||||||
Use atomized E2E targets when available, for example
|
Use atomized E2E targets when available, for example
|
||||||
`pnpm nx run web-e2e:e2e-ci--src/xtream.e2e.ts`.
|
`pnpm nx run web-e2e:e2e-ci--src/xtream.e2e.ts`.
|
||||||
|
|
||||||
|
After changing a Playwright config's `webServer` list, `nx.json` target
|
||||||
|
defaults or an E2E project's `dependsOn`, run
|
||||||
|
`pnpm run e2e:task-graphs:validate`. It builds every Playwright target's task
|
||||||
|
graph with and without `CI` and fails on the graphs Nx refuses to run; CI runs
|
||||||
|
it in the `unit-and-typecheck` job. See
|
||||||
|
[Xtream mock Playwright integration](xtream-mock-server.md#playwright-integration).
|
||||||
|
|
||||||
Inside `expect.poll`, read a changing list in one DOM snapshot
|
Inside `expect.poll`, read a changing list in one DOM snapshot
|
||||||
(`allTextContents()` or `evaluateAll()`, as in
|
(`allTextContents()` or `evaluateAll()`, as in
|
||||||
`apps/electron-backend-e2e/src/sidebar-categories.e2e-support.ts`), not by
|
`apps/electron-backend-e2e/src/sidebar-categories.e2e-support.ts`), not by
|
||||||
|
|||||||
@@ -586,6 +586,27 @@ continuous `serve` task. The same spec guards the `e2e*` entries of `nx.json`
|
|||||||
`targetDefaults` and every target in the `apps/*-e2e` `project.json` files. The web-e2e `web-backend` entry uses the same
|
`targetDefaults` and every target in the `apps/*-e2e` `project.json` files. The web-e2e `web-backend` entry uses the same
|
||||||
launch form; see [PWA web backend](pwa-self-hosted.md#web-backend).
|
launch form; see [PWA web backend](pwa-self-hosted.md#web-backend).
|
||||||
|
|
||||||
|
Playwright, not Nx, starts every E2E server, including the web dev server.
|
||||||
|
`@nx/playwright/plugin` infers a continuous `serve` dependency from a
|
||||||
|
`pnpm nx run <project>:serve` webServer only while `reuseExistingServer` is
|
||||||
|
true, which the configs set only when `CI` is unset. It also sets
|
||||||
|
`parallelism: false` on a target when a webServer has an `env` or a plain
|
||||||
|
`node` command, because no Nx task covers that server. Nx refuses to run a
|
||||||
|
non-parallel task that depends on a continuous task, so the inferred
|
||||||
|
`web-e2e:e2e` failed locally with "do not support parallelism but depend on
|
||||||
|
continuous tasks" and passed only in CI. Therefore:
|
||||||
|
|
||||||
|
- `apps/web-e2e/project.json` sets `e2e.dependsOn` to `[]`, and the filtered
|
||||||
|
`e2e-ci--src/*.e2e.ts` target default in `nx.json` does the same for the
|
||||||
|
per-file web targets. The per-file Electron targets depend only on
|
||||||
|
`electron-backend:build-e2e`.
|
||||||
|
- The plugin runs with `waitForWebServer: false`, because no target consumes
|
||||||
|
its `e2e--wait-for-webserver` readiness task. Playwright's own URL probe
|
||||||
|
covers readiness.
|
||||||
|
- `pnpm run e2e:task-graphs:validate` (`tools/nx/check-e2e-task-graphs.mjs`)
|
||||||
|
builds each Playwright target's task graph with Nx's own validation, once
|
||||||
|
with `CI` unset and once with it set.
|
||||||
|
|
||||||
### Request Interception
|
### Request Interception
|
||||||
|
|
||||||
The Angular PWA calls `localhost:3000/xtream?...`. Playwright intercepts these:
|
The Angular PWA calls `localhost:3000/xtream?...`. Playwright intercepts these:
|
||||||
|
|||||||
@@ -88,7 +88,8 @@
|
|||||||
{
|
{
|
||||||
"plugin": "@nx/playwright/plugin",
|
"plugin": "@nx/playwright/plugin",
|
||||||
"options": {
|
"options": {
|
||||||
"targetName": "e2e"
|
"targetName": "e2e",
|
||||||
|
"waitForWebServer": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -44,6 +44,9 @@
|
|||||||
"styles:inputs:test": "node --test tools/nx/check-stylesheet-inputs.test.mjs",
|
"styles:inputs:test": "node --test tools/nx/check-stylesheet-inputs.test.mjs",
|
||||||
"styles:inputs:check": "node tools/nx/check-stylesheet-inputs.mjs",
|
"styles:inputs:check": "node tools/nx/check-stylesheet-inputs.mjs",
|
||||||
"styles:inputs:validate": "pnpm run styles:inputs:test && pnpm run styles:inputs:check",
|
"styles:inputs:validate": "pnpm run styles:inputs:test && pnpm run styles:inputs:check",
|
||||||
|
"e2e:task-graphs:test": "node --test tools/nx/check-e2e-task-graphs.test.mjs",
|
||||||
|
"e2e:task-graphs:check": "node tools/nx/check-e2e-task-graphs.mjs",
|
||||||
|
"e2e:task-graphs:validate": "pnpm run e2e:task-graphs:test && pnpm run e2e:task-graphs:check",
|
||||||
"coverage:tools:test": "node --test tools/coverage/coverage-integrity.test.mjs tools/coverage/e2e-shard-reports.test.mjs tools/coverage/coverage-run-pool.test.mjs tools/coverage/unit-coverage-scope.test.mjs",
|
"coverage:tools:test": "node --test tools/coverage/coverage-integrity.test.mjs tools/coverage/e2e-shard-reports.test.mjs tools/coverage/coverage-run-pool.test.mjs tools/coverage/unit-coverage-scope.test.mjs",
|
||||||
"coverage:unit:ci": "node tools/coverage/run-tier-a-coverage.mjs",
|
"coverage:unit:ci": "node tools/coverage/run-tier-a-coverage.mjs",
|
||||||
"coverage:merge": "node tools/coverage/merge-coverage.mjs",
|
"coverage:merge": "node tools/coverage/merge-coverage.mjs",
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
import { execFileSync } from 'node:child_process';
|
||||||
|
import { readdirSync } from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nx refuses to run a task with `parallelism: false` that depends on a
|
||||||
|
* continuous task. `@nx/playwright/plugin` sets `parallelism: false` whenever
|
||||||
|
* a config has a webServer no inferred Nx task covers (our mocks and the web
|
||||||
|
* backend launch as plain `node` processes), and it infers continuous serve
|
||||||
|
* dependencies only from servers with `reuseExistingServer`, which the
|
||||||
|
* configs enable only when `CI` is unset. A target graph can therefore pass
|
||||||
|
* in CI and still fail locally, so every Playwright target is validated with
|
||||||
|
* Nx's own task-graph checks under both environments.
|
||||||
|
*/
|
||||||
|
const RESULT_PREFIX = 'e2e-task-graphs-result:';
|
||||||
|
|
||||||
|
export const MODES = {
|
||||||
|
local: { CI: undefined },
|
||||||
|
ci: { CI: 'true' },
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Playwright projects whose targets must be inferred. The check only sees
|
||||||
|
* targets the plugin tags as Playwright ones, so a target that drops out of
|
||||||
|
* the inference would otherwise pass unchecked.
|
||||||
|
*/
|
||||||
|
export const E2E_PROJECTS = {
|
||||||
|
'web-e2e': 'apps/web-e2e',
|
||||||
|
'electron-backend-e2e': 'apps/electron-backend-e2e',
|
||||||
|
};
|
||||||
|
|
||||||
|
/** `e2e` plus one atomized target per spec, as the plugin names them. */
|
||||||
|
export function expectedPlaywrightTargets(specFilesByProject) {
|
||||||
|
return Object.entries(specFilesByProject).flatMap(([project, specs]) => [
|
||||||
|
`${project}:e2e`,
|
||||||
|
...specs.map((spec) => `${project}:e2e-ci--${spec}`),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findMissingTargets(projectGraph, expectedTargets) {
|
||||||
|
const inferred = new Set(
|
||||||
|
playwrightTargets(projectGraph).map(
|
||||||
|
({ project, target }) => `${project}:${target}`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return expectedTargets.filter((task) => !inferred.has(task));
|
||||||
|
}
|
||||||
|
|
||||||
|
function readSpecFiles(workspaceRoot) {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(E2E_PROJECTS).map(([project, root]) => [
|
||||||
|
project,
|
||||||
|
readdirSync(path.join(workspaceRoot, root, 'src'), {
|
||||||
|
recursive: true,
|
||||||
|
})
|
||||||
|
.filter((file) => file.endsWith('.e2e.ts'))
|
||||||
|
.map((file) => `src/${file.split(path.sep).join('/')}`)
|
||||||
|
.sort(),
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function playwrightTargets(projectGraph) {
|
||||||
|
return Object.values(projectGraph.nodes)
|
||||||
|
.flatMap((node) =>
|
||||||
|
Object.entries(node.data.targets ?? {})
|
||||||
|
.filter(([, target]) =>
|
||||||
|
target.metadata?.technologies?.includes('playwright')
|
||||||
|
)
|
||||||
|
.map(([target]) => ({ project: node.name, target }))
|
||||||
|
)
|
||||||
|
.sort((a, b) =>
|
||||||
|
`${a.project}:${a.target}`.localeCompare(`${b.project}:${b.target}`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function findInvalidTaskGraphs(projectGraph) {
|
||||||
|
const { createTaskGraph } =
|
||||||
|
await import('nx/src/tasks-runner/create-task-graph.js');
|
||||||
|
const { assertTaskGraphDoesNotContainInvalidTargets } =
|
||||||
|
await import('nx/src/tasks-runner/task-graph-utils.js');
|
||||||
|
|
||||||
|
return playwrightTargets(projectGraph).flatMap(({ project, target }) => {
|
||||||
|
try {
|
||||||
|
assertTaskGraphDoesNotContainInvalidTargets(
|
||||||
|
createTaskGraph(
|
||||||
|
projectGraph,
|
||||||
|
{},
|
||||||
|
[project],
|
||||||
|
[target],
|
||||||
|
undefined,
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return [];
|
||||||
|
} catch (error) {
|
||||||
|
return [{ task: `${project}:${target}`, message: error.message }];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function childEnv(mode) {
|
||||||
|
const env = { ...process.env, NX_DAEMON: 'false' };
|
||||||
|
for (const [name, value] of Object.entries(MODES[mode])) {
|
||||||
|
if (value === undefined) delete env[name];
|
||||||
|
else env[name] = value;
|
||||||
|
}
|
||||||
|
return env;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkCurrentEnvironment() {
|
||||||
|
const { createProjectGraphAsync, workspaceRoot } =
|
||||||
|
await import('@nx/devkit');
|
||||||
|
const projectGraph = await createProjectGraphAsync({ exitOnError: true });
|
||||||
|
const failures = await findInvalidTaskGraphs(projectGraph);
|
||||||
|
const checked = playwrightTargets(projectGraph).length;
|
||||||
|
const missing = findMissingTargets(
|
||||||
|
projectGraph,
|
||||||
|
expectedPlaywrightTargets(readSpecFiles(workspaceRoot))
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
`${RESULT_PREFIX}${JSON.stringify({ checked, failures, missing })}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
let failed = false;
|
||||||
|
for (const mode of Object.keys(MODES)) {
|
||||||
|
const output = execFileSync(
|
||||||
|
process.execPath,
|
||||||
|
[fileURLToPath(import.meta.url), '--current-environment'],
|
||||||
|
{
|
||||||
|
env: childEnv(mode),
|
||||||
|
encoding: 'utf8',
|
||||||
|
stdio: ['ignore', 'pipe', 'inherit'],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const result = output
|
||||||
|
.split('\n')
|
||||||
|
.find((line) => line.startsWith(RESULT_PREFIX));
|
||||||
|
const { checked, failures, missing } = JSON.parse(
|
||||||
|
result.slice(RESULT_PREFIX.length)
|
||||||
|
);
|
||||||
|
for (const task of missing) {
|
||||||
|
failed = true;
|
||||||
|
console.error(
|
||||||
|
`[${mode}] ${task} was not inferred as a Playwright target`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (const { task, message } of failures) {
|
||||||
|
failed = true;
|
||||||
|
console.error(
|
||||||
|
`[${mode}] ${task}\n ${message.replace(/\n/g, '\n ')}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (failures.length === 0 && missing.length === 0) {
|
||||||
|
console.log(
|
||||||
|
`[${mode}] ${checked} Playwright task graphs are valid`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
process.exitCode = failed ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
||||||
|
if (process.argv.includes('--current-environment')) {
|
||||||
|
await checkCurrentEnvironment();
|
||||||
|
} else {
|
||||||
|
main();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { test } from 'node:test';
|
||||||
|
|
||||||
|
import {
|
||||||
|
expectedPlaywrightTargets,
|
||||||
|
findInvalidTaskGraphs,
|
||||||
|
findMissingTargets,
|
||||||
|
MODES,
|
||||||
|
playwrightTargets,
|
||||||
|
} from './check-e2e-task-graphs.mjs';
|
||||||
|
|
||||||
|
const playwright = { technologies: ['playwright'] };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mirrors web-e2e as the Playwright plugin infers it without CI: an uncovered
|
||||||
|
* mock webServer makes `e2e` non-parallel while `pnpm nx run web:serve` adds a
|
||||||
|
* continuous dependency on the dev server.
|
||||||
|
*/
|
||||||
|
function graphWithE2eTarget(e2eTarget) {
|
||||||
|
const node = (name, targets) => ({
|
||||||
|
name,
|
||||||
|
type: 'app',
|
||||||
|
data: { root: `apps/${name}`, targets },
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
nodes: {
|
||||||
|
web: node('web', {
|
||||||
|
serve: {
|
||||||
|
executor: '@angular/build:dev-server',
|
||||||
|
continuous: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
'web-e2e': node('web-e2e', {
|
||||||
|
e2e: {
|
||||||
|
executor: 'nx:run-commands',
|
||||||
|
options: { command: 'playwright test' },
|
||||||
|
metadata: playwright,
|
||||||
|
...e2eTarget,
|
||||||
|
},
|
||||||
|
lint: {
|
||||||
|
executor: '@nx/eslint:lint',
|
||||||
|
parallelism: false,
|
||||||
|
dependsOn: [{ projects: ['web'], target: 'serve' }],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
dependencies: { web: [], 'web-e2e': [] },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test('checks only Playwright-inferred targets', () => {
|
||||||
|
const graph = graphWithE2eTarget({ parallelism: false });
|
||||||
|
|
||||||
|
assert.deepEqual(playwrightTargets(graph), [
|
||||||
|
{ project: 'web-e2e', target: 'e2e' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('reports a non-parallel e2e target that depends on a continuous serve', async () => {
|
||||||
|
const graph = graphWithE2eTarget({
|
||||||
|
parallelism: false,
|
||||||
|
dependsOn: [{ projects: ['web'], target: 'serve' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const failures = await findInvalidTaskGraphs(graph);
|
||||||
|
|
||||||
|
assert.equal(failures.length, 1);
|
||||||
|
assert.equal(failures[0].task, 'web-e2e:e2e');
|
||||||
|
assert.match(failures[0].message, /web-e2e:e2e -> web:serve/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('accepts a non-parallel e2e target whose servers Playwright starts', async () => {
|
||||||
|
const graph = graphWithE2eTarget({ parallelism: false, dependsOn: [] });
|
||||||
|
|
||||||
|
assert.deepEqual(await findInvalidTaskGraphs(graph), []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('validates both the local and the CI plugin inference', () => {
|
||||||
|
assert.deepEqual(MODES, { local: { CI: undefined }, ci: { CI: 'true' } });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('expects the e2e target and one atomized target per spec', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
expectedPlaywrightTargets({ 'web-e2e': ['src/basic.e2e.ts'] }),
|
||||||
|
['web-e2e:e2e', 'web-e2e:e2e-ci--src/basic.e2e.ts']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('reports expected targets the plugin no longer infers', () => {
|
||||||
|
const graph = graphWithE2eTarget({ parallelism: false, dependsOn: [] });
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
findMissingTargets(graph, [
|
||||||
|
'web-e2e:e2e',
|
||||||
|
'web-e2e:e2e-ci--src/basic.e2e.ts',
|
||||||
|
]),
|
||||||
|
['web-e2e:e2e-ci--src/basic.e2e.ts']
|
||||||
|
);
|
||||||
|
});
|
||||||
Reference in new issue
Block a user