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('