React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

gherkincucumberplaywrightgithub-actionsci-cd

Gherkin to Deploy Pipeline

Connect Gherkin .feature files to automated Playwright tests, run them in CI, publish pass/fail reports, and gate deployments -- so no code ships unless every requirement passes.

The Full Flow

StepStageDetail
1Product RequirementsDefine what to build
2User Story in GitHub Issues@REQ-xxx tagged acceptance criteria
3Gherkin .feature fileCucumber parses scenarios
4Playwright + Cucumber step defsBrowser automation runs tests
5GitHub Actions pipeline runsOn every PR and push to main
6Publish pass/fail reportPer scenario, per requirement
7Allow or block deploydeploy job needs: [test] gate

Each stage is traceable. A failing scenario names the requirement it covers, the user story it belongs to, and the exact behavior that broke.


1. Product Requirements to User Stories

Requirements start as GitHub Issues. Each issue gets a requirement ID in the title and acceptance criteria written as Gherkin scenarios.

GitHub Issue title: "REQ-101: Profile Form Validation" Labels: requirement, priority: high

As a Cloud Architect I want the profile form to validate my inputs before submission So that I get immediate feedback and don't submit incomplete data

Acceptance Criteria (Gherkin)

  • Required fields show errors when empty
  • Email format is validated
  • LinkedIn URL must point to linkedin.com
  • Years of experience must be 0-50
  • Job end date must be after start date

See: features/profile-form/04-validation.feature


2. Gherkin Feature Files with Requirement Tags

Every scenario links back to its requirement via @REQ-xxx tags. Cucumber collects these tags for reporting.

# features/profile-form/04-validation.feature
 
@REQ-101
Feature: Profile Form Validation
  As a Cloud Architect
  I want the form to validate my inputs
  So that I get immediate feedback on errors
 
  Background:
    Given the architect is logged in
    And they navigate to "/profile/create"
 
  @REQ-101 @critical
  Scenario: Required fields show errors when empty
    Given the architect is on step 1 "Personal Info"
    And they have not filled in any fields
    When they click "Next"
    Then they should see "Full name is required"
    And they should see "Email is required"
    And the form should not advance to step 2
 
  @REQ-101
  Scenario: Email format is validated
    Given the architect is on step 1 "Personal Info"
    When they type "not-an-email" in the "Email" field
    And they click "Next"
    Then they should see "Please enter a valid email address"
 
  @REQ-101
  Scenario: LinkedIn URL must point to linkedin.com
    Given the architect is on step 1 "Personal Info"
    When they type "https://twitter.com/someone" in the "LinkedIn URL" field
    And they click "Next"
    Then they should see "Must be a valid LinkedIn profile URL"
 
  @REQ-102
  Scenario: Years of experience must be between 0 and 50
    Given the architect is on step 2 "Experience"
    When they type "-3" in the "Years of Experience" field
    And they click "Next"
    Then they should see "Must be between 0 and 50 years"
 
  @REQ-102
  Scenario: Job end date must be after start date
    Given the architect is on step 3 "Job History"
    When they set start date to "2025-06-01"
    And they set end date to "2024-01-01"
    And they click "Next"
    Then they should see "End date must be after start date"
# features/profile-form/08-file-uploads.feature
 
@REQ-201
Feature: File Upload Protection
  As a Cloud Architect
  I want the form to reject invalid files
  So that only safe, correct images are uploaded
 
  @REQ-201 @critical
  Scenario: Profile photo rejects oversized file
    Given the architect is on step 5 "Uploads"
    When they select a 15MB file for "Profile Photo"
    Then they should see "File must be under 5MB"
 
  @REQ-201 @critical
  Scenario: Profile photo rejects non-image files
    Given the architect is on step 5 "Uploads"
    When they select a .exe file for "Profile Photo"
    Then they should see "Only JPEG, PNG, and WebP files are accepted"
 
  @REQ-201
  Scenario: Maximum file count enforced
    Given the architect has uploaded 10 architecture diagrams
    When they try to add another file
    Then they should see "Maximum 10 files allowed"
# features/profile-form/03-multi-step.feature
 
