diff --git a/readme.txt b/readme.txt index 84b4dff4..c274f6de 100755 --- a/readme.txt +++ b/readme.txt @@ -139,6 +139,7 @@ Release date: 11th August 2026 **Fixes** +* [#618](https://github.com/beyondwords-io/wordpress-plugin/pull/618) Poll content status before embedding the block-editor Preview panel player, so a still-processing post (more likely with a customised voice/model) no longer shows a broken preview. * [#613](https://github.com/beyondwords-io/wordpress-plugin/pull/613) Fix author names with an ampersand showing as `Smith & Sons` in the BeyondWords dashboard. * [#612](https://github.com/beyondwords-io/wordpress-plugin/pull/612) Fix tags with an ampersand showing as `r&d` in the BeyondWords dashboard. * [#610](https://github.com/beyondwords-io/wordpress-plugin/pull/610) Stop the bulk "Generate audio" notice counting skipped posts as failures. diff --git a/src/editor/components/play-audio/hooks.js b/src/editor/components/play-audio/hooks.js index a4ed5a21..263ad6fd 100644 --- a/src/editor/components/play-audio/hooks.js +++ b/src/editor/components/play-audio/hooks.js @@ -5,7 +5,7 @@ */ import apiFetch from '@wordpress/api-fetch'; import { useSelect } from '@wordpress/data'; -import { useEffect, useRef, useState } from '@wordpress/element'; +import { useEffect, useState } from '@wordpress/element'; /** * Internal dependencies @@ -65,8 +65,12 @@ export function useBeyondWordsNamespace() { /** * Create a (preview) BeyondWords player once its content is ready. * - * A contentId that appears during this session may still be processing, so it - * polls until `processed` before embedding (see lib/poll-content-status.js). + * Always confirms `processed` before embedding a contentId, whether it just + * appeared this session or was already on the post when the editor opened — + * a just-published, still-processing post (e.g. a slower voice/model + * override) is otherwise indistinguishable from a long-finished one. Mirrors + * the classic editor metabox, which polls unconditionally for the same + * reason. See lib/poll-content-status.js. * * @param {Object} options Options. * @param {HTMLElement} options.target Player mount node. @@ -94,10 +98,6 @@ export function useBeyondWordsPlayer( { timedOut: false, } ); - // Content that existed at mount finished processing long ago and embeds - // immediately; only a contentId appearing during this session polls first. - const mountContentIdRef = useRef( contentId ); - useEffect( () => { if ( ! BeyondWords?.Player || ! target ) { setPlayer( null ); @@ -145,9 +145,9 @@ export function useBeyondWordsPlayer( { setPlayer( newPlayer ); }; - if ( contentId && contentId !== mountContentIdRef.current ) { - // Session-fresh content: poll until processed, then embed, so a 404 - // is never CDN-cached for still-processing content. + if ( contentId ) { + // Poll until processed, then embed, so a 404 is never CDN-cached + // for content that's still processing. setPollState( { status: undefined, isPolling: true, @@ -187,8 +187,8 @@ export function useBeyondWordsPlayer( { // Aborted (unmount or dependency change) — nothing to do. } ); } else { - // Already-processed content (present at mount) or client-side - // integration (keyed on sourceId): nothing to poll, embed now. + // Client-side integration, keyed on sourceId: nothing to poll, + // embed now. setPollState( { status: undefined, isPolling: false, diff --git a/tests/cypress/e2e/block-editor/preview-panel.cy.js b/tests/cypress/e2e/block-editor/preview-panel.cy.js index 2ba23c02..f8224917 100644 --- a/tests/cypress/e2e/block-editor/preview-panel.cy.js +++ b/tests/cypress/e2e/block-editor/preview-panel.cy.js @@ -1,19 +1,41 @@ /** * @group block-editor - * @covers src/editor/components/preview-panel/,src/editor/components/error-notice/ + * @covers src/editor/components/preview-panel/,src/editor/components/error-notice/,src/editor/components/play-audio/ */ -/* global cy, beforeEach, context, it */ +/* global Cypress, cy, beforeEach, context, it, expect */ + +const PLAYER_SCRIPT_SRC = + 'https://proxy.beyondwords.io/npm/@beyondwords/player@latest/dist/umd.js'; + +/** + * Stub the BeyondWords player SDK instead of loading the real CDN script. + * + * The real script's load time isn't deterministic in CI, which is why the + * poll-before-embed behaviour (src/editor/components/play-audio/hooks.js) + * wasn't covered here before; stubbing it removes that non-determinism so + * `window.BeyondWords.Player` calls can be asserted on directly. + */ +function stubPlayerScript() { + cy.intercept( 'GET', PLAYER_SCRIPT_SRC, { + headers: { 'content-type': 'application/javascript' }, + body: ` + window.__beyondwordsPlayerCalls = []; + window.BeyondWords = { + Player: function ( params ) { + window.__beyondwordsPlayerCalls.push( params ); + this.destroy = function () {}; + }, + }; + `, + } ).as( 'playerScript' ); +} context( 'Block Editor: Preview panel', () => { beforeEach( () => { cy.login(); } ); - // Note: the block-editor player-preview poll is deliberately not covered - // here. It only starts once the CDN player SDK global is available, which - // isn't deterministic in CI, so a block spinner assertion is flaky. - it( 'shows a BeyondWords error message in the Preview panel', () => { cy.createTestPost( { title: 'Cypress Test: preview panel error', @@ -36,4 +58,126 @@ context( 'Block Editor: Preview panel', () => { .should( 'contain', 'Cypress preview error' ); } ); } ); + + it( 'waits for a still-processing voice-customised preview instead of embedding it early', () => { + const projectId = Cypress.expose( 'projectId' ); + const contentId = 'cypress-voice-preview-still-processing'; + + cy.createTestPost( { + title: 'Cypress Test: preview panel voice customised, still processing', + status: 'publish', + postType: 'post', + } ).then( ( postId ) => { + cy.task( 'setPostMeta', { + postId, + metaKey: 'beyondwords_project_id', + metaValue: projectId, + } ); + cy.task( 'setPostMeta', { + postId, + metaKey: 'beyondwords_content_id', + metaValue: contentId, + } ); + // Voice customisation: a non-default voice/model can take longer to + // process than the project default, widening the window where the + // content is still processing when the editor is opened. + cy.task( 'setPostMeta', { + postId, + metaKey: 'beyondwords_language_code', + metaValue: 'en_US', + } ); + cy.task( 'setPostMeta', { + postId, + metaKey: 'beyondwords_body_voice_id', + metaValue: '9001', + } ); + + cy.intercept( + 'GET', + // apiFetch appends `_locale`, hence the trailing wildcard. + `**/beyondwords/v1/projects/${ projectId }/content/${ contentId }*`, + { statusCode: 200, body: { status: 'processing' } } + ).as( 'statusCheck' ); + + stubPlayerScript(); + + cy.visitPostEditorById( postId ); + cy.openBeyondwordsPluginSidebar(); + + cy.wait( '@statusCheck' ); + + cy.get( '.beyondwords-sidebar__preview' ) + .find( '.beyondwords-player-loading' ) + .should( 'contain', 'Generating' ); + + // The still-processing content must never reach the player SDK — + // embedding it would 404 (and the CDN would cache that 404). + cy.window() + .its( '__beyondwordsPlayerCalls' ) + .should( 'have.length', 0 ); + } ); + } ); + + it( 'embeds a voice-customised preview once it has finished processing', () => { + const projectId = Cypress.expose( 'projectId' ); + const contentId = 'cypress-voice-preview-processed'; + + cy.createTestPost( { + title: 'Cypress Test: preview panel voice customised, processed', + status: 'publish', + postType: 'post', + } ).then( ( postId ) => { + cy.task( 'setPostMeta', { + postId, + metaKey: 'beyondwords_project_id', + metaValue: projectId, + } ); + cy.task( 'setPostMeta', { + postId, + metaKey: 'beyondwords_content_id', + metaValue: contentId, + } ); + cy.task( 'setPostMeta', { + postId, + metaKey: 'beyondwords_language_code', + metaValue: 'en_US', + } ); + cy.task( 'setPostMeta', { + postId, + metaKey: 'beyondwords_body_voice_id', + metaValue: '9001', + } ); + + cy.intercept( + 'GET', + // apiFetch appends `_locale`, hence the trailing wildcard. + `**/beyondwords/v1/projects/${ projectId }/content/${ contentId }*`, + { statusCode: 200, body: { status: 'processed' } } + ).as( 'statusCheck' ); + + stubPlayerScript(); + + cy.visitPostEditorById( postId ); + cy.openBeyondwordsPluginSidebar(); + + cy.wait( '@statusCheck' ); + + // PlayAudio also mounts in the core "Post" tab's document-setting + // panel (src/editor/block/document-setting/index.js), so more than + // one instance can embed for the same post — assert every embed + // used the right content, not an exact count. + cy.window() + .its( '__beyondwordsPlayerCalls' ) + .should( 'have.length.at.least', 1 ) + .then( ( calls ) => { + calls.forEach( ( call ) => { + expect( call ).to.include( { contentId, projectId } ); + } ); + } ); + + cy.get( '.beyondwords-sidebar__preview' ) + .find( '.beyondwords-player-loading' ) + .should( 'not.exist' ); + } ); + } ); } );