A calm place to write long-form, and publish it to the open social web.
skypress.blog/
1import { describe, expect, it } from 'vitest';
2import { assemblePostText } from './post-text';
3
4const URL = 'https://skypress.blog/@me/pub/3kabcde12345';
5
6function bytes( s: string ): number {
7 return new TextEncoder().encode( s ).length;
8}
9
10describe( 'assemblePostText', () => {
11 it( 'with no lede or mentions, is title + URL (back-compat)', () => {
12 const { text, facets } = assemblePostText( { title: 'Hello', articleUrl: URL } );
13 expect( text ).toBe( `Hello\n\n${ URL }` );
14 expect( facets ).toHaveLength( 1 );
15 const link = facets[ 0 ];
16 expect( link.features[ 0 ] ).toEqual( {
17 $type: 'app.bsky.richtext.facet#link',
18 uri: URL,
19 } );
20 expect( link.index.byteStart ).toBe( bytes( 'Hello\n\n' ) );
21 expect( link.index.byteEnd ).toBe( bytes( 'Hello\n\n' ) + bytes( URL ) );
22 } );
23
24 it( 'includes the lede between title and URL when provided', () => {
25 const { text } = assemblePostText( {
26 title: 'Hello',
27 articleUrl: URL,
28 bodyLede: 'A short lede.',
29 } );
30 expect( text ).toBe( `Hello\n\nA short lede.\n\n${ URL }` );
31 } );
32
33 it( 'adds a cc line with a mention facet per handle', () => {
34 const { text, facets } = assemblePostText( {
35 title: 'Hi',
36 articleUrl: URL,
37 mentions: [
38 { did: 'did:plc:a', handle: 'alice.bsky.social', displayText: '@alice.bsky.social' },
39 { did: 'did:plc:b', handle: 'bob.bsky.social', displayText: '@bob.bsky.social' },
40 ],
41 } );
42 expect( text ).toBe( `Hi\n\ncc @alice.bsky.social @bob.bsky.social\n\n${ URL }` );
43
44 const mentions = facets.filter(
45 ( f ) => f.features[ 0 ].$type === 'app.bsky.richtext.facet#mention'
46 );
47 expect( mentions ).toHaveLength( 2 );
48
49 const ccPrefix = 'Hi\n\ncc ';
50 const aliceStart = bytes( ccPrefix );
51 expect( mentions[ 0 ].index ).toEqual( {
52 byteStart: aliceStart,
53 byteEnd: aliceStart + bytes( '@alice.bsky.social' ),
54 } );
55 expect( mentions[ 0 ].features[ 0 ] ).toEqual( {
56 $type: 'app.bsky.richtext.facet#mention',
57 did: 'did:plc:a',
58 } );
59 } );
60
61 it( 'computes correct byte offsets with a multibyte title (emoji)', () => {
62 const title = '🚀 Launch';
63 const { text, facets } = assemblePostText( {
64 title,
65 articleUrl: URL,
66 mentions: [
67 { did: 'did:plc:a', handle: 'alice.bsky.social', displayText: '@alice.bsky.social' },
68 ],
69 } );
70 const mention = facets.find(
71 ( f ) => f.features[ 0 ].$type === 'app.bsky.richtext.facet#mention'
72 )!;
73 const prefix = `${ title }\n\ncc `;
74 expect( mention.index.byteStart ).toBe( bytes( prefix ) );
75 const buf = new TextEncoder().encode( text );
76 const slice = new TextDecoder().decode(
77 buf.slice( mention.index.byteStart, mention.index.byteEnd )
78 );
79 expect( slice ).toBe( '@alice.bsky.social' );
80 } );
81
82 it( 'computes correct byte offsets for a multibyte handle', () => {
83 const handle = 'señor.bsky.social';
84 const { text, facets } = assemblePostText( {
85 title: 'Hi',
86 articleUrl: URL,
87 mentions: [ { did: 'did:plc:s', handle, displayText: `@${ handle }` } ],
88 } );
89 const mention = facets.find(
90 ( f ) => f.features[ 0 ].$type === 'app.bsky.richtext.facet#mention'
91 )!;
92 const buf = new TextEncoder().encode( text );
93 const slice = new TextDecoder().decode(
94 buf.slice( mention.index.byteStart, mention.index.byteEnd )
95 );
96 expect( slice ).toBe( `@${ handle }` );
97 } );
98} );