Skip to content
Back to Knowledge Base

Skip unaffected split e2e tests

Skip unaffected split e2e tests in this workspace

You are scoping split (atomized) e2e test files in an Nx workspace so that each test file's task reruns only when the code it tests changes. Read this guide first: https://nx.dev/docs/kb/skip-unaffected-e2e-tests.md

1. Find the e2e projects and their split targets with `nx show project <e2e-project> --json`. Split targets are named `e2e-ci--<test file path>`. If there are none, stop. Cypress creates them only when the `nxE2EPreset` options set `ciWebServerCommand`, and Vitest and Cypress component tests don't read the directives.

2. Pick a directive for each test file:- `// @nx-ignore-server-inputs` when the workspace has Ultracache turned on in Nx Cloud, the runner is Playwright or Cypress, and everything the test checks is in modules the test file can import.

- `// @nx-depends-on: <project>, <project>` otherwise. List every project the test exercises. The projects they depend on are included automatically. Don't list the app project, because that puts the whole app back in the hash.

- Use `@nx-depends-on` when the test depends on stylesheets (screenshots, visibility checks) outside Cypress with the Vite preprocessor, on files loaded by path (`templateUrl`, `styleUrl`, static assets, fetched data), on the app shell, layouts or shared providers, on an API server, or on the server task's environment variables, runtime inputs or external dependencies.

- Leave smoke tests that exercise the whole app unscoped.

3. Put the directive at the top of the file, before the first line of code, at the start of a comment line. `@nx-depends-on` needs the colon right after its name.

4. With `@nx-ignore-server-inputs`, add bare imports (`import '<path>'`) of the feature's own page or component module:- Not route tables, the app entry point, or barrel files. They pull every feature into the hash.

- For lazy-loaded features, import the feature module directly. Playwright doesn't read modules that a dynamic `import()` loads.

- Playwright loads imports in Node. A module that uses `window` or `document` at the top level, a stylesheet in an ECMAScript module package, or a `.vue` file fails the whole test file. Cypress with webpack fails to bundle stylesheets and `.vue` files. If a feature won't load, import the TypeScript modules it uses, or switch the file to `@nx-depends-on`.

- When unsure, import more. An extra import costs extra reruns. A missing one replays a stale result.

5. Fix the tooling errors the imports cause:- `@nx/enforce-module-boundaries` skips most checks for bare imports in files with `@nx-ignore-server-inputs`, but still checks imports that name values or types.

- Disable `import/no-unassigned-import` (eslint-plugin-import or the Oxlint import plugin) for the e2e test files instead of removing the imports.

- If a test file imports `.tsx` files, set `jsx` (for example `"react-jsx"`) in the e2e project's TypeScript configuration. A missing `jsx` shows up as TS6305 with project references or TS6142 with path aliases.

6. Verify with `nx show target inputs <e2e-project>:<split target> --check <file the test exercises> <file it doesn't>`. With `@nx-depends-on`, the first file is an input and the second isn't. For `@nx-ignore-server-inputs`, run the target in CI, then inspect its Ultracache configuration in the Nx Cloud UI. Check that it includes the files the test exercises and excludes unrelated files. If the configuration isn't scoped as expected:- Playwright: Nx runs the `webServer` as a task only when `reuseExistingServer` is `true`, the entry sets no `wait` or `env`, and `command` is `nx run <project>:<target>` or `nx <target> <project>` with no other arguments. Nx evaluates the config with its own environment, so `reuseExistingServer: !process.env.CI` is `false` in CI. Without a server task, `@nx-ignore-server-inputs` has no effect.

- An `nx.json` target default that matches the split target, such as one keyed `e2e-ci--*`, replaces the inferred `inputs` and `dependsOn`. Add `"..."` to each array it sets, and set `"inputs": false` on any server dependency it adds.

- If the test needs some of the server task's environment variables, runtime inputs or external dependencies, add them to the test file's target in `project.json`, with `"..."` in `inputs`.

7. Prefer directives over `project.json` entries. A `project.json` entry under the split target's name replaces the inferred `inputs` and `dependsOn`, so copy every entry you keep and don't add `"..."`. The entry stops matching when the test file moves.

