// 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/`); });