feat: tablet-first UX overhaul with wizard improvements
- Tablet-first responsive scoreboard: whole player card is tap target - Per-step wizard backgrounds and live summary panel - Larger touch targets, safe-area handling, reduced-motion support - German UI error strings in App.tsx - Updated SOUL.md and README.md for current architecture - Cleanup: removed legacy components, .cursor/, UI_IMPROVEMENTS.md - Renamed package to bscscore, fixed TOTAL_WIZARD_STEPS
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
const PLAYER_INPUT_IDS: Record<string, string> = {
|
||||
'Spieler 1': 'player1-input',
|
||||
'Spieler 2': 'player2-input',
|
||||
'Spieler 3 (optional)': 'player3-input',
|
||||
};
|
||||
|
||||
/** Open name modal, fill input, confirm — advances wizard step. */
|
||||
export async function enterPlayerName(
|
||||
page: Page,
|
||||
label: string,
|
||||
placeholder: string,
|
||||
name: string,
|
||||
) {
|
||||
const inputId = PLAYER_INPUT_IDS[label];
|
||||
await page.getByRole('button', { name: `Name für ${label} eingeben` }).click();
|
||||
const dialog = page.getByRole('dialog');
|
||||
await expect(dialog).toBeVisible();
|
||||
const input = inputId ? page.locator(`#${inputId}`) : dialog.getByRole('textbox');
|
||||
await expect(input).toBeFocused();
|
||||
await input.fill(name);
|
||||
await dialog.getByRole('button', { name: 'Übernehmen' }).click();
|
||||
await expect(dialog).toBeHidden();
|
||||
}
|
||||
|
||||
export async function skipPlayer3(page: Page) {
|
||||
await page.getByRole('button', { name: 'Überspringen' }).click();
|
||||
}
|
||||
|
||||
export async function addPoint(page: Page, playerName: string, currentScore: number) {
|
||||
await page
|
||||
.getByRole('button', { name: `Punkt für ${playerName}. Aktueller Stand: ${currentScore}` })
|
||||
.click();
|
||||
}
|
||||
|
||||
export async function addPoints(
|
||||
page: Page,
|
||||
playerName: string,
|
||||
count: number,
|
||||
startScore = 0,
|
||||
) {
|
||||
for (let i = 0; i < count; i++) {
|
||||
await addPoint(page, playerName, startScore + i);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* Capture wow-brief verification screenshots across 4 viewports.
|
||||
* Usage: PW_EXECUTABLE=... node tests/helpers/wow-screenshots.mjs
|
||||
*/
|
||||
import { chromium } from 'playwright';
|
||||
|
||||
const BASE_URL = process.env.BASE_URL ?? 'http://localhost:3000';
|
||||
const executablePath = process.env.PW_EXECUTABLE;
|
||||
|
||||
const viewports = [
|
||||
{ name: '1280x800', width: 1280, height: 800 },
|
||||
{ name: '1024x768', width: 1024, height: 768 },
|
||||
{ name: '844x390', width: 844, height: 390 },
|
||||
{ name: '390x844', width: 390, height: 844 },
|
||||
];
|
||||
|
||||
async function dismissDevToolbar(page) {
|
||||
await page.evaluate(() => {
|
||||
document.querySelector('astro-dev-toolbar')?.remove();
|
||||
});
|
||||
}
|
||||
|
||||
async function clickButton(page, name) {
|
||||
await dismissDevToolbar(page);
|
||||
await page.getByRole('button', { name }).click({ force: true });
|
||||
}
|
||||
|
||||
async function capture(page, path) {
|
||||
await dismissDevToolbar(page);
|
||||
await page.screenshot({ path, fullPage: false });
|
||||
console.log(`saved ${path}`);
|
||||
}
|
||||
|
||||
async function enterPlayerName(page, label, name, inputId) {
|
||||
await clickButton(page, `Name für ${label} eingeben`);
|
||||
const dialog = page.getByRole('dialog');
|
||||
const input = dialog.locator(`#${inputId}`);
|
||||
await input.waitFor({ state: 'visible' });
|
||||
await input.click();
|
||||
await input.pressSequentially(name, { delay: 15 });
|
||||
await dialog.getByRole('button', { name: 'Übernehmen' }).click({ force: true });
|
||||
await dialog.waitFor({ state: 'hidden', timeout: 10000 });
|
||||
}
|
||||
|
||||
async function freshPage(page, vp) {
|
||||
await page.setViewportSize({ width: vp.width, height: vp.height });
|
||||
await page.goto(`${BASE_URL}/?t=${Date.now()}`, { waitUntil: 'domcontentloaded' });
|
||||
await dismissDevToolbar(page);
|
||||
await page.waitForTimeout(400);
|
||||
}
|
||||
|
||||
async function runWizardToScoreboard(page) {
|
||||
await clickButton(page, 'Neues Spiel starten');
|
||||
await enterPlayerName(page, 'Spieler 1', 'Alice', 'player1-input');
|
||||
await enterPlayerName(page, 'Spieler 2', 'Bob', 'player2-input');
|
||||
await clickButton(page, 'Überspringen');
|
||||
await clickButton(page, '9-Ball');
|
||||
await clickButton(page, '3');
|
||||
await clickButton(page, 'Break-Regel wählen: Winnerbreak');
|
||||
await clickButton(page, 'Zuerst: Alice');
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const browser = await chromium.launch({
|
||||
headless: true,
|
||||
...(executablePath ? { executablePath } : {}),
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
|
||||
for (const vp of viewports) {
|
||||
await freshPage(page, vp);
|
||||
await capture(page, `/tmp/wow-list-${vp.name}.png`);
|
||||
|
||||
await clickButton(page, 'Neues Spiel starten');
|
||||
await page.waitForTimeout(400);
|
||||
await capture(page, `/tmp/wow-wizard-${vp.name}.png`);
|
||||
|
||||
await freshPage(page, vp);
|
||||
await runWizardToScoreboard(page);
|
||||
await page.waitForTimeout(400);
|
||||
await capture(page, `/tmp/wow-scoreboard-${vp.name}.png`);
|
||||
|
||||
for (let score = 0; score < 3; score++) {
|
||||
await clickButton(page, `Punkt für Alice. Aktueller Stand: ${score}`);
|
||||
await page.waitForTimeout(150);
|
||||
}
|
||||
await page.waitForTimeout(600);
|
||||
await capture(page, `/tmp/wow-victory-${vp.name}.png`);
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
console.log('Done — screenshots in /tmp/wow-*.png');
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user