Refine live control and show overview

This commit is contained in:
hnzio committed 2026-10-24 09:45:01 +02:00
1 parent ec596d442e
commit b1474bc33d
16 files changed
+1580 -405

No files matched your search

+188 -19
View File
@@ -196,6 +196,38 @@ function removePresentationFixture() {
database.close();
}
function addCalendarFixture() {
const dataDirectory = process.env.SENDEWERK_E2E_DATA_DIR;
if (!dataDirectory) throw new Error('E2E-Datenverzeichnis ist nicht konfiguriert.');
const database = new Database(path.join(dataDirectory, 'e2e.sqlite'));
const insert = database.prepare(
`INSERT INTO shows (id, title, starts_at_utc, twitch_title, announcement_text, status,
revision, created_at_utc, updated_at_utc) VALUES (?, ?, ?, '', '', 'draft', 1, ?, ?)`
);
const now = '2026-10-24T10:00:00.000Z';
database.transaction(() => {
insert.run('e2e-calendar-late', 'Kalender Spättermin', '2026-10-24T18:00:00.000Z', now, now);
insert.run('e2e-calendar-early', 'Kalender Frühtermin', '2026-10-24T08:00:00.000Z', now, now);
insert.run(
'e2e-calendar-next',
'Kalender Novembertermin',
'2026-11-03T17:00:00.000Z',
now,
now
);
insert.run('e2e-calendar-undated', 'Kalender ohne Termin', null, now, now);
})();
database.close();
}
function removeCalendarFixture() {
const dataDirectory = process.env.SENDEWERK_E2E_DATA_DIR;
if (!dataDirectory) return;
const database = new Database(path.join(dataDirectory, 'e2e.sqlite'));
database.prepare("DELETE FROM shows WHERE id LIKE 'e2e-calendar-%'").run();
database.close();
}
function setSectionDuration(sectionId: string, seconds: number | null) {
const dataDirectory = process.env.SENDEWERK_E2E_DATA_DIR;
if (!dataDirectory) throw new Error('E2E-Datenverzeichnis ist nicht konfiguriert.');
@@ -495,6 +527,56 @@ test('sent and planned source filters stay independent and removing the last usa
}
});
test('show overview switches persistently between list, calendar and mobile agenda', async ({
page
}, testInfo) => {
addCalendarFixture();
try {
await page.setViewportSize({ width: 1440, height: 1000 });
await page.goto('/sendungen?month=2026-10');
await page.getByRole('button', { name: 'Kalender' }).click();
await expect(page.getByRole('heading', { name: 'Oktober 2026' })).toBeVisible();
const octoberDay = page
.locator('.show-calendar-day')
.filter({ hasText: 'Kalender Frühtermin' });
await expect(octoberDay).toHaveClass(/today/);
await expect(octoberDay).toContainText('Kalender Spättermin');
await expect(octoberDay.locator('.show-calendar-event')).toHaveCount(2);
await expect(octoberDay.locator('.show-calendar-event').first()).toContainText('10:00');
await expect(page.locator('.show-undated')).toContainText('Kalender ohne Termin');
await expectNoHorizontalOverflow(page);
await page.screenshot({
path: testInfo.outputPath('sendungen-kalender-desktop.png'),
fullPage: true
});
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 page.reload();
await expect(page.getByRole('button', { name: 'Kalender' })).toHaveAttribute(
'aria-pressed',
'true'
);
await page.setViewportSize({ width: 390, height: 844 });
await page.getByRole('link', { name: 'Vorheriger Monat' }).click();
await expect(page.locator('.show-calendar')).toBeHidden();
await expect(page.locator('.show-agenda')).toContainText('Kalender Frühtermin');
await expect(page.locator('.show-agenda')).toContainText('Kalender Spättermin');
await expectNoHorizontalOverflow(page);
await page.screenshot({
path: testInfo.outputPath('sendungen-kalender-mobile.png'),
fullPage: true
});
await page.getByRole('button', { name: 'Liste' }).click();
await expect(page.getByRole('heading', { name: 'Geplante Sendungen' })).toBeVisible();
} finally {
removeCalendarFixture();
}
});
test('desktop builder keeps the fundus beside the wider run sheet', async ({ page }, testInfo) => {
await page.setViewportSize({ width: 1280, height: 900 });
await page.goto('/fundus');
@@ -1083,10 +1165,25 @@ test('live control persists state and exposes only explicit presentation data',
await expect(page.locator('.mobile-navigation')).toHaveCount(0);
await page.getByRole('button', { name: 'Sendung 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}/);
await expect(
page.locator('.live-menu button', { hasText: 'Live-Stand wiederaufnehmen' })
).toHaveCount(0);
const liveMenuTrigger = page.getByRole('button', { name: 'Menü', exact: true });
await liveMenuTrigger.click();
const liveMenu = page.getByRole('dialog', { name: 'Regiemenü' });
await expect(liveMenu).toBeVisible();
await expect(liveMenu).toContainText('Herbstsendung: Kleine Welten');
await expect(liveMenu.getByRole('button', { name: 'Live-Stand wiederaufnehmen' })).toHaveCount(0);
await expect(page.getByRole('button', { name: 'Schrift verkleinern' })).toBeFocused();
await page.keyboard.press('Escape');
await expect(liveMenu).toBeHidden();
await expect(liveMenuTrigger).toBeFocused();
await liveMenuTrigger.press('Enter');
await expect(liveMenu).toBeVisible();
await page.locator('.live-current').click({ position: { x: 8, y: 8 } });
await expect(liveMenu).toBeHidden();
await expect(liveMenuTrigger).toBeFocused();
await expect(page.locator('#current-title')).toHaveText('Die kleine Konsole mit dem großen Dreh');
await expect(page.locator('.live-output-state')).toContainText('Leer');
await page.getByText('Quellen · 1', { exact: true }).click();
await expect(page.locator('.live-materials')).toContainText('Herstellerseite');
await expect(
@@ -1152,7 +1249,7 @@ test('live control persists state and exposes only explicit presentation data',
await page.getByText('Reserve', { exact: true }).click();
await page.getByRole('button', { name: 'Als Nächstes' }).click();
await expect(page.locator('.live-next-summary')).toContainText('Pixelart');
await expect(page.locator('.live-next-inline')).toContainText('Pixelart');
await page.getByRole('button', { name: 'Erledigen & weiter' }).click();
await expect(page.locator('.live-current')).toContainText('Pixelart');
await page.getByRole('button', { name: 'Überspringen' }).click();
@@ -1181,9 +1278,28 @@ test('live control persists state and exposes only explicit presentation data',
await page.locator('.live-current-scroll').evaluate((element) => element.scrollTop)
).toBeGreaterThan(0);
const countdownBeforeBack = await page.locator('.section-countdown strong').textContent();
await page.getByRole('button', { name: 'Zurück', exact: true }).click();
await expect(page.locator('#current-title')).toHaveText(
'Pixelart, die nicht nostalgisch sein will'
);
await expect(
page
.locator('.live-run-item.current')
.filter({ hasText: 'Warum praktische Effekte wieder auffallen' })
).toBeVisible();
await expect(page.getByRole('button', { name: 'Überspringen' })).toBeDisabled();
await expect(page.getByRole('button', { name: /Erledigen/ })).toBeDisabled();
await expect(
output.getByRole('heading', { name: 'Die kleine Konsole mit dem großen Dreh' })
).toBeVisible();
const countdownAfterBack = await page.locator('.section-countdown strong').textContent();
expect(countdownAfterBack).not.toBe('05:00');
expect(countdownAfterBack?.slice(0, 3)).toBe(countdownBeforeBack?.slice(0, 3));
await page
.getByRole('button', {
name: 'Die kleine Konsole mit dem großen Dreh als aktuelles Piece auswählen'
name: 'Die kleine Konsole mit dem großen Dreh in der Regie auswählen'
})
.click();
await expect(page.locator('#current-title')).toHaveText('Die kleine Konsole mit dem großen Dreh');
@@ -1192,13 +1308,13 @@ test('live control persists state and exposes only explicit presentation data',
.toBe(0);
await expect(
page.locator('.live-run-item').filter({ hasText: 'Warum praktische Effekte wieder auffallen' })
).toContainText('Kommend');
).toContainText('Aktuell');
await expect(
output.getByRole('heading', { name: 'Die kleine Konsole mit dem großen Dreh' })
).toBeVisible();
await page
.getByRole('button', {
name: 'Warum praktische Effekte wieder auffallen als aktuelles Piece auswählen'
name: 'Warum praktische Effekte wieder auffallen in der Regie auswählen'
})
.click();
await expect(page.locator('#current-title')).toHaveText(
@@ -1267,6 +1383,53 @@ test('live control persists state and exposes only explicit presentation data',
});
await page.screenshot({ path: testInfo.outputPath('live-controller-compact-desktop.png') });
await page.setViewportSize({ width: 670, height: 450 });
await page.evaluate(() => {
const section = document.querySelector('.live-context-title.section span');
const next = document.querySelector('.live-context-title.next span');
if (section)
section.textContent =
'Aktuelles Segment mit einem außergewöhnlich langen Namen für die kompakte Regie';
if (next)
next.textContent =
'Danach folgt ein Piece mit einem ebenfalls außergewöhnlich langen und vollständigen Titel';
});
const smallHeader = page.locator('.live-command-header');
const smallHeaderBox = await smallHeader.boundingBox();
expect(smallHeaderBox?.height ?? 999).toBeLessThan(105);
await expect(page.locator('.live-context-title.section span')).toHaveCSS('white-space', 'nowrap');
await expect(page.locator('.live-context-title.next span')).toHaveCSS('white-space', 'nowrap');
await page.locator('.live-context-title.section').click();
await expect(page.locator('.live-context-title.section')).toHaveClass(/expanded/);
await expect(page.locator('.live-context-title.section span')).toHaveCSS('white-space', 'normal');
await page.keyboard.press('Escape');
await expect(page.locator('.live-context-title.section')).not.toHaveClass(/expanded/);
await page.locator('.live-context-title.next').focus();
await page.locator('.live-context-title.next').press('Enter');
await expect(page.locator('.live-context-title.next')).toHaveClass(/expanded/);
await page.keyboard.press('Escape');
await liveMenuTrigger.click();
await expect(liveMenu).toBeVisible();
const compactMenuBox = await liveMenu.boundingBox();
expect(compactMenuBox?.x ?? -1).toBeGreaterThanOrEqual(0);
expect(compactMenuBox?.y ?? -1).toBeGreaterThanOrEqual(0);
expect(
Math.ceil((compactMenuBox?.x ?? 999) + (compactMenuBox?.width ?? 999))
).toBeLessThanOrEqual(670);
expect(
Math.ceil((compactMenuBox?.y ?? 999) + (compactMenuBox?.height ?? 999))
).toBeLessThanOrEqual(450);
await liveMenu.getByRole('link', { name: /Zur Planung/ }).scrollIntoViewIfNeeded();
await expect(liveMenu.getByRole('link', { name: /Zur Planung/ })).toBeVisible();
await liveMenu.getByRole('button', { name: 'Zeiten ausblenden' }).click();
await expect(page.locator('.live-piece-time')).toHaveCount(0);
await liveMenu.getByRole('button', { name: 'Zeiten einblenden' }).click();
await expect(page.locator('.live-piece-time')).toBeVisible();
await page.keyboard.press('Escape');
await expect(liveMenuTrigger).toBeFocused();
await expectNoHorizontalOverflow(page);
await page.screenshot({ path: testInfo.outputPath('live-controller-670x450.png') });
const countdownBeforeReload = await page.locator('.section-countdown strong').textContent();
await page.reload();
await expect(page.locator('#current-title')).toHaveText(
@@ -1287,7 +1450,17 @@ test('live control persists state and exposes only explicit presentation data',
await page.evaluate(() => document.documentElement.scrollHeight <= window.innerHeight)
).toBe(true);
await page.screenshot({ path: testInfo.outputPath('live-controller-mobile.png') });
await page.getByRole('button', { name: /Ablauf · Piece/ }).click();
await liveMenuTrigger.click();
const mobileMenuBox = await liveMenu.boundingBox();
expect(Math.ceil((mobileMenuBox?.x ?? 999) + (mobileMenuBox?.width ?? 999))).toBeLessThanOrEqual(
390
);
expect(Math.ceil((mobileMenuBox?.y ?? 999) + (mobileMenuBox?.height ?? 999))).toBeLessThanOrEqual(
700
);
await page.keyboard.press('Escape');
await expect(liveMenuTrigger).toBeFocused();
await page.getByRole('button', { name: /Ablauf ·/ }).click();
await expect(liveRail).toHaveClass(/open/);
await expect(railClose).toBeFocused();
await expect
@@ -1302,26 +1475,26 @@ test('live control persists state and exposes only explicit presentation data',
await page.screenshot({ path: testInfo.outputPath('live-controller-mobile-run.png') });
await page.keyboard.press('Escape');
await expect(liveRail).toBeHidden();
await expect(page.getByRole('button', { name: /Ablauf · Piece/ })).toBeFocused();
await page.getByRole('button', { name: /Ablauf · Piece/ }).click();
await expect(page.getByRole('button', { name: /Ablauf ·/ })).toBeFocused();
await page.getByRole('button', { name: /Ablauf ·/ }).click();
await page.setViewportSize({ width: 900, height: 450 });
await expect(liveRail).not.toHaveAttribute('role', 'dialog');
await expect(page.locator('.live-command-header')).not.toHaveAttribute('inert', '');
await page.setViewportSize({ width: 390, height: 700 });
await expect(liveRail).toBeHidden();
await page.getByRole('button', { name: /Ablauf · Piece/ }).click();
await page.getByRole('button', { name: /Ablauf ·/ }).click();
await liveRail
.getByRole('button', {
name: 'Die kleine Konsole mit dem großen Dreh als aktuelles Piece auswählen'
name: 'Die kleine Konsole mit dem großen Dreh in der Regie auswählen'
})
.click();
await expect(liveRail).toBeHidden();
await expect(page.locator('#current-title')).toHaveText('Die kleine Konsole mit dem großen Dreh');
await page.getByRole('button', { name: /Ablauf · Piece/ }).click();
await page.getByRole('button', { name: /Ablauf ·/ }).click();
await liveRail
.getByRole('button', {
name: 'Warum praktische Effekte wieder auffallen als aktuelles Piece auswählen'
name: 'Warum praktische Effekte wieder auffallen in der Regie auswählen'
})
.click();
await expect(liveRail).toBeHidden();
@@ -1329,15 +1502,11 @@ test('live control persists state and exposes only explicit presentation data',
'Warum praktische Effekte wieder auffallen'
);
await page.getByRole('button', { name: 'Erledigen' }).click();
await expect(page.getByRole('button', { name: 'Live-Stand wiederaufnehmen' })).toBeVisible();
await page.getByRole('button', { name: 'Live-Stand wiederaufnehmen' }).click();
await expect(page.locator('#current-title')).toHaveText('Die kleine Konsole mit dem großen Dreh');
await expectNoHorizontalOverflow(page);
await page.getByRole('button', { name: 'Erledigen & Sendungsende' }).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);
await expectNoHorizontalOverflow(page);
await output.route('**/state', (route) => route.abort());
await expect(output.getByText('Verbindung unterbrochen')).toBeVisible({ timeout: 5_000 });