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.

Lock RecordJsonViewer open/close/focus behaviour

+84
+84
src/components/RecordJsonViewer.test.tsx
··· 1 + import { describe, it, expect, afterEach } from 'vitest'; 2 + import { act, createElement } from 'react'; 3 + import { createRoot, type Root } from 'react-dom/client'; 4 + import RecordJsonViewer from './RecordJsonViewer'; 5 + 6 + // react-dom/client + act need this flag so React treats vitest's jsdom as a test env. 7 + ( globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean } ).IS_REACT_ACT_ENVIRONMENT = true; 8 + 9 + const HTML = 10 + '<code class="hljs language-json"><span class="hljs-attr">"title"</span>: <span class="hljs-string">"Hi"</span></code>'; 11 + 12 + let container: HTMLDivElement; 13 + let root: Root; 14 + 15 + async function mount() { 16 + container = document.createElement( 'div' ); 17 + document.body.appendChild( container ); 18 + root = createRoot( container ); 19 + await act( async () => { 20 + root.render( createElement( RecordJsonViewer, { highlightedHtml: HTML } ) ); 21 + } ); 22 + } 23 + 24 + afterEach( () => { 25 + act( () => root.unmount() ); 26 + container.remove(); 27 + } ); 28 + 29 + const trigger = () => container.querySelector( '.record-json__trigger' ) as HTMLButtonElement; 30 + const dialog = () => document.querySelector( '[role="dialog"]' ); 31 + 32 + describe( 'RecordJsonViewer interaction', () => { 33 + it( 'renders a labelled trigger and no dialog initially', async () => { 34 + await mount(); 35 + expect( trigger() ).not.toBeNull(); 36 + expect( trigger().getAttribute( 'aria-label' ) ).toBe( 'View record JSON' ); 37 + expect( dialog() ).toBeNull(); 38 + } ); 39 + 40 + it( 'opens the dialog with the highlighted JSON on click', async () => { 41 + await mount(); 42 + await act( async () => { 43 + trigger().click(); 44 + } ); 45 + expect( dialog() ).not.toBeNull(); 46 + expect( dialog()!.innerHTML ).toContain( 'hljs-string' ); 47 + } ); 48 + 49 + it( 'closes via the close button and restores focus to the trigger', async () => { 50 + await mount(); 51 + await act( async () => { 52 + trigger().click(); 53 + } ); 54 + const closeBtn = document.querySelector( '.record-json__close' ) as HTMLButtonElement; 55 + await act( async () => { 56 + closeBtn.click(); 57 + } ); 58 + expect( dialog() ).toBeNull(); 59 + expect( document.activeElement ).toBe( trigger() ); 60 + } ); 61 + 62 + it( 'closes when the backdrop is clicked', async () => { 63 + await mount(); 64 + await act( async () => { 65 + trigger().click(); 66 + } ); 67 + const backdrop = document.querySelector( '.record-json__backdrop' ) as HTMLElement; 68 + await act( async () => { 69 + backdrop.click(); 70 + } ); 71 + expect( dialog() ).toBeNull(); 72 + } ); 73 + 74 + it( 'closes on Escape', async () => { 75 + await mount(); 76 + await act( async () => { 77 + trigger().click(); 78 + } ); 79 + await act( async () => { 80 + document.dispatchEvent( new KeyboardEvent( 'keydown', { key: 'Escape' } ) ); 81 + } ); 82 + expect( dialog() ).toBeNull(); 83 + } ); 84 + } );