Acht Meilensteine lagen ohne fremden Blick auf main. Ein Review mit eigener
Datenbank, Gegenproben und vollem Suitenlauf hat sie gegen die
Akzeptanzkriterien und gegen CLAUDE.md geprueft.
Die Kernregeln halten: fuenf von fuenf Gegenproben gegen Versionierung,
Mandantengrenze, Abschlussregel und Selbstauskunft wurden rot. Alle Befunde
lagen am Rand — und die drei wertvollsten waren Gegenproben, die gruen blieben.
Schwer:
- Der Link in der Mail "Verzeichnis-Abgleich fehlgeschlagen" war ein 404
(/verwaltung/verzeichnis statt /admin/verzeichnis). Die eine Meldung, die an
alle Administratoren geht, im Moment eines Ausfalls. Ein Test prueft jetzt
jeden Meldungspfad gegen die vorhandenen Routen.
- Der Export eines Entwurfs mit Baustein lieferte einen leeren Abschnitt unter
dem Platzhalter "wird ersetzt" — der Zweck des Formats ist, eine Vorlage in
eine zweite Installation zu bringen. Bausteine werden jetzt aufgeloest.
Mittel:
- Der Anhang-Handler hatte keinen Test; die ganze Rechtepruefung liess sich
durch einen hartcodierten ADMIN ersetzen, Suite gruen. Es ist die einzige
Stelle, die rohe Dateibytes ausliefert. tests/anhang-route.test.ts faehrt ihn
jetzt echt, acht Pruefungen.
- /api/scim in der Proxy-Allowlist stand in keinem Test. OEFFENTLICH ist
exportiert, der Test vergleicht beide Richtungen: eine Liste, die nur in einer
Richtung geprueft wird, laeuft dem Code hinterher.
- Ein offener Ereignisstrom ueberlebte Abmelden, Deaktivieren und
Sitzungswiderruf. Der Herzschlag prueft die Sitzung jetzt nach. Dass es kein
Test zeigte, lag an einer Attrappe `sitzung: {}` — eine Attrappe ohne die Form
der Wirklichkeit prueft etwas anderes als die Wirklichkeit.
- README kannte MITARBEITER nicht und behauptete, unbekannte Benutzer wuerden
angelegt. Wer auf v0.2.0 hebt und nur das README liest, haette genau diese
Leute ausgesperrt.
- sicherung.md nannte einen Anhaenge-Pfad, den es nirgends gibt — als
Rueckfallwert in einem Skript mit rm -rf. Jetzt bricht es ab statt zu raten.
- Attributnamen wurden nur in der Server Action geprueft, waehrend der
Graph-Leser sich im Kommentar darauf berief.
Dazu #027: die Ende-zu-Ende-Ablaeufe raeumen jetzt zentral auf, vor und nach
dem Lauf, und ein struktureller Test bewacht es. Beim ersten echten Lauf seit
dem 26. September waren sieben Ablaeufe rot, vier Ursachen, keine ein
Produktfehler — unbemerkt, weil pnpm test:e2e in keinem Tor steht. Das ist jetzt
#038 und keine Fussnote.
Ein Befund des Reviews war falsch (der Zustands-Handler habe keinen Test; er hat
acht direkte Aufrufe). Nachgeprueft, bevor etwas geaendert wurde.
1454 Vitest-Tests (vorher 1432), 113 E2E-Ablaeufe gruen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
fcd67018b8
commit
80d97467da
@@ -0,0 +1,185 @@
|
||||
import { prisma } from "@/lib/db";
|
||||
|
||||
/**
|
||||
* Was die Ende-zu-Ende-Abläufe hinterlassen, wieder wegräumen (#027).
|
||||
*
|
||||
* **Alles, was ein Ablauf anlegt, beginnt mit `ZZ`.** Das Präfix sortiert
|
||||
* die Zeilen in jeder Liste ans Ende und ist zugleich der Griff, an dem sie
|
||||
* wieder herausgehen — ohne gemerkte Kennungen, denn ein Lauf, der mitten im
|
||||
* Ablauf abbricht, merkt sich nichts.
|
||||
*
|
||||
* **Warum vor *und* nach dem Lauf geräumt wird.** Ein `afterAll` je Datei und
|
||||
* ein `globalTeardown` laufen bei einem Testfehler und bei einem Zeitablauf —
|
||||
* aber nicht, wenn jemand `Strg+C` drückt oder der Prozess hart stirbt. Dann
|
||||
* bleiben die Zeilen stehen, und der nächste Lauf findet sie vor. Deshalb räumt
|
||||
* derselbe Code auch im Vorlauf: was vom letzten Mal übrig ist, geht weg, bevor
|
||||
* der erste Ablauf startet. Ein Vorlauf kann nicht übersprungen werden.
|
||||
*
|
||||
* Aufgefallen ist das Ganze, weil in der Entwicklungsdatenbank 105 Vorlagen mit
|
||||
* Namen wie `ZZ E2E Editor 1790324192596 C` standen — aus einem Fenster von
|
||||
* sechzehn Stunden, in dem noch kein Ablauf aufräumte. Der Schaden liegt nicht
|
||||
* bei den Tests, sondern bei allem, was **danach** in dieselbe Datenbank sieht:
|
||||
* die Vorlagenliste, die Screenshots, jede Abfrage von Hand. Ein Fehler, der
|
||||
* „zu viele Treffer“ heißt, kostet dann eine Stunde.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Das Präfix, das jeder Ablauf seinen Zeilen gibt.
|
||||
*
|
||||
* **`ZZ` und nicht `ZZ E2E`**, weil die Abläufe es in fünf Schreibweisen
|
||||
* benutzen: `ZZ E2E Editor …`, `ZZ-E2E-<zeit>-PC-01` (Gerätenamen dürfen keine
|
||||
* Leerzeichen haben), `ZZPruef…`, `ZZQuelle…` und `ZZ Abteilung …`. Alle fünf
|
||||
* sind sinnvoll — ein Hostname sieht anders aus als ein Vorlagenname —, und
|
||||
* alle fünf beginnen mit `ZZ`. Darauf zu greifen deckt sie ab, ohne sechzehn
|
||||
* Dateien umzubenennen.
|
||||
*
|
||||
* Zwei Buchstaben sind kurz. Das ist hier in Ordnung: geräumt wird nur beim
|
||||
* Ende-zu-Ende-Lauf und nur gegen die Entwicklungsdatenbank, und `ZZ` am
|
||||
* **Anfang** eines Namens ist in echten Daten dieses Hauses nichts, was jemand
|
||||
* tippt. Der Grund für die Wahl ist ohnehin derselbe: `ZZ` sortiert in jeder
|
||||
* Liste ans Ende.
|
||||
*/
|
||||
export const E2E_PRAEFIX = "ZZ";
|
||||
|
||||
/** Was aufgeräumt wurde — damit der Aufrufer es melden kann. */
|
||||
export type Aufgeraeumt = Readonly<Record<string, number>>;
|
||||
|
||||
/**
|
||||
* Räumt alles mit `ZZ` weg und meldet, was es war.
|
||||
*
|
||||
* Die Reihenfolge ist nicht beliebig: erst was an einem Lauf hängt, dann die
|
||||
* Läufe, dann woran ein Lauf hängen könnte. Ein Fremdschlüssel, der sich
|
||||
* beschwert, ist dabei **kein** Fehler dieser Funktion, sondern die Auskunft,
|
||||
* dass ein Ablauf etwas verknüpft hat, das er nicht aufräumen kann — dann soll
|
||||
* es auffallen und nicht stillschweigend bleiben.
|
||||
*/
|
||||
export async function e2eRestlosAufraeumen(): Promise<Aufgeraeumt> {
|
||||
const praefix = { startsWith: E2E_PRAEFIX };
|
||||
const gezaehlt: Record<string, number> = {};
|
||||
const merken = (was: string, anzahl: number): void => {
|
||||
if (anzahl > 0) gezaehlt[was] = (gezaehlt[was] ?? 0) + anzahl;
|
||||
};
|
||||
|
||||
/*
|
||||
* `Protokoll.entitaetId` ist **kein** Fremdschlüssel: die Zeilen überleben
|
||||
* ihren Gegenstand absichtlich (ein archivierter Lauf soll seine Geschichte
|
||||
* behalten). Hier heißt das, dass sie als Waisen stehen bleiben, wenn sie
|
||||
* nicht mitgenommen werden — darum werden die Kennungen vorher gesammelt.
|
||||
*/
|
||||
const laeufe = await prisma.lauf.findMany({ where: { titel: praefix }, select: { id: true } });
|
||||
const vorlagen = await prisma.vorlage.findMany({
|
||||
where: { name: praefix },
|
||||
select: { id: true, versionen: { select: { id: true } } },
|
||||
});
|
||||
const bausteine = await prisma.baustein.findMany({
|
||||
where: { name: praefix },
|
||||
select: { id: true, versionen: { select: { id: true } } },
|
||||
});
|
||||
const geraete = await prisma.geraet.findMany({
|
||||
where: { OR: [{ bezeichnung: praefix }, { hostname: praefix }] },
|
||||
select: { id: true },
|
||||
});
|
||||
const personen = await prisma.mitarbeiter.findMany({
|
||||
where: { anzeigename: praefix },
|
||||
select: { id: true },
|
||||
});
|
||||
const abteilungen = await prisma.abteilung.findMany({
|
||||
where: { name: praefix },
|
||||
select: { id: true },
|
||||
});
|
||||
const quellen = await prisma.verzeichnisquelle.findMany({
|
||||
where: { name: praefix },
|
||||
select: { id: true },
|
||||
});
|
||||
const benutzer = await prisma.benutzer.findMany({
|
||||
where: { name: praefix },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
/*
|
||||
* **Waisen werden nicht erraten.** Eine Protokollzeile, deren Gegenstand schon
|
||||
* fort ist, bleibt stehen — ihre `entitaetId` ist hier nicht mehr zu finden.
|
||||
* Sie über den Namen im Json zu greifen wäre naheliegend und falsch: derselbe
|
||||
* Name steht auch in echter Historie einer Zeile, die es weiterhin gibt (etwa
|
||||
* „Version veröffentlicht“ mit der Liste der eingesetzten Bausteine). Dass
|
||||
* eine solche Zeile ihren Gegenstand überlebt, ist genau der Grund, warum
|
||||
* `entitaetId` kein Fremdschlüssel ist.
|
||||
*/
|
||||
const laufIds = laeufe.map((zeile) => zeile.id);
|
||||
const betroffen = [
|
||||
...laufIds,
|
||||
...vorlagen.flatMap((v) => [v.id, ...v.versionen.map((version) => version.id)]),
|
||||
...bausteine.flatMap((b) => [b.id, ...b.versionen.map((version) => version.id)]),
|
||||
...geraete.map((zeile) => zeile.id),
|
||||
...personen.map((zeile) => zeile.id),
|
||||
...abteilungen.map((zeile) => zeile.id),
|
||||
...quellen.map((zeile) => zeile.id),
|
||||
...benutzer.map((zeile) => zeile.id),
|
||||
];
|
||||
|
||||
if (laufIds.length > 0) {
|
||||
merken("Anhänge", (await prisma.anhang.deleteMany({ where: { laufId: { in: laufIds } } })).count);
|
||||
merken("Kommentare", (await prisma.kommentar.deleteMany({ where: { laufId: { in: laufIds } } })).count);
|
||||
merken(
|
||||
"Schrittergebnisse",
|
||||
(await prisma.schrittergebnis.deleteMany({ where: { laufId: { in: laufIds } } })).count,
|
||||
);
|
||||
merken(
|
||||
"Zustellungen",
|
||||
(await prisma.zustellung.deleteMany({ where: { laufId: { in: laufIds } } })).count,
|
||||
);
|
||||
}
|
||||
|
||||
if (betroffen.length > 0) {
|
||||
merken(
|
||||
"Protokollzeilen",
|
||||
(
|
||||
await prisma.protokoll.deleteMany({
|
||||
where: { OR: [{ entitaetId: { in: betroffen } }, { laufId: { in: laufIds } }] },
|
||||
})
|
||||
).count,
|
||||
);
|
||||
}
|
||||
|
||||
merken("Läufe", (await prisma.lauf.deleteMany({ where: { titel: praefix } })).count);
|
||||
merken("Vorlagen", (await prisma.vorlage.deleteMany({ where: { name: praefix } })).count);
|
||||
merken("Bausteine", (await prisma.baustein.deleteMany({ where: { name: praefix } })).count);
|
||||
merken(
|
||||
"Geräte",
|
||||
(
|
||||
await prisma.geraet.deleteMany({
|
||||
where: { OR: [{ bezeichnung: praefix }, { hostname: praefix }] },
|
||||
})
|
||||
).count,
|
||||
);
|
||||
merken("Personen", (await prisma.mitarbeiter.deleteMany({ where: { anzeigename: praefix } })).count);
|
||||
merken("Abteilungen", (await prisma.abteilung.deleteMany({ where: { name: praefix } })).count);
|
||||
merken(
|
||||
"Verzeichnisquellen",
|
||||
(await prisma.verzeichnisquelle.deleteMany({ where: { name: praefix } })).count,
|
||||
);
|
||||
merken("Benutzer", (await prisma.benutzer.deleteMany({ where: { name: praefix } })).count);
|
||||
|
||||
return gezaehlt;
|
||||
}
|
||||
|
||||
/** Dasselbe mit einer Zeile Ausgabe — für Vor- und Nachlauf. */
|
||||
export async function e2eAufraeumenMitBericht(wann: "vor" | "nach"): Promise<void> {
|
||||
const gezaehlt = await e2eRestlosAufraeumen();
|
||||
const summe = Object.values(gezaehlt).reduce((a, b) => a + b, 0);
|
||||
if (summe === 0) return;
|
||||
|
||||
const liste = Object.entries(gezaehlt)
|
||||
.map(([was, anzahl]) => `${anzahl} ${was}`)
|
||||
.join(", ");
|
||||
/*
|
||||
* Der Vorlauf **sagt es laut**: Zeilen, die er findet, sind der Rückstand
|
||||
* eines abgebrochenen Durchgangs. Still wegzuräumen hieße, die Ursache zu
|
||||
* verdecken — und ein Ablauf, der jedes Mal abbricht, fiele nie auf.
|
||||
*/
|
||||
console.info(
|
||||
wann === "vor"
|
||||
? `E2E-Aufräumen (Rückstand aus einem früheren Lauf): ${liste}.`
|
||||
: `E2E-Aufräumen: ${liste}.`,
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import { chromium } from "@playwright/test";
|
||||
import "dotenv/config";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { ABTEILUNGEN } from "@/../prisma/seed-daten/stammdaten";
|
||||
import { anmelden, SEED_GERAET, SEED_TAG, SEED_VORLAGE } from "./helfer";
|
||||
import { e2eAufraeumenMitBericht } from "./aufraeumen";
|
||||
|
||||
/**
|
||||
* Was vor dem ersten Ablauf stimmen muss (#018).
|
||||
@@ -23,6 +25,14 @@ function fehlen(was: string, abhilfe: string): never {
|
||||
}
|
||||
|
||||
export default async function vorbereiten(): Promise<void> {
|
||||
/*
|
||||
* **Zuerst aufraeumen, dann pruefen** (#027). Was hier noch steht, ist der
|
||||
* Rueckstand eines abgebrochenen Durchgangs: `globalTeardown` und die
|
||||
* `afterAll`-Bloecke laufen bei einem Testfehler, aber nicht bei `Strg+C`.
|
||||
* Ein Vorlauf laesst sich nicht ueberspringen — das ist der Unterschied.
|
||||
*/
|
||||
await e2eAufraeumenMitBericht("vor");
|
||||
|
||||
if (!process.env["SEED_ADMIN_EMAIL"] || !process.env["SEED_ADMIN_PASSWORD"]) {
|
||||
fehlen(
|
||||
"SEED_ADMIN_EMAIL oder SEED_ADMIN_PASSWORD fehlt.",
|
||||
@@ -59,6 +69,37 @@ export default async function vorbereiten(): Promise<void> {
|
||||
fehlen(`Das Beispielgerät ${SEED_GERAET} (${SEED_TAG}) fehlt.`, "pnpm db:seed laufen lassen.");
|
||||
}
|
||||
|
||||
/*
|
||||
* 2c. Stehen die **Werte** des Seed-Bestands noch, nicht nur die Zeilen?
|
||||
*
|
||||
* Der Onboarding-Ablauf prueft den `New-ADUser`-Befehl **zeichengenau**, und
|
||||
* darin steckt die Ziel-OU der Abteilung. In der Entwicklungsdatenbank stand
|
||||
* an „Buchhaltung“ `DC=firma,DC=de` statt `DC=firma,DC=local` — von Hand
|
||||
* geaendert oder aus einer aelteren Seed-Fassung. Der Seed setzt bestehende
|
||||
* Abteilungen absichtlich **nicht** zurueck (gepflegte Daten sollen bleiben),
|
||||
* also blieb die Abweichung stehen.
|
||||
*
|
||||
* Der Ablauf scheiterte daran mit „expected … received …“ — einer Meldung, die
|
||||
* nach einem Fehler im Befehlsbau aussieht und keiner war. Hier faellt es
|
||||
* vorher auf, und die Meldung sagt, was zu tun ist (Reviewbefund zu #029).
|
||||
*/
|
||||
for (const vorgabe of ABTEILUNGEN) {
|
||||
const abteilung = await prisma.abteilung.findFirst({
|
||||
where: { name: vorgabe.name },
|
||||
select: { zielOu: true, standardGruppen: true },
|
||||
});
|
||||
if (!abteilung) continue;
|
||||
if (abteilung.zielOu !== vorgabe.zielOu) {
|
||||
fehlen(
|
||||
`Die Abteilung „${vorgabe.name}“ hat eine abweichende Ziel-OU: ` +
|
||||
`„${abteilung.zielOu}“ statt „${vorgabe.zielOu}“.`,
|
||||
"Der Onboarding-Ablauf prüft den erzeugten Befehl zeichengenau. Entweder die Ziel-OU " +
|
||||
"unter Admin → Abteilungen zurückstellen, oder gegen eine frische Datenbank fahren: " +
|
||||
"pnpm db:seed:reset.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 2b. In welchem Betriebsmodus laeuft die Installation?
|
||||
*
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { prisma } from "@/lib/db";
|
||||
import { e2eAufraeumenMitBericht } from "./aufraeumen";
|
||||
|
||||
/**
|
||||
* Nach dem letzten Ablauf: wegräumen, was die Abläufe angelegt haben (#027).
|
||||
*
|
||||
* Läuft auch dann, wenn Abläufe fehlgeschlagen sind — Playwright ruft den
|
||||
* Nachlauf nach dem Lauf, nicht nach dem Erfolg. Was er **nicht** abdeckt, ist
|
||||
* ein harter Abbruch (`Strg+C`, `kill`); dafür räumt derselbe Code im Vorlauf,
|
||||
* siehe `aufraeumen.ts`.
|
||||
*/
|
||||
export default async function aufraeumen(): Promise<void> {
|
||||
try {
|
||||
await e2eAufraeumenMitBericht("nach");
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
}
|
||||
@@ -273,7 +273,19 @@ test.describe("Der Zustands-Endpunkt", () => {
|
||||
expect(antwort.headers()["cache-control"]).toBe("no-store");
|
||||
|
||||
const zustand: unknown = await antwort.json();
|
||||
/*
|
||||
* Die Felder **namentlich** und vollständig: der Zustand ist die einzige
|
||||
* Antwort, die alle zehn Sekunden (seit #033 bei jeder Änderung) hinausgeht,
|
||||
* und ein Feld, das sich hier einschleicht, wandert ungesehen mit. Deshalb
|
||||
* `toEqual` und nicht `toContain`.
|
||||
*
|
||||
* `anzahlAnhaenge` kam mit #033 dazu und fehlte hier — der Ablauf war seitdem
|
||||
* rot, und niemand hat es gemerkt, weil `pnpm test:e2e` nicht im Tor vor dem
|
||||
* Commit steht (Reviewbefund zu #029). Wer hier ein Feld ergänzt, trägt es
|
||||
* ein; genau das ist der Zweck dieser Liste.
|
||||
*/
|
||||
expect(Object.keys(zustand as object).sort()).toEqual([
|
||||
"anzahlAnhaenge",
|
||||
"anzahlKommentare",
|
||||
"anzahlPflichtOffen",
|
||||
"geaendertAm",
|
||||
|
||||
@@ -184,6 +184,22 @@ const SEITEN: readonly Hauptseite[] = [
|
||||
*/
|
||||
{ name: "Admin: Verzeichnis", pfad: () => "/admin/verzeichnis", nurModus: "INTERN" },
|
||||
{ name: "Kunden", pfad: () => "/kunden", nurModus: "DIENSTLEISTUNG" },
|
||||
/*
|
||||
* **Die Seiten aus Phase 2.** Sie fehlten hier, obwohl jedes der Issues eine
|
||||
* Klickprobe bei 390 px von Hand berichtet — eine Probe von Hand ist kein
|
||||
* Regressionstest (Reviewbefund zu #029). Gerade die Bausteinliste und das
|
||||
* Zustellungs-Protokoll sind Tabellen, und Tabellen sind die Stellen, an denen
|
||||
* horizontales Scrollen entsteht.
|
||||
*
|
||||
* Nicht dabei: `/meine-geraete` (#035) — es verlangt das Recht
|
||||
* `selbstauskunft`, und das hat nur `MITARBEITER`. Dieser Ablauf meldet sich
|
||||
* als Admin an, der dort „kein Zugriff“ sähe; gemessen würde die
|
||||
* Verweigerungsseite. Dafür braucht es ein eigenes Konto, siehe #037.
|
||||
*/
|
||||
{ name: "Vorlagen: Bausteine", pfad: () => "/vorlagen/bausteine" },
|
||||
{ name: "Admin: Schnellstart", pfad: () => "/admin/schnellstart" },
|
||||
{ name: "Admin: Protokoll", pfad: () => "/admin/protokoll" },
|
||||
{ name: "Admin: Zustellungen", pfad: () => "/admin/zustellungen" },
|
||||
];
|
||||
|
||||
type Messung = {
|
||||
|
||||
@@ -112,7 +112,17 @@ test.describe("Personen", () => {
|
||||
|
||||
await treffer.click();
|
||||
await page.waitForURL(/\/mitarbeiter\/[^/]+$/);
|
||||
await expect(page.getByText(/kommt aus dem Verzeichnis/)).toBeVisible();
|
||||
/*
|
||||
* **Der ganze Satz, nicht nur der Anfang.** „kommt aus dem Verzeichnis“
|
||||
* allein traf seit #020 zwei Stellen: dieselbe Auskunft steht auch am
|
||||
* Vorgesetzten-Feld. Playwright wies das als „strict mode violation“ ab, und
|
||||
* der Ablauf war seitdem rot — unbemerkt, weil `pnpm test:e2e` nicht im Tor
|
||||
* vor dem Commit steht (Reviewbefund zu #029).
|
||||
*
|
||||
* Hier zählt die Auskunft an den **Stammdaten**: dass sie dort gepflegt
|
||||
* werden und eine Änderung hier der nächste Abgleich zurücksetzen würde.
|
||||
*/
|
||||
await expect(page.getByText(/kommt aus dem Verzeichnis\. Die Stammdaten werden dort/u)).toBeVisible();
|
||||
await expect(page.getByLabel("Vorname")).toHaveAttribute("readonly", "");
|
||||
await expect(page.getByRole("button", { name: "Stammdaten speichern" })).toHaveCount(0);
|
||||
// Deaktivieren muss trotzdem gehen: das Offboarding wartet nicht auf den
|
||||
|
||||
@@ -132,8 +132,15 @@ test.describe("Vorlagen-Editor", () => {
|
||||
.evaluateAll((felder) => felder.map((feld) => (feld as HTMLInputElement).value));
|
||||
expect(await werte()).toEqual(["Erster", "Zweiter"]);
|
||||
|
||||
// Der Anfasser ist mindestens 44 px hoch – sonst trifft ihn niemand.
|
||||
/*
|
||||
* **Erst in den Blick scrollen.** Am Handy stand der erste Anfasser nach dem
|
||||
* Anlegen zweier Abschnitte bei `y = -115`, also oberhalb des Sichtfensters;
|
||||
* der Touch-Start landete damit außerhalb und bewegte nichts. Das fiel
|
||||
* nicht auf, weil `pnpm test:e2e` nicht im Tor vor dem Commit steht
|
||||
* (Reviewbefund zu #029).
|
||||
*/
|
||||
const anfasser = page.getByRole("button", { name: "Verschieben" }).first();
|
||||
await anfasser.scrollIntoViewIfNeeded();
|
||||
const kasten = await anfasser.boundingBox();
|
||||
expect(kasten?.height ?? 0).toBeGreaterThanOrEqual(44);
|
||||
expect(kasten?.width ?? 0).toBeGreaterThanOrEqual(44);
|
||||
@@ -142,13 +149,32 @@ test.describe("Vorlagen-Editor", () => {
|
||||
// eine Wischgeste ein Bildlauf bleibt und kein Verschieben.
|
||||
const x = kasten!.x + kasten!.width / 2;
|
||||
const y = kasten!.y + kasten!.height / 2;
|
||||
|
||||
/*
|
||||
* **Die Zugweite wird gemessen, nicht gesetzt.** Vorher waren es fest
|
||||
* 12 × 25 px = 300; der Abstand zwischen den beiden Anfassern ist aber
|
||||
* 532 px, seit mit #030 und #031 Bausteinverweis und Abteilungsfilter in
|
||||
* jeden Abschnitt gekommen sind. Der Finger kam damit nicht mehr über den
|
||||
* zweiten Abschnitt, und die Reihenfolge blieb stehen.
|
||||
*
|
||||
* Eine feste Weite in einem Sortiertest ist eine Zusage über das Layout,
|
||||
* die niemand pflegt. Gemessen wird sie bei jedem Lauf richtig — und wenn
|
||||
* der Abstand sich wieder ändert, bleibt der Test grün, statt eine
|
||||
* Pixelzahl nachzuziehen.
|
||||
*/
|
||||
const zweiterAnfasser = await page.getByRole("button", { name: "Verschieben" }).nth(1).boundingBox();
|
||||
// Ein Stück über die Mitte des zweiten hinaus, sonst entscheidet dnd-kit
|
||||
// sich fuer „bleibt“.
|
||||
const weite = (zweiterAnfasser?.y ?? y + 300) - kasten!.y + kasten!.height;
|
||||
const schritte = 20;
|
||||
|
||||
const sitzung = await page.context().newCDPSession(page);
|
||||
await sitzung.send("Input.dispatchTouchEvent", { type: "touchStart", touchPoints: [{ x, y }] });
|
||||
await page.waitForTimeout(350);
|
||||
for (let schritt = 1; schritt <= 12; schritt += 1) {
|
||||
for (let schritt = 1; schritt <= schritte; schritt += 1) {
|
||||
await sitzung.send("Input.dispatchTouchEvent", {
|
||||
type: "touchMove",
|
||||
touchPoints: [{ x, y: y + schritt * 25 }],
|
||||
touchPoints: [{ x, y: y + (weite * schritt) / schritte }],
|
||||
});
|
||||
await page.waitForTimeout(40);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user