A calm place to write long-form, and publish it to the open social web.
skypress.blog/
1// @vitest-environment node
2// Pure module — no DOM needed. Node's global Blob exposes `.text()`, which the
3// repo's pinned jsdom Blob does not, so run this file under the node environment.
4import { describe, it, expect } from 'vitest';
5import {
6 isDataUrl,
7 dataUrlToBlob,
8 splitAssets,
9 mergeAssets,
10} from './held-assets';
11import type { BlockNode } from '../blocks/render';
12
13const DATA_A = 'data:image/png;base64,QUFB'; // "AAA"
14const DATA_B = 'data:image/jpeg;base64,QkJC'; // "BBB"
15
16function tree(): BlockNode[] {
17 return [
18 { name: 'core/paragraph', attributes: { content: 'hi' }, innerBlocks: [] },
19 { name: 'core/image', attributes: { url: DATA_A }, innerBlocks: [] },
20 {
21 name: 'core/columns',
22 attributes: {},
23 innerBlocks: [
24 { name: 'core/image', attributes: { url: DATA_B }, innerBlocks: [] },
25 { name: 'core/image', attributes: { url: 'https://ext/img.png' }, innerBlocks: [] },
26 ],
27 },
28 ];
29}
30
31describe( 'isDataUrl', () => {
32 it( 'matches data: URLs only', () => {
33 expect( isDataUrl( DATA_A ) ).toBe( true );
34 expect( isDataUrl( 'https://x/y.png' ) ).toBe( false );
35 expect( isDataUrl( undefined ) ).toBe( false );
36 } );
37} );
38
39describe( 'dataUrlToBlob', () => {
40 it( 'reconstructs bytes + mime from a data: URL', async () => {
41 const blob = dataUrlToBlob( DATA_A );
42 expect( blob.type ).toBe( 'image/png' );
43 expect( await blob.text() ).toBe( 'AAA' );
44 } );
45} );
46
47describe( 'splitAssets / mergeAssets', () => {
48 it( 'extracts every data: image URL (depth-first) + cover into tokens and round-trips', () => {
49 const { skeleton, assets } = splitAssets( tree(), DATA_A );
50
51 // Body data URLs replaced by tokens; external + non-image untouched.
52 expect( skeleton.blocks[ 1 ].attributes!.url ).toBe( 'a0' );
53 expect( skeleton.blocks[ 2 ].innerBlocks![ 0 ].attributes!.url ).toBe( 'a1' );
54 expect( skeleton.blocks[ 2 ].innerBlocks![ 1 ].attributes!.url ).toBe( 'https://ext/img.png' );
55 expect( skeleton.cover ).toBe( 'cover' );
56 expect( assets ).toEqual( { a0: DATA_A, a1: DATA_B, cover: DATA_A } );
57
58 const merged = mergeAssets( skeleton, assets );
59 expect( merged.blocks ).toEqual( tree() );
60 expect( merged.coverDataUrl ).toBe( DATA_A );
61 } );
62
63 it( 'drops a dangling token (held bytes missing) but keeps external image URLs', () => {
64 // localStorage skeleton survived but the IndexedDB bytes were evicted, so the
65 // `a0` token has no entry in `assets`. The merged image must not keep `a0` as a
66 // broken src; an external image alongside it stays untouched.
67 const skeleton = {
68 blocks: [
69 { name: 'core/image', attributes: { url: 'a0', alt: 'gone' }, innerBlocks: [] },
70 { name: 'core/image', attributes: { url: 'https://ext/x.png' }, innerBlocks: [] },
71 ],
72 cover: 'cover',
73 };
74 const merged = mergeAssets( skeleton, {} );
75 expect( merged.blocks[ 0 ].attributes ).toEqual( { alt: 'gone' } );
76 expect( merged.blocks[ 1 ].attributes!.url ).toBe( 'https://ext/x.png' );
77 expect( merged.coverDataUrl ).toBe( null );
78 } );
79
80 it( 'leaves a null cover null and a tree with no data URLs unchanged', () => {
81 const plain: BlockNode[] = [
82 { name: 'core/image', attributes: { url: 'https://ext/x.png' }, innerBlocks: [] },
83 ];
84 const { skeleton, assets } = splitAssets( plain, null );
85 expect( assets ).toEqual( {} );
86 expect( skeleton.cover ).toBe( null );
87 expect( mergeAssets( skeleton, assets ) ).toEqual( { blocks: plain, coverDataUrl: null } );
88 } );
89} );