feat: add playwright e2e workflow
This commit is contained in:
75
.gitea/workflows/playwright.yml
Normal file
75
.gitea/workflows/playwright.yml
Normal 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
4
.gitignore
vendored
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
79
package-lock.json
generated
@@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
23
playwright.config.ts
Normal 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
23
tests/e2e/README.md
Normal 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
14
tests/e2e/health.spec.ts
Normal 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
23
tests/e2e/home.spec.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
13
tests/e2e/responsive.spec.ts
Normal file
13
tests/e2e/responsive.spec.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
Reference in New Issue
Block a user