diff --git a/.gitea/workflows/playwright.yml b/.gitea/workflows/playwright.yml new file mode 100644 index 0000000..ca5c143 --- /dev/null +++ b/.gitea/workflows/playwright.yml @@ -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 + diff --git a/.gitignore b/.gitignore index 71ea5f2..e356bad 100644 --- a/.gitignore +++ b/.gitignore @@ -34,10 +34,12 @@ yarn-error.log* # Testing coverage +playwright-report +test-results +.playwright-browsers # Prisma prisma/dev.db # Misc .cache - diff --git a/PROJECT.md b/PROJECT.md index 8abe059..e35b3c8 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -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 diff --git a/README.md b/README.md index 3773aa8..52a6148 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/package-lock.json b/package-lock.json index 222d43b..0a951d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" }, diff --git a/package.json b/package.json index 873a5d8..b9a6855 100644 --- a/package.json +++ b/package.json @@ -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" } } - diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..914f095 --- /dev/null +++ b/playwright.config.ts @@ -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'] }, + }, + ], +}); + diff --git a/tests/e2e/README.md b/tests/e2e/README.md new file mode 100644 index 0000000..9ae6ae3 --- /dev/null +++ b/tests/e2e/README.md @@ -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`. + diff --git a/tests/e2e/health.spec.ts b/tests/e2e/health.spec.ts new file mode 100644 index 0000000..838cb26 --- /dev/null +++ b/tests/e2e/health.spec.ts @@ -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', + }); + }); +}); diff --git a/tests/e2e/home.spec.ts b/tests/e2e/home.spec.ts new file mode 100644 index 0000000..6f0ace2 --- /dev/null +++ b/tests/e2e/home.spec.ts @@ -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(); + }); +}); + diff --git a/tests/e2e/responsive.spec.ts b/tests/e2e/responsive.spec.ts new file mode 100644 index 0000000..5c7025d --- /dev/null +++ b/tests/e2e/responsive.spec.ts @@ -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(); + }); +}); +