diff --git a/.wp-env.json b/.wp-env.json index 60fac31da..afed5482f 100644 --- a/.wp-env.json +++ b/.wp-env.json @@ -9,6 +9,7 @@ "config": { "WP_DEBUG": true, "WP_DEBUG_LOG": true, + "WP_DEBUG_DISPLAY": false, "WP_ENVIRONMENT_TYPE": "local" } } diff --git a/bin/wp-env-setup.sh b/bin/wp-env-setup.sh index 77e431813..e999a4d81 100755 --- a/bin/wp-env-setup.sh +++ b/bin/wp-env-setup.sh @@ -169,3 +169,40 @@ cat > "$CREDENTIALS_FILE" < data += chunk); + process.stdin.on('end', () => { + try { JSON.parse(data); } + catch { process.exit(1); } + }); + "; then + echo "Editor settings endpoint is ready." + break + fi + + if [ "$attempt" -eq "$READY_MAX_RETRIES" ]; then + echo "Error: editor settings endpoint did not return valid JSON after $((READY_MAX_RETRIES * READY_RETRY_INTERVAL)) seconds." + exit 1 + fi + + sleep $READY_RETRY_INTERVAL +done diff --git a/e2e/wp-env-fixtures.js b/e2e/wp-env-fixtures.js index fd1a5483e..b29c21982 100644 --- a/e2e/wp-env-fixtures.js +++ b/e2e/wp-env-fixtures.js @@ -30,6 +30,64 @@ let cachedEditorSettings = null; /** Cached editor assets — fetched once and reused across all tests. */ let cachedEditorAssets = null; +const MAX_FETCH_ATTEMPTS = 15; +const FETCH_RETRY_DELAY_MS = 2000; + +const delay = ( ms ) => new Promise( ( resolve ) => setTimeout( resolve, ms ) ); + +/** + * Fetch and parse JSON from a wp-env REST endpoint, retrying on transient + * failures. + * + * During warm-up the Playground/wp-env instance can briefly return a 5xx or + * leak a PHP notice into the body, which corrupts the JSON. Both surface here + * as errors we retry, and the last failure carries a snippet of the body so a + * genuine leak is diagnosable instead of an opaque "Unexpected token '<'". + * + * @param {string} url Fully-qualified REST URL. + * @param {Object} creds Credentials object from readCredentials(). + * @param {string} label Human-readable resource name for error messages. + * @return {Promise} The parsed JSON response. + */ +async function fetchJson( url, creds, label ) { + let lastError; + + for ( let attempt = 1; attempt <= MAX_FETCH_ATTEMPTS; attempt++ ) { + try { + const response = await fetch( url, { + headers: { Authorization: creds.authHeader }, + } ); + + const body = await response.text(); + + if ( ! response.ok ) { + throw new Error( + `HTTP ${ response.status } ${ + response.statusText + } — ${ body.slice( 0, 200 ) }` + ); + } + + try { + return JSON.parse( body ); + } catch { + throw new Error( + `response was not valid JSON — ${ body.slice( 0, 200 ) }` + ); + } + } catch ( error ) { + lastError = error; + if ( attempt < MAX_FETCH_ATTEMPTS ) { + await delay( FETCH_RETRY_DELAY_MS ); + } + } + } + + throw new Error( + `Failed to fetch ${ label } after ${ MAX_FETCH_ATTEMPTS } attempts: ${ lastError.message }` + ); +} + /** * Fetch editor settings from the wp-env WordPress instance. * @@ -44,20 +102,11 @@ async function fetchEditorSettings( creds ) { return cachedEditorSettings; } - const response = await fetch( + cachedEditorSettings = await fetchJson( `${ creds.siteApiRoot }wp-block-editor/v1/settings`, - { - headers: { Authorization: creds.authHeader }, - } + creds, + 'editor settings' ); - - if ( ! response.ok ) { - throw new Error( - `Failed to fetch editor settings: ${ response.status } ${ response.statusText }` - ); - } - - cachedEditorSettings = await response.json(); return cachedEditorSettings; } @@ -75,17 +124,11 @@ async function fetchEditorAssets( creds ) { const url = new URL( `${ creds.siteApiRoot }wpcom/v2/editor-assets` ); url.searchParams.set( 'exclude', 'core,gutenberg' ); - const response = await fetch( url.toString(), { - headers: { Authorization: creds.authHeader }, - } ); - - if ( ! response.ok ) { - throw new Error( - `Failed to fetch editor assets: ${ response.status } ${ response.statusText }` - ); - } - - cachedEditorAssets = await response.json(); + cachedEditorAssets = await fetchJson( + url.toString(), + creds, + 'editor assets' + ); return cachedEditorAssets; }