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
4 changed files with 74 additions and 18 deletions
Showing only changes of commit 8b11df2727 - Show all commits

View file

@ -125,6 +125,6 @@ Je Art (p50) im ersten Lauf dieser Fassung: Textsuchen 436 bis 455 ms (Titel, Ti
### 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). `screenText` in `krit04-06-07-anmeldung.spec.ts` wartet jetzt vor dem Schnappschuss, bis die Live-Region (`flt-announcement-polite/-assertive`, wird von Flutter nach etwa 250 ms geleert) leer ist; der Baum selbst wird nicht verändert, die Prüfung wird nicht abgeschwächt. Die übrigen Instabilitäten sind nicht untersucht (allgemeine Robustheit der Anmelde-Tests: Issue #68).
- 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 (ein Lauf, lokal; CI nicht gemessen); 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). `screenText` in `krit04-06-07-anmeldung.spec.ts` sammelt jetzt über einen `MutationObserver` (Init-Skript) jede nichtleere Ansage der Live-Regionen (`flt-announcement-polite/-assertive`, getrimmt) und gibt sie zusammen mit dem Baum zurück; der Test vergleicht sie zwischen neuer und vorhandener Adresse bzw. falscher E-Mail und falschem Passwort. Der Schnappschuss entsteht erst, wenn die Region leer ist und entweder mindestens eine Ansage registriert wurde oder die Region 400 ms am Stück leer war (Flutter setzt die Ansage zeitversetzt und leert sie nach 300 ms). Damit ist die Prüfung gegenüber vorher nicht abgeschwächt, sondern um den Vergleich der Ansagen erweitert. Die übrigen Instabilitäten sind nicht untersucht (allgemeine Robustheit der Anmelde-Tests: Issue #68).

View file

@ -72,13 +72,13 @@ async function choose(page: Page, field: keyof typeof dropdownIndex, option: str
await page.getByRole('menuitem', { name: option, exact: true }).click();
}
/** Alle Kacheln zu einem Titel (ohne .first()), zum Zaehlen. */
const tiles = (page: Page, title: string) => page.getByText(new RegExp(`^${escapeRe(title)}(,|$)`));
/**
* Hinweistext im Semantics-Baum. Nur `span`: Dieselben Texte stehen zusaetzlich in der
* Live-Region `flt-announcement-polite` (Ansage), die nach dem Verschwinden noch kurz bleibt.
*/
/** Alle Kacheln zu einem Titel (ohne .first()), zum Zaehlen. */
const tiles = (page: Page, title: string) => page.getByText(new RegExp(`^${escapeRe(title)}(,|$)`));
const message = (page: Page, text: string) => page.locator('span', { hasText: text });
const chip = (page: Page, name: string) => page.getByRole('checkbox', { name, exact: true });

View file

@ -5,23 +5,67 @@ import { freshBrowserContext, fillField, login, open, register, t, textOf } from
const gotrue = new GoTrueClient();
/** Inhalt des Semantics-Baums, ohne die (je Lauf wechselnde) Adresse. */
async function screenText(page: import('@playwright/test').Page): Promise<string> {
// Die Live-Region (Ansage) wird von Flutter nach etwa 250 ms geleert; ein Schnappschuss davor
// enthaelt sie, einer danach nicht. Fuer einen stabilen Vergleich zuerst ihr Verklingen abwarten
// (der Baum selbst wird nicht veraendert, die Live-Region wird nicht ausgeblendet).
type Page = import('@playwright/test').Page;
/**
* Sammelt ab dem naechsten Seitenladen jede Ansage der Live-Regionen (`flt-announcement-polite/-assertive`)
* in `window.__announcements` (nur nichtleerer, getrimmter Text; Flutter haengt abwechselnd ein Leerzeichen an,
* damit gleiche Ansagen erneut vorgelesen werden). Vor `register`/`login` der Seite aufrufen.
*/
async function trackAnnouncements(page: Page): Promise<void> {
await page.addInitScript(() => {
const w = window as any;
w.__announcements = [] as { text: string; path: string }[];
w.__annLastChange = performance.now();
let last = '';
const read = () =>
Array.from(document.querySelectorAll('flt-announcement-polite, flt-announcement-assertive'))
.map((e) => (e.textContent ?? '').trim())
.join('')
.trim();
const start = () => {
new MutationObserver(() => {
const cur = read();
if (cur !== last) w.__annLastChange = performance.now();
if (cur !== '' && last === '') w.__announcements.push({ text: cur, path: location.pathname });
last = cur;
}).observe(document, { subtree: true, childList: true, characterData: true });
};
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start);
else start();
});
}
/**
* Inhalt des Semantics-Baums, ohne die (je Lauf wechselnde) Adresse, PLUS alle gesammelten Ansagen.
* Die Live-Region ist fuer Screenreader-Nutzer Teil des Bildschirms (Enumerationsschutz): Ansagen fuer neue
* und vorhandene Adresse muessen gleich sein. Flutter setzt eine Ansage zeitversetzt und leert sie nach 300 ms;
* deshalb zuerst warten, bis die Region leer ist UND entweder mindestens eine Ansage registriert wurde oder die
* Region 400 ms am Stueck leer war (Seite ohne Ansage). Erst dann der Schnappschuss.
*/
async function screenText(page: Page): Promise<string> {
await expect
.poll(() =>
page.evaluate(() =>
Array.from(document.querySelectorAll('flt-announcement-polite, flt-announcement-assertive'))
.map((e) => e.textContent ?? '')
.join('')
.trim(),
),
page.evaluate(() => {
const w = window as any;
const empty =
Array.from(document.querySelectorAll('flt-announcement-polite, flt-announcement-assertive'))
.map((e) => (e.textContent ?? '').trim())
.join('') === '';
return empty && (w.__announcements.some((a: any) => a.path === location.pathname) || performance.now() - w.__annLastChange >= 400);
}),
)
.toBe('');
.toBe(true);
// Zaehlerstaende der Eingabefelder ("7/30") werden beim Tippen angesagt und haengen vom getippten Text ab, nicht vom Ergebnis.
// Nur Ansagen der aktuellen Seite (Pfad): Ansagen waehrend des Ausfuellens (Validierung, Zaehler) gehoeren zur
// Eingabe, nicht zum Ergebnis der Absendung.
const path = new URL(page.url()).pathname;
const announcements = ((await page.evaluate(() => (window as any).__announcements)) as { text: string; path: string }[])
.filter((a) => a.path === path && !/^\d+\/\d+$/.test(a.text))
.map((a) => a.text);
// Eingabefelder (mit der je Lauf wechselnden Adresse) gehoeren nicht zum Vergleich.
return (await page.locator('body').ariaSnapshot()).replace(/^\s*- textbox.*\n?/gm, '').trim();
const tree = (await page.locator('body').ariaSnapshot()).replace(/^\s*- textbox.*\n?/gm, '').trim();
return `${tree}\n--- Ansagen ---\n${JSON.stringify(announcements)}`;
}
test.describe('KTO-01 Kriterium 4 (web): gleicher Bildschirm fuer neue und vorhandene Adresse', () => {
@ -30,6 +74,7 @@ test.describe('KTO-01 Kriterium 4 (web): gleicher Bildschirm fuer neue und vorha
browser,
}) => {
// Neue Adresse.
await trackAnnouncements(page);
await register(page, { email: randomEmail(), password: randomPassword() });
await expect(page).toHaveURL(/\/register\/check-email$/);
await expect(textOf(page, t.checkEmailTitle)).toBeVisible();
@ -40,6 +85,7 @@ test.describe('KTO-01 Kriterium 4 (web): gleicher Bildschirm fuer neue und vorha
await gotrue.createConfirmedUser(existing, randomPassword());
const second = await (await freshBrowserContext(browser)).newPage();
try {
await trackAnnouncements(second);
await register(second, { email: existing, password: randomPassword() });
await expect(second).toHaveURL(/\/register\/check-email$/);
await expect(textOf(second, t.checkEmailTitle)).toBeVisible();
@ -76,6 +122,7 @@ test.describe('KTO-01 Kriterium 7 (web): keine Auskunft ueber Konten, Drosselung
const password = randomPassword();
await gotrue.createConfirmedUser(email, password);
await trackAnnouncements(page);
await login(page, email, `${password}-falsch`);
await expect(textOf(page, t.invalidCredentials)).toBeVisible();
const wrongPassword = await screenText(page);
@ -83,6 +130,7 @@ test.describe('KTO-01 Kriterium 7 (web): keine Auskunft ueber Konten, Drosselung
const ctx = await freshBrowserContext(browser);
try {
const other = await ctx.newPage();
await trackAnnouncements(other);
await login(other, randomEmail('unknown'), password);
await expect(textOf(other, t.invalidCredentials)).toBeVisible();
expect(await screenText(other)).toBe(wrongPassword);

View file

@ -20,6 +20,14 @@ select :'confirm_local' = 'yes' as confirm_ok, coalesce(:'HOST', '') like '/%' a
do $abort$ begin raise exception 'Abbruch (siehe Meldung davor)'; end $abort$;
\endif
do $guard$
begin
if inet_server_addr() is not null then
raise exception 'Abbruch: Verbindung ueber TCP (inet_server_addr gesetzt); nur Unix-Socket im lokalen Container.';
end if;
end
$guard$;
begin;
delete from public.games where id::text like '9e4f0000-%';
delete from public.publishers where id::text like '9e4f0000-%';