Notes:

- Without a recorded Ultracache run (local machines, the first CI run, or `NX_CLOUD_USE_ULTRACACHE=false`), a task with `@nx-ignore-server-inputs` hashes its declared inputs minus the server task's inputs.

- Without Ultracache, every split task hashes the whole e2e project, so an edit to any test file reruns all of them.

- Ultracache records reads as folder patterns per kind of file (JavaScript and TypeScript together, other files by extension). A new file under a folder the test read from changes the hash. Existing files the test didn't read don't.

- `nx affected -t e2e-ci` still selects `e2e-ci` by its unscoped inputs. Each split task whose hash didn't change replays from the cache.

- A test file with both directives is scoped by projects.

Report which test files you scoped, the directive and imports you added to each, and any test file you left unscoped and why.

You are scoping split (atomized) e2e test files in an Nx workspace so that each test file's task reruns only when the code it tests changes. Read this guide first: https://nx.dev/docs/kb/skip-unaffected-e2e-tests.md

1. Find the e2e projects and their split targets with `nx show project <e2e-project> --json`. Split targets are named `e2e-ci--<test file path>`. If there are none, stop. Cypress creates them only when the `nxE2EPreset` options set `ciWebServerCommand`, and Vitest and Cypress component tests don't read the directives.

2. Pick a directive for each test file:- `// @nx-ignore-server-inputs` when the workspace has Ultracache turned on in Nx Cloud, the runner is Playwright or Cypress, and everything the test checks is in modules the test file can import.

- `// @nx-depends-on: <project>, <project>` otherwise. List every project the test exercises. The projects they depend on are included automatically. Don't list the app project, because that puts the whole app back in the hash.

- Use `@nx-depends-on` when the test depends on stylesheets (screenshots, visibility checks) outside Cypress with the Vite preprocessor, on files loaded by path (`templateUrl`, `styleUrl`, static assets, fetched data), on the app shell, layouts or shared providers, on an API server, or on the server task's environment variables, runtime inputs or external dependencies.

- Leave smoke tests that exercise the whole app unscoped.

3. Put the directive at the top of the file, before the first line of code, at the start of a comment line. `@nx-depends-on` needs the colon right after its name.

4. With `@nx-ignore-server-inputs`, add bare imports (`import '<path>'`) of the feature's own page or component module:- Not route tables, the app entry point, or barrel files. They pull every feature into the hash.

- For lazy-loaded features, import the feature module directly. Playwright doesn't read modules that a dynamic `import()` loads.

- Playwright loads imports in Node. A module that uses `window` or `document` at the top level, a stylesheet in an ECMAScript module package, or a `.vue` file fails the whole test file. Cypress with webpack fails to bundle stylesheets and `.vue` files. If a feature won't load, import the TypeScript modules it uses, or switch the file to `@nx-depends-on`.

- When unsure, import more. An extra import costs extra reruns. A missing one replays a stale result.

5. Fix the tooling errors the imports cause:- `@nx/enforce-module-boundaries` skips most checks for bare imports in files with `@nx-ignore-server-inputs`, but still checks imports that name values or types.

- Disable `import/no-unassigned-import` (eslint-plugin-import or the Oxlint import plugin) for the e2e test files instead of removing the imports.

- If a test file imports `.tsx` files, set `jsx` (for example `"react-jsx"`) in the e2e project's TypeScript configuration. A missing `jsx` shows up as TS6305 with project references or TS6142 with path aliases.

6. Verify with `nx show target inputs <e2e-project>:<split target> --check <file the test exercises> <file it doesn't>`. With `@nx-depends-on`, the first file is an input and the second isn't. For `@nx-ignore-server-inputs`, run the target in CI, then inspect its Ultracache configuration in the Nx Cloud UI. Check that it includes the files the test exercises and excludes unrelated files. If the configuration isn't scoped as expected:- Playwright: Nx runs the `webServer` as a task only when `reuseExistingServer` is `true`, the entry sets no `wait` or `env`, and `command` is `nx run <project>:<target>` or `nx <target> <project>` with no other arguments. Nx evaluates the config with its own environment, so `reuseExistingServer: !process.env.CI` is `false` in CI. Without a server task, `@nx-ignore-server-inputs` has no effect.

