Release 0.5.1

This commit is contained in:
hnzio committed 2026-10-22 07:56:25 +02:00
1 parent 5a9d7735d4
commit fb24d171ae
36 files changed
+5099 -263

No files matched your search

+466 -12
View File
@@ -1,4 +1,5 @@
import { expect, test, type Page } from '@playwright/test';
import Database from 'better-sqlite3';
import { readFileSync, readdirSync } from 'node:fs';
import path from 'node:path';
@@ -61,6 +62,124 @@ function latestMailLink(subject: string): string {
return match[0];
}
function addSentHistoryFixture(pieceId: string) {
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'));
database.pragma('foreign_keys = ON');
const insertShow = database.prepare(
`INSERT INTO shows (id, title, starts_at_utc, twitch_title, announcement_text, status,
revision, created_at_utc, updated_at_utc) VALUES (?, ?, ?, '', '', 'completed', 1, ?, ?)`
);
const insertSection = database.prepare(
`INSERT INTO show_sections (id, show_id, name, category_id, position,
explicit_duration_seconds, is_reserve_bank) VALUES (?, ?, 'Ablauf', NULL, 0, NULL, 0)`
);
const insertUsage = database.prepare(
`INSERT INTO piece_usages (id, show_section_id, original_piece_id, position, status,
duration_seconds, title_snapshot, intro_snapshot, research_snapshot, moderation_snapshot,
transition_snapshot, presentation_snapshot, own_notes, is_reserve)
VALUES (?, ?, ?, 0, 'done', 300, 'Historische Verwendung', '', '', '', '', '', '', 0)`
);
database.transaction(() => {
for (const [suffix, title, date] of [
['older', 'Rückblick im September', '2026-09-10T18:00:00.000Z'],
['latest', 'Rückblick im Oktober', '2026-10-10T18:00:00.000Z']
] as const) {
const showId = `e2e-history-${suffix}`;
const sectionId = `e2e-history-section-${suffix}`;
insertShow.run(showId, title, date, date, date);
insertSection.run(sectionId, showId);
insertUsage.run(`e2e-history-usage-${suffix}`, sectionId, pieceId);
}
})();
database.close();
}
function removeSentHistoryFixture() {
const dataDirectory = process.env.SENDEWERK_E2E_DATA_DIR;
if (!dataDirectory) return;
const database = new Database(path.join(dataDirectory, 'e2e.sqlite'));
database.pragma('foreign_keys = ON');
database.prepare("DELETE FROM shows WHERE id LIKE 'e2e-history-%'").run();
database.close();
}
function addPresentationFixture() {
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'));
database.pragma('foreign_keys = ON');
const now = '2026-10-22T19:00:00.000Z';
database.transaction(() => {
database
.prepare(
`INSERT INTO shows (id, title, starts_at_utc, twitch_title, announcement_text, status,
revision, created_at_utc, updated_at_utc)
VALUES ('e2e-presentation', 'E2E Präsentation', ?, '', '', 'ready', 1, ?, ?)`
)
.run(now, now, now);
database
.prepare(
`INSERT INTO show_sections (id, show_id, name, category_id, position,
explicit_duration_seconds, is_reserve_bank)
VALUES ('e2e-presentation-section', 'e2e-presentation', 'Player', NULL, 0, NULL, 0),
('e2e-presentation-reserve', 'e2e-presentation', 'Reservebank', NULL, 1, NULL, 1)`
)
.run();
const insert = database.prepare(
`INSERT INTO piece_usages (id, show_section_id, original_piece_id, position, status,
duration_seconds, title_snapshot, intro_snapshot, research_snapshot, moderation_snapshot,
transition_snapshot, presentation_snapshot, own_notes, is_reserve,
presentation_mode_snapshot, presentation_source_label_snapshot,
presentation_source_url_snapshot, presentation_source_timecode_snapshot,
presentation_title_meta_snapshot, presentation_description_snapshot,
presentation_image_url_snapshot, presentation_site_name_snapshot,
presentation_provider_snapshot, presentation_provider_id_snapshot)
VALUES (?, 'e2e-presentation-section', NULL, ?, 'open', 300, ?, '', ?, ?, '', '', '', 0,
'video', ?, ?, ?, ?, '', '', ?, ?, ?)`
);
insert.run(
'e2e-youtube',
0,
'YouTube Kurzlink',
'PRIVATE_RESEARCH_YOUTUBE',
'PRIVATE_MODERATION_YOUTUBE',
'YouTube',
'https://youtu.be/dQw4w9WgXcQ',
75,
'YouTube Vorschau',
'YouTube',
'youtube',
'dQw4w9WgXcQ'
);
insert.run(
'e2e-tiktok',
1,
'TikTok Hochformat',
'PRIVATE_RESEARCH_TIKTOK',
'PRIVATE_MODERATION_TIKTOK',
'TikTok',
'https://www.tiktok.com/@scout2015/video/6718335390845095173',
12,
'TikTok Vorschau',
'TikTok',
'tiktok',
'6718335390845095173'
);
})();
database.close();
}
function removePresentationFixture() {
const dataDirectory = process.env.SENDEWERK_E2E_DATA_DIR;
if (!dataDirectory) return;
const database = new Database(path.join(dataDirectory, 'e2e.sqlite'));
database.pragma('foreign_keys = ON');
database.prepare("DELETE FROM shows WHERE id = 'e2e-presentation'").run();
database.close();
}
test.beforeAll(async ({ browser }) => {
const page = await browser.newPage();
await page.goto('/');
@@ -176,6 +295,8 @@ test('desktop fundus is a compact, filterable list with persistent themes', asyn
await expect(page.locator('.sidebar')).toBeVisible();
await expect(page.locator('.mobile-navigation')).toBeHidden();
await expect(page.locator('.utility-navigation a')).toHaveText('Verwaltung');
await expect(page.locator('.piece-card')).toHaveCount(1);
await page.getByLabel('Auch eingeplante Inhalte zeigen').check();
await expect(page.locator('.piece-card')).toHaveCount(4);
const firstRow = await page.locator('.piece-card').nth(0).boundingBox();
@@ -193,8 +314,8 @@ test('desktop fundus is a compact, filterable list with persistent themes', asyn
await page.getByLabel('Status').selectOption('preparing');
await page.getByRole('button', { name: 'Anwenden' }).click();
await expect(page.getByRole('heading', { name: 'Kein passendes Piece' })).toBeVisible();
await page.getByRole('link', { name: 'Filter zurücksetzen' }).click();
await expect(page.locator('.piece-card')).toHaveCount(4);
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 expect(page.locator('html')).toHaveAttribute('data-theme-preference', 'light');
@@ -205,6 +326,118 @@ test('desktop fundus is a compact, filterable list with persistent themes', asyn
await page.screenshot({ path: testInfo.outputPath('fundus-desktop-light.png'), fullPage: true });
});
test('sent and planned source filters stay independent and removing the last usage offers the piece again', async ({
page
}, testInfo) => {
addSentHistoryFixture('piece-effects');
try {
await page.goto('/fundus');
await expect(
page.getByRole('heading', { name: 'Warum praktische Effekte wieder auffallen' })
).toHaveCount(0);
await page.evaluate(() => (document.body.dataset.filterDocument = 'stable'));
await page.getByLabel('Auch verwendete Inhalte zeigen').check();
await expect(
page.getByRole('heading', { name: 'Warum praktische Effekte wieder auffallen' })
).toHaveCount(0);
await page.getByLabel('Auch eingeplante Inhalte zeigen').check();
const usedPiece = page
.locator('.piece-card')
.filter({ hasText: 'Warum praktische Effekte wieder auffallen' });
await expect(usedPiece).toBeVisible();
await expect(usedPiece).toContainText('Gesendet am 10.10.2026');
await expect(usedPiece).toContainText('+ 1 weitere');
await expect(usedPiece.getByRole('link', { name: 'Rückblick im Oktober' })).toHaveAttribute(
'href',
'/sendungen/e2e-history-latest'
);
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: 'Anwenden' }).click();
await expect(page.getByLabel('Auch verwendete Inhalte zeigen')).toBeChecked();
await expect(usedPiece).toBeVisible();
await page.screenshot({
path: testInfo.outputPath('fundus-verwendungshistorie-desktop.png'),
fullPage: true
});
await page.goto('/sendungen');
await page.getByLabel('Titel').fill('E2E Filterprüfung');
await page.getByLabel('Ausgangspunkt').selectOption('template-default');
await page.getByRole('button', { name: 'Sendung anlegen' }).click();
await page.getByPlaceholder('Fundus durchsuchen').fill('kleine Konsole');
await page.getByRole('button', { name: 'Filtern' }).click();
const consolePiece = page
.locator('.builder-source-card')
.filter({ hasText: 'Die kleine Konsole mit dem großen Dreh' });
await consolePiece.getByRole('button', { name: 'Hinzufügen' }).click();
await expect(consolePiece).toHaveCount(0);
await page.getByPlaceholder('Fundus durchsuchen').fill('');
await page.getByRole('button', { name: 'Filtern' }).click();
await page.getByLabel('Auch eingeplante Inhalte zeigen').check();
await expect(
page
.locator('.builder-source-card')
.filter({ hasText: 'Die kleine Konsole mit dem großen Dreh' })
).toContainText('Eingeplant');
await expect(
page
.locator('.builder-source-card')
.filter({ hasText: 'Warum praktische Effekte wieder auffallen' })
).toHaveCount(0);
await page.getByPlaceholder('Fundus durchsuchen').fill('kleine Konsole');
await page.getByRole('button', { name: 'Filtern' }).click();
await expect(page.getByLabel('Auch eingeplante Inhalte zeigen')).toBeChecked();
await expect(
page
.locator('.builder-source-card')
.filter({ hasText: 'Die kleine Konsole mit dem großen Dreh' })
).toContainText('Eingeplant');
await page.getByPlaceholder('Fundus durchsuchen').fill('');
await page.getByRole('button', { name: 'Filtern' }).click();
await page.getByLabel('Auch eingeplante Inhalte zeigen').uncheck();
await page.getByLabel('Auch verwendete Inhalte zeigen').check();
await expect(
page
.locator('.builder-source-card')
.filter({ hasText: 'Die kleine Konsole mit dem großen Dreh' })
).toHaveCount(0);
await expect(
page
.locator('.builder-source-card')
.filter({ hasText: 'Warum praktische Effekte wieder auffallen' })
).toContainText('Rückblick im Oktober');
await page.screenshot({
path: testInfo.outputPath('sendungsfilter-desktop.png'),
fullPage: true
});
await page
.locator('.reserve-usage')
.getByRole('button', { name: /aus Reserve entfernen/ })
.click();
await page.getByLabel('Auch verwendete Inhalte zeigen').uncheck();
await expect(
page
.locator('.builder-source-card')
.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 expectNoHorizontalOverflow(page);
await page.screenshot({
path: testInfo.outputPath('sendungsfilter-mobile.png'),
fullPage: true
});
} finally {
removeSentHistoryFixture();
}
});
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');
@@ -322,10 +555,12 @@ test('a show can be planned from a template with independent usages and reserve'
await expect(source).toHaveClass(/is-dragging/);
await expect(source).toHaveCSS('opacity', '0.45');
await emptyReactionTarget.dispatchEvent('drop', { dataTransfer });
await sourceDragHandle.dispatchEvent('dragend', { dataTransfer });
await dataTransfer.dispose();
await expect(reactionSection.locator('.editable-usage')).toHaveCount(1);
await expect(page.locator('body')).toHaveAttribute('data-builder-document', 'stable');
await expect(source).toHaveCount(0);
await page.getByLabel('Auch eingeplante Inhalte zeigen').check();
await expect(source).toContainText('Eingeplant');
await sourceDragHandle.dragTo(reactionSection.locator('.editable-usage'));
let usages = page
@@ -333,7 +568,41 @@ test('a show can be planned from a template with independent usages and reserve'
.filter({ has: page.getByRole('heading', { name: 'JC / Reaction' }) })
.locator('.editable-usage');
await expect(usages).toHaveCount(2);
await usages.nth(0).locator('summary').click();
await usages
.nth(0)
.getByRole('button', {
name: 'Die kleine Konsole mit dem großen Dreh bearbeiten und verschieben'
})
.click();
await expect(page.locator('dialog[open]')).toContainText('Sendungsspezifische Kopie');
const desktopEditorItems = await page
.locator('dialog[open] .usage-dialog-editor')
.evaluate((element) =>
Array.from(element.children)
.filter((child) => child.matches('section, button'))
.map((child) => child.getBoundingClientRect().top)
);
expect(Math.abs(desktopEditorItems[0] - desktopEditorItems[1])).toBeLessThan(2);
expect(desktopEditorItems[2]).toBeGreaterThan(desktopEditorItems[1]);
await page.screenshot({
path: testInfo.outputPath('verwendungseditor-desktop.png'),
fullPage: true
});
await page
.locator('dialog[open]')
.getByRole('button', { name: 'Aus Fundus aktualisieren' })
.click();
await expect(page.locator('dialog[open]')).toHaveCount(0);
usages = page
.locator('.run-section')
.filter({ has: page.getByRole('heading', { name: 'JC / Reaction' }) })
.locator('.editable-usage');
await usages
.nth(0)
.getByRole('button', {
name: 'Die kleine Konsole mit dem großen Dreh bearbeiten und verschieben'
})
.click();
await usages.nth(0).getByLabel('Titel').fill('Konsole als Auftakt');
await usages.nth(0).getByLabel('Dauer in Minuten').fill('5');
await usages.nth(0).getByLabel('Eigene Notizen').fill('Kurbel zuerst zeigen');
@@ -344,20 +613,51 @@ test('a show can be planned from a template with independent usages and reserve'
.locator('.run-section')
.filter({ has: page.getByRole('heading', { name: 'JC / Reaction' }) })
.locator('.editable-usage');
await usages.nth(1).locator('summary').click();
await usages
.nth(1)
.getByRole('button', {
name: 'Die kleine Konsole mit dem großen Dreh bearbeiten und verschieben'
})
.click();
await usages.nth(1).getByLabel('Titel').fill('Konsole als Rückblick');
await usages.nth(1).getByLabel('Dauer in Minuten').fill('15');
await usages.nth(1).getByLabel('Eigene Notizen').fill('Andere Perspektive');
await usages.nth(1).getByRole('button', { name: 'Verwendung speichern' }).click();
await expect(page.locator('.show-summary')).toContainText('2 Std. 25 Min.');
usages = page
.locator('.run-section')
.filter({ has: page.getByRole('heading', { name: 'JC / Reaction' }) })
.locator('.editable-usage');
await usages
.nth(1)
.getByRole('button', { name: 'Konsole als Rückblick bearbeiten und verschieben' })
.click();
await usages.nth(1).getByRole('button', { name: 'Duplizieren' }).click();
await expect(reactionSection.locator('.editable-usage')).toHaveCount(3);
await expect(reactionSection.locator('.editable-usage').nth(2)).toContainText(
'Konsole als Rückblick'
);
await expect(page.locator('.show-summary')).toContainText('2 Std. 40 Min.');
const planTimeBeforeReserve = await page.locator('.show-summary').textContent();
await source.getByLabel(/Ziel für/).selectOption('');
await source.getByRole('button', { name: 'Hinzufügen' }).click();
await expect(page.locator('.reserve-usage')).toHaveCount(1);
expect(await page.locator('.show-summary').textContent()).toBe(planTimeBeforeReserve);
await page
.locator('.reserve-usage')
.getByRole('button', {
name: 'Die kleine Konsole mit dem großen Dreh bearbeiten und verschieben'
})
.click();
await expect(page.locator('dialog[open]')).toContainText('Sendungsspezifische Kopie');
await expect(
page.locator('dialog[open]').getByRole('button', { name: 'Duplizieren' })
).toBeVisible();
await page.locator('dialog[open]').getByRole('button', { name: 'Editor schließen' }).click();
const moveUp = page.getByRole('button', { name: 'Konsole als Rückblick nach oben' });
const moveUp = page.getByRole('button', { name: 'Konsole als Rückblick nach oben' }).first();
await moveUp.focus();
await page.keyboard.press('Enter');
await expect(
@@ -384,12 +684,60 @@ test('a show can be planned from a template with independent usages and reserve'
await expect(page.locator('.reserve-usage')).toHaveCount(0);
await expect(page.locator('body')).toHaveAttribute('data-builder-document', 'stable');
await page.reload();
await expect(page.locator('.editable-usage')).toHaveCount(3);
await expect(page.locator('.editable-usage')).toHaveCount(4);
const savedUsage = page.locator('.editable-usage').filter({ hasText: 'Konsole als Auftakt' });
await savedUsage.locator('summary').click();
await savedUsage
.getByRole('button', { name: 'Konsole als Auftakt bearbeiten und verschieben' })
.click();
await expect(savedUsage.getByLabel('Eigene Notizen')).toHaveValue('Kurbel zuerst zeigen');
await page.locator('dialog[open]').getByRole('button', { name: 'Editor schließen' }).click();
const duplicatedUsage = page
.locator('.editable-usage')
.filter({ hasText: 'Konsole als Rückblick' });
await expect(duplicatedUsage).toHaveCount(2);
await duplicatedUsage
.nth(1)
.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).getByLabel('Eigene Notizen')).toHaveValue(
'Andere Perspektive'
);
await page.locator('dialog[open]').getByRole('button', { name: 'Editor schließen' }).click();
await page.setViewportSize({ width: 390, height: 844 });
await savedUsage
.getByRole('button', { name: 'Konsole als Auftakt bearbeiten und verschieben' })
.click();
const mobileEditorItems = await page
.locator('dialog[open] .usage-dialog-editor')
.evaluate((element) =>
Array.from(element.children)
.filter((child) => child.matches('section, button'))
.map((child) => child.getBoundingClientRect().top)
);
expect(mobileEditorItems[1]).toBeGreaterThan(mobileEditorItems[0]);
expect(mobileEditorItems[2]).toBeGreaterThan(mobileEditorItems[1]);
await expectNoHorizontalOverflow(page);
await page.screenshot({
path: testInfo.outputPath('verwendungseditor-mobile.png'),
fullPage: true
});
await page
.locator('dialog[open]')
.getByRole('button', { name: 'Entfernen' })
.scrollIntoViewIfNeeded();
await expect(
page.locator('dialog[open]').getByRole('button', { name: 'Entfernen' })
).toBeVisible();
await page
.locator('dialog[open] form[action="?/updateUsage"] input[name="revision"]')
.evaluate((element) => ((element as HTMLInputElement).value = '1'));
await page.locator('dialog[open]').getByRole('button', { name: 'Verwendung speichern' }).click();
await expect(page.locator('dialog[open]').getByRole('alert')).toContainText(
'inzwischen geändert'
);
await page.locator('dialog[open]').getByRole('button', { name: 'Editor schließen' }).click();
await page.getByRole('button', { name: 'Ablauf', exact: true }).click();
await page.getByPlaceholder('Neuer Abschnitt').fill('Finales Spiel');
await page.getByRole('button', { name: 'Abschnitt hinzufügen' }).click();
@@ -397,7 +745,7 @@ test('a show can be planned from a template with independent usages and reserve'
await expectNoHorizontalOverflow(page);
await page.screenshot({ path: testInfo.outputPath('sendungsbau-mobile.png'), fullPage: true });
await page.goto('/fundus?q=kleine+Konsole');
await page.goto('/fundus?q=kleine+Konsole&includePlanned=1');
await expect(
page.getByRole('heading', { name: 'Die kleine Konsole mit dem großen Dreh' })
).toBeVisible();
@@ -430,7 +778,7 @@ test('all work areas fit at required widths in light and dark mode', async ({ pa
test('long titles and longer fundus lists reflow without overflow', async ({ page }, testInfo) => {
await page.setViewportSize({ width: 1280, height: 900 });
await page.goto('/fundus');
await expect(page.locator('.piece-card')).toHaveCount(4);
await expect(page.locator('.piece-card')).toHaveCount(1);
await page.evaluate(() => {
const list = document.querySelector('.piece-grid');
const rows = Array.from(document.querySelectorAll('.piece-card'));
@@ -444,7 +792,7 @@ test('long titles and longer fundus lists reflow without overflow', async ({ pag
}
});
await expect(page.locator('.piece-card')).toHaveCount(16);
await expect(page.locator('.piece-card')).toHaveCount(4);
await expectNoHorizontalOverflow(page);
await page.setViewportSize({ width: 390, height: 844 });
await expectNoHorizontalOverflow(page);
@@ -486,12 +834,71 @@ test('inbox, archive and administration stay concise', async ({ page }) => {
expect(boxes[1]?.y).toBe(boxes[2]?.y);
});
test('validated YouTube and TikTok players switch without exposing private fields', async ({
page
}, testInfo) => {
addPresentationFixture();
try {
await page.context().route('https://www.youtube-nocookie.com/**', (route) =>
route.fulfill({
contentType: 'text/html',
body: '<!doctype html><title>YouTube Test</title>'
})
);
await page.context().route('https://www.tiktok.com/player/**', (route) =>
route.fulfill({
contentType: 'text/html',
body: '<!doctype html><title>TikTok Test</title>'
})
);
await page.goto('/live?show=e2e-presentation');
const popupPromise = page.waitForEvent('popup');
await page.getByRole('button', { name: 'Präsentationsfenster öffnen' }).click();
const output = await popupPromise;
await output.waitForLoadState('networkidle');
await expect(output.getByText('Bereit für den nächsten Inhalt')).toBeVisible();
await page.getByRole('button', { name: 'YouTube Kurzlink präsentieren' }).click();
const youtube = output.locator('iframe[title="YouTube Vorschau"]');
await expect(youtube).toHaveAttribute(
'src',
'https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ?start=75'
);
await expect(output.locator('.output-connection')).toHaveClass(/sr-only/);
const publicState = await output.evaluate(async () =>
JSON.stringify(await fetch(`${location.pathname}/state`).then((response) => response.json()))
);
expect(publicState).not.toContain('PRIVATE_RESEARCH');
expect(publicState).not.toContain('PRIVATE_MODERATION');
await page.getByRole('button', { name: 'TikTok Hochformat präsentieren' }).click();
await expect(youtube).toHaveCount(0);
await expect(output.locator('iframe[title="TikTok Vorschau"]')).toHaveAttribute(
'src',
'https://www.tiktok.com/player/v1/6718335390845095173?autoplay=0&controls=1'
);
await expect(output.getByRole('link', { name: 'Original öffnen' })).toHaveAttribute(
'href',
'https://www.tiktok.com/@scout2015/video/6718335390845095173'
);
await output.setViewportSize({ width: 390, height: 844 });
await expectNoHorizontalOverflow(output);
await output.screenshot({
path: testInfo.outputPath('heinzlive-video-mobile.png'),
fullPage: true
});
await output.close();
} finally {
removePresentationFixture();
}
});
test('live control persists state and exposes only explicit presentation data', async ({
page,
browser
}, testInfo) => {
await page.setViewportSize({ width: 1280, height: 900 });
await page.goto('/fundus?q=kleine+Konsole');
await page.goto('/fundus?q=kleine+Konsole&includePlanned=1');
await page.getByRole('heading', { name: 'Die kleine Konsole mit dem großen Dreh' }).click();
const png = Buffer.from(
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=',
@@ -701,3 +1108,50 @@ test('pieces autosave safely, detect conflicts and protect uploaded images', asy
fullPage: true
});
});
test('link preview uses one existing source, persists manual fields and isolates its test view', async ({
page
}, testInfo) => {
await page.goto('/fundus/piece-console');
await page.getByLabel('Linkvorschau').check();
await page.getByRole('combobox', { name: 'Quelle', exact: true }).selectOption('0');
await page.getByLabel('Titel', { exact: true }).last().fill('Manuell korrigierte Linkvorschau');
await page.getByLabel('Anreißer').fill('Ein kurzer manueller Anreißer ohne Vorschaubild.');
await expect(page.locator('.save-indicator')).toContainText('Gespeichert', { timeout: 10_000 });
await page.reload();
await expect(page.getByLabel('Linkvorschau')).toBeChecked();
await expect(page.getByLabel('Titel', { exact: true }).last()).toHaveValue(
'Manuell korrigierte Linkvorschau'
);
const previewPromise = page.waitForEvent('popup');
await page.getByRole('link', { name: 'Präsentation testen' }).click();
const preview = await previewPromise;
await preview.waitForLoadState('networkidle');
await expect(preview.getByText('Isolierte Testansicht')).toBeVisible();
await expect(
preview.getByRole('heading', { name: 'Manuell korrigierte Linkvorschau' })
).toBeVisible();
await expect(preview.getByText('Ein kurzer manueller Anreißer ohne Vorschaubild.')).toBeVisible();
await expect(preview.locator('.output-link-image')).toHaveCount(0);
await preview.screenshot({
path: testInfo.outputPath('linkvorschau-desktop.png'),
fullPage: true
});
await preview.setViewportSize({ width: 390, height: 844 });
await expectNoHorizontalOverflow(preview);
await preview.screenshot({
path: testInfo.outputPath('linkvorschau-mobile.png'),
fullPage: true
});
await preview.close();
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 });
const blockedPreview = page.waitForResponse((response) =>
response.url().includes('?/loadPreview')
);
await page.getByRole('button', { name: 'Vorschau laden' }).click();
expect((await blockedPreview).status()).toBe(400);
await expect(page.getByRole('alert')).toContainText('Interne oder private Netzwerkziele');
});