diff --git a/packages/runtime-playground/src/editor-command-runners.ts b/packages/runtime-playground/src/editor-command-runners.ts index 32311461..a1f42601 100644 --- a/packages/runtime-playground/src/editor-command-runners.ts +++ b/packages/runtime-playground/src/editor-command-runners.ts @@ -648,9 +648,9 @@ export async function runEditorOpenCommand({ if (editorReadiness) { await dismissWordPressOnboardingDialogs(page) - editorPresentation = await captureEditorPresentation(page, waitTimeoutMs) + const expected = await captureExpectedEditorPresentationIdentities(target, runPlaygroundCommand, runtimeSpec, server) + editorPresentation = await captureEditorPresentation(page, waitTimeoutMs, expected?.complete ? expected.identities : []) if (editorPresentation) { - const expected = await captureExpectedEditorPresentationIdentities(target, runPlaygroundCommand, runtimeSpec, server) await dismissWordPressOnboardingDialogs(page) const idleCanvas = await captureEditorIdleCanvas(page) editorPresentation = { @@ -861,7 +861,7 @@ export function summarizeEditorPresentation(capture: EditorPresentationCapture): } } -export async function captureEditorPresentation(page: import("playwright").Page, timeoutMs: number): Promise { +export async function captureEditorPresentation(page: import("playwright").Page, timeoutMs: number, expectedIdentities: string[] = []): Promise { const startedAtMs = Date.now() const deadlineMs = startedAtMs + Math.min(timeoutMs, EDITOR_PRESENTATION_MAX_CAPTURE_MS) let previousFingerprint: string | undefined @@ -902,7 +902,13 @@ export async function captureEditorPresentation(page: import("playwright").Page, const summary = summarizeEditorPresentation(capture) const fingerprint = `${capture.documentIdentity}\n${JSON.stringify(summary)}` const observedAtMs = Date.now() - if (fingerprint === previousFingerprint) { + const expectedIdentitiesObserved = expectedIdentities.length > 0 + && expectedIdentities.every((identity) => summary.generatedPresentationIdentities.includes(identity)) + const summarySettled = fingerprint === previousFingerprint + && stableSinceMs !== undefined + && observedAtMs - stableSinceMs >= EDITOR_PRESENTATION_SETTLE_MS + if ((expectedIdentitiesObserved || summarySettled) + && capture.documentAgeMs >= EDITOR_PRESENTATION_MIN_OBSERVATION_MS) { const currentDocumentIdentity = capture.canvasDocumentType === "iframe" ? await resolveEditorCanvasFrame(page, EDITOR_CANVAS_DEFAULT_IFRAME_SELECTOR) .then(async (currentFrame) => currentFrame && currentFrame === frame ? await currentFrame.evaluate(() => `${location.href}\n${performance.timeOrigin}`) : undefined) @@ -910,13 +916,11 @@ export async function captureEditorPresentation(page: import("playwright").Page, : await resolveEditorCanvasFrame(page, EDITOR_CANVAS_DEFAULT_IFRAME_SELECTOR) .then(async (currentFrame) => currentFrame ? undefined : await page.evaluate(() => `${location.href}\n${performance.timeOrigin}`)) .catch(() => undefined) - if (stableSinceMs !== undefined - && observedAtMs - stableSinceMs >= EDITOR_PRESENTATION_SETTLE_MS - && capture.documentAgeMs >= EDITOR_PRESENTATION_MIN_OBSERVATION_MS - && currentDocumentIdentity === capture.documentIdentity) { + if (currentDocumentIdentity === capture.documentIdentity) { return summary } - } else { + } + if (fingerprint !== previousFingerprint) { previousFingerprint = fingerprint stableSinceMs = observedAtMs } diff --git a/tests/browser-routed-command-security.test.ts b/tests/browser-routed-command-security.test.ts index 7ade09a8..3f5bdb4e 100644 --- a/tests/browser-routed-command-security.test.ts +++ b/tests/browser-routed-command-security.test.ts @@ -23,6 +23,7 @@ const DELAYED_CANVAS_PRESENTATION_IDENTITY = "e".repeat(64) 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 matchedPresentationMarkup = `
Matched presentation
` const editorShell = `` const slowPresentationEditorHtml = `${editorShell}` const pendingStylesEditorHtml = `${editorShell}` +const growingPresentationEditorHtml = `${editorShell}` 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("/broken") + response.end(request.url?.startsWith("/wp-admin/post.php") + ? growingPresentationEditorHtml + : request.url?.startsWith("/broken") ? "
Broken editor fixture
" : request.url?.startsWith("/presentation") ? "
Deliberately different frontend fixture
" @@ -88,7 +92,12 @@ test("real browser commands sanitize console, artifacts, stdout, and failure std }, }, } as RuntimeCreateSpec - const runPlaygroundCommand = async () => ({ text: "[]", exitCode: 0 }) + const runPlaygroundCommand = async (command: string) => ({ + text: command === "wordpress.editor-open.capture-presentation-contract" + ? JSON.stringify({ identities: GROWING_PRESENTATION_IDENTITIES, complete: true }) + : "[]", + exitCode: 0, + }) try { await withTempDir("wp-codebox-real-browser-actions-security-", async (artifactRoot) => { @@ -239,6 +248,20 @@ 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-growing-presentation-security-", async (artifactRoot) => { + const result = await runEditorOpenCommand({ + artifactRoot, + runPlaygroundCommand, + runtimeSpec, + server, + spec: { command: "wordpress.editor-open", args: ["post-id=1", "capture=steps", "wait-timeout=5s"] }, + }) + const output = JSON.parse(result.output) as { summary: { editorPresentation: { generatedPresentationIdentities: string[]; expectedGeneratedPresentationIdentities: string[]; expectedGeneratedPresentationIdentitiesComplete: boolean } } } + assert.deepEqual(output.summary.editorPresentation.expectedGeneratedPresentationIdentities, GROWING_PRESENTATION_IDENTITIES) + assert.equal(output.summary.editorPresentation.expectedGeneratedPresentationIdentitiesComplete, true) + assert.equal(GROWING_PRESENTATION_IDENTITIES.every((identity) => output.summary.editorPresentation.generatedPresentationIdentities.includes(identity)), true) + }) + await withTempDir("wp-codebox-real-editor-canvas-security-", async (artifactRoot) => { const result = await runEditorCanvasProbeCommand({ artifactRoot,