From 79e09f049f7fbd37d30e9cff1c2777fbc0bd1ce2 Mon Sep 17 00:00:00 2001 From: Daniel Noulet Date: Sun, 20 Sep 2026 09:13:48 +0200 Subject: [PATCH] test: add full application BDD suite Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .dockerignore | 1 + .gitignore | 2 + Makefile | 18 +- README.md | 14 + bdd/.dockerignore | 2 + bdd/Dockerfile | 8 + bdd/cucumber.mjs | 9 + bdd/features/recruitment-workspace.feature | 86 ++ bdd/package-lock.json | 1082 ++++++++++++++++++++ bdd/package.json | 13 + bdd/steps/workspace.steps.js | 93 ++ bdd/support/hooks.js | 24 + docker-compose.bdd.yml | 58 ++ docker/entrypoint.sh | 2 +- 14 files changed, 1408 insertions(+), 4 deletions(-) create mode 100644 bdd/.dockerignore create mode 100644 bdd/Dockerfile create mode 100644 bdd/cucumber.mjs create mode 100644 bdd/features/recruitment-workspace.feature create mode 100644 bdd/package-lock.json create mode 100644 bdd/package.json create mode 100644 bdd/steps/workspace.steps.js create mode 100644 bdd/support/hooks.js create mode 100644 docker-compose.bdd.yml diff --git a/.dockerignore b/.dockerignore index b2e5d7e..5d44513 100644 --- a/.dockerignore +++ b/.dockerignore @@ -9,6 +9,7 @@ **/node_modules frontend/dist frontend/coverage +bdd/reports **/*.tsbuildinfo backend/bin diff --git a/.gitignore b/.gitignore index ecc768e..2f5592d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ backend/api frontend/node_modules/ frontend/dist/ frontend/coverage/ +bdd/node_modules/ +bdd/reports/ diff --git a/Makefile b/Makefile index f98ef85..793ac58 100644 --- a/Makefile +++ b/Makefile @@ -3,8 +3,9 @@ SHELL := /bin/sh DATABASE_URL ?= postgres://go_guess:go_guess@localhost:5432/go_guess?sslmode=disable JWT_SECRET ?= local-development-secret-change-me-12345 GOOSE := cd backend && go run github.com/pressly/goose/v3/cmd/goose@v3.26.0 +BDD_COMPOSE := docker compose -f docker-compose.bdd.yml -.PHONY: dev db-up db-down migrate migrate-down seed backend frontend frontend-install test test-backend test-frontend test-integration test-env test-env-down lint build +.PHONY: help dev db-up db-down migrate migrate-down seed backend frontend frontend-install test test-backend test-frontend test-integration test-bdd test-bdd-down test-env test-env-down lint build help: @echo "make dev builds the demo docker image" @@ -16,10 +17,12 @@ help: @echo "make backend runs the backend" @echo "make frontend runs the frontend" @echo "make frontend-install installs the dependencies for the frontend" - @echo "make test full test suite: test-backend test-frontend and test-integration" + @echo "make test full test suite: frontend, integration, and browser BDD" @echo "make test-backend runs the tests for the backend" @echo "make test-frontend runs the tests for the fronted" @echo "make test-integration runs the integration tests" + @echo "make test-bdd runs browser BDD stories against the production image" + @echo "make test-bdd-down removes the isolated BDD stack and database" @echo "make test-env builds the docker tests image" @echo "make test-env-down stops the test image" @echo "make lint runs lint on the frontend" @@ -51,7 +54,7 @@ frontend: frontend-install: cd frontend && npm ci -test: frontend-install test-integration test-frontend +test: frontend-install test-integration test-frontend test-bdd test-backend: cd backend && go test ./... @@ -62,6 +65,15 @@ test-frontend: test-integration: cd backend && go test -tags=integration ./... +test-bdd: + @set -eu; \ + $(BDD_COMPOSE) down -v --remove-orphans; \ + trap '$(BDD_COMPOSE) down -v --remove-orphans >/dev/null 2>&1' EXIT INT TERM; \ + $(BDD_COMPOSE) up --build --abort-on-container-exit --exit-code-from bdd + +test-bdd-down: + $(BDD_COMPOSE) down -v --remove-orphans + test-env: COMPOSE_PROJECT_NAME=go-guess-test APP_ENV=test DB_PORT=15432 API_PORT=18080 \ WEB_PORT=15173 FRONTEND_URL=http://localhost:15173 RABBITMQ_AMQP_PORT=15673 \ diff --git a/README.md b/README.md index 1d134fe..9ab82fb 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,20 @@ Run one frontend test: cd frontend && npm test -- src/app/App.test.tsx -t "filters jobs by title" ``` +Run the browser-level BDD user stories against an isolated production image: + +```bash +make test-bdd +``` + +The human-readable scenarios live in +`bdd/features/recruitment-workspace.feature`. Add, remove, or edit scenarios in +that single file; reusable Playwright-backed steps live under `bdd/steps`. +Every run creates a fresh PostgreSQL database, loads deterministic test fixtures, +starts the complete production container with RabbitMQ, and writes an HTML report +to `bdd/reports/cucumber.html`. Use `make test-bdd-down` to remove a manually +started BDD stack. + Docker must be available for `make test`. The integration suite starts PostgreSQL 18 and RabbitMQ 4 in disposable Testcontainers, applies every Goose migration, loads deterministic test fixtures, exercises login and the complete assessment and diff --git a/bdd/.dockerignore b/bdd/.dockerignore new file mode 100644 index 0000000..ab12c38 --- /dev/null +++ b/bdd/.dockerignore @@ -0,0 +1,2 @@ +node_modules +reports diff --git a/bdd/Dockerfile b/bdd/Dockerfile new file mode 100644 index 0000000..dd6f55d --- /dev/null +++ b/bdd/Dockerfile @@ -0,0 +1,8 @@ +FROM mcr.microsoft.com/playwright:v1.63.0-noble + +WORKDIR /work +COPY package.json package-lock.json ./ +RUN npm ci +COPY . . + +CMD ["npm", "test"] diff --git a/bdd/cucumber.mjs b/bdd/cucumber.mjs new file mode 100644 index 0000000..d3f3fee --- /dev/null +++ b/bdd/cucumber.mjs @@ -0,0 +1,9 @@ +export default { + paths: ["features/**/*.feature"], + import: ["support/**/*.js", "steps/**/*.js"], + format: ["progress", "html:reports/cucumber.html"], + formatOptions: { + snippetInterface: "async-await", + }, + parallel: 1, +}; diff --git a/bdd/features/recruitment-workspace.feature b/bdd/features/recruitment-workspace.feature new file mode 100644 index 0000000..643ea9e --- /dev/null +++ b/bdd/features/recruitment-workspace.feature @@ -0,0 +1,86 @@ +Feature: Recruitment workspace + As an interview team + We want one connected workspace for jobs, candidates, assessments, and interviews + So that we can move qualified people through a consistent hiring process + + Scenario: User story - a valid interviewer signs in + Given the Go Guess application is open + When I sign in with email "integration@example.com" and password "integration-password" + Then I should see the heading "Your hiring pipeline, at a glance." + And I should be on the "/" page + + Scenario: User story - invalid credentials are rejected + Given the Go Guess application is open + When I sign in with email "integration@example.com" and password "incorrect-password" + Then I should see the error "invalid email or password" + And I should see the heading "Sign in to your workspace" + + Scenario: User story - an interviewer gets a quick pipeline overview + Given I am signed in as a valid interviewer + Then I should see the heading "Your hiring pipeline, at a glance." + And the dashboard should link to jobs, participants, and questions + And I should see "Integration Backend Engineer" + + Scenario: User story - an interviewer creates a job posting + Given I am signed in as a valid interviewer + When I follow the "Jobs" link + And I follow the "Create job" link + And I fill in the form: + | Job title | BDD Platform Engineer | + | Position | Platform Engineer | + | Seniority | Senior | + | Labels | Backend | + | Description | Build reliable production infrastructure. | + | Required skills | Go, PostgreSQL | + | Additional skills | Docker | + And I click the "Create job" button + Then I should see the heading "BDD Platform Engineer" + And I should see "Build reliable production infrastructure." + And I should see "draft" + + Scenario: User story - an interviewer adds a participant + Given I am signed in as a valid interviewer + When I follow the "Participants" link + And I follow the "Add participant" link + And I fill in the form: + | First name | Beatrice | + | Last name | Driven | + | Birthday | 1993-06-14 | + | Email address | beatrice.bdd@example.com | + | Contact information | Brussels, +32 470 11 22 33 | + And I click the "Create participant" button + Then I should see the heading "Beatrice Driven" + And I should see "beatrice.bdd@example.com" + + Scenario: User story - an interviewer creates a reusable assessment question + Given I am signed in as a valid interviewer + When I follow the "Question library" link + And I fill in the form: + | Question prompt | How do you keep a deployment reversible? | + | Reference answer | Use versioned artifacts and tested rollback procedures. | + And I click the "Create question" button + Then I should see "How do you keep a deployment reversible?" + And I should see "Use versioned artifacts and tested rollback procedures." + + Scenario: User story - an interviewer invites a matching candidate + Given I am signed in as a valid interviewer + When I follow the "Jobs" link + And I follow the "Integration Backend Engineer" link + And I select the "Candidate match" tab + Then I should see "Integration Candidate" + And I should see "100%" + When I select the "Invitations" tab + And I click the "Generate invitation" button + Then an invitation should be shown for "Integration Candidate" + + Scenario: User story - an interviewer can reach collaboration areas + Given I am signed in as a valid interviewer + When I follow the "Inbox" link + Then I should see the heading "Inbox" + And I should see "Inbox zero" + When I follow the "Calendar" link + Then I should be on the "/calendar" page + And I should see "Schedule" + When I follow the "Users" link + Then I should see the heading "Interview team" + And I should see "Integration Interviewer" diff --git a/bdd/package-lock.json b/bdd/package-lock.json new file mode 100644 index 0000000..5b5eb36 --- /dev/null +++ b/bdd/package-lock.json @@ -0,0 +1,1082 @@ +{ + "name": "go-guess-bdd", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "go-guess-bdd", + "version": "0.1.0", + "devDependencies": { + "@cucumber/cucumber": "13.2.1", + "playwright": "1.63.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@cucumber/ci-environment": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@cucumber/ci-environment/-/ci-environment-14.0.0.tgz", + "integrity": "sha512-CJbjPPGuk1fArPRVKB6FU2q9FHxOQOeq7IEJcB2NLNhOhV97uazLxvwD5ucAJb/flky+czOLhvaYaRAF6l5QYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@cucumber/cucumber": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/@cucumber/cucumber/-/cucumber-13.2.1.tgz", + "integrity": "sha512-Km3mLbZKOE/TlR9ExGWcG7PwOG1ArMt/R6BexrMhGA8pLGaLCWYISTHStpftLuuhVKvVd3RO5M/pfARKyvcyWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cucumber/ci-environment": "14.0.0", + "@cucumber/cucumber-expressions": "20.0.0", + "@cucumber/gherkin": "42.0.0", + "@cucumber/gherkin-streams": "7.0.1", + "@cucumber/gherkin-utils": "12.0.1", + "@cucumber/html-formatter": "24.1.0", + "@cucumber/junit-xml-formatter": "0.14.0", + "@cucumber/message-streams": "5.0.1", + "@cucumber/messages": "34.2.0", + "@cucumber/pretty-formatter": "4.0.0", + "@cucumber/tag-expressions": "11.0.0", + "assertion-error-formatter": "^3.0.0", + "cli-table3": "0.6.5", + "commander": "^15.0.0", + "debug": "^4.3.4", + "error-stack-parser": "^2.1.4", + "figures": "^6.0.0", + "has-ansi": "^6.0.0", + "indent-string": "^5.0.0", + "is-installed-globally": "^1.0.0", + "knuth-shuffle-seeded": "^1.0.6", + "lodash.merge": "^4.6.2", + "lodash.mergewith": "^4.6.2", + "luxon": "3.7.2", + "read-package-up": "^12.0.0", + "semver": "7.8.5", + "string-argv": "0.3.2", + "supports-color": "^11.0.0", + "type-fest": "^5.0.0", + "util-arity": "^1.1.0", + "yaml": "^2.2.2", + "yup": "1.7.1" + }, + "bin": { + "cucumber-js": "bin/cucumber.js" + }, + "engines": { + "node": "22 || 24 || >=26" + }, + "funding": { + "url": "https://opencollective.com/cucumber" + } + }, + "node_modules/@cucumber/cucumber-expressions": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@cucumber/cucumber-expressions/-/cucumber-expressions-20.0.0.tgz", + "integrity": "sha512-t2FBLKuU0U1nP5FM8pHiVonYcXVm07fvXr/H9osdvrjcLXSWc5OCbOOqV/k5S56w/YndPv1Sv2/m/x1KBYvIlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "regexp-match-indices": "1.0.2" + } + }, + "node_modules/@cucumber/gherkin": { + "version": "42.0.0", + "resolved": "https://registry.npmjs.org/@cucumber/gherkin/-/gherkin-42.0.0.tgz", + "integrity": "sha512-kBcv+NV4FGQYX6NIsSCjsjaX8MsRdEH559BQ9xEFPgkLQX/Z//JZrY94fpjxW6quo4V5kxlzFiiVC5xouF9Akg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cucumber/messages": ">=34.0.0 <35" + } + }, + "node_modules/@cucumber/gherkin-streams": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@cucumber/gherkin-streams/-/gherkin-streams-7.0.1.tgz", + "integrity": "sha512-8W1lmof0hbQ1HgOEpPCkz1KCFaM6MfA7n7rr6vX/7ffgUQ0saOAwapKSVj7P68Aht0K9XqHaw8BDLtqaK/jRIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "15.0.0", + "source-map-support": "0.5.21" + }, + "bin": { + "gherkin-javascript": "bin/gherkin" + }, + "peerDependencies": { + "@cucumber/gherkin": ">=22.0.0", + "@cucumber/message-streams": ">=4.0.0", + "@cucumber/messages": ">=17.1.1" + } + }, + "node_modules/@cucumber/gherkin-utils": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/@cucumber/gherkin-utils/-/gherkin-utils-12.0.1.tgz", + "integrity": "sha512-vImHbmTzvGcZP4SFSbx7P6janHZN+BqGWXc3EIQ7UeYMeDzwV9ybSlRfpECV340pxKYZyMTtuSCP3CDXfuWCQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cucumber/gherkin": "^41.0.0", + "@cucumber/messages": "^33.0.0", + "@teppeis/multimaps": "3.0.0", + "commander": "15.0.0", + "source-map-support": "^0.5.21" + }, + "bin": { + "gherkin-utils": "bin/gherkin-utils" + } + }, + "node_modules/@cucumber/gherkin-utils/node_modules/@cucumber/gherkin": { + "version": "41.0.0", + "resolved": "https://registry.npmjs.org/@cucumber/gherkin/-/gherkin-41.0.0.tgz", + "integrity": "sha512-pKGx1EzNjtWbpw74kEevKDMj71dF3ZSaFJpLYuWVvRZKe+Cwoq5iEkuMaELIg1jxIu8jH/A2HPMpMR8UBvdG0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cucumber/messages": ">=31.0.0 <34" + } + }, + "node_modules/@cucumber/gherkin-utils/node_modules/@cucumber/messages": { + "version": "33.0.4", + "resolved": "https://registry.npmjs.org/@cucumber/messages/-/messages-33.0.4.tgz", + "integrity": "sha512-i6P1a0bnmhecOHfvIW0rhMxv/gMKfBKwDMxmD9/Uo1HSqpQ17ocHms1JwWW6uTMLAopqqWlcz7l6Pax+qUOTzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@cucumber/html-formatter": { + "version": "24.1.0", + "resolved": "https://registry.npmjs.org/@cucumber/html-formatter/-/html-formatter-24.1.0.tgz", + "integrity": "sha512-5TBLnBT+tIvtamw/jtblBVgzQ2/ckd+md8I0rwkEk29vFMCF68jNwhyONGq6leUYMq4kVF+KdFhE8xGrSc3kJw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@cucumber/messages": ">=18" + } + }, + "node_modules/@cucumber/junit-xml-formatter": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@cucumber/junit-xml-formatter/-/junit-xml-formatter-0.14.0.tgz", + "integrity": "sha512-uDuJySsUr1b1VEAERC+YywvVhygBgYIAMyfSMzLZ3LrourudaWZIhfTND4uLNkRZZQcPr6IYv+qj5TP8RTJJ+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cucumber/query": "^16.0.0", + "@teppeis/multimaps": "^3.0.0", + "luxon": "^3.5.0", + "xmlbuilder": "^15.1.1" + }, + "peerDependencies": { + "@cucumber/messages": "*" + } + }, + "node_modules/@cucumber/message-streams": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@cucumber/message-streams/-/message-streams-5.0.1.tgz", + "integrity": "sha512-MXsD7ZAWWAiWMrn3Lzq2nwu9DJpPbbvdqXj2ot1BSM2gXnoSvg8d0pc6PzS9pI02swMSn/KN11mTdF5ScE7X0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime": "^4.0.0" + }, + "peerDependencies": { + "@cucumber/messages": ">=17.1.1" + } + }, + "node_modules/@cucumber/messages": { + "version": "34.2.0", + "resolved": "https://registry.npmjs.org/@cucumber/messages/-/messages-34.2.0.tgz", + "integrity": "sha512-6X6T1TApmbQNqxC0Oh3GcnLU+gfE5fp+cEXE/ml6+Ldx76Q6BDmkgM5vagSSDXP+vetPV2Fb0kWVeP7QIR6+Aw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@cucumber/pretty-formatter": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@cucumber/pretty-formatter/-/pretty-formatter-4.0.0.tgz", + "integrity": "sha512-vWKHUfjMphtbeHHlQBZ1gQzPL3/HE/O2dNnpBqEOIDwvhp7tKvqSe8kscp2H7pArbxN6euEpR5JbstJdzo3iyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cucumber/query": "16.0.0", + "luxon": "^3.7.2" + }, + "peerDependencies": { + "@cucumber/messages": "*" + } + }, + "node_modules/@cucumber/pretty-formatter/node_modules/@cucumber/query": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@cucumber/query/-/query-16.0.0.tgz", + "integrity": "sha512-CtGBHLc92y1RNogXGOFVF7so+XdLR5rVuJGsS6nH7yLeayyYhdV8lYW/LG8R05cm4irG/RO6Wn5I2CAgxMhmwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@teppeis/multimaps": "3.0.0", + "lodash.sortby": "^4.7.0" + }, + "peerDependencies": { + "@cucumber/messages": "*" + } + }, + "node_modules/@cucumber/query": { + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/@cucumber/query/-/query-16.1.1.tgz", + "integrity": "sha512-SBaP4Lc7xv55jY78Q8raoxvbBIPGhQKrui/Tq+l6jYBl1alV0TG1sW8TRtk9W7RQl27nVJo9DyyvASp374L9Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@teppeis/multimaps": "3.0.0", + "lodash.sortby": "^4.7.0" + }, + "peerDependencies": { + "@cucumber/messages": "*" + } + }, + "node_modules/@cucumber/tag-expressions": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@cucumber/tag-expressions/-/tag-expressions-11.0.0.tgz", + "integrity": "sha512-7Uo6dYST8xZbHwwemXCt7Kv80Yh/SE85DyOed1/FTwxRXs3NYVOCm6e/h8DxlhTGIpD6rYXfpY3nHnIXAFMBlw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@teppeis/multimaps": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@teppeis/multimaps/-/multimaps-3.0.0.tgz", + "integrity": "sha512-ID7fosbc50TbT0MK0EG12O+gAP3W3Aa/Pz4DaTtQtEvlc9Odaqi0de+xuZ7Li2GtK4HzEX7IuRWS/JmZLksR3Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-regex": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/assertion-error-formatter": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/assertion-error-formatter/-/assertion-error-formatter-3.0.0.tgz", + "integrity": "sha512-6YyAVLrEze0kQ7CmJfUgrLHb+Y7XghmL2Ie7ijVa2Y9ynP3LV+VDiwFk62Dn0qtqbmY0BT0ss6p1xxpiF2PYbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "diff": "^4.0.1", + "pad-right": "^0.2.2", + "repeat-string": "^1.6.1" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/commander": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz", + "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/error-stack-parser": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", + "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "stackframe": "^1.3.4" + } + }, + "node_modules/figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-up-simple": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.1.tgz", + "integrity": "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/global-directory": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", + "integrity": "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "4.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-ansi": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-6.0.2.tgz", + "integrity": "sha512-vAyM+6+jAYwSwz0/M0jYKfU9AvAMCz0kH791RsUhvMKGUHXled/3FjcQB3YiQ4Astj5srHdb6B2FHGIfZkOQNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/has-ansi?sponsor=1" + } + }, + "node_modules/hosted-git-info": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.3.tgz", + "integrity": "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^11.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/indent-string": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", + "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/index-to-position": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz", + "integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ini": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", + "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-installed-globally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-1.0.0.tgz", + "integrity": "sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "global-directory": "^4.0.1", + "is-path-inside": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-path-inside": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", + "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/knuth-shuffle-seeded": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/knuth-shuffle-seeded/-/knuth-shuffle-seeded-1.0.6.tgz", + "integrity": "sha512-9pFH0SplrfyKyojCLxZfMcvkhf5hH0d+UwR9nTVJ/DDQJGuzcXjTwB7TP7sDfehSudlGGaOLblmEWqv04ERVWg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "seed-random": "~2.2.0" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/luxon": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", + "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/mime": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-4.1.0.tgz", + "integrity": "sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa" + ], + "license": "MIT", + "bin": { + "mime": "bin/cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-package-data": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-8.0.0.tgz", + "integrity": "sha512-RWk+PI433eESQ7ounYxIp67CYuVsS1uYSonX3kA6ps/3LWfjVQa/ptEg6Y3T6uAMq1mWpX9PQ+qx+QaHpsc7gQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^9.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/pad-right": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/pad-right/-/pad-right-0.2.2.tgz", + "integrity": "sha512-4cy8M95ioIGolCoMmm2cMntGR1lPLEbOMzOKu8bzjuJP6JpzEMQcDHmh7hHLYGgob+nKe1YHFMaG4V59HQa89g==", + "dev": true, + "license": "MIT", + "dependencies": { + "repeat-string": "^1.5.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parse-json": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", + "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "index-to-position": "^1.1.0", + "type-fest": "^4.39.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-json/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/playwright": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.63.0.tgz", + "integrity": "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.63.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/playwright-core": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.63.0.tgz", + "integrity": "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/property-expr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz", + "integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/read-package-up": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/read-package-up/-/read-package-up-12.0.0.tgz", + "integrity": "sha512-Q5hMVBYur/eQNWDdbF4/Wqqr9Bjvtrw2kjGxxBbKLbx8bVCL8gcArjTy8zDUuLGQicftpMuU0riQNcAsbtOVsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up-simple": "^1.0.1", + "read-pkg": "^10.0.0", + "type-fest": "^5.2.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-10.1.0.tgz", + "integrity": "sha512-I8g2lArQiP78ll51UeMZojewtYgIRCKCWqZEgOO8c/uefTI+XDXvCSXu3+YNUaTNvZzobrL5+SqHjBrByRRTdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/normalize-package-data": "^2.4.4", + "normalize-package-data": "^8.0.0", + "parse-json": "^8.3.0", + "type-fest": "^5.4.4", + "unicorn-magic": "^0.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/regexp-match-indices": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regexp-match-indices/-/regexp-match-indices-1.0.2.tgz", + "integrity": "sha512-DwZuAkt8NF5mKwGGER1EGh2PRqyvhRhhLviH+R8y8dIuaQROlUfXjt4s9ZTXstIsSkptf06BSvwcEmmfheJJWQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "regexp-tree": "^0.1.11" + } + }, + "node_modules/regexp-tree": { + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", + "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==", + "dev": true, + "license": "MIT", + "bin": { + "regexp-tree": "bin/regexp-tree" + } + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/seed-random": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/seed-random/-/seed-random-2.2.0.tgz", + "integrity": "sha512-34EQV6AAHQGhoc0tn/96a9Fsi6v2xdqe/dMUwljGRaFOzR3EgRmECvD0O8vi8X+/uQ50LGHfkNu/Eue5TPKZkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.24", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.24.tgz", + "integrity": "sha512-cLS9TtWkIQFyLkJ3/5aFQAOHOSKTlOs/7WDut/XPSdjom1fJhUdkepeJAKXa9Y05+FabHd0sti+Et559vDtkpQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/stackframe": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", + "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-11.0.0.tgz", + "integrity": "sha512-/zyImLdxhdygBIaVX0xTlQhKaCDLCrm665aqHk8xqK/Pa6k61fL6gwQGQq1k31yNyz6x55PppQgF2DMyPQa5xw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tagged-tag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", + "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tiny-case": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz", + "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/type-fest": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.10.0.tgz", + "integrity": "sha512-NoSdpq/WEiAg5sjmBkmV/hfxv6HJH4NqPNrqjtSO5CwRmpsDfaf4begxW34KdJykH/l1yHtwBWQkCRdoXO8mPA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "dependencies": { + "tagged-tag": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unicorn-magic": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.4.0.tgz", + "integrity": "sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/util-arity": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/util-arity/-/util-arity-1.1.0.tgz", + "integrity": "sha512-kkyIsXKwemfSy8ZEoaIz06ApApnWsk5hQO0vLjZS6UkBiGiW++Jsyb8vSBoc0WKlffGoGs5yYy/j5pp8zckrFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, + "node_modules/yaml": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz", + "integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yup": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/yup/-/yup-1.7.1.tgz", + "integrity": "sha512-GKHFX2nXul2/4Dtfxhozv701jLQHdf6J34YDh2cEkpqoo8le5Mg6/LrdseVLrFarmFygZTlfIhHx/QKfb/QWXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "property-expr": "^2.0.5", + "tiny-case": "^1.0.3", + "toposort": "^2.0.2", + "type-fest": "^2.19.0" + } + }, + "node_modules/yup/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/bdd/package.json b/bdd/package.json new file mode 100644 index 0000000..7e66231 --- /dev/null +++ b/bdd/package.json @@ -0,0 +1,13 @@ +{ + "name": "go-guess-bdd", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "test": "cucumber-js --config cucumber.mjs" + }, + "devDependencies": { + "@cucumber/cucumber": "13.2.1", + "playwright": "1.63.0" + } +} diff --git a/bdd/steps/workspace.steps.js b/bdd/steps/workspace.steps.js new file mode 100644 index 0000000..97e40dd --- /dev/null +++ b/bdd/steps/workspace.steps.js @@ -0,0 +1,93 @@ +import assert from "node:assert/strict"; +import { Given, Then, When } from "@cucumber/cucumber"; + +async function openLoginPage(world) { + await world.page.goto(world.baseURL); + await world.page + .getByRole("heading", { name: "Sign in to your workspace", exact: true }) + .waitFor(); +} + +async function signIn(world, email, password) { + await world.page.getByLabel("Email address", { exact: true }).fill(email); + await world.page.getByLabel("Password", { exact: true }).fill(password); + await world.page + .getByRole("button", { name: "Sign in", exact: true }) + .click(); +} + +Given("the Go Guess application is open", async function () { + await openLoginPage(this); +}); + +Given("I am signed in as a valid interviewer", async function () { + await openLoginPage(this); + await signIn(this, "integration@example.com", "integration-password"); + await this.page + .getByRole("heading", { + name: "Your hiring pipeline, at a glance.", + exact: true, + }) + .waitFor(); +}); + +When( + "I sign in with email {string} and password {string}", + async function (email, password) { + await signIn(this, email, password); + }, +); + +When("I follow the {string} link", async function (name) { + await this.page.getByRole("link", { name, exact: true }).first().click(); +}); + +When("I click the {string} button", async function (name) { + await this.page.getByRole("button", { name, exact: true }).click(); +}); + +When("I select the {string} tab", async function (name) { + await this.page.getByRole("tab", { name: new RegExp(`^${name}`) }).click(); +}); + +When("I fill in the form:", async function (table) { + for (const [label, value] of table.raw()) { + await this.page.getByLabel(label, { exact: true }).fill(value); + } +}); + +Then("I should see the heading {string}", async function (heading) { + await this.page + .getByRole("heading", { name: heading, exact: true }) + .waitFor(); +}); + +Then("I should see {string}", async function (text) { + await this.page.getByText(text, { exact: true }).first().waitFor(); +}); + +Then("I should see the error {string}", async function (message) { + await this.page.getByRole("alert").filter({ hasText: message }).waitFor(); +}); + +Then("I should be on the {string} page", async function (path) { + await this.page.waitForURL((url) => url.pathname === path); + assert.equal(new URL(this.page.url()).pathname, path); +}); + +Then( + "the dashboard should link to jobs, participants, and questions", + async function () { + for (const path of ["/jobs", "/participants", "/questions"]) { + await this.page.locator(`a[href="${path}"]`).first().waitFor(); + } + }, +); + +Then("an invitation should be shown for {string}", async function (candidate) { + await this.page.getByText(candidate, { exact: true }).last().waitFor(); + const invitationURL = await this.page + .locator(".invitation-row code") + .textContent(); + assert.match(invitationURL ?? "", /\/participant\/[a-zA-Z0-9_-]+/); +}); diff --git a/bdd/support/hooks.js b/bdd/support/hooks.js new file mode 100644 index 0000000..c879600 --- /dev/null +++ b/bdd/support/hooks.js @@ -0,0 +1,24 @@ +import { After, Before, Status, setDefaultTimeout } from "@cucumber/cucumber"; +import { chromium } from "playwright"; + +setDefaultTimeout(Number(process.env.BDD_STEP_TIMEOUT_MS ?? 15000)); + +Before(async function () { + this.browser = await chromium.launch({ + headless: process.env.BDD_HEADLESS !== "false", + }); + this.context = await this.browser.newContext(); + this.page = await this.context.newPage(); + this.baseURL = process.env.BDD_BASE_URL ?? "http://127.0.0.1:8080"; +}); + +After(async function ({ result }) { + if (result?.status === Status.FAILED && this.page) { + await this.attach( + await this.page.screenshot({ fullPage: true }), + "image/png", + ); + } + await this.context?.close(); + await this.browser?.close(); +}); diff --git a/docker-compose.bdd.yml b/docker-compose.bdd.yml new file mode 100644 index 0000000..041c916 --- /dev/null +++ b/docker-compose.bdd.yml @@ -0,0 +1,58 @@ +services: + db: + image: postgres:18-alpine + environment: + POSTGRES_DB: go_guess + POSTGRES_USER: go_guess + POSTGRES_PASSWORD: go_guess + healthcheck: + test: ["CMD-SHELL", "pg_isready -U go_guess -d go_guess"] + interval: 2s + timeout: 3s + retries: 30 + + rabbitmq: + image: rabbitmq:4-alpine + healthcheck: + test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"] + interval: 3s + timeout: 5s + retries: 30 + + app: + build: + context: . + dockerfile: Dockerfile + environment: + DATABASE_URL: postgres://go_guess:go_guess@db:5432/go_guess?sslmode=disable + JWT_SECRET: bdd-test-secret-that-is-at-least-32-characters + FRONTEND_URL: http://app:8080 + RABBITMQ_URL: amqp://guest:guest@rabbitmq:5672/ + APP_ENV: test + ports: + - "${BDD_WEB_PORT:-15174}:8080" + depends_on: + db: + condition: service_healthy + rabbitmq: + condition: service_healthy + healthcheck: + test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8080/api/health"] + interval: 3s + timeout: 3s + retries: 30 + + bdd: + build: + context: bdd + dockerfile: Dockerfile + environment: + BDD_BASE_URL: http://127.0.0.1:8080 + BDD_HEADLESS: "true" + BDD_STEP_TIMEOUT_MS: "15000" + network_mode: service:app + volumes: + - ./bdd/reports:/work/reports + depends_on: + app: + condition: service_healthy diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 2f93ca7..0de1df0 100755 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -13,7 +13,7 @@ seed api & api_pid=$! -nginx -g 'daemon off;' & +nginx -e /dev/stderr -g 'daemon off;' & nginx_pid=$! trap shutdown TERM INT