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;
6import SignInPanel from './SignInPanel';
7
8function mount( props: Record< string, unknown > ) {
9 const container = document.createElement( 'div' );
10 document.body.appendChild( container );
11 act( () => createRoot( container ).render( createElement( SignInPanel, props as never ) ) );
12 return container;
13}
14
15// React 18 tracks a controlled input's value via a private setter; assigning `.value`
16// directly then dispatching `input` is invisible to React and onChange never fires. Set
17// through the native prototype setter so React's tracker sees the change (standard idiom).
18const setInputValue = ( input: HTMLInputElement, value: string ) => {
19 const setter = Object.getOwnPropertyDescriptor(
20 window.HTMLInputElement.prototype,
21 'value'
22 )!.set!;
23 setter.call( input, value );
24};
25
26describe( 'SignInPanel', () => {
27 it( 'for-publish variant frames the CTA around publishing and links out to signup in a new tab', () => {
28 const c = mount( { forPublish: true, error: null, onSubmit: vi.fn() } );
29 expect( c.textContent?.toLowerCase() ).toContain( 'publish' );
30 const signup = c.querySelector( 'a[href*="mu.social"]' ) as HTMLAnchorElement | null;
31 expect( signup ).not.toBe( null );
32 // Opens in a new tab, hardened, and labelled so the new-tab behaviour is announced.
33 expect( signup!.target ).toBe( '_blank' );
34 expect( signup!.rel ).toContain( 'noopener' );
35 expect( signup!.getAttribute( 'aria-label' )?.toLowerCase() ).toContain( 'new tab' );
36 // Carries the usual external-link icon (not a bare "→").
37 expect( signup!.querySelector( 'svg' ) ).not.toBe( null );
38 expect( signup!.textContent ).not.toContain( '→' );
39 } );
40
41 it( 'submits the typed handle', () => {
42 const onSubmit = vi.fn();
43 const c = mount( { forPublish: false, error: null, onSubmit } );
44 const input = c.querySelector( 'input' )!;
45 const form = c.querySelector( 'form' )!;
46 act( () => {
47 setInputValue( input as HTMLInputElement, 'alice.bsky.social' );
48 input.dispatchEvent( new Event( 'input', { bubbles: true } ) );
49 } );
50 act( () => form.dispatchEvent( new Event( 'submit', { bubbles: true, cancelable: true } ) ) );
51 expect( onSubmit ).toHaveBeenCalledWith( 'alice.bsky.social' );
52 } );
53
54 it( 'shows an error when provided', () => {
55 const c = mount( { forPublish: false, error: 'Bad handle', onSubmit: vi.fn() } );
56 expect( c.textContent ).toContain( 'Bad handle' );
57 } );
58} );