Skip to content

Commit d57e303

Browse files
committed
Fix editor saves on internal runtime origin
1 parent e438b0e commit d57e303

6 files changed

Lines changed: 177 additions & 201 deletions

File tree

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@
159159
"test:browser-runner-template": "tsx tests/browser-runner-template.test.ts",
160160
"test:browser-observation-assertions": "tsx tests/browser-observation-assertions.test.ts",
161161
"test:editor-actions": "tsx tests/editor-actions.test.ts",
162+
"test:editor-actions-save-integration": "node --import tsx --test tests/editor-actions-save.integration.test.ts",
162163
"test:editor-validate-blocks": "tsx tests/editor-validate-blocks.test.ts",
163164
"test:artifact-result-envelope": "tsx tests/artifact-result-envelope.test.ts",
164165
"test:async-agent-task-contracts": "tsx tests/async-agent-task-contracts.test.ts",

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

Lines changed: 78 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,14 @@ const EDITOR_VALIDITY_WARNING_SELECTORS = [
2828
".components-notice",
2929
]
3030

31+
export function editorCommandWordPressUrl(server: PlaygroundCliServer): string {
32+
return server.wordpressUrl ?? server.serverUrl
33+
}
34+
35+
function editorCommandPreviewTopology(args: string[], runtimeSpec: RuntimeCreateSpec, server: PlaygroundCliServer) {
36+
return browserPreviewTopology(["preview-mode=local", ...args], runtimeSpec, editorCommandWordPressUrl(server))
37+
}
38+
3139
export async function runEditorCanvasProbeCommand({
3240
artifactRoot,
3341
runtimeSpec,
@@ -520,7 +528,7 @@ export async function runEditorOpenCommand({
520528
}
521529

522530
const waitTimeoutMs = durationArg(args, "wait-timeout", BROWSER_STEP_DEFAULT_TIMEOUT_MS)
523-
const topology = browserPreviewTopology(args, runtimeSpec, server.serverUrl)
531+
const topology = editorCommandPreviewTopology(args, runtimeSpec, server)
524532
const { preview, networkPolicy } = topology
525533
const targetUrl = topology.resolveUrl(target.url)
526534
const artifactPathPrefix = editorOpenArtifactPathPrefixFromArgs(args)
@@ -803,7 +811,7 @@ export async function runEditorActionsCommand({
803811
const waitTimeoutMs = durationArg(args, "wait-timeout", BROWSER_STEP_DEFAULT_TIMEOUT_MS)
804812
const stepTimeoutMs = durationArg(args, "step-timeout", BROWSER_STEP_DEFAULT_TIMEOUT_MS)
805813
const totalTimeoutMs = durationArg(args, "timeout", BROWSER_SCRIPT_DEFAULT_TIMEOUT_MS)
806-
const topology = browserPreviewTopology(args, runtimeSpec, server.serverUrl)
814+
const topology = editorCommandPreviewTopology(args, runtimeSpec, server)
807815
const { preview, networkPolicy } = topology
808816
const targetUrl = topology.resolveUrl(target.url)
809817
const artifactSession = new BrowserArtifactSession(artifactRoot, "files/browser", { source: "wordpress.editor-actions", operation: "editor-actions" })
@@ -1171,11 +1179,14 @@ async function executeEditorBlockMutation(page: import("playwright").Page, step:
11711179
let siblings = blocks
11721180
let block: Block | undefined
11731181
if (typeof input.clientId === "string") {
1174-
const locate = (items: Block[], parent?: string): boolean => items.some((item) => {
1175-
if (item.clientId === input.clientId) { block = item; parentClientId = parent; siblings = items; return true }
1176-
return locate(item.innerBlocks ?? [], item.clientId)
1177-
})
1178-
locate(blocks)
1182+
const pending: Array<{ items: Block[]; parent?: string }> = [{ items: blocks }]
1183+
while (!block && pending.length > 0) {
1184+
const current = pending.pop()!
1185+
for (const item of current.items) {
1186+
if (item.clientId === input.clientId) { block = item; parentClientId = current.parent; siblings = current.items; break }
1187+
if (item.innerBlocks?.length) pending.push({ items: item.innerBlocks, parent: item.clientId })
1188+
}
1189+
}
11791190
} else {
11801191
const path = input.path ?? [input.index ?? -1]
11811192
for (const position of path) {
@@ -1192,22 +1203,33 @@ async function executeEditorBlockMutation(page: import("playwright").Page, step:
11921203
}
11931204
}
11941205
if (!block?.clientId) throw new Error("wp-codebox-editor-target-not-found: clientId, index, or path did not resolve a block")
1195-
const requireAction = (name: string): ((...args: unknown[]) => unknown) => {
1196-
const action = actions[name]
1197-
if (typeof action !== "function") throw new Error(`wp-codebox-editor-${input.kind}-unsupported: core/block-editor.${name} is unavailable`)
1198-
return action as (...args: unknown[]) => unknown
1199-
}
1200-
const create = (spec: { name: string; attributes?: Record<string, unknown>; innerBlocks?: unknown[] }): unknown => {
1201-
if (typeof wp?.blocks?.createBlock !== "function") throw new Error("wp-codebox-editor-create-block-unsupported: wp.blocks.createBlock is unavailable")
1202-
return wp.blocks.createBlock(spec.name, spec.attributes ?? {}, (spec.innerBlocks ?? []).map((child) => create(child as typeof spec)))
1203-
}
1204-
if (input.kind === "selectBlock") return void requireAction("selectBlock")(block.clientId)
1205-
if (input.kind === "updateBlockAttributes") return void requireAction("updateBlockAttributes")(block.clientId, input.attributes)
1206-
if (input.kind === "removeBlock") return void requireAction("removeBlocks")([block.clientId])
1207-
if (input.kind === "duplicateBlock") return void requireAction("duplicateBlocks")([block.clientId])
1208-
if (input.kind === "moveBlock") return void requireAction("moveBlocksToPosition")([block.clientId], parentClientId, parentClientId, input.position)
1209-
if (input.kind === "replaceBlock") return void requireAction("replaceBlock")(block.clientId, create(input.block))
1210-
if (input.kind === "replaceInnerBlocks") return void requireAction("replaceInnerBlocks")(block.clientId, input.blocks.map(create))
1206+
let actionName: string = input.kind
1207+
let actionArgs: unknown[] = [block.clientId]
1208+
if (input.kind === "updateBlockAttributes") actionArgs.push(input.attributes)
1209+
if (input.kind === "removeBlock") { actionName = "removeBlocks"; actionArgs = [[block.clientId]] }
1210+
if (input.kind === "duplicateBlock") { actionName = "duplicateBlocks"; actionArgs = [[block.clientId]] }
1211+
if (input.kind === "moveBlock") { actionName = "moveBlocksToPosition"; actionArgs = [[block.clientId], parentClientId, parentClientId, input.position] }
1212+
if (input.kind === "replaceBlock" || input.kind === "replaceInnerBlocks") {
1213+
const createBlock = wp?.blocks?.createBlock
1214+
if (typeof createBlock !== "function") throw new Error("wp-codebox-editor-create-block-unsupported: wp.blocks.createBlock is unavailable")
1215+
const specs = input.kind === "replaceBlock" ? [input.block] : input.blocks
1216+
const created = new Map<object, unknown>()
1217+
const pending = specs.map((spec) => ({ spec, visited: false }))
1218+
while (pending.length > 0) {
1219+
const current = pending.pop()!
1220+
if (!current.visited) {
1221+
pending.push({ spec: current.spec, visited: true })
1222+
for (const child of current.spec.innerBlocks ?? []) pending.push({ spec: child, visited: false })
1223+
continue
1224+
}
1225+
created.set(current.spec, createBlock(current.spec.name, current.spec.attributes ?? {}, (current.spec.innerBlocks ?? []).map((child) => created.get(child))))
1226+
}
1227+
actionName = input.kind
1228+
actionArgs = input.kind === "replaceBlock" ? [block.clientId, created.get(input.block)] : [block.clientId, input.blocks.map((spec) => created.get(spec))]
1229+
}
1230+
const action = actions[actionName]
1231+
if (typeof action !== "function") throw new Error(`wp-codebox-editor-${input.kind}-unsupported: core/block-editor.${actionName} is unavailable`)
1232+
action(...actionArgs)
12111233
}, step)
12121234
}
12131235

@@ -1286,7 +1308,7 @@ async function waitForEditorSemanticReadiness(page: import("playwright").Page, t
12861308
}
12871309

12881310
async function saveEditorPost(page: import("playwright").Page, step: Extract<EditorActionStep, { kind: "savePost" }>, timeoutMs: number): Promise<BrowserEditorSaveSummary> {
1289-
const save = await page.evaluate(async (input) => {
1311+
await page.evaluate(async (input) => {
12901312
const win = window as unknown as {
12911313
wp?: {
12921314
blocks?: { createBlock?: (name: string, attributes?: Record<string, unknown>) => unknown }
@@ -1316,31 +1338,19 @@ async function saveEditorPost(page: import("playwright").Page, step: Extract<Edi
13161338
blockEditor.insertBlocks([createBlock("core/paragraph", { content: input.content ?? input.marker })])
13171339
}
13181340
await Promise.resolve(editorDispatch.savePost())
1319-
const deadline = Date.now() + input.timeoutMs
1320-
await new Promise<void>((resolve, reject) => {
1321-
const done = () => {
1322-
const isSaving = typeof editor.isSavingPost === "function" ? Boolean(editor.isSavingPost()) : false
1323-
const didSucceed = typeof editor.didPostSaveRequestSucceed === "function" ? Boolean(editor.didPostSaveRequestSucceed()) : undefined
1324-
const didFail = typeof editor.didPostSaveRequestFail === "function" ? Boolean(editor.didPostSaveRequestFail()) : false
1325-
if (!isSaving && didSucceed !== false) {
1326-
cleanup()
1327-
resolve()
1328-
} else if (didFail) {
1329-
cleanup()
1330-
reject(new Error("wp-codebox-editor-save-failed: core/editor savePost reported a failed request"))
1331-
} else if (Date.now() > deadline) {
1332-
cleanup()
1333-
reject(new Error("wp-codebox-editor-save-timeout: timed out waiting for core/editor savePost to settle"))
1334-
}
1335-
}
1336-
const unsubscribe = typeof wpData?.subscribe === "function" ? wpData.subscribe(done) : undefined
1337-
const interval = window.setInterval(done, 250)
1338-
const cleanup = () => {
1339-
window.clearInterval(interval)
1340-
if (unsubscribe) unsubscribe()
1341-
}
1342-
done()
1343-
})
1341+
}, { marker: step.marker, content: step.content })
1342+
const settled = await page.waitForFunction(() => {
1343+
const editor = (window as unknown as { wp?: { data?: { select?: (store: string) => Record<string, unknown> } } }).wp?.data?.select?.("core/editor")
1344+
if (!editor) return false
1345+
const saving = typeof editor.isSavingPost === "function" ? Boolean(editor.isSavingPost()) : false
1346+
const failed = typeof editor.didPostSaveRequestFail === "function" ? Boolean(editor.didPostSaveRequestFail()) : false
1347+
const succeeded = typeof editor.didPostSaveRequestSucceed === "function" ? Boolean(editor.didPostSaveRequestSucceed()) : undefined
1348+
return !saving && (failed || succeeded !== false) ? { failed } : false
1349+
}, undefined, { timeout: timeoutMs }).then((handle) => handle.jsonValue() as Promise<{ failed: boolean }>)
1350+
if (settled.failed) throw new Error("wp-codebox-editor-save-failed: core/editor savePost reported a failed request")
1351+
const save = await page.evaluate((input) => {
1352+
const editor = (window as unknown as { wp?: { data?: { select?: (store: string) => Record<string, unknown> } } }).wp?.data?.select?.("core/editor")
1353+
if (!editor) throw new Error("wp-codebox-editor-readiness-unavailable: core/editor store is unavailable")
13441354
const editedContent = typeof editor.getEditedPostContent === "function" ? String(editor.getEditedPostContent() ?? "") : ""
13451355
return {
13461356
schema: "wp-codebox/editor-save/v1",
@@ -1351,7 +1361,7 @@ async function saveEditorPost(page: import("playwright").Page, step: Extract<Edi
13511361
markerPresent: input.marker ? editedContent.includes(input.marker) : undefined,
13521362
content: editedContent,
13531363
}
1354-
}, { marker: step.marker, content: step.content, timeoutMs })
1364+
}, { marker: step.marker })
13551365

13561366
const { content, ...summary } = save as BrowserEditorSaveSummary & { content?: string }
13571367
return {
@@ -1446,13 +1456,22 @@ export async function captureEditorState(page: import("playwright").Page, target
14461456
const currentPost = typeof editor.getCurrentPost === "function" ? editor.getCurrentPost() as Record<string, unknown> | null : null
14471457
const blocks = typeof blockEditor.getBlocks === "function" ? blockEditor.getBlocks() as Array<Record<string, unknown>> : []
14481458
const serialize = (window as unknown as { wp?: { blocks?: { serialize?: (items: unknown[]) => string } } }).wp?.blocks?.serialize
1449-
const toTree = (items: Array<Record<string, unknown>>): NonNullable<EditorStateSnapshot["blocks"]> => items.map((block) => ({
1450-
name: typeof block.name === "string" ? block.name : "",
1451-
clientId: typeof block.clientId === "string" ? block.clientId : undefined,
1452-
attributes: typeof block.attributes === "object" && block.attributes ? block.attributes as Record<string, unknown> : undefined,
1453-
isValid: typeof block.isValid === "boolean" ? block.isValid : undefined,
1454-
innerBlocks: Array.isArray(block.innerBlocks) ? toTree(block.innerBlocks as Array<Record<string, unknown>>) : undefined,
1455-
}))
1459+
const tree: NonNullable<EditorStateSnapshot["blocks"]> = []
1460+
const pending: Array<{ items: Array<Record<string, unknown>>; output: NonNullable<EditorStateSnapshot["blocks"]> }> = [{ items: blocks, output: tree }]
1461+
while (pending.length > 0) {
1462+
const current = pending.pop()!
1463+
for (const block of current.items) {
1464+
const innerBlocks: NonNullable<EditorStateSnapshot["blocks"]> = []
1465+
current.output.push({
1466+
name: typeof block.name === "string" ? block.name : "",
1467+
clientId: typeof block.clientId === "string" ? block.clientId : undefined,
1468+
attributes: typeof block.attributes === "object" && block.attributes ? block.attributes as Record<string, unknown> : undefined,
1469+
isValid: typeof block.isValid === "boolean" ? block.isValid : undefined,
1470+
innerBlocks,
1471+
})
1472+
if (Array.isArray(block.innerBlocks)) pending.push({ items: block.innerBlocks as Array<Record<string, unknown>>, output: innerBlocks })
1473+
}
1474+
}
14561475
const savedContent = typeof currentPost?.content === "object" && currentPost.content
14571476
? stringValue((currentPost.content as Record<string, unknown>).raw ?? (currentPost.content as Record<string, unknown>).rendered)
14581477
: undefined
@@ -1464,7 +1483,7 @@ export async function captureEditorState(page: import("playwright").Page, target
14641483
status: typeof currentPost?.status === "string" ? currentPost.status : undefined,
14651484
title: typeof currentPost?.title === "object" && currentPost.title ? (currentPost.title as Record<string, unknown>).raw ?? (currentPost.title as Record<string, unknown>).rendered : undefined,
14661485
},
1467-
blocks: toTree(blocks),
1486+
blocks: tree,
14681487
serializedContent: typeof serialize === "function" ? serialize(blocks) : typeof editor.getEditedPostContent === "function" ? String(editor.getEditedPostContent() ?? "") : undefined,
14691488
dirty: typeof editor.isEditedPostDirty === "function" ? Boolean(editor.isEditedPostDirty()) : undefined,
14701489
saving: typeof editor.isSavingPost === "function" ? Boolean(editor.isSavingPost()) : undefined,
@@ -1910,7 +1929,7 @@ export async function runEditorValidateBlocksCommand({
19101929
const content = await editorValidateContentFromArgs(args)
19111930
const provider = editorValidateProviderFromArgs(args)
19121931
const waitTimeoutMs = durationArg(args, "wait-timeout", EDITOR_VALIDATE_BLOCKS_READY_TIMEOUT_MS)
1913-
const topology = browserPreviewTopology(args, runtimeSpec, server.serverUrl)
1932+
const topology = editorCommandPreviewTopology(args, runtimeSpec, server)
19141933
const { preview, networkPolicy } = topology
19151934
const targetUrl = topology.resolveUrl(target.url)
19161935
const artifactSession = new BrowserArtifactSession(artifactRoot, "files/browser", { source: "wordpress.editor-validate-blocks", operation: "editor-validate-blocks" })

0 commit comments

Comments
 (0)