Die Handbuchseite erklärt, was der Planer liest und was ausdrücklich nicht, warum alle Kalender gegen jeden Bereich zählen und was beim Trennen mit den Terminen passiert. Die README bekommt eine Schritt-für-Schritt-Anleitung für Entra ID und die Google Cloud Console — mit den genauen Rückleit-URIs und Berechtigungen. Dazu der Hinweis, dass ein Google-Projekt im Status "Test" die Aktualisierungstoken nach sieben Tagen verfallen lässt; das kostet sonst einen halben Abend Fehlersuche. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
94 lines
2.9 KiB
TypeScript
94 lines
2.9 KiB
TypeScript
import { mkdir } from 'node:fs/promises'
|
|
import { test, type Page } from '@playwright/test'
|
|
|
|
/**
|
|
* Nimmt die Bilder für das Handbuch auf.
|
|
*
|
|
* Je Seite vier Bilder: hell und dunkel, Schreibtisch und Handy. Die Uhr steht
|
|
* auf einem festen Zeitpunkt, damit sich zwischen zwei Läufen nur das ändert,
|
|
* was sich wirklich geändert hat.
|
|
*/
|
|
|
|
const ZIEL = 'docs/bilder'
|
|
const SCHREIBTISCH = { width: 1440, height: 900 }
|
|
const HANDY = { width: 390, height: 844 }
|
|
|
|
// Dienstag, 17. März 2026, 09:20 Uhr in Berlin.
|
|
const FESTE_ZEIT = new Date('2026-03-17T08:20:00.000Z')
|
|
|
|
const EMAIL = 'martina.beispiel@planer.invalid'
|
|
const PASSWORT = 'demo-instanz-nur-fuer-bilder'
|
|
|
|
type Aufnahme = {
|
|
name: string
|
|
pfad: string
|
|
/** Wird vor der Aufnahme ausgeführt, z. B. um ein Formular aufzuklappen. */
|
|
vorbereiten?: (seite: Page) => Promise<void>
|
|
}
|
|
|
|
const AUFNAHMEN: Aufnahme[] = [
|
|
{ name: 'anmelden', pfad: '/anmelden' },
|
|
{ name: 'start', pfad: '/' },
|
|
{ name: 'inbox', pfad: '/inbox' },
|
|
{ name: 'aufgaben', pfad: '/aufgaben' },
|
|
{ name: 'projekte', pfad: '/projekte' },
|
|
{ name: 'ideen', pfad: '/ideen' },
|
|
{ name: 'bereiche', pfad: '/bereiche' },
|
|
{ name: 'kalenderkonten', pfad: '/kalenderkonten' },
|
|
{ name: 'einstellungen', pfad: '/einstellungen' },
|
|
{ name: 'protokoll', pfad: '/verwaltung/protokoll' },
|
|
]
|
|
|
|
async function anmelden(seite: Page): Promise<void> {
|
|
await seite.goto('/anmelden')
|
|
await seite.getByLabel('E-Mail-Adresse').fill(EMAIL)
|
|
await seite.getByLabel('Passwort').fill(PASSWORT)
|
|
await seite.getByRole('button', { name: 'Mit Passwort anmelden' }).click()
|
|
await seite.waitForURL('/')
|
|
}
|
|
|
|
test.beforeAll(async () => {
|
|
await mkdir(ZIEL, { recursive: true })
|
|
})
|
|
|
|
for (const schema of ['hell', 'dunkel'] as const) {
|
|
for (const [geraet, groesse] of [
|
|
['schreibtisch', SCHREIBTISCH],
|
|
['handy', HANDY],
|
|
] as const) {
|
|
test(`Bilder: ${schema}, ${geraet}`, async ({ browser }) => {
|
|
const kontext = await browser.newContext({
|
|
viewport: groesse,
|
|
colorScheme: schema === 'dunkel' ? 'dark' : 'light',
|
|
deviceScaleFactor: 2,
|
|
})
|
|
const seite = await kontext.newPage()
|
|
await seite.clock.setFixedTime(FESTE_ZEIT)
|
|
|
|
for (const aufnahme of AUFNAHMEN) {
|
|
if (aufnahme.pfad !== '/anmelden') {
|
|
// Für alles außer der Anmeldeseite braucht es eine Sitzung.
|
|
if (
|
|
!(await seite.context().cookies()).some((k) => k.name.includes('session'))
|
|
) {
|
|
await anmelden(seite)
|
|
}
|
|
}
|
|
|
|
await seite.goto(aufnahme.pfad)
|
|
await seite.waitForLoadState('networkidle')
|
|
await aufnahme.vorbereiten?.(seite)
|
|
// Der Farbschema-Umschalter und Schriften sollen fertig sein.
|
|
await seite.waitForTimeout(300)
|
|
|
|
await seite.screenshot({
|
|
path: `${ZIEL}/${aufnahme.name}-${schema}-${geraet}.png`,
|
|
fullPage: true,
|
|
})
|
|
}
|
|
|
|
await kontext.close()
|
|
})
|
|
}
|
|
}
|