Skip to content

chore: add sharding for playwright tests#7187

Merged
beaussan merged 7 commits intomainfrom
atomize-playwright
Apr 14, 2026
Merged

chore: add sharding for playwright tests#7187
beaussan merged 7 commits intomainfrom
atomize-playwright

Conversation

@beaussan
Copy link
Copy Markdown
Contributor

@beaussan beaussan commented Apr 14, 2026

Summary by CodeRabbit

Release Notes

  • New Features

    • Implemented E2E test sharding to enable parallel test execution with automatic port management
    • Tests now run in isolated shards to prevent port conflicts during concurrent runs
  • Chores

    • Updated development dependencies to latest versions

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 14, 2026

📝 Walkthrough

Walkthrough

This pull request introduces Nx-based Playwright test sharding with a custom plugin that generates shard-specific targets, adds environment-variable-based port key management to prevent test interference, and updates related E2E configuration and dependencies to support distributed test execution.

Changes

Cohort / File(s) Summary
Changeset Configuration
.nx/workflows/dynamic-changesets.yaml
Increased changeset size thresholds: small-changeset 3→4, medium-changeset 6→8, large-changeset 10→14.
Playwright Sharding Plugin
scripts/nx/playwright-plugin.ts, scripts/nx/playwright-plugin.md
New Nx plugin that reads playwrightShards from package.json and generates per-shard Playwright targets with --shard=i/count, per-shard E2E_PORT_KEY, and a parent test:e2e aggregator target; includes documentation on port management and cleanup patterns.
E2E Test Configuration
e2e/react-start/rsc/package.json, e2e/react-start/rsc/playwright.config.ts, e2e/react-start/rsc/tests/setup/global.setup.ts, e2e/react-start/rsc/tests/setup/global.teardown.ts
Added playwrightShards: 6 metadata, replaced test:e2e with test:e2e-full (runs build, cleans shard-specific port files, runs Playwright); updated Playwright config to use E2E_PORT_KEY environment variable for port derivation and add cleanup logic; updated setup/teardown to use E2E_PORT_KEY fallback to package name for dummy server management.
Nx Configuration
nx.json
Added "test:e2e--*" to test:e2e target dependencies, registered Playwright Sharding Plugin, and enabled Nx analytics.
Dependencies
package.json
Added @nx/devkit@22.6.5, @swc-node/core, @swc-node/register to devDependencies; bumped nx from 22.5.1 to 22.6.5.

Sequence Diagram

sequenceDiagram
    participant Nx as Nx Plugin
    participant PM as Package.json
    participant PT as Playwright Config
    participant Setup as Global Setup
    participant DS as Dummy Server
    participant PW as Playwright
    participant TD as Global Teardown

    Nx->>PM: Read playwrightShards config
    Nx->>Nx: Generate shard targets<br/>(test:e2e--shard-1-of-6, etc.)
    
    loop For each shard
        Note over Nx,PW: Shard Execution
        PT->>PT: Determine e2ePortKey<br/>from E2E_PORT_KEY env var
        PT->>PT: Delete shard-specific<br/>port-*.txt files
        PT->>PT: Resolve PORT via<br/>getTestServerPort(e2ePortKey)
        PT->>PT: Start webServer<br/>on resolved PORT
        
        Setup->>DS: Start dummy server<br/>with e2ePortKey
        
        PW->>PW: Run playwright test<br/>--shard={i}/{shardCount}<br/>--project=chromium
        
        TD->>DS: Stop dummy server<br/>using e2ePortKey
    end
    
    Nx->>Nx: Parent test:e2e target<br/>waits for all shards
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 With shards we hop and split the load,
Each rabbit runs its own test road,
Port keys dance in harmony,
No conflicts in our E2E
Parallel hops make testing free! 🌟

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding Playwright test sharding support through a new plugin, workflow adjustments, and configuration updates across multiple e2e test files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch atomize-playwright

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@nx-cloud
Copy link
Copy Markdown
Contributor

nx-cloud bot commented Apr 14, 2026

View your CI Pipeline Execution ↗ for commit ace374f

