Skip to content

Commit 2e1cf8d

Browse files
committed
Use semantic editor readiness and retain failure artifacts
1 parent 40d5f68 commit 2e1cf8d

4 files changed

Lines changed: 89 additions & 21 deletions

File tree

packages/runtime-core/src/command-registry.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1264,7 +1264,7 @@ export const commandRegistry = [
12641264
{ name: "post-id", description: "Existing post ID to open in the post editor.", format: "positive integer" },
12651265
{ name: "post-type", description: "Post type for post-new or post-id targets; defaults to post.", format: "post type slug" },
12661266
{ name: "url", description: "Explicit editor path or absolute URL to open instead of resolving a target.", format: "path or URL" },
1267-
{ name: "wait-selector", description: "Selector that marks the editor as ready; defaults to the block editor shell.", format: "CSS selector" },
1267+
{ name: "wait-selector", description: "Optional visible selector assertion evaluated after semantic editor readiness.", format: "CSS selector" },
12681268
{ name: "wait-timeout", description: "Timeout for navigation and editor-ready waits.", format: "duration, e.g. 15s or 500ms" },
12691269
{ name: "capture", description: "Comma-separated artifacts to capture after opening the editor.", format: "steps,console,errors,html,screenshot,editor-state,editor-validity" },
12701270
{ name: "artifact-prefix", description: "Optional artifact directory relative to the runtime artifact root for this invocation; defaults to files/browser. Use files/browser/editor-open/<name> to isolate per-fixture editor-open evidence in a batch.", format: "relative artifact directory" },

packages/runtime-playground/src/editor-actions.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@ export interface EditorOpenTarget {
2020
postId?: number
2121
postSlug?: string
2222
postType?: string
23-
waitSelector: string
23+
waitSelector?: string
2424
}
2525

26+
export const DEFAULT_EDITOR_WAIT_SELECTOR = ".edit-post-visual-editor, .editor-styles-wrapper, .block-editor, .interface-interface-skeleton"
27+
2628
export type EditorActionStep =
2729
| { kind: "open"; timeout?: string }
2830
| { kind: "waitForReady"; timeout?: string }
@@ -31,11 +33,9 @@ export type EditorActionStep =
3133
| { kind: "savePost"; marker?: string; content?: string; timeout?: string }
3234
| { kind: "inspectState"; timeout?: string }
3335

34-
const DEFAULT_EDITOR_WAIT_SELECTOR = ".edit-post-visual-editor, .editor-styles-wrapper, .block-editor, .interface-interface-skeleton"
35-
3636
export function editorOpenTargetFromArgs(args: string[]): EditorOpenTarget {
3737
const explicitUrl = argValue(args, "url")?.trim()
38-
const waitSelector = argValue(args, "wait-selector")?.trim() || DEFAULT_EDITOR_WAIT_SELECTOR
38+
const waitSelector = argValue(args, "wait-selector")?.trim() || undefined
3939
if (explicitUrl) {
4040
return { url: explicitUrl, kind: "url", waitSelector }
4141
}

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

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { browserStepRecord } from "./browser-interactions.js"
99
import { browserPreviewNetworkPolicyIsActive, browserPreviewNetworkPolicySummary, browserPreviewNeedsContextRouting, browserPreviewOrigins, browserPreviewReadinessError, browserPreviewRouting, browserPreviewSecureContextError, browserPreviewTopology, resolveBrowserPreviewUrl, routeBrowserPreviewContextNetwork } from "./browser-preview-routing.js"
1010
import { browserProbeReplayability, browserProbeViewport } from "./browser-probe.js"
1111
import { argValue, commaListArg, durationArg, jsonArrayArg } from "./commands.js"
12-
import { editorActionStepsFromArgs, editorOpenTargetFromArgs, editorValidateContentFromArgs, editorValidateProviderFromArgs, resolveEditorOpenTarget, type EditorActionStep } from "./editor-actions.js"
12+
import { DEFAULT_EDITOR_WAIT_SELECTOR, editorActionStepsFromArgs, editorOpenTargetFromArgs, editorValidateContentFromArgs, editorValidateProviderFromArgs, resolveEditorOpenTarget, type EditorActionStep } from "./editor-actions.js"
1313
import type { PlaygroundRunResponse } from "./playground-command-errors.js"
1414
import type { PlaygroundCliServer } from "./preview-server.js"
1515
import { serializeBrowserError } from "./browser-metrics.js"
@@ -538,6 +538,7 @@ export async function runEditorOpenCommand({
538538
let editorState: EditorStateSnapshot | undefined
539539
let editorValidity: EditorValidityArtifact | undefined
540540
let editorCanvasReadiness: BrowserEditorCanvasProbeSummary | undefined
541+
let editorReadiness: BrowserEditorReadinessSummary | undefined
541542
let authSummary: BrowserProbeAuthSummary | undefined
542543
let pendingError: Error | undefined
543544
let artifact: BrowserArtifact | undefined
@@ -577,10 +578,12 @@ export async function runEditorOpenCommand({
577578
const waitStartedAt = now()
578579
const waitStartedAtMs = Date.now()
579580
try {
580-
await waitForAnyVisibleSelector(page, target.waitSelector, waitTimeoutMs)
581-
editorCanvasReadiness = await waitForEditorOpenCanvasReadiness(page, target.waitSelector, waitTimeoutMs)
581+
const readiness = await waitForEditorOpenReadiness(page, target.waitSelector, waitTimeoutMs)
582+
editorReadiness = readiness.editorReadiness
583+
editorCanvasReadiness = readiness.editorCanvasReadiness
582584
finalUrl = page.url()
583-
stepRecords.push(browserStepRecord(1, { kind: "waitFor", selector: target.waitSelector }, "ok", waitStartedAt, waitStartedAtMs, finalUrl, {
585+
stepRecords.push(browserStepRecord(1, { kind: "waitFor", selector: target.waitSelector ?? "wp.data core/editor + core/block-editor + core/editor.savePost" }, "ok", waitStartedAt, waitStartedAtMs, finalUrl, {
586+
editorReadiness,
584587
...(editorCanvasReadiness ? { editorCanvas: editorCanvasReadiness } : {}),
585588
} as never))
586589
} catch (error) {
@@ -606,7 +609,7 @@ export async function runEditorOpenCommand({
606609
}
607610
if (capture.has("screenshot")) {
608611
await artifactSession.writeGenerated("screenshot", "editor-screenshot.png", async (path) => {
609-
if (editorCanvasReadiness?.ready) {
612+
if (editorCanvasReadiness?.ready && target.waitSelector) {
610613
const frame = await resolveEditorCanvasFrame(page, target.waitSelector)
611614
if (frame) {
612615
await frame.locator(EDITOR_CANVAS_DEFAULT_LAYOUT_SELECTOR).first().screenshot({ path, timeout: waitTimeoutMs })
@@ -653,6 +656,7 @@ export async function runEditorOpenCommand({
653656
screenshot: capture.has("screenshot"),
654657
...(editorSummary ? { editor: editorSummary } : {}),
655658
...(editorValidity ? { editorValidity: editorValidity.summary } : {}),
659+
...(editorReadiness ? { editorReadiness } : {}),
656660
...(editorCanvasReadiness ? { editorCanvas: editorCanvasReadiness } : {}),
657661
viewport,
658662
},
@@ -682,7 +686,7 @@ export async function runEditorOpenCommand({
682686
}
683687

684688
if (pendingError) {
685-
throw new Error(`wordpress.editor-open failed after ${stepRecords.length} step(s): ${pendingError.message}`)
689+
throw editorOpenArtifactError(stepRecords.length, pendingError, artifact)
686690
}
687691

688692
return {
@@ -727,9 +731,15 @@ export function editorOpenArtifactFilesForCapture(capture: ReadonlySet<string>,
727731
}
728732
}
729733

730-
async function waitForEditorOpenCanvasReadiness(page: import("playwright").Page, waitSelector: string, timeoutMs: number): Promise<BrowserEditorCanvasProbeSummary | undefined> {
734+
export async function waitForEditorOpenReadiness(page: import("playwright").Page, waitSelector: string | undefined, timeoutMs: number): Promise<{ editorReadiness: BrowserEditorReadinessSummary; editorCanvasReadiness?: BrowserEditorCanvasProbeSummary }> {
735+
const editorReadiness = await waitForEditorReadiness(page, timeoutMs)
736+
if (!waitSelector) {
737+
return { editorReadiness }
738+
}
739+
740+
await waitForAnyVisibleSelector(page, waitSelector, timeoutMs)
731741
if (!waitSelector.includes("editor-canvas")) {
732-
return undefined
742+
return { editorReadiness }
733743
}
734744

735745
const probe = await waitForEditorCanvasProbe(page, {
@@ -744,7 +754,11 @@ async function waitForEditorOpenCanvasReadiness(page: import("playwright").Page,
744754
throw new Error(`Editor canvas was not ready: ${probe.summary.diagnostics.map((diagnostic) => diagnostic.code).join(", ") || "not-ready"}`)
745755
}
746756

747-
return probe.summary
757+
return { editorReadiness, editorCanvasReadiness: probe.summary }
758+
}
759+
760+
export function editorOpenArtifactError(stepCount: number, error: Error, artifact: BrowserArtifact): BrowserCommandArtifactError {
761+
return new BrowserCommandArtifactError(`wordpress.editor-open failed after ${stepCount} step(s): ${error.message}`, artifact)
748762
}
749763

750764
export async function runEditorActionsCommand({
@@ -768,6 +782,7 @@ export async function runEditorActionsCommand({
768782
server,
769783
})
770784
const actionSteps = await editorActionStepsFromArgs(args)
785+
const editorWaitSelector = target.waitSelector ?? DEFAULT_EDITOR_WAIT_SELECTOR
771786
const capture = new Set(commaListArg(args, "capture"))
772787
if (capture.size === 0) {
773788
capture.add("steps")
@@ -845,13 +860,13 @@ export async function runEditorActionsCommand({
845860
const waitStartedAt = now()
846861
const waitStartedAtMs = Date.now()
847862
try {
848-
await waitForAnyVisibleSelector(page, target.waitSelector, waitTimeoutMs)
863+
await waitForAnyVisibleSelector(page, editorWaitSelector, waitTimeoutMs)
849864
finalUrl = page.url()
850-
stepRecords.push(browserStepRecord(1, { kind: "waitFor", selector: target.waitSelector }, "ok", waitStartedAt, waitStartedAtMs, finalUrl, {}))
865+
stepRecords.push(browserStepRecord(1, { kind: "waitFor", selector: editorWaitSelector }, "ok", waitStartedAt, waitStartedAtMs, finalUrl, {}))
851866
} catch (error) {
852867
const serialized = serializeBrowserError("probe-error", error)
853868
errors.push(serialized)
854-
stepRecords.push(browserStepRecord(1, { kind: "waitFor", selector: target.waitSelector }, "failed", waitStartedAt, waitStartedAtMs, page.url(), { error: serialized }))
869+
stepRecords.push(browserStepRecord(1, { kind: "waitFor", selector: editorWaitSelector }, "failed", waitStartedAt, waitStartedAtMs, page.url(), { error: serialized }))
855870
pendingError = error instanceof Error ? error : new Error(String(error))
856871
}
857872
}
@@ -1113,14 +1128,14 @@ async function waitForEditorReadiness(page: import("playwright").Page, timeoutMs
11131128
const editor = select("core/editor")
11141129
const blockEditor = select("core/block-editor")
11151130
const editorDispatch = dispatch("core/editor")
1116-
if (!editor || !blockEditor || !editorDispatch) {
1131+
if (!editor || !blockEditor || !editorDispatch || typeof editorDispatch.savePost !== "function") {
11171132
return false
11181133
}
11191134
return {
11201135
schema: "wp-codebox/editor-readiness/v1",
11211136
status: "ready",
11221137
storesAvailable: true,
1123-
canSave: typeof editorDispatch.savePost === "function",
1138+
canSave: true,
11241139
postId: typeof editor.getCurrentPostId === "function" ? editor.getCurrentPostId() : undefined,
11251140
postType: typeof editor.getCurrentPostType === "function" ? editor.getCurrentPostType() : undefined,
11261141
}
@@ -1601,6 +1616,7 @@ export async function runEditorValidateBlocksCommand({
16011616
runtimeSpec,
16021617
server,
16031618
})
1619+
const editorWaitSelector = target.waitSelector ?? DEFAULT_EDITOR_WAIT_SELECTOR
16041620
const content = await editorValidateContentFromArgs(args)
16051621
const provider = editorValidateProviderFromArgs(args)
16061622
const waitTimeoutMs = durationArg(args, "wait-timeout", EDITOR_VALIDATE_BLOCKS_READY_TIMEOUT_MS)
@@ -1639,7 +1655,7 @@ export async function runEditorValidateBlocksCommand({
16391655

16401656
await page.goto(targetUrl, { waitUntil: "domcontentloaded", timeout: waitTimeoutMs })
16411657
finalUrl = page.url()
1642-
await waitForAnyVisibleSelector(page, target.waitSelector, waitTimeoutMs)
1658+
await waitForAnyVisibleSelector(page, editorWaitSelector, waitTimeoutMs)
16431659
await waitForEditorBlocksRuntime(page, waitTimeoutMs)
16441660
finalUrl = page.url()
16451661
validation = await validateEditorBlocks(page, { content, provider })

tests/editor-actions.test.ts

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import assert from "node:assert/strict"
2-
import { captureEditorState, captureEditorValidity, editorOpenArtifactFilesForCapture, editorOpenArtifactPathPrefixFromArgs } from "../packages/runtime-playground/src/editor-command-runners.js"
2+
import { captureEditorState, captureEditorValidity, editorOpenArtifactError, editorOpenArtifactFilesForCapture, editorOpenArtifactPathPrefixFromArgs, waitForEditorOpenReadiness } from "../packages/runtime-playground/src/editor-command-runners.js"
3+
import { isBrowserCommandArtifactError } from "../packages/runtime-playground/src/browser-command-artifact-error.js"
34
import { editorActionStepsFromArgs, editorOpenTargetFromArgs, resolveEditorOpenTarget } from "../packages/runtime-playground/src/editor-actions.js"
45

56
const steps = await editorActionStepsFromArgs([
@@ -24,6 +25,7 @@ await assert.rejects(
2425
)
2526

2627
const target = editorOpenTargetFromArgs(["target=post-new"])
28+
assert.equal(target.waitSelector, undefined)
2729
const unavailableEditorState = await captureEditorState({
2830
evaluate: async (callback: () => unknown) => {
2931
const globals = globalThis as typeof globalThis & { window?: unknown }
@@ -166,4 +168,54 @@ assert.throws(
166168
/relative artifact directory/,
167169
)
168170

171+
// Semantic readiness is sufficient when legacy presentation-shell selectors are
172+
// absent or hidden. An explicitly supplied selector remains a caller assertion.
173+
const readinessCalls: Array<string | undefined> = []
174+
const semanticReadyPage = {
175+
waitForFunction: async (predicate: (selector?: string) => unknown, selector?: string) => {
176+
readinessCalls.push(selector)
177+
if (selector) {
178+
throw new Error(`Timed out waiting for ${selector}`)
179+
}
180+
const globals = globalThis as typeof globalThis & { window?: unknown }
181+
const previousWindow = globals.window
182+
globals.window = {
183+
wp: {
184+
data: {
185+
select: (store: string) => store === "core/editor"
186+
? { getCurrentPostId: () => 21, getCurrentPostType: () => "page" }
187+
: store === "core/block-editor" ? {} : undefined,
188+
dispatch: (store: string) => store === "core/editor" ? { savePost: () => undefined } : undefined,
189+
},
190+
},
191+
}
192+
try {
193+
const readiness = predicate()
194+
assert.ok(readiness)
195+
return { jsonValue: async () => readiness }
196+
} finally {
197+
globals.window = previousWindow
198+
}
199+
},
200+
} as never
201+
const semanticReadiness = await waitForEditorOpenReadiness(semanticReadyPage, undefined, 1)
202+
assert.equal(semanticReadiness.editorReadiness.canSave, true)
203+
await assert.rejects(
204+
() => waitForEditorOpenReadiness(semanticReadyPage, ".legacy-editor-shell", 1),
205+
/Timed out waiting for \.legacy-editor-shell/,
206+
)
207+
assert.deepEqual(readinessCalls, [undefined, undefined, ".legacy-editor-shell"])
208+
209+
const retainedArtifact = {
210+
artifactType: "editor-open",
211+
requestedUrl: "http://example.test/wp-admin/post-new.php",
212+
url: "http://example.test/wp-admin/post-new.php",
213+
preview: { effectiveOrigin: "http://example.test" },
214+
files: { screenshot: "files/browser/editor-screenshot.png", editorState: "files/browser/editor-state.json", console: "files/browser/editor-console.jsonl", summary: "files/browser/editor-summary.json" },
215+
summary: { consoleMessages: 1, errors: 1, finalUrl: "http://example.test/wp-admin/post-new.php", htmlSnapshot: false, networkEvents: 0, replayability: "diagnostic-only", screenshot: true, viewport: null },
216+
} as never
217+
const readinessFailure = editorOpenArtifactError(2, new Error("editor readiness timed out"), retainedArtifact)
218+
assert.equal(isBrowserCommandArtifactError(readinessFailure), true)
219+
assert.strictEqual(readinessFailure.artifact, retainedArtifact)
220+
169221
console.log("editor actions ok")

0 commit comments

Comments
 (0)