test(catalog): E2E und vorläufige Latenzmessung der Katalogsuche (KAT-01, 3c) #70

Open
cschlaefke wants to merge 6 commits from feature/3c-katalog-suche-e2e into main
5 changed files with 46 additions and 23 deletions
Showing only changes of commit f6103fd6aa - Show all commits

View file

@ -112,8 +112,14 @@ Je Art (p50): Titel 436, Titelanfang 455, Wort 451, Wortkombination 437, Verlag
### 5.3 Befunde und offene Punkte
- **Trefferkacheln im Semantics-Baum:** Alle Kacheln liegen als Kinder eines einzigen Listen-Containers (`role=group`); die Texte stehen in `span`-Knoten. Der Container hat keinen eigenen Namen. Für Bildschirmleser ist das Verhalten nicht geprüft (NVDA/VoiceOver-Durchgang, manuell). Die Tests suchen die Kacheln als Textknoten.
- **Semantics-Eingabefeld:** Ein direkt geladenes `/catalog` nimmt per Playwright getippten Text nicht verlässlich an (Feld bleibt leer; Autofokus ist mal gesetzt, mal nicht). Über die Startseite funktioniert es. Nicht untersucht, ob echte Nutzer (Mausklick) betroffen sind. Der Wert des Eingabefelds ist nach einem Auswahlmenü oder Neuladen im DOM leer, obwohl die Suche den Begriff behält (Beleg: URL und Ergebnis); die Tests prüfen deshalb die URL.
- **Ausgeblendete Spiele:** Die Tabelle `game_alt_titles` ist für `anon` auch für alternative Titel ausgeblendeter Katalogspiele lesbar (die RLS-Regel folgt der Sichtbarkeit in `games`, nicht der View `catalog_games`). Über die Suche erscheinen sie nicht. Ob das gewollt ist, ist fachlich zu entscheiden (nicht geraten).
- **Direkt geladenes `/catalog` und Playwright:** Text, der über den eingeschalteten Semantics-Baum getippt wird, geht bei einem direkt geladenen `/catalog` verloren (der Autofokus des Suchfelds ist beim Einschalten der Semantics schon gesetzt). Gegenprobe ohne Semantics: Mausklick auf das Feld und Tastatureingabe auf einem direkt geladenen `/catalog` setzen den Suchbegriff in die URL (3 von 3 Läufen). Das ist ein Effekt des Testwerkzeugs, kein Produktfehler; die Tests öffnen die Suche deshalb über die Startseite. Der Wert des Semantics-Eingabefelds ist nach einem Auswahlmenü oder Neuladen im DOM leer, obwohl die Suche den Begriff behält (Beleg: URL und Ergebnis); die Tests prüfen deshalb die URL.
- **Ausgeblendete Spiele über die Tabellen:** Die Policy `games_select` lässt jede Zeile mit `visibility = 'catalog'` zu, auch ausgeblendete und zusammengeführte; nur die View `catalog_games` filtert sie. Folglich sind auch deren `game_alt_titles` (RLS folgt `games`) für `anon` direkt lesbar, so wie ihre Zeilen in `games` selbst. Das folgt aus der RLS von Spec #2 und wurde mit #3 nicht eingeführt; über `search_catalog_games` erscheinen sie nie. Offene Frage an Chris/architect: Soll `games_select` ausgeblendete Katalogspiele für `anon` verbergen (nur die View liest sie dann)? Nicht geraten, keine Änderung hier.
- **Gateway ohne `apikey`:** Die lokale CLI beantwortet die RPC auch ohne `apikey`-Header als `anon` (HTTP 200). Auf dem Server ist das eine Frage der Kong-Konfiguration (Release-Checkliste).
- Krit. 6: „Spiel melden“ ist erst mit #13 sichtbar; der Test prüft den Hinweis, nicht die Aktion (Spec-F4).
- Nicht automatisiert: „Weitere laden“ per Tastatur (das automatische Nachladen kommt der Schaltfläche zuvor; Widget-Tests in 3b), Ansage der Trefferzahl für Bildschirmleser, Textskalierung 200 %, Tab-Reihenfolge im Browser.
### 5.4 Laufzeit und Stabilität der Suite
- Lokal, `--workers=2` (CI-Wert), Projekte `api` und `web`: ohne die neuen Tests (87 Tests) 43,6 bis 47,8 s (4 Läufe), mit ihnen (122 Tests) rund 78 s; die Mehrlaufzeit beträgt lokal also etwa 30 s. Die Web-Datei `kat01-suche.spec.ts` ist ein serieller Block von etwa 45 s (ein Worker); ein CI-Retry (`retries: 1`) wiederholt die ganze Datei. Die API-Datei braucht etwa 2 s. Der Job `e2e` und `e2e-run.sh` sind unverändert (`api`, `web` und `throttle` ziehen die neuen Dateien automatisch mit).
- Die bestehenden Web-Tests der Anmeldung sind lastabhängig flaky (zeitversetzte Live-Region, zu knappe Wartezeiten): Bei höherer Rechnerlast scheitert in einzelnen Läufen ein Test (mal `krit04`, `krit07`, `krit01`, `krit49`). Das trat in Läufen mit und ohne die neuen Tests auf (ohne: 1 von 4 Läufen, mit: 5 von 9; alle Läufe auf einem gemeinsam genutzten Rechner mit wechselnder Last, daher nicht belastbar). In `krit04-06-07-anmeldung.spec.ts` ignoriert `screenText` jetzt die Live-Region auf oberster Ebene des Baums (Ursache einer beobachteten Abweichung); die übrigen Fälle sind nicht untersucht. Empfehlung: Flake-Issue für die Anmelde-Tests.

