Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions readme.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
24 changes: 12 additions & 12 deletions src/editor/components/play-audio/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 );
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
156 changes: 150 additions & 6 deletions tests/cypress/e2e/block-editor/preview-panel.cy.js
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -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' );
} );
} );
} );
Loading