feat: add playwright e2e workflow
Some checks failed
Deploy Holiday Property Booking / deploy (push) Successful in 3m0s
Playwright Holiday Property Booking / playwright (push) Successful in 10m59s
Test & Build Holiday Property Booking / test-build (push) Has been cancelled

This commit is contained in:
2026-05-22 08:40:45 +00:00
parent b781967a14
commit 909e8062f4
11 changed files with 250 additions and 11 deletions

View File

@@ -0,0 +1,75 @@
name: Playwright Holiday Property Booking
on:
pull_request:
branches:
- develop
- qa
push:
branches:
- develop
- qa
jobs:
playwright:
runs-on: docker
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
github-server-url: https://git.dumas.ddns.net
- name: Resolve environment
shell: bash
env:
GITEA_REF: ${{ gitea.ref }}
run: |
set -Eeuo pipefail
case "$GITEA_REF" in
refs/heads/develop)
echo "PLAYWRIGHT_BASE_URL=http://192.168.1.15:7003" >> "$GITHUB_ENV"
echo "PLAYWRIGHT_ENV_NAME=dev" >> "$GITHUB_ENV"
;;
refs/heads/qa)
echo "PLAYWRIGHT_BASE_URL=http://192.168.1.15:6003" >> "$GITHUB_ENV"
echo "PLAYWRIGHT_ENV_NAME=qa" >> "$GITHUB_ENV"
;;
*)
echo "Skipping unmapped ref: $GITEA_REF"
exit 0
;;
esac
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- name: Install dependencies
run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium
- name: Wait for deployed app
shell: bash
run: |
set -Eeuo pipefail
for attempt in 1 2 3 4 5 6 7 8 9 10; do
if curl --fail --silent --show-error --location --max-time 15 "${PLAYWRIGHT_BASE_URL}/api/health" >/dev/null; then
echo "Application is ready on attempt $attempt"
exit 0
fi
echo "Application not ready on attempt $attempt; retrying..."
sleep 6
done
echo "Application never became ready"
exit 1
- name: Run Playwright suite
run: npm run test:e2e

4
.gitignore vendored
View File

@@ -34,10 +34,12 @@ yarn-error.log*
# Testing # Testing
coverage coverage
playwright-report
test-results
.playwright-browsers
# Prisma # Prisma
prisma/dev.db prisma/dev.db
# Misc # Misc
.cache .cache

View File

@@ -28,6 +28,7 @@ Phase 1 scaffold started from the approved planning docs.
- Health endpoint - Health endpoint
- Environment ports aligned to `7003`, `6003`, and `5003` for dev, QA, and production - Environment ports aligned to `7003`, `6003`, and `5003` for dev, QA, and production
- Container and host ports match for each environment - Container and host ports match for each environment
- Playwright browser test harness for dev and QA
## Next Build Step ## Next Build Step

View File

@@ -5,6 +5,7 @@ Planning workspace for the new project based on the functional specification.
## Current Status ## Current Status
Phase 1 scaffold has started. The repo now contains the Next.js app shell, Prisma schema, Docker entrypoint, and baseline project docs. Environment ports are mapped to `7003` for Dev, `6003` for QA, and `5003` for Production. Phase 1 scaffold has started. The repo now contains the Next.js app shell, Prisma schema, Docker entrypoint, and baseline project docs. Environment ports are mapped to `7003` for Dev, `6003` for QA, and `5003` for Production.
The browser test harness is now being added as a separate action that targets Dev and QA only.
## Working Rule ## Working Rule

79
package-lock.json generated
View File

@@ -15,7 +15,8 @@
"react-dom": "^19.0.0" "react-dom": "^19.0.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "20.19.37", "@playwright/test": "^1.60.0",
"@types/node": "20.17.6",
"@types/react": "19.2.14", "@types/react": "19.2.14",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"eslint": "^9", "eslint": "^9",
@@ -1697,6 +1698,22 @@
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://github.com/sponsors/jonschlinkert"
} }
}, },
"node_modules/@playwright/test": {
"version": "1.60.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz",
"integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==",
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.60.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@popperjs/core": { "node_modules/@popperjs/core": {
"version": "2.11.8", "version": "2.11.8",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
@@ -1856,13 +1873,13 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "20.19.37", "version": "20.17.6",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.6.tgz",
"integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==", "integrity": "sha512-VEI7OdvK2wP7XHnsuXbAJnEpEkF6NjSN45QJlL4VGqZSXsnicpesdTWsg9RISeSdYd3yeRj/y3k5KGjUXYnFwQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"undici-types": "~6.21.0" "undici-types": "~6.19.2"
} }
}, },
"node_modules/@types/react": { "node_modules/@types/react": {
@@ -5540,6 +5557,52 @@
"pathe": "^2.0.3" "pathe": "^2.0.3"
} }
}, },
"node_modules/playwright": {
"version": "1.60.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz",
"integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==",
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.60.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.60.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz",
"integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==",
"devOptional": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/playwright/node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/possible-typed-array-names": { "node_modules/possible-typed-array-names": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
@@ -6630,9 +6693,9 @@
} }
}, },
"node_modules/undici-types": { "node_modules/undici-types": {
"version": "6.21.0", "version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },

View File

@@ -7,6 +7,7 @@
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "eslint .", "lint": "eslint .",
"test:e2e": "playwright test",
"prisma:generate": "prisma generate", "prisma:generate": "prisma generate",
"prisma:migrate:dev": "prisma migrate dev", "prisma:migrate:dev": "prisma migrate dev",
"prisma:seed": "tsx prisma/seed.ts" "prisma:seed": "tsx prisma/seed.ts"
@@ -19,7 +20,8 @@
"react-dom": "^19.0.0" "react-dom": "^19.0.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "20.19.37", "@playwright/test": "^1.60.0",
"@types/node": "20.17.6",
"@types/react": "19.2.14", "@types/react": "19.2.14",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"eslint": "^9", "eslint": "^9",
@@ -30,4 +32,3 @@
"typescript": "5.9.3" "typescript": "5.9.3"
} }
} }

23
playwright.config.ts Normal file
View File

@@ -0,0 +1,23 @@
import { defineConfig, devices } from '@playwright/test';
const baseURL = process.env.PLAYWRIGHT_BASE_URL || 'http://127.0.0.1:3000';
export default defineConfig({
testDir: './tests/e2e',
fullyParallel: true,
retries: process.env.CI ? 1 : 0,
reporter: process.env.CI ? 'list' : [['list'], ['html', { open: 'never' }]],
use: {
baseURL,
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
});

23
tests/e2e/README.md Normal file
View File

@@ -0,0 +1,23 @@
# Playwright Coverage
This suite is the browser-level guardrail for the holiday booking app.
## Current Coverage
- Homepage shell and content structure
- Health endpoint
## Planned Coverage
- Property listing and detail flows
- Availability search and date handling
- Booking form validation and summary
- Stripe checkout handoff and return states
- Confirmation and failure states
- Admin management flows
- SEO, accessibility, and responsive behavior
## Rule
Update this suite as each user-facing flow lands. The workflow is separate from deploy and only runs against `develop` and `qa`.

14
tests/e2e/health.spec.ts Normal file
View File

@@ -0,0 +1,14 @@
import { expect, test } from '@playwright/test';
test.describe('health endpoint', () => {
test('returns a ready JSON payload', async ({ request }) => {
const response = await request.get('/api/health');
expect(response.ok()).toBeTruthy();
const payload = await response.json();
expect(payload).toMatchObject({
status: 'ok',
service: 'holiday-property-booking',
});
});
});

23
tests/e2e/home.spec.ts Normal file
View File

@@ -0,0 +1,23 @@
import { expect, test } from '@playwright/test';
test.describe('homepage', () => {
test('renders the phase 1 scaffold and primary navigation', async ({ page }) => {
await page.goto('/');
await expect(page.getByRole('heading', { name: 'Holiday Property Booking' })).toBeVisible();
await expect(page.getByText('Phase 1 foundation')).toBeVisible();
await expect(page.getByRole('navigation', { name: 'Primary' })).toBeVisible();
await expect(page.getByRole('link', { name: 'Review foundation' })).toHaveAttribute('href', '#foundation');
await expect(page.getByRole('link', { name: 'Check health' })).toHaveAttribute('href', '/api/health');
});
test('shows the core planning sections', async ({ page }) => {
await page.goto('/');
await expect(page.getByRole('heading', { name: 'Foundation work starts here' })).toBeVisible();
await expect(page.getByRole('heading', { name: 'The implementation stack is locked' })).toBeVisible();
await expect(page.getByRole('heading', { name: 'The first schema pass is ready' })).toBeVisible();
await expect(page.getByRole('heading', { name: 'Ready for the first implementation slice' })).toBeVisible();
});
});

View File

@@ -0,0 +1,13 @@
import { expect, test } from '@playwright/test';
test.describe('responsive shell', () => {
test('keeps the core content visible on mobile widths', async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto('/');
await expect(page.getByRole('heading', { name: 'Holiday Property Booking' })).toBeVisible();
await expect(page.getByRole('heading', { name: 'Foundation work starts here' })).toBeVisible();
await expect(page.getByRole('link', { name: 'Review foundation' })).toBeVisible();
});
});