View file

@ -9,8 +9,8 @@ die Tests laufen deshalb parallel.
| Projekt | Ziel | Voraussetzung | Tests |
|---|---|---|---|
| `api` | GoTrue und `search_catalog_games` über Kong (`:54321`) + Mailpit (`:54324`) + Postgres (`:54322`) | `supabase start` | 70 |
| `web` | gebaute Flutter-Web-App auf `http://localhost:3000` | zusätzlich `flutter build web` | 44 |
| `api` | GoTrue und `search_catalog_games` über Kong (`:54321`) + Mailpit (`:54324`) + Postgres (`:54322`) | `supabase start` | 77 |
| `web` | gebaute Flutter-Web-App auf `http://localhost:3000` | zusätzlich `flutter build web` | 45 |
| `throttle` | Extra-GoTrue-Container mit `GOTRUE_RATE_LIMIT_HEADER` (AUTH-CFG-13) | `THROTTLE_GOTRUE_URL` | 4 |
| `spike` | GoTrue mit unerreichbarem SMTP (Issue #37), nicht blockierend | `SPIKE_GOTRUE_URL` | 3 |
@ -46,7 +46,7 @@ Umgebungsvariablen (Standard = lokale CLI-Demo-Werte, keine Secrets): `SUPABASE_
- `tests/web/kat01-suche.spec.ts` (Suche per Schaltfläche „Search games“, Relevanz, alternative Titel, Verlage, Filter, Nachladen, leere Zustände, URL/Neuladen/Zurück, Offline) und `tests/api/kat01-suche.spec.ts` (`search_catalog_games` als `anon` über Kong, Fehlerfälle). Testnamen tragen „KAT-01 Kriterium n“.
- **Testdaten:** `support/catalog.ts` legt je Spec-Datei ein eigenes Datenset an (eigener UUID-Präfix, eigenes Kunstwort: `zqvk`/`e2ec0001-` für web, `wlpd`/`e2ec0002-` für api), im `beforeAll`, aufgeräumt im `afterAll` (idempotent: `seed` löscht zuerst). Die Dateien laufen intern seriell, untereinander parallel. Suchen mit dem Kunstwort treffen nie Startbestand oder die pgTAP-Fixtures (`qxz`). Nur lokal/CI (`support/db.ts`).
- **Bekannte Eigenheiten der Flutter-Semantics (beim Schreiben weiterer Tests beachten):** Die Trefferkacheln sind Textknoten (`span`) unter *einem* Listen-Container, nicht einzelne Elemente mit Rolle; die Liste ist virtualisiert. Während einer neuen Suche bleiben die alten Treffer sichtbar: nie nur auf „irgendeine Kachel“ warten, sondern auf Trefferzahl und erwartete Kachel. Hinweistexte stehen zusätzlich in der Live-Region `flt-announcement-polite`; Texte mit `locator('span', { hasText })` suchen. Das Semantics-Eingabefeld zeigt seinen Wert nur, solange es Fokus hat (nach Auswahlmenü oder Neuladen ist es leer); den Suchbegriff über die URL prüfen. Ein direkt geladenes `/catalog` nimmt Tipp-Eingaben nicht verlässlich an: erst über die Startseite öffnen (`openCatalog` im Spec).
- **Bekannte Eigenheiten der Flutter-Semantics (beim Schreiben weiterer Tests beachten):** Die Trefferkacheln sind Textknoten (`span`) unter *einem* Listen-Container, nicht einzelne Elemente mit Rolle; die Liste ist virtualisiert. Während einer neuen Suche bleiben die alten Treffer sichtbar: nie nur auf „irgendeine Kachel“ warten, sondern auf Trefferzahl und erwartete Kachel. Hinweistexte stehen zusätzlich in der Live-Region `flt-announcement-polite`; Texte mit `locator('span', { hasText })` suchen. Das Semantics-Eingabefeld zeigt seinen Wert nur, solange es Fokus hat (nach Auswahlmenü oder Neuladen ist es leer); den Suchbegriff über die URL prüfen. Ein direkt geladenes `/catalog` nimmt über den Semantics-Baum getippten Text nicht verlässlich an (Autofokus des Suchfelds ist beim Einschalten der Semantics schon gesetzt); ohne Semantics, mit Mausklick und Tastatur, funktioniert es (Probe: 3 von 3). Es ist ein Testwerkzeug-Effekt, kein Produktfehler. Deshalb die Suche in Tests über die Startseite öffnen (`openCatalog` im Spec).
### Latenzmessung (Projekt `perf`, KAT-01 Krit. 7, vorläufig)

View file

@ -247,8 +247,10 @@ test.describe('KAT-01 Kriterium 4 und 8: Filter (api)', () => {
expect(idSet(await search({ p_genre_ids: [ids.genreA, ids.genreA] }))).toEqual(genreA);
// Unbekannte Genre-ID: keine Treffer.
expect(await search({ p_genre_ids: [ids.genreA, '00000000-0000-4000-8000-00000000dead'] })).toEqual([]);
// Suchbegriff plus Filter.
expect(idSet(await search({ p_query: 'wlpd', p_genre_ids: [ids.genreB] }))).toEqual([ids.experte]);
// Suchbegriff UND Filter: der Begriff grenzt innerhalb des Genres ein ...
expect(idSet(await search({ p_query: 'wlpd duo', p_genre_ids: [ids.genreA] }))).toEqual([ids.duo]);
// ... und ein Begriff, der dem Filter widerspricht, ergibt nichts (Massenspiele haben das Genre nicht).
expect(await search({ p_query: 'wlpd massenspiel', p_genre_ids: [ids.genreA] })).toEqual([]);
});
test('KAT-01 Kriterium 4: Spielerzahl, Spieldauer und Mindestalter', async () => {

View file

@ -47,9 +47,10 @@ async function openSearchViaButton(page: Page): Promise<void> {
/**
* Oeffnet die Suche wie ein Nutzer (Startseite, Schaltflaeche) und wartet, bis die erste Suche
* (ganzer Katalog) abgeschlossen ist. Ein direkt geladenes /catalog nimmt Eingaben ueber den
* Semantics-Baum nicht verlaesslich an (Feld bleibt leer, Autofokus mal gesetzt, mal nicht);
* deshalb tippen die Tests nur nach diesem Einstieg. Deep Links werden ohne Tippen geprueft.
* (ganzer Katalog) abgeschlossen ist. Ein direkt geladenes /catalog nimmt Text, der ueber den erst
* danach eingeschalteten Semantics-Baum getippt wird, nicht verlaesslich an (Testwerkzeug-Effekt:
* mit Mausklick und Tastatur ohne Semantics funktioniert es); deshalb tippen die Tests nur nach
* diesem Einstieg. Deep Links werden ohne Tippen geprueft.
*/
async function openCatalog(page: Page): Promise<void> {
await openSearchViaButton(page);
@ -107,12 +108,11 @@ test.describe('KAT-01 Kriterium 1: Titelsuche (web)', () => {
test('KAT-01 Kriterium 1 / KAT-05 Kriterium 4: ausgeblendete und zusammengeführte Spiele erscheinen nicht, auch nicht über alternative Titel', async ({ page }) => {
await openCatalog(page);
await search(page, 'zqvk verborgen');
await expect(page.getByText(/^No games found for/)).toBeVisible();
await search(page, 'verstecktername');
await expect(page.getByText(/^No games found for/)).toBeVisible();
await search(page, 'zqvk zusammengefuehrt');
await expect(page.getByText(/^No games found for/)).toBeVisible();
// Je Suche die genaue Meldung abwarten: Die Meldung der vorigen Suche steht bis zur neuen Antwort noch im Baum.
for (const q of ['zqvk verborgen', 'verstecktername', 'zqvk zusammengefuehrt']) {
await search(page, q);
await expect(message(page, `No games found for “${q}”.`)).toBeVisible();
}
});
});
@ -152,17 +152,25 @@ test.describe('KAT-01 Kriterium 3: Verlagssuche (web)', () => {
test.describe('KAT-01 Kriterium 4 und 8: Filter (web)', () => {
test('KAT-01 Kriterium 4: Filter Genre und Spielerzahl mit Suchbegriff zeigen nur passende Spiele; Zurücksetzen hebt sie auf', async ({ page }) => {
await openCatalog(page);
await search(page, 'zqvk familienfest');
await expect(count(page, 1)).toBeVisible();
await search(page, 'zqvk');
// Das Genre A trägt Familienfest, Expertenrunde und Duo; "zqvk" trifft zusätzlich alle anderen.
// Das Genre A trägt Familienfest, Expertenrunde und Duo; "zqvk" allein trifft sehr viel mehr.
await expect(textCount(page)).not.toHaveText('3 games');
await chip(page, t.genreA).click();
await expect(count(page, 3)).toBeVisible();
await expect(tile(page, t.familie)).toBeVisible();
await expect(tile(page, t.experte)).toBeVisible();
await expect(tile(page, t.duo)).toBeVisible();
// Suchbegriff UND Filter: ein Begriff, der innerhalb des Genres eingrenzt ...
await search(page, 'zqvk duo');
await expect(count(page, 1)).toBeVisible();
await expect(tile(page, t.duo)).toBeVisible();
// ... und einer, der dem Filter widerspricht (Massenspiele haben das Genre nicht).
await search(page, 'zqvk massenspiel');
await expect(message(page, 'No games found for “zqvk massenspiel”.')).toBeVisible();
await search(page, 'zqvk');
await expect(count(page, 3)).toBeVisible();
// Spielerzahl 5: nur das Familienfest (3 bis 8).
await choose(page, 'players', '5 players');
await expect(count(page, 1)).toBeVisible();
@ -257,8 +265,9 @@ test.describe('KAT-01 Kriterium 6: keine Treffer (web)', () => {
await choose(page, 'players', '12 players');
await expect(message(page, 'No games match your filters.')).toBeVisible();
await page.getByRole('button', { name: 'Reset filters' }).first().click();
// Positiv: Die Liste ist zurueck (ganzer Katalog, mehr als ein Spiel), der Leerzustand ist weg.
await expect(page.getByText(/^([2-9]|\d{2,}) games$/).first()).toBeVisible();
await expect(message(page, 'No games match your filters.')).toHaveCount(0);
await expect(count(page, 1)).toHaveCount(0);
});
});

View file

@ -7,8 +7,14 @@ const gotrue = new GoTrueClient();
/** Inhalt des Semantics-Baums, ohne die (je Lauf wechselnde) Adresse. */
async function screenText(page: import('@playwright/test').Page): Promise<string> {
// Eingabefelder (mit der je Lauf wechselnden Adresse) gehoeren nicht zum Vergleich.
return (await page.locator('body').ariaSnapshot()).replace(/^\s*- textbox.*\n?/gm, '').trim();
// Eingabefelder (mit der je Lauf wechselnden Adresse) gehoeren nicht zum Vergleich. Ebenso die
// Live-Region `flt-announcement-polite` (Ansage, Textknoten auf oberster Ebene des Baums): Flutter
// leert sie zeitversetzt, ein Schnappschuss davor oder danach unterscheidet sich sonst je nach
// Rechnerlast (Flake, trat bei hoeherer Parallelauslastung auf).
return (await page.locator('body').ariaSnapshot())
.replace(/^\s*- textbox.*\n?/gm, '')
.replace(/^- text: .*\n?/gm, '')
.trim();
}
test.describe('KTO-01 Kriterium 4 (web): gleicher Bildschirm fuer neue und vorhandene Adresse', () => {