- An `nx.json` target default that matches the split target, such as one keyed `e2e-ci--*`, replaces the inferred `inputs` and `dependsOn`. Add `"..."` to each array it sets, and set `"inputs": false` on any server dependency it adds.

- If the test needs some of the server task's environment variables, runtime inputs or external dependencies, add them to the test file's target in `project.json`, with `"..."` in `inputs`.

7. Prefer directives over `project.json` entries. A `project.json` entry under the split target's name replaces the inferred `inputs` and `dependsOn`, so copy every entry you keep and don't add `"..."`. The entry stops matching when the test file moves.

Notes:

- Without a recorded Ultracache run (local machines, the first CI run, or `NX_CLOUD_USE_ULTRACACHE=false`), a task with `@nx-ignore-server-inputs` hashes its declared inputs minus the server task's inputs.

- Without Ultracache, every split task hashes the whole e2e project, so an edit to any test file reruns all of them.

- Ultracache records reads as folder patterns per kind of file (JavaScript and TypeScript together, other files by extension). A new file under a folder the test read from changes the hash. Existing files the test didn't read don't.

- `nx affected -t e2e-ci` still selects `e2e-ci` by its unscoped inputs. Each split task whose hash didn't change replays from the cache.

- A test file with both directives is scoped by projects.

Report which test files you scoped, the directive and imports you added to each, and any test file you left unscoped and why.

The Atomizer splits an end-to-end (e2e) suite into one task per test file, such as e2e-ci--src/checkout.spec.ts. Nx caches each of those tasks, but by default each task's hash covers the whole app and the whole e2e project. A change to any feature, or to any test file, reruns every test file.

For example, a shop app has checkout/, search/, cart/, and shared/ folders, and its e2e project has two test files, checkout.spec.ts and search.spec.ts. Before, without a directive, checkout.spec.ts and search.spec.ts both hash every folder, so an edit to search/ reruns both:

Without a directive, checkout.spec.ts and search.spec.ts both hash checkout/, search/, cart/, and shared/, so an edit in search/ reruns both tests.

To fix that, add a one-line directive comment at the top of a test file. The directive tells Nx which code the test depends on. When a change doesn't touch that code, Nx skips the test and replays its cached result.

After, with a directive in each test file, checkout.spec.ts hashes only checkout/ and shared/, and search.spec.ts hashes only search/ and shared/. The same edit to search/ reruns only search.spec.ts:

With a directive, checkout.spec.ts hashes checkout/ and shared/, and search.spec.ts hashes search/ and shared/, so an edit in search/ reruns only search.spec.ts.

// @nx-depends-on: <projects> scopes the test to the projects you list and the projects they depend on. The test reruns when one of those projects changes. It works in Playwright, Cypress, and Jest test files.

// @nx-ignore-server-inputs drops the app server from the test's hash. With Ultracache turned on in Nx Cloud, Nx then hashes the test from the files its last CI run read: the test file and the app code it imports. The test reruns when one of those files changes. It works in Playwright and Cypress test files.

Without Ultracache, each test also hashes every file in the e2e project, so an edit to any test file still reruns it.

Use @nx-ignore-server-inputs when Ultracache is on and the application code your test exercises is in TypeScript or JavaScript files the test can import, such as React components. Use @nx-depends-on in other cases, such as applications that use external templates.

Both examples test the checkout page from the checkout library, which the shop application uses.

apps/shop-e2e/src/checkout.spec.ts
// @nx-depends-on: checkout
import { expect, test } from '@playwright/test';
test('shows the order summary', async ({ page }) => {
await page.goto('/checkout');
await expect(
page.getByRole('heading', { name: 'Order summary' })
).toBeVisible();
});

The test reruns when checkout or a project it depends on changes. An edit to the unrelated search library replays the cached result.

apps/shop-e2e/src/checkout.spec.ts
// @nx-ignore-server-inputs
import '@myorg/checkout/checkout-page';
import { expect, test } from '@playwright/test';
test('shows the order summary', async ({ page }) => {
await page.goto('/checkout');
await expect(
page.getByRole('heading', { name: 'Order summary' })
).toBeVisible();
});

