A calm place to write long-form, and publish it to the open social web.
skypress.blog/
1import { describe, it, expect, vi } from 'vitest';
2import { act, createElement } from 'react';
3import { createRoot } from 'react-dom/client';
4
5( globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean } ).IS_REACT_ACT_ENVIRONMENT = true;
6
7// Stub the heavy/irrelevant children with markers.
8vi.mock( './SkyEditor', () => ( {
9 default: () => createElement( 'div', { 'data-testid': 'sky-editor' } ),
10} ) );
11vi.mock( './CoverImagePicker', () => ( {
12 default: () => createElement( 'div', { 'data-testid': 'cover-picker' } ),
13} ) );
14
15import EditorCanvas from './EditorCanvas';
16
17const base = {
18 title: '',
19 onTitleChange: vi.fn(),
20 lede: '',
21 onLedeChange: vi.fn(),
22 onBlocksChange: vi.fn(),
23 cover: null,
24 onCoverChange: vi.fn(),
25};
26
27function mount( props: Record< string, unknown > ) {
28 const container = document.createElement( 'div' );
29 document.body.appendChild( container );
30 act( () => createRoot( container ).render( createElement( EditorCanvas, { ...base, ...props } as never ) ) );
31 return container;
32}
33
34// React 18 tracks a controlled value via a prototype setter; a direct `el.value = …`
35// is invisible to it, so set through the native setter to make onChange fire.
36function setValue( el: HTMLTextAreaElement, val: string ) {
37 const proto = Object.getPrototypeOf( el );
38 Object.getOwnPropertyDescriptor( proto, 'value' )!.set!.call( el, val );
39 el.dispatchEvent( new Event( 'input', { bubbles: true } ) );
40}
41
42describe( 'EditorCanvas', () => {
43 it( 'renders the title + lede fields and the block editor', () => {
44 const c = mount( {} );
45 expect( c.querySelector( 'textarea.studio__title' ) ).not.toBe( null );
46 expect( c.querySelector( 'textarea.studio__lede' ) ).not.toBe( null );
47 expect( c.querySelector( '[data-testid="sky-editor"]' ) ).not.toBe( null );
48 } );
49
50 it( 'reports title and lede edits to the parent', () => {
51 const onTitleChange = vi.fn();
52 const onLedeChange = vi.fn();
53 const c = mount( { onTitleChange, onLedeChange } );
54 setValue( c.querySelector( 'textarea.studio__title' )!, 'My title' );
55 setValue( c.querySelector( 'textarea.studio__lede' )!, 'My lede' );
56 expect( onTitleChange ).toHaveBeenCalledWith( 'My title' );
57 expect( onLedeChange ).toHaveBeenCalledWith( 'My lede' );
58 } );
59
60 it( 'shows the Bluesky-truncation hint only for a long lede', () => {
61 expect( mount( { lede: 'short' } ).querySelector( '.studio__lede-hint' ) ).toBe( null );
62 expect( mount( { lede: 'x'.repeat( 201 ) } ).querySelector( '.studio__lede-hint' ) ).not.toBe( null );
63 } );
64
65 it( 'shows the cover picker only when an upload handler is provided', () => {
66 expect( mount( {} ).querySelector( '[data-testid="cover-picker"]' ) ).toBe( null );
67 expect(
68 mount( { onCoverUpload: vi.fn() } ).querySelector( '[data-testid="cover-picker"]' )
69 ).not.toBe( null );
70 } );
71} );