@REQ-301
Feature: Multi-Step Navigation
  As a Cloud Architect
  I want to navigate between form steps
  So that I can fill out the form at my own pace
 
  @REQ-301 @critical
  Scenario: Completing step 1 unlocks step 2
    Given the architect fills all required fields on step 1
    When they click "Next"
    Then step 2 "Experience" should be active
    And step 1 should show a checkmark
 
  @REQ-301
  Scenario: Navigating back preserves data
    Given the architect is on step 3 "Job History"
    And they have entered "Acme Corp" as company name
    When they click "Back"
    And they click "Next"
    Then the company name field should still contain "Acme Corp"
 
  @REQ-301
  Scenario: Cannot skip ahead past invalid steps
    Given the architect is on step 1 "Personal Info"
    And the "Full Name" field is empty
    When they click step 3 in the progress indicator
    Then step 1 should remain active

3. Install Playwright + Cucumber

Set up the toolchain that connects .feature files to browser automation.

npm install -D @playwright/test playwright @cucumber/cucumber @badeball/cypress-cucumber-preprocessor
# We use @cucumber/cucumber for the Gherkin parser, not Cypress
# For Playwright-native Cucumber, use playwright-bdd:
npm install -D playwright-bdd

playwright-bdd is the recommended bridge -- it parses .feature files and generates Playwright test files automatically.

playwright.config.ts

// playwright.config.ts
import { defineConfig, devices } from "@playwright/test";
import { defineBddConfig } from "playwright-bdd";
 
const testDir = defineBddConfig({
  features: "features/**/*.feature",
  steps: "features/step-definitions/**/*.ts",
});
 
export default defineConfig({
  testDir,
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: [
    ["html", { open: "never" }],
    ["json", { outputFile: "test-results/results.json" }],
    // Cucumber JSON reporter for requirement tracking
    ["junit", { outputFile: "test-results/junit.xml" }],
  ],
  use: {
    baseURL: "http://localhost:3000",
    trace: "on-first-retry",
    screenshot: "only-on-failure",
  },
  projects: [
    { name: "chromium", use: { ...devices["Desktop Chrome"] } },
    { name: "mobile", use: { ...devices["iPhone 14"] } },
  ],
  webServer: {
    command: "npm run dev",
    port: 3000,
    reuseExistingServer: !process.env.CI,
  },
});

4. Step Definitions -- Connect Gherkin to Playwright

Step definitions are the glue. Each Given/When/Then in the feature file matches a step definition that drives the browser.

// features/step-definitions/navigation-steps.ts
import { createBdd } from "playwright-bdd";
import { expect } from "@playwright/test";
 
const { Given, When, Then } = createBdd();
 
// ── Background steps ───────────────────────────────────
 
Given("the architect is logged in", async ({ page }) => {
  // Set auth cookie or use storageState
  await page.goto("/api/test/login");
});
 
Given("they navigate to {string}", async ({ page }, url: string) => {
  await page.goto(url);
});
 
// ── Step navigation ────────────────────────────────────
 
Given(
  "the architect is on step {int} {string}",
  async ({ page }, step: number, _name: string) => {
    await page.goto("/profile/create");
    for (let i = 1; i < step; i++) {
      await fillStepWithValidData(page, i);
      await page.getByTestId("btn-next").click();
    }
    await expect(
      page.getByTestId(`step-${step}`)
    ).toHaveAttribute("data-status", "current");
  }
);
 
Given(
  "the architect fills all required fields on step {int}",
  async ({ page }, step: number) => {
    await fillStepWithValidData(page, step);
  }
);
 
When("they click {string}", async ({ page }, buttonText: string) => {
  await page.getByRole("button", { name: buttonText }).click();
});
 
When(
  "they click step {int} in the progress indicator",
  async ({ page }, step: number) => {
    await page.getByTestId(`step-${step}`).click();
  }
);
 
Then(
  "step {int} {string} should be active",
  async ({ page }, step: number, _name: string) => {
    await expect(
      page.getByTestId(`step-${step}`)
    ).toHaveAttribute("data-status", "current");
  }
);
 
Then("step {int} should show a checkmark", async ({ page }, step: number) => {
  await expect(
    page.getByTestId(`step-${step}`)
  ).toHaveAttribute("data-status", "completed");
});
 
Then("step {int} should remain active", async ({ page }, step: number) => {
  await expect(
    page.getByTestId(`step-${step}`)
  ).toHaveAttribute("data-status", "current");
});
 
// ── Helper ─────────────────────────────────────────────
 