The bare import makes the test runner load the checkout library's page when it loads the test file. Ultracache records those reads, so the test reruns when the checkout page, or a module it imports, changes.

Import the feature's own page or component, not the route table, the app entry point, or a barrel file. Those pull every feature into the hash. See choose what to import for more.

Leave the directive off a test that exercises the whole app, such as a login-and-checkout smoke test. It reruns on any change to the app.

Put each directive on its own comment line at the top of the test file, before the first line of code. Line comments and block comments both work, and blank lines, a #! line, or other comments can come first:

apps/shop-e2e/src/checkout.spec.ts
// @nx-depends-on: checkout, payments
// @nx-depends-on: tag:scope:shared
import { test } from '@playwright/test';
/**
* Checkout flow tests.
* @nx-ignore-server-inputs
*/
import '../../shop/src/app/checkout/checkout-page';

@nx-depends-on takes a colon and a comma-separated list of project names or patterns, such as tag:scope:shared. Repeat the line to list more. @nx-ignore-server-inputs takes no value.

A test file can have both directives, and Nx then scopes it by projects, because @nx-depends-on also leaves out the server task's inputs.

Run nx show target inputs for the test file's split task, and pass --check with a file the test exercises and a file it doesn't:

Terminal window
nx show target inputs shop-e2e:e2e-ci--src/checkout.spec.ts --check libs/checkout/src/index.ts libs/search/src/index.ts

For a test file with // @nx-depends-on: checkout, Nx lists the checkout file as an input and the search file as not an input:

✓ These arguments were inputs for shop-e2e:e2e-ci--src/checkout.spec.ts:
libs/checkout/src/index.ts
✗ These arguments were not inputs for shop-e2e:e2e-ci--src/checkout.spec.ts:
libs/search/src/index.ts

The listed projects stay in the hash with or without Ultracache, so the result matches CI. On your machine, the inputs also include every file in the e2e project, such as search.spec.ts. In CI with Ultracache, Nx keeps only the e2e project files the test read.

Run the split e2e target in CI so Ultracache records its file inputs. Then inspect the target's Ultracache configuration in the Nx Cloud UI to see which files the test depends on.

For e2e-ci--src/checkout.spec.ts, check that it includes the test file, the checkout library, and the modules it imports, without unrelated files from the search library.

Read these sections when a directive doesn't skip the tests you expect, or when you're unsure which directive fits.

@nx-ignore-server-inputs fits a test when all the code it exercises is in modules the test file can import, such as pages, components, and the services they call. Use @nx-depends-on instead when the test's results depend on something the test runner doesn't read:

  • Files the app loads by path instead of importing them, such as static assets, data it fetches at runtime, and Angular templateUrl and styleUrl files.
  • App code the test exercises but doesn't import, such as the app shell, layouts, and shared providers.
  • Stylesheets, for screenshot comparisons and visibility checks. Only Cypress with the Vite preprocessor reads the stylesheets that imports reach.
  • Other services the app calls, such as an API server.
  • The server task's configuration files, environment variables, runtime inputs, and external dependencies.

The test runner reads each module the test file imports, and each module those modules import.

Decide what each test file is meant to test, then import the modules that cover that scope. This may require multiple imports.

Lazy-loaded modules sometimes belong in that scope, but often represent other parts of the app that the test doesn't exercise. Include them only when they are part of what the test is meant to cover.

Playwright doesn't read modules reached through dynamic import(), so import a lazy-loaded module directly when the test covers it. Cypress bundles dynamic imports, so importing a route table can pull unrelated lazy-loaded features into the test's hash.

Playwright loads imported modules in Node. Cypress bundles them with its preprocessor: Vite when the nxE2EPreset options set bundler: 'vite', webpack otherwise.

File an import reachesPlaywrightCypress with webpackCypress with Vite
A module that a dynamic import() loadsNot readReadRead
A stylesheetNot read, or fails to load in an ECMAScript module packageFails to bundleRead
A .vue fileFails to loadFails to bundleFails without a Vue plugin
A file a component names by path, such as templateUrlNot readNot readNot read

