Files
bindery/tests/js/story-tab.test.mjs
tim 3e29ab6bf0 Name downloaded books with a template
The new "Book Downloads" group in Settings holds a Calibre template for the file name of a
direct download. Without a template, the name stays the title. The name is also the title if
the template fails or gives no name for a book.
2026-10-03 08:09:41 +02:00

94 lines
4.0 KiB
JavaScript

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;'));
});