mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 01:56:16 -08:00
fix(e2e): let web-e2e:e2e run outside CI
Without CI, every web-e2e webServer has reuseExistingServer, so @nx/playwright infers a continuous web:serve dependency for the `pnpm nx run web:serve` entry. The mocks and the web backend launch as plain node processes with env, which no Nx task covers, so the plugin also marks the target non-parallel. Nx refuses a non-parallel task with continuous dependencies, and `nx run web-e2e:e2e` failed before running anything; CI passed only because CI=1 disables reuse and with it the inferred dependency. Give web-e2e:e2e an empty dependsOn so Playwright starts every server, as it already does in CI. The Angular dev-server runs inside the Nx process, so Playwright's process-group kill still stops it. Turn off the plugin's readiness gate, which no target consumes any more. Add `pnpm run e2e:task-graphs:validate`: it builds every Playwright target's task graph with Nx's own validation, with CI unset and set, and runs in the unit-and-typecheck job. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
525ca7bc44
commit
3f3e9ffefb
9 files changed
+260
-16
No files matched your search
@@ -0,0 +1,120 @@
|
||||
import { execFileSync } from 'node:child_process';
|
||||
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' },
|
||||
};
|
||||
|
||||
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 } = await import('@nx/devkit');
|
||||
const projectGraph = await createProjectGraphAsync({ exitOnError: true });
|
||||
const failures = await findInvalidTaskGraphs(projectGraph);
|
||||
const checked = playwrightTargets(projectGraph).length;
|
||||
console.log(`${RESULT_PREFIX}${JSON.stringify({ checked, failures })}`);
|
||||
}
|
||||
|
||||
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 } = JSON.parse(
|
||||
result.slice(RESULT_PREFIX.length)
|
||||
);
|
||||
if (checked === 0) {
|
||||
failed = true;
|
||||
console.error(`[${mode}] no Playwright targets were inferred`);
|
||||
}
|
||||
for (const { task, message } of failures) {
|
||||
failed = true;
|
||||
console.error(
|
||||
`[${mode}] ${task}\n ${message.replace(/\n/g, '\n ')}`
|
||||
);
|
||||
}
|
||||
if (failures.length === 0 && checked > 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,78 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { test } from 'node:test';
|
||||
|
||||
import {
|
||||
findInvalidTaskGraphs,
|
||||
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' } });
|
||||
});
|
||||
Reference in new issue
Block a user