feat(vorlagen): Editor, Liste und Versionsgeschichte (#24)
This commit was merged in pull request #24.
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
import { expect, test, type Page } from "@playwright/test";
|
||||
import "dotenv/config";
|
||||
|
||||
/**
|
||||
* Der Vorlagen-Editor durch die Oberflaeche (#010).
|
||||
*
|
||||
* Diese Datei gibt es, weil ein Screenshot ueber Bedienbarkeit **nichts**
|
||||
* aussagt: die Seiten werden serverseitig gerendert und sehen auch dann richtig
|
||||
* aus, wenn React sich nie anhaengt. Geprueft wird deshalb, was nur mit
|
||||
* angehaengtem React gelingt — anlegen, tippen, sortieren, speichern — und
|
||||
* anschliessend, ob es wirklich in der Datenbank steht: durch Neuladen.
|
||||
*
|
||||
* Voraussetzung: ein **gebauter** Stand laeuft (`pnpm build && pnpm start`) und
|
||||
* `SEED_ADMIN_EMAIL`/`SEED_ADMIN_PASSWORD` sind gesetzt. Gegen `pnpm dev`
|
||||
* schlaegt das hier fehl, und das ist richtig so — siehe CLAUDE.md,
|
||||
* „Oberflaeche pruefen".
|
||||
*
|
||||
* Der Ablauf legt seine eigene Vorlage an und **archiviert** sie am Ende. Nicht
|
||||
* loescht: eine veroeffentlichte Version laesst sich nicht mehr entfernen, auch
|
||||
* nicht ueber einen Kaskadenloeschvorgang — der Trigger aus #009 verweigert das
|
||||
* ausdruecklich, und genau das ist die Zusage, an der das ganze Archiv haengt.
|
||||
* Wer diese Tests oft laufen laesst, braucht deshalb eine Wegwerf-Datenbank
|
||||
* (kommt mit #018).
|
||||
*/
|
||||
|
||||
const NAME = `ZZ E2E Editor ${Date.now()}`;
|
||||
|
||||
async function anmelden(seite: Page) {
|
||||
const email = process.env["SEED_ADMIN_EMAIL"];
|
||||
const passwort = process.env["SEED_ADMIN_PASSWORD"];
|
||||
if (!email || !passwort) throw new Error("SEED_ADMIN_EMAIL und SEED_ADMIN_PASSWORD fehlen.");
|
||||
await seite.goto("/anmelden");
|
||||
await seite.fill('input[name="email"]', email);
|
||||
await seite.fill('input[name="passwort"]', passwort);
|
||||
await Promise.all([seite.waitForURL("**/"), seite.click('button[type="submit"]')]);
|
||||
}
|
||||
|
||||
/** Legt die Vorlage an und fuehrt in ihren Entwurf. Gibt die Adresse des Editors zurueck. */
|
||||
async function entwurfAnlegen(seite: Page, name: string): Promise<string> {
|
||||
await seite.goto("/vorlagen");
|
||||
await seite.getByLabel("Name").fill(name);
|
||||
await seite.getByRole("button", { name: "Vorlage anlegen" }).click();
|
||||
await expect(seite.getByText(`„${name}“ wurde angelegt.`)).toBeVisible();
|
||||
|
||||
await seite.goto("/vorlagen");
|
||||
await seite.getByRole("link", { name }).click();
|
||||
await seite.getByRole("button", { name: /bearbeiten/i }).click();
|
||||
await seite.waitForURL(/\/vorlagen\/version\//);
|
||||
return seite.url();
|
||||
}
|
||||
|
||||
async function aufraeumen(seite: Page, name: string) {
|
||||
await seite.goto("/vorlagen");
|
||||
const verweis = seite.getByRole("link", { name });
|
||||
if ((await verweis.count()) === 0) return;
|
||||
await verweis.click();
|
||||
await seite.getByRole("button", { name: "archivieren" }).click();
|
||||
await expect(seite.getByRole("button", { name: "wieder freigeben" })).toBeVisible();
|
||||
}
|
||||
|
||||
test.describe("Vorlagen-Editor", () => {
|
||||
test("legt an, baut auf, speichert und veröffentlicht", async ({ page }) => {
|
||||
test.slow();
|
||||
const name = `${NAME} A`;
|
||||
await anmelden(page);
|
||||
await entwurfAnlegen(page, name);
|
||||
|
||||
// Ein Abschnitt mit einem Befehlsschritt.
|
||||
await page.getByRole("button", { name: "Abschnitt hinzufügen" }).click();
|
||||
await page.getByLabel("Titel des Abschnitts").fill("Vorbereitung");
|
||||
const leiste = page.locator("form.sticky");
|
||||
await expect(leiste).toContainText("Nicht gespeicherte Änderungen");
|
||||
|
||||
await page.getByRole("button", { name: "Schritt hinzufügen" }).click();
|
||||
await page.getByLabel("Titel", { exact: true }).last().fill("Rechner umbenennen");
|
||||
|
||||
await leiste.getByRole("button", { name: "Speichern" }).click();
|
||||
await expect(leiste).toContainText("Entwurf gespeichert.");
|
||||
await expect(leiste).toContainText("Alles gespeichert");
|
||||
|
||||
// Der Beweis, dass es nicht nur im Browser steht: neu laden.
|
||||
await page.reload();
|
||||
await expect(page.getByLabel("Titel des Abschnitts")).toHaveValue("Vorbereitung");
|
||||
await expect(page.locator("form.sticky")).toContainText("Alles gespeichert");
|
||||
|
||||
// Veröffentlichen macht die Version unveränderlich.
|
||||
await page.getByLabel("Änderungsnotiz (optional)").fill("Erste Fassung");
|
||||
await page.getByRole("button", { name: "Veröffentlichen", exact: true }).click();
|
||||
/*
|
||||
* Die Erfolgsmeldung steht im Veroeffentlichen-Formular – und genau das ist
|
||||
* nach dem `router.refresh()` verschwunden. Geprueft wird deshalb der
|
||||
* Zustand danach, nicht die Meldung: das ist ohnehin die haerteren Zusage.
|
||||
*/
|
||||
await expect(page.locator("form.sticky")).toHaveCount(0);
|
||||
await expect(page.getByRole("button", { name: "Verschieben" })).toHaveCount(0);
|
||||
await expect(page.getByText(/ist veröffentlicht und wird nicht mehr verändert/)).toBeVisible();
|
||||
|
||||
await aufraeumen(page, name);
|
||||
});
|
||||
|
||||
test("warnt vor einem unbekannten Platzhalter, ohne zu blockieren", async ({ page }) => {
|
||||
test.slow();
|
||||
const name = `${NAME} B`;
|
||||
await anmelden(page);
|
||||
await entwurfAnlegen(page, name);
|
||||
|
||||
await page.getByRole("button", { name: "Abschnitt hinzufügen" }).click();
|
||||
await page.getByLabel("Titel des Abschnitts").fill("Installation");
|
||||
await page.getByRole("button", { name: "Schritt hinzufügen" }).click();
|
||||
await page.getByLabel("Titel", { exact: true }).last().fill("{{hostnmae}} setzen");
|
||||
|
||||
await expect(page.getByText(/gehört zu keiner Variablen/)).toBeVisible();
|
||||
await expect(page.getByText("{{hostnmae}}", { exact: true })).toBeVisible();
|
||||
|
||||
// Die Warnung ist eine Warnung: Speichern bleibt möglich.
|
||||
const leiste = page.locator("form.sticky");
|
||||
await leiste.getByRole("button", { name: "Speichern" }).click();
|
||||
await expect(leiste).toContainText("Entwurf gespeichert.");
|
||||
|
||||
// Ein eingebauter Platzhalter loest keine Warnung aus.
|
||||
await page.getByLabel("Titel", { exact: true }).last().fill("{{hostname}} setzen");
|
||||
await expect(page.getByText(/gehört zu keiner Variablen/)).toHaveCount(0);
|
||||
|
||||
await aufraeumen(page, name);
|
||||
});
|
||||
|
||||
test("sortiert am Handy mit dem Finger", async ({ page, isMobile, viewport }) => {
|
||||
test.skip(!isMobile, "Nur im Projekt „handy“ – am Schreibtisch gibt es keinen Finger.");
|
||||
test.slow();
|
||||
const name = `${NAME} C`;
|
||||
await anmelden(page);
|
||||
await entwurfAnlegen(page, name);
|
||||
|
||||
for (const titel of ["Erster", "Zweiter"]) {
|
||||
await page.getByRole("button", { name: "Abschnitt hinzufügen" }).click();
|
||||
await page.getByLabel("Titel des Abschnitts").last().fill(titel);
|
||||
}
|
||||
const werte = async () =>
|
||||
page
|
||||
.getByLabel("Titel des Abschnitts")
|
||||
.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.
|
||||
const anfasser = page.getByRole("button", { name: "Verschieben" }).first();
|
||||
const kasten = await anfasser.boundingBox();
|
||||
expect(kasten?.height ?? 0).toBeGreaterThanOrEqual(44);
|
||||
expect(kasten?.width ?? 0).toBeGreaterThanOrEqual(44);
|
||||
|
||||
// Halten, ziehen, loslassen. Der TouchSensor wartet bewusst 200 ms, damit
|
||||
// eine Wischgeste ein Bildlauf bleibt und kein Verschieben.
|
||||
const x = kasten!.x + kasten!.width / 2;
|
||||
const y = kasten!.y + kasten!.height / 2;
|
||||
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) {
|
||||
await sitzung.send("Input.dispatchTouchEvent", {
|
||||
type: "touchMove",
|
||||
touchPoints: [{ x, y: y + schritt * 25 }],
|
||||
});
|
||||
await page.waitForTimeout(40);
|
||||
}
|
||||
await sitzung.send("Input.dispatchTouchEvent", { type: "touchEnd", touchPoints: [] });
|
||||
|
||||
await expect.poll(werte).toEqual(["Zweiter", "Erster"]);
|
||||
|
||||
// Und dabei wird nicht horizontal gescrollt.
|
||||
const masse = await page.evaluate(() => ({
|
||||
scroll: document.documentElement.scrollWidth,
|
||||
sichtbar: document.documentElement.clientWidth,
|
||||
}));
|
||||
expect(masse.scroll).toBeLessThanOrEqual(masse.sichtbar);
|
||||
expect(viewport?.width).toBeLessThanOrEqual(430);
|
||||
|
||||
await aufraeumen(page, name);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user