Release 0.6.0
This commit is contained in:
1 parent
fd47910d09
commit
b80dded55d
40 files changed
+4080
-856
No files matched your search
+288
-7
@@ -1,6 +1,6 @@
|
||||
import { expect, test, type Locator, type Page } from '@playwright/test';
|
||||
import Database from 'better-sqlite3';
|
||||
import { readFileSync, readdirSync } from 'node:fs';
|
||||
import { mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const adminEmail = 'barkeeper@example.test';
|
||||
@@ -85,7 +85,8 @@ function addSentHistoryFixture(pieceId: string) {
|
||||
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, ?, ?)`
|
||||
revision, created_at_utc, updated_at_utc, completed_at_utc)
|
||||
VALUES (?, ?, ?, '', '', 'completed', 1, ?, ?, ?)`
|
||||
);
|
||||
const insertSection = database.prepare(
|
||||
`INSERT INTO show_sections (id, show_id, name, category_id, position,
|
||||
@@ -104,9 +105,24 @@ function addSentHistoryFixture(pieceId: string) {
|
||||
] as const) {
|
||||
const showId = `e2e-history-${suffix}`;
|
||||
const sectionId = `e2e-history-section-${suffix}`;
|
||||
insertShow.run(showId, title, date, date, date);
|
||||
insertShow.run(showId, title, date, date, date, date);
|
||||
insertSection.run(sectionId, showId);
|
||||
insertUsage.run(`e2e-history-usage-${suffix}`, sectionId, pieceId);
|
||||
const usageId = `e2e-history-usage-${suffix}`;
|
||||
insertUsage.run(usageId, sectionId, pieceId);
|
||||
database
|
||||
.prepare(
|
||||
`INSERT INTO show_finalizations
|
||||
(show_id, command_id, source_revision, finalized_at_utc) VALUES (?, ?, 1, ?)`
|
||||
)
|
||||
.run(showId, `fixture-${suffix}`, date);
|
||||
database
|
||||
.prepare(
|
||||
`INSERT INTO piece_usage_history
|
||||
(usage_id, show_id, original_piece_id, show_title_snapshot,
|
||||
usage_title_snapshot, used_at_utc, actual_duration_seconds)
|
||||
VALUES (?, ?, ?, ?, 'Historische Verwendung', ?, 300)`
|
||||
)
|
||||
.run(usageId, showId, pieceId, title, date);
|
||||
}
|
||||
})();
|
||||
database.close();
|
||||
@@ -117,10 +133,120 @@ function removeSentHistoryFixture() {
|
||||
if (!dataDirectory) return;
|
||||
const database = new Database(path.join(dataDirectory, 'e2e.sqlite'));
|
||||
database.pragma('foreign_keys = ON');
|
||||
database.prepare("DELETE FROM piece_usage_history WHERE show_id LIKE 'e2e-history-%'").run();
|
||||
database.prepare("DELETE FROM shows WHERE id LIKE 'e2e-history-%'").run();
|
||||
database.close();
|
||||
}
|
||||
|
||||
function addCompletionFixture() {
|
||||
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-25T19:00:00.000Z';
|
||||
const png = Buffer.from(
|
||||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=',
|
||||
'base64'
|
||||
);
|
||||
mkdirSync(path.join(dataDirectory, 'uploads'), { recursive: true });
|
||||
writeFileSync(path.join(dataDirectory, 'uploads', 'e2e-share.png'), png);
|
||||
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-completion', 'E2E Abschluss', ?, '', '', 'live', 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-completion-section', 'e2e-completion', 'Finaler Ablauf', NULL, 0, NULL, 0),
|
||||
('e2e-completion-reserve', 'e2e-completion', 'Reservebank', NULL, 1, NULL, 1)`
|
||||
)
|
||||
.run();
|
||||
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,
|
||||
interaction_snapshot, transition_snapshot, presentation_snapshot, own_notes, is_reserve)
|
||||
VALUES (?, 'e2e-completion-section', ?, ?, ?, 300, ?, ?, ?, ?, ?, ?, '', ?, 0)`
|
||||
);
|
||||
insertUsage.run(
|
||||
'e2e-completion-done',
|
||||
'piece-console',
|
||||
0,
|
||||
'done',
|
||||
'Gesendeter Abschlussbeitrag',
|
||||
'FREIGABE_EINLEITUNG',
|
||||
'PRIVATE_FREIGABE_RECHERCHE',
|
||||
'FREIGABE_MODERATION',
|
||||
'FREIGABE_INTERAKTION',
|
||||
'FREIGABE_UEBERLEITUNG',
|
||||
'PRIVATE_FREIGABE_NOTIZ'
|
||||
);
|
||||
insertUsage.run(
|
||||
'e2e-completion-skipped',
|
||||
'piece-effects',
|
||||
1,
|
||||
'skipped',
|
||||
'Übersprungener Abschlussbeitrag',
|
||||
'',
|
||||
'PRIVATE_SKIPPED_RESEARCH',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
''
|
||||
);
|
||||
database
|
||||
.prepare(
|
||||
`INSERT INTO show_live_states
|
||||
(show_id, revision, started_at_utc, ended_at_utc, presentation_revision, updated_at_utc)
|
||||
VALUES ('e2e-completion', 0, ?, ?, 0, ?)`
|
||||
)
|
||||
.run('2026-10-25T18:00:00.000Z', now, now);
|
||||
database
|
||||
.prepare(
|
||||
`INSERT INTO piece_media
|
||||
(id, piece_id, storage_name, original_name, mime_type, byte_size, alt_text, position, created_at_utc)
|
||||
VALUES ('e2e-share-media', 'piece-console', 'e2e-share.png', 'intern.png',
|
||||
'image/png', ?, 'Freigegebenes Abschlussbild', 77, ?)`
|
||||
)
|
||||
.run(png.byteLength, now);
|
||||
database
|
||||
.prepare(
|
||||
`INSERT INTO usage_media_snapshots
|
||||
(id, usage_id, original_media_id, alt_text, position, is_presentation)
|
||||
VALUES ('e2e-share-snapshot', 'e2e-completion-done', 'e2e-share-media',
|
||||
'Freigegebenes Abschlussbild', 0, 1)`
|
||||
)
|
||||
.run();
|
||||
})();
|
||||
database.close();
|
||||
}
|
||||
|
||||
function removeCompletionFixture() {
|
||||
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(() => {
|
||||
database
|
||||
.prepare(
|
||||
`DELETE FROM show_share_sessions WHERE share_id IN (
|
||||
SELECT id FROM show_shares WHERE show_id = 'e2e-completion'
|
||||
)`
|
||||
)
|
||||
.run();
|
||||
database.prepare("DELETE FROM show_shares WHERE show_id = 'e2e-completion'").run();
|
||||
database.prepare("DELETE FROM piece_usage_history WHERE show_id = 'e2e-completion'").run();
|
||||
database.prepare("DELETE FROM shows WHERE id = 'e2e-completion'").run();
|
||||
database.prepare("DELETE FROM piece_media WHERE id = 'e2e-share-media'").run();
|
||||
})();
|
||||
database.close();
|
||||
rmSync(path.join(dataDirectory, 'uploads', 'e2e-share.png'), { force: true });
|
||||
}
|
||||
|
||||
function addPresentationFixture() {
|
||||
const dataDirectory = process.env.SENDEWERK_E2E_DATA_DIR;
|
||||
if (!dataDirectory) throw new Error('E2E-Datenverzeichnis ist nicht konfiguriert.');
|
||||
@@ -204,10 +330,10 @@ function addCalendarFixture() {
|
||||
`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';
|
||||
const now = '2026-10-25T10: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-late', 'Kalender Spättermin', '2026-10-25T18:00:00.000Z', now, now);
|
||||
insert.run('e2e-calendar-early', 'Kalender Frühtermin', '2026-10-25T09:00:00.000Z', now, now);
|
||||
insert.run(
|
||||
'e2e-calendar-next',
|
||||
'Kalender Novembertermin',
|
||||
@@ -1532,6 +1658,161 @@ test('live control persists state and exposes only explicit presentation data',
|
||||
await expectNoHorizontalOverflow(page);
|
||||
});
|
||||
|
||||
test('completion, archive and protected guest sharing work end to end', async ({
|
||||
page,
|
||||
browser
|
||||
}, testInfo) => {
|
||||
addCompletionFixture();
|
||||
try {
|
||||
await page.goto('/sendungen/e2e-completion');
|
||||
const planningShares = page.locator('.share-management');
|
||||
await expect(planningShares.getByText('Aktualisiert sich bis zum Abschluss')).toBeVisible();
|
||||
await planningShares.getByLabel('Informationsumfang').selectOption('moderation');
|
||||
await planningShares.getByRole('button', { name: 'Leselink anlegen' }).click();
|
||||
const planningSharePath = await planningShares.locator('.share-result a').getAttribute('href');
|
||||
expect(planningSharePath).toMatch(/^\/freigabe\//);
|
||||
await page.screenshot({
|
||||
path: testInfo.outputPath('share-editor-desktop.png'),
|
||||
fullPage: true
|
||||
});
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await expectNoHorizontalOverflow(page);
|
||||
await page.screenshot({ path: testInfo.outputPath('share-editor-mobile.png'), fullPage: true });
|
||||
await page.setViewportSize({ width: 1280, height: 900 });
|
||||
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('Gesendeter Abschlussbeitrag')).toBeVisible();
|
||||
await expect(planningGuestPage.getByText('FREIGABE_MODERATION')).toBeVisible();
|
||||
const guestThemePreference = await planningGuestPage
|
||||
.locator('html')
|
||||
.getAttribute('data-theme-preference');
|
||||
await planningGuestPage.getByRole('button', { name: /Darstellung:/ }).click();
|
||||
await expect(planningGuestPage.locator('html')).not.toHaveAttribute(
|
||||
'data-theme-preference',
|
||||
guestThemePreference!
|
||||
);
|
||||
|
||||
const planningShareRow = planningShares.locator('.share-row').first();
|
||||
await planningShareRow.locator('summary').getByText('Ablauf und Moderation').click();
|
||||
await planningShareRow.getByLabel('Informationsumfang').selectOption('research');
|
||||
await planningShareRow.locator('select[name="passwordMode"]').selectOption('replace');
|
||||
await planningShareRow.locator('input[name="password"]').fill('Planung-Freigabe-Passwort');
|
||||
await planningShareRow.getByRole('button', { name: 'Freigabe speichern' }).click();
|
||||
await planningGuestPage.reload();
|
||||
await expect(
|
||||
planningGuestPage.getByRole('heading', { name: 'Passwort erforderlich' })
|
||||
).toBeVisible();
|
||||
|
||||
page.once('dialog', (dialog) => dialog.accept());
|
||||
await planningShares
|
||||
.locator('.share-row')
|
||||
.first()
|
||||
.locator('summary')
|
||||
.getByText('Vollständig mit Recherche')
|
||||
.click();
|
||||
await planningShares.getByRole('button', { name: 'Freigabe löschen' }).click();
|
||||
await planningGuestPage.reload();
|
||||
await expect(planningGuestPage.getByRole('heading', { name: 'Nicht verfügbar' })).toBeVisible();
|
||||
await planningGuest.close();
|
||||
|
||||
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();
|
||||
const completion = page.locator('.completion-dialog');
|
||||
await expect(completion).toBeVisible();
|
||||
await expect(completion).toContainText('1 gesendet');
|
||||
await expect(completion).toContainText('1 übersprungen');
|
||||
await expect(completion.getByText('Die kleine Konsole mit dem großen Dreh')).toBeVisible();
|
||||
await expect(completion.getByText('Warum praktische Effekte wieder auffallen')).toHaveCount(0);
|
||||
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 expect(page.getByText('Gesendeter Abschlussbeitrag')).toBeVisible();
|
||||
await expect(page.getByText('Übersprungener Abschlussbeitrag')).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Sendungsdaten speichern' })).toHaveCount(0);
|
||||
|
||||
await page.goto('/verwaltung');
|
||||
const shareManagement = page.locator('.share-management');
|
||||
await shareManagement.getByLabel('Sendung').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();
|
||||
const shareLink = page.locator('.share-result a');
|
||||
await expect(shareLink).toBeVisible();
|
||||
const sharePath = await shareLink.getAttribute('href');
|
||||
expect(sharePath).toMatch(/^\/freigabe\//);
|
||||
await page.screenshot({ path: testInfo.outputPath('share-admin-desktop.png'), fullPage: true });
|
||||
|
||||
const guest = await browser.newContext();
|
||||
const guestPage = await guest.newPage();
|
||||
await guestPage.goto(sharePath!);
|
||||
await expect(guestPage.getByRole('heading', { name: 'Passwort erforderlich' })).toBeVisible();
|
||||
const passwordHtml = await guestPage.content();
|
||||
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 expect(guestPage.getByRole('alert')).toContainText('Passwort falsch');
|
||||
await guestPage.getByLabel('Passwort').fill('E2E-Freigabe-Passwort');
|
||||
await guestPage.getByRole('button', { name: 'Sendung ö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);
|
||||
await expect(guestPage.getByText('PRIVATE_FREIGABE_NOTIZ')).toHaveCount(0);
|
||||
await expect(guestPage.getByAltText('Freigegebenes Abschlussbild')).toBeVisible();
|
||||
await guestPage.setViewportSize({ width: 390, height: 844 });
|
||||
await expectNoHorizontalOverflow(guestPage);
|
||||
await guestPage.screenshot({ path: testInfo.outputPath('share-mobile.png'), fullPage: true });
|
||||
|
||||
const imagePath = await guestPage
|
||||
.getByAltText('Freigegebenes Abschlussbild')
|
||||
.getAttribute('src');
|
||||
const sharedImage = await guest.request.get(imagePath!);
|
||||
expect(sharedImage.status()).toBe(200);
|
||||
expect(sharedImage.headers()['content-security-policy']).toBe("default-src 'none'; sandbox");
|
||||
expect((await guest.request.get(`${sharePath}/media/foreign-id`)).status()).toBe(404);
|
||||
const guestWrite = await guest.request.post('/sendungen/e2e-completion?/saveMetadata', {
|
||||
maxRedirects: 0,
|
||||
form: { title: 'Verboten', revision: '2', status: 'draft' }
|
||||
});
|
||||
expect(guestWrite.status()).toBe(403);
|
||||
|
||||
await page.goto('/verwaltung');
|
||||
const showCard = page.locator('.share-show-card').filter({ hasText: 'E2E Abschluss' });
|
||||
await expect(showCard.getByText('Abgeschlossen')).toBeVisible();
|
||||
const shareRow = showCard.locator('.share-row').first();
|
||||
await shareRow.locator('summary').getByText('Ablauf und Moderation').click();
|
||||
await shareRow.getByLabel('Informationsumfang').selectOption('rundown');
|
||||
await shareRow.locator('select[name="passwordMode"]').selectOption('remove');
|
||||
await shareRow.getByRole('button', { name: 'Freigabe speichern' }).click();
|
||||
await guestPage.reload();
|
||||
await expect(guestPage.getByRole('heading', { name: 'E2E Abschluss' })).toBeVisible();
|
||||
await expect(guestPage.getByText('FREIGABE_MODERATION')).toHaveCount(0);
|
||||
|
||||
page.once('dialog', (dialog) => dialog.accept());
|
||||
const updatedCard = page.locator('.share-show-card').filter({ hasText: 'E2E Abschluss' });
|
||||
await updatedCard
|
||||
.locator('.share-row')
|
||||
.first()
|
||||
.locator('summary')
|
||||
.getByText('Nur Ablauf')
|
||||
.click();
|
||||
await updatedCard.getByRole('button', { name: 'Freigabe löschen' }).click();
|
||||
await guestPage.reload();
|
||||
await expect(guestPage.getByRole('heading', { name: 'Nicht verfügbar' })).toBeVisible();
|
||||
expect((await guest.request.get(imagePath!)).status()).toBe(404);
|
||||
await guest.close();
|
||||
} finally {
|
||||
removeCompletionFixture();
|
||||
}
|
||||
});
|
||||
|
||||
test('pieces autosave safely, detect conflicts and protect uploaded images', async ({
|
||||
page,
|
||||
browser
|
||||
|
||||
Reference in new issue
Block a user