A calm place to write long-form, and publish it to the open social web. skypress.blog/
0

Configure Feed

Select the types of activity you want to include in your feed.

at trunk 5.5 kB View raw
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} );