Command Status Duration Result
nx affected --targets=test:eslint,test:unit,tes... ✅ Succeeded 1m 27s View ↗
nx run-many --target=build --exclude=examples/*... ✅ Succeeded 5s View ↗

☁️ Nx Cloud last updated this comment at 2026-04-14 10:25:50 UTC

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 14, 2026

🚀 Changeset Version Preview

No changeset entries found. Merging this PR will not cause a version bump for any packages.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 14, 2026

More templates

@tanstack/arktype-adapter

npm i https://pkg.pr.new/@tanstack/arktype-adapter@7187

@tanstack/eslint-plugin-router

npm i https://pkg.pr.new/@tanstack/eslint-plugin-router@7187

@tanstack/eslint-plugin-start

npm i https://pkg.pr.new/@tanstack/eslint-plugin-start@7187

@tanstack/history

npm i https://pkg.pr.new/@tanstack/history@7187

@tanstack/nitro-v2-vite-plugin

npm i https://pkg.pr.new/@tanstack/nitro-v2-vite-plugin@7187

@tanstack/react-router

npm i https://pkg.pr.new/@tanstack/react-router@7187

@tanstack/react-router-devtools

npm i https://pkg.pr.new/@tanstack/react-router-devtools@7187

@tanstack/react-router-ssr-query

npm i https://pkg.pr.new/@tanstack/react-router-ssr-query@7187

@tanstack/react-start

npm i https://pkg.pr.new/@tanstack/react-start@7187

@tanstack/react-start-client

npm i https://pkg.pr.new/@tanstack/react-start-client@7187

@tanstack/react-start-rsc

npm i https://pkg.pr.new/@tanstack/react-start-rsc@7187

@tanstack/react-start-server

npm i https://pkg.pr.new/@tanstack/react-start-server@7187

@tanstack/router-cli

npm i https://pkg.pr.new/@tanstack/router-cli@7187

@tanstack/router-core

npm i https://pkg.pr.new/@tanstack/router-core@7187

@tanstack/router-devtools

npm i https://pkg.pr.new/@tanstack/router-devtools@7187

@tanstack/router-devtools-core

npm i https://pkg.pr.new/@tanstack/router-devtools-core@7187

@tanstack/router-generator

npm i https://pkg.pr.new/@tanstack/router-generator@7187

@tanstack/router-plugin

npm i https://pkg.pr.new/@tanstack/router-plugin@7187

@tanstack/router-ssr-query-core

npm i https://pkg.pr.new/@tanstack/router-ssr-query-core@7187

@tanstack/router-utils

npm i https://pkg.pr.new/@tanstack/router-utils@7187

@tanstack/router-vite-plugin

npm i https://pkg.pr.new/@tanstack/router-vite-plugin@7187

@tanstack/solid-router

npm i https://pkg.pr.new/@tanstack/solid-router@7187

@tanstack/solid-router-devtools

npm i https://pkg.pr.new/@tanstack/solid-router-devtools@7187

@tanstack/solid-router-ssr-query

npm i https://pkg.pr.new/@tanstack/solid-router-ssr-query@7187

@tanstack/solid-start

npm i https://pkg.pr.new/@tanstack/solid-start@7187

@tanstack/solid-start-client

npm i https://pkg.pr.new/@tanstack/solid-start-client@7187

@tanstack/solid-start-server

npm i https://pkg.pr.new/@tanstack/solid-start-server@7187

@tanstack/start-client-core

npm i https://pkg.pr.new/@tanstack/start-client-core@7187

@tanstack/start-fn-stubs

npm i https://pkg.pr.new/@tanstack/start-fn-stubs@7187

@tanstack/start-plugin-core

npm i https://pkg.pr.new/@tanstack/start-plugin-core@7187

@tanstack/start-server-core

npm i https://pkg.pr.new/@tanstack/start-server-core@7187

@tanstack/start-static-server-functions

npm i https://pkg.pr.new/@tanstack/start-static-server-functions@7187

@tanstack/start-storage-context

npm i https://pkg.pr.new/@tanstack/start-storage-context@7187

@tanstack/valibot-adapter

npm i https://pkg.pr.new/@tanstack/valibot-adapter@7187

@tanstack/virtual-file-routes

npm i https://pkg.pr.new/@tanstack/virtual-file-routes@7187

@tanstack/vue-router

npm i https://pkg.pr.new/@tanstack/vue-router@7187

@tanstack/vue-router-devtools

npm i https://pkg.pr.new/@tanstack/vue-router-devtools@7187

@tanstack/vue-router-ssr-query

npm i https://pkg.pr.new/@tanstack/vue-router-ssr-query@7187

@tanstack/vue-start

npm i https://pkg.pr.new/@tanstack/vue-start@7187

@tanstack/vue-start-client

npm i https://pkg.pr.new/@tanstack/vue-start-client@7187

@tanstack/vue-start-server

npm i https://pkg.pr.new/@tanstack/vue-start-server@7187

@tanstack/zod-adapter

npm i https://pkg.pr.new/@tanstack/zod-adapter@7187

commit: ace374f

nx-cloud[bot]

This comment was marked as outdated.

nx-cloud[bot]

This comment was marked as outdated.

@beaussan beaussan marked this pull request as ready for review April 14, 2026 10:16
Copy link
Copy Markdown
Contributor

@nx-cloud nx-cloud bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nx Cloud is proposing a fix for your failed CI:

We reordered the imports in hydrateStart.ts to fix the three import/order ESLint errors that were blocking CI. The #tanstack-* package imports (Node.js subpath imports) are not resolvable by the ESLint import plugin and are therefore placed in a trailing group, so all regular relative and type imports must precede them.

Warning

We could not verify this fix.

diff --git a/packages/start-client-core/src/client/hydrateStart.ts b/packages/start-client-core/src/client/hydrateStart.ts
index 3b0ca44cee..d77c98c60e 100644
--- a/packages/start-client-core/src/client/hydrateStart.ts
+++ b/packages/start-client-core/src/client/hydrateStart.ts
@@ -1,14 +1,14 @@
 import { hydrate } from '@tanstack/router-core/ssr/client'
 
+import { ServerFunctionSerializationAdapter } from './ServerFunctionSerializationAdapter'
+import type { AnyStartInstanceOptions } from '../createStart'
+import type { AnyRouter, AnySerializationAdapter } from '@tanstack/router-core'
 import { startInstance } from '#tanstack-start-entry'
 import {
   hasPluginAdapters,
   pluginSerializationAdapters,
 } from '#tanstack-start-plugin-adapters'
 import { getRouter } from '#tanstack-router-entry'
-import { ServerFunctionSerializationAdapter } from './ServerFunctionSerializationAdapter'
-import type { AnyStartInstanceOptions } from '../createStart'
-import type { AnyRouter, AnySerializationAdapter } from '@tanstack/router-core'
 
 export async function hydrateStart(): Promise<AnyRouter> {
   const router = await getRouter()

Apply fix via Nx Cloud  Reject fix via Nx Cloud


Or Apply changes locally with:

npx nx-cloud apply-locally 0pRD-zd9Y

Apply fix locally with your editor ↗   View interactive diff ↗



🎓 Learn more about Self-Healing CI on nx.dev

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
scripts/nx/playwright-plugin.ts (1)

51-57: Fix the tag typo before anything starts depending on it.

playwright:shareded looks like playwright:sharded. If this tag gets used for filtering or lint constraints later, the misspelling will be easy to miss and hard to unwind.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/nx/playwright-plugin.ts` around lines 51 - 57, The tags array
currently contains a misspelled tag 'playwright:shareded'; update that string to
the intended tag 'playwright:sharded' inside the metadata ->
targetGroups.playwright object so any filtering or linting later uses the
correct tag value.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@nx.json`:
- Line 86: Remove the explicit "analytics": true setting from nx.json; locate
the "analytics" property (the "analytics" key at the root of nx.json) and delete
that entry so telemetry is not forcibly enabled for all developers/CI, leaving
Nx to use its default behavior or be configured separately by the team.

In `@scripts/nx/playwright-plugin.ts`:
- Around line 40-43: Sanitize the value used for E2E_PORT_KEY before it's used
to build marker filenames (e.g., the `port-${E2E_PORT_KEY}.txt` output) so
scoped package names and path-like roots don't create nested filesystem paths;
replace or normalize characters such as "/" and "@" (and any platform path
separators) with a safe delimiter (e.g., "-") or use a basename-style
extraction. Update the code paths where E2E_PORT_KEY is derived (the call site
around `buildShardedTargets(root, projectConfiguration.name ?? root,
projectConfiguration.nx.metadata.playwrightShards, ...)` and the other
occurrences around lines 76-88 that generate the marker file names) to use the
sanitized key so marker files are created as single flat filenames.
- Around line 26-45: Validate that
projectConfiguration.nx?.metadata?.playwrightShards is a positive integer before
calling buildShardedTargets: read it into a local const (e.g. const shards =
projectConfiguration.nx?.metadata?.playwrightShards), check typeof shards ===
'number' && Number.isInteger(shards) && shards > 0, and if the check fails throw
a descriptive Error (including configFilePath and the property name) or return
the non-sharded fallback; then pass the validated shards variable into
buildShardedTargets instead of using the raw property.

---

Nitpick comments:
In `@scripts/nx/playwright-plugin.ts`:
- Around line 51-57: The tags array currently contains a misspelled tag
'playwright:shareded'; update that string to the intended tag
'playwright:sharded' inside the metadata -> targetGroups.playwright object so
any filtering or linting later uses the correct tag value.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c75440b2-4bd2-4922-adee-5b7e288a5c62

📥 Commits

Reviewing files that changed from the base of the PR and between 8caa202 and ace374f.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (9)
  • .nx/workflows/dynamic-changesets.yaml
  • e2e/react-start/rsc/package.json
  • e2e/react-start/rsc/playwright.config.ts
  • e2e/react-start/rsc/tests/setup/global.setup.ts
  • e2e/react-start/rsc/tests/setup/global.teardown.ts
  • nx.json
  • package.json
  • scripts/nx/playwright-plugin.md
  • scripts/nx/playwright-plugin.ts

"plugin": "./scripts/nx/playwright-plugin.ts"
}
],
"analytics": true
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Official Nx docs: what does setting "analytics": trueinnx.json do? Does it enable telemetry for all users and CI runs by default, and what opt-in or environment-gated alternatives does Nx recommend?

💡 Result:

Setting "analytics": true in nx.json enables Nx to collect and send anonymous usage telemetry data (command usage, environment info, etc.) to help improve the tool. It is not enabled by default for all users and CI runs. When "analytics" is not set in nx.json: - In interactive terminals, Nx prompts the user on first run to opt in/out, storing true/false in nx.json. - In CI environments, telemetry does not run (no prompt). With "analytics": true explicitly set: - Telemetry runs in interactive sessions and CI. With "analytics": false: - Telemetry is disabled everywhere. Nx recommends the prompt-based opt-in for interactive use and explicitly setting "analytics": true only if desired in CI (no environment variables or other alternatives mentioned in docs).

Citations:


Remove "analytics": true from nx.json—this is unrelated to Playwright sharding.

Explicitly setting this enables telemetry for all developers and CI runs, overriding Nx's default behavior (which disables telemetry in CI and prompts users interactively otherwise). This is a repo-wide privacy/process change that should be decided separately from this feature and requires explicit team agreement.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nx.json` at line 86, Remove the explicit "analytics": true setting from
nx.json; locate the "analytics" property (the "analytics" key at the root of
nx.json) and delete that entry so telemetry is not forcibly enabled for all
developers/CI, leaving Nx to use its default behavior or be configured
separately by the team.

Comment on lines +26 to +45
const projectConfiguration = readJsonFile<{
name?: string
nx?: { metadata?: { playwrightShards?: number } }
}>(configFilePath)
const root = dirname(configFilePath)

if (!projectConfiguration.nx?.metadata?.playwrightShards) {
return {
projects: {
[root]: {},
},
}
}

const { targets, targetGroupEntries } = buildShardedTargets(
root,
projectConfiguration.name ?? root,
projectConfiguration.nx.metadata.playwrightShards,
['default', '^production'],
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Validate nx.metadata.playwrightShards before using it.

readJsonFile<...>() only types the JSON for TypeScript; it does not guarantee the runtime value is a positive integer. A string, decimal, or negative number here will quietly generate a broken target graph instead of failing fast.

Suggested fix
 async function createNodesInternal(
   configFilePath: string,
   context: CreateNodesContextV2,
 ) {
   const projectConfiguration = readJsonFile<{
     name?: string
     nx?: { metadata?: { playwrightShards?: number } }
   }>(configFilePath)
   const root = dirname(configFilePath)
+  const shardCount = projectConfiguration.nx?.metadata?.playwrightShards
 
-  if (!projectConfiguration.nx?.metadata?.playwrightShards) {
+  if (shardCount == null) {
     return {
       projects: {
         [root]: {},
       },
     }
   }
+
+  if (!Number.isInteger(shardCount) || shardCount < 1) {
+    throw new Error(
+      `${configFilePath}: nx.metadata.playwrightShards must be a positive integer`,
+    )
+  }
 
   const { targets, targetGroupEntries } = buildShardedTargets(
     root,
     projectConfiguration.name ?? root,
-    projectConfiguration.nx.metadata.playwrightShards,
+    shardCount,
     ['default', '^production'],
   )
As per coding guidelines, `**/*.{ts,tsx}`: Use TypeScript strict mode with extensive type safety.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/nx/playwright-plugin.ts` around lines 26 - 45, Validate that
projectConfiguration.nx?.metadata?.playwrightShards is a positive integer before
calling buildShardedTargets: read it into a local const (e.g. const shards =
projectConfiguration.nx?.metadata?.playwrightShards), check typeof shards ===
'number' && Number.isInteger(shards) && shards > 0, and if the check fails throw
a descriptive Error (including configFilePath and the property name) or return
the non-sharded fallback; then pass the validated shards variable into
buildShardedTargets instead of using the raw property.

