Build compact live control view
This commit is contained in:
1 parent
3389ba17f4
commit
bd9ffe6faa
10 files changed
+1735
-787
No files matched your search
+17
-7
@@ -413,14 +413,24 @@
|
|||||||
- reale OBS-Browserquelle, echte YouTube-/TikTok-Wiedergabe und Safari auf einem
|
- reale OBS-Browserquelle, echte YouTube-/TikTok-Wiedergabe und Safari auf einem
|
||||||
iOS-Gerät nicht ausgeführt; Anbieterrequests wurden lokal abgefangen
|
iOS-Gerät nicht ausgeführt; Anbieterrequests wurden lokal abgefangen
|
||||||
|
|
||||||
## Verwaltungskorrektur 0.5.11
|
## Kompakte Live-Regie und Verwaltungskorrektur 0.5.11
|
||||||
|
|
||||||
| Kriterium | Nachweis | Stand |
|
| Kriterium | Nachweis | Stand |
|
||||||
| ------------------------------------------ | ---------------------------------------------------------------------------------------- | --------------------- |
|
| ------------------------------------------- | ------------------------------------------------------------------------------------------------ | --------------------- |
|
||||||
| Werkzeugdialog ist initial geschlossen | Chromium prüft den Dialog vor jeder Bedienaktion bei 390 und 1280 Pixeln | erfüllt am 23.10.2026 |
|
| Werkzeugdialog ist initial geschlossen | Chromium prüft den Dialog vor jeder Bedienaktion bei 390 und 1280 Pixeln | erfüllt am 24.10.2026 |
|
||||||
| Dialog öffnet nur nach bewusster Aktion | Browser legt ein Werkzeug an und öffnet ein bestehendes Werkzeug anschließend zum Ändern | erfüllt am 23.10.2026 |
|
| Dialog öffnet nur nach bewusster Aktion | Browser legt ein Werkzeug an und öffnet ein bestehendes Werkzeug anschließend zum Ändern | erfüllt am 24.10.2026 |
|
||||||
| Bestehender Werkzeugablauf bleibt erhalten | Browser prüft Icon, Format, Deaktivierung und Piece-Zuordnung nach erneutem Laden | erfüllt am 23.10.2026 |
|
| Bestehender Werkzeugablauf bleibt erhalten | Browser prüft Icon, Format, Deaktivierung und Piece-Zuordnung nach erneutem Laden | erfüllt am 24.10.2026 |
|
||||||
| Themen-Iconpicker bleibt im Viewport | Chromium prüft Öffnen und Auswahl bei 390, 960 und 1280 Pixeln | erfüllt am 23.10.2026 |
|
| Themen-Iconpicker bleibt im Viewport | Chromium prüft Öffnen und Auswahl bei 390, 960 und 1280 Pixeln | erfüllt am 24.10.2026 |
|
||||||
|
| Globale Navigation bleibt aus der Regie | Chromium findet weder Seitenleiste noch mobilen App-Header oder Bottomnavigation | erfüllt am 24.10.2026 |
|
||||||
|
| Kleines Desktopfenster bleibt zweispaltig | Chromium prüft Moderation, Ablauf und feste Aktionen bei 900 × 450 Pixeln | erfüllt am 24.10.2026 |
|
||||||
|
| Lange Inhalte scrollen innerhalb der Fläche | Synthetischer Langtext und zusätzliche Ablaufabschnitte erzeugen zwei unabhängige Scroller | erfüllt am 24.10.2026 |
|
||||||
|
| Mobile Regie verwendet ein Ablauf-Sheet | 390 × 700 prüft Endlage, Fokus, Escape, Aktionsschließen und Wechsel auf 900 Pixel | erfüllt am 24.10.2026 |
|
||||||
|
| Leseposition bleibt am Piece | Nach tiefem Scrollen beginnt ein direkt gewähltes anderes Piece am oberen Rand | erfüllt am 24.10.2026 |
|
||||||
|
| Zustände sind ausgeschrieben | Ablauf prüft Kommend, Aktuell, Erledigt und Übersprungen | erfüllt am 24.10.2026 |
|
||||||
|
| Erstes und letztes Piece direkt wählbar | Browser wechselt per stabiler Usage-ID auf Desktop und aus dem mobilen Sheet in beide Richtungen | erfüllt am 24.10.2026 |
|
||||||
|
| Navigation veröffentlicht nicht automatisch | Öffentliche Ausgabe bleibt bei direkter Auswahl unverändert | erfüllt am 24.10.2026 |
|
||||||
|
| Timer und Livezustand überleben Reload | Aktuelles Piece und laufender Abschnittscountdown bleiben nach Neuladen erhalten | erfüllt am 24.10.2026 |
|
||||||
|
| Sendungsende ist eindeutig | Letztes Piece zeigt Endaktion ohne horizontalen Überlauf; danach fehlen Weiter-Aktionen | erfüllt am 24.10.2026 |
|
||||||
|
|
||||||
## Ausgeführte Prüfungen 0.5.11
|
## Ausgeführte Prüfungen 0.5.11
|
||||||
|
|
||||||
|
|||||||
@@ -283,6 +283,17 @@ Der Live-Modus ist eine eigene, fokussierte Arbeitsansicht. Die
|
|||||||
Präsentationsausgabe verwendet eine separate, strikt reduzierte Datenansicht
|
Präsentationsausgabe verwendet eine separate, strikt reduzierte Datenansicht
|
||||||
ohne interne Anwendungsnavigation.
|
ohne interne Anwendungsnavigation.
|
||||||
|
|
||||||
|
Die private Route `/live` verwendet ebenfalls einen eigenen authentifizierten
|
||||||
|
Layoutmodus ohne allgemeine Seitenleiste, mobilen App-Header oder
|
||||||
|
Bottomnavigation. Ihre Regiefläche belegt die verfügbare Viewporthöhe als Raster
|
||||||
|
aus fester Statuszeile, unabhängig scrollender Piece- und Ablaufspalte sowie
|
||||||
|
fester Aktionszeile. Unterhalb des schmalen Breakpoints wird ausschließlich die
|
||||||
|
Ablaufspalte als modales Sheet mit Fokusbegrenzung eingeblendet; beim Wechsel
|
||||||
|
auf einen breiteren Viewport wird dieser Modalzustand aufgehoben. Dieser
|
||||||
|
Layoutmodus ändert weder die Live-Actions noch die getrennte öffentliche
|
||||||
|
`/heinzlive`-Projektion. Lokale Lesepositionen sind an die jeweilige Verwendung
|
||||||
|
gebunden und werden nicht zwischen Pieces übertragen.
|
||||||
|
|
||||||
Icons werden aus dem lokalen Font-Awesome-Classic-Regular-Sprite unter
|
Icons werden aus dem lokalen Font-Awesome-Classic-Regular-Sprite unter
|
||||||
`static/icons` gerendert. Die Verwaltung lädt zusätzlich die durchsuchbaren
|
`static/icons` gerendert. Die Verwaltung lädt zusätzlich die durchsuchbaren
|
||||||
Metadaten aller 3.671 Icons, zeigt je Suchergebnis aber höchstens 120 Einträge
|
Metadaten aller 3.671 Icons, zeigt je Suchergebnis aber höchstens 120 Einträge
|
||||||
|
|||||||
@@ -360,3 +360,17 @@ Validierung, während ein statischer SVG-Sprite über den gemeinsamen
|
|||||||
Pfaddaten in ihren JavaScript-Chunk. Es gibt weder einen externen Laufzeitabruf
|
Pfaddaten in ihren JavaScript-Chunk. Es gibt weder einen externen Laufzeitabruf
|
||||||
noch eine Laufzeitabhängigkeit vom benachbarten Quellverzeichnis; Herkunft und
|
noch eine Laufzeitabhängigkeit vom benachbarten Quellverzeichnis; Herkunft und
|
||||||
Lizenz stehen in `THIRD_PARTY_NOTICES.md`.
|
Lizenz stehen in `THIRD_PARTY_NOTICES.md`.
|
||||||
|
|
||||||
|
## D-024: Private Live-Regie als fokussierter Viewport
|
||||||
|
|
||||||
|
Die authentifizierte Live-Steuerung nutzt einen eigenen Root-Layoutmodus und
|
||||||
|
rendert die allgemeine App-Navigation nicht. Kopf- und Aktionszeile bleiben fest;
|
||||||
|
Moderation und Ablauf sind die einzigen unabhängig scrollenden Arbeitsflächen.
|
||||||
|
Die Umschaltung zum mobilen Sheet hängt ausschließlich von der Fensterbreite ab,
|
||||||
|
damit ein breites, niedriges Desktopfenster weiterhin zwei Spalten verwendet.
|
||||||
|
|
||||||
|
Diese Darstellung ist keine neue Live-Zustandsgrenze. Start, direkte Auswahl,
|
||||||
|
Erledigen, Überspringen, Reserveeinfügung und Präsentation bleiben unveränderte
|
||||||
|
serverseitige Actions mit ihren bisherigen Revisions- und Idempotenzregeln. Ein
|
||||||
|
Piece-Wechsel veröffentlicht daher weiterhin nichts; nur die ausdrückliche
|
||||||
|
Präsentationsaction verändert die getrennte öffentliche Ausgabe.
|
||||||
+61
-6
@@ -1,17 +1,72 @@
|
|||||||
# Release Notes
|
# Release Notes
|
||||||
|
|
||||||
## 0.5.11 - Verwaltungskorrektur
|
## 0.5.11 - Kompakte Live-Regie
|
||||||
|
|
||||||
Veröffentlicht am 23. Oktober 2026
|
Veröffentlicht am 23. Oktober 2026
|
||||||
|
|
||||||
|
Sendewerk 0.5.11 macht aus der privaten Live-Steuerung eine kompakte Regieansicht
|
||||||
|
für Vollbild, Smartphone und kleine Desktopfenster neben OBS oder Firebot. Das
|
||||||
|
aktuelle Piece, die Position im Ablauf, der Abschnittscountdown und die
|
||||||
|
wichtigsten Aktionen bleiben gleichzeitig erreichbar. Die öffentliche
|
||||||
|
heinzlive-Ausgabe und die gespeicherten Livezustände bleiben technisch getrennt.
|
||||||
|
|
||||||
|
### Highlights
|
||||||
|
|
||||||
|
- Die Live-Regie verwendet die gesamte Fensterfläche ohne normale
|
||||||
|
App-Seitenleiste, mobilen App-Header oder Bottomnavigation.
|
||||||
|
- Eine kompakte Kopfzeile zeigt aktuellen Abschnitt, Abschnittscountdown und den
|
||||||
|
tatsächlich präsentierten Ausgabezustand.
|
||||||
|
- Moderation und Ablauf bleiben auch bei einem nur 900 × 450 Pixel großen
|
||||||
|
Desktopfenster nebeneinander. Lange Inhalte scrollen innerhalb ihrer
|
||||||
|
Arbeitsfläche; Kopf- und Aktionszeile bleiben sichtbar.
|
||||||
|
- Die feste Fußleiste bündelt nächstes Piece, „Präsentieren“, „Überspringen“ und
|
||||||
|
„Erledigen & weiter“. Beim letzten offenen Piece wird das Sendungsende
|
||||||
|
eindeutig benannt.
|
||||||
|
- Auf dem Smartphone öffnet „Ablauf · Piece X/Y“ den Ablauf als separates Sheet,
|
||||||
|
während das aktuelle Piece die Hauptfläche bleibt. Fokusführung, Escape und
|
||||||
|
der Wechsel zurück auf eine breite Ansicht schließen den Modalzustand sauber.
|
||||||
|
|
||||||
|
### Moderation und Materialien
|
||||||
|
|
||||||
|
- Interner Titel, Einleitung, Moderationsstichpunkte und Überleitung erscheinen
|
||||||
|
in klarer Lesereihenfolge. Leere Bereiche werden weggelassen.
|
||||||
|
- Recherche, Quellen, Werkzeuge und eigene Notizen sind kompakt beschriftet und
|
||||||
|
werden nur bei Bedarf geöffnet.
|
||||||
|
- Die Prompter-Schriftgröße bleibt einstellbar und wird weiterhin lokal
|
||||||
|
gespeichert. Texte werden nicht automatisch verkleinert.
|
||||||
|
|
||||||
|
### Ablauf und Liveverhalten
|
||||||
|
|
||||||
|
- Abschnitte sind kompakt aufklappbar; der aktuelle Abschnitt ist geöffnet und
|
||||||
|
das aktive Piece bleibt in der Ablaufspalte sichtbar.
|
||||||
|
- Pieces kennzeichnen „Aktuell“, „Erledigt“, „Übersprungen“ und „Kommend“ auch
|
||||||
|
textlich und lassen sich direkt auswählen, einschließlich des ersten Eintrags.
|
||||||
|
- Die Reserve bleibt zusammengeklappt erreichbar. Dauerhafte Sortierpfeile und
|
||||||
|
die separate große „Als Nächstes“-Karte entfallen; Umsortieren bleibt über
|
||||||
|
eine kompakte Aktion am jeweiligen Eintrag erreichbar.
|
||||||
|
- Die lokal gespeicherte Leseposition gehört zum jeweiligen Piece. Eine direkte
|
||||||
|
Auswahl beginnt daher oben, ohne die Position des vorherigen Textes zu erben.
|
||||||
|
- Direkte Auswahl verändert keinen Erledigungsstatus und veröffentlicht nichts.
|
||||||
|
Nur „Präsentieren“ oder „Präsentation leeren“ verändert die öffentliche
|
||||||
|
Ausgabe.
|
||||||
|
- Abschnittstimer, tatsächliche Laufzeiten, Livefortschritt und Wiederaufnahme
|
||||||
|
verwenden unverändert die vorhandenen persistenten Zustände.
|
||||||
|
|
||||||
|
### Einstellungen und Verwaltung
|
||||||
|
|
||||||
|
- Ausgabekanal verbinden, Schlüssel erneuern, Kanal trennen, Theme wechseln,
|
||||||
|
Zeiten ein-/ausblenden und „Zur Planung“ bleiben im Regiemenü erreichbar.
|
||||||
- Der Dialog „Werkzeug hinzufügen“ bleibt beim Aufruf der Verwaltungsseite
|
- Der Dialog „Werkzeug hinzufügen“ bleibt beim Aufruf der Verwaltungsseite
|
||||||
wieder zuverlässig geschlossen.
|
zuverlässig geschlossen und erscheint erst nach einer bewussten Aktion.
|
||||||
- Der Dialog erscheint erst, wenn ein neues Werkzeug angelegt oder ein
|
|
||||||
bestehendes Werkzeug bearbeitet wird.
|
|
||||||
- Die Iconauswahl unter „Inhaltliche Zuordnung“ bleibt auf Desktop und
|
- Die Iconauswahl unter „Inhaltliche Zuordnung“ bleibt auf Desktop und
|
||||||
Smartphone vollständig im sichtbaren Browserbereich.
|
Smartphone vollständig im sichtbaren Browserbereich.
|
||||||
- Die Korrektur gilt gleichermaßen für Desktop- und Smartphone-Ansichten und
|
|
||||||
verändert keine vorhandenen Werkzeuge oder Zuordnungen.
|
### Update und Kompatibilität
|
||||||
|
|
||||||
|
- Dieses Release benötigt keine neue Datenbankmigration und keine Änderung der
|
||||||
|
Konfiguration.
|
||||||
|
- Bestehende Sendungen, Livezustände, Timer, Ausgabekanäle und
|
||||||
|
Präsentationssnapshots bleiben erhalten.
|
||||||
|
|
||||||
## 0.5.10 - Editor-Erlebnis
|
## 0.5.10 - Editor-Erlebnis
|
||||||
|
|
||||||
|
|||||||
+24
-7
@@ -7,14 +7,31 @@ Anmeldung, Rollen, persistente Inhaltsbearbeitung, Sendungsplanung,
|
|||||||
Live-Steuerung, Präsentationsausgabe und lokaler Betriebsweg sind dokumentiert.
|
Live-Steuerung, Präsentationsausgabe und lokaler Betriebsweg sind dokumentiert.
|
||||||
Etappe 6 wurde nicht begonnen.
|
Etappe 6 wurde nicht begonnen.
|
||||||
|
|
||||||
## Verwaltungskorrektur 0.5.11
|
## Kompakte Live-Regie 0.5.11
|
||||||
|
|
||||||
- der Werkzeugdialog bleibt nach dem Laden der Verwaltungsseite geschlossen und
|
- fokussierte private Liveansicht ohne normale App-Seitenleiste oder mobile
|
||||||
wird erst durch „Werkzeug hinzufügen“ oder die Bearbeitung eines vorhandenen
|
Bottomnavigation; Planung, Theme, Schriftgröße, Zeitdarstellung und
|
||||||
Werkzeugs als modaler Dialog eingeblendet
|
Ausgabekanal bleiben über das kompakte Regiemenü erreichbar
|
||||||
- die Iconauswahl der Themenverwaltung bleibt auch in der rechten Desktop-Spalte
|
- feste Kopfzeile mit aktuellem Abschnitt, persistentem Abschnittscountdown und
|
||||||
vollständig innerhalb des sichtbaren Browserbereichs
|
öffentlichem Ausgabezustand sowie feste Fußleiste mit nächstem Piece,
|
||||||
- ein Browser-Regressionscheck sichert den geschlossenen Ausgangszustand ab
|
Präsentation, Überspringen und kontextgerechtem Erledigen
|
||||||
|
- aktuelle Moderation und schmale Ablaufspalte bleiben auch bei 900 × 450 Pixeln
|
||||||
|
nebeneinander; beide Arbeitsflächen scrollen unabhängig, ohne die ganze Seite
|
||||||
|
oder einzelne Textblöcke zu verschachtelten Scrollcontainern zu machen
|
||||||
|
- auf schmalen Ansichten bleibt das Piece die Hauptfläche und der Ablauf öffnet
|
||||||
|
als separates, safe-area-fähiges Sheet mit Fokusführung und Escape-Schließen
|
||||||
|
- kompakter aufklappbarer Ablauf mit ausgeschriebenen Zuständen, direkter
|
||||||
|
Auswahl jedes Pieces, automatisch sichtbarem aktuellen Eintrag und
|
||||||
|
zusammengeklappter Reserve; Sortierung bleibt in einer kompakten Aktion
|
||||||
|
erreichbar, dauerhafte Sortierpfeile entfallen
|
||||||
|
- die Leseposition wird je Piece lokal erhalten; ein erstmals gewähltes Piece
|
||||||
|
beginnt oben statt an der Position des vorherigen Textes
|
||||||
|
- Recherche, Quellen, Werkzeuge und eigene Notizen erscheinen nur bei Bedarf;
|
||||||
|
leere Moderationsbereiche werden nicht dargestellt
|
||||||
|
- Livefortschritt, direkte Auswahl, Abschnittstimer und öffentliche Präsentation
|
||||||
|
verwenden unverändert ihre getrennten persistenten Zustände
|
||||||
|
- der Werkzeugdialog bleibt nach dem Laden der Verwaltungsseite geschlossen;
|
||||||
|
die Themen-Iconauswahl bleibt vollständig innerhalb des Viewports
|
||||||
|
|
||||||
Etappe 6 bleibt unverändert unbegonnen.
|
Etappe 6 bleibt unverändert unbegonnen.
|
||||||
|
|
||||||
|
|||||||
+779
-402
File diff suppressed because it is too large.
Load diff
@@ -2,5 +2,6 @@ import type { LayoutServerLoad } from './$types';
|
|||||||
|
|
||||||
export const load: LayoutServerLoad = ({ locals, url }) => ({
|
export const load: LayoutServerLoad = ({ locals, url }) => ({
|
||||||
user: url.pathname.startsWith('/heinzlive/') ? null : locals.user,
|
user: url.pathname.startsWith('/heinzlive/') ? null : locals.user,
|
||||||
presentationMode: url.pathname.startsWith('/heinzlive/')
|
presentationMode: url.pathname.startsWith('/heinzlive/'),
|
||||||
|
liveControllerMode: url.pathname === '/live'
|
||||||
});
|
});
|
||||||
@@ -34,6 +34,8 @@
|
|||||||
|
|
||||||
{#if data.presentationMode}
|
{#if data.presentationMode}
|
||||||
{@render children()}
|
{@render children()}
|
||||||
|
{:else if data.user && data.liveControllerMode}
|
||||||
|
<main id="main-content" class="live-controller-shell">{@render children()}</main>
|
||||||
{:else if data.user}
|
{:else if data.user}
|
||||||
<div class="app-shell">
|
<div class="app-shell">
|
||||||
<aside class="sidebar">
|
<aside class="sidebar">
|
||||||
|
|||||||
+632
-351
File diff suppressed because it is too large.
Load diff
+193
-13
@@ -1003,6 +1003,7 @@ test('validated YouTube and TikTok players switch without exposing private field
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
await page.goto('/live?show=e2e-presentation');
|
await page.goto('/live?show=e2e-presentation');
|
||||||
|
await page.getByText('Menü', { exact: true }).click();
|
||||||
const popupPromise = page.waitForEvent('popup');
|
const popupPromise = page.waitForEvent('popup');
|
||||||
await page.getByRole('button', { name: 'Ausgabe verbinden und öffnen' }).click();
|
await page.getByRole('button', { name: 'Ausgabe verbinden und öffnen' }).click();
|
||||||
const output = await popupPromise;
|
const output = await popupPromise;
|
||||||
@@ -1076,10 +1077,14 @@ test('live control persists state and exposes only explicit presentation data',
|
|||||||
await plannedConsole.getByRole('button', { name: 'Aus Fundus aktualisieren' }).click();
|
await plannedConsole.getByRole('button', { name: 'Aus Fundus aktualisieren' }).click();
|
||||||
|
|
||||||
await page.goto('/live?show=show-october');
|
await page.goto('/live?show=show-october');
|
||||||
await expect(page.getByText('Noch nicht gestartet.')).toBeVisible();
|
await expect(page.getByRole('heading', { name: 'Bereit für die Sendung' })).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: 'Sendung starten' }).click();
|
||||||
await expect(page.locator('#current-title')).toHaveText('Die kleine Konsole mit dem großen Dreh');
|
await expect(page.locator('#current-title')).toHaveText('Die kleine Konsole mit dem großen Dreh');
|
||||||
await expect(page.locator('.section-countdown')).toContainText(/Noch 0[45]:\d{2}/);
|
await expect(page.locator('.section-countdown')).toContainText(/0[45]:\d{2}/);
|
||||||
|
await page.getByText('Quellen · 1', { exact: true }).click();
|
||||||
await expect(page.locator('.live-materials')).toContainText('Herstellerseite');
|
await expect(page.locator('.live-materials')).toContainText('Herstellerseite');
|
||||||
await expect(
|
await expect(
|
||||||
page.getByText('Herstellerangaben gegen unabhängige Tests prüfen')
|
page.getByText('Herstellerangaben gegen unabhängige Tests prüfen')
|
||||||
@@ -1088,15 +1093,17 @@ test('live control persists state and exposes only explicit presentation data',
|
|||||||
const before = await page
|
const before = await page
|
||||||
.locator('.live-current')
|
.locator('.live-current')
|
||||||
.evaluate((element) => getComputedStyle(element).getPropertyValue('--prompt-size'));
|
.evaluate((element) => getComputedStyle(element).getPropertyValue('--prompt-size'));
|
||||||
|
await page.getByText('Menü', { exact: true }).click();
|
||||||
await page.getByRole('button', { name: 'Schrift vergrößern' }).click();
|
await page.getByRole('button', { name: 'Schrift vergrößern' }).click();
|
||||||
await page.reload();
|
await page.reload();
|
||||||
const restored = await page
|
const restored = await page
|
||||||
.locator('.live-current')
|
.locator('.live-current')
|
||||||
.evaluate((element) => getComputedStyle(element).getPropertyValue('--prompt-size'));
|
.evaluate((element) => getComputedStyle(element).getPropertyValue('--prompt-size'));
|
||||||
expect(restored).not.toBe(before);
|
expect(restored).not.toBe(before);
|
||||||
await page.getByText('Manuelle Recherche öffnen').click();
|
await page.locator('.research-details > summary').click();
|
||||||
await expect(page.locator('.research-details')).toHaveAttribute('open', '');
|
await expect(page.locator('.research-details')).toHaveAttribute('open', '');
|
||||||
|
|
||||||
|
await page.getByText('Menü', { exact: true }).click();
|
||||||
const [popup] = await Promise.all([
|
const [popup] = await Promise.all([
|
||||||
page.waitForEvent('popup'),
|
page.waitForEvent('popup'),
|
||||||
page.getByRole('button', { name: 'Ausgabe verbinden und öffnen' }).click()
|
page.getByRole('button', { name: 'Ausgabe verbinden und öffnen' }).click()
|
||||||
@@ -1140,28 +1147,201 @@ test('live control persists state and exposes only explicit presentation data',
|
|||||||
await expect(output.locator('.heinzlive-output')).toHaveClass(/empty/);
|
await expect(output.locator('.heinzlive-output')).toHaveClass(/empty/);
|
||||||
await page.getByRole('button', { name: 'Präsentieren', exact: true }).click();
|
await page.getByRole('button', { name: 'Präsentieren', exact: true }).click();
|
||||||
|
|
||||||
|
await page.getByText('Reserve', { exact: true }).click();
|
||||||
await page.getByRole('button', { name: 'Als Nächstes' }).click();
|
await page.getByRole('button', { name: 'Als Nächstes' }).click();
|
||||||
await expect(page.locator('.next-card')).toContainText('Pixelart');
|
await expect(page.locator('.live-next-summary')).toContainText('Pixelart');
|
||||||
await page.getByRole('button', { name: 'Erledigen und weiter' }).click();
|
await page.getByRole('button', { name: 'Erledigen & weiter' }).click();
|
||||||
await expect(page.locator('.live-current')).toContainText('Pixelart');
|
await expect(page.locator('.live-current')).toContainText('Pixelart');
|
||||||
await page.getByRole('button', { name: 'Überspringen' }).click();
|
await page.getByRole('button', { name: 'Überspringen' }).click();
|
||||||
await page.getByRole('button', { name: 'Zurück' }).click();
|
await expect(page.locator('#current-title')).toHaveText(
|
||||||
await expect(page.locator('.live-current')).toContainText('Pixelart');
|
'Warum praktische Effekte wieder auffallen'
|
||||||
|
);
|
||||||
|
await expect(
|
||||||
|
page.locator('.live-run-item').filter({ hasText: 'Die kleine Konsole mit dem großen Dreh' })
|
||||||
|
).toContainText('Erledigt');
|
||||||
|
await expect(
|
||||||
|
page.locator('.live-run-item').filter({ hasText: 'Pixelart, die nicht nostalgisch sein will' })
|
||||||
|
).toContainText('Übersprungen');
|
||||||
|
await expect(
|
||||||
|
page.locator('.live-run-item').filter({ hasText: 'Warum praktische Effekte wieder auffallen' })
|
||||||
|
).toContainText('Aktuell');
|
||||||
|
await page.locator('.live-current-scroll').evaluate((element) => {
|
||||||
|
for (let index = 0; index < 12; index += 1) {
|
||||||
|
const paragraph = document.createElement('p');
|
||||||
|
paragraph.textContent = 'Temporärer Langtext für die Lesepositionsprüfung.';
|
||||||
|
element.append(paragraph);
|
||||||
|
}
|
||||||
|
element.scrollTop = element.scrollHeight;
|
||||||
|
element.dispatchEvent(new Event('scroll'));
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
await page.locator('.live-current-scroll').evaluate((element) => element.scrollTop)
|
||||||
|
).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
await page
|
||||||
|
.getByRole('button', {
|
||||||
|
name: 'Die kleine Konsole mit dem großen Dreh als aktuelles Piece auswählen'
|
||||||
|
})
|
||||||
|
.click();
|
||||||
|
await expect(page.locator('#current-title')).toHaveText('Die kleine Konsole mit dem großen Dreh');
|
||||||
|
await expect
|
||||||
|
.poll(() => page.locator('.live-current-scroll').evaluate((element) => element.scrollTop))
|
||||||
|
.toBe(0);
|
||||||
|
await expect(
|
||||||
|
page.locator('.live-run-item').filter({ hasText: 'Warum praktische Effekte wieder auffallen' })
|
||||||
|
).toContainText('Kommend');
|
||||||
|
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'
|
||||||
|
})
|
||||||
|
.click();
|
||||||
|
await expect(page.locator('#current-title')).toHaveText(
|
||||||
|
'Warum praktische Effekte wieder auffallen'
|
||||||
|
);
|
||||||
|
await expect(output.getByText('Wenn das Licht wirklich auf ein Modell fällt.')).toHaveCount(0);
|
||||||
|
|
||||||
|
await page.setViewportSize({ width: 1440, height: 900 });
|
||||||
|
await setTheme(page, 'dark');
|
||||||
|
await page.reload();
|
||||||
|
await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark');
|
||||||
|
await expectNoHorizontalOverflow(page);
|
||||||
|
await page.screenshot({ path: testInfo.outputPath('live-controller-desktop-dark.png') });
|
||||||
|
|
||||||
|
await page.setViewportSize({ width: 900, height: 450 });
|
||||||
|
await setTheme(page, 'light');
|
||||||
|
await page.reload();
|
||||||
|
await expect(page.locator('html')).toHaveAttribute('data-theme', 'light');
|
||||||
|
await page.evaluate(() => {
|
||||||
|
const title = document.querySelector('#current-title');
|
||||||
|
if (title)
|
||||||
|
title.textContent =
|
||||||
|
'Warum praktische Effekte in einer außergewöhnlich langen Live-Moderation wieder auffallen';
|
||||||
|
const copy = document.querySelector('.prompter-copy');
|
||||||
|
if (copy) {
|
||||||
|
for (let index = 0; index < 8; index += 1) {
|
||||||
|
const paragraph = document.createElement('p');
|
||||||
|
paragraph.textContent =
|
||||||
|
'Zusätzlicher langer Moderationstext prüft die unabhängige Scrollfläche ohne die feste Bedienleiste zu verdrängen.';
|
||||||
|
copy.append(paragraph);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const rail = document.querySelector('.live-run-scroll');
|
||||||
|
const section = rail?.querySelector('.rail-section');
|
||||||
|
if (rail && section) {
|
||||||
|
for (let index = 0; index < 8; index += 1) rail.append(section.cloneNode(true));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const compactLayout = await page.evaluate(() => {
|
||||||
|
const current = document.querySelector('.live-current')?.getBoundingClientRect();
|
||||||
|
const rail = document.querySelector('.live-rail')?.getBoundingClientRect();
|
||||||
|
const footer = document.querySelector('.live-action-bar')?.getBoundingClientRect();
|
||||||
|
const pieceScroll = document.querySelector('.live-current-scroll');
|
||||||
|
const railScroll = document.querySelector('.live-run-scroll');
|
||||||
|
return {
|
||||||
|
pageFits: document.documentElement.scrollHeight <= innerHeight,
|
||||||
|
twoColumns: Boolean(current && rail && rail.x > current.x + current.width - 1),
|
||||||
|
footerVisible: Boolean(footer && footer.bottom <= innerHeight && footer.top >= 0),
|
||||||
|
pieceScrolls: Boolean(pieceScroll && pieceScroll.scrollHeight > pieceScroll.clientHeight),
|
||||||
|
railScrolls: Boolean(railScroll && railScroll.scrollHeight > railScroll.clientHeight)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
expect(compactLayout).toEqual({
|
||||||
|
pageFits: true,
|
||||||
|
twoColumns: true,
|
||||||
|
footerVisible: true,
|
||||||
|
pieceScrolls: true,
|
||||||
|
railScrolls: true
|
||||||
|
});
|
||||||
|
await page.screenshot({ path: testInfo.outputPath('live-controller-compact-desktop.png') });
|
||||||
|
|
||||||
|
const countdownBeforeReload = await page.locator('.section-countdown strong').textContent();
|
||||||
|
await page.reload();
|
||||||
|
await expect(page.locator('#current-title')).toHaveText(
|
||||||
|
'Warum praktische Effekte wieder auffallen'
|
||||||
|
);
|
||||||
|
const countdownAfterReload = await page.locator('.section-countdown strong').textContent();
|
||||||
|
expect(countdownAfterReload).not.toBe('05:00');
|
||||||
|
expect(countdownAfterReload).toMatch(/0[34]:\d{2}/);
|
||||||
|
expect(countdownBeforeReload).toMatch(/0[34]:\d{2}/);
|
||||||
|
|
||||||
|
await page.setViewportSize({ width: 390, height: 700 });
|
||||||
|
const liveRail = page.locator('.live-rail');
|
||||||
|
const railClose = liveRail.getByRole('button', { name: 'Ablauf schließen' });
|
||||||
|
await expect(liveRail).toBeHidden();
|
||||||
|
await expect(page.locator('.live-action-bar')).toBeVisible();
|
||||||
|
await expectNoHorizontalOverflow(page);
|
||||||
|
expect(
|
||||||
|
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 expect(liveRail).toHaveClass(/open/);
|
||||||
|
await expect(railClose).toBeFocused();
|
||||||
|
await expect
|
||||||
|
.poll(async () => {
|
||||||
|
const box = await liveRail.boundingBox();
|
||||||
|
return box ? Math.ceil(box.x + box.width) : Number.POSITIVE_INFINITY;
|
||||||
|
})
|
||||||
|
.toBeLessThanOrEqual(390);
|
||||||
|
const mobileRailBox = await liveRail.boundingBox();
|
||||||
|
expect(mobileRailBox?.width ?? 0).toBeGreaterThan(340);
|
||||||
|
await expect(page.locator('.live-run-item.current')).toBeInViewport();
|
||||||
|
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 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 liveRail
|
||||||
|
.getByRole('button', {
|
||||||
|
name: 'Die kleine Konsole mit dem großen Dreh als aktuelles Piece 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 liveRail
|
||||||
|
.getByRole('button', {
|
||||||
|
name: 'Warum praktische Effekte wieder auffallen als aktuelles Piece auswählen'
|
||||||
|
})
|
||||||
|
.click();
|
||||||
|
await expect(liveRail).toBeHidden();
|
||||||
|
await expect(page.locator('#current-title')).toHaveText(
|
||||||
|
'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 output.route('**/state', (route) => route.abort());
|
await output.route('**/state', (route) => route.abort());
|
||||||
await expect(output.getByText('Verbindung unterbrochen')).toBeVisible({ timeout: 5_000 });
|
await expect(output.getByText('Verbindung unterbrochen')).toBeVisible({ timeout: 5_000 });
|
||||||
await output.unroute('**/state');
|
await output.unroute('**/state');
|
||||||
await expect(output.getByText('Verbunden')).toBeVisible({ timeout: 5_000 });
|
await expect(output.getByText('Verbunden')).toBeVisible({ timeout: 5_000 });
|
||||||
await page.setViewportSize({ width: 390, height: 844 });
|
await page.getByRole('button', { name: 'Präsentation leeren' }).click();
|
||||||
await expectNoHorizontalOverflow(page);
|
await expect(output.locator('.output-content')).toHaveCount(0);
|
||||||
await page.screenshot({
|
await expect(output.locator('.heinzlive-output')).toHaveClass(/empty/);
|
||||||
path: testInfo.outputPath('live-controller-mobile.png'),
|
|
||||||
fullPage: true
|
|
||||||
});
|
|
||||||
await page.setViewportSize({ width: 1280, height: 900 });
|
await page.setViewportSize({ width: 1280, height: 900 });
|
||||||
|
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
window.open = () => null;
|
window.open = () => null;
|
||||||
});
|
});
|
||||||
|
await page.getByText('Menü', { exact: true }).click();
|
||||||
await page.getByRole('button', { name: 'Ausgabe verbinden und öffnen' }).click();
|
await page.getByRole('button', { name: 'Ausgabe verbinden und öffnen' }).click();
|
||||||
await expect(
|
await expect(
|
||||||
page
|
page
|
||||||
|
|||||||
Reference in new issue
Block a user