import assert from 'node:assert/strict'; import { test } from 'node:test'; import { mediaFromEmbed, selfThread, toPost } from '../src/platforms/bluesky.ts'; import { fixture } from './helpers.ts'; const URL_ = 'https://bsky.app/profile/example/post/abc'; /** The three single-post fixtures are captured `thread.post` objects. */ function single(name: string) { return toPost({ post: fixture(`bluesky/${name}.json`) }, URL_); } test('a text-only post carries no media', () => { const post = single('text-only'); assert.equal(post.platform, 'bluesky'); assert.equal(post.textPosition, 'above'); assert.equal(post.segments.length, 1); assert.deepEqual(post.segments[0]?.media, []); assert.ok(post.segments[0]?.text); assert.match(post.author.handle, /^@/); }); test('an image post keeps every image, in order, with alt text', () => { const media = single('images').segments[0]?.media ?? []; assert.ok(media.length > 1, 'expected a multi-image post'); assert.ok(media.every((m) => m.kind === 'image')); assert.ok(media.every((m) => m.url.startsWith('https://'))); assert.ok(media.some((m) => m.kind === 'image' && m.alt)); }); test('a video post is a direct HLS playlist with a poster', () => { const item = single('video').segments[0]?.media[0]; assert.equal(item?.kind, 'video'); assert.ok(item?.kind === 'video' && item.hls); // Proxying HLS would mean rewriting the manifest, so it is linked direct. assert.ok(item?.direct); assert.ok(item?.kind === 'video' && item.poster); }); test('a quote post with media reaches through the nested embed', () => { const media = mediaFromEmbed({ $type: 'app.bsky.embed.recordWithMedia#view', media: { $type: 'app.bsky.embed.images#view', images: [{ fullsize: 'https://example/i.jpg' }] }, }); assert.deepEqual(media, [{ kind: 'image', url: 'https://example/i.jpg' }]); }); test('a quote post shows the media of the post it quotes', () => { const media = mediaFromEmbed({ $type: 'app.bsky.embed.record#view', record: { embeds: [{ $type: 'app.bsky.embed.images#view', images: [{ fullsize: 'https://cdn/q.jpg' }] }], }, }); assert.deepEqual(media, [{ kind: 'image', url: 'https://cdn/q.jpg' }]); }); // The chain fixture is a real thread. Other people's replies were trimmed to // two per level for size, but deliberately kept: the whole point is that they // are there in the payload and must not appear in the output. test("the author's own chain is followed, in order, and marked", () => { const post = toPost(fixture('bluesky/thread-chain.json'), URL_); assert.equal(post.segments.length, 6, 'anchor plus five follow-ups'); assert.equal(post.segments[0]?.isAnchor, true, 'the linked post leads the chain here'); assert.equal(post.segments.filter((s) => s.isAnchor).length, 1); const times = post.segments.map((s) => s.postedAt ?? ''); assert.deepEqual([...times].sort(), times, 'segments must be in the order written'); assert.match(post.segments[0]?.text ?? '', /redesigning the notifications tab/); assert.match(post.segments[5]?.text ?? '', /Let us know what works/); }); test("other people's replies never appear in the thread", () => { const chain = selfThread(fixture('bluesky/thread-chain.json')); const texts = chain.map((s) => s.text ?? '').join('\n'); // All present in the fixture as replies to the same posts. for (const stranger of ['Really? One of the most visited?', 'Show us notifications from muted', 'WHY ARE YOU NOT USING DARK MODE']) { assert.ok(!texts.includes(stranger), `a reply leaked into the thread: ${stranger}`); } }); test('a post with no thread around it is a single segment', () => { const chain = selfThread({ post: { uri: 'at://x/app.bsky.feed.post/1', author: { handle: 'someone.bsky.social' }, record: { text: 'alone' }, }, replies: [ { post: { uri: 'at://y/1', author: { handle: 'other.bsky.social' }, record: { text: 'hi' } } }, ], }); assert.equal(chain.length, 1); assert.equal(chain[0]?.isAnchor, true); }); test('a linked post mid-thread keeps the posts that came before it', () => { const chain = selfThread({ post: { uri: 'at://a/2', author: { handle: 'me.bsky.social' }, record: { text: 'second' } }, parent: { post: { uri: 'at://a/1', author: { handle: 'me.bsky.social' }, record: { text: 'first' } }, }, replies: [ { post: { uri: 'at://a/3', author: { handle: 'me.bsky.social' }, record: { text: 'third' } } }, { post: { uri: 'at://b/1', author: { handle: 'other.bsky.social' }, record: { text: 'nope' } } }, ], }); assert.deepEqual(chain.map((s) => s.text), ['first', 'second', 'third']); assert.deepEqual(chain.map((s) => s.isAnchor), [undefined, true, undefined]); }); test('the walk up stops at the first post by someone else', () => { const chain = selfThread({ post: { uri: 'at://a/2', author: { handle: 'me.bsky.social' }, record: { text: 'mine' } }, parent: { post: { uri: 'at://b/1', author: { handle: 'other.bsky.social' }, record: { text: 'theirs' } }, parent: { post: { uri: 'at://a/0', author: { handle: 'me.bsky.social' }, record: { text: 'older' } }, }, }, }); assert.deepEqual(chain.map((s) => s.text), ['mine']); });