import assert from 'node:assert/strict'; import { test } from 'node:test'; import { escapeHtml, html, raw } from '../src/render/html.ts'; import { linkify } from '../src/render/text.ts'; import { renderPost } from '../src/render/post.ts'; import { renderError } from '../src/render/error.ts'; import { oneSegment, type Media, type Post } from '../src/types.ts'; test('interpolations are escaped, Raw values are not', () => { assert.equal(html`

${''}

`.value, '

<script>alert(1)</script>

'); assert.equal(html`

${raw('ok')}

`.value, '

ok

'); assert.equal(html`${['a', '']}`.value, 'a<b>'); assert.equal(html`${undefined}${null}${false}`.value, ''); }); test('attribute-breaking characters are escaped', () => { assert.equal(escapeHtml(`" onload='x'`), '" onload='x''); }); test('post text is escaped before links are made', () => { const out = linkify(' https://example.com/a', 'x').value; assert.ok(!out.includes(' { assert.ok(linkify('hi @nasa', 'tiktok').value.includes('https://www.tiktok.com/@nasa')); assert.ok(linkify('hi @nasa', 'bluesky').value.includes('https://bsky.app/profile/nasa')); // Threads has no hashtag pages, so a tag stays plain text there. assert.ok(!linkify('#moon', 'threads').value.includes(' { assert.equal(linkify('a\nb', 'x').value, 'a
b'); }); function post(overrides: Partial = {}): Post { return { platform: 'instagram', platformLabel: 'Instagram', originalUrl: 'https://www.instagram.com/p/ABC/', author: { handle: '@nasa' }, textPosition: 'below', segments: oneSegment({ text: 'caption', media: [{ kind: 'image', url: 'https://cdn/1.jpg' }, { kind: 'image', url: 'https://cdn/2.jpg' }], }), ...overrides, }; } function withMedia(media: Media[], overrides: Partial = {}): Post { return post({ segments: oneSegment({ text: 'caption', media }), ...overrides }); } test('media is proxied, never linked straight at the CDN', () => { const page = renderPost(post()); assert.ok(!page.includes('https://cdn/1.jpg'), 'upstream URLs must not reach the page'); assert.equal((page.match(/src="\/m\//g) ?? []).length, 2); }); test('an HLS video is linked directly, because a proxy cannot rewrite a playlist', () => { const page = renderPost(withMedia([ { kind: 'video', url: 'https://video.bsky.app/x/playlist.m3u8', hls: true, direct: true }, ])); assert.ok(page.includes('https://video.bsky.app/x/playlist.m3u8')); }); test('text sits below the media for Instagram and above it for X', () => { const below = renderPost(post()); assert.ok(below.indexOf('class="media"') < below.indexOf('class="text"')); const above = renderPost(post({ platform: 'x', platformLabel: 'X', textPosition: 'above' })); assert.ok(above.indexOf('class="text"') < above.indexOf('class="media"')); }); test('the layout toggle only appears when there is more than one item', () => { assert.ok(renderPost(post()).includes('data-view="grid"')); assert.ok(!renderPost(withMedia([{ kind: 'image', url: 'https://cdn/1.jpg' }])) .includes('data-view="grid"')); }); test('a video gets native controls and a source, not an iframe', () => { const page = renderPost(withMedia([{ kind: 'video', url: 'https://cdn/v.mp4' }])); assert.ok(page.includes(' { const page = renderPost(post()); assert.ok(page.includes('data-url="https://www.instagram.com/p/ABC/"')); assert.ok(page.includes('https://www.instagram.com/p/ABC/')); }); test('a failure still hands the link back', () => { const page = renderError({ platform: 'tiktok', platformLabel: 'TikTok', originalUrl: 'https://www.tiktok.com/@a/video/1', heading: 'Could not read that TikTok post', detail: 'TikTok showed a verification puzzle instead of the post.', }); assert.ok(page.includes('data-url="https://www.tiktok.com/@a/video/1"')); assert.ok(page.includes('https://www.tiktok.com/@a/video/1')); assert.ok(page.includes('verification puzzle')); // Following the original link in the browser the rewrite rules are // installed in only comes back here, so it stays hidden until the script // has a browser to hand it to. assert.ok(/
]* hidden>Open on TikTok<\/a>/.test(page)); }); function redditPost(overrides: Partial = {}): Post { return post({ platform: 'reddit', platformLabel: 'Reddit', originalUrl: 'https://www.reddit.com/r/aww/comments/abc/a/', author: { handle: 'r/aww' }, textPosition: 'above', segments: oneSegment({ title: 'A headline', media: [] }), ...overrides, }); } test('a post with no comments renders no comment section at all', () => { assert.ok(!renderPost(redditPost()).includes('class="thread"')); assert.ok(!renderPost(post()).includes('class="thread"')); }); test('comments nest, so collapsing one takes its replies with it', () => { const page = renderPost(redditPost({ comments: [ { author: 'u/a', text: 'top', replies: [{ author: 'u/b', text: 'nested', replies: [] }], }, ], })); // The reply is inside the parent's
, not a sibling of it. const parent = page.slice(page.indexOf('
'); assert.ok(parent.slice(0, closing).includes('nested'), 'a reply must live inside its parent'); assert.ok(page.includes('
{ const page = renderPost(redditPost({ comments: [ { author: 'u/a', text: 'top', replies: [ { author: 'u/b', text: 'one', replies: [{ author: 'u/c', text: 'two', replies: [] }] }, ], }, ], })); assert.ok(page.includes('+2 replies'), 'the whole subtree is counted, not just direct replies'); }); test('comment text is escaped, and its markdown is rendered', () => { const page = renderPost(redditPost({ comments: [{ author: 'u/a', text: '**bold** ', replies: [] }], })); assert.ok(!page.includes('bold')); }); test('what the first page did not carry is counted rather than pretended away', () => { const page = renderPost(redditPost({ comments: [{ author: 'u/a', text: 'x', replies: [], moreReplies: 3 }], moreComments: 40, commentCount: 443, })); assert.ok(page.includes('3 more replies, on Reddit')); assert.ok(page.includes('40 more, behind')); assert.ok(page.includes('1 of 443')); }); function quoting(quoted: Post['segments'][number]['quoted']): Post { return post({ platform: 'x', platformLabel: 'X', textPosition: 'above', segments: oneSegment({ text: 'look at this', media: [], quoted }), }); } test('a quoted post is drawn with its own author, not folded under the quoter', () => { const page = quoting({ author: { handle: '@other', displayName: 'Someone Else' }, text: 'the original words', media: [], url: 'https://x.com/other/status/9', }); const html_ = renderPost(page); assert.ok(html_.includes('class="quote"')); assert.ok(html_.includes('@other'), 'the quoted handle must be on the page'); assert.ok(html_.includes('Someone Else')); assert.ok(html_.includes('the original words')); assert.ok(html_.includes('href="https://x.com/other/status/9"')); }); test('a quoted post with no quote renders no block', () => { assert.ok(!renderPost(quoting(undefined)).includes('class="quote"')); }); test("the quoted post's media is proxied like any other", () => { const html_ = renderPost(quoting({ author: { handle: '@other' }, media: [{ kind: 'image', url: 'https://pbs.twimg.com/q.jpg' }], })); assert.ok(!html_.includes('https://pbs.twimg.com/q.jpg'), 'upstream URLs must not reach the page'); assert.ok(html_.includes('src="/m/')); }); test('a quoted post is separated from the words that quote it', () => { const html_ = renderPost(quoting({ author: { handle: '@other' }, text: 'quoted words', media: [], })); // The quoter's text closes before the block opens, so the two can never // read as one paragraph by the same person. assert.ok(html_.indexOf('look at this') < html_.indexOf('class="quote"')); assert.ok(html_.indexOf('class="quote"') < html_.indexOf('quoted words')); }); test('markup in a quoted post is escaped like any other stranger\'s text', () => { const html_ = renderPost(quoting({ author: { handle: '' }, text: '', media: [], })); assert.ok(!html_.includes('')); assert.ok(!html_.includes(' { const page = renderPost(redditPost({ comments: [{ author: 'u/a', text: '![" onerror=alert(1) x="](https://preview.redd.it/x.jpeg?s=abc)\n\nhttps://i.redd.it/y.png', replies: [], }], })); assert.ok(!page.includes('https://preview.redd.it/x.jpeg'), 'upstream URLs must not reach the page'); assert.ok(!page.includes('https://i.redd.it/y.png'), 'a pasted address is proxied too'); assert.equal((page.match(/