test(auth): Ansagen der Live-Region im Bildschirmvergleich pruefen
All checks were successful
All checks were successful
screenText sammelt Ansagen per MutationObserver und vergleicht sie; wartet auf leere Region mit registrierter Ansage bzw. 400 ms Ruhe. Unload-Skript mit TCP-Schutz, Doku-Praezisierungen. Refs #3 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
cd96d91eae
commit
8b11df2727
4 changed files with 74 additions and 18 deletions
|
|
@ -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
|
### 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).
|
- 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` 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).
|
- 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).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -72,13 +72,13 @@ async function choose(page: Page, field: keyof typeof dropdownIndex, option: str
|
||||||
await page.getByRole('menuitem', { name: option, exact: true }).click();
|
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
|
* 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.
|
* 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 message = (page: Page, text: string) => page.locator('span', { hasText: text });
|
||||||
|
|
||||||
const chip = (page: Page, name: string) => page.getByRole('checkbox', { name, exact: true });
|
const chip = (page: Page, name: string) => page.getByRole('checkbox', { name, exact: true });
|
||||||
|
|
|
||||||
|
|
@ -5,23 +5,67 @@ import { freshBrowserContext, fillField, login, open, register, t, textOf } from
|
||||||
|
|
||||||
const gotrue = new GoTrueClient();
|
const gotrue = new GoTrueClient();
|
||||||
|
|
||||||
/** Inhalt des Semantics-Baums, ohne die (je Lauf wechselnde) Adresse. */
|
type Page = import('@playwright/test').Page;
|
||||||
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
|
* Sammelt ab dem naechsten Seitenladen jede Ansage der Live-Regionen (`flt-announcement-polite/-assertive`)
|
||||||
// (der Baum selbst wird nicht veraendert, die Live-Region wird nicht ausgeblendet).
|
* 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
|
await expect
|
||||||
.poll(() =>
|
.poll(() =>
|
||||||
page.evaluate(() =>
|
page.evaluate(() => {
|
||||||
Array.from(document.querySelectorAll('flt-announcement-polite, flt-announcement-assertive'))
|
const w = window as any;
|
||||||
.map((e) => e.textContent ?? '')
|
const empty =
|
||||||
.join('')
|
Array.from(document.querySelectorAll('flt-announcement-polite, flt-announcement-assertive'))
|
||||||
.trim(),
|
.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.
|
// 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', () => {
|
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,
|
browser,
|
||||||
}) => {
|
}) => {
|
||||||
// Neue Adresse.
|
// Neue Adresse.
|
||||||
|
await trackAnnouncements(page);
|
||||||
await register(page, { email: randomEmail(), password: randomPassword() });
|
await register(page, { email: randomEmail(), password: randomPassword() });
|
||||||
await expect(page).toHaveURL(/\/register\/check-email$/);
|
await expect(page).toHaveURL(/\/register\/check-email$/);
|
||||||
await expect(textOf(page, t.checkEmailTitle)).toBeVisible();
|
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());
|
await gotrue.createConfirmedUser(existing, randomPassword());
|
||||||
const second = await (await freshBrowserContext(browser)).newPage();
|
const second = await (await freshBrowserContext(browser)).newPage();
|
||||||
try {
|
try {
|
||||||
|
await trackAnnouncements(second);
|
||||||
await register(second, { email: existing, password: randomPassword() });
|
await register(second, { email: existing, password: randomPassword() });
|
||||||
await expect(second).toHaveURL(/\/register\/check-email$/);
|
await expect(second).toHaveURL(/\/register\/check-email$/);
|
||||||
await expect(textOf(second, t.checkEmailTitle)).toBeVisible();
|
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();
|
const password = randomPassword();
|
||||||
await gotrue.createConfirmedUser(email, password);
|
await gotrue.createConfirmedUser(email, password);
|
||||||
|
|
||||||
|
await trackAnnouncements(page);
|
||||||
await login(page, email, `${password}-falsch`);
|
await login(page, email, `${password}-falsch`);
|
||||||
await expect(textOf(page, t.invalidCredentials)).toBeVisible();
|
await expect(textOf(page, t.invalidCredentials)).toBeVisible();
|
||||||
const wrongPassword = await screenText(page);
|
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);
|
const ctx = await freshBrowserContext(browser);
|
||||||
try {
|
try {
|
||||||
const other = await ctx.newPage();
|
const other = await ctx.newPage();
|
||||||
|
await trackAnnouncements(other);
|
||||||
await login(other, randomEmail('unknown'), password);
|
await login(other, randomEmail('unknown'), password);
|
||||||
await expect(textOf(other, t.invalidCredentials)).toBeVisible();
|
await expect(textOf(other, t.invalidCredentials)).toBeVisible();
|
||||||
expect(await screenText(other)).toBe(wrongPassword);
|
expect(await screenText(other)).toBe(wrongPassword);
|
||||||
|
|
|
||||||
|
|
@ -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$;
|
do $abort$ begin raise exception 'Abbruch (siehe Meldung davor)'; end $abort$;
|
||||||
\endif
|
\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;
|
begin;
|
||||||
delete from public.games where id::text like '9e4f0000-%';
|
delete from public.games where id::text like '9e4f0000-%';
|
||||||
delete from public.publishers where id::text like '9e4f0000-%';
|
delete from public.publishers where id::text like '9e4f0000-%';
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue