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