118 lines
5.7 KiB
TypeScript
118 lines
5.7 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import {
|
|
buildColumnCss, compileFilters, createUnreadTracker, isSourcePage, normalizeColumnConfig, parseTerm, type ColumnSite,
|
|
} from '../src/column.ts';
|
|
|
|
const SITE: ColumnSite = {
|
|
prefix: 'demo',
|
|
keep: '#content',
|
|
hide: ['nav.rail', '.fab'],
|
|
sourceHide: ['.page-title'],
|
|
itemSelector: 'article',
|
|
itemId: item => item.getAttribute('data-id'),
|
|
css: density => `html[data-demo-column][data-demo-density="${density}"] article { padding: 0 }`,
|
|
};
|
|
|
|
const item = (text: string, kind: Partial<{ reply: boolean; repost: boolean; media: boolean }> = {}) =>
|
|
({ text, reply: false, repost: false, media: false, ...kind });
|
|
|
|
test('column: config is normalized and bounded', () => {
|
|
assert.equal(normalizeColumnConfig(null), null);
|
|
assert.equal(normalizeColumnConfig({ url: 5 }), null);
|
|
const config = normalizeColumnConfig({
|
|
url: 'https://site.test/home', density: 'weird', filters: { terms: ['ok', '', 7, 'x'.repeat(201), ...Array(40).fill('t')], mediaOnly: 'yes' },
|
|
readSeq: -3, jumpSeq: 2.7, kind: 'home',
|
|
})!;
|
|
assert.equal(config.density, 'compact');
|
|
assert.equal(config.filters.terms.length, 32);
|
|
assert.equal(config.filters.terms[0], 'ok');
|
|
assert.equal(config.filters.mediaOnly, false);
|
|
assert.deepEqual([config.readSeq, config.jumpSeq, config.unread], [0, 2, true]);
|
|
});
|
|
|
|
test('column: terms are keywords or /regex/flags; bad regexes are skipped', () => {
|
|
assert.equal(parseTerm(' Crypto '), 'crypto');
|
|
const re = parseTerm('/gm+\\b/');
|
|
assert.ok(re instanceof RegExp && re.flags.includes('i'), 'regexes are case-insensitive');
|
|
assert.equal(parseTerm('/(unclosed/'), null);
|
|
assert.equal(parseTerm('/a/g'), '/a/g', 'only i, m, s, u flags make a regex');
|
|
assert.equal(parseTerm(' '), null);
|
|
});
|
|
|
|
test('column: filters hide replies, reposts, non-media and matching text', () => {
|
|
const none = compileFilters({ terms: [], hideReplies: false, hideReposts: false, mediaOnly: false });
|
|
assert.equal(none(item('anything', { reply: true })), false);
|
|
const f = compileFilters({ terms: ['giveaway', '/^gm\\b/', '/(bad/'], hideReplies: true, hideReposts: true, mediaOnly: false });
|
|
assert.equal(f(item('Huge GIVEAWAY today')), true);
|
|
assert.equal(f(item('GM frens')), true);
|
|
assert.equal(f(item('a normal post')), false);
|
|
assert.equal(f(item('normal', { reply: true })), true);
|
|
assert.equal(f(item('normal', { repost: true })), true);
|
|
const media = compileFilters({ terms: [], hideReplies: false, hideReposts: false, mediaOnly: true });
|
|
assert.equal(media(item('text only')), true);
|
|
assert.equal(media(item('photo', { media: true })), false);
|
|
});
|
|
|
|
test('column: the source page is compared by origin, path and query', () => {
|
|
assert.equal(isSourcePage('https://site.test/home/', 'https://site.test/home'), true);
|
|
assert.equal(isSourcePage('https://site.test/search?q=a&f=live', 'https://site.test/search?q=a&f=live'), true);
|
|
assert.equal(isSourcePage('https://site.test/search?q=b&f=live', 'https://site.test/search?q=a&f=live'), false);
|
|
assert.equal(isSourcePage('https://site.test/home/status/1', 'https://site.test/home'), false);
|
|
assert.equal(isSourcePage('https://other.test/home', 'https://site.test/home'), false);
|
|
assert.equal(isSourcePage('nonsense', 'https://site.test/home'), false);
|
|
});
|
|
|
|
test('column: CSS is scoped to the column attribute and hides chrome', () => {
|
|
const css = buildColumnCss(SITE, 'comfortable');
|
|
for (const line of css.split('\n').filter(Boolean)) assert.match(line, /^html\[data-demo-column\]/, line);
|
|
assert.match(css, /:is\(nav\.rail, \.fab\) \{ display: none !important; \}/);
|
|
assert.match(css, /html\[data-demo-column\]\[data-demo-column-home\] :is\(\.page-title\)/);
|
|
assert.match(css, /#content \{ width: 100% !important; max-width: none !important/);
|
|
assert.match(css, /\[data-demo-filtered\] \{ display: none !important; \}/);
|
|
assert.match(css, /data-demo-density="comfortable"/);
|
|
});
|
|
|
|
test('unread: first render is the baseline; new ids above the read ones count until seen', () => {
|
|
const t = createUnreadTracker();
|
|
t.observe([], new Set());
|
|
t.observe(['c', 'b', 'a'], new Set(['c', 'b', 'a']));
|
|
assert.equal(t.count(), 0, 'baseline');
|
|
// Two new posts rendered above while the user is scrolled down (not on screen).
|
|
t.observe(['e', 'd', 'c', 'b', 'a'], new Set(['a']));
|
|
assert.equal(t.count(), 2);
|
|
assert.equal(t.nextUnread(['e', 'd', 'c', 'b', 'a']), 'd', 'the one closest to what was read');
|
|
// Older posts loaded below are read.
|
|
t.observe(['e', 'd', 'c', 'b', 'a', 'z', 'y'], new Set());
|
|
assert.equal(t.count(), 2);
|
|
t.seen('d');
|
|
assert.equal(t.count(), 1);
|
|
// A new post rendered while on screen is read at once.
|
|
t.observe(['f', 'e', 'd', 'c'], new Set(['f']));
|
|
assert.equal(t.count(), 1);
|
|
assert.equal(t.isUnread('e'), true);
|
|
t.markRead();
|
|
assert.equal(t.count(), 0);
|
|
assert.equal(t.nextUnread(['f', 'e']), null);
|
|
});
|
|
|
|
test('unread: a new id below what is on screen is an older item, never unread', () => {
|
|
const t = createUnreadTracker();
|
|
t.observe(['a', 'b', 'c'], new Set(['a']));
|
|
// X re-keys or lazily renders an item further down while the top is on screen.
|
|
t.observe(['a', 'x', 'b', 'c'], new Set(['a']));
|
|
assert.equal(t.count(), 0);
|
|
// Scrolled down (c on screen), a new one on top is unread; one between the top and c is not.
|
|
t.observe(['n', 'a', 'y', 'b', 'c'], new Set(['c']));
|
|
assert.equal(t.isUnread('n'), true);
|
|
assert.equal(t.isUnread('y'), false);
|
|
});
|
|
|
|
test('unread: items rendered on screen again are marked read by observe', () => {
|
|
const t = createUnreadTracker();
|
|
t.observe(['a'], new Set());
|
|
t.observe(['b', 'a'], new Set());
|
|
assert.equal(t.count(), 1);
|
|
t.observe(['b', 'a'], new Set(['b']));
|
|
assert.equal(t.count(), 0);
|
|
});
|