A calm place to write long-form, and publish it to the open social web.
skypress.blog/
1// src/lib/embeds/resolve.test.ts
2import { describe, expect, it, vi } from 'vitest';
3import { fetchAtprotoCard, fetchVideoCard, resolveEmbeds } from './resolve';
4import type { EmbedMatch } from './registry';
5
6function jsonResponse( body: unknown, ok = true ): Response {
7 return { ok, json: async () => body } as unknown as Response;
8}
9
10describe( 'fetchAtprotoCard', () => {
11 it( 'resolves a handle authority to a DID, then fetches the post', async () => {
12 const fetchImpl = vi
13 .fn()
14 .mockResolvedValueOnce( jsonResponse( { did: 'did:plc:abc' } ) ) // getProfile
15 .mockResolvedValueOnce(
16 jsonResponse( {
17 posts: [
18 {
19 uri: 'at://did:plc:abc/app.bsky.feed.post/xyz',
20 author: { handle: 'jeremy.herve.bzh', displayName: 'Jeremy', avatar: 'https://cdn/av.jpg' },
21 record: { text: 'Hi there', createdAt: '2026-06-19T10:00:00Z' },
22 embed: {
23 $type: 'app.bsky.embed.images#view',
24 images: [ { fullsize: 'https://cdn/i.jpg', alt: 'pic' } ],
25 },
26 },
27 ],
28 } )
29 );
30
31 const match: EmbedMatch = { kind: 'atproto', id: 'jeremy.herve.bzh/xyz' };
32 const card = await fetchAtprotoCard( match, fetchImpl );
33
34 expect( fetchImpl.mock.calls[ 0 ][ 0 ] ).toContain( 'app.bsky.actor.getProfile?actor=jeremy.herve.bzh' );
35 expect( fetchImpl.mock.calls[ 1 ][ 0 ] ).toContain(
36 'getPosts?uris=at%3A%2F%2Fdid%3Aplc%3Aabc%2Fapp.bsky.feed.post%2Fxyz'
37 );
38 expect( card ).toMatchObject( {
39 kind: 'atproto',
40 authorName: 'Jeremy',
41 handle: 'jeremy.herve.bzh',
42 text: 'Hi there',
43 images: [ { src: 'https://cdn/i.jpg', alt: 'pic' } ],
44 viewUrl: 'https://mu.social/profile/did:plc:abc/post/xyz',
45 } );
46 } );
47
48 it( 'reads images from a recordWithMedia quote-post embed', async () => {
49 const fetchImpl = vi.fn().mockResolvedValueOnce(
50 jsonResponse( {
51 posts: [
52 {
53 uri: 'at://did:plc:abc/app.bsky.feed.post/xyz',
54 author: { handle: 'a.bsky.social' },
55 record: { text: 'quoting' },
56 embed: {
57 $type: 'app.bsky.embed.recordWithMedia#view',
58 media: {
59 $type: 'app.bsky.embed.images#view',
60 images: [ { fullsize: 'https://cdn/q.jpg', alt: 'quoted pic' } ],
61 },
62 },
63 },
64 ],
65 } )
66 );
67 const card = await fetchAtprotoCard( { kind: 'atproto', id: 'did:plc:abc/xyz' }, fetchImpl );
68 expect( card?.images ).toEqual( [ { src: 'https://cdn/q.jpg', alt: 'quoted pic' } ] );
69 } );
70
71 it( 'skips profile resolution when the authority is already a DID', async () => {
72 const fetchImpl = vi.fn().mockResolvedValueOnce(
73 jsonResponse( {
74 posts: [ { uri: 'at://did:plc:abc/app.bsky.feed.post/xyz', author: { handle: 'a.bsky.social' }, record: { text: 't' } } ],
75 } )
76 );
77 await fetchAtprotoCard( { kind: 'atproto', id: 'did:plc:abc/xyz' }, fetchImpl );
78 expect( fetchImpl ).toHaveBeenCalledTimes( 1 );
79 expect( fetchImpl.mock.calls[ 0 ][ 0 ] ).toContain( 'getPosts' );
80 } );
81
82 it( 'returns null when the post is missing', async () => {
83 const fetchImpl = vi.fn().mockResolvedValue( jsonResponse( { posts: [] } ) );
84 expect( await fetchAtprotoCard( { kind: 'atproto', id: 'did:plc:abc/xyz' }, fetchImpl ) ).toBeNull();
85 } );
86
87 it( 'returns null on a thrown fetch', async () => {
88 const fetchImpl = vi.fn().mockRejectedValue( new Error( 'network' ) );
89 expect( await fetchAtprotoCard( { kind: 'atproto', id: 'did:plc:abc/xyz' }, fetchImpl ) ).toBeNull();
90 } );
91} );
92
93describe( 'fetchVideoCard', () => {
94 it( 'reads title + thumbnail from youtube oEmbed', async () => {
95 const fetchImpl = vi.fn().mockResolvedValue(
96 jsonResponse( { title: 'Cool', thumbnail_url: 'https://i.ytimg/x.jpg' } )
97 );
98 const card = await fetchVideoCard( { kind: 'youtube', id: 'dQw4w9WgXcQ' }, fetchImpl );
99 expect( card ).toEqual( {
100 kind: 'youtube',
101 id: 'dQw4w9WgXcQ',
102 title: 'Cool',
103 thumbnail: 'https://i.ytimg/x.jpg',
104 url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
105 } );
106 } );
107
108 it( 'returns null on oEmbed failure', async () => {
109 const fetchImpl = vi.fn().mockResolvedValue( jsonResponse( {}, false ) );
110 expect( await fetchVideoCard( { kind: 'vimeo', id: '1' }, fetchImpl ) ).toBeNull();
111 } );
112} );
113
114describe( 'resolveEmbeds', () => {
115 const blocks = [
116 { name: 'core/paragraph', attributes: { content: 'hello' } },
117 { name: 'core/embed', attributes: { url: 'https://vimeo.com/123' } },
118 ];
119
120 it( 'attaches resolved data onto recognised core/embed nodes', async () => {
121 const fetchImpl = vi.fn().mockResolvedValue( jsonResponse( { title: 'V', thumbnail_url: 'https://t/x.jpg' } ) );
122 const out = await resolveEmbeds( blocks, { fetchImpl } );
123 expect( out[ 0 ].attributes ).not.toHaveProperty( '_skypressEmbed' );
124 expect( out[ 1 ].attributes?._skypressEmbed ).toMatchObject( { kind: 'vimeo', title: 'V' } );
125 } );
126
127 it( 'leaves unrecognised embeds and other blocks untouched', async () => {
128 const fetchImpl = vi.fn();
129 const out = await resolveEmbeds(
130 [ { name: 'core/embed', attributes: { url: 'https://example.com/x' } } ],
131 { fetchImpl }
132 );
133 expect( fetchImpl ).not.toHaveBeenCalled();
134 expect( out[ 0 ].attributes ).not.toHaveProperty( '_skypressEmbed' );
135 } );
136
137 it( 'honours the `only` filter (RSS resolves atproto, not video)', async () => {
138 const fetchImpl = vi.fn();
139 const out = await resolveEmbeds( blocks, { only: [ 'atproto' ], fetchImpl } );
140 expect( fetchImpl ).not.toHaveBeenCalled();
141 expect( out[ 1 ].attributes ).not.toHaveProperty( '_skypressEmbed' );
142 } );
143} );