test(catalog): Assertions der Suche geschaerft, Doku zu Laufzeit und Flake
Meldung je Suche genau abwarten (kein Treffer durch Reste der vorigen Suche), Suchbegriff UND Filter zusammen pruefen, positive Pruefung nach Filter-Reset; screenText der Anmelde-Tests ignoriert die Live-Region. Refs #3 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
4bf9e3052a
commit
f6103fd6aa
5 changed files with 46 additions and 23 deletions
|
|
@ -112,8 +112,14 @@ Je Art (p50): Titel 436, Titelanfang 455, Wort 451, Wortkombination 437, Verlag
|
||||||
### 5.3 Befunde und offene Punkte
|
### 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.
|
- **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.
|
- **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:** 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).
|
- **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).
|
- **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).
|
- 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.
|
- 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.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@ die Tests laufen deshalb parallel.
|
||||||
|
|
||||||
| Projekt | Ziel | Voraussetzung | Tests |
|
| Projekt | Ziel | Voraussetzung | Tests |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `api` | GoTrue und `search_catalog_games` über Kong (`:54321`) + Mailpit (`:54324`) + Postgres (`:54322`) | `supabase start` | 70 |
|
| `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` | 44 |
|
| `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 |
|
| `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 |
|
| `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“.
|
- `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`).
|
- **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)
|
### Latenzmessung (Projekt `perf`, KAT-01 Krit. 7, vorläufig)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
expect(idSet(await search({ p_genre_ids: [ids.genreA, ids.genreA] }))).toEqual(genreA);
|
||||||
// Unbekannte Genre-ID: keine Treffer.
|
// Unbekannte Genre-ID: keine Treffer.
|
||||||
expect(await search({ p_genre_ids: [ids.genreA, '00000000-0000-4000-8000-00000000dead'] })).toEqual([]);
|
expect(await search({ p_genre_ids: [ids.genreA, '00000000-0000-4000-8000-00000000dead'] })).toEqual([]);
|
||||||
// Suchbegriff plus Filter.
|
// Suchbegriff UND Filter: der Begriff grenzt innerhalb des Genres ein ...
|
||||||
expect(idSet(await search({ p_query: 'wlpd', p_genre_ids: [ids.genreB] }))).toEqual([ids.experte]);
|
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 () => {
|
test('KAT-01 Kriterium 4: Spielerzahl, Spieldauer und Mindestalter', async () => {
|
||||||
|
|
|
||||||
|
|
@ -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
|
* 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
|
* (ganzer Katalog) abgeschlossen ist. Ein direkt geladenes /catalog nimmt Text, der ueber den erst
|
||||||
* Semantics-Baum nicht verlaesslich an (Feld bleibt leer, Autofokus mal gesetzt, mal nicht);
|
* danach eingeschalteten Semantics-Baum getippt wird, nicht verlaesslich an (Testwerkzeug-Effekt:
|
||||||
* deshalb tippen die Tests nur nach diesem Einstieg. Deep Links werden ohne Tippen geprueft.
|
* 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> {
|
async function openCatalog(page: Page): Promise<void> {
|
||||||
await openSearchViaButton(page);
|
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 }) => {
|
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 openCatalog(page);
|
||||||
await search(page, 'zqvk verborgen');
|
// Je Suche die genaue Meldung abwarten: Die Meldung der vorigen Suche steht bis zur neuen Antwort noch im Baum.
|
||||||
await expect(page.getByText(/^No games found for/)).toBeVisible();
|
for (const q of ['zqvk verborgen', 'verstecktername', 'zqvk zusammengefuehrt']) {
|
||||||
await search(page, 'verstecktername');
|
await search(page, q);
|
||||||
await expect(page.getByText(/^No games found for/)).toBeVisible();
|
await expect(message(page, `No games found for “${q}”.`)).toBeVisible();
|
||||||
await search(page, 'zqvk zusammengefuehrt');
|
}
|
||||||
await expect(page.getByText(/^No games found for/)).toBeVisible();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -152,17 +152,25 @@ test.describe('KAT-01 Kriterium 3: Verlagssuche (web)', () => {
|
||||||
test.describe('KAT-01 Kriterium 4 und 8: Filter (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 }) => {
|
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 openCatalog(page);
|
||||||
await search(page, 'zqvk familienfest');
|
|
||||||
await expect(count(page, 1)).toBeVisible();
|
|
||||||
|
|
||||||
await search(page, 'zqvk');
|
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 chip(page, t.genreA).click();
|
||||||
await expect(count(page, 3)).toBeVisible();
|
await expect(count(page, 3)).toBeVisible();
|
||||||
await expect(tile(page, t.familie)).toBeVisible();
|
await expect(tile(page, t.familie)).toBeVisible();
|
||||||
await expect(tile(page, t.experte)).toBeVisible();
|
await expect(tile(page, t.experte)).toBeVisible();
|
||||||
await expect(tile(page, t.duo)).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).
|
// Spielerzahl 5: nur das Familienfest (3 bis 8).
|
||||||
await choose(page, 'players', '5 players');
|
await choose(page, 'players', '5 players');
|
||||||
await expect(count(page, 1)).toBeVisible();
|
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 choose(page, 'players', '12 players');
|
||||||
await expect(message(page, 'No games match your filters.')).toBeVisible();
|
await expect(message(page, 'No games match your filters.')).toBeVisible();
|
||||||
await page.getByRole('button', { name: 'Reset filters' }).first().click();
|
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(message(page, 'No games match your filters.')).toHaveCount(0);
|
||||||
await expect(count(page, 1)).toHaveCount(0);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,14 @@ const gotrue = new GoTrueClient();
|
||||||
|
|
||||||
/** Inhalt des Semantics-Baums, ohne die (je Lauf wechselnde) Adresse. */
|
/** Inhalt des Semantics-Baums, ohne die (je Lauf wechselnde) Adresse. */
|
||||||
async function screenText(page: import('@playwright/test').Page): Promise<string> {
|
async function screenText(page: import('@playwright/test').Page): Promise<string> {
|
||||||
// Eingabefelder (mit der je Lauf wechselnden Adresse) gehoeren nicht zum Vergleich.
|
// Eingabefelder (mit der je Lauf wechselnden Adresse) gehoeren nicht zum Vergleich. Ebenso die
|
||||||
return (await page.locator('body').ariaSnapshot()).replace(/^\s*- textbox.*\n?/gm, '').trim();
|
// 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', () => {
|
test.describe('KTO-01 Kriterium 4 (web): gleicher Bildschirm fuer neue und vorhandene Adresse', () => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue