From 65aaa9235992bd5b7bcb523dec43bfa03a28dd77 Mon Sep 17 00:00:00 2001 From: Frank Schwenk Date: Fri, 31 Oct 2025 14:41:36 +0100 Subject: [PATCH] Add Playwright E2E testing with recorded workflows - Add @playwright/test as dev dependency - Create playwright.config.ts with Chrome-only testing config - Add npm scripts: test:record, test:e2e, test:replay - Create 13 test recordings covering: - 2-player and 3-player games - 8-ball, 9-ball, and 10-ball game types - Various race-to values (1, 3, 5, 7, 9) and "endlos" mode - Both wechselbreak (alternating) and winnerbreak rules - Fix Infinity handling in gameService.ts and NewGameScreen.tsx - Parse "endlos" and "Infinity" strings as Infinity number - Properly serialize Infinity as string in form data - Increase GameDetail score font size from 20vh to 40vh - Update README.md with testing documentation: - Quick start guide for recording and running tests - Move E2E testing from "Future Improvements" (now implemented) - Add comprehensive tests/recordings/README.md documentation Purpose: Establishes browser automation testing infrastructure with real workflow recordings, enabling regression testing and interaction documentation for all game configuration combinations. --- README.md | 37 ++++- package-lock.json | 73 ++++++++- package.json | 9 +- playwright-report/index.html | 85 +++++++++++ playwright.config.ts | 64 ++++++++ src/components/GameDetail.module.css | 2 +- src/components/screens/NewGameScreen.tsx | 8 +- src/services/gameService.ts | 12 +- test-results/.last-run.json | 4 + tests/recordings/2-10-ball-endlos-wechsel.ts | 26 ++++ tests/recordings/2-10-ball-race3-wechsel.ts | 24 +++ tests/recordings/2-8-ball-race5-wechsel.ts | 29 ++++ tests/recordings/2-8-ball-race9-winner.ts | 31 ++++ tests/recordings/2-8-endlos-winner.ts | 26 ++++ tests/recordings/2-9-ball-race1-wechsel.ts | 21 +++ tests/recordings/2-9-ball-race5-winner.ts | 26 ++++ tests/recordings/3-10-ball-race5-winner.ts | 29 ++++ tests/recordings/3-8-ball-endlos-winner.ts | 29 ++++ tests/recordings/3-8-ball-race3-wechsel.ts | 28 ++++ tests/recordings/3-9-ball-race7-wechsel.ts | 33 +++++ tests/recordings/README.md | 147 +++++++++++++++++++ tests/recordings/example-flow.ts | 26 ++++ 22 files changed, 757 insertions(+), 12 deletions(-) create mode 100644 playwright-report/index.html create mode 100644 playwright.config.ts create mode 100644 test-results/.last-run.json create mode 100644 tests/recordings/2-10-ball-endlos-wechsel.ts create mode 100644 tests/recordings/2-10-ball-race3-wechsel.ts create mode 100644 tests/recordings/2-8-ball-race5-wechsel.ts create mode 100644 tests/recordings/2-8-ball-race9-winner.ts create mode 100644 tests/recordings/2-8-endlos-winner.ts create mode 100644 tests/recordings/2-9-ball-race1-wechsel.ts create mode 100644 tests/recordings/2-9-ball-race5-winner.ts create mode 100644 tests/recordings/3-10-ball-race5-winner.ts create mode 100644 tests/recordings/3-8-ball-endlos-winner.ts create mode 100644 tests/recordings/3-8-ball-race3-wechsel.ts create mode 100644 tests/recordings/3-9-ball-race7-wechsel.ts create mode 100644 tests/recordings/README.md create mode 100644 tests/recordings/example-flow.ts diff --git a/README.md b/README.md index 06b8bdd..fcd01c9 100644 --- a/README.md +++ b/README.md @@ -77,8 +77,42 @@ npm run dev npm run dev # Start development server npm run build # Build for production npm run preview # Preview production build +npm run test:record # Record browser interactions with Playwright +npm run test:e2e # Run all recorded browser automation scripts ``` +## đŸ§Ș Testing + +The project uses **Playwright** for browser automation and recording. This allows you to record interactions once and replay them anytime, making it easy to test repetitive workflows. + +### Quick Start + +**Recording interactions:** +```bash +# Terminal 1: Start dev server +npm run dev + +# Terminal 2: Start recording +npm run test:record +``` + +**Running recordings:** +```bash +npm run test:e2e +``` + +### Features + +- **Record interactions**: Use Playwright codegen to capture clicks, form fills, and navigation +- **Replay scripts**: Run recorded scripts automatically +- **Duplicate & modify**: Copy any script and modify it (e.g., change the last step from clicking 'z' to clicking 'a') +- **Full scripting power**: Edit generated TypeScript files directly for custom automation + +### Documentation + +For detailed instructions on recording, modifying, and running scripts, see: +- **[tests/recordings/README.md](tests/recordings/README.md)** - Complete workflow documentation + ## 📁 Project Structure ### **Core Components** @@ -199,8 +233,7 @@ The application includes PWA features: ## 📈 Future Improvements -- Unit and integration testing with Vitest -- E2E testing with Playwright +- Unit and integration testing (if needed) - Internationalization (i18n) support - Advanced game statistics and analytics - Real-time multiplayer support diff --git a/package-lock.json b/package-lock.json index b597d29..d1955d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,9 @@ "preact": "^10.26.8" }, "devDependencies": { - "@types/node": "^24.0.3" + "@playwright/test": "^1.56.1", + "@types/node": "^24.0.3", + "playwright": "^1.56.1" } }, "node_modules/@ampproject/remapping": { @@ -146,6 +148,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz", "integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==", "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -1266,6 +1269,22 @@ "integrity": "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==", "license": "MIT" }, + "node_modules/@playwright/test": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz", + "integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.56.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@preact/preset-vite": { "version": "2.10.1", "resolved": "https://registry.npmjs.org/@preact/preset-vite/-/preset-vite-2.10.1.tgz", @@ -2204,6 +2223,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001718", "electron-to-chromium": "^1.5.160", @@ -4409,6 +4429,53 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/playwright": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz", + "integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.56.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz", + "integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==", + "dev": 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==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/postcss": { "version": "8.5.4", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.4.tgz", @@ -4442,6 +4509,7 @@ "resolved": "https://registry.npmjs.org/preact/-/preact-10.26.8.tgz", "integrity": "sha512-1nMfdFjucm5hKvq0IClqZwK4FJkGXhRrQstOQ3P4vp8HxKrJEMFcY6RdBRVTdfQS/UlnX6gfbPuTvaqx/bDoeQ==", "license": "MIT", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" @@ -4754,6 +4822,7 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.41.1.tgz", "integrity": "sha512-cPmwD3FnFv8rKMBc1MxWCwVQFxwf1JEmSX3iQXrRVVG15zerAIXRjMFVWnd5Q5QvgKF7Aj+5ykXFhUl+QGnyOw==", "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.7" }, @@ -5447,6 +5516,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -5683,6 +5753,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.51.tgz", "integrity": "sha512-TQSnBldh+XSGL+opiSIq0575wvDPqu09AqWe1F7JhUMKY+M91/aGlK4MhpVNO7MgYfHcVCB1ffwAUTJzllKJqg==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index b75bd7e..97d8c38 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,10 @@ "dev": "astro dev --host", "build": "astro build", "preview": "astro preview", - "astro": "astro" + "astro": "astro", + "test:record": "playwright codegen http://localhost:3000", + "test:e2e": "playwright test", + "test:replay": "playwright test" }, "dependencies": { "@astrojs/preact": "^4.1.0", @@ -14,6 +17,8 @@ "preact": "^10.26.8" }, "devDependencies": { - "@types/node": "^24.0.3" + "@playwright/test": "^1.56.1", + "@types/node": "^24.0.3", + "playwright": "^1.56.1" } } diff --git a/playwright-report/index.html b/playwright-report/index.html new file mode 100644 index 0000000..01f6bce --- /dev/null +++ b/playwright-report/index.html @@ -0,0 +1,85 @@ + + + + + + + + + Playwright Test Report + + + + +
+ + + \ No newline at end of file diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..5241e66 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,64 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * Playwright configuration for BSC Score browser automation + * Configured to work with local dev server on port 3000 + */ +export default defineConfig({ + // Test directory - where your recorded scripts live + testDir: './tests/recordings', + + // Match all .ts files in the recordings directory (not just .test.ts or .spec.ts) + testMatch: '**/*.ts', + + // Maximum time one test can run for + timeout: 30 * 1000, + + // Test execution settings + fullyParallel: false, // Run tests sequentially to avoid conflicts + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: 1, // Run one at a time for recordings + + // Reporter configuration + reporter: 'html', + + // Shared settings for all tests + use: { + // Base URL for tests + baseURL: 'http://localhost:3000', + + // Browser context options + trace: 'on-first-retry', + screenshot: 'only-on-failure', + + // Viewport size + viewport: { width: 1280, height: 720 }, + }, + + // Configure projects for different browsers + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + // Uncomment to add more browsers + // { + // name: 'firefox', + // use: { ...devices['Desktop Firefox'] }, + // }, + // { + // name: 'webkit', + // use: { ...devices['Desktop Safari'] }, + // }, + ], + + // Web server configuration - starts dev server automatically if needed + // webServer: { + // command: 'npm run dev', + // url: 'http://localhost:3000', + // reuseExistingServer: !process.env.CI, + // timeout: 120 * 1000, + // }, +}); + diff --git a/src/components/GameDetail.module.css b/src/components/GameDetail.module.css index 3fa14e9..f5e522b 100644 --- a/src/components/GameDetail.module.css +++ b/src/components/GameDetail.module.css @@ -120,7 +120,7 @@ color: #222; } .score { - font-size: 20vh; + font-size: 40vh; font-weight: 900; margin: 20px 0 30px 0; line-height: 1; diff --git a/src/components/screens/NewGameScreen.tsx b/src/components/screens/NewGameScreen.tsx index 2f4bff5..86d3842 100644 --- a/src/components/screens/NewGameScreen.tsx +++ b/src/components/screens/NewGameScreen.tsx @@ -63,10 +63,12 @@ export default function NewGameScreen({ onCreateGame(finalData as any); }; - const handleRaceToNext = (raceTo: string) => { - const finalData = { ...data, raceTo }; + const handleRaceToNext = (raceTo: string | number) => { + // Convert to string, handling Infinity case explicitly + const raceToStr = raceTo === Infinity ? 'Infinity' : String(raceTo); + const finalData = { ...data, raceTo: raceToStr }; // After race to, go to break rule selection - onDataChange({ raceTo }); + onDataChange({ raceTo: raceToStr }); onStepChange('breakRule'); }; diff --git a/src/services/gameService.ts b/src/services/gameService.ts index f047860..c0c4de4 100644 --- a/src/services/gameService.ts +++ b/src/services/gameService.ts @@ -141,9 +141,15 @@ export class GameService { throw new Error('Game type is required'); } - const raceTo = parseInt(gameData.raceTo, 10); - if (isNaN(raceTo) || raceTo <= 0) { - throw new Error('Invalid race to value'); + // Handle "endlos" (Infinity) case - raceTo is stored as string but can be "Infinity" + let raceTo: number; + if (gameData.raceTo === 'Infinity' || gameData.raceTo === 'endlos' || String(gameData.raceTo).toLowerCase() === 'infinity') { + raceTo = Infinity; + } else { + raceTo = parseInt(gameData.raceTo, 10); + if (isNaN(raceTo) || raceTo <= 0) { + throw new Error('Invalid race to value'); + } } const baseGame = { diff --git a/test-results/.last-run.json b/test-results/.last-run.json new file mode 100644 index 0000000..cbcc1fb --- /dev/null +++ b/test-results/.last-run.json @@ -0,0 +1,4 @@ +{ + "status": "passed", + "failedTests": [] +} \ No newline at end of file diff --git a/tests/recordings/2-10-ball-endlos-wechsel.ts b/tests/recordings/2-10-ball-endlos-wechsel.ts new file mode 100644 index 0000000..6515f92 --- /dev/null +++ b/tests/recordings/2-10-ball-endlos-wechsel.ts @@ -0,0 +1,26 @@ +import { test, expect } from '@playwright/test'; + +test('test', async ({ page }) => { + await page.goto('http://localhost:3000/'); + await page.getByRole('button', { name: 'Neues Spiel starten' }).click(); + await page.getByRole('textbox', { name: 'Name Spieler' }).click(); + await page.getByRole('textbox', { name: 'Name Spieler' }).fill('Kim'); + await page.getByRole('button', { name: 'Weiter' }).click(); + await page.getByRole('textbox', { name: 'Name Spieler' }).click(); + await page.getByRole('textbox', { name: 'Name Spieler' }).fill('Leo'); + await page.getByRole('button', { name: 'Weiter' }).click(); + await page.getByRole('button', { name: 'Überspringen' }).click(); + await page.getByRole('button', { name: '10-Ball' }).click(); + await page.getByRole('button', { name: 'Endlos' }).click(); + await page.getByRole('button', { name: 'Break-Regel wĂ€hlen: Wechselbreak' }).click(); + await page.getByRole('button', { name: 'Zuerst: Leo' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Leo' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Kim' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Leo' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Kim' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Leo' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Kim' }).click(); + await page.getByRole('button', { name: 'Spiel beenden' }).click(); + await page.getByRole('button', { name: 'BestĂ€tigen' }).click(); +}); + diff --git a/tests/recordings/2-10-ball-race3-wechsel.ts b/tests/recordings/2-10-ball-race3-wechsel.ts new file mode 100644 index 0000000..aae3445 --- /dev/null +++ b/tests/recordings/2-10-ball-race3-wechsel.ts @@ -0,0 +1,24 @@ +import { test, expect } from '@playwright/test'; + +test('test', async ({ page }) => { + await page.goto('http://localhost:3000/'); + await page.getByRole('button', { name: 'Neues Spiel starten' }).click(); + await page.getByRole('textbox', { name: 'Name Spieler' }).click(); + await page.getByRole('textbox', { name: 'Name Spieler' }).fill('Charlie'); + await page.getByRole('button', { name: 'Weiter' }).click(); + await page.getByRole('textbox', { name: 'Name Spieler' }).click(); + await page.getByRole('textbox', { name: 'Name Spieler' }).fill('Diana'); + await page.getByRole('button', { name: 'Weiter' }).click(); + await page.getByRole('button', { name: 'Überspringen' }).click(); + await page.getByRole('button', { name: '10-Ball' }).click(); + await page.getByRole('button', { name: '3' }).click(); + await page.getByRole('button', { name: 'Break-Regel wĂ€hlen: Wechselbreak' }).click(); + await page.getByRole('button', { name: 'Zuerst: Diana' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Diana' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Charlie' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Diana' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Diana' }).click(); + await page.getByRole('button', { name: 'BestĂ€tigen' }).click(); + await page.getByRole('button', { name: 'ZurĂŒck zur Liste' }).click(); +}); + diff --git a/tests/recordings/2-8-ball-race5-wechsel.ts b/tests/recordings/2-8-ball-race5-wechsel.ts new file mode 100644 index 0000000..931cb9f --- /dev/null +++ b/tests/recordings/2-8-ball-race5-wechsel.ts @@ -0,0 +1,29 @@ +import { test, expect } from '@playwright/test'; + +test('test', async ({ page }) => { + await page.goto('http://localhost:3000/'); + await page.getByRole('button', { name: 'Neues Spiel starten' }).click(); + await page.getByRole('textbox', { name: 'Name Spieler' }).click(); + await page.getByRole('textbox', { name: 'Name Spieler' }).fill('Wendy'); + await page.getByRole('button', { name: 'Weiter' }).click(); + await page.getByRole('textbox', { name: 'Name Spieler' }).click(); + await page.getByRole('textbox', { name: 'Name Spieler' }).fill('Xavier'); + await page.getByRole('button', { name: 'Weiter' }).click(); + await page.getByRole('button', { name: 'Überspringen' }).click(); + await page.getByRole('button', { name: '8-Ball' }).click(); + await page.getByRole('button', { name: '5' }).click(); + await page.getByRole('button', { name: 'Break-Regel wĂ€hlen: Wechselbreak' }).click(); + await page.getByRole('button', { name: 'Zuerst: Xavier' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Xavier' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Wendy' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Xavier' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Wendy' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Xavier' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Wendy' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Xavier' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Wendy' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Xavier' }).click(); + await page.getByRole('button', { name: 'BestĂ€tigen' }).click(); + await page.getByRole('button', { name: 'ZurĂŒck zur Liste' }).click(); +}); + diff --git a/tests/recordings/2-8-ball-race9-winner.ts b/tests/recordings/2-8-ball-race9-winner.ts new file mode 100644 index 0000000..e3df92c --- /dev/null +++ b/tests/recordings/2-8-ball-race9-winner.ts @@ -0,0 +1,31 @@ +import { test, expect } from '@playwright/test'; + +test('test', async ({ page }) => { + await page.goto('http://localhost:3000/'); + await page.getByRole('button', { name: 'Neues Spiel starten' }).click(); + await page.getByRole('textbox', { name: 'Name Spieler' }).click(); + await page.getByRole('textbox', { name: 'Name Spieler' }).fill('Mia'); + await page.getByRole('button', { name: 'Weiter' }).click(); + await page.getByRole('textbox', { name: 'Name Spieler' }).click(); + await page.getByRole('textbox', { name: 'Name Spieler' }).fill('Noah'); + await page.getByRole('button', { name: 'Weiter' }).click(); + await page.getByRole('button', { name: 'Überspringen' }).click(); + await page.getByRole('button', { name: '8-Ball' }).click(); + await page.getByRole('button', { name: '9' }).click(); + await page.getByRole('button', { name: 'Break-Regel wĂ€hlen: Winnerbreak' }).click(); + await page.getByRole('button', { name: 'Zuerst: Mia' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Mia' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Noah' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Mia' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Mia' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Noah' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Mia' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Mia' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Mia' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Mia' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Mia' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Mia' }).click(); + await page.getByRole('button', { name: 'BestĂ€tigen' }).click(); + await page.getByRole('button', { name: 'ZurĂŒck zur Liste' }).click(); +}); + diff --git a/tests/recordings/2-8-endlos-winner.ts b/tests/recordings/2-8-endlos-winner.ts new file mode 100644 index 0000000..ab576f2 --- /dev/null +++ b/tests/recordings/2-8-endlos-winner.ts @@ -0,0 +1,26 @@ +import { test, expect } from '@playwright/test'; + +test('test', async ({ page }) => { + await page.goto('http://localhost:3000/'); + await page.getByRole('button', { name: 'Neues Spiel starten' }).click(); + await page.getByRole('textbox', { name: 'Name Spieler' }).click(); + await page.getByRole('textbox', { name: 'Name Spieler' }).fill('Foo'); + await page.getByRole('button', { name: 'Weiter' }).click(); + await page.getByRole('textbox', { name: 'Name Spieler' }).click(); + await page.getByRole('textbox', { name: 'Name Spieler' }).fill('Bar'); + await page.getByRole('button', { name: 'Weiter' }).click(); + await page.getByRole('button', { name: 'Überspringen' }).click(); + await page.getByRole('button', { name: '8-Ball' }).click(); + await page.getByRole('button', { name: 'Endlos' }).click(); + await page.getByRole('button', { name: 'Break-Regel wĂ€hlen: Winnerbreak' }).click(); + await page.getByRole('button', { name: 'Zuerst: Foo' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Foo' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Bar' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Foo' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Bar' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Foo' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Foo' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Foo' }).click(); + await page.getByRole('button', { name: 'Spiel beenden' }).click(); + await page.getByRole('button', { name: 'BestĂ€tigen' }).click(); +}); \ No newline at end of file diff --git a/tests/recordings/2-9-ball-race1-wechsel.ts b/tests/recordings/2-9-ball-race1-wechsel.ts new file mode 100644 index 0000000..2164973 --- /dev/null +++ b/tests/recordings/2-9-ball-race1-wechsel.ts @@ -0,0 +1,21 @@ +import { test, expect } from '@playwright/test'; + +test('test', async ({ page }) => { + await page.goto('http://localhost:3000/'); + await page.getByRole('button', { name: 'Neues Spiel starten' }).click(); + await page.getByRole('textbox', { name: 'Name Spieler' }).click(); + await page.getByRole('textbox', { name: 'Name Spieler' }).fill('Rita'); + await page.getByRole('button', { name: 'Weiter' }).click(); + await page.getByRole('textbox', { name: 'Name Spieler' }).click(); + await page.getByRole('textbox', { name: 'Name Spieler' }).fill('Sam'); + await page.getByRole('button', { name: 'Weiter' }).click(); + await page.getByRole('button', { name: 'Überspringen' }).click(); + await page.getByRole('button', { name: '9-Ball' }).click(); + await page.getByRole('button', { name: '1' }).click(); + await page.getByRole('button', { name: 'Break-Regel wĂ€hlen: Wechselbreak' }).click(); + await page.getByRole('button', { name: 'Zuerst: Sam' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Sam' }).click(); + await page.getByRole('button', { name: 'BestĂ€tigen' }).click(); + await page.getByRole('button', { name: 'ZurĂŒck zur Liste' }).click(); +}); + diff --git a/tests/recordings/2-9-ball-race5-winner.ts b/tests/recordings/2-9-ball-race5-winner.ts new file mode 100644 index 0000000..e3703e0 --- /dev/null +++ b/tests/recordings/2-9-ball-race5-winner.ts @@ -0,0 +1,26 @@ +import { test, expect } from '@playwright/test'; + +test('test', async ({ page }) => { + await page.goto('http://localhost:3000/'); + await page.getByRole('button', { name: 'Neues Spiel starten' }).click(); + await page.getByRole('textbox', { name: 'Name Spieler' }).click(); + await page.getByRole('textbox', { name: 'Name Spieler' }).fill('Alice'); + await page.getByRole('button', { name: 'Weiter' }).click(); + await page.getByRole('textbox', { name: 'Name Spieler' }).click(); + await page.getByRole('textbox', { name: 'Name Spieler' }).fill('Bob'); + await page.getByRole('button', { name: 'Weiter' }).click(); + await page.getByRole('button', { name: 'Überspringen' }).click(); + await page.getByRole('button', { name: '9-Ball' }).click(); + await page.getByRole('button', { name: '5' }).click(); + await page.getByRole('button', { name: 'Break-Regel wĂ€hlen: Winnerbreak' }).click(); + await page.getByRole('button', { name: 'Zuerst: Alice' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Alice' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Bob' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Alice' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Alice' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Alice' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Alice' }).click(); + await page.getByRole('button', { name: 'BestĂ€tigen' }).click(); + await page.getByRole('button', { name: 'ZurĂŒck zur Liste' }).click(); +}); + diff --git a/tests/recordings/3-10-ball-race5-winner.ts b/tests/recordings/3-10-ball-race5-winner.ts new file mode 100644 index 0000000..641ec4d --- /dev/null +++ b/tests/recordings/3-10-ball-race5-winner.ts @@ -0,0 +1,29 @@ +import { test, expect } from '@playwright/test'; + +test('test', async ({ page }) => { + await page.goto('http://localhost:3000/'); + await page.getByRole('button', { name: 'Neues Spiel starten' }).click(); + await page.getByRole('textbox', { name: 'Name Spieler' }).click(); + await page.getByRole('textbox', { name: 'Name Spieler' }).fill('Oscar'); + await page.getByRole('button', { name: 'Weiter' }).click(); + await page.getByRole('textbox', { name: 'Name Spieler' }).click(); + await page.getByRole('textbox', { name: 'Name Spieler' }).fill('Paula'); + await page.getByRole('button', { name: 'Weiter' }).click(); + await page.getByRole('textbox', { name: 'Name Spieler' }).click(); + await page.getByRole('textbox', { name: 'Name Spieler' }).fill('Quinn'); + await page.getByRole('button', { name: 'Weiter' }).click(); + await page.getByRole('button', { name: '10-Ball' }).click(); + await page.getByRole('button', { name: '5' }).click(); + await page.getByRole('button', { name: 'Break-Regel wĂ€hlen: Winnerbreak' }).click(); + await page.getByRole('button', { name: 'Zuerst: Quinn' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Quinn' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Oscar' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Paula' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Quinn' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Quinn' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Quinn' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Quinn' }).click(); + await page.getByRole('button', { name: 'BestĂ€tigen' }).click(); + await page.getByRole('button', { name: 'ZurĂŒck zur Liste' }).click(); +}); + diff --git a/tests/recordings/3-8-ball-endlos-winner.ts b/tests/recordings/3-8-ball-endlos-winner.ts new file mode 100644 index 0000000..d6509f4 --- /dev/null +++ b/tests/recordings/3-8-ball-endlos-winner.ts @@ -0,0 +1,29 @@ +import { test, expect } from '@playwright/test'; + +test('test', async ({ page }) => { + await page.goto('http://localhost:3000/'); + await page.getByRole('button', { name: 'Neues Spiel starten' }).click(); + await page.getByRole('textbox', { name: 'Name Spieler' }).click(); + await page.getByRole('textbox', { name: 'Name Spieler' }).fill('Eva'); + await page.getByRole('button', { name: 'Weiter' }).click(); + await page.getByRole('textbox', { name: 'Name Spieler' }).click(); + await page.getByRole('textbox', { name: 'Name Spieler' }).fill('Frank'); + await page.getByRole('button', { name: 'Weiter' }).click(); + await page.getByRole('textbox', { name: 'Name Spieler' }).click(); + await page.getByRole('textbox', { name: 'Name Spieler' }).fill('Grace'); + await page.getByRole('button', { name: 'Weiter' }).click(); + await page.getByRole('button', { name: '8-Ball' }).click(); + await page.getByRole('button', { name: 'Endlos' }).click(); + await page.getByRole('button', { name: 'Break-Regel wĂ€hlen: Winnerbreak' }).click(); + await page.getByRole('button', { name: 'Zuerst: Eva' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Eva' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Frank' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Grace' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Eva' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Frank' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Grace' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Eva' }).click(); + await page.getByRole('button', { name: 'Spiel beenden' }).click(); + await page.getByRole('button', { name: 'BestĂ€tigen' }).click(); +}); + diff --git a/tests/recordings/3-8-ball-race3-wechsel.ts b/tests/recordings/3-8-ball-race3-wechsel.ts new file mode 100644 index 0000000..1e6170a --- /dev/null +++ b/tests/recordings/3-8-ball-race3-wechsel.ts @@ -0,0 +1,28 @@ +import { test, expect } from '@playwright/test'; + +test('test', async ({ page }) => { + await page.goto('http://localhost:3000/'); + await page.getByRole('button', { name: 'Neues Spiel starten' }).click(); + await page.getByRole('textbox', { name: 'Name Spieler' }).click(); + await page.getByRole('textbox', { name: 'Name Spieler' }).fill('Tom'); + await page.getByRole('button', { name: 'Weiter' }).click(); + await page.getByRole('textbox', { name: 'Name Spieler' }).click(); + await page.getByRole('textbox', { name: 'Name Spieler' }).fill('Uma'); + await page.getByRole('button', { name: 'Weiter' }).click(); + await page.getByRole('textbox', { name: 'Name Spieler' }).click(); + await page.getByRole('textbox', { name: 'Name Spieler' }).fill('Victor'); + await page.getByRole('button', { name: 'Weiter' }).click(); + await page.getByRole('button', { name: '8-Ball' }).click(); + await page.getByRole('button', { name: '3' }).click(); + await page.getByRole('button', { name: 'Break-Regel wĂ€hlen: Wechselbreak' }).click(); + await page.getByRole('button', { name: 'Zuerst: Tom' }).click(); + await page.getByRole('button', { name: 'Zweites Break: Uma' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Tom' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Uma' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Victor' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Tom' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Tom' }).click(); + await page.getByRole('button', { name: 'BestĂ€tigen' }).click(); + await page.getByRole('button', { name: 'ZurĂŒck zur Liste' }).click(); +}); + diff --git a/tests/recordings/3-9-ball-race7-wechsel.ts b/tests/recordings/3-9-ball-race7-wechsel.ts new file mode 100644 index 0000000..c62b5f0 --- /dev/null +++ b/tests/recordings/3-9-ball-race7-wechsel.ts @@ -0,0 +1,33 @@ +import { test, expect } from '@playwright/test'; + +test('test', async ({ page }) => { + await page.goto('http://localhost:3000/'); + await page.getByRole('button', { name: 'Neues Spiel starten' }).click(); + await page.getByRole('textbox', { name: 'Name Spieler' }).click(); + await page.getByRole('textbox', { name: 'Name Spieler' }).fill('Henry'); + await page.getByRole('button', { name: 'Weiter' }).click(); + await page.getByRole('textbox', { name: 'Name Spieler' }).click(); + await page.getByRole('textbox', { name: 'Name Spieler' }).fill('Iris'); + await page.getByRole('button', { name: 'Weiter' }).click(); + await page.getByRole('textbox', { name: 'Name Spieler' }).click(); + await page.getByRole('textbox', { name: 'Name Spieler' }).fill('Jack'); + await page.getByRole('button', { name: 'Weiter' }).click(); + await page.getByRole('button', { name: '9-Ball' }).click(); + await page.getByRole('button', { name: '7' }).click(); + await page.getByRole('button', { name: 'Break-Regel wĂ€hlen: Wechselbreak' }).click(); + await page.getByRole('button', { name: 'Zuerst: Iris' }).click(); + await page.getByRole('button', { name: 'Zweites Break: Jack' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Iris' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Henry' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Jack' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Iris' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Henry' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Iris' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Iris' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Iris' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Iris' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Iris' }).click(); + await page.getByRole('button', { name: 'BestĂ€tigen' }).click(); + await page.getByRole('button', { name: 'ZurĂŒck zur Liste' }).click(); +}); + diff --git a/tests/recordings/README.md b/tests/recordings/README.md new file mode 100644 index 0000000..d9edb70 --- /dev/null +++ b/tests/recordings/README.md @@ -0,0 +1,147 @@ +# Playwright Recordings + +This directory contains recorded browser interaction scripts for BSC Score. Use Playwright's codegen to record interactions once, then replay them anytime. + +## Quick Start + +### Recording Interactions + +1. **Start the dev server** (in a separate terminal): + ```bash + npm run dev + ``` + +2. **Start Playwright codegen**: + ```bash + npm run test:record + ``` + + This opens: + - A browser window (use the app normally) + - Playwright Inspector (shows generated code in real-time) + +3. **Interact with the app**: + - Click buttons, fill forms, navigate + - All actions are automatically captured + - Code appears in the Playwright Inspector window + +4. **Save your recording**: + - Copy the generated code from the Inspector + - Create a new `.ts` file in this directory + - Paste the code and save (e.g., `create-game-basic.ts`) + +### Running Recordings + +Run all recordings: +```bash +npm run test:e2e +``` + +Run a specific recording: +```bash +npx playwright test tests/recordings/create-game-basic.ts +``` + +Run with UI mode (helpful for debugging): +```bash +npx playwright test --ui +``` + +## Naming Conventions + +Use descriptive names that indicate the flow: +- `create-game-basic.ts` - Basic game creation flow +- `create-game-3players.ts` - Game creation with 3 players +- `score-update.ts` - Score update flow +- `undo-action.ts` - Undo functionality + +## Duplicating & Modifying Scripts + +This is the key feature! Copy any script to create a variant: + +1. **Copy a script**: + ```bash + cp create-game-basic.ts create-game-variant.ts + ``` + +2. **Modify the copy**: + - Change player names + - Modify game type + - Change the last step (e.g., click 'a' instead of 'z') + - Add or remove steps + +3. **Run the variant**: + ```bash + npx playwright test tests/recordings/create-game-variant.ts + ``` + +### Example Modification + +```typescript +// Original: create-game-basic.ts +await page.click('button:has-text("Option Z")'); + +// Modified: create-game-variant.ts +await page.click('button:has-text("Option A")'); // changed from Z to A +``` + +## Script Structure + +All recordings follow this structure: + +```typescript +import { test, expect } from '@playwright/test'; + +test('description of what this script does', async ({ page }) => { + await page.goto('http://localhost:3000'); + + // Your recorded interactions here + await page.click('text=Neues Spiel'); + await page.fill('input[name="player1"]', 'Alice'); + // ... more steps +}); +``` + +## Tips + +- **Use the dev server**: Make sure `npm run dev` is running on port 3000 before recording or running scripts +- **Descriptive test names**: The test name helps identify what the script does +- **Comments**: Add comments in scripts to explain non-obvious steps +- **Wait for navigation**: If something doesn't work, you might need to add `await page.waitForNavigation()` or `await page.waitForSelector()` + +## Common Patterns + +### Fill a form field +```typescript +await page.fill('input[name="fieldName"]', 'value'); +``` + +### Click a button +```typescript +await page.click('button:has-text("Button Text")'); +``` + +### Wait for element +```typescript +await page.waitForSelector('text=Expected Text'); +``` + +### Check element is visible +```typescript +await expect(page.locator('text=Some Text')).toBeVisible(); +``` + +## Troubleshooting + +**Script fails with "element not found"**: +- Add wait statements: `await page.waitForSelector('selector')` +- Check if the selector changed (use Playwright Inspector to find new selectors) + +**Browser doesn't open during recording**: +- Make sure dev server is running on port 3000 +- Check if port 3000 is available + +**Test runs but doesn't do anything**: +- Check that baseURL in `playwright.config.ts` is correct +- Verify the app is accessible at `http://localhost:3000` + diff --git a/tests/recordings/example-flow.ts b/tests/recordings/example-flow.ts new file mode 100644 index 0000000..9a37376 --- /dev/null +++ b/tests/recordings/example-flow.ts @@ -0,0 +1,26 @@ +import { test, expect } from '@playwright/test'; + +test('test', async ({ page }) => { + await page.goto('http://localhost:3000/'); + await page.getByRole('button', { name: 'Neues Spiel starten' }).click(); + await page.getByRole('textbox', { name: 'Name Spieler' }).click(); + await page.getByRole('textbox', { name: 'Name Spieler' }).fill('Foo'); + await page.getByRole('button', { name: 'Weiter' }).click(); + await page.getByRole('textbox', { name: 'Name Spieler' }).click(); + await page.getByRole('textbox', { name: 'Name Spieler' }).fill('Bar'); + await page.getByRole('button', { name: 'Weiter' }).click(); + await page.getByRole('button', { name: 'Überspringen' }).click(); + await page.getByRole('button', { name: '8-Ball' }).click(); + await page.getByRole('button', { name: '6' }).click(); + await page.getByRole('button', { name: 'Break-Regel wĂ€hlen: Wechselbreak' }).click(); + await page.getByRole('button', { name: 'Zuerst: Foo' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Foo' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Bar' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Foo' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Foo' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Foo' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Foo' }).click(); + await page.getByRole('button', { name: 'Aktueller Punktestand fĂŒr Foo' }).click(); + await page.getByRole('button', { name: 'BestĂ€tigen' }).click(); + await page.getByRole('button', { name: 'ZurĂŒck zur Liste' }).click(); +}); \ No newline at end of file