Shuffle/e2e/support/catalog.ts
cschlaefke cd96d91eae
All checks were successful
CI / flutter (pull_request) Successful in 1m29s
CI / web-build (pull_request) Successful in 1m1s
CI / supabase (pull_request) Successful in 8s
CI / supabase-tests (pull_request) Successful in 33s
CI / e2e (pull_request) Successful in 3m33s
test(catalog): Datensaetze trennen, Assertions und Latenzmessung schaerfen
Jede Suche enthaelt das Kunstwort des eigenen Datensets (Kollision web/api
behoben); Kachelzaehlung ohne first(), konkrete Trefferzahlen; screenText
wartet auf die Live-Region statt sie zu entfernen; Latenzmessung ueberspringt
bereits sichtbare Treffer, Filtersuchen ergaenzt; Ladeskripte mit Exit-Code
und TCP-Schutz; Doku aktualisiert.

Refs #3

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 21:32:29 +01:00

181 lines
7.4 KiB
TypeScript

/**
* Testdaten fuer die Katalogsuche (KAT-01, Issue #3, PR 3c). NUR lokal/CI (`withDb`, als `postgres`).
*
* Jede Spec-Datei bekommt ein EIGENES Datenset (eigener UUID-Praefix, eigenes Kunstwort), weil die
* Dateien unter `fullyParallel` gleichzeitig laufen: Raeumt eine Datei auf, darf sie keine Zeilen der
* anderen loeschen. Das Kunstwort steht in allen Titeln, alternativen Titeln und Verlagen. Die Garantie
* gilt nur, wenn JEDE Suche das Kunstwort des eigenen Datensets enthaelt (z. B. "zqvk dunkelforst"):
* Die Unterscheidungswoerter (Dunkelforst, Quarzfeld, Duo, ...) sind in beiden Sets gleich und wuerden
* ohne Kunstwort beide Sets treffen. Mit Kunstwort treffen Suchen weder das andere Set noch den
* Startbestand (auch nicht, wenn er waechst, solange kein echter Titel das Kunstwort enthaelt) noch
* die pgTAP-Fixtures (`qxz`). Ohne Suchbegriff wird nur ueber die eigenen Genres/Mechaniken gefiltert.
*
* Aufraeumen ueber den UUID-Praefix (Spiele, Verlage, Genres, Mechaniken); Zuordnungen und
* alternative Titel haengen per `on delete cascade` an den Spielen. `seed` loescht zuerst, ist also
* nach einem abgebrochenen Lauf idempotent.
*/
import type { Client } from 'pg';
import { withDb } from './db';
export interface CatalogDatasetOptions {
/** 8 Hex-Zeichen, UUID-Praefix aller Zeilen des Sets. */
prefix: string;
/** Kunstwort (nur Kleinbuchstaben a-z), steht in jedem Titel. */
word: string;
}
const uuid = (prefix: string, n: number) => `${prefix}-0000-4000-8000-${n.toString(16).padStart(12, '0')}`;
const cap = (w: string) => w.charAt(0).toUpperCase() + w.slice(1);
/** Anzahl der Massenspiele (Seitengroesse 30 + 6, KAT-01 Kriterium 5). */
export const MASS_COUNT = 36;
export function catalogDataset({ prefix, word }: CatalogDatasetOptions) {
const W = cap(word);
const id = (n: number) => uuid(prefix, n);
const mass = Array.from({ length: MASS_COUNT }, (_, i) => {
const nn = String(i + 1).padStart(2, '0');
return { id: id(1000 + i + 1), title: `${W} Massenspiel ${nn}` };
});
const ids = {
cafe: id(1),
wald: id(2),
rot: id(3),
blau: id(4),
familie: id(5),
experte: id(6),
duo: id(7),
relExact: id(10),
relBegins: id(11),
relWord: id(12),
relContains: id(13),
relPublisher: id(14),
hidden: id(20),
merged: id(21),
genreA: id(101),
genreB: id(102),
mechanicM1: id(111),
mechanicM2: id(112),
pubQuarz: id(201),
pubNebel: id(202),
pubRelevanz: id(203),
};
const t = {
cafe: `${W} Café Größe: Duel®`,
wald: `${W} Schattenwald`,
waldAltDe: `${W} Dunkelforst`,
waldAltEn: `${W} Shadowwood`,
rot: `${W} Rotkehlchen`,
blau: `${W} Blaumeise`,
familie: `${W} Familienfest`,
experte: `${W} Expertenrunde`,
duo: `${W} Duo`,
relExact: `${W} Relevanz`,
relBegins: `${W} Relevanz Deluxe`,
relWord: `Die ${W} Relevanz`,
relContains: `X${W} Relevanzen`,
relPublisher: `${W} Fremdtitel`,
hidden: `${W} Verborgen`,
merged: `${W} Zusammengefuehrt`,
genreA: `${W} Genre A`,
genreB: `${W} Genre B`,
mechanicM1: `${W} Mechanik 1`,
mechanicM2: `${W} Mechanik 2`,
pubQuarz: `Quarzfeld ${W} Presse`,
pubNebel: `Nebelkamm ${W} Verlag`,
pubRelevanz: `Relevanz Verlag ${W}`,
};
return { word, W, prefix, ids, titles: t, mass };
}
export type CatalogDataset = ReturnType<typeof catalogDataset>;
/** Loescht alle Zeilen des Sets (anhand des UUID-Praefixes). */
export async function cleanupCatalog(c: Client, ds: Pick<CatalogDataset, 'prefix'>): Promise<void> {
const like = `${ds.prefix}-%`;
await c.query(`delete from public.games where id::text like $1`, [like]);
await c.query(`delete from public.publishers where id::text like $1`, [like]);
await c.query(`delete from public.genres where id::text like $1`, [like]);
await c.query(`delete from public.mechanics where id::text like $1`, [like]);
}
/** Legt das Datenset an (nach Aufraeumen). */
export async function seedCatalog(ds: CatalogDataset): Promise<void> {
const { ids: i, titles: t } = ds;
await withDb(async (c) => {
await cleanupCatalog(c, ds);
await c.query(
`insert into public.genres (id, name_de, name_en) values ($1, $2, $2), ($3, $4, $4)`,
[i.genreA, t.genreA, i.genreB, t.genreB],
);
await c.query(
`insert into public.mechanics (id, name_de, name_en) values ($1, $2, $2), ($3, $4, $4)`,
[i.mechanicM1, t.mechanicM1, i.mechanicM2, t.mechanicM2],
);
await c.query(
`insert into public.publishers (id, name) values ($1, $2), ($3, $4), ($5, $6)`,
[i.pubQuarz, t.pubQuarz, i.pubNebel, t.pubNebel, i.pubRelevanz, t.pubRelevanz],
);
// (id, title, year, min_p, max_p, min_min, max_min, min_age)
const games: [string, string, number | null, number | null, number | null, number | null, number | null, number | null][] = [
[i.cafe, t.cafe, 2021, 2, 2, 20, 30, 12],
[i.wald, t.wald, 2019, 2, 4, 45, 60, 10],
[i.rot, t.rot, 2018, 2, 4, 30, 45, 8],
[i.blau, t.blau, 2022, 2, 5, 30, 45, 8],
[i.familie, t.familie, 2017, 3, 8, 20, 45, 6],
[i.experte, t.experte, 2016, 1, 2, 120, 180, 14],
[i.duo, t.duo, 2015, 2, 2, 30, 30, 8],
[i.relExact, t.relExact, 2000, 2, 4, 30, 60, 10],
[i.relBegins, t.relBegins, 2000, 2, 4, 30, 60, 10],
[i.relWord, t.relWord, 2000, 2, 4, 30, 60, 10],
[i.relContains, t.relContains, 2000, 2, 4, 30, 60, 10],
[i.relPublisher, t.relPublisher, 2000, 2, 4, 30, 60, 10],
...ds.mass.map(
(m) => [m.id, m.title, 2020, 2, 4, 30, 60, 10] as [string, string, number, number, number, number, number, number],
),
];
for (const g of games) {
await c.query(
`insert into public.games (id, title, visibility, year, min_players, max_players, min_minutes, max_minutes, min_age)
values ($1, $2, 'catalog', $3, $4, $5, $6, $7, $8)`,
g,
);
}
// Ausgeblendet und zusammengefuehrt: duerfen nie erscheinen (KAT-05 Kriterium 4).
await c.query(
`insert into public.games (id, title, visibility, hidden_at) values ($1, $2, 'catalog', now())`,
[i.hidden, t.hidden],
);
await c.query(
`insert into public.games (id, title, visibility, hidden_at, merged_into_id) values ($1, $2, 'catalog', now(), $3)`,
[i.merged, t.merged, i.relExact],
);
await c.query(
`insert into public.game_alt_titles (game_id, locale, title) values ($1, 'de', $2), ($1, 'en', $3)`,
[i.wald, t.waldAltDe, t.waldAltEn],
);
// Der alternative Titel des ausgeblendeten Spiels darf es nicht auffindbar machen.
await c.query(`insert into public.game_alt_titles (game_id, locale, title) values ($1, 'de', $2)`, [
i.hidden,
`${ds.W} Verstecktername`,
]);
await c.query(
`insert into public.game_publishers (game_id, publisher_id) values ($1, $2), ($3, $2), ($3, $4), ($5, $6)`,
[i.rot, i.pubQuarz, i.blau, i.pubNebel, i.relPublisher, i.pubRelevanz],
);
await c.query(
`insert into public.game_genres (game_id, genre_id) values ($1, $2), ($3, $2), ($4, $2), ($4, $5)`,
[i.familie, i.genreA, i.duo, i.experte, i.genreB],
);
await c.query(
`insert into public.game_mechanics (game_id, mechanic_id) values ($1, $3), ($2, $3)`,
[i.experte, i.duo, i.mechanicM1],
);
});
}
export async function cleanupCatalogDataset(ds: CatalogDataset): Promise<void> {
await withDb((c) => cleanupCatalog(c, ds));
}