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.

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} );