A calm place to write long-form, and publish it to the open social web.
skypress.blog/
1import { describe, it, expect, vi, beforeEach } from 'vitest';
2import { act, createElement } from 'react';
3import { createRoot } from 'react-dom/client';
4import type { CoverUpload } from '../lib/media/cover';
5import CoverImagePicker from './CoverImagePicker';
6
7( globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean } ).IS_REACT_ACT_ENVIRONMENT = true;
8
9function mount( props: {
10 cover: CoverUpload | null;
11 onUpload: ( file: File ) => Promise< CoverUpload >;
12 onChange: ( cover: CoverUpload | null ) => void;
13} ) {
14 const container = document.createElement( 'div' );
15 document.body.appendChild( container );
16 const root = createRoot( container );
17 act( () => {
18 root.render( createElement( CoverImagePicker, props ) );
19 } );
20 return {
21 container,
22 cleanup: () => {
23 root.unmount();
24 container.remove();
25 },
26 };
27}
28
29function setFiles( input: HTMLInputElement, files: File[] ) {
30 Object.defineProperty( input, 'files', { value: files, configurable: true } );
31 input.dispatchEvent( new Event( 'change', { bubbles: true } ) );
32}
33
34const onUpload = vi.fn< ( file: File ) => Promise< CoverUpload > >();
35const onChange = vi.fn();
36
37beforeEach( () => {
38 onUpload.mockReset();
39 onChange.mockReset();
40} );
41
42describe( 'CoverImagePicker', () => {
43 it( 'shows the empty state with the fallback hint and the 1 MB cap', () => {
44 const { container, cleanup } = mount( { cover: null, onUpload, onChange } );
45 expect( container.textContent ).toMatch( /first image in your article/i );
46 expect( container.textContent ).toMatch( /1 MB/i );
47 cleanup();
48 } );
49
50 it( 'renders a preview and a Remove control when a cover is set', () => {
51 const cover: CoverUpload = {
52 ref: { $type: 'blob', ref: { $link: 'bafyc' }, mimeType: 'image/png', size: 10 },
53 previewUrl: 'data:image/png;base64,AAA',
54 };
55 const { container, cleanup } = mount( { cover, onUpload, onChange } );
56 const img = container.querySelector( 'img' )!;
57 expect( img.getAttribute( 'src' ) ).toBe( 'data:image/png;base64,AAA' );
58 const remove = Array.from( container.querySelectorAll( 'button' ) ).find(
59 ( b ) => b.textContent === 'Remove'
60 )!;
61 act( () => {
62 remove.dispatchEvent( new MouseEvent( 'click', { bubbles: true } ) );
63 } );
64 expect( onChange ).toHaveBeenCalledWith( null );
65 cleanup();
66 } );
67
68 it( 'rejects an oversize file inline without uploading', () => {
69 const { container, cleanup } = mount( { cover: null, onUpload, onChange } );
70 const input = container.querySelector( 'input[type="file"]' ) as HTMLInputElement;
71 const big = new File( [ new Uint8Array( 1_000_001 ) ], 'big.png', { type: 'image/png' } );
72 act( () => {
73 setFiles( input, [ big ] );
74 } );
75 expect( container.textContent ).toMatch( /too large/i );
76 expect( onUpload ).not.toHaveBeenCalled();
77 cleanup();
78 } );
79
80 it( 'uploads a valid file and reports the result via onChange', async () => {
81 const result: CoverUpload = {
82 ref: { $type: 'blob', ref: { $link: 'bafynew' }, mimeType: 'image/png', size: 20 },
83 previewUrl: 'data:image/png;base64,BBB',
84 };
85 onUpload.mockResolvedValue( result );
86 const { container, cleanup } = mount( { cover: null, onUpload, onChange } );
87 const input = container.querySelector( 'input[type="file"]' ) as HTMLInputElement;
88 const ok = new File( [ 'x' ], 'ok.png', { type: 'image/png' } );
89 await act( async () => {
90 setFiles( input, [ ok ] );
91 } );
92 expect( onUpload ).toHaveBeenCalledTimes( 1 );
93 expect( onChange ).toHaveBeenCalledWith( result );
94 cleanup();
95 } );
96} );