Skip to content

Commit c69a176

Browse files
authored
Merge pull request #2438 from Automattic/fix/2437-editor-presentation-json
Fix editor presentation capture diagnostics and canvas selection
2 parents 6c1bb39 + 119efc8 commit c69a176

2 files changed

Lines changed: 38 additions & 8 deletions

File tree

packages/runtime-playground/src/editor-command-runners.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { browserCommandResult } from "./browser-result-sanitization.js"
1111
import { browserProbeReplayability, browserProbeViewport } from "./browser-probe.js"
1212
import { argValue, commaListArg, durationArg, jsonArrayArg } from "./commands.js"
1313
import { DEFAULT_EDITOR_WAIT_SELECTOR, editorActionStepsFromArgs, editorOpenTargetFromArgs, editorValidateContentFromArgs, editorValidateProviderFromArgs, resolveEditorOpenTarget, type EditorActionStep, type EditorBlockSpec, type EditorBlockTarget, type EditorOpenTarget } from "./editor-actions.js"
14-
import { assertPlaygroundResponseOk, type PlaygroundRunResponse } from "./playground-command-errors.js"
14+
import { assertPlaygroundResponseOk, attachPlaygroundDiagnostics, type PlaygroundRunResponse } from "./playground-command-errors.js"
1515
import type { PlaygroundCliServer } from "./preview-server.js"
1616
import { serializeBrowserError } from "./browser-metrics.js"
1717
import { fileSha256, installWordPressAdminAuthCookies } from "./browser-probe-support.js"
@@ -30,6 +30,7 @@ const EDITOR_PRESENTATION_MIN_OBSERVATION_MS = 4_000
3030
const EDITOR_PRESENTATION_POLL_MS = 50
3131
const EDITOR_PRESENTATION_IFRAME_DISCOVERY_MS = 1_000
3232
const EDITOR_PRESENTATION_MAX_CAPTURE_MS = 10_000
33+
const EDITOR_PRESENTATION_CONTRACT_MARKER = "WP_CODEBOX_EDITOR_PRESENTATION_CONTRACT:"
3334
const EDITOR_VALIDITY_WARNING_SELECTORS = [
3435
".block-editor-warning",
3536
".block-editor-block-list__block.is-invalid",
@@ -873,7 +874,8 @@ export async function captureEditorPresentation(page: import("playwright").Page,
873874
let canvas: import("playwright").Page | import("playwright").Frame | undefined = frame ?? undefined
874875
let canvasDocumentType: "iframe" | "parent" = "iframe"
875876
if (!frame) {
876-
const hasParentDocumentCanvas = await page.locator(EDITOR_CANVAS_DEFAULT_LAYOUT_SELECTOR).first().isVisible().catch(() => false)
877+
const parentDocumentCanvases = await page.locator(EDITOR_CANVAS_DEFAULT_LAYOUT_SELECTOR).all()
878+
const hasParentDocumentCanvas = (await Promise.all(parentDocumentCanvases.map((candidate) => candidate.isVisible().catch(() => false)))).some(Boolean)
877879
if (sawCanvas || !hasParentDocumentCanvas || Date.now() - startedAtMs < EDITOR_PRESENTATION_IFRAME_DISCOVERY_MS) {
878880
previousFingerprint = undefined
879881
stableSinceMs = undefined
@@ -945,6 +947,8 @@ async function captureExpectedEditorPresentationIdentities(
945947
code: bootstrapPhpCode(runtimeSpec, `
946948
$post = get_post(${target.postId});
947949
if ( ! $post instanceof WP_Post ) { throw new RuntimeException( 'Editor target post is unavailable.' ); }
950+
wp_styles();
951+
wp_scripts();
948952
$settings = get_block_editor_settings( array(), new WP_Block_Editor_Context( array( 'post' => $post ) ) );
949953
$identities = array();
950954
foreach ( (array) ( $settings['styles'] ?? array() ) as $style ) {
@@ -955,17 +959,31 @@ foreach ( (array) ( $settings['styles'] ?? array() ) as $style ) {
955959
}
956960
$identities = array_values( array_unique( $identities ) );
957961
sort( $identities, SORT_STRING );
958-
echo wp_json_encode( array( 'identities' => $identities, 'complete' => true ) );
962+
echo PHP_EOL . '${EDITOR_PRESENTATION_CONTRACT_MARKER}' . base64_encode( (string) wp_json_encode( array( 'identities' => $identities, 'complete' => true ) ) ) . PHP_EOL;
959963
`, []),
960964
})
961965
assertPlaygroundResponseOk("wordpress.editor-open.capture-presentation-contract", response)
962-
const value = JSON.parse(cleanWpCliOutput(response.text)) as { identities?: unknown; complete?: unknown }
966+
const value = parseEditorPresentationContract(response.text) as { identities?: unknown; complete?: unknown }
963967
if (!Array.isArray(value.identities) || value.complete !== true || !value.identities.every((identity) => typeof identity === "string" && /^[a-f0-9]{64}$/.test(identity))) {
964968
throw new Error("wordpress.editor-open presentation contract returned an invalid identity set")
965969
}
966970
return { identities: [...new Set(value.identities)].sort(), complete: true }
967971
}
968972

973+
export function parseEditorPresentationContract(output: string): unknown {
974+
const text = cleanWpCliOutput(output)
975+
const payload = text.match(new RegExp(`${EDITOR_PRESENTATION_CONTRACT_MARKER}([A-Za-z0-9+/=]+)`))?.[1]
976+
if (!payload) {
977+
throw attachPlaygroundDiagnostics(new Error("wordpress.editor-open presentation contract did not emit framed JSON"), "PHP output", text)
978+
}
979+
980+
try {
981+
return JSON.parse(Buffer.from(payload, "base64").toString("utf8"))
982+
} catch (error) {
983+
throw attachPlaygroundDiagnostics(new Error(`wordpress.editor-open presentation contract emitted invalid framed JSON: ${error instanceof Error ? error.message : String(error)}`), "PHP output", text)
984+
}
985+
}
986+
969987
export async function captureEditorIdleCanvas(page: import("playwright").Page): Promise<BrowserEditorIdleCanvasSummary> {
970988
const idleCanvas = await page.evaluate(() => {
971989
const selectors = [".components-guide", ".welcome-panel", ".components-modal__frame"]

tests/browser-routed-command-security.test.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { runBrowserActionsCommand, runBrowserScenarioCommand } from "../packages
99
import { isBrowserCommandArtifactError } from "../packages/runtime-playground/src/browser-command-artifact-error.js"
1010
import { runBrowserMultiActorScenarioCommand } from "../packages/runtime-playground/src/browser-multi-actor-scenario-runner.js"
1111
import { runBrowserProbeCommand } from "../packages/runtime-playground/src/browser-probe-runner.js"
12-
import { runEditorCanvasProbeCommand, runEditorOpenCommand } from "../packages/runtime-playground/src/editor-command-runners.js"
12+
import { parseEditorPresentationContract, runEditorCanvasProbeCommand, runEditorOpenCommand } from "../packages/runtime-playground/src/editor-command-runners.js"
1313
import { closeHttpServer, listenLocalHttpServer, type PlaygroundCliServer } from "../packages/runtime-playground/src/preview-server.js"
1414
import { withTempDir } from "../scripts/test-kit.js"
1515

@@ -25,6 +25,7 @@ const SLOW_PRESENTATION_IDENTITY = "1".repeat(64)
2525
const PENDING_STYLES_PRESENTATION_IDENTITY = "2".repeat(64)
2626
const GROWING_PRESENTATION_IDENTITIES = ["3".repeat(64), "4".repeat(64)]
2727
const DELAYED_POST_PRESENTATION_IDENTITY = "5".repeat(64)
28+
const EDITOR_PRESENTATION_CONTRACT_MARKER = "WP_CODEBOX_EDITOR_PRESENTATION_CONTRACT:"
2829
const matchedPresentationMarkup = `<style>html,body{margin:0}.block-editor-block-list__layout{box-sizing:border-box;width:200px;height:400px;background:linear-gradient(#123,#abc);color:white;padding:12px}</style><div class="block-editor-block-list__layout">Matched presentation</div>`
2930
const editorShell = `<!doctype html><script>
3031
globalThis.__name = (value) => value
@@ -45,14 +46,25 @@ window.wp = {
4546
const editorHtml = `${editorShell}<iframe name="unrelated" srcdoc="<style>/* blocks-engine-presentation:${UNRELATED_PRESENTATION_IDENTITY} */<\/style>"></iframe><iframe name="editor-canvas" srcdoc="<script>globalThis.__name = (value) => value;setTimeout(() => { const style = document.createElement('style'); style.textContent = '/* blocks-engine-presentation:${CANVAS_PRESENTATION_IDENTITY} */'; document.head.append(style) }, 400)<\/script><div class='block-editor-block-list__layout'><div class='block-editor-block-list__block' data-block='fixture'>Block</div></div>"></iframe>`
4647
const onboardingEditorHtml = `${editorShell}<script>setTimeout(() => document.body.insertAdjacentHTML('afterbegin', '<div class=components-guide>Late guide without controls</div>'), 1000); const fixtureSelect = wp.data.select; wp.data.select = (store) => store === 'core/edit-post' ? { isFeatureActive: () => true } : fixtureSelect(store); wp.data.dispatch = (store) => store === 'core/preferences' ? { set: (scope, feature, value) => { if (scope === 'core/edit-post' && feature === 'welcomeGuide' && value === false) document.querySelector('.components-guide')?.remove() } } : store === 'core/edit-post' ? { toggleFeature: () => { document.body.insertAdjacentHTML('afterbegin', '<div class=components-guide>Retoggled guide</div>') } } : ({})<\/script><iframe name="editor-canvas" srcdoc="<div class='block-editor-block-list__layout'><div class='block-editor-block-list__block' data-block='fixture'>Block</div></div>"></iframe>`
4748
const matchedPresentationEditorHtml = `${editorShell}<iframe name="editor-canvas" style="border:0;width:200px;height:80px" srcdoc="${matchedPresentationMarkup.replaceAll('"', '&quot;')}"></iframe>`
48-
const parentCanvasEditorHtml = `${editorShell}<style>/* blocks-engine-presentation:${PARENT_CANVAS_PRESENTATION_IDENTITY} */</style><div class="block-editor-block-list__layout"><div class="block-editor-block-list__block" data-block="fixture">Block</div></div>`
49+
const parentCanvasEditorHtml = `${editorShell}<style>/* blocks-engine-presentation:${PARENT_CANVAS_PRESENTATION_IDENTITY} */</style><div class="block-editor-block-list__layout" hidden>Hidden duplicate</div><div class="block-editor-block-list__layout"><div class="block-editor-block-list__block" data-block="fixture">Block</div></div>`
4950
const replacingCanvasEditorHtml = `${editorShell}<iframe name="editor-canvas" srcdoc="<style>/* blocks-engine-presentation:${INITIAL_CANVAS_PRESENTATION_IDENTITY} */<\/style>"></iframe><script>setTimeout(() => { document.querySelector('iframe[name=editor-canvas]').srcdoc = '<style>/* blocks-engine-presentation:${REPLACED_CANVAS_PRESENTATION_IDENTITY} */<\\/style>' }, 150)</script>`
5051
const delayedCanvasEditorHtml = `${editorShell}<div class="block-editor-block-list__layout"><div class="block-editor-block-list__block" data-block="transition">Transition</div></div><script>setTimeout(() => { const iframe = document.createElement('iframe'); iframe.name = 'editor-canvas'; iframe.srcdoc = '<style>/* blocks-engine-presentation:${DELAYED_CANVAS_PRESENTATION_IDENTITY} */<\\/style>'; document.body.append(iframe) }, 300)</script>`
5152
const slowPresentationEditorHtml = `${editorShell}<iframe name="editor-canvas" srcdoc="<script>setTimeout(() => { const style = document.createElement('style'); style.textContent = '/* blocks-engine-presentation:${SLOW_PRESENTATION_IDENTITY} */'; document.head.append(style) }, 3500)<\/script><div class='block-editor-block-list__layout'><div class='block-editor-block-list__block' data-block='slow'>Slow</div></div>"></iframe>`
5253
const pendingStylesEditorHtml = `${editorShell}<iframe name="editor-canvas" srcdoc="<link rel='stylesheet' href='http://127.0.0.1:9/unavailable.css'><style>/* blocks-engine-presentation:${PENDING_STYLES_PRESENTATION_IDENTITY} */<\/style><div class='block-editor-block-list__layout'><div class='block-editor-block-list__block' data-block='pending'>Pending</div></div>"></iframe>`
5354
const delayedPostEditorHtml = `${editorShell}<script>const fixtureSelect = wp.data.select; let currentPostId = null; wp.data.select = (store) => store === 'core/editor' ? { getCurrentPostId: () => currentPostId, getCurrentPostType: () => currentPostId ? 'page' : null } : fixtureSelect(store); setTimeout(() => { currentPostId = 4; const iframe = document.createElement('iframe'); iframe.name = 'editor-canvas'; iframe.srcdoc = "<style>/* blocks-engine-presentation:${DELAYED_POST_PRESENTATION_IDENTITY} */<\\/style><div class='block-editor-block-list__layout'>Hydrated post</div>"; document.body.append(iframe) }, 1500)<\/script>`
5455
const growingPresentationEditorHtml = `${editorShell}<iframe name="editor-canvas" srcdoc="<style>/* blocks-engine-presentation:${GROWING_PRESENTATION_IDENTITIES[0]} */<\/style><script>let identity = 0; setInterval(() => { const style = document.createElement('style'); style.textContent = '/* blocks-engine-presentation:' + (identity++).toString(16).padStart(64, '9') + ' */'; document.head.append(style) }, 100); setTimeout(() => { const style = document.createElement('style'); style.textContent = '/* blocks-engine-presentation:${GROWING_PRESENTATION_IDENTITIES[1]} */'; document.head.append(style) }, 4100)<\/script><div class='block-editor-block-list__layout'><div class='block-editor-block-list__block' data-block='growing'>Growing</div></div>"></iframe>`
5556

57+
function editorPresentationContractOutput(value: unknown): string {
58+
return `${EDITOR_PRESENTATION_CONTRACT_MARKER}${Buffer.from(JSON.stringify(value)).toString("base64")}\n`
59+
}
60+
61+
test("editor presentation contract isolates framed JSON from PHP diagnostics", () => {
62+
const warning = '<br />\n<b>Warning</b>: Undefined array key "css" in <b>/wordpress/wp-includes/block-editor.php</b> on line <b>123</b><br />\n'
63+
const value = { identities: GROWING_PRESENTATION_IDENTITIES, complete: true }
64+
assert.deepEqual(parseEditorPresentationContract(`${warning}${editorPresentationContractOutput(value)}`), value)
65+
assert.throws(() => parseEditorPresentationContract(warning), /Undefined array key "css"/)
66+
})
67+
5668
test("real browser commands sanitize console, artifacts, stdout, and failure stderr", async () => {
5769
const httpServer = createServer((request, response) => {
5870
response.setHeader("content-type", "text/html")
@@ -98,7 +110,7 @@ test("real browser commands sanitize console, artifacts, stdout, and failure std
98110
} as RuntimeCreateSpec
99111
const runPlaygroundCommand = async (command: string) => ({
100112
text: command === "wordpress.editor-open.capture-presentation-contract"
101-
? JSON.stringify({ identities: GROWING_PRESENTATION_IDENTITIES, complete: true })
113+
? editorPresentationContractOutput({ identities: GROWING_PRESENTATION_IDENTITIES, complete: true })
102114
: "[]",
103115
exitCode: 0,
104116
})
@@ -257,7 +269,7 @@ test("real browser commands sanitize console, artifacts, stdout, and failure std
257269
artifactRoot,
258270
runPlaygroundCommand: async (command) => ({
259271
text: command.includes("capture-presentation-contract")
260-
? JSON.stringify({ identities: [DELAYED_POST_PRESENTATION_IDENTITY], complete: true })
272+
? editorPresentationContractOutput({ identities: [DELAYED_POST_PRESENTATION_IDENTITY], complete: true })
261273
: "[]",
262274
exitCode: 0,
263275
}),

0 commit comments

Comments
 (0)