Release 0.7.2

This commit is contained in:
hnzio committed 2026-10-26 21:20:55 +01:00
1 parent 64f55a8b1f
commit d5b1c58ba2
23 files changed
+1630 -455

No files matched your search

+216 -3
View File
@@ -247,6 +247,80 @@ function removeCompletionFixture() {
rmSync(path.join(dataDirectory, 'uploads', 'e2e-share.png'), { force: true });
}
function addRssSelectionFixture() {
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 now = '2026-10-26T12:00:00.000Z';
const insertFeed = database.prepare(
`INSERT INTO feed_sources
(id, name, url, is_enabled, refresh_interval_minutes, created_at_utc, updated_at_utc)
VALUES (?, ?, ?, 1, 60, ?, ?)`
);
const insertEntry = database.prepare(
`INSERT INTO feed_entries
(id, feed_id, external_id, title, url, summary, published_at_utc, first_seen_at_utc, updated_at_utc)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
);
database.transaction(() => {
insertFeed.run('e2e-feed-a', 'E2E Quelle A', 'https://example.org/e2e-a.xml', now, now);
insertFeed.run('e2e-feed-b', 'E2E Quelle B', 'https://example.org/e2e-b.xml', now, now);
for (let index = 1; index <= 25; index += 1) {
const published = new Date(Date.UTC(2026, 8, index)).toISOString();
insertEntry.run(
`e2e-entry-a-${index}`,
'e2e-feed-a',
`a-${index}`,
`E2E Quelle A Beitrag ${index}`,
`https://example.org/a/${index}`,
`Auszug für Beitrag ${index} aus Quelle A.`,
published,
now,
now
);
}
for (let index = 1; index <= 20; index += 1) {
const published = new Date(Date.UTC(2026, 7, index)).toISOString();
insertEntry.run(
`e2e-entry-b-${index}`,
'e2e-feed-b',
`b-${index}`,
`E2E Quelle B Beitrag ${index}`,
`https://example.org/b/${index}`,
`Auszug für Beitrag ${index} aus Quelle B.`,
published,
now,
now
);
}
})();
database.close();
}
function removeIntegrationFixtures() {
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.transaction(() => {
const pieceIds = database
.prepare(
"SELECT piece_id AS id FROM feed_entries WHERE id LIKE 'e2e-entry-%' AND piece_id IS NOT NULL"
)
.all() as { id: string }[];
database.prepare("DELETE FROM feed_sources WHERE id LIKE 'e2e-feed-%'").run();
for (const piece of pieceIds) database.prepare('DELETE FROM pieces WHERE id = ?').run(piece.id);
const accessIds = database
.prepare("SELECT id FROM technical_accesses WHERE name LIKE 'E2E API-Zugang%'")
.all() as { id: string }[];
for (const access of accessIds) {
database.prepare('DELETE FROM audit_events WHERE target_id = ?').run(access.id);
database.prepare('DELETE FROM technical_accesses WHERE id = ?').run(access.id);
}
})();
database.close();
}
function addPresentationFixture() {
const dataDirectory = process.env.SENDEWERK_E2E_DATA_DIR;
if (!dataDirectory) throw new Error('E2E-Datenverzeichnis ist nicht konfiguriert.');
@@ -1173,6 +1247,124 @@ test('inbox, archive and administration stay concise', async ({ page }) => {
await expect(topicIconPicker.locator('input[type="hidden"]')).toHaveValue('satellite-dish');
});
test('rss selection and integration catalogs keep deliberate choices', async ({
page
}, testInfo) => {
addRssSelectionFixture();
try {
await page.context().route('https://example.org/**', (route) =>
route.fulfill({
contentType: 'text/html',
body: '<!doctype html><title>RSS-Original</title>'
})
);
await page.setViewportSize({ width: 1100, height: 760 });
await setTheme(page, 'light');
await page.goto('/eingang');
const rssPanel = page.locator('.rss-inbox-panel');
await expect(rssPanel).toContainText('45 Beiträge sind verfügbar');
await rssPanel.getByRole('button', { name: 'RSS-Beiträge auswählen' }).click();
const rssDialog = page.getByRole('dialog', { name: 'RSS-Beiträge auswählen' });
await expect(rssDialog).toBeVisible();
await expect(rssDialog.getByRole('option')).toHaveCount(20);
await rssDialog.getByRole('button', { name: 'E2E Quelle B' }).click();
await expect(rssDialog).toContainText('25 verfügbare Beiträge');
const firstChoice = rssDialog.getByRole('option', { name: /E2E Quelle A Beitrag 25/ });
await firstChoice.press('Enter');
await expect(firstChoice).toHaveAttribute('aria-selected', 'true');
const [original] = await Promise.all([
page.waitForEvent('popup'),
firstChoice.getByRole('link', { name: 'Original öffnen' }).click()
]);
await original.close();
await expect(firstChoice).toHaveAttribute('aria-selected', 'true');
await rssDialog.getByRole('button', { name: 'Weiter' }).click();
const secondChoice = rssDialog.getByRole('option', { name: /E2E Quelle A Beitrag 1/ });
await secondChoice.press(' ');
await expect(secondChoice).toHaveAttribute('aria-selected', 'true');
await rssDialog.getByRole('button', { name: 'E2E Quelle B' }).click();
await expect(
rssDialog.getByRole('button', { name: 'Auswahl in den Eingang übernehmen (2)' })
).toBeEnabled();
await page.setViewportSize({ width: 390, height: 844 });
await page.evaluate(() => (document.documentElement.dataset.theme = 'dark'));
await expectNoHorizontalOverflow(page);
await expect(
rssDialog.getByRole('button', { name: 'Auswahl in den Eingang übernehmen (2)' })
).toBeVisible();
await page.screenshot({ path: testInfo.outputPath('rss-selection-mobile-dark.png') });
await rssDialog.getByRole('button', { name: 'Auswahl in den Eingang übernehmen (2)' }).click();
await expect(page.getByRole('status')).toContainText('2 Beiträge in den Eingang übernommen');
await expect(rssPanel).toContainText('43 Beiträge sind verfügbar');
await expect(page.getByText('E2E Quelle A Beitrag 25', { exact: true })).toBeVisible();
await rssPanel.getByRole('button', { name: 'RSS-Beiträge auswählen' }).click();
await expect(rssDialog.getByText('E2E Quelle A Beitrag 25', { exact: true })).toHaveCount(0);
await expect(rssDialog.getByText('E2E Quelle A Beitrag 1', { exact: true })).toHaveCount(0);
await rssDialog.getByRole('button', { name: 'RSS-Auswahl schließen' }).click();
await page.setViewportSize({ width: 1100, height: 800 });
await setTheme(page, 'light');
await page.goto('/verwaltung');
await page.getByRole('button', { name: 'Feed hinzufügen' }).click();
let feedDialog = page.getByRole('dialog', { name: 'Feed hinzufügen' });
await feedDialog.getByLabel('Name').fill('E2E Feed Verwaltung');
await feedDialog.getByLabel('Feed-URL').fill('https://example.org/e2e-admin.xml');
await feedDialog.getByLabel('Intervall in Minuten').fill('90');
await feedDialog.getByLabel('Standardkategorie').selectOption({ index: 1 });
await feedDialog.getByRole('button', { name: 'Feed anlegen' }).click();
const feedEntry = page.getByRole('button', { name: 'E2E Feed Verwaltung bearbeiten' });
await expect(feedEntry).toBeVisible();
await feedEntry.click();
feedDialog = page.getByRole('dialog', { name: 'E2E Feed Verwaltung' });
await feedDialog.getByLabel('Name').fill('E2E Feed Verwaltung bearbeitet');
await feedDialog.getByLabel('Intervall in Minuten').fill('120');
await feedDialog.getByRole('button', { name: 'Änderungen speichern' }).click();
await expect(
page.getByRole('button', { name: 'E2E Feed Verwaltung bearbeitet bearbeiten' })
).toBeVisible();
await page.setViewportSize({ width: 390, height: 844 });
await page.evaluate(() => (document.documentElement.dataset.theme = 'dark'));
await page.getByRole('button', { name: 'E2E Feed Verwaltung bearbeitet bearbeiten' }).click();
feedDialog = page.getByRole('dialog', { name: 'E2E Feed Verwaltung bearbeitet' });
await expect(feedDialog.getByRole('button', { name: 'Änderungen speichern' })).toBeVisible();
await expectNoHorizontalOverflow(page);
await page.screenshot({ path: testInfo.outputPath('feed-editor-mobile-dark.png') });
await feedDialog.getByRole('button', { name: 'Feeddialog schließen' }).click();
await page.setViewportSize({ width: 1100, height: 800 });
await page.evaluate(() => (document.documentElement.dataset.theme = 'light'));
await page.context().grantPermissions(['clipboard-read', 'clipboard-write']);
await page.getByRole('button', { name: 'Zugang hinzufügen' }).click();
let accessDialog = page.getByRole('dialog', { name: 'API-Zugang hinzufügen' });
await accessDialog.getByLabel('Name').fill('E2E API-Zugang');
await accessDialog.getByLabel('Berechtigung').selectOption('read_write');
await accessDialog.getByRole('button', { name: 'Zugang anlegen' }).click();
await expect(accessDialog.getByText('Token, einmalig sichtbar')).toBeVisible();
const token = await accessDialog.locator('code').textContent();
expect(token?.length ?? 0).toBeGreaterThan(20);
await accessDialog.getByRole('button', { name: 'Token kopieren' }).click();
await expect(accessDialog.getByText('Token kopiert.')).toBeVisible();
expect(await page.evaluate(() => navigator.clipboard.readText())).toBe(token);
await accessDialog.getByRole('button', { name: 'Zugangsdialog schließen' }).click();
await page.getByRole('button', { name: 'E2E API-Zugang bearbeiten' }).click();
accessDialog = page.getByRole('dialog', { name: 'E2E API-Zugang' });
await accessDialog.getByLabel('Name').fill('E2E API-Zugang bearbeitet');
await accessDialog.getByLabel('Berechtigung').selectOption('read');
await accessDialog.getByRole('button', { name: 'Änderungen speichern' }).click();
await expect(
page.getByRole('button', { name: 'E2E API-Zugang bearbeitet bearbeiten' })
).toContainText('Nur lesen');
await page.screenshot({
path: testInfo.outputPath('integration-catalogs-light.png'),
fullPage: true
});
} finally {
removeIntegrationFixtures();
}
});
test('tools stay editable in pieces after catalog deactivation', async ({ page }, testInfo) => {
await page.goto('/verwaltung');
for (const width of [390, 1280]) {
@@ -1266,8 +1458,9 @@ test('validated YouTube and TikTok players switch without exposing private field
);
await page.goto('/live?show=e2e-presentation');
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: 'Ausgabe verbinden und öffnen' }).click();
await page.getByRole('button', { name: 'Mit dieser Sendung verbinden und öffnen' }).click();
const output = await popupPromise;
await expect(output.locator('.output-content')).toHaveCount(0);
await expect(output.locator('.heinzlive-output')).toHaveClass(/empty/);
@@ -1360,11 +1553,23 @@ test('live control persists state and exposes only explicit presentation data',
const liveMenu = page.getByRole('dialog', { name: 'Regiemenü' });
await expect(liveMenu).toBeVisible();
await expect(liveMenu).toContainText('Herbstsendung: Kleine Welten');
await expect(liveMenu).toContainText('Läuft');
await expect(liveMenu.getByRole('button', { name: 'Live-Stand wiederaufnehmen' })).toHaveCount(0);
await expect(liveMenu.getByRole('button', { name: 'Pausieren', exact: true })).toHaveCount(0);
await expect(liveMenu.getByRole('button', { name: 'Fortsetzen', exact: true })).toHaveCount(0);
await expect(liveMenu.getByText(/zurücksetzen/)).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 page.getByRole('button', { name: 'Sendung pausieren' }).click();
await expect(page.getByRole('button', { name: 'Sendung 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 liveMenuTrigger.press('Enter');
await expect(liveMenu).toBeVisible();
await page.locator('.live-current').click({ position: { x: 8, y: 8 } });
@@ -1392,9 +1597,10 @@ test('live control persists state and exposes only explicit presentation data',
await expect(page.locator('.research-details')).toHaveAttribute('open', '');
await page.getByText('Menü', { exact: true }).click();
await page.getByText('Ausgabe einrichten', { exact: true }).click();
const [popup] = await Promise.all([
page.waitForEvent('popup'),
page.getByRole('button', { name: 'Ausgabe verbinden und öffnen' }).click()
page.getByRole('button', { name: 'Mit dieser Sendung verbinden und öffnen' }).click()
]);
await popup.waitForURL(/\/heinzlive\/default#/);
const outputUrl = popup.url();
@@ -1712,12 +1918,19 @@ test('live control persists state and exposes only explicit presentation data',
window.open = () => null;
});
await page.getByText('Menü', { exact: true }).click();
await page.getByRole('button', { name: 'Ausgabe verbinden und öffnen' }).click();
await page.getByText('Ausgabe einrichten', { exact: true }).click();
await page.getByRole('button', { name: 'Ausgabe öffnen' }).click();
await expect(
page
.getByRole('status')
.filter({ hasText: 'Präsentationsfenster konnte nicht geöffnet werden' })
).toBeVisible();
await page.getByText('Menü', { exact: true }).click();
page.once('dialog', (dialog) => dialog.accept());
await page.getByRole('button', { name: 'Sendung 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();
await anonymous.close();
await popup.close();
await expectNoHorizontalOverflow(page);