diora-web/e2e/navigation.spec.js
mrwnslz 1cb6584395
Some checks failed
Test / e2e (push) Waiting to run
Build and push Docker image / build (push) Successful in 16s
Test / test (push) Has been cancelled
Buch-Metadaten: Änderung auf veraltetem Stand wird abgelehnt (409 mit dem aktuellen), das Web baut sie darauf neu auf
Eine App mit alter Bücherliste (Desktop während des langsamen Syncs) hat beim Öffnen fünf
MEW-Bände mit ihrem alten Stand überschrieben – Titel, Autor, Ordner weg. base_iv sagt dem
Server, von welchem Stand eine Änderung ausgeht.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 18:03:49 +02:00

169 lines
9.7 KiB
JavaScript

// The navigation (docs/navigation.md): Home, Lesen (Bücher, Zeitschriften, Nachrichten,
// Blogs), Hören (Hörbücher, Podcasts, Radio, Ambient), on the web page's own bar.
const { test, expect } = require('@playwright/test');
const path = require('path');
test.use({ storageState: path.join(__dirname, '.auth', 'user.json') });
const visiblePanel = page => page.evaluate(() =>
[...document.querySelectorAll('section.tab-panel')].filter(p => p.style.display !== 'none').map(p => p.id));
test('wide: every section in the bar under its group, each shows its panel and view', async ({ page }) => {
await page.setViewportSize({ width: 1200, height: 800 });
await page.goto('/#books');
const wide = page.locator('#tabs .nav-wide');
await expect(wide.locator('.nav-group-label')).toHaveText(['Lesen', 'Hören']);
await expect(wide.locator('[data-nav]')).toHaveText(
['Home', 'Bücher', 'Zeitschriften', 'Nachrichten', 'Blogs', 'Hörbücher', 'Podcasts', 'Radio', 'Ambient', 'Fokus', 'Abos & Quellen']);
await wide.locator('[data-nav="blogs"]').click();
await expect(page).toHaveURL(/#blogs$/);
expect(await visiblePanel(page)).toEqual(['tab-feeds']);
expect(await page.evaluate(() => localStorage.getItem('diora_feeds_view'))).toBe('blogs');
await expect(page.locator('#tab-feeds .fd-seg')).toBeHidden(); // the bar chooses, not the tab
await expect(page.locator('#nav-segments')).toBeHidden();
await wide.locator('[data-nav="audiobooks"]').click();
expect(await visiblePanel(page)).toEqual(['tab-audiobooks']);
await expect(page.locator('#audiobook-shelf')).toBeVisible();
await expect(wide.locator('[data-nav="audiobooks"]')).toHaveClass(/active/);
});
test('narrow: three groups, the sections as a row on top, each group remembers its last', async ({ page }) => {
await page.setViewportSize({ width: 390, height: 800 });
await page.goto('/#news');
const narrow = page.locator('#tabs .nav-narrow');
await expect(narrow.locator('[data-nav]')).toHaveText(['Home', 'Lesen', 'Hören', '']);
await expect(narrow.locator('[data-nav="sources"] svg.dnav-ic')).toBeVisible();
await expect(narrow.locator('[data-nav="lesen"]')).toHaveClass(/active/);
const segs = page.locator('#nav-segments');
await expect(segs.locator('[data-nav]')).toHaveText(['Bücher', 'Zeitschriften', 'Nachrichten', 'Blogs']);
await expect(segs.locator('[data-nav="news"]')).toHaveClass(/active/);
await narrow.locator('[data-nav="hoeren"]').click();
await segs.locator('[data-nav="radio"]').click();
expect(await visiblePanel(page)).toEqual(['tab-radio']);
await narrow.locator('[data-nav="lesen"]').click();
await expect(page).toHaveURL(/#news$/); // back where Lesen was left
await narrow.locator('[data-nav="hoeren"]').click();
await expect(page).toHaveURL(/#radio/);
});
test('old links and direct panel switches land in their section', async ({ page }) => {
await page.setViewportSize({ width: 1200, height: 800 });
await page.goto('/#feeds');
expect(await visiblePanel(page)).toEqual(['tab-feeds']);
await expect(page.locator('#tabs .nav-wide [data-nav].active')).toHaveText(/Nachrichten|Blogs/);
await page.evaluate(() => showTab('podcasts'));
await expect(page.locator('#tabs [data-nav="podcasts"]')).toHaveClass(/active/);
await expect(page).toHaveURL(/#podcasts$/);
await page.reload();
expect(await visiblePanel(page)).toEqual(['tab-podcasts']); // remembered
});
test('Abos & Quellen: one page for every subscription, reached from the feeds too', async ({ page }) => {
await page.setViewportSize({ width: 1200, height: 800 });
await page.goto('/#news');
await page.locator('#tab-feeds [data-act="manage"]').first().click();
await expect(page).toHaveURL(/#sources$/);
const tab = page.locator('#tab-sources');
await expect(tab.locator('.src-h1')).toHaveText('Abos & Quellen');
await expect(tab.locator('.src-add-q')).toBeVisible();
await expect(tab.locator('.src-chip.on')).toContainText('Nachrichten'); // came from the news
await tab.locator('[data-filter="all"]').click();
await expect(tab.locator('.src-chip.on')).toContainText('Alle');
});
test('the library: a header with search, sort, layout and +, the add menu', async ({ page }) => {
await page.setViewportSize({ width: 1200, height: 800 });
await page.goto('/#books');
const list = page.locator('#book-list');
await expect(list.locator('.lib-h')).toHaveText('Bücher');
await expect(list.locator('.lib-q')).toBeVisible();
await expect(list.locator('.lib-sort')).toBeVisible();
await expect(page.locator('#book-drop-zone')).toBeHidden(); // adding is behind +
await list.locator('[data-lib="add"]').click();
await expect(page.locator('.dfo-sheet')).toContainText('E-Book-Datei');
await page.locator('.dfo-sheet [data-x]').click();
await expect(page.locator('.dfo-sheet')).toHaveCount(0);
await page.goto('/#magazines');
await expect(list.locator('.lib-h')).toHaveText('Zeitschriften');
});
test('several books at once; one already there is left out', async ({ page }) => {
const files = ['Eins', 'Zwei'].map(n => path.join(__dirname, '.auth', `many-${n}.epub`));
await page.goto('/#books');
await page.waitForFunction(() => typeof _importMany === 'function' && Array.isArray(_lastBookListData));
const status = page.locator('#book-upload-status');
await page.locator('#book-file-input').setInputFiles(files);
await expect(status).toContainText('2 Bücher hinzugefügt', { timeout: 30000 });
await page.waitForFunction(() => _lastBookListData.filter(b => /^Mehrfach/.test(b.title)).length === 2);
await page.locator('#book-file-input').setInputFiles(files);
await expect(status).toContainText('2 schon da', { timeout: 30000 });
// Out again, so the other tests see the library as before.
const books = await page.evaluate(() => _lastBookListData.filter(b => /^Mehrfach/.test(b.title)).map(b => b.id));
for (const id of books) await page.request.post(`/books/${id}/delete/`);
});
test('editions: two books joined show once with their formats, set apart show twice', async ({ page }) => {
await page.setViewportSize({ width: 1200, height: 800 });
const files = ['Eins', 'Zwei'].map(n => path.join(__dirname, '.auth', `many-${n}.epub`));
await page.goto('/#books');
await page.waitForFunction(() => typeof _importMany === 'function' && Array.isArray(_lastBookListData));
await page.locator('#book-file-input').setInputFiles(files);
await page.waitForFunction(() => _lastBookListData.filter(b => /^Mehrfach/.test(b.title) && b.id).length === 2, null, { timeout: 30000 });
const ids = await page.evaluate(() => _lastBookListData.filter(b => /^Mehrfach/.test(b.title)).map(b => b.id));
const list = page.locator('#book-list');
// New books: as cards under "Ungelesen" (not in the "Zuletzt gelesen" area on top).
const unread = () => page.evaluate(() => { _lib.status = 'unread'; renderBookList(_lastBookListData); });
await unread();
const cards = () => list.locator('.lib-grid [data-lib-book]').filter({ hasText: 'Mehrfach' });
await expect(cards()).toHaveCount(2);
// Choose both, join them.
await page.evaluate(ids => { _lib.selecting = new Set(ids); renderBookList(_lastBookListData); }, ids);
await list.locator('[data-lib="sel-editions"]').click();
await page.waitForFunction(ids => {
const w = ids.map(id => _lastBookListData.find(b => b.id === id).work);
return w[0] && w[0] === w[1];
}, ids);
await expect(cards()).toHaveCount(1);
await expect(cards().locator('.lib-ed')).toHaveText('EPUB');
// The work survives a reload (it is in the encrypted meta).
await page.reload();
await page.waitForFunction(() => Array.isArray(_lastBookListData) && _lastBookListData.length);
await unread();
await expect(cards()).toHaveCount(1);
// Ausgaben: both listed; one set apart, and the other is on its own again.
await cards().locator('.lib-more').click({ force: true });
await page.locator('.dfo-sheet .lib-sheet-item', { hasText: 'Ausgaben (2)' }).click();
await expect(page.locator('.dfo-sheet .lib-ed-row')).toHaveCount(2);
await page.locator('.dfo-sheet [data-unlink]').first().click();
await page.waitForFunction(ids => ids.every(id => !_lastBookListData.find(b => b.id === id).work), ids);
await expect(cards()).toHaveCount(2);
await page.evaluate(() => { _lib.status = 'all'; });
for (const id of ids) await page.request.post(`/books/${id}/delete/`);
});
test('a meta change from an old list goes onto the current meta (409, then again)', async ({ page }) => {
const files = [path.join(__dirname, '.auth', 'many-Eins.epub')];
await page.goto('/#books');
await page.waitForFunction(() => typeof _importMany === 'function' && Array.isArray(_lastBookListData));
await page.locator('#book-file-input').setInputFiles(files);
await page.waitForFunction(() => _lastBookListData.some(b => b.title === 'Mehrfach Eins' && b.id && b.meta_iv), null, { timeout: 30000 });
const id = await page.evaluate(() => _lastBookListData.find(b => b.title === 'Mehrfach Eins').id);
// Elsewhere the title changes; this page still has the old list.
await page.evaluate(id => _updateBookMeta(id, {title: 'Anderswo umbenannt'}), id);
await page.evaluate(id => {
const b = _lastBookListData.find(x => x.id === id);
b.title = 'Mehrfach Eins'; b.meta_iv = '000000000000000000000000'; // the old list
bookMetaCache[id] = {...bookMetaCache[id], title: 'Mehrfach Eins'};
}, id);
await page.evaluate(id => _updateBookMeta(id, {folders: ['Probe']}), id);
const meta = await page.evaluate(async id => {
const row = (await (await fetch('/books/', {cache: 'no-store'})).json()).find(b => b.id === id);
return JSON.parse(new TextDecoder().decode(await decryptBytes(await getOrCreateEncKey(), row.meta_iv, row.meta_ct)));
}, id);
expect(meta.title).toBe('Anderswo umbenannt');
expect(meta.folders).toEqual(['Probe']);
await page.request.post(`/books/${id}/delete/`);
});