diff --git a/apps/xtream-mock-server/src/app/project-config.spec.ts b/apps/xtream-mock-server/src/app/project-config.spec.ts index 9c1895228..740a1ee5c 100644 --- a/apps/xtream-mock-server/src/app/project-config.spec.ts +++ b/apps/xtream-mock-server/src/app/project-config.spec.ts @@ -87,3 +87,99 @@ describe('Playwright mock-server launch', () => { } ); }); + +describe('Nx E2E task dependencies', () => { + type Dependency = + | string + | { + projects?: string | string[]; + dependencies?: boolean; + target?: string; + }; + type TargetConfig = { + dependsOn?: Dependency[]; + continuous?: boolean; + filter?: { projects?: string[] }; + }; + type ProjectJson = { targets: Record }; + const readJson = (path: string) => + JSON.parse(readFileSync(join(process.cwd(), path), 'utf8')) as T; + const nxJson = readJson<{ + targetDefaults: Record; + }>('nx.json'); + const appNames = readdirSync(join(process.cwd(), 'apps')); + const e2eProjectFiles = appNames + .filter((name) => name.endsWith('-e2e')) + .map((name) => `apps/${name}/project.json`); + // The E2E projects list the mocks as implicit dependencies, so a `^serve` + // dependency schedules the mock serve tasks as well. + const mockContinuousTargets = new Set( + appNames + .filter((name) => name.endsWith('-mock-server')) + .flatMap((name) => + Object.entries( + readJson(`apps/${name}/project.json`).targets + ) + .filter(([, target]) => target.continuous) + .map(([targetName]) => targetName) + ) + ); + const dependsOnMock = (dependency: Dependency) => + typeof dependency === 'string' + ? dependency.includes('-mock-server:') || + (dependency.startsWith('^') && + mockContinuousTargets.has(dependency.slice(1))) + : [dependency.projects ?? []] + .flat() + .some((project) => project.includes('-mock-server')) || + (dependency.dependencies === true && + mockContinuousTargets.has(dependency.target ?? '')); + const perFileDefaultFor = (project: string) => + [nxJson.targetDefaults['e2e-ci--src/*.e2e.ts'] ?? []] + .flat() + .find((entry) => entry.filter?.projects?.includes(project)); + + // The Playwright configs start the mocks themselves, so @nx/playwright + // infers their E2E targets as non-parallel, and Nx refuses to run a + // non-parallel task that depends on a continuous `serve` task. + it('never makes an E2E target depend on a mock-server task', () => { + const e2eDefaults = Object.entries(nxJson.targetDefaults) + .filter(([targetName]) => targetName.startsWith('e2e')) + .flatMap(([targetName, config]) => + [config].flat().map((entry) => ({ + source: `nx.json ${targetName}`, + config: entry, + })) + ); + const e2eProjectTargets = e2eProjectFiles.flatMap((path) => + Object.entries(readJson(path).targets).map( + ([targetName, config]) => ({ + source: `${path} ${targetName}`, + config, + }) + ) + ); + const mockDependencies = [...e2eDefaults, ...e2eProjectTargets] + .map(({ source, config }) => ({ + source, + dependencies: (config.dependsOn ?? []).filter(dependsOnMock), + })) + .filter(({ dependencies }) => dependencies.length > 0); + + expect(e2eProjectFiles).toEqual( + expect.arrayContaining([ + 'apps/electron-backend-e2e/project.json', + 'apps/web-e2e/project.json', + ]) + ); + expect([...mockContinuousTargets]).toContain('serve'); + expect(mockDependencies).toEqual([]); + }); + + it('builds the Electron app only before Electron per-file E2E targets', () => { + expect(perFileDefaultFor('electron-backend-e2e')?.dependsOn).toEqual([ + { projects: ['electron-backend'], target: 'build-e2e' }, + ]); + expect(perFileDefaultFor('web-e2e')?.dependsOn).toEqual([]); + }); +}); diff --git a/docs/architecture/xtream-mock-server.md b/docs/architecture/xtream-mock-server.md index 571c2f461..a82e1618f 100644 --- a/docs/architecture/xtream-mock-server.md +++ b/docs/architecture/xtream-mock-server.md @@ -579,7 +579,11 @@ 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. The web-e2e `web-backend` entry uses the same +for starting a mock by hand, and no E2E target may depend on them: with no +Nx-launched server, `@nx/playwright` infers the atomized `e2e-ci--*` targets +as non-parallel, and Nx refuses to run a non-parallel task that depends on a +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 launch form; see [PWA web backend](pwa-self-hosted.md#web-backend). ### Request Interception diff --git a/nx.json b/nx.json index ff0a58753..2f36ffe99 100644 --- a/nx.json +++ b/nx.json @@ -68,18 +68,21 @@ "dependsOn": ["^build"], "inputs": ["production", "^production"] }, - "e2e-ci--src/*.e2e.ts": { - "dependsOn": [ - { - "projects": ["electron-backend"], - "target": "build-e2e" - }, - { - "projects": ["stalker-mock-server", "xtream-mock-server"], - "target": "serve" - } - ] - } + "e2e-ci--src/*.e2e.ts": [ + { + "filter": { "projects": ["electron-backend-e2e"] }, + "dependsOn": [ + { + "projects": ["electron-backend"], + "target": "build-e2e" + } + ] + }, + { + "filter": { "projects": ["web-e2e"] }, + "dependsOn": [] + } + ] }, "plugins": [ {