tests/page-unit.js loads the built index.html script into the stub DOM page-smoke.js uses and calls its helpers directly with node:test: escaping, times and sizes, file kinds, an untitled item's name, the directory's sorts, filter words, DeadAir's playlist address. Top-level consts are not properties of the vm context, so each check evaluates its expression in it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
133 lines
6.1 KiB
JavaScript
133 lines
6.1 KiB
JavaScript
// Unit tests for the page's own functions (#234): the script as shipped, loaded into the stub DOM
|
|
// page-smoke.js uses, and its helpers called directly. The browser suite tests what people do;
|
|
// this tests the small pieces it is made of, in a second, with nothing running.
|
|
//
|
|
// node --test tests/page-unit.js
|
|
//
|
|
// The files in web/src are one script, not modules. Its top-level `function`s are properties of
|
|
// the context; its top-level `const`s are not, but a script run in the same context sees them, as
|
|
// a second <script> on a page would, so `run` evaluates an expression there.
|
|
const { test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const { makeContext, vm } = require('./dom-stub.js');
|
|
const { buildPage } = require('../web/build.mjs');
|
|
|
|
const { html, js } = buildPage('index.html');
|
|
const { ctx } = makeContext(html, js);
|
|
vm.createContext(ctx);
|
|
vm.runInContext(js, ctx, { filename: 'index.html<script>', timeout: 5000 });
|
|
const run = expr => vm.runInContext(expr, ctx);
|
|
// Time as the page sees it, for the helpers that count from now.
|
|
const NOW = 1_800_000_000;
|
|
run(`Date.now = () => ${NOW * 1000}`);
|
|
|
|
test('esc makes text safe to put in markup and attributes', () => {
|
|
assert.equal(run(`esc('<a href="x">Tom & Jerry\\'s</a>')`), '<a href="x">Tom & Jerry's</a>');
|
|
assert.equal(run('esc(null)'), '');
|
|
assert.equal(run('esc(undefined)'), '');
|
|
});
|
|
|
|
test('clock reads seconds as a player does', () => {
|
|
assert.equal(run('clock(0)'), '0:00');
|
|
assert.equal(run('clock(65)'), '1:05');
|
|
assert.equal(run('clock(3661)'), '1:01:01');
|
|
assert.equal(run('clock(59.9)'), '0:59');
|
|
assert.equal(run('clock(-5)'), '0:00');
|
|
assert.equal(run('clock(undefined)'), '0:00');
|
|
});
|
|
|
|
test('mb sizes a file in the unit that reads best', () => {
|
|
assert.equal(run('mb(0)'), '');
|
|
// An article's small image is not "0 KB" or "0 MB".
|
|
assert.equal(run('mb(100)'), '1 KB');
|
|
assert.equal(run('mb(2048)'), '2 KB');
|
|
assert.equal(run('mb(5 * 1048576)'), '5 MB');
|
|
assert.equal(run('mb(1.5 * 1073741824)'), '1.5 GB');
|
|
});
|
|
|
|
test('ago says how long since, and never for nothing', () => {
|
|
assert.equal(run('ago(0)'), 'never');
|
|
assert.equal(run(`ago(${NOW - 10})`), '1m ago');
|
|
assert.equal(run(`ago(${NOW - 45 * 60})`), '45m ago');
|
|
assert.equal(run(`ago(${NOW - 5 * 3600})`), '5h ago');
|
|
assert.equal(run(`ago(${NOW - 3 * 86400})`), '3d ago');
|
|
// Past a month, a date: whatever the locale, it names the year.
|
|
assert.match(run(`ago(${NOW - 90 * 86400})`), /2026/);
|
|
});
|
|
|
|
test('due says how long until a feed is checked', () => {
|
|
assert.equal(run(`due(${NOW - 1})`), 'due now');
|
|
assert.equal(run(`due(${NOW + 10})`), 'in 1m');
|
|
assert.equal(run(`due(${NOW + 90 * 60})`), 'in 2h');
|
|
assert.equal(run(`due(${NOW + 3 * 86400})`), 'in 3d');
|
|
});
|
|
|
|
test('DeadAir savings read as time', () => {
|
|
assert.equal(run('spanText(40)'), '40 s');
|
|
assert.equal(run('spanText(12 * 60 + 5)'), '12 min');
|
|
assert.equal(run('spanText(3 * 3600 + 12 * 60)'), '3 h 12 min');
|
|
assert.equal(run('shorter(45)'), '45 s shorter');
|
|
assert.equal(run('shorter(150)'), '3 min shorter');
|
|
});
|
|
|
|
test('initials and plural', () => {
|
|
assert.equal(run(`initials('The Verge')`), 'TV');
|
|
assert.equal(run(`initials('accidental tech podcast')`), 'AT');
|
|
assert.equal(run(`initials('')`), '?');
|
|
assert.equal(run(`initials('!!!')`), '?');
|
|
assert.equal(run(`plural(1, 'feed')`), '1 feed');
|
|
assert.equal(run(`plural(0, 'feed')`), '0 feeds');
|
|
assert.equal(run(`plural(2, 'feed')`), '2 feeds');
|
|
});
|
|
|
|
test("kindOf names a file by its type, or else its address's extension", () => {
|
|
const kind = enc => run(`kindOf(${JSON.stringify(enc)})`);
|
|
assert.equal(kind({ mime: 'image/png' }), 'image');
|
|
assert.equal(kind({ mime: 'Video/MP4' }), 'video');
|
|
assert.equal(kind({ mime: 'audio/mpeg' }), 'audio');
|
|
assert.equal(kind({ mime: 'application/pdf' }), 'pdf');
|
|
assert.equal(kind({ mime: 'application/x-bittorrent' }), 'torrent');
|
|
assert.equal(kind({ url: 'https://x/ep.MP3?token=1' }), 'mp3');
|
|
assert.equal(kind({ url: 'https://x/download' }), 'file');
|
|
assert.equal(kind({ url: 'https://x/a.toolongext' }), 'file');
|
|
assert.equal(kind({}), 'file');
|
|
});
|
|
|
|
test('an item without a title is named by its file', () => {
|
|
const name = e => run(`entryName(${JSON.stringify(e)})`);
|
|
assert.deepEqual({ ...name({ title: 'Episode 1' }) }, { text: 'Episode 1', derived: false });
|
|
assert.deepEqual({ ...name({ title: ' ', enclosures: [{ url: 'https://x/My%20Show%20101.mp3?x=1' }] }) },
|
|
{ text: 'My Show 101', derived: true });
|
|
// A name that does not decode is shown as it is, not dropped.
|
|
assert.deepEqual({ ...name({ enclosures: [{ url: 'https://x/100%.mp3' }] }) }, { text: '100%', derived: true });
|
|
});
|
|
|
|
test('the directory sorts by name as people read, and by subscribers', () => {
|
|
const sorted = (list, by) => run(`${JSON.stringify(list)}.sort(${by}).map(p => p.title)`);
|
|
assert.deepEqual([...sorted([{ title: 'ep 10' }, { title: 'Ep 2' }, { title: 'ep 1' }], 'byName')], ['ep 1', 'Ep 2', 'ep 10']);
|
|
assert.deepEqual([...sorted([{ title: 'B', subscribers: 1 }, { title: 'C', subscribers: 5 }, { title: 'A', subscribers: 1 }], 'bySubs')],
|
|
['C', 'A', 'B']);
|
|
});
|
|
|
|
test('feeds with something unread go first, in the order they came', () => {
|
|
const ids = run(`[{id:'a',unread:0},{id:'b',unread:3},{id:'c',unread:0},{id:'d',unread:1}].sort(unreadFirst).map(f => f.id)`);
|
|
assert.deepEqual([...ids], ['b', 'd', 'a', 'c']);
|
|
});
|
|
|
|
test('words for filters split on commas, blanks dropped', () => {
|
|
assert.deepEqual([...run(`splitWords(' one, two words ,, three ')`)], ['one', 'two words', 'three']);
|
|
assert.deepEqual([...run(`splitWords('')`)], []);
|
|
});
|
|
|
|
test("a file plays from DeadAir's playlist when it has one", () => {
|
|
assert.equal(run(`mediaSrc({ id: 7, deadair: null })`), '/media/7');
|
|
assert.equal(run(`mediaSrc({ id: 7 })`), '/media/7');
|
|
assert.equal(run(`mediaSrc({ id: 7, deadair: 0 })`), '/media/7/deadair.m3u8');
|
|
});
|
|
|
|
test('a Patreon feed is told by its address', () => {
|
|
assert.equal(run(`isPatreon({ url: 'https://www.patreon.com/rss/show?auth=x' })`), true);
|
|
assert.equal(run(`isPatreon({ url: 'https://example.com/feed' })`), false);
|
|
assert.equal(run('isPatreon(null)'), false);
|
|
});
|