Comment on lines +40 to +43
const { targets, targetGroupEntries } = buildShardedTargets(
root,
projectConfiguration.name ?? root,
projectConfiguration.nx.metadata.playwrightShards,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Make E2E_PORT_KEY filesystem-safe before deriving marker-file names.

packageJson.name can be scoped, and the fallback root definitely contains path separators. That value eventually becomes port-${E2E_PORT_KEY}.txt, so names like @scope/pkg or e2e/react-start/rsc resolve to nested paths instead of a single port marker file.

Suggested fix
 function buildShardedTargets(
   projectRoot: string,
   packageName: string,
   shardCount: number,
   testInputs: TargetConfiguration['inputs'],
 ): {
   targets: Record<string, TargetConfiguration>
   targetGroupEntries: Array<string>
 } {
   const targets: Record<string, TargetConfiguration> = {}
   const targetGroup: Array<string> = []
+  const safePackageName = packageName.replace(/[\\/]/g, '-')
 
   // Create individual shard targets
   for (let shardIndex = 1; shardIndex <= shardCount; shardIndex++) {
     const shardTargetName = `${CI_TARGET_NAME}--shard-${shardIndex}-of-${shardCount}`
-    const e2ePortKey = `${packageName}-shard-${shardIndex}-of-${shardCount}`
+    const e2ePortKey = `${safePackageName}-shard-${shardIndex}-of-${shardCount}`

Also applies to: 76-88

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/nx/playwright-plugin.ts` around lines 40 - 43, Sanitize the value
used for E2E_PORT_KEY before it's used to build marker filenames (e.g., the
`port-${E2E_PORT_KEY}.txt` output) so scoped package names and path-like roots
don't create nested filesystem paths; replace or normalize characters such as
"/" and "@" (and any platform path separators) with a safe delimiter (e.g., "-")
or use a basename-style extraction. Update the code paths where E2E_PORT_KEY is
derived (the call site around `buildShardedTargets(root,
projectConfiguration.name ?? root,
projectConfiguration.nx.metadata.playwrightShards, ...)` and the other
occurrences around lines 76-88 that generate the marker file names) to use the
sanitized key so marker files are created as single flat filenames.

@beaussan beaussan merged commit df61199 into main Apr 14, 2026
7 checks passed
@beaussan beaussan deleted the atomize-playwright branch April 14, 2026 11:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant