Web: Suche per Schaltflaeche, Relevanz, alternative Titel, Verlage, Filter, Nachladen, leere Zustaende, URL/Reload/Zurueck, Offline. API: RPC als anon ueber Kong inkl. Fehlerfaellen. Eigene Datensets je Spec-Datei. Refs #3 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
135 lines
5.9 KiB
TypeScript
135 lines
5.9 KiB
TypeScript
/**
|
|
* Helfer fuer das Projekt `web` (Flutter-Web-Build, Port 3000).
|
|
*
|
|
* Flutter zeichnet auf ein Canvas; Playwright sieht Elemente erst, wenn der Semantics-Baum
|
|
* eingeschaltet ist (Platzhalter `flt-semantics-placeholder`). Locator-Namen stammen aus
|
|
* `app/lib/l10n/app_en.arb` (Browser-Sprache en-US, siehe playwright.config.ts).
|
|
*/
|
|
import { expect, type BrowserContext, type Locator, type Page } from '@playwright/test';
|
|
import { env, TERMS_VERSION } from './auth-config';
|
|
import { enableSemantics, fillField, open, textOf } from './flutter';
|
|
|
|
export { TERMS_VERSION };
|
|
|
|
/** Texte (app_en.arb). */
|
|
export const t = {
|
|
invalidCredentials: 'Email address or password is incorrect.',
|
|
emailNotConfirmed: 'Please confirm your email address first. We sent you an email for this.',
|
|
resendConfirmation: 'Resend confirmation email',
|
|
rateLimited: 'Too many attempts. Please wait a few minutes and try again.',
|
|
linkInvalid: 'The link has expired or has already been used.',
|
|
checkEmailTitle: 'Check your inbox',
|
|
checkEmailResent: 'We resent the email, if there is something to send for this address.',
|
|
ageNotConfirmed: 'Shuffle can only be used from age 16.',
|
|
termsNotAccepted: 'Please agree to the privacy policy and the terms of use.',
|
|
confirmSuccess: 'Email confirmed.',
|
|
passwordChanged: 'Password changed. Other devices will be signed out; this can take up to an hour.',
|
|
confirmSubmit: 'Confirm email address',
|
|
recoverySubmit: 'Continue to new password',
|
|
home: 'Welcome to Shuffle. The board game catalog is coming soon.',
|
|
} as const;
|
|
|
|
export { textOf, enableSemantics, open, fillField };
|
|
|
|
export async function fillRegistration(
|
|
page: Page,
|
|
v: { email: string; password: string; displayName?: string; age?: boolean; terms?: boolean },
|
|
): Promise<void> {
|
|
await fillField(page.getByRole('textbox', { name: 'Email address' }), v.email);
|
|
await fillField(page.getByRole('textbox', { name: 'Password' }), v.password);
|
|
await fillField(page.getByRole('textbox', { name: 'Display name' }), v.displayName ?? 'E2E Tester');
|
|
if (v.age ?? true) await page.getByRole('checkbox', { name: /at least 16 years old/ }).click();
|
|
if (v.terms ?? true) await page.getByRole('checkbox', { name: /privacy policy/ }).click();
|
|
}
|
|
|
|
export async function register(page: Page, v: Parameters<typeof fillRegistration>[1]): Promise<void> {
|
|
await open(page, '/register');
|
|
await fillRegistration(page, v);
|
|
await page.getByRole('button', { name: 'Create account' }).click();
|
|
}
|
|
|
|
export async function login(page: Page, email: string, password: string): Promise<void> {
|
|
await open(page, '/login');
|
|
await fillField(page.getByRole('textbox', { name: 'Email address' }), email);
|
|
await fillField(page.getByRole('textbox', { name: 'Password' }), password);
|
|
await page.getByRole('button', { name: 'Sign in', exact: true }).click();
|
|
}
|
|
|
|
export async function expectSignedIn(page: Page): Promise<void> {
|
|
await expect(page).toHaveURL(/\/$/);
|
|
await expect(textOf(page, t.home)).toBeVisible();
|
|
}
|
|
|
|
/**
|
|
* Angemeldet <=> `/login` leitet weg (Guest-only-Route, Spec 5.3). Prueft ohne Fixture-Annahmen
|
|
* ueber den Storage-Key.
|
|
*/
|
|
export async function expectSignedInViaLoginRedirect(page: Page): Promise<void> {
|
|
await page.goto('/login');
|
|
await expect(page).toHaveURL(/\/$/);
|
|
}
|
|
|
|
/** `localStorage`-Eintraege von supabase_flutter im Web (Schluessel nicht raten, auslesen). */
|
|
export async function readAuthStorage(page: Page): Promise<Record<string, string>> {
|
|
return page.evaluate(() => {
|
|
const out: Record<string, string> = {};
|
|
for (let i = 0; i < localStorage.length; i++) {
|
|
const k = localStorage.key(i)!;
|
|
if (k.includes('supabase.auth.token') || /sb-.*-auth-token/.test(k)) out[k] = localStorage.getItem(k)!;
|
|
}
|
|
return out;
|
|
});
|
|
}
|
|
|
|
/** Neue Seite in einem frischen Kontext mit denselben Browser-Optionen. */
|
|
export async function freshContext(context: BrowserContext): Promise<BrowserContext> {
|
|
return context.browser()!.newContext({ locale: 'en-US', serviceWorkers: 'block', baseURL: env.webUrl });
|
|
}
|
|
|
|
/**
|
|
* Verschiebt die Browser-Uhr ab dem naechsten Seitenladen um `offsetMs` in die Zukunft.
|
|
*
|
|
* Zweck (Krit. 9, F1): Der Access-Token laeuft erst nach jwt_expiry (AUTH-CFG-14, 60 min) ab; der
|
|
* Test erzwingt den Refresh, ohne zu warten. `Session.expiresAt` in supabase_flutter wird aus dem
|
|
* JWT-Claim `exp` berechnet, nicht aus dem gespeicherten Feld `expires_at`; das Aendern des
|
|
* localStorage haette also keine Wirkung. Der Server rechnet weiter mit der echten Zeit.
|
|
*/
|
|
export async function shiftBrowserClock(page: Page, offsetMs: number): Promise<void> {
|
|
await page.addInitScript((offset: number) => {
|
|
const RealDate = Date;
|
|
class ShiftedDate extends RealDate {
|
|
constructor(...args: any[]) {
|
|
if (args.length === 0) super(RealDate.now() + offset);
|
|
else super(...(args as [any]));
|
|
}
|
|
static now() {
|
|
return RealDate.now() + offset;
|
|
}
|
|
}
|
|
(globalThis as any).Date = ShiftedDate;
|
|
}, offsetMs);
|
|
}
|
|
|
|
/** Schluessel der supabase_flutter-Sitzung im localStorage (nicht fest verdrahtet, nicht ueber Index). */
|
|
export async function authStorageKey(page: Page): Promise<string | null> {
|
|
return page.evaluate(() => {
|
|
for (let i = 0; i < localStorage.length; i++) {
|
|
const k = localStorage.key(i)!;
|
|
if (/^sb-.*-auth-token$/.test(k)) return k;
|
|
}
|
|
return null;
|
|
});
|
|
}
|
|
|
|
/** Aktuelle Sitzung aus dem localStorage oder `null`. */
|
|
export async function readSession(page: Page): Promise<{ access_token: string; refresh_token: string } | null> {
|
|
const key = await authStorageKey(page);
|
|
if (!key) return null;
|
|
const raw = await page.evaluate((k) => localStorage.getItem(k), key);
|
|
return raw ? JSON.parse(raw) : null;
|
|
}
|
|
|
|
/** Frischer Browser-Kontext mit den Projekt-Optionen (en-US, kein Service Worker, Basis-URL aus der Config). */
|
|
export async function freshBrowserContext(browser: import('@playwright/test').Browser): Promise<BrowserContext> {
|
|
return browser.newContext({ locale: 'en-US', serviceWorkers: 'block', baseURL: env.webUrl });
|
|
}
|