Files
bindery/tests/js/upload-hold.test.mjs
T
tim 2f4271314b Add the web interface
Plain HTML, CSS and JavaScript modules, with no build step. Hash routes
and relative URLs make it work under any path. The job list is polled:
every 2 seconds while a job runs, and less often otherwise.
2026-09-27 23:38:28 +02:00

122 lines
5.7 KiB
JavaScript

import assert from 'node:assert/strict';
import { test } from 'node:test';
import { Element, change, click, find, install, nodes, section, server, settle } from './flow-helpers.mjs';
const ui = install();
const { settingsPage } = await import('../../bindery/static/js/settings.js');
const { storyPage } = await import('../../bindery/static/js/story.js');
const { storiesPage } = await import('../../bindery/static/js/stories.js');
const book = { id: 7, uuid: 'book-7', title: 'Library Title', authors: ['Writer'] };
const details = (story, extra = {}) => ({
story: { id: 1, title: 'Story', author: 'Writer', url: 'https://example.com/story', site: 'example.com',
locked_fields: [], pinned: {}, site_values: {}, auto_update: true, status: 'ok', cover: null,
calibre_id: null, calibre_uuid: null, ingested_at: null, upload_held: false, needs_approval: false, ...story },
chapters: [],
recent_chapters: 0,
uploads_paused: false,
book: null,
...extra,
});
async function renderStory(t, data) {
const backend = server(t);
backend.on('GET', 'stories/1', data);
backend.on('GET', 'stories/1/rules', { rules: [], paragraphs: null, global_paragraphs: false });
backend.on('GET', 'jobs?story_id=1', []);
backend.on('GET', 'jobs', []);
const main = new Element('main');
const cleanup = storyPage(main, { storyId: '1' }, { metadata_fields: {}, mode_labels: {} });
t.after(() => cleanup?.());
await settle();
const summary = nodes(main).find((el) => el.className === 'card story-summary');
return { backend, notice: nodes(summary).find((el) => el.className === 'notice') };
}
test('the upload switches load and save on the settings page', async (t) => {
const backend = server(t);
backend.on('GET', 'settings', { recent_chapters: 0, hold_new_links: true, pause_uploads: false, personal_ini: '' });
backend.on('PUT', 'settings', {});
backend.on('GET', 'rules', { rules: [], paragraphs: false });
backend.on('GET', 'stories', []);
const main = new Element('main');
settingsPage(main);
await settle();
const card = section(main, 'Uploads to CWA');
const [hold, pause] = nodes(card).filter((el) => el.attributes.type === 'checkbox');
assert.deepEqual([hold.checked, pause.checked], [true, false]);
change(pause, true);
await click(card, 'Save');
assert.deepEqual(backend.sent('PUT', 'settings')[0].body, { hold_new_links: true, pause_uploads: true });
});
test('a held book with a new link names the book and sends it on approval', async (t) => {
const data = details(
{ calibre_id: 7, calibre_uuid: 'book-7', ingested_at: '2026-01-01', upload_held: true, needs_approval: true },
{ book },
);
const { backend, notice } = await renderStory(t, data);
assert.match(notice.textContent, /waits for your approval\. The next upload replaces the ePub of library book 7: Library Title · Writer\./);
backend.on('POST', 'stories/1/approve', { story: data.story, job: { id: 'j1' } });
await click(notice, 'Approve and send');
await settle();
assert.deepEqual(backend.sent('POST', 'stories/1/approve')[0].body, { uuid: 'book-7' });
assert.match(ui.toasts.textContent, /Approved\. Bindery rebuilds the book and sends it\./);
});
test('a wrong link can go to CWA as a new book instead', async (t) => {
const data = details(
{ calibre_id: 7, calibre_uuid: 'book-7', ingested_at: '2026-01-01', upload_held: true, needs_approval: true },
{ book },
);
const { backend, notice } = await renderStory(t, data);
backend.on('POST', 'stories/1/link', data.story);
backend.on('POST', 'stories/1/approve', { story: data.story, job: null });
await click(notice, 'Send as a new book instead');
await settle();
assert.deepEqual(
backend.requests.filter((r) => r.method === 'POST').map((r) => [r.path, r.body]),
[['stories/1/link', { uuid: null }], ['stories/1/approve', { uuid: null }]],
);
});
test('a new story asks for approval to go to CWA as a new book', async (t) => {
const { notice } = await renderStory(t, details({ needs_approval: true }));
assert.match(notice.textContent, /The next upload goes to CWA as a new book\./);
assert.ok(find(notice, 'button', 'Approve'));
assert.equal(find(notice, 'button', 'Send as a new book instead'), undefined);
});
test('a paused upload points to the settings', async (t) => {
const { notice } = await renderStory(
t,
details({ calibre_id: 7, calibre_uuid: 'book-7', ingested_at: '2026-01-01', upload_held: true }, { book, uploads_paused: true }),
);
assert.match(notice.textContent, /Uploads to CWA are paused/);
assert.equal(find(notice, 'a', 'Settings').attributes.href, '#/settings');
assert.equal(nodes(notice).filter((el) => el.tagName === 'button').length, 0);
});
test('a story that went to CWA without a link shows no approval until Bindery finds its book', async (t) => {
const { notice } = await renderStory(t, details({ ingested_at: '2026-01-01', needs_approval: true }));
assert.equal(notice, undefined);
});
test('the story list marks a held book', async (t) => {
const backend = server(t);
backend.on('GET', 'stories', [
{ id: 1, url: 'u1', title: 'Held', author: 'A', site: 's', chapter_count: 1, offsite_count: 0, status: 'ok',
message: '', upload_held: true },
{ id: 2, url: 'u2', title: 'Sent', author: 'A', site: 's', chapter_count: 1, offsite_count: 0, status: 'ok',
message: '', upload_held: false },
]);
backend.on('GET', 'jobs', []);
const main = new Element('main');
const cleanup = storiesPage(main, {}, { library_available: false });
t.after(() => cleanup?.());
await settle();
const held = nodes(main).filter((el) => el.tagName === 'tr').filter((row) => find(row, 'span', 'held'));
assert.equal(held.length, 1);
assert.match(held[0].textContent, /^Held/);
});