In Playwright, a module that fails to load, or that uses window or document in its top-level code, fails the test file before its tests run. With a custom Cypress preprocessor, its configuration decides what it reads. For a feature that doesn't load, import the TypeScript modules it uses, or scope the test with @nx-depends-on.

With Ultracache enabled, Nx 24 and later automatically use recorded file inputs in CI when a recording is available for the task. Set NX_CLOUD_USE_ULTRACACHE=false to use declared inputs instead. See Ultracache and Nx versions.

Without a recorded run, such as on your machine, a task with @nx-ignore-server-inputs hashes its declared inputs without the server task's inputs. Each split task's declared inputs cover the whole e2e project, so an edit to any test file reruns every test file. They also cover the app through ^production when the e2e project depends on the app.

@nx-ignore-server-inputs works only when Nx starts your app server as an Nx task. If Playwright or Cypress starts the server directly, this directive has no effect. @nx-depends-on still limits the test's inputs to the projects you list, but excludes the server's inputs only when Nx starts it.

For Playwright, configure webServer as follows:

  • Set reuseExistingServer: true, including in CI. Using !process.env.CI turns it off in CI.
  • Set command to nx run <project>:<target> or nx <target> <project>, with no extra arguments. A prefix such as npx or pnpm exec is fine.
  • Leave out wait and env.

For more on the commands Nx recognizes, see Playwright web server readiness.

For Cypress, set ciWebServerCommand in nxE2EPreset to an Nx command. Don't set webServerConfig.reuseExistingServer to false.

Jest supports only @nx-depends-on. Neither directive is supported in Vitest or Cypress component tests.

Excluding the server's inputs also excludes its environment variables, runtime inputs, and external dependencies. If the test needs any of these, add them to the test target's inputs in project.json. Include "..." to keep the inputs Nx already provides.

For // @nx-depends-on: checkout, checkout-utils in a Cypress test file, the plugin infers this configuration:

apps/shop-e2e/project.json
{
"targets": {
"e2e-ci--src/e2e/checkout.cy.ts": {
"inputs": [
"default",
{
"input": "production",
"projects": ["checkout", "checkout-utils"],
"always": true,
},
{
"input": "production",
"projects": ["checkout", "checkout-utils"],
"dependencies": true,
"always": true,
},
{ "externalDependencies": ["cypress"] },
],
"dependsOn": [
{
"target": "serve-static",
"projects": ["shop"],
"waitFor": "ready",
"inputs": false,
},
],
},
},
}
  • The two production inputs replace the ^ inputs the plugin infers: ^production for Cypress, plus ^{projectRoot}/tsconfig*.json for Playwright. They use default when there's no production named input.
  • "always": true keeps these projects in the hash under Ultracache, which would otherwise drop them because the test runner doesn't read the code the server serves.
  • "inputs": false on the server dependency leaves the server task's inputs out of the hash.

You can write the same configuration in project.json, under the split target's name. The inputs and dependsOn you set there replace the arrays the plugin infers, so copy every inferred entry you keep, and don't add the spread token "...", which restores ^production and the server task's inputs. The entry stops matching when you move or rename the test file, and each new test file needs its own entry.

A target default in nx.json that matches a split target, such as one keyed with a glob that starts with e2e-ci--, also replaces the inferred inputs and dependsOn. To keep what a directive inferred, add "..." to each array the target default sets, and set "inputs": false yourself on any server dependency it adds.

The @nx/enforce-module-boundaries rule skips most of its checks on bare imports of other projects in a test file with @nx-ignore-server-inputs. It still checks imports that name values or types, and it runs every check on test files without that directive.

The import/no-unassigned-import rule of eslint-plugin-import and of the Oxlint import plugin reports bare imports. Disable it for the e2e project's test files instead of removing the imports.

When a test file imports .tsx files, set jsx in the compilerOptions of the e2e project's TypeScript configuration, such as "jsx": "react-jsx". Without it, TypeScript reports error TS6305 (Output file '<path>' has not been built from source file '<path>') with project references, or TS6142 with path aliases.

Last updated: