Files
antisocial/test/render.test.ts
T
thatguygriffandClaude Opus 5 b5f9483615
CI / Typecheck, test, build (push) Successful in 28s
Publish / Build and push (push) Failing after 2m24s
Initial commit: read social posts back without the app
antisocial is the other half of a StopTheMadness redirect rule. Links to X,
Threads, Instagram, TikTok and Bluesky get rewritten to /<prefix>/<original
path>, and this resolves the post and shows the media and the words, with a
badge saying where it came from and a button to copy the original URL.

Every request drives a real headless Chromium, logged out, from a residential
IP. One code path, and it survives markup changes better than parsing from the
outside would. Extraction is layered, most structured first: the platform's own
API response caught in flight, then an inline payload, then the rendered DOM,
then Open Graph tags.

Media is never linked straight at a CDN. Instagram and TikTok reject requests
without a matching Referer and cookies, and proxying keeps the viewer's browser
from talking to the platform at all. Range is forwarded so the native video
scrubber can seek. HLS is the exception, since proxying it would mean rewriting
playlists.

TikTok sometimes answers with a slider puzzle. Rather than reporting that as a
failure, the page is parked and the viewer is handed the puzzle: screenshots
stream out, pointer events are replayed back. Solving it leaves the cookie in
the shared browser context, so the retry is an ordinary request.

A failed resolve is never a blank error page. The card carries the platform, the
original URL and the copy button, so a broken adapter still leaves the link one
tap away.

Verified end to end against real shared links on all five platforms, in the
container, including multi-image carousels, reels, TikTok short links and photo
posts. 49 tests run the adapters against captured payloads with no network.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BGkRmLfiWuJHx6tQ12EELY
2026-08-26 11:32:25 -03:00

105 lines
4.3 KiB
TypeScript

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 type { Post } from '../src/types.ts';
test('interpolations are escaped, Raw values are not', () => {
assert.equal(html`<p>${'<script>alert(1)</script>'}</p>`.value,
'<p>&lt;script&gt;alert(1)&lt;/script&gt;</p>');
assert.equal(html`<p>${raw('<b>ok</b>')}</p>`.value, '<p><b>ok</b></p>');
assert.equal(html`${['a', '<b>']}`.value, 'a&lt;b&gt;');
assert.equal(html`${undefined}${null}${false}`.value, '');
});
test('attribute-breaking characters are escaped', () => {
assert.equal(escapeHtml(`" onload='x'`), '&quot; onload=&#39;x&#39;');
});
test('post text is escaped before links are made', () => {
const out = linkify('<img src=x onerror=alert(1)> https://example.com/a', 'x').value;
assert.ok(!out.includes('<img'), 'markup in post text must not survive');
assert.ok(out.includes('<a href="https://example.com/a"'));
});
test('mentions and hashtags point back at the right platform', () => {
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('<a '));
assert.ok(linkify('#moon', 'instagram').value.includes('explore/tags/moon'));
});
test('newlines become line breaks', () => {
assert.equal(linkify('a\nb', 'x').value, 'a<br>b');
});
function post(overrides: Partial<Post> = {}): Post {
return {
platform: 'instagram',
platformLabel: 'Instagram',
originalUrl: 'https://www.instagram.com/p/ABC/',
author: { handle: '@nasa' },
text: 'caption',
textPosition: 'below',
media: [{ kind: 'image', url: 'https://cdn/1.jpg' }, { kind: 'image', url: 'https://cdn/2.jpg' }],
...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(post({
media: [{ 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(post({ media: [{ 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(post({ media: [{ kind: 'video', url: 'https://cdn/v.mp4' }] }));
assert.ok(page.includes('<video'));
assert.ok(page.includes('controls'));
assert.ok(page.includes('playsinline'));
assert.ok(!page.includes('<iframe'));
});
test('the copy button carries the clean original URL, and so does the page text', () => {
const page = renderPost(post());
assert.ok(page.includes('data-url="https://www.instagram.com/p/ABC/"'));
assert.ok(page.includes('<code>https://www.instagram.com/p/ABC/</code>'));
});
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('Open on TikTok'));
assert.ok(page.includes('verification puzzle'));
});