diff --git a/readme.txt b/readme.txt index 957c62f..987dc43 100644 --- a/readme.txt +++ b/readme.txt @@ -4,7 +4,7 @@ Tags: asciicast, asciiplayer, asciinema, terminal, block Requires at least: 6.3 Requires PHP: 7.4 Stable tag: 1.1.0 -License: GPLv3 +License: GPL-3.0-only License URI: https://www.gnu.org/licenses/gpl-3.0.html Play terminal recordings with a native block, real editor playback previews, and compatible shortcodes. diff --git a/scripts/verify-build.mjs b/scripts/verify-build.mjs index 3e1d3f0..7fad113 100644 --- a/scripts/verify-build.mjs +++ b/scripts/verify-build.mjs @@ -1,5 +1,7 @@ import { readFile } from 'node:fs/promises'; import { createHash } from 'node:crypto'; +import { runInNewContext } from 'node:vm'; +import assert from 'node:assert/strict'; const hash = (data) => createHash('sha256').update(data).digest('hex'); const asset = await readFile('build/index.asset.php', 'utf8'); if (asset.includes('react-jsx-runtime')) throw new Error('Automatic JSX runtime would break the WordPress 6.3 baseline.'); @@ -14,4 +16,38 @@ for (const [destination, original] of [ } const metadata = JSON.parse(await readFile('build/block.json', 'utf8')); if (metadata.apiVersion !== 3 || metadata.name !== 'lamgc/asciiplayer') throw new Error('Unexpected block metadata.'); -console.log('Verified WordPress 6.3-compatible JSX dependencies, block metadata, and exact local vendor files.'); +// Execute the emitted bundle too: source-only tests miss minifier scope errors. +// In particular, never shadow the global document used by mountPlayers' default. +const view = await readFile('build/view.js', 'utf8'); +for (const readyState of ['loading', 'complete']) { + let mounts = 0; + let onReady; + const container = { + dataset: { apSrc: '/recording.cast', apOptions: '{"preload":false}' }, + }; + const document = { + readyState, + body: { classList: { contains: () => false } }, + querySelector: () => null, + querySelectorAll: () => [container], + addEventListener(event, callback) { + assert.equal(event, 'DOMContentLoaded'); + onReady = callback; + }, + defaultView: { AsciinemaPlayer: { create(source, target) { + assert.equal(source, '/recording.cast'); + assert.equal(target, container); + mounts++; + return { addEventListener() {} }; + } } }, + }; + runInNewContext(view, { document, window: { wp: { i18n: { __: (text) => text } } }, URL }); + if (readyState === 'loading') { + assert.equal(mounts, 0); + onReady(); + onReady(); // A repeated boot must not mount the same container twice. + } + assert.equal(mounts, 1, `Frontend boot failed at readyState=${readyState}`); + assert.equal(container.dataset.apStatus, 'idle'); +} +console.log('Verified WordPress 6.3 dependencies, block metadata, exact vendor files, and emitted frontend boot.'); diff --git a/src/view.js b/src/view.js index d71a02a..21a987f 100644 --- a/src/view.js +++ b/src/view.js @@ -15,9 +15,9 @@ function showError( container ) { /** One explicit instance per front-end container; never scan an editor canvas. */ export function mountPlayers( root = document ) { - const document = root.ownerDocument || root; - if ( document.body?.classList.contains( 'wp-admin' ) || document.querySelector( '.editor-styles-wrapper, .block-editor-block-list__layout' ) ) return; - const api = document.defaultView?.AsciinemaPlayer; + const ownerDocument = root.ownerDocument || root; + if ( ownerDocument.body?.classList.contains( 'wp-admin' ) || ownerDocument.querySelector( '.editor-styles-wrapper, .block-editor-block-list__layout' ) ) return; + const api = ownerDocument.defaultView?.AsciinemaPlayer; if ( typeof api?.create !== 'function' ) return; root.querySelectorAll( '.asciiplayer-container' ).forEach( ( container ) => { if ( instances.has( container ) ) return; @@ -45,10 +45,10 @@ export function mountPlayers( root = document ) { // by its parsed duration rather than merely by a mounted DOM wrapper. if ( options.preload || options.autoPlay || options.poster?.startsWith( 'npt:' ) ) { let ticks = 0; - const timer = document.defaultView.setInterval( () => { + const timer = ownerDocument.defaultView.setInterval( () => { const duration = player.getDuration(); if ( typeof duration === 'number' && Number.isFinite( duration ) ) container.dataset.apStatus = 'ready'; - if ( ! container.isConnected || container.dataset.apStatus !== 'loading' || ++ticks > 200 ) document.defaultView.clearInterval( timer ); + if ( ! container.isConnected || container.dataset.apStatus !== 'loading' || ++ticks > 200 ) ownerDocument.defaultView.clearInterval( timer ); }, 150 ); } else { container.dataset.apStatus = 'idle';