import assert from 'node:assert/strict'; import { test } from 'node:test'; import { install, nodes, server, settle } from './flow-helpers.mjs'; const { app } = install(); const { storiesPage } = await import('../../bindery/static/js/stories.js'); const { stopPolling } = await import('../../bindery/static/js/jobs.js'); const { fmtDate } = await import('../../bindery/static/js/lib.js'); const story = (id, title, changes = {}) => ({ id, title, url: `https://example.com/${id}`, author: 'Reader', site: 'example.com', chapter_count: 42, offsite_count: 0, revised_count: 0, status: 'ok', message: 'No changes', last_changed: null, last_checked: null, ...changes, }); const rows = () => { const table = nodes(app).find((el) => el.className === 'table-wrap stories-table'); return nodes(table).find((el) => el.tagName === 'tbody').children; }; const cell = (row, label) => row.children.find((el) => el.attributes['data-label'] === label); async function mount(t, list) { const backend = server(t); backend.on('GET', 'stories', list); backend.on('GET', 'jobs', []); const cleanup = storiesPage(app, {}, { library_available: false, main_jobs_hours: 24, main_jobs_count: 10 }); t.after(() => { cleanup(); stopPolling(); }); await settle(); return backend; } test('date columns keep each timestamp on one line, with last change before last check', async (t) => { const changed = '2026-01-02T03:04:00Z'; const checked = '2026-01-03T05:06:00Z'; await mount(t, [story(1, 'Alpha', { last_changed: changed, last_checked: checked })]); const headings = nodes(app).filter((el) => el.tagName === 'th').map((el) => el.textContent); assert.deepEqual(headings, ['Title', 'Author', 'Site', 'Chapters', 'Status', 'Last change', 'Last check']); for (const [label, value] of [['Last change', changed], ['Last check', checked]]) { const dateCell = cell(rows()[0], label); const time = nodes(dateCell).find((el) => el.tagName === 'time'); assert.equal(time.attributes.datetime, value); assert.equal(time.attributes.title, fmtDate(value)); assert.equal(nodes(time).filter((el) => el.tagName === 'br').length, 0); assert.equal(time.textContent, fmtDate(value)); } }); test('status messages move into accessible pill descriptions without hiding held or chapter counts', async (t) => { await mount(t, [ story(1, 'Alpha', { offsite_count: 3, revised_count: 2, message: 'Updated 2 chapters', upload_held: true }), story(2, 'Beta', { status: 'error', message: '' }), story(3, 'Gamma'), story(4, 'New', { status: '', message: '' }), story(5, 'Queued', { status: 'queued', message: '' }), ]); for (const [row, label, message] of [ [rows()[0], 'ok', 'Updated 2 chapters'], [rows()[1], 'error', ''], [rows()[2], 'ok', 'No changes'], ]) { const status = cell(row, 'Status'); const pill = status.children[0]; assert.equal(pill.textContent, label); assert.equal(pill.attributes.title, message); assert.equal(pill.attributes['aria-description'], message); assert.equal(pill.attributes.tabindex, '0'); assert.ok(!status.textContent.includes(message)); assert.equal(nodes(status).filter((el) => el.tagName === 'small' || el.tagName === 'script').length, 0); } assert.match(cell(rows()[0], 'Status').textContent, /held/); const chapters = cell(rows()[0], 'Chapters'); assert.match(chapters.textContent, /^42/); assert.equal(nodes(chapters).filter((el) => el.className.startsWith('badge')).length, 0); const counts = nodes(chapters).filter((el) => el.tagName === 'small'); assert.deepEqual(counts.map((el) => el.textContent), ['3 removed', '2 revised']); assert.deepEqual(counts.map((el) => el.attributes.title), [ 'Chapters the site no longer lists', 'Chapters the site changed since Bindery added them', ]); assert.equal(cell(rows()[1], 'Chapters').textContent, '42'); assert.equal(cell(rows()[3], 'Status').textContent, ''); assert.equal(cell(rows()[4], 'Status').textContent, 'queued'); });