|
| 1 | +/** |
| 2 | + * Tests for the Search blocks overlay bootstrap's initial-load URL trigger. |
| 3 | + * |
| 4 | + * The module runs side effects at import time (registers listeners and syncs |
| 5 | + * the overlay to the current URL), so each case sets up the DOM + URL + |
| 6 | + * `document.readyState`, then imports the module fresh via `jest.resetModules`. |
| 7 | + */ |
| 8 | + |
| 9 | +// `ensureHydrated()` dynamically imports this; stub it so the hydration branch |
| 10 | +// no-ops instead of reaching for the real Interactivity runtime in jsdom. |
| 11 | +jest.mock( '@wordpress/interactivity', () => ( {} ), { virtual: true } ); |
| 12 | + |
| 13 | +const OVERLAY_ID = 'jetpack-search-block-overlay'; |
| 14 | + |
| 15 | +/** |
| 16 | + * Render the server-side overlay shell (closed) into the document body. |
| 17 | + */ |
| 18 | +function renderOverlayShell() { |
| 19 | + document.body.innerHTML = ` |
| 20 | + <div id="${ OVERLAY_ID }" hidden> |
| 21 | + <div class="jetpack-search-block-overlay__content"></div> |
| 22 | + </div> |
| 23 | + <template id="jetpack-search-block-overlay-template"></template> |
| 24 | + `; |
| 25 | +} |
| 26 | + |
| 27 | +/** |
| 28 | + * Point `window.location` at the given path+query without a real navigation. |
| 29 | + * |
| 30 | + * @param {string} url - Path with optional query string, e.g. `/?s=foo`. |
| 31 | + */ |
| 32 | +function setUrl( url ) { |
| 33 | + window.history.replaceState( {}, '', url ); |
| 34 | +} |
| 35 | + |
| 36 | +/** |
| 37 | + * Override `document.readyState` for the duration of a test. |
| 38 | + * |
| 39 | + * @param {string} value - `'loading'`, `'interactive'`, or `'complete'`. |
| 40 | + */ |
| 41 | +function setReadyState( value ) { |
| 42 | + Object.defineProperty( document, 'readyState', { |
| 43 | + configurable: true, |
| 44 | + get: () => value, |
| 45 | + } ); |
| 46 | +} |
| 47 | + |
| 48 | +/** |
| 49 | + * Import the bootstrap fresh and let the fire-and-forget `openOverlay` settle. |
| 50 | + */ |
| 51 | +async function loadBootstrap() { |
| 52 | + jest.resetModules(); |
| 53 | + await import( '../index.js' ); |
| 54 | + // `handlePopState` → `openOverlay` awaits hydration before toggling `hidden`; |
| 55 | + // flush the microtask queue so the attribute change has landed. |
| 56 | + await new Promise( resolve => setTimeout( resolve, 0 ) ); |
| 57 | +} |
| 58 | + |
| 59 | +const isOpen = () => ! document.getElementById( OVERLAY_ID ).hasAttribute( 'hidden' ); |
| 60 | + |
| 61 | +afterEach( () => { |
| 62 | + setReadyState( 'complete' ); |
| 63 | + setUrl( '/' ); |
| 64 | + document.body.innerHTML = ''; |
| 65 | +} ); |
| 66 | + |
| 67 | +describe( 'overlay-bootstrap initial-load URL trigger', () => { |
| 68 | + it( 'opens the overlay on initial load when the URL has ?s=', async () => { |
| 69 | + setReadyState( 'complete' ); |
| 70 | + renderOverlayShell(); |
| 71 | + setUrl( '/?s=hello' ); |
| 72 | + |
| 73 | + await loadBootstrap(); |
| 74 | + |
| 75 | + expect( isOpen() ).toBe( true ); |
| 76 | + } ); |
| 77 | + |
| 78 | + it( 'opens the overlay on initial load when the URL has ?q=', async () => { |
| 79 | + setReadyState( 'complete' ); |
| 80 | + renderOverlayShell(); |
| 81 | + setUrl( '/?q=hello' ); |
| 82 | + |
| 83 | + await loadBootstrap(); |
| 84 | + |
| 85 | + expect( isOpen() ).toBe( true ); |
| 86 | + } ); |
| 87 | + |
| 88 | + it( 'leaves the overlay closed when the URL has no search param', async () => { |
| 89 | + setReadyState( 'complete' ); |
| 90 | + renderOverlayShell(); |
| 91 | + setUrl( '/' ); |
| 92 | + |
| 93 | + await loadBootstrap(); |
| 94 | + |
| 95 | + expect( isOpen() ).toBe( false ); |
| 96 | + } ); |
| 97 | + |
| 98 | + it( 'waits for DOMContentLoaded when the document is still loading', async () => { |
| 99 | + setReadyState( 'loading' ); |
| 100 | + renderOverlayShell(); |
| 101 | + setUrl( '/?s=hello' ); |
| 102 | + |
| 103 | + await loadBootstrap(); |
| 104 | + // Still parsing — the overlay must not open yet. |
| 105 | + expect( isOpen() ).toBe( false ); |
| 106 | + |
| 107 | + document.dispatchEvent( new Event( 'DOMContentLoaded' ) ); |
| 108 | + await new Promise( resolve => setTimeout( resolve, 0 ) ); |
| 109 | + |
| 110 | + expect( isOpen() ).toBe( true ); |
| 111 | + } ); |
| 112 | + |
| 113 | + it( 'is a no-op when the overlay shell is not rendered', async () => { |
| 114 | + setReadyState( 'complete' ); |
| 115 | + document.body.innerHTML = ''; |
| 116 | + setUrl( '/?s=hello' ); |
| 117 | + |
| 118 | + await expect( loadBootstrap() ).resolves.toBeUndefined(); |
| 119 | + expect( document.getElementById( OVERLAY_ID ) ).toBeNull(); |
| 120 | + } ); |
| 121 | +} ); |
0 commit comments