Release 0.8.0

This commit is contained in:
hnzio committed 2026-10-27 14:57:17 +01:00
1 parent 21c0de7dd9
commit 07f2c4607b
47 files changed
+5314 -2303

No files matched your search

+153 -105
View File
@@ -31,12 +31,14 @@ async function setTheme(page: Page, theme: 'light' | 'dark') {
}
async function openEditorialField(page: Page, heading: string) {
// Fundus pieces show all writing areas at once; usage editors still use tabs.
if (!(await page.getByRole('button', { name: 'Streamtext', exact: true }).count())) return;
if (heading.includes('Recherche')) {
await page.getByRole('button', { name: 'Recherche & Quellen' }).click();
} else if (heading.includes('Präsentation')) {
await page.getByRole('button', { name: 'Präsentation', exact: true }).click();
} else {
await page.getByRole('button', { name: 'Sendungstext', exact: true }).click();
await page.getByRole('button', { name: 'Streamtext', exact: true }).click();
}
}
@@ -46,6 +48,18 @@ async function editInlineTitle(scope: Page | Locator, title: string, value: stri
await scope.getByLabel('Titel', { exact: true }).press('Enter');
}
async function fillMarkdownSource(page: Page, label: string, value: string) {
const fieldNames: Record<string, string> = {
'Private Recherche': 'researchText',
'Öffentlicher Vorspann': 'presentationText'
};
const field = page.locator(`.rich-markdown-field:has(input[name="${fieldNames[label]}"])`);
if (!(await field.locator('textarea:visible').count())) {
await field.getByRole('button', { name: 'Markdown' }).click();
}
await field.locator('textarea').fill(value);
}
async function login(page: Page, email = adminEmail, password = adminPassword) {
await page.goto('/login');
await page.getByLabel('E-Mail').fill(email);
@@ -56,7 +70,10 @@ async function login(page: Page, email = adminEmail, password = adminPassword) {
async function openSeededShow(page: Page) {
await page.goto('/sendungen');
await page.getByRole('link', { name: /Herbstsendung: Kleine Welten/ }).click();
await page
.locator('.show-card-list')
.getByRole('link', { name: /Herbstsendung: Kleine Welten/ })
.click();
await expect(page).toHaveURL(/\/sendungen\/show-october$/);
}
@@ -478,6 +495,7 @@ test.beforeAll(async ({ browser }) => {
expect(sessionCookie?.sameSite).toBe('Lax');
await page.goto('/setup');
await expect(page).toHaveURL(/\/fundus$/);
await page.locator('.app-header .account-menu > summary').click();
await page.getByRole('button', { name: 'Abmelden', exact: true }).click();
await expect(page).toHaveURL(/\/login$/);
await page.close();
@@ -487,6 +505,35 @@ test.beforeEach(async ({ page }) => {
await login(page);
});
test('dashboard, global search and responsive primary navigation use the shared app chrome', async ({
page
}, testInfo) => {
await page.setViewportSize({ width: 1440, height: 1000 });
await page.goto('/');
await expect(page.getByRole('heading', { name: 'Was als Nächstes ansteht' })).toBeVisible();
for (const label of ['Start', 'Eingang', 'Pieces', 'Streams', 'Live']) {
await expect(
page.locator('.main-navigation').getByRole('link', { name: label, exact: true })
).toBeVisible();
}
await page.getByPlaceholder('Pieces und Streams suchen').fill('Herstellerangaben');
await page.getByPlaceholder('Pieces und Streams suchen').press('Enter');
await expect(page).toHaveURL(/\/suche\?q=Herstellerangaben/);
await expect(page.getByRole('heading', { name: 'Pieces', exact: true })).toBeVisible();
await expect(page.getByText('Die kleine Konsole mit dem großen Dreh')).toBeVisible();
await expect(page.getByText('Recherche', { exact: true })).toBeVisible();
await page.setViewportSize({ width: 390, height: 844 });
await page.goto('/');
await expect(page.locator('.app-header')).toBeHidden();
await expect(
page.locator('.mobile-header').getByPlaceholder('Suchen', { exact: true })
).toBeVisible();
await expect(page.locator('.mobile-navigation').getByRole('link')).toHaveCount(5);
await expectNoHorizontalOverflow(page);
await page.screenshot({ path: testInfo.outputPath('start-mobile.png'), fullPage: true });
});
test('invitations, direct authorization, locking and password reset work end to end', async ({
page,
browser
@@ -566,16 +613,23 @@ test('desktop fundus is a compact, filterable list with persistent themes', asyn
await page.setViewportSize({ width: 1440, height: 1000 });
await page.goto('/fundus');
await expect(page.getByRole('heading', { name: 'Fundus', exact: true })).toBeVisible();
await expect(page.locator('.sidebar')).toBeVisible();
await expect(page.getByRole('heading', { name: 'Pieces', exact: true }).first()).toBeVisible();
await expect(page.locator('.app-header')).toBeVisible();
await expect(page.locator('.mobile-navigation')).toBeHidden();
await expect(page.locator('.utility-navigation a')).toHaveText('Verwaltung');
await expect(page.locator('.app-header .account-menu > summary')).toBeVisible();
await expect(page.locator('.piece-card')).toHaveCount(1);
const navigationBox = await page.locator('.fundus-navigation').boundingBox();
const listBox = await page.locator('.fundus-list-pane').boundingBox();
expect(navigationBox?.x).toBeLessThan(listBox?.x ?? 0);
await expect(page.locator('.fundus-context-pane')).toHaveCount(0);
await page
.locator('.filter-picker')
.evaluate((element) => ((element as HTMLDetailsElement).open = true));
await page.getByLabel('Auch eingeplante Inhalte zeigen').check();
await expect(page.locator('.piece-card')).toHaveCount(4);
await page
.locator('.filter-picker')
.evaluate((element) => ((element as HTMLDetailsElement).open = false));
await page
.getByRole('button', { name: 'Die kleine Konsole mit dem großen Dreh als Favorit markieren' })
.click();
@@ -587,6 +641,15 @@ test('desktop fundus is a compact, filterable list with persistent themes', asyn
await expect(page.locator('.piece-card').first()).toContainText(
'Die kleine Konsole mit dem großen Dreh'
);
await page
.locator('.piece-card')
.filter({ hasText: 'Die kleine Konsole mit dem großen Dreh' })
.getByRole('link', { name: 'Die kleine Konsole mit dem großen Dreh', exact: true })
.click();
await expect(page).toHaveURL(/\/fundus\/piece-console\?includePlanned=1$/);
await page.getByRole('button', { name: 'Editor schließen' }).click();
await expect(page).toHaveURL(/\/fundus\?includePlanned=1$/);
await expect(page.getByLabel('Auch eingeplante Inhalte zeigen')).toBeChecked();
await page
.locator('.filter-picker')
.evaluate((element) => ((element as HTMLDetailsElement).open = true));
@@ -595,13 +658,9 @@ test('desktop fundus is a compact, filterable list with persistent themes', asyn
const secondRow = await page.locator('.piece-card').nth(1).boundingBox();
expect(firstRow?.x).toBe(secondRow?.x);
expect(firstRow?.y).toBeLessThan(secondRow?.y ?? 0);
expect(firstRow?.height ?? 999).toBeLessThan(130);
const searchBox = await page.locator('.search-field').boundingBox();
const statusBox = await page.getByLabel('Status').boundingBox();
expect(searchBox?.height).toBe(statusBox?.height);
await page.getByRole('searchbox', { name: 'Fundus durchsuchen' }).fill('Pixelart');
await page.getByRole('button', { name: 'Filter anwenden' }).click();
expect(firstRow?.height ?? 999).toBeLessThan(150);
await page.getByRole('searchbox', { name: 'Pieces durchsuchen' }).fill('Pixelart');
await page.getByRole('searchbox', { name: 'Pieces durchsuchen' }).press('Enter');
await expect(page.locator('.piece-card')).toHaveCount(1);
await page
.locator('.filter-picker')
@@ -615,11 +674,13 @@ test('desktop fundus is a compact, filterable list with persistent themes', asyn
await page.getByRole('button', { name: 'Zurücksetzen' }).click();
await expect(page.locator('.piece-card')).toHaveCount(1);
await page.getByRole('button', { name: 'Darstellung: System' }).click();
await page.locator('.app-header .account-menu > summary').click();
await page.getByRole('button', { name: 'Hell' }).click();
await expect(page.locator('html')).toHaveAttribute('data-theme-preference', 'light');
await page.reload();
await expect(page.locator('html')).toHaveAttribute('data-theme-preference', 'light');
await expect(page.getByRole('button', { name: 'Darstellung: Hell' })).toBeVisible();
await page.locator('.app-header .account-menu > summary').click();
await expect(page.getByRole('button', { name: 'Hell' })).toHaveAttribute('aria-pressed', 'true');
await expectNoHorizontalOverflow(page);
await page.screenshot({ path: testInfo.outputPath('fundus-desktop-light.png'), fullPage: true });
});
@@ -634,17 +695,7 @@ test('sent and planned source filters stay independent and removing the last usa
page.getByRole('heading', { name: 'Warum praktische Effekte wieder auffallen' })
).toHaveCount(0);
await page.evaluate(() => (document.body.dataset.filterDocument = 'stable'));
await page
.locator('.filter-picker')
.evaluate((element) => ((element as HTMLDetailsElement).open = true));
await page.getByLabel('Auch verwendete Inhalte zeigen').check();
await page
.locator('.filter-picker')
.evaluate((element) => ((element as HTMLDetailsElement).open = true));
await expect(
page.getByRole('heading', { name: 'Warum praktische Effekte wieder auffallen' })
).toHaveCount(0);
await page.getByLabel('Auch eingeplante Inhalte zeigen').check();
await page.getByRole('link', { name: /Abgeschlossen/ }).click();
const usedPiece = page
.locator('.piece-card')
.filter({ hasText: 'Warum praktische Effekte wieder auffallen' });
@@ -658,9 +709,9 @@ test('sent and planned source filters stay independent and removing the last usa
await usedPiece.getByText('Vollständige Verwendungshistorie').click();
await expect(usedPiece.locator('.piece-usage-history li')).toHaveCount(2);
await expect(page.locator('body')).toHaveAttribute('data-filter-document', 'stable');
await page.getByRole('searchbox', { name: 'Fundus durchsuchen' }).fill('praktische Effekte');
await page.getByRole('button', { name: 'Filter anwenden' }).click();
await expect(page.getByLabel('Auch verwendete Inhalte zeigen')).toBeChecked();
await page.getByRole('searchbox', { name: 'Pieces durchsuchen' }).fill('praktische Effekte');
await page.getByRole('searchbox', { name: 'Pieces durchsuchen' }).press('Enter');
await expect(page).toHaveURL(/view=completed/);
await expect(usedPiece).toBeVisible();
await page.screenshot({
path: testInfo.outputPath('fundus-verwendungshistorie-desktop.png'),
@@ -668,11 +719,11 @@ test('sent and planned source filters stay independent and removing the last usa
});
await page.goto('/sendungen');
await page.getByText('Neue Sendung planen', { exact: true }).click();
await page.locator('.show-create-panel > summary').click();
await page.getByLabel('Titel', { exact: true }).fill('E2E Filterprüfung');
await page.getByRole('radio', { name: /Standardablauf/ }).check();
await page.getByRole('button', { name: 'Sendung anlegen' }).click();
await page.getByPlaceholder('Fundus durchsuchen').fill('kleine Konsole');
await page.getByRole('button', { name: 'Stream anlegen' }).click();
await page.getByPlaceholder('Pieces durchsuchen').fill('kleine Konsole');
await page.getByRole('button', { name: 'Filtern' }).click();
const consolePiece = page
.locator('.builder-source-card')
@@ -680,7 +731,7 @@ test('sent and planned source filters stay independent and removing the last usa
await consolePiece.getByRole('button', { name: 'Hinzufügen' }).click();
await expect(consolePiece).toHaveCount(0);
await page.getByPlaceholder('Fundus durchsuchen').fill('');
await page.getByPlaceholder('Pieces durchsuchen').fill('');
await page.getByRole('button', { name: 'Filtern' }).click();
await page.getByLabel('Auch eingeplante Inhalte zeigen').check();
await expect(
@@ -693,7 +744,7 @@ test('sent and planned source filters stay independent and removing the last usa
.locator('.builder-source-card')
.filter({ hasText: 'Warum praktische Effekte wieder auffallen' })
).toHaveCount(0);
await page.getByPlaceholder('Fundus durchsuchen').fill('kleine Konsole');
await page.getByPlaceholder('Pieces durchsuchen').fill('kleine Konsole');
await page.getByRole('button', { name: 'Filtern' }).click();
await expect(page.getByLabel('Auch eingeplante Inhalte zeigen')).toBeChecked();
await expect(
@@ -701,7 +752,7 @@ test('sent and planned source filters stay independent and removing the last usa
.locator('.builder-source-card')
.filter({ hasText: 'Die kleine Konsole mit dem großen Dreh' })
).toContainText('Eingeplant');
await page.getByPlaceholder('Fundus durchsuchen').fill('');
await page.getByPlaceholder('Pieces durchsuchen').fill('');
await page.getByRole('button', { name: 'Filtern' }).click();
await page.getByLabel('Auch eingeplante Inhalte zeigen').uncheck();
@@ -724,7 +775,7 @@ test('sent and planned source filters stay independent and removing the last usa
page.once('dialog', (dialog) => dialog.accept());
await page
.locator('.reserve-usage')
.getByRole('button', { name: /aus Sendung entfernen/ })
.getByRole('button', { name: /aus Stream entfernen/ })
.click();
await page.getByLabel('Auch verwendete Inhalte zeigen').uncheck();
await expect(
@@ -733,7 +784,7 @@ test('sent and planned source filters stay independent and removing the last usa
.filter({ hasText: 'Die kleine Konsole mit dem großen Dreh' })
).toBeVisible();
await page.setViewportSize({ width: 390, height: 844 });
await page.getByRole('button', { name: 'Fundus', exact: true }).click();
await page.getByRole('button', { name: 'Pieces', exact: true }).click();
await expectNoHorizontalOverflow(page);
await page.screenshot({
path: testInfo.outputPath('sendungsfilter-mobile.png'),
@@ -768,7 +819,9 @@ test('show overview switches persistently between list, calendar and mobile agen
await page.getByRole('link', { name: 'Nächster Monat' }).click();
await expect(page.getByRole('heading', { name: 'November 2026' })).toBeVisible();
await expect(page.getByRole('link', { name: 'Kalender Novembertermin' })).toBeVisible();
await expect(
page.locator('.show-calendar-event').filter({ hasText: 'Kalender Novembertermin' })
).toBeVisible();
await page.reload();
await expect(page.getByRole('button', { name: 'Kalender' })).toHaveAttribute(
'aria-pressed',
@@ -787,7 +840,7 @@ test('show overview switches persistently between list, calendar and mobile agen
});
await page.getByRole('button', { name: 'Liste' }).click();
await expect(page.getByRole('heading', { name: 'Geplante Sendungen' })).toBeVisible();
await expect(page.getByRole('heading', { name: 'Streams', exact: true }).last()).toBeVisible();
} finally {
removeCalendarFixture();
}
@@ -818,7 +871,7 @@ test('preparation opens and updates the concrete scheduled usage', async ({ page
await expect(editor.getByRole('combobox', { name: 'Video-URL', exact: true })).toBeVisible();
await expect(editor.getByLabel('Videotitel')).toBeVisible();
await editor.getByRole('radio', { name: 'Text', exact: true }).check();
await editor.getByRole('button', { name: 'Sendungstext', exact: true }).click();
await editor.getByRole('button', { name: 'Streamtext', exact: true }).click();
await editor.getByText('Dauer und Einplanung', { exact: true }).click();
await editor.getByLabel('Vorbereitungsstatus').selectOption('ready');
await editor.getByRole('button', { name: 'Verwendung speichern' }).click();
@@ -856,16 +909,18 @@ test('desktop builder keeps the fundus beside the wider run sheet', async ({ pag
});
});
test('narrow desktop retains the sidebar and switches the builder panes', async ({ page }) => {
test('narrow desktop retains the horizontal navigation and switches the builder panes', async ({
page
}) => {
await page.setViewportSize({ width: 960, height: 850 });
await openSeededShow(page);
await expect(page.locator('.sidebar')).toBeVisible();
await expect(page.locator('.app-header')).toBeVisible();
await expect(page.locator('.mobile-navigation')).toBeHidden();
await expect(page.locator('.mobile-pane-switch')).toBeVisible();
await expect(page.locator('.run-pane')).toBeVisible();
await expect(page.locator('.source-pane')).toBeHidden();
await page.getByRole('button', { name: 'Fundus', exact: true }).click();
await page.getByRole('button', { name: 'Pieces', exact: true }).click();
await expect(page.locator('.source-pane')).toBeVisible();
await expect(page.locator('.run-pane')).toBeHidden();
await expectNoHorizontalOverflow(page);
@@ -881,7 +936,7 @@ test('mobile and tablet navigation do not obscure switchable content', async ({
await expect(page.locator('.sidebar')).toBeHidden();
await expect(page.locator('.mobile-navigation')).toBeVisible();
await expect(page.locator('.run-pane')).toBeVisible();
await page.getByRole('button', { name: 'Fundus', exact: true }).click();
await page.getByRole('button', { name: 'Pieces', exact: true }).click();
await expect(page.locator('.source-pane')).toBeVisible();
await expect(page.locator('.run-pane')).toBeHidden();
await expectNoHorizontalOverflow(page);
@@ -906,10 +961,10 @@ test('a show can be planned from a template with independent usages and reserve'
test.slow();
await page.setViewportSize({ width: 1440, height: 1000 });
await page.goto('/sendungen');
await page.getByText('Neue Sendung planen', { exact: true }).click();
await page.locator('.show-create-panel > summary').click();
await page.getByLabel('Titel', { exact: true }).fill('E2E Planung Spiele und JC');
await page.getByRole('radio', { name: /Standardablauf/ }).check();
await page.getByRole('button', { name: 'Sendung anlegen' }).click();
await page.getByRole('button', { name: 'Stream anlegen' }).click();
await expect(page).toHaveURL(/\/sendungen\/[a-f0-9-]+$/);
const showUrl = page.url();
await expect(page.locator('.run-section')).toHaveCount(5);
@@ -929,7 +984,7 @@ test('a show can be planned from a template with independent usages and reserve'
await sectionTransfer.dispose();
await expect(page.locator('[data-drop-preview="section"]')).toHaveCount(0);
await page.getByPlaceholder('Fundus durchsuchen').fill('kleine Konsole');
await page.getByPlaceholder('Pieces durchsuchen').fill('kleine Konsole');
await page.getByRole('button', { name: 'Filtern' }).click();
const source = page
.locator('.builder-source-card')
@@ -968,7 +1023,7 @@ test('a show can be planned from a template with independent usages and reserve'
name: 'Die kleine Konsole mit dem großen Dreh bearbeiten und verschieben'
})
.click();
await expect(page.locator('dialog[open]')).toContainText('Sendungsfassung');
await expect(page.locator('dialog[open]')).toContainText('Streamfassung');
await expect(page.locator('dialog[open] .piece-dialog-workspace')).toBeVisible();
await expectNoHorizontalOverflow(page);
await page.screenshot({
@@ -977,7 +1032,7 @@ test('a show can be planned from a template with independent usages and reserve'
});
await page
.locator('dialog[open]')
.getByRole('button', { name: 'Aus Fundus aktualisieren' })
.getByRole('button', { name: 'Aus Piece aktualisieren' })
.click();
await expect(page.locator('dialog[open]')).toHaveCount(0);
usages = page
@@ -1056,7 +1111,7 @@ test('a show can be planned from a template with independent usages and reserve'
name: 'Die kleine Konsole mit dem großen Dreh bearbeiten und verschieben'
})
.click();
await expect(page.locator('dialog[open]')).toContainText('Sendungsfassung');
await expect(page.locator('dialog[open]')).toContainText('Streamfassung');
await expect(
page.locator('dialog[open]').getByRole('button', { name: 'Duplizieren' })
).toBeVisible();
@@ -1093,7 +1148,7 @@ test('a show can be planned from a template with independent usages and reserve'
await savedUsage
.getByRole('button', { name: 'Konsole als Auftakt bearbeiten und verschieben' })
.click();
await expect(savedUsage.locator('textarea[name="ownNotes"]')).toHaveValue('Kurbel zuerst zeigen');
await expect(savedUsage.locator('input[name="ownNotes"]')).toHaveValue('Kurbel zuerst zeigen');
await page.locator('dialog[open]').getByRole('button', { name: 'Editor schließen' }).click();
const duplicatedUsage = page
.locator('.editable-usage')
@@ -1104,7 +1159,7 @@ test('a show can be planned from a template with independent usages and reserve'
.getByRole('button', { name: 'Konsole als Rückblick bearbeiten und verschieben' })
.click();
await expect(duplicatedUsage.nth(1).getByLabel('Dauer in Minuten')).toHaveValue('15');
await expect(duplicatedUsage.nth(1).locator('textarea[name="ownNotes"]')).toHaveValue(
await expect(duplicatedUsage.nth(1).locator('input[name="ownNotes"]')).toHaveValue(
'Andere Perspektive'
);
await page.locator('dialog[open]').getByRole('button', { name: 'Editor schließen' }).click();
@@ -1121,10 +1176,10 @@ test('a show can be planned from a template with independent usages and reserve'
});
await page
.locator('dialog[open]')
.getByRole('button', { name: 'Aus Sendung entfernen' })
.getByRole('button', { name: 'Aus Stream entfernen' })
.scrollIntoViewIfNeeded();
await expect(
page.locator('dialog[open]').getByRole('button', { name: 'Aus Sendung entfernen' })
page.locator('dialog[open]').getByRole('button', { name: 'Aus Stream entfernen' })
).toBeVisible();
await page
.locator('dialog[open] form[action="?/updateUsage"] input[name="revision"]')
@@ -1392,7 +1447,6 @@ test('tools stay editable in pieces after catalog deactivation', async ({ page }
await page.screenshot({ path: testInfo.outputPath('tool-catalog-desktop.png'), fullPage: true });
await page.goto('/fundus/piece-quiz');
await page.getByRole('button', { name: 'Recherche & Quellen' }).click();
await page.getByText('Werkzeuge auswählen', { exact: true }).click();
const toolChoice = page.getByRole('checkbox', { name: /E2E Tierlist/ });
await expect(toolChoice.locator('xpath=..').locator('svg')).toBeVisible();
@@ -1424,7 +1478,6 @@ test('tools stay editable in pieces after catalog deactivation', async ({ page }
await page.screenshot({ path: testInfo.outputPath('tool-catalog-mobile.png'), fullPage: true });
await page.goto('/fundus/piece-quiz');
await page.getByRole('button', { name: 'Recherche & Quellen' }).click();
await expect(page.locator('.tool-assignment-row')).toContainText('E2E Tierlist');
await expect(page.locator('.tool-assignment-row')).toContainText('Deaktiviert');
await expect(page.getByLabel('Arbeitslink')).toHaveValue(
@@ -1460,7 +1513,7 @@ test('validated YouTube and TikTok players switch without exposing private field
await page.getByText('Menü', { exact: true }).click();
await page.getByText('Ausgabe einrichten', { exact: true }).click();
const popupPromise = page.waitForEvent('popup');
await page.getByRole('button', { name: 'Mit dieser Sendung verbinden und öffnen' }).click();
await page.getByRole('button', { name: 'Mit diesem Stream verbinden und öffnen' }).click();
const output = await popupPromise;
await expect(output.locator('.output-content')).toHaveCount(0);
await expect(output.locator('.heinzlive-output')).toHaveClass(/empty/);
@@ -1510,8 +1563,9 @@ test('live control persists state and exposes only explicit presentation data',
setSectionDuration('section-stories', 300);
await page.setViewportSize({ width: 1280, height: 900 });
await page.goto('/fundus?q=kleine+Konsole&includePlanned=1');
await page.getByRole('heading', { name: 'Die kleine Konsole mit dem großen Dreh' }).click();
await page.getByRole('button', { name: 'Präsentation', exact: true }).click();
await page
.getByRole('link', { name: 'Die kleine Konsole mit dem großen Dreh', exact: true })
.click();
const png = Buffer.from(
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=',
'base64'
@@ -1523,11 +1577,9 @@ test('live control persists state and exposes only explicit presentation data',
});
await page.getByLabel('Alternativtext').fill('Kleine Konsole im Live-Bild');
await page.getByRole('button', { name: 'Bild hochladen' }).click();
await page.getByRole('button', { name: 'Präsentation', exact: true }).click();
await page.getByRole('radio', { name: 'Nur Bild', exact: true }).check();
await expect(page.locator('.save-indicator')).toContainText('Gespeichert', { timeout: 10_000 });
await page.reload();
await page.getByRole('button', { name: 'Präsentation', exact: true }).click();
await expect(page.getByRole('radio', { name: 'Nur Bild', exact: true })).toBeChecked();
await page.getByRole('radio', { name: 'Text', exact: true }).check();
await expect(page.locator('.save-indicator')).toContainText('Gespeichert', { timeout: 10_000 });
@@ -1537,14 +1589,14 @@ test('live control persists state and exposes only explicit presentation data',
.filter({ hasText: 'Die kleine Konsole mit dem großen Dreh' })
.first();
await plannedConsole.getByRole('button', { name: /bearbeiten und verschieben/ }).click();
await plannedConsole.getByRole('button', { name: 'Aus Fundus aktualisieren' }).click();
await plannedConsole.getByRole('button', { name: 'Aus Piece aktualisieren' }).click();
await page.goto('/live?show=show-october');
await expect(page.getByRole('heading', { name: 'Bereit für die Sendung' })).toBeVisible();
await expect(page.getByRole('heading', { name: 'Bereit für den Stream' })).toBeVisible();
await expect(page.locator('.sidebar')).toHaveCount(0);
await expect(page.locator('.mobile-header')).toHaveCount(0);
await expect(page.locator('.mobile-navigation')).toHaveCount(0);
await page.getByRole('button', { name: 'Sendung starten' }).click();
await page.getByRole('button', { name: 'Stream starten' }).click();
await expect(page.locator('#current-title')).toHaveText('Die kleine Konsole mit dem großen Dreh');
await expect(page.getByRole('button', { name: 'Zurück', exact: true })).toBeDisabled();
await expect(page.locator('.section-countdown')).toContainText(/0[45]:\d{2}/);
@@ -1562,14 +1614,14 @@ test('live control persists state and exposes only explicit presentation data',
await page.keyboard.press('Escape');
await expect(liveMenu).toBeHidden();
await expect(liveMenuTrigger).toBeFocused();
await page.getByRole('button', { name: 'Sendung pausieren' }).click();
await expect(page.getByRole('button', { name: 'Sendung fortsetzen' })).toBeVisible();
await page.getByRole('button', { name: 'Stream pausieren' }).click();
await expect(page.getByRole('button', { name: 'Stream fortsetzen' })).toBeVisible();
await liveMenuTrigger.click();
await expect(liveMenu).toContainText('Pausiert');
await expect(liveMenu.getByRole('button', { name: 'Fortsetzen', exact: true })).toHaveCount(0);
await page.keyboard.press('Escape');
await page.getByRole('button', { name: 'Sendung fortsetzen' }).click();
await expect(page.getByRole('button', { name: 'Sendung pausieren' })).toBeVisible();
await page.getByRole('button', { name: 'Stream fortsetzen' }).click();
await expect(page.getByRole('button', { name: 'Stream pausieren' })).toBeVisible();
await liveMenuTrigger.press('Enter');
await expect(liveMenu).toBeVisible();
await page.locator('.live-current').click({ position: { x: 8, y: 8 } });
@@ -1600,7 +1652,7 @@ test('live control persists state and exposes only explicit presentation data',
await page.getByText('Ausgabe einrichten', { exact: true }).click();
const [popup] = await Promise.all([
page.waitForEvent('popup'),
page.getByRole('button', { name: 'Mit dieser Sendung verbinden und öffnen' }).click()
page.getByRole('button', { name: 'Mit diesem Stream verbinden und öffnen' }).click()
]);
await popup.waitForURL(/\/heinzlive\/default#/);
const outputUrl = popup.url();
@@ -1899,7 +1951,7 @@ test('live control persists state and exposes only explicit presentation data',
'Warum praktische Effekte wieder auffallen'
);
await page.getByRole('button', { name: 'Erledigen & Sendungsende' }).click();
await page.getByRole('button', { name: 'Erledigen & Streamende' }).click();
await expect(page.getByRole('heading', { name: 'Alle Pieces bearbeitet' })).toBeVisible();
await expect(page.locator('.live-finished-label')).toHaveText('Ablauf abgeschlossen');
await expect(page.getByRole('button', { name: 'Erledigen & weiter' })).toHaveCount(0);
@@ -1927,7 +1979,7 @@ test('live control persists state and exposes only explicit presentation data',
).toBeVisible();
await page.getByText('Menü', { exact: true }).click();
page.once('dialog', (dialog) => dialog.accept());
await page.getByRole('button', { name: 'Sendung beenden' }).click();
await page.getByRole('button', { name: 'Stream beenden' }).click();
await page.getByText('Menü', { exact: true }).click();
await expect(page.getByRole('dialog', { name: 'Regiemenü' })).toContainText('Lauf beendet');
await expect(page.getByRole('button', { name: 'Beendeten Lauf wiederaufnehmen' })).toBeVisible();
@@ -1961,7 +2013,7 @@ test('completion, archive and protected guest sharing work end to end', async ({
const planningGuest = await browser.newContext();
const planningGuestPage = await planningGuest.newPage();
await planningGuestPage.goto(planningSharePath!);
await expect(planningGuestPage.getByText('Aktuelle Sendungsplanung · Nur lesen')).toBeVisible();
await expect(planningGuestPage.getByText('Aktuelle Streamplanung · Nur lesen')).toBeVisible();
await expect(planningGuestPage.getByText('Gesendeter Abschlussbeitrag')).toBeVisible();
await expect(planningGuestPage.getByText('FREIGABE_MODERATION')).toBeVisible();
const guestThemePreference = await planningGuestPage
@@ -1998,7 +2050,7 @@ test('completion, archive and protected guest sharing work end to end', async ({
await page.goto('/live?show=e2e-completion');
await page.getByRole('button', { name: 'Menü', exact: true }).click();
await page.getByRole('button', { name: 'Sendung abschließen' }).click();
await page.getByRole('button', { name: 'Stream abschließen' }).click();
const completion = page.locator('.completion-dialog');
await expect(completion).toBeVisible();
await expect(completion).toContainText('1 gesendet');
@@ -2008,13 +2060,18 @@ test('completion, archive and protected guest sharing work end to end', async ({
await completion.getByRole('button', { name: 'Historischen Stand abschließen' }).click();
await expect(completion).toBeHidden();
await page.goto('/archiv');
await expect(page.getByRole('link', { name: 'E2E Abschluss' })).toBeVisible();
await page.getByRole('link', { name: 'E2E Abschluss' }).click();
await expect(page.getByText('Unveränderliche Sendungshistorie')).toBeVisible();
await page.goto('/sendungen?state=completed');
await expect(
page.locator('.show-card-list').getByRole('link', { name: /E2E Abschluss/ })
).toBeVisible();
await page
.locator('.show-card-list')
.getByRole('link', { name: /E2E Abschluss/ })
.click();
await expect(page.getByText('Unveränderliche Streamhistorie')).toBeVisible();
await expect(page.getByText('Gesendeter Abschlussbeitrag')).toBeVisible();
await expect(page.getByText('Übersprungener Abschlussbeitrag')).toBeVisible();
await expect(page.getByRole('button', { name: 'Sendungsdaten speichern' })).toHaveCount(0);
await expect(page.getByRole('button', { name: 'Streamdaten speichern' })).toHaveCount(0);
expect(
await page
.locator('.historical-section')
@@ -2040,7 +2097,7 @@ test('completion, archive and protected guest sharing work end to end', async ({
}));
expect(dialogStyle.borderRadius).toBeGreaterThan(0);
expect(dialogStyle.background).not.toBe('rgba(0, 0, 0, 0)');
await shareManagement.getByLabel('Sendung').selectOption('e2e-completion');
await shareManagement.getByLabel('Stream').selectOption('e2e-completion');
await shareManagement.getByLabel('Informationsumfang').selectOption('moderation');
await shareManagement.getByLabel('Optionales Passwort').fill('E2E-Freigabe-Passwort');
await shareManagement.getByRole('button', { name: 'Leselink anlegen' }).click();
@@ -2058,10 +2115,10 @@ test('completion, archive and protected guest sharing work end to end', async ({
expect(passwordHtml).not.toContain('password_hash');
expect(passwordHtml).not.toContain('e2e-completion');
await guestPage.getByLabel('Passwort').fill('falsch');
await guestPage.getByRole('button', { name: 'Sendung öffnen' }).click();
await guestPage.getByRole('button', { name: 'Stream öffnen' }).click();
await expect(guestPage.getByRole('alert')).toContainText('Passwort falsch');
await guestPage.getByLabel('Passwort').fill('E2E-Freigabe-Passwort');
await guestPage.getByRole('button', { name: 'Sendung öffnen' }).click();
await guestPage.getByRole('button', { name: 'Stream öffnen' }).click();
await expect(guestPage.getByRole('heading', { name: 'E2E Abschluss' })).toBeVisible();
await expect(guestPage.getByText('FREIGABE_MODERATION')).toBeVisible();
await expect(guestPage.getByText('PRIVATE_FREIGABE_RECHERCHE')).toHaveCount(0);
@@ -2125,20 +2182,24 @@ test('pieces autosave safely, detect conflicts and protect uploaded images', asy
page,
browser
}, testInfo) => {
test.setTimeout(45_000);
await page.setViewportSize({ width: 1280, height: 1000 });
await page.goto('/fundus');
await page.locator('.template-picker > summary').click();
await page.getByRole('button', { name: /Reaction \/ Fundstück/ }).click();
await expect(page).toHaveURL(/\/fundus\/[a-f0-9-]+$/);
const pieceUrl = page.url();
const writingBox = await page.locator('.piece-writing-stack').first().boundingBox();
const metadataBox = await page.locator('.piece-meta-details').boundingBox();
expect(writingBox?.x).toBeLessThan(metadataBox?.x ?? 0);
await expectNoHorizontalOverflow(page);
await editInlineTitle(page, 'Neues Fundstück', 'E2E Fundstück mit Markdown');
await expect(page.locator('.save-indicator')).toContainText('Gespeichert', { timeout: 10_000 });
await page.getByRole('button', { name: 'Editor schließen' }).click();
await expect(page).toHaveURL(/\/fundus$/);
await page.getByRole('heading', { name: 'E2E Fundstück mit Markdown' }).click();
await page.getByRole('link', { name: 'E2E Fundstück mit Markdown', exact: true }).click();
await expect(page).toHaveURL(pieceUrl);
await page.getByText('Status, Dauer und Einordnung').click();
await page
.getByLabel('Kurzbeschreibung')
.fill('Persistiert nach Neuladen und ist gezielt suchbar.');
@@ -2147,21 +2208,21 @@ test('pieces autosave safely, detect conflicts and protect uploaded images', asy
await page.getByText('Themen auswählen', { exact: true }).click();
await page.getByLabel('Gaming').check();
await openEditorialField(page, 'Private Recherche');
await page
.getByRole('textbox', { name: 'Private Recherche', exact: true })
.fill('Nur intern: **Hintergrund prüfen**.');
await fillMarkdownSource(page, 'Private Recherche', 'Nur intern: **Hintergrund prüfen**.');
await openEditorialField(page, 'Moderationsstichpunkte');
await page
.getByRole('textbox', { name: 'Moderationsstichpunkte', exact: true })
.fill('Den Kern in zwei Sätzen erklären.');
await openEditorialField(page, 'Private Recherche');
await expect(page.getByRole('textbox', { name: 'Private Recherche', exact: true })).toHaveValue(
await expect(page.locator('input[name="researchText"]')).toHaveValue(
'Nur intern: **Hintergrund prüfen**.'
);
await openEditorialField(page, 'Präsentationstext');
await page
.getByRole('textbox', { name: 'Öffentlicher Vorspann', exact: true })
.fill('## Sichere Vorschau\n\n<script>globalThis.e2eXss = true</script> **sichtbar**');
await fillMarkdownSource(
page,
'Öffentlicher Vorspann',
'## Sichere Vorschau\n\n<script>globalThis.e2eXss = true</script> **sichtbar**'
);
await openEditorialField(page, 'Private Recherche');
await page.getByRole('button', { name: 'Quelle hinzufügen' }).click();
const source = page.locator('.source-row').last();
@@ -2178,10 +2239,9 @@ test('pieces autosave safely, detect conflicts and protect uploaded images', asy
await expect(
page.getByRole('button', { name: 'E2E Fundstück mit Markdown Titel bearbeiten' })
).toBeVisible();
await page.getByText('Status, Dauer und Einordnung').click();
await expect(page.getByLabel('Dauer in Minuten')).toHaveValue('10.25');
await openEditorialField(page, 'Private Recherche');
await expect(page.getByRole('textbox', { name: 'Private Recherche', exact: true })).toHaveValue(
await expect(page.locator('input[name="researchText"]')).toHaveValue(
'Nur intern: **Hintergrund prüfen**.'
);
@@ -2189,7 +2249,6 @@ test('pieces autosave safely, detect conflicts and protect uploaded images', asy
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=',
'base64'
);
await page.getByRole('button', { name: 'Präsentation', exact: true }).click();
await page.getByLabel('Bild', { exact: true }).setInputFiles({
name: 'redaktionsbild.png',
mimeType: 'image/png',
@@ -2212,7 +2271,6 @@ test('pieces autosave safely, detect conflicts and protect uploaded images', asy
await concurrent.goto(pieceUrl);
await editInlineTitle(page, 'E2E Fundstück mit Markdown', 'E2E Fundstück, extern aktualisiert');
await expect(page.locator('.save-indicator')).toContainText('Gespeichert', { timeout: 10_000 });
await concurrent.getByText('Status, Dauer und Einordnung').click();
await concurrent
.getByLabel('Kurzbeschreibung')
.fill('Diese veraltete Änderung darf nicht gewinnen.');
@@ -2223,7 +2281,6 @@ test('pieces autosave safely, detect conflicts and protect uploaded images', asy
await expect(
concurrent.getByRole('button', { name: 'E2E Fundstück, extern aktualisiert Titel bearbeiten' })
).toBeVisible();
await concurrent.getByText('Status, Dauer und Einordnung').click();
await expect(concurrent.getByLabel('Kurzbeschreibung')).not.toHaveValue(
'Diese veraltete Änderung darf nicht gewinnen.'
);
@@ -2234,7 +2291,7 @@ test('pieces autosave safely, detect conflicts and protect uploaded images', asy
await expect(
page.getByRole('heading', { name: 'E2E Fundstück, extern aktualisiert' })
).toBeVisible();
await page.getByRole('heading', { name: 'E2E Fundstück, extern aktualisiert' }).click();
await page.getByRole('link', { name: 'E2E Fundstück, extern aktualisiert', exact: true }).click();
await page.getByRole('button', { name: 'Piece archivieren' }).click();
await expect(page).toHaveURL(/\/archiv$/);
const archived = page.locator('.archive-list article').filter({ hasText: 'extern aktualisiert' });
@@ -2267,7 +2324,6 @@ test('pieces autosave safely, detect conflicts and protect uploaded images', asy
await page.setViewportSize({ width: 390, height: 844 });
await setTheme(page, 'dark');
await page.reload();
await page.getByRole('button', { name: 'Recherche & Quellen' }).click();
await expect(page.getByRole('heading', { name: 'Quellen', exact: true })).toBeVisible();
await expectNoHorizontalOverflow(page);
await page.screenshot({
@@ -2291,11 +2347,10 @@ test('a quickly captured video can be written and previewed before classificatio
await page.getByRole('button', { name: 'Im Eingang ablegen' }).click();
await expect(page).toHaveURL(/\/eingang(?:\?\/capture)?$/);
await page.locator('.inbox-item').filter({ hasText: 'Schnelles Video-Fundstück' }).click();
await page.getByRole('button', { name: 'Recherche & Quellen' }).click();
await expect(page.locator('.source-row').getByLabel('URL')).toHaveValue(
'https://youtu.be/dQw4w9WgXcQ'
);
await expect(page.getByLabel('Status')).toBeHidden();
await expect(page.getByLabel('Status')).toBeVisible();
await openEditorialField(page, 'Private Recherche');
await page
@@ -2306,7 +2361,6 @@ test('a quickly captured video can be written and previewed before classificatio
.getByRole('textbox', { name: 'Moderationsstichpunkte', exact: true })
.fill('Erst Clip zeigen, dann die Beobachtung öffnen.');
await page.getByRole('button', { name: 'Präsentation', exact: true }).click();
await page.getByRole('radio', { name: 'Video', exact: true }).check();
await page.getByRole('combobox', { name: 'Video-URL', exact: true }).selectOption('0');
await expect(page.locator('.save-indicator')).toContainText('Gespeichert', { timeout: 10_000 });
@@ -2321,9 +2375,7 @@ test('a quickly captured video can be written and previewed before classificatio
await preview.close();
await page.setViewportSize({ width: 390, height: 844 });
await page.getByRole('button', { name: 'Präsentation', exact: true }).click();
await expect(page.getByRole('radio', { name: 'Video', exact: true })).toBeVisible();
await page.getByRole('button', { name: 'Recherche & Quellen' }).click();
await expect(page.getByRole('textbox', { name: 'Private Recherche', exact: true })).toBeVisible();
await expectNoHorizontalOverflow(page);
});
@@ -2332,7 +2384,6 @@ test('link preview uses one existing source, persists manual fields and isolates
page
}, testInfo) => {
await page.goto('/fundus/piece-console');
await page.getByRole('button', { name: 'Präsentation', exact: true }).click();
await page.getByRole('radio', { name: 'Linkvorschau', exact: true }).check();
await page.getByRole('combobox', { name: 'Quelle/URL', exact: true }).selectOption('0');
await page.getByLabel('Kartentitel').fill('Manuell korrigierte Linkvorschau');
@@ -2340,7 +2391,6 @@ test('link preview uses one existing source, persists manual fields and isolates
await page.getByLabel('Vorschaubild-URL').fill('');
await expect(page.locator('.save-indicator')).toContainText('Gespeichert', { timeout: 10_000 });
await page.reload();
await page.getByRole('button', { name: 'Präsentation', exact: true }).click();
await expect(page.getByRole('radio', { name: 'Linkvorschau', exact: true })).toBeChecked();
await expect(page.getByLabel('Kartentitel')).toHaveValue('Manuell korrigierte Linkvorschau');
@@ -2366,10 +2416,8 @@ test('link preview uses one existing source, persists manual fields and isolates
});
await preview.close();
await page.getByRole('button', { name: 'Recherche & Quellen' }).click();
await page.locator('.source-row').first().getByLabel('URL').fill('http://127.0.0.1/private');
await expect(page.locator('.save-indicator')).toContainText('Gespeichert', { timeout: 10_000 });
await page.getByRole('button', { name: 'Präsentation', exact: true }).click();
const blockedPreview = page.waitForResponse((response) =>
response.url().includes('?/loadPreview')
);