async function fillStepWithValidData(page: import("@playwright/test").Page, step: number) {
  switch (step) {
    case 1:
      await page.getByTestId("field-fullName").fill("Jane Doe");
      await page.getByTestId("field-email").fill("jane@example.com");
      break;
    case 2:
      await page.getByTestId("field-yearsOfExperience").fill("8");
      await page.getByTestId("field-currentRole").fill("Principal Architect");
      await page.getByTestId("cert-aws-solutions-architect-professional").click();
      break;
    case 3:
      await page.getByTestId("job-0-company").fill("Acme Corp");
      await page.getByTestId("job-0-role").fill("Cloud Architect");
      await page.getByTestId("job-0-start").fill("2020-01-01");
      await page.getByTestId("job-0-current").click();
      break;
    case 4:
      await page.getByTestId("platform-aws").click();
      await page.getByTestId("specialty-serverless").click();
      break;
    case 5:
      // Uploads are optional -- skip
      break;
  }
}
// features/step-definitions/validation-steps.ts
import { createBdd } from "playwright-bdd";
import { expect } from "@playwright/test";
 
const { Given, When, Then } = createBdd();
 
When(
  "they type {string} in the {string} field",
  async ({ page }, value: string, fieldLabel: string) => {
    const field = page.getByLabel(fieldLabel, { exact: false });
    await field.clear();
    await field.fill(value);
  }
);
 
When(
  "they set start date to {string}",
  async ({ page }, date: string) => {
    await page.getByTestId("job-0-start").fill(date);
  }
);
 
When(
  "they set end date to {string}",
  async ({ page }, date: string) => {
    await page.getByTestId("job-0-end").fill(date);
  }
);
 
Given(
  "they have not filled in any fields",
  async () => {
    // No-op -- fields are empty by default
  }
);
 
Given(
  "the {string} field is empty",
  async () => {
    // No-op -- field is empty by default
  }
);
 
Then(
  "they should see {string}",
  async ({ page }, errorText: string) => {
    await expect(page.getByText(errorText)).toBeVisible();
  }
);
 
Then(
  "the form should not advance to step {int}",
  async ({ page }, step: number) => {
    await expect(
      page.getByTestId(`step-${step}`)
    ).not.toHaveAttribute("data-status", "current");
  }
);
// features/step-definitions/upload-steps.ts
import { createBdd } from "playwright-bdd";
import { expect } from "@playwright/test";
 
const { Given, When, Then } = createBdd();
 
When(
  "they select a {int}MB file for {string}",
  async ({ page }, sizeMB: number, fieldLabel: string) => {
    const testId = fieldLabel.toLowerCase().replace(/\s+/g, "");
    const input = page.getByTestId(`upload-${testId}`).locator("input[type=file]");
    await input.setInputFiles({
      name: "test-file.jpg",
      mimeType: "image/jpeg",
      buffer: Buffer.alloc(sizeMB * 1024 * 1024),
    });
  }
);
 
When(
  "they select a .exe file for {string}",
  async ({ page }, fieldLabel: string) => {
    const testId = fieldLabel.toLowerCase().replace(/\s+/g, "");
    const input = page.getByTestId(`upload-${testId}`).locator("input[type=file]");
    await input.setInputFiles({
      name: "malware.exe",
      mimeType: "application/x-msdownload",
      buffer: Buffer.alloc(1024),
    });
  }
);
 
Given(
  "the architect has uploaded {int} architecture diagrams",
  async ({ page }, count: number) => {
    const input = page
      .getByTestId("upload-architectureDiagrams")
      .locator("input[type=file]");
    for (let i = 0; i < count; i++) {
      await input.setInputFiles({
        name: `diagram-${i + 1}.png`,
        mimeType: "image/png",
        buffer: Buffer.alloc(1024),
      });
    }
  }
);
 
When("they try to add another file", async ({ page }) => {
  const input = page
    .getByTestId("upload-architectureDiagrams")
    .locator("input[type=file]");
  await input.setInputFiles({
    name: "one-more.png",
    mimeType: "image/png",
    buffer: Buffer.alloc(1024),
  });
});
// features/step-definitions/data-persistence-steps.ts
import { createBdd } from "playwright-bdd";
import { expect } from "@playwright/test";
 
const { Given, Then } = createBdd();
 
Given(
  "they have entered {string} as company name",
  async ({ page }, company: string) => {
    await page.getByTestId("job-0-company").fill(company);
  }
);
 
Then(
  "the company name field should still contain {string}",
  async ({ page }, expected: string) => {
    await expect(page.getByTestId("job-0-company")).toHaveValue(expected);
  }
);

5. Generate Tests from Features

