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
18 changes: 11 additions & 7 deletions packages/runtime-playground/src/editor-command-runners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -630,7 +630,7 @@ export async function runEditorOpenCommand({
const waitStartedAt = now()
const waitStartedAtMs = Date.now()
try {
const readiness = await waitForEditorOpenReadiness(page, target.waitSelector, waitTimeoutMs)
const readiness = await waitForEditorOpenReadiness(page, target, target.waitSelector, waitTimeoutMs)
editorReadiness = readiness.editorReadiness
editorCanvasReadiness = readiness.editorCanvasReadiness
finalUrl = page.url()
Expand Down Expand Up @@ -812,8 +812,8 @@ export function editorOpenArtifactFilesForCapture(capture: ReadonlySet<string>,
}
}

export async function waitForEditorOpenReadiness(page: import("playwright").Page, waitSelector: string | undefined, timeoutMs: number): Promise<{ editorReadiness: BrowserEditorReadinessSummary; editorCanvasReadiness?: BrowserEditorCanvasProbeSummary }> {
const editorReadiness = await waitForEditorSemanticReadiness(page, timeoutMs)
export async function waitForEditorOpenReadiness(page: import("playwright").Page, target: EditorOpenTarget, waitSelector: string | undefined, timeoutMs: number): Promise<{ editorReadiness: BrowserEditorReadinessSummary; editorCanvasReadiness?: BrowserEditorCanvasProbeSummary }> {
const editorReadiness = await waitForEditorSemanticReadiness(page, target, timeoutMs)
if (!waitSelector) {
return { editorReadiness }
}
Expand Down Expand Up @@ -1781,8 +1781,8 @@ async function waitForEditorReadiness(page: import("playwright").Page, timeoutMs

// Opening and validating an editor require the block-editor data store. Global
// block APIs and save availability are stricter, separate capabilities.
async function waitForEditorSemanticReadiness(page: import("playwright").Page, timeoutMs: number): Promise<BrowserEditorReadinessSummary> {
return page.waitForFunction(() => {
async function waitForEditorSemanticReadiness(page: import("playwright").Page, target: EditorOpenTarget, timeoutMs: number): Promise<BrowserEditorReadinessSummary> {
return page.waitForFunction((expectedPostId) => {
const win = window as unknown as {
wp?: {
blocks?: { parse?: unknown; getBlockTypes?: () => unknown[] }
Expand All @@ -1804,16 +1804,20 @@ async function waitForEditorSemanticReadiness(page: import("playwright").Page, t
const dispatch = win.wp?.data?.dispatch
const editor = select("core/editor")
const editorDispatch = typeof dispatch === "function" ? dispatch("core/editor") : undefined
const postId = typeof editor?.getCurrentPostId === "function" ? editor.getCurrentPostId() : undefined
if (expectedPostId !== null && Number(postId) !== expectedPostId) {
return false
}
return {
schema: "wp-codebox/editor-readiness/v1",
status: "ready",
storesAvailable: Boolean(editor && blockEditor),
canSave: typeof editorDispatch?.savePost === "function",
...(Array.isArray(blockTypes) ? { blockTypesRegistered: blockTypes.length } : {}),
postId: typeof editor?.getCurrentPostId === "function" ? editor.getCurrentPostId() : undefined,
postId,
postType: typeof editor?.getCurrentPostType === "function" ? editor.getCurrentPostType() : undefined,
}
}, undefined, { timeout: timeoutMs }).then(async (handle) => {
}, target.kind === "post" && target.postId ? target.postId : null, { timeout: timeoutMs }).then(async (handle) => {
const readiness = await handle.jsonValue() as BrowserEditorReadinessSummary | false
if (!readiness) {
throw new Error("wp-codebox-editor-readiness-timeout: Gutenberg block runtime did not become available")
Expand Down
26 changes: 24 additions & 2 deletions tests/browser-routed-command-security.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const PARENT_CANVAS_PRESENTATION_IDENTITY = "f".repeat(64)
const SLOW_PRESENTATION_IDENTITY = "1".repeat(64)
const PENDING_STYLES_PRESENTATION_IDENTITY = "2".repeat(64)
const GROWING_PRESENTATION_IDENTITIES = ["3".repeat(64), "4".repeat(64)]
const DELAYED_POST_PRESENTATION_IDENTITY = "5".repeat(64)
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>`
const editorShell = `<!doctype html><script>
globalThis.__name = (value) => value
Expand All @@ -49,13 +50,16 @@ const replacingCanvasEditorHtml = `${editorShell}<iframe name="editor-canvas" sr
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>`
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>`
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>`
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>`
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>`

test("real browser commands sanitize console, artifacts, stdout, and failure stderr", async () => {
const httpServer = createServer((request, response) => {
response.setHeader("content-type", "text/html")
response.end(request.url?.startsWith("/wp-admin/post.php")
? growingPresentationEditorHtml
response.end(request.url?.includes("post=4")
? delayedPostEditorHtml
: request.url?.startsWith("/wp-admin/post.php")
? growingPresentationEditorHtml
: request.url?.startsWith("/broken")
? "<main>Broken editor fixture</main>"
: request.url?.startsWith("/presentation")
Expand Down Expand Up @@ -248,6 +252,24 @@ test("real browser commands sanitize console, artifacts, stdout, and failure std
assert.deepEqual(output.summary.editorPresentation.generatedPresentationIdentities, [PENDING_STYLES_PRESENTATION_IDENTITY])
})

await withTempDir("wp-codebox-real-editor-delayed-post-readiness-", async (artifactRoot) => {
const result = await runEditorOpenCommand({
artifactRoot,
runPlaygroundCommand: async (command) => ({
text: command.includes("capture-presentation-contract")
? JSON.stringify({ identities: [DELAYED_POST_PRESENTATION_IDENTITY], complete: true })
: "[]",
exitCode: 0,
}),
runtimeSpec,
server,
spec: { command: "wordpress.editor-open", args: ["post-id=4", "post-type=page", "capture=steps", "wait-timeout=5s"] },
})
const output = JSON.parse(result.output) as { summary: { editorReadiness: { postId: number }; editorPresentation: { generatedPresentationIdentities: string[] } } }
assert.equal(output.summary.editorReadiness.postId, 4)
assert.deepEqual(output.summary.editorPresentation.generatedPresentationIdentities, [DELAYED_POST_PRESENTATION_IDENTITY])
})

await withTempDir("wp-codebox-real-editor-growing-presentation-security-", async (artifactRoot) => {
const result = await runEditorOpenCommand({
artifactRoot,
Expand Down
9 changes: 5 additions & 4 deletions tests/editor-actions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -461,23 +461,24 @@ const semanticReadyPage = {
},
}
try {
const readiness = predicate()
const readiness = predicate(selector)
assert.ok(readiness)
return { jsonValue: async () => readiness }
} finally {
globals.window = previousWindow
}
},
} as never
const semanticReadiness = await waitForEditorOpenReadiness(semanticReadyPage, undefined, 1)
const genericEditorTarget = { kind: "url", url: "/wp-admin/site-editor.php" } as const
const semanticReadiness = await waitForEditorOpenReadiness(semanticReadyPage, genericEditorTarget, undefined, 1)
assert.equal(semanticReadiness.editorReadiness.blockTypesRegistered, undefined)
assert.equal(semanticReadiness.editorReadiness.storesAvailable, false)
assert.equal(semanticReadiness.editorReadiness.canSave, false)
await assert.rejects(
() => waitForEditorOpenReadiness(semanticReadyPage, ".legacy-editor-shell", 1),
() => waitForEditorOpenReadiness(semanticReadyPage, genericEditorTarget, ".legacy-editor-shell", 1),
/Timed out waiting for \.legacy-editor-shell/,
)
assert.deepEqual(readinessCalls, [undefined, undefined, ".legacy-editor-shell"])
assert.deepEqual(readinessCalls, [null, null, ".legacy-editor-shell"])

const retainedArtifact = {
artifactType: "editor-open",
Expand Down
Loading