import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { test } from 'node:test'; import { install, nodes, server, settle } from './flow-helpers.mjs'; const ui = install(); const { stopPolling } = await import('../../bindery/static/js/jobs.js'); const title = 'A very long story title that the tab cuts off with an ellipsis'; const story = (id, storyTitle) => ({ story: { id, title: storyTitle, author: 'Writer', url: 'https://example.com/story', site: 'example.com', locked_fields: [], pinned: {}, site_values: {}, auto_update: true, status: 'ok' }, chapters: [{ id: 2, title: 'One', url: 'chapter-1', included: true, on_site: true, current_rev: 1, revisions: 1 }], recent_chapters: 0, }); const diff = { chapter: { id: 2, title: 'One', url: 'https://example.com/c/1', current_rev: 1 }, story_title: title, a: 1, b: 1, identical: true, html: '', changed: 0, added: 0, removed: 0, revisions: [{ rev: 1, size: 10 }] }; const rules = { rules: [], paragraphs: null, global_paragraphs: false }; const tabs = () => nodes(ui.app).filter((el) => el.tagName === 'nav').flatMap((nav) => nav.children); const storyTabs = () => tabs().filter((a) => a.className === 'story-tab'); const current = () => tabs().filter((a) => a.attributes['aria-current'] === 'page').map((a) => a.textContent); const go = async (hash) => { window.location.hash = hash; ui.windowEvents.hashchange(); await settle(); }; test('an open story has a tab with its title, also on its chapter pages', async (t) => { t.mock.timers.enable({ apis: ['setTimeout'] }); t.after(stopPolling); const backend = server(t); backend.on('GET', 'auth/check', { authenticated: true, mode: 'none', user: '' }); backend.on('GET', 'config', { metadata_fields: {}, mode_labels: {}, library_available: false, download_fields: {} }); backend.on('GET', 'jobs', []); backend.on('GET', 'stories', []); backend.on('GET', 'settings', { recent_chapters: 0, personal_ini: '' }); backend.on('GET', 'rules', rules); backend.on('GET', 'stories/1', story(1, title)); backend.on('GET', 'stories/1/rules', rules); backend.on('GET', 'stories/2', story(2, 'Second')); backend.on('GET', 'stories/2/rules', rules); backend.on('GET', 'chapters/2/diff?', diff); await import('../../bindery/static/js/main.js'); await settle(); assert.deepEqual(storyTabs(), []); await go('#/story/1'); const [tab] = storyTabs(); assert.equal(tab.textContent, title); assert.equal(tab.attributes.title, title); assert.equal(tab.attributes.href, '#/story/1'); assert.deepEqual(current(), [title]); await go('#/story/1/chapter/2'); assert.deepEqual(storyTabs(), [tab]); assert.deepEqual(current(), [title]); await go('#/story/2'); assert.equal(storyTabs().length, 1); assert.deepEqual(current(), ['Second']); await go('#/settings'); assert.deepEqual(storyTabs(), []); assert.deepEqual(current(), ['Settings']); }); test('a story that loads after the user left it gets no tab', async (t) => { t.mock.timers.enable({ apis: ['setTimeout'] }); t.after(stopPolling); const backend = server(t); let release; backend.on('GET', 'stories', []); backend.on('GET', 'jobs', []); backend.on('GET', 'stories/3', () => new Promise((resolve) => { release = () => resolve(story(3, 'Late')); })); backend.on('GET', 'stories/3/rules', rules); await go('#/story/3'); await go('#/'); release(); await settle(); assert.deepEqual(storyTabs(), []); assert.deepEqual(current(), ['Stories']); }); test('a long title ends in an ellipsis', () => { const css = readFileSync(new URL('../../bindery/static/style.css', import.meta.url), 'utf8'); const rule = css.match(/header nav a\.story-tab \{([^}]*)\}/)[1]; for (const declaration of ['max-width: 24ch', 'overflow: hidden', 'text-overflow: ellipsis', 'white-space: nowrap']) { assert.ok(rule.includes(`${declaration};`), declaration); } // Without it, the nav keeps the width of the whole title and runs past the header on a phone. assert.ok(css.match(/header nav \{([^}]*)\}/)[1].includes('min-width: 0;')); });