diff --git a/.wp-env.json b/.wp-env.json index 9d19ec37e..61758738a 100644 --- a/.wp-env.json +++ b/.wp-env.json @@ -5,6 +5,9 @@ "." ], "themes": [], + "mappings": { + "wp-content/mu-plugins/visualizer-e2e-force-lazy-render.php": "./tests/e2e/config/force-lazy-render.php" + }, "config": { "WP_DEBUG": true, "WP_DEBUG_LOG": true, diff --git a/classes/Visualizer/Module/Frontend.php b/classes/Visualizer/Module/Frontend.php index 1d3bbabd6..eca006a98 100644 --- a/classes/Visualizer/Module/Frontend.php +++ b/classes/Visualizer/Module/Frontend.php @@ -803,14 +803,20 @@ function visualizerTriggerScriptLoader() { function visualizerLoadScripts() { document.querySelectorAll("script[data-visualizer-script]").forEach(function(elem) { - jQuery.getScript( elem.getAttribute("data-visualizer-script") ) - .done( function( script, textStatus ) { - elem.setAttribute("src", elem.getAttribute("data-visualizer-script")); - elem.removeAttribute("data-visualizer-script"); + // Replace the placeholder with a real script tag using async=false: + // scripts download in parallel but execute in insertion order, which + // preserves the WordPress dependency order. Parallel jQuery.getScript + // calls could execute render-facade.js before the chart renderer had + // registered its render event listener, leaving charts blank. + var script = document.createElement("script"); + script.src = elem.getAttribute("data-visualizer-script"); + script.async = false; + script.onload = function() { setTimeout( function() { visualizerRefreshChart(); } ); - } ); + }; + elem.parentNode.replaceChild(script, elem); }); } diff --git a/tests/e2e/config/force-lazy-render.php b/tests/e2e/config/force-lazy-render.php new file mode 100644 index 000000000..2de83bd0a --- /dev/null +++ b/tests/e2e/config/force-lazy-render.php @@ -0,0 +1,10 @@ + { + test.beforeEach( async ( { requestUtils } ) => { + await deleteAllCharts( requestUtils ); + } ); + + async function createChartOnPost( admin, page, requestUtils ) { + // Lazy rendering is forced on by tests/e2e/config/force-lazy-render.php + // (mapped as an mu-plugin in .wp-env.json). + const chartId = await createChartWithAdmin( admin, page ); + + const post = await requestUtils.createPost( { + title: 'Lazy render', + content: `[visualizer id="${ chartId }" lazy="no" class=""]`, + status: 'publish', + } ); + + return { chartId, post }; + } + + async function triggerLazyLoader( page ) { + // Scripts are swapped to `data-visualizer-script` placeholders. + await expect( page.locator( 'script[data-visualizer-script]' ).first() ).toBeAttached(); + + // The loader starts on the first user interaction. + await page.evaluate( () => window.dispatchEvent( new Event( 'scroll' ) ) ); + } + + test( 'chart renders when the renderer script loads after render-facade.js', async ( { admin, page, requestUtils } ) => { + const { chartId, post } = await createChartOnPost( admin, page, requestUtils ); + + // Delay the chart renderer scripts so render-facade.js would win the + // load race — the deterministic reproduction of issue #1319. + await page.route( /js\/render-(google|chartjs|datatables)\.js/, async ( route ) => { + await new Promise( ( resolve ) => setTimeout( resolve, 2000 ) ); + await route.continue(); + } ); + + await page.goto( `/?p=${ post.id }` ); + await triggerLazyLoader( page ); + + const chart = page.locator( `.visualizer-front-${ chartId }` ).first(); + await expect( chart ).toHaveClass( /visualizer-chart-loaded/, { timeout: 15000 } ); + await expect( chart.locator( 'svg, canvas, table' ).first() ).toBeVisible(); + } ); + + test( 'chart renders without artificial script delay', async ( { admin, page, requestUtils } ) => { + const { chartId, post } = await createChartOnPost( admin, page, requestUtils ); + + await page.goto( `/?p=${ post.id }` ); + await triggerLazyLoader( page ); + + const chart = page.locator( `.visualizer-front-${ chartId }` ).first(); + await expect( chart ).toHaveClass( /visualizer-chart-loaded/, { timeout: 15000 } ); + await expect( chart.locator( 'svg, canvas, table' ).first() ).toBeVisible(); + } ); +} );