IKivan-kozenko -aqa
Try yourself as a QA tester
All topics·Advanced·5 / 24

Sharding

When I hit 800+ e2e tests, parallel workers on one machine stopped being enough. Sharding splits your test suite across multiple CI machines so they run simultaneously. The setup is two lines of YAML — the reporting part takes a bit more work to wire up properly.

The --shard flag

Sharding distributes tests across CI machines; blob reports are merged into one HTML report after all shards finish

One flag, and each CI job runs its own slice. If I have 4 machines, I run these four commands in parallel — each machine picks up its quarter of the test suite and ignores the rest.

Playwright distributes by test file by default. So if shard 1/4 gets 10 files and shard 2/4 gets 2 files, the timing will be uneven. That's where fullyParallel helps.

bash
npx playwright test --shard=1/4
npx playwright test --shard=2/4
npx playwright test --shard=3/4
npx playwright test --shard=4/4

Getting even distribution — fullyParallel

Without fullyParallel, Playwright splits at the file level. A file with 50 tests counts the same as a file with 2 tests. One shard ends up doing most of the work while others finish early and sit idle.

With fullyParallel: true, Playwright splits at the individual test level. 400 tests across 4 shards = ~100 tests per shard, regardless of how they're distributed across files. I always use this when sharding.

ts
// playwright.config.ts
export default defineConfig({
  fullyParallel: true,  // ← тест-рівневий розподіл, а не файл-рівневий
  reporter: process.env.CI ? 'blob' : 'html',
})

Blob reporter — collecting results from all shards

Each shard produces its own test report. To get one combined report after all shards finish, I use the blob reporter on CI. It saves raw test data (including traces, screenshots, all attachments) to a zip file that can be merged later.

After downloading all blob reports from CI artifacts into one directory, I merge them into a single HTML report. The blob file names include the shard number so they never conflict.

bash
# Злити blob-звіти з усіх шардів в один HTML-звіт
npx playwright merge-reports --reporter html ./all-blob-reports

GitHub Actions setup — matrix strategy

GitHub Actions matrix lets each shard run as an independent job. I define shardIndex as an array and shardTotal as a fixed number — GitHub spawns one job per index value, each referencing both variables.

The artifact upload step has if: ${{ !cancelled() }} — without this, if a test fails (the job fails), the artifact upload is skipped and you have no report to look at. This condition runs the upload even when the job failed.

Then a separate merge-reports job waits for all shards via needs: [playwright-tests], downloads all blob artifacts, and merges them into one HTML report.

yaml
name: Playwright Tests
on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]
jobs:
  playwright-tests:
    timeout-minutes: 60
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        shardIndex: [1, 2, 3, 4]
        shardTotal: [4]
    steps:
    - uses: actions/checkout@v5
    - uses: actions/setup-node@v5
      with:
        node-version: lts/*
    - name: Install dependencies
      run: npm ci
    - name: Install Playwright browsers
      run: npx playwright install --with-deps
    - name: Run Playwright tests
      run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
    - name: Upload blob report
      if: ${{ !cancelled() }}
      uses: actions/upload-artifact@v4
      with:
        name: blob-report-${{ matrix.shardIndex }}
        path: blob-report
        retention-days: 1
yaml
  merge-reports:
    if: ${{ !cancelled() }}
    needs: [playwright-tests]
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v5
    - uses: actions/setup-node@v5
      with:
        node-version: lts/*
    - name: Install dependencies
      run: npm ci
    - name: Download blob reports
      uses: actions/download-artifact@v5
      with:
        path: all-blob-reports
        pattern: blob-report-*
        merge-multiple: true
    - name: Merge into HTML Report
      run: npx playwright merge-reports --reporter html ./all-blob-reports
    - name: Upload HTML report
      uses: actions/upload-artifact@v4
      with:
        name: html-report--attempt-${{ github.run_attempt }}
        path: playwright-report
        retention-days: 14

Merging reports from different environments

If I run the same tests against staging and production simultaneously, I tag each run with the environment name via TestConfig.tag. The blob report picks up this tag automatically, so the merged report shows which results came from which environment.

ts
// playwright.config.ts
export default defineConfig({
  reporter: process.env.CI ? 'blob' : 'html',
  tag: process.env.CI_ENVIRONMENT_NAME,  // наприклад "@staging" або "@production"
})