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
| Step | Stage | Detail |
|---|---|---|
| 1 | Product Requirements | Define what to build |
| 2 | User Story in GitHub Issues | @REQ-xxx tagged acceptance criteria |
| 3 | Gherkin .feature file | Cucumber parses scenarios |
| 4 | Playwright + Cucumber step defs | Browser automation runs tests |
| 5 | GitHub Actions pipeline runs | On every PR and push to main |
| 6 | Publish pass/fail report | Per scenario, per requirement |
| 7 | Allow or block deploy | deploy 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 active3. 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-bddplaywright-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
npm ci-- clean install of dependenciesnpx bddgen-- reads.featurefiles + step definitions, generates Playwright test filesnpm run build-- builds the Next.js appnpx playwright test-- runs all generated tests in headless Chromium- Uploads artifacts -- HTML report + JSON results preserved for 30 days
- Posts PR comment -- markdown table showing pass/fail per
@REQ-xxxtag - Publishes JUnit check -- GitHub Check with detailed test results
- Deploy job -- only runs if
testjob 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
Requirement Passed Failed Status @REQ-101 3 0 Pass @REQ-102 2 0 Pass @REQ-201 2 1 Fail @REQ-301 3 0 Pass Total: 10 passed, 1 failed
Tag Conventions
| Tag | Meaning | Example |
|---|---|---|
@REQ-xxx | Links scenario to a requirement/user story | @REQ-101 |
@critical | Must pass to deploy | @critical @REQ-101 |
@smoke | Included in smoke test suite | @smoke @REQ-301 |
@wip | Work 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 tooCritical-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 onlyEnvironment protection rules
For an additional layer, configure GitHub Environment protection:
- Go to Settings > Environments > production
- Add Required reviewers (optional manual approval)
- Add Deployment branch rules (only
main) - The
environment: productionin the workflow enforces these rules
9. Link Scenarios to User Stories
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.featurescenarios 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-reportWhen 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 bddgenmust run beforenpx 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
--grepnot--tags-- Playwright uses--grepfor filtering, not Cucumber's--tagssyntax.@REQ-101becomes--grep "@REQ-101". -
continue-on-error: truevsif: always()-- usecontinue-on-erroron the test step to allow report publishing. Useif: always()on artifact upload steps. Don't confuse them or you'll miss failures. -
JSON reporter path must match the parsing script -- the
results.jsonpath inplaywright.config.tsmust match what the GitHub Actions script reads. Both default totest-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
| Alternative | Use When | Don't Use When |
|---|---|---|
| Cucumber.js standalone (no Playwright) | API-only testing without a browser | You need to test real browser behavior |
| Playwright without Cucumber | Dev team prefers writing tests directly | Stakeholders need to read and approve scenarios |
| Cypress + cypress-cucumber-preprocessor | Team already uses Cypress | Starting fresh (Playwright has better CI perf) |
| SpecFlow (.NET) | Backend is .NET | JavaScript/TypeScript stack |
| GitLab CI instead of GitHub Actions | Using GitLab | Using GitHub |
Related
- Gherkin Form Decision Checklist -- write the Gherkin scenarios
- Gherkin to Code -- implement the form code
- Filetree Example -- every file in the working form
- Playwright Setup -- Playwright configuration basics
- Playwright Patterns -- locator and assertion patterns