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.
This commit is contained in:
37
README.md
37
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
|
||||
|
||||
73
package-lock.json
generated
73
package-lock.json
generated
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
85
playwright-report/index.html
Normal file
85
playwright-report/index.html
Normal file
File diff suppressed because one or more lines are too long
64
playwright.config.ts
Normal file
64
playwright.config.ts
Normal file
@@ -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,
|
||||
// },
|
||||
});
|
||||
|
||||
@@ -120,7 +120,7 @@
|
||||
color: #222;
|
||||
}
|
||||
.score {
|
||||
font-size: 20vh;
|
||||
font-size: 40vh;
|
||||
font-weight: 900;
|
||||
margin: 20px 0 30px 0;
|
||||
line-height: 1;
|
||||
|
||||
@@ -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');
|
||||
};
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
4
test-results/.last-run.json
Normal file
4
test-results/.last-run.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"status": "passed",
|
||||
"failedTests": []
|
||||
}
|
||||
26
tests/recordings/2-10-ball-endlos-wechsel.ts
Normal file
26
tests/recordings/2-10-ball-endlos-wechsel.ts
Normal file
@@ -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();
|
||||
});
|
||||
|
||||
24
tests/recordings/2-10-ball-race3-wechsel.ts
Normal file
24
tests/recordings/2-10-ball-race3-wechsel.ts
Normal file
@@ -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();
|
||||
});
|
||||
|
||||
29
tests/recordings/2-8-ball-race5-wechsel.ts
Normal file
29
tests/recordings/2-8-ball-race5-wechsel.ts
Normal file
@@ -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();
|
||||
});
|
||||
|
||||
31
tests/recordings/2-8-ball-race9-winner.ts
Normal file
31
tests/recordings/2-8-ball-race9-winner.ts
Normal file
@@ -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();
|
||||
});
|
||||
|
||||
26
tests/recordings/2-8-endlos-winner.ts
Normal file
26
tests/recordings/2-8-endlos-winner.ts
Normal file
@@ -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();
|
||||
});
|
||||
21
tests/recordings/2-9-ball-race1-wechsel.ts
Normal file
21
tests/recordings/2-9-ball-race1-wechsel.ts
Normal file
@@ -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();
|
||||
});
|
||||
|
||||
26
tests/recordings/2-9-ball-race5-winner.ts
Normal file
26
tests/recordings/2-9-ball-race5-winner.ts
Normal file
@@ -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();
|
||||
});
|
||||
|
||||
29
tests/recordings/3-10-ball-race5-winner.ts
Normal file
29
tests/recordings/3-10-ball-race5-winner.ts
Normal file
@@ -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();
|
||||
});
|
||||
|
||||
29
tests/recordings/3-8-ball-endlos-winner.ts
Normal file
29
tests/recordings/3-8-ball-endlos-winner.ts
Normal file
@@ -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();
|
||||
});
|
||||
|
||||
28
tests/recordings/3-8-ball-race3-wechsel.ts
Normal file
28
tests/recordings/3-8-ball-race3-wechsel.ts
Normal file
@@ -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();
|
||||
});
|
||||
|
||||
33
tests/recordings/3-9-ball-race7-wechsel.ts
Normal file
33
tests/recordings/3-9-ball-race7-wechsel.ts
Normal file
@@ -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();
|
||||
});
|
||||
|
||||
147
tests/recordings/README.md
Normal file
147
tests/recordings/README.md
Normal file
@@ -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`
|
||||
|
||||
26
tests/recordings/example-flow.ts
Normal file
26
tests/recordings/example-flow.ts
Normal file
@@ -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();
|
||||
});
|
||||
Reference in New Issue
Block a user