A portfolio-focused QA automation project modelled on Booking.com, using Gherkin BDD, Playwright (TypeScript), Cypress (JavaScript), and GitHub Actions CI/CD.
Execution model: the mock API suite is deterministic and runs for real. The local teaching UI and mock API run together for manual browser checks. Supported UI automation runs for real against the local teaching stand; it never visits Booking.com. Experimental scenarios remain visible under
@WIPbut are deliberately excluded from quality gates.
| Layer | Tool | Language |
|---|---|---|
| Test design & specification | Gherkin / Cucumber | — |
| E2E & API automation | Playwright | TypeScript |
| E2E & API automation | Cypress | JavaScript |
| SQL learning examples (not DB tests) | SQL | PostgreSQL syntax |
| Lightweight API checks | cURL | Bash |
| CI/CD quality gates | GitHub Actions | YAML |
scripts/
├── ai-generate-feature.ts # AI feature generator (Claude API or offline mock)
└── validate-generated-draft.js # parser + explicit dry-run step matching
tests/
├── manual/
│ └── features/
│ ├── api/ # executable API specifications
│ │ ├── 01_login_success.feature
│ │ ├── 02_login_failure.feature
│ │ ├── 03_search_city.feature
│ │ ├── 04_search_dates.feature
│ │ ├── 05_price_filter.feature
│ │ ├── 06_currency_conversion.feature
│ │ ├── 07_add_to_wishlist.feature
│ │ ├── 08_remove_from_wishlist.feature
│ │ ├── 09_view_details.feature
│ │ ├── 10_get_reviews.feature
│ │ ├── 11_hotel_review_crud.feature ← Full CRUD lifecycle
│ │ └── ai_reviews_ep.feature ← 🤖 AI-generated (EP)
│ ├── e2e/ # UI specifications
│ ├── booking.feature
│ ├── language_switch.feature
│ ├── currency_switch.feature
│ ├── search_filters.feature
│ ├── invalid_login.feature
│ ├── form_validation.feature
│ ├── bva_search_boundaries.feature ← Boundary Value Analysis
│ ├── ep_login_credentials.feature ← Equivalence Partitioning
│ ├── decision_table_search_filters.feature ← Decision Table
│ ├── state_transition_booking_flow.feature ← State Transition
│ ├── ai_price_filter_currency_switch.feature ← 🤖 AI-generated (Decision Table, WIP)
│ ├── ai_wishlist_state_transition.feature ← 🤖 AI-generated (State Transition, WIP)
│ └── ai_guest_count_bva.feature ← 🤖 AI-generated (BVA, WIP)
│ └── drafts/{api,e2e}/ # new generated drafts, always @WIP
└── automation/
├── api/
│ ├── playwright/ # Playwright API step definitions (TS)
│ ├── cypress/ # Cypress API step definitions (JS)
│ ├── curl/ # 10 cURL scripts with response validation
│ └── sql/ # 10 SQL queries (JOINs, CTEs, window fns)
└── e2e/
├── playwright/
│ ├── pages/ # Page Object Model (TypeScript)
│ └── steps/ # Step definitions (TypeScript)
├── cypress/
│ ├── pages/ # Page Object Model (JavaScript)
│ └── steps/ # Step definitions (JavaScript)
└── fixtures/
└── world.ts # Playwright browser/request lifecycle
This project demonstrates six core ISTQB test design techniques:
| Technique | Feature file | Coverage |
|---|---|---|
| Boundary Value Analysis | bva_search_boundaries.feature |
Guest count (0–31), stay duration |
| Equivalence Partitioning | ep_login_credentials.feature |
Email format classes, password classes |
| Decision Table | decision_table_search_filters.feature |
Filter combinations → expected outcomes |
| State Transition | state_transition_booking_flow.feature |
Full booking lifecycle state machine |
| Scenario Outline / Pairwise | booking.feature, invalid_login.feature |
Multi-city search, multi-credential sets |
| Error Guessing | 02_login_failure.feature, form_validation.feature |
Invalid inputs, empty fields |
| Suite | Tag | When | Scope |
|---|---|---|---|
| Acceptance | @Acceptance |
Every push to main |
Core happy paths |
| Smoke | @Smoke |
Nightly (sequential after Acceptance) | Key critical flows |
| Regression | @Regression |
Nightly (sequential after Smoke) | Full coverage |
| Work In Progress | @WIP |
Never in CI (excluded) | Features in development |
| # | Endpoint | Method | Test type |
|---|---|---|---|
| 01 | /api/login |
POST | Auth success, token present |
| 02 | /api/login |
POST | Auth failure, 401 response |
| 03 | /api/hotels?city= |
GET | City search, result set validation |
| 04 | /api/hotels?city=&checkin=&checkout= |
GET | Date-filtered availability |
| 05 | /api/hotels?min_price=&max_price= |
GET | Price range filter |
| 06 | /api/hotels?currency= |
GET | Currency conversion (USD/EUR/GBP) |
| 07 | /api/wishlist |
POST | Create wishlist entry |
| 08 | /api/wishlist/{id} |
DELETE | Remove wishlist entry |
| 09 | /api/hotel/{id} |
GET | Hotel detail completeness |
| 10 | /api/hotel/{id}/reviews |
GET | Review list with user & rating |
| 11 | /api/hotel/{id}/reviews/{id} |
POST/GET/PUT/DELETE | Full CRUD lifecycle |
- Node.js 22.23.2 (pinned in
.nvmrc; CI reads the same file) - npm 10.9.x (validated with 10.9.8, bundled with this Node.js release)
git clone https://github.com/yaoleshchuk/qa-pet-project.git
cd qa-pet-project
nvm install
nvm use
npm ci
npx playwright install chromium --with-depsnvm is optional: another version manager may install the exact version in
.nvmrc. Browser installation is not needed for the API-only Cucumber suites.
The staged improvement plan is in docs/improvement-plan.md. See the stage 0 baseline for the historical baseline, and the stage 1 integration evidence for checks against the current PR base.
No .env, credentials, external service, global jq, or global Allure install is
needed for the supported local commands. npx --no-install and npm scripts use the
locked project dependencies. Do not point mock API commands at Booking.com.
# Validate suite structure without a browser (fast, used in CI).
# The command exits non-zero if any non-WIP step is undefined.
npm run test:pw:dry-run
# Run by suite (dry-run)
npm run test:pw:acceptance
npm run test:pw:smoke
npm run test:pw:regression# 1. Start the mock API server (Express, port 3001)
npm run mock:start
# 2. In a separate terminal — run tests by suite
npm run test:api:all # all implemented @API scenarios
npm run test:api:acceptance # @API + @Acceptance
npm run test:api:smoke # @API + @Smoke
npm run test:api:regression # @API + @Regression
# 3. Generate & open the Allure HTML report
npm run allure:generate
npm run allure:openWait for readiness before another terminal runs a command:
curl --fail --silent http://127.0.0.1:3001/healthStop only the npm run mock:start process started for this task with Ctrl-C.
Each test:api:* run first removes reports/api/latest/, then writes its JSON and
Allure results there. Thus allure:generate always consumes one current real run,
never an old top-level allure-results/ directory.
The mock server simulates all Booking.com API endpoints with realistic seed data, enabling fully deterministic test results without network access.
# Starts one process on http://localhost:3001, waits up to 10 seconds for
# /health, and forwards Ctrl-C/SIGTERM to stop the process cleanly.
npm run ui:localOpen http://localhost:3001. The UI has accessible labels and stable
data-testid values for the stage-6 automation work. It searches city/dates and
guests, receives availability, price ranges, and converted currencies from the
mock API, and provides client-side amenity/star filters and numeric price sort on
the received response. It never calculates exchange rates or availability in the
browser.
Test-only credentials are testuser@example.com / correct_password. A successful
login stores only the display user in tab-scoped sessionStorage; Sign out clears
that local display session. Wishlist state can be reloaded while signed in, but belongs
to the running mock process and is not tied to an authenticated user. Restart the
process, or run
curl -X POST http://localhost:3001/api/test/reset, to restore the seed state.
The search form visibly states that guests are validated only from 1 to 30: the
existing API has no guest-capacity parameter, so this input does not change
availability and the UI does not claim otherwise. Bookings, checkout and cancellation
are intentionally not implemented.
To use a different isolated port: MOCK_PORT=13025 npm run ui:local. Set
UI_READY_TIMEOUT_MS (milliseconds) only when a longer readiness wait is needed.
These commands start their own local mock, wait for /health, and stop only that
child process. They need the Chromium install from Setup; do not start another server.
npm run test:ui:playwright
npm run test:ui:cypressThe generic legacy Cypress commands remain configuration-level commands and are not coverage evidence for the teaching model; use the two commands above for real UI runs.
Start npm run mock:start, verify /health as above, then run one of the API-only
commands below. They use cypress.api.config.js, load only shared @API Gherkin
features, and reject a non-local API URL; they never visit Booking.com.
npm run test:cypress:api:all
npm run test:cypress:api:acceptance
npm run test:cypress:api:smoke
npm run test:cypress:api:regression# Starts a private mock on 127.0.0.1:3218, waits for /health, runs all ten
# checks, then stops only that child process.
npm run test:curl:smokeEach script uses the documented mock contract, verifies the HTTP status, parses JSON
with the repository's Node.js runtime, uses connect/total timeouts (5/15 seconds),
resets its own test state, and cleans its wishlist mutation. The scripts require the
runner's CURL_DEDICATED_MOCK=1 marker, so they cannot reset an API/UI server owned by
another run. Override CURL_TEST_PORT, CURL_CONNECT_TIMEOUT, or CURL_MAX_TIME
when diagnosing; BASE_URL is set by the runner and remains local.
The scripts have been run with macOS Bash and use Bash 3-compatible syntax; Linux is
documented-compatible but is not claimed as locally executed evidence.
Rows are groups, not additive scenario totals: the same API or UI Gherkin scenario may appear in more than one runner. “Real” means steps/browser requests execute; “dry-run” checks matching only.
| Scenario/group | Tool | Command | Coverage status |
|---|---|---|---|
Non-@WIP Gherkin selected by default/Acceptance/Smoke/Regression tags |
Cucumber + Playwright definitions | npm run test:pw:dry-run, test:pw:acceptance, test:pw:smoke, test:pw:regression |
Strict dry-run only; non-empty current JSON and no undefined/ambiguous step. No browser or API execution. |
Implemented @API and not @WIP scenarios |
Playwright API + local mock | npm run mock:start; then npm run test:api:all (or a tagged suite) |
Real API coverage; isolated in-memory mock and fresh JSON/Allure directory. |
| Same implemented API set | Cypress API + local mock | npm run mock:start; then npm run test:cypress:api:all |
Real API coverage; CI compares passed identities with Playwright. Overlap, not extra unique scenarios. |
Supported @LocalUI and not @WIP UI group (22 Examples/scenarios) |
Playwright UI + local UI | npm run test:ui:playwright |
Real browser run; rejects empty/non-passed JSON and retains diagnostics. |
| Same supported UI group | Cypress UI + local UI | npm run test:ui:cypress |
Real browser run; passed total must equal the same selection manifest. Overlaps Playwright UI. |
| Ten API teaching checks | Bash cURL + local mock | bash tests/automation/api/curl/01_login_success.sh through 10_get_reviews.sh |
Real HTTP smoke checks, not a separate count of Gherkin coverage. |
Generated @WIP @GeneratedDraft API/E2E files |
Parser + opt-in Cucumber dry-run | npm run ai:generate:mock or npm run test:ai-generator |
Draft syntax/matching only; excluded from ordinary and real suites; external AI is optional and not used for baseline validation. |
Existing @WIP and non-@LocalUI model exclusions |
— | — | Visible, but deliberately not passed/covered by normal gates. Booking, payment, cancellation, guest-capacity availability, ownership and persistent auth are outside the teaching model. |
tests/automation/api/sql/*.sql |
— | — | PostgreSQL learning examples only. No database, connection, fixture lifecycle, or executable DB harness; not integration-test coverage. |
| Workflow | Trigger | Jobs |
|---|---|---|
on-main-acceptance.yml |
Pull request, push to main, manual |
Typecheck plus four independent strict dry-run profiles |
manual-quality-gate.yml |
Manual dispatch | Selectable strict dry-run profile |
nightly-full-run.yml |
Daily 00:00 UTC, manual | Four independent strict dry-run profiles |
api-mock-tests.yml |
Pull request, push to main, daily 01:30 UTC, manual |
Real API suites against a restarted mock, retained diagnostics and Allure report |
The dry-run workflows type-check the TypeScript suite and fail on undefined or ambiguous non-WIP steps without opening a browser. The mock-server workflow runs API tests for real, records every selected suite, and preserves reports on failure. Pages publishing is a separate job and never runs for pull requests.
@WIP scenarios are excluded from these gates. A green dry-run therefore means
that all included steps are matched; it does not prove WIP coverage. The current
exclusion is quantified in stage 1 integration evidence.
To enable GitHub Pages: go to Settings → Pages → Source and set it to GitHub Actions.
API tests produce a full Allure HTML report as a CI artifact only from current results. A runner with no current result must fail rather than publish an old report.
The report is published to GitHub Pages after every push to main and every
nightly run via the api-mock-tests.yml
workflow. It includes:
- ✅ Pass / ❌ Fail /
⚠️ Broken status per test - Full step-by-step execution log for each scenario
- Tags, feature grouping, and suite breakdown
- Duration timeline and statistics
Mock server starts (Express, port 3001)
↓
Cucumber runs API tests against mock server
↓
allure-cucumberjs writes results to reports/api/latest/allure-results/
↓
allure generate → allure-report/ (HTML)
↓
GitHub Pages deploys report → live URL
# Terminal 1 — start mock server
npm run mock:start
# Terminal 2 — run one fresh suite and generate its report
npm run test:api:acceptance
npm run allure:generate
npm run allure:open # opens browser automaticallyLocated in tests/automation/api/sql/. These are educational PostgreSQL examples
mapped to test ideas; they do not validate this mock server at a database layer.
The repository has no test database, DB connection, migration/fixture lifecycle, or
executable SQL harness. Do not report them as passed integration tests.
- Window functions:
RANK(),ROW_NUMBER(),LAG(),PERCENT_RANK() - CTEs (Common Table Expressions) for multi-step logic
PERCENTILE_CONTfor statistical assertions on review ratingsSTRING_AGGfor amenity list validation- Conditional aggregation with
SUM(CASE WHEN … END) - Date overlap logic for availability validation
An interactive CLI tool that uses the Claude API to generate draft Gherkin .feature files from a plain-English description.
You describe what to test
↓
CLI asks: test type, tags, test design technique
↓
Claude API generates a draft Gherkin feature tagged @WIP + @API/@E2E
↓
It is saved under tests/manual/features/drafts/{e2e|api}/
↓
Gherkin is parsed and that exact @WIP draft is dry-run matched to steps
Add your Anthropic API key to .env:
ANTHROPIC_API_KEY=sk-ant-...Get a key at console.anthropic.com.
npm run ai:generate
# Offline, without ANTHROPIC_API_KEY and without an external request
npm run ai:generate:mock
# Reproducible non-interactive mock example
npx ts-node --project tsconfig.json scripts/ai-generate-feature.ts --mock --non-interactive \
--description "login response" --type apiExample session:
╔══════════════════════════════════════╗
║ AI Feature File Generator 🤖 ║
╚══════════════════════════════════════╝
? Describe the feature to test: hotel price filter with currency switch
? Test type (e2e / api): (e2e) e2e
? Tags (e.g. @Smoke @Regression): (@ Smoke @Regression)
Techniques: 0) none 1) BVA 2) EP 3) Decision Table 4) State Transition 5) Error Guessing
? Test design technique (0–5): (0) 3
⏳ Calling Claude API…
✅ Feature file generated!
→ tests/manual/features/e2e/decision_table_hotel_price_filter_with_currency_switch.feature
Generated files always have @WIP, @GeneratedDraft, and exactly one type tag:
@API or @E2E. They live in tests/manual/features/drafts/, are excluded from
ordinary profiles, and never receive @LocalUI automatically. The generator gives
the provider the local mock API contract (for API drafts), local-UI limitations, and
the supported step wording. Its examples use fixed fixture-friendly dates, not a
specific current year.
The validation parses Gherkin first, then explicitly selects the generated
@WIP draft in Cucumber dry-run mode and requires at least one selected scenario
with no undefined or ambiguous steps. It does not execute steps or launch a browser.
Passing it only proves syntax and step matching, not that the scenario is meaningful
or that the system behaves correctly.
To promote a draft: review its intent against the contract and supported product
model; implement or confirm the required assertions and steps; run the relevant
scenario for real against the local mock/local UI; then move it into the appropriate
working feature directory, remove @WIP and @GeneratedDraft, and add @LocalUI
only after local-UI support was verified. Finally run the affected normal suite.
Never promote a draft based only on generator validation.
Built with ☕ by Yaroslav Oleshchuk