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
|
||||
coverage
|
||||
playwright-report
|
||||
test-results
|
||||
.playwright-browsers
|
||||
|
||||
# Prisma
|
||||
prisma/dev.db
|
||||
|
||||
# Misc
|
||||
.cache
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ Phase 1 scaffold started from the approved planning docs.
|
||||
- Health endpoint
|
||||
- Environment ports aligned to `7003`, `6003`, and `5003` for dev, QA, and production
|
||||
- Container and host ports match for each environment
|
||||
- Playwright browser test harness for dev and QA
|
||||
|
||||
## Next Build Step
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ Planning workspace for the new project based on the functional specification.
|
||||
## 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.
|
||||
The browser test harness is now being added as a separate action that targets Dev and QA only.
|
||||
|
||||
## Working Rule
|
||||
|
||||
|
||||
79
package-lock.json
generated
79
package-lock.json
generated
@@ -15,7 +15,8 @@
|
||||
"react-dom": "^19.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "20.19.37",
|
||||
"@playwright/test": "^1.60.0",
|
||||
"@types/node": "20.17.6",
|
||||
"@types/react": "19.2.14",
|
||||
"@types/react-dom": "^19",
|
||||
"eslint": "^9",
|
||||
@@ -1697,6 +1698,22 @@
|
||||
"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": {
|
||||
"version": "2.11.8",
|
||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
||||
@@ -1856,13 +1873,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.19.37",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz",
|
||||
"integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==",
|
||||
"version": "20.17.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.6.tgz",
|
||||
"integrity": "sha512-VEI7OdvK2wP7XHnsuXbAJnEpEkF6NjSN45QJlL4VGqZSXsnicpesdTWsg9RISeSdYd3yeRj/y3k5KGjUXYnFwQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
"undici-types": "~6.19.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
@@ -5540,6 +5557,52 @@
|
||||
"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": {
|
||||
"version": "1.1.0",
|
||||
"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": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"version": "6.19.8",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint .",
|
||||
"test:e2e": "playwright test",
|
||||
"prisma:generate": "prisma generate",
|
||||
"prisma:migrate:dev": "prisma migrate dev",
|
||||
"prisma:seed": "tsx prisma/seed.ts"
|
||||
@@ -19,7 +20,8 @@
|
||||
"react-dom": "^19.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "20.19.37",
|
||||
"@playwright/test": "^1.60.0",
|
||||
"@types/node": "20.17.6",
|
||||
"@types/react": "19.2.14",
|
||||
"@types/react-dom": "^19",
|
||||
"eslint": "^9",
|
||||
@@ -30,4 +32,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