playwright-bdd reads your .feature files and step definitions, then generates standard Playwright test files. You never write Playwright tests manually -- they come from Gherkin.

# Generate Playwright tests from .feature files
npx bddgen
 
# Run the generated tests
npx playwright test
 
# Run only scenarios tagged @critical
npx playwright test --grep "@critical"
 
# Run only REQ-101 scenarios
npx playwright test --grep "@REQ-101"

The generated test files look like this (you do not edit these):

// .features-gen/profile-form/04-validation.feature.spec.ts (auto-generated)
import { test } from "playwright-bdd";
 
test.describe("Profile Form Validation @REQ-101", () => {
  test.beforeEach(async ({ Given, page }) => {
    await Given("the architect is logged in");
    await Given('they navigate to "/profile/create"');
  });
 
  test("Required fields show errors when empty @REQ-101 @critical", async ({
    Given, When, Then
  }) => {
    await Given('the architect is on step 1 "Personal Info"');
    await Given("they have not filled in any fields");
    await When('they click "Next"');
    await Then('they should see "Full name is required"');
    await Then('they should see "Email is required"');
    await Then("the form should not advance to step 2");
  });
 
  // ... more scenarios auto-generated from the .feature file
});

6. GitHub Actions Pipeline

The CI workflow runs on every PR and push to main. It generates tests from features, runs them, publishes reports, and gates the deploy.

# .github/workflows/gherkin-deploy.yml
name: Gherkin Test & Deploy Pipeline
 
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
 
permissions:
  contents: read
  checks: write
  pull-requests: write
 
jobs:
  # ── Stage 1: Generate and run Cucumber/Playwright tests ──
  test:
    name: Run Gherkin Tests
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
 
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm
 
      - name: Install dependencies
        run: npm ci
 
      - name: Install Playwright browsers
        run: npx playwright install --with-deps chromium
 
      - name: Generate tests from .feature files
        run: npx bddgen
 
      - name: Build application
        run: npm run build
 
      - name: Run Gherkin Playwright tests
        run: npx playwright test
        env:
          CI: true
 
      # ── Publish test results ──
      - name: Upload Playwright report
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 30
 
      - name: Upload test results JSON
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: test-results
          path: test-results/
          retention-days: 30
 
      # ── Post results to PR as comment ──
      - name: Parse test results and post PR comment
        if: always() && github.event_name == 'pull_request'
        uses: actions/github-script@v7
        with:
          script: |
            const fs = require('fs');
            const results = JSON.parse(
              fs.readFileSync('test-results/results.json', 'utf8')
            );
 
            // Count pass/fail per @REQ tag
            const reqMap = new Map();
            for (const suite of results.suites ?? []) {
              for (const spec of suite.specs ?? []) {
                const tags = spec.title.match(/@REQ-\d+/g) ?? [];
                const passed = spec.tests.every(t => t.status === 'expected');
                for (const tag of tags) {
                  if (!reqMap.has(tag)) reqMap.set(tag, { pass: 0, fail: 0, scenarios: [] });
                  const entry = reqMap.get(tag);
                  passed ? entry.pass++ : entry.fail++;
                  entry.scenarios.push({ title: spec.title, passed });
                }
              }
            }
 
            // Build markdown table
            let body = '## Gherkin Test Results\n\n';
            body += '| Requirement | Passed | Failed | Status |\n';
            body += '|-------------|--------|--------|--------|\n';
 
            for (const [req, data] of [...reqMap.entries()].sort()) {
              const status = data.fail === 0 ? '✅ Pass' : '❌ Fail';
              body += `| ${req} | ${data.pass} | ${data.fail} | ${status} |\n`;
            }
 
            const totalPass = [...reqMap.values()].reduce((s, d) => s + d.pass, 0);
            const totalFail = [...reqMap.values()].reduce((s, d) => s + d.fail, 0);
            body += `\n**Total: ${totalPass} passed, ${totalFail} failed**\n`;
            body += '\n📊 [Full Report](../actions/runs/' + context.runId + ')\n';
 
            // Post or update PR comment
            const { data: comments } = await github.rest.issues.listComments({
              ...context.repo,
              issue_number: context.issue.number,
            });
            const existing = comments.find(c =>
              c.body?.includes('## Gherkin Test Results')
            );
 
            if (existing) {
              await github.rest.issues.updateComment({
                ...context.repo,
                comment_id: existing.id,
                body,
              });
            } else {
              await github.rest.issues.createComment({
                ...context.repo,
                issue_number: context.issue.number,
                body,
              });
            }
 
      # ── Post JUnit results as GitHub Check ──
      - name: Publish JUnit results
        uses: mikepenz/action-junit-report@v4
        if: always()
        with:
          report_paths: test-results/junit.xml
          check_name: Gherkin BDD Results
          detailed_summary: true
          include_passed: true
 
  # ── Stage 2: Deploy (gated by test job) ──
  deploy:
    name: Deploy to Production
    needs: [test]
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    environment:
      name: production
      url: https://your-app.vercel.app
    steps:
      - uses: actions/checkout@v4
 
      - name: Deploy to Vercel
        run: npx vercel --prod --token=${{ secrets.VERCEL_TOKEN }}
        env:
          VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
          VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}

What the pipeline does

  1. npm ci -- clean install of dependencies
  2. npx bddgen -- reads .feature files + step definitions, generates Playwright test files
  3. npm run build -- builds the Next.js app
  4. npx playwright test -- runs all generated tests in headless Chromium
  5. Uploads artifacts -- HTML report + JSON results preserved for 30 days
  6. Posts PR comment -- markdown table showing pass/fail per @REQ-xxx tag
  7. Publishes JUnit check -- GitHub Check with detailed test results
  8. Deploy job -- only runs if test job passes (needs: [test])

7. Track Pass/Fail Per Requirement

The @REQ-xxx tags in your .feature files enable requirement-level tracking. The pipeline parses these from test results and reports them.

PR Comment Output

When the pipeline runs on a PR, it posts a comment like this:

Gherkin Test Results

RequirementPassedFailedStatus
@REQ-10130Pass
@REQ-10220Pass
@REQ-20121Fail
@REQ-30130Pass

Total: 10 passed, 1 failed

Tag Conventions

TagMeaningExample
@REQ-xxxLinks scenario to a requirement/user story@REQ-101
@criticalMust pass to deploy@critical @REQ-101
@smokeIncluded in smoke test suite@smoke @REQ-301
@wipWork in progress, skipped in CI@wip @REQ-401

Running by tag

# All critical scenarios
npx playwright test --grep "@critical"
 
# All scenarios for REQ-101
npx playwright test --grep "@REQ-101"
 
# Skip work-in-progress
npx playwright test --grep-invert "@wip"
 
# Smoke tests only (fast gate check)
npx playwright test --grep "@smoke"

8. Gate Deploys

The deploy job uses needs: [test] to block deployment when any scenario fails. This is the enforcement mechanism.

How the gate works

deploy:
  needs: [test]          # ← Will not run if test job fails
  if: github.ref == 'refs/heads/main'
  environment: production # ← Optional: require manual approval too

Critical-only gate (optional)

If you want to allow deploys when non-critical tests fail, add a separate critical-only job:

jobs:
  test-all:
    name: Run All Gherkin Tests
    runs-on: ubuntu-latest
    steps:
      # ... same setup ...
      - name: Run all tests
        run: npx playwright test
        continue-on-error: true  # Don't fail the job
 
      # ... publish reports ...
 
  test-critical:
    name: Run Critical Tests (Deploy Gate)
    runs-on: ubuntu-latest
    steps:
      # ... same setup ...
      - name: Run critical tests only
        run: npx playwright test --grep "@critical"
        # This job WILL fail if any @critical scenario fails
 
  deploy:
    needs: [test-critical]  # ← Only gated by critical tests
    # test-all results are informational only

Environment protection rules

For an additional layer, configure GitHub Environment protection:

  1. Go to Settings > Environments > production
  2. Add Required reviewers (optional manual approval)
  3. Add Deployment branch rules (only main)
  4. The environment: production in the workflow enforces these rules

Close the traceability loop by linking Gherkin tags back to GitHub Issues.

In the feature file

# Links to GitHub Issue #42 and requirement REQ-101
@REQ-101 @issue-42
Scenario: Required fields show errors when empty
  Given ...

In the GitHub Issue

REQ-101: Profile Form Validation

Linked Tests

  • features/profile-form/04-validation.feature scenarios 1-5
  • Pipeline: Latest run link

Status: All scenarios passing

Automated issue status update (optional)

Add this step to the pipeline to comment on linked issues when their scenarios fail:

- name: Notify linked issues on failure
  if: failure()
  uses: actions/github-script@v7
  with:
    script: |
      const fs = require('fs');
      const results = JSON.parse(
        fs.readFileSync('test-results/results.json', 'utf8')
      );
 
      // Find failed @issue-xxx tags
      const failedIssues = new Set();
      for (const suite of results.suites ?? []) {
        for (const spec of suite.specs ?? []) {
          const hasFail = spec.tests.some(t => t.status !== 'expected');
          if (!hasFail) continue;
          const issueTags = spec.title.match(/@issue-(\d+)/g) ?? [];
          for (const tag of issueTags) {
            failedIssues.add(parseInt(tag.replace('@issue-', '')));
          }
        }
      }
 
      // Comment on each failed issue
      for (const issueNum of failedIssues) {
        await github.rest.issues.createComment({
          ...context.repo,
          issue_number: issueNum,
          body: `⚠️ **BDD test failure** in run [#${context.runNumber}](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})\n\nOne or more Gherkin scenarios linked to this issue failed.`,
        });
      }

Complete Filetree

project/
├── features/
│   ├── profile-form/
│   │   ├── 01-purpose.feature         ← @REQ-001
│   │   ├── 02-field-types.feature     ← @REQ-002
│   │   ├── 03-multi-step.feature      ← @REQ-301
│   │   ├── 04-validation.feature      ← @REQ-101, @REQ-102
│   │   ├── 05-submission.feature      ← @REQ-401
│   │   ├── 06-ui-ux.feature           ← @REQ-501
│   │   ├── 07-accessibility.feature   ← @REQ-601
│   │   ├── 08-file-uploads.feature    ← @REQ-201
│   │   ├── 09-type-safety.feature     ← @REQ-701
│   │   └── 10-tech-stack.feature      ← @REQ-801
│   └── step-definitions/
│       ├── navigation-steps.ts
│       ├── validation-steps.ts
│       ├── upload-steps.ts
│       └── data-persistence-steps.ts
├── .features-gen/                      ← Auto-generated by bddgen (gitignored)
├── playwright.config.ts               ← Cucumber + Playwright config
├── .github/
│   └── workflows/
│       └── gherkin-deploy.yml         ← CI/CD pipeline
├── test-results/                       ← JSON + JUnit output (gitignored)
└── playwright-report/                  ← HTML report (gitignored)

Recipe

Quick-reference recipe card -- copy-paste ready.

# 1. Write .feature files with @REQ-xxx tags
#    features/profile-form/04-validation.feature
 
# 2. Write step definitions
#    features/step-definitions/validation-steps.ts
 
# 3. Generate Playwright tests from features
npx bddgen
 
# 4. Run locally
npx playwright test
 
# 5. Run specific requirement
npx playwright test --grep "@REQ-101"
 
# 6. Run critical-only (deploy gate)
npx playwright test --grep "@critical"
 
# 7. View HTML report
npx playwright show-report

When to reach for this: After you have working .feature files and step definitions. This doc shows how to wire them into CI/CD so they enforce requirements on every PR.

Gotchas

  • npx bddgen must run before npx playwright test -- the generated test files in .features-gen/ do not exist until you run the generator. Add it as a CI step before the test step.

  • .features-gen/ should be gitignored -- these are generated files. Committing them causes merge conflicts and stale tests.

  • Step definition parameter types matter -- {int} matches numbers, {string} matches quoted strings. Mismatched parameter types cause "step not found" errors that are hard to debug.

  • Tag filtering uses --grep not --tags -- Playwright uses --grep for filtering, not Cucumber's --tags syntax. @REQ-101 becomes --grep "@REQ-101".

  • continue-on-error: true vs if: always() -- use continue-on-error on the test step to allow report publishing. Use if: always() on artifact upload steps. Don't confuse them or you'll miss failures.

  • JSON reporter path must match the parsing script -- the results.json path in playwright.config.ts must match what the GitHub Actions script reads. Both default to test-results/results.json.

  • Environment protection is separate from job needs -- needs: [test] blocks the deploy job. Environment protection rules add manual approval. Use both for production deploys.

Alternatives

AlternativeUse WhenDon't Use When
Cucumber.js standalone (no Playwright)API-only testing without a browserYou need to test real browser behavior
Playwright without CucumberDev team prefers writing tests directlyStakeholders need to read and approve scenarios
Cypress + cypress-cucumber-preprocessorTeam already uses CypressStarting fresh (Playwright has better CI perf)
SpecFlow (.NET)Backend is .NETJavaScript/TypeScript stack
GitLab CI instead of GitHub ActionsUsing GitLabUsing GitHub