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
32 changes: 27 additions & 5 deletions packages/runtime-playground/src/editor-command-runners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const EDITOR_PRESENTATION_MIN_OBSERVATION_MS = 4_000
const EDITOR_PRESENTATION_POLL_MS = 50
const EDITOR_PRESENTATION_IFRAME_DISCOVERY_MS = 1_000
const EDITOR_PRESENTATION_MAX_CAPTURE_MS = 10_000
const EDITOR_PRESENTATION_VERSION_PARAM = "ver"
const EDITOR_PRESENTATION_CONTRACT_MARKER = "WP_CODEBOX_EDITOR_PRESENTATION_CONTRACT:"
const EDITOR_VALIDITY_WARNING_SELECTORS = [
".block-editor-warning",
Expand Down Expand Up @@ -846,11 +847,32 @@ interface EditorPresentationCapture {
inlineStyleContents: string[]
}

export function summarizeEditorPresentation(capture: EditorPresentationCapture): BrowserEditorPresentationSummary {
// A generated stylesheet delivered as an external asset carries its content
// hash in the canonical cache-busting version parameter rather than in inline
// marker text. Read that version so bounded external delivery stays observable.
function externalStylesheetPresentationIdentity(url: string): string | undefined {
let version: string | null = null
try {
version = new URL(url, "https://wp-codebox.invalid").searchParams.get(EDITOR_PRESENTATION_VERSION_PARAM)
} catch {
return undefined
}
const identity = version?.trim().toLowerCase()
return identity && /^[a-f0-9]{64}$/.test(identity) ? identity : undefined
}

export function summarizeEditorPresentation(capture: EditorPresentationCapture, expectedIdentities: readonly string[] = []): BrowserEditorPresentationSummary {
const iframeStylesheetUrls = [...new Set(capture.stylesheetUrls.map((url) => url.trim()).filter(Boolean))].sort()
const generatedPresentationIdentities = [...new Set(
capture.inlineStyleContents.flatMap((content) => [...content.matchAll(/blocks-engine-presentation:([a-f0-9]{64})/gi)].map((match) => match[1]!.toLowerCase())),
)].sort()
const inlineIdentities = capture.inlineStyleContents.flatMap((content) => [...content.matchAll(/blocks-engine-presentation:([a-f0-9]{64})/gi)].map((match) => match[1]!.toLowerCase()))
// Only an expected identity can be certified from a URL version. An
// unrequested or arbitrary version stays out of the observed set so the
// expected-set comparison remains fail-closed.
const expected = new Set(expectedIdentities.map((identity) => identity.trim().toLowerCase()).filter(Boolean))
const externalIdentities = iframeStylesheetUrls.flatMap((url) => {
const identity = externalStylesheetPresentationIdentity(url)
return identity && expected.has(identity) ? [identity] : []
})
const generatedPresentationIdentities = [...new Set([...inlineIdentities, ...externalIdentities])].sort()
return {
schema: "wp-codebox/editor-presentation/v1",
canvasDocumentType: capture.canvasDocumentType,
Expand Down Expand Up @@ -901,7 +923,7 @@ export async function captureEditorPresentation(page: import("playwright").Page,
}
}, { canvasDocumentType, iframeCount: canvasDocumentType === "iframe" ? 1 : 0 }).catch(() => null) as (EditorPresentationCapture & { documentIdentity: string; documentAgeMs: number }) | null
if (capture) {
const summary = summarizeEditorPresentation(capture)
const summary = summarizeEditorPresentation(capture, expectedIdentities)
const fingerprint = `${capture.documentIdentity}\n${JSON.stringify(summary)}`
const observedAtMs = Date.now()
const expectedIdentitiesObserved = expectedIdentities.length > 0
Expand Down
52 changes: 52 additions & 0 deletions tests/editor-actions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,58 @@ assert.deepEqual(summarizeEditorPresentation({ canvasDocumentType: "parent", ifr
generatedPresentationIdentities: [],
})

// Bounded external delivery: a generated stylesheet enqueued as a real asset
// carries its content hash in the version parameter instead of inline marker
// text. Automattic/blocks-engine#1478 delivers presentation styles this way.
const externalIdentityA = "1".repeat(64)
const externalIdentityB = "2".repeat(64)
const unrequestedIdentity = "3".repeat(64)
const externalOnlyPresentation = summarizeEditorPresentation({
canvasDocumentType: "iframe",
iframeCount: 1,
stylesheetUrls: [`https://example.test/wp-content/themes/fixture/assets/a.css?ver=${externalIdentityA.toUpperCase()}`],
inlineStyleContents: [],
}, [externalIdentityA])
assert.deepEqual(externalOnlyPresentation.generatedPresentationIdentities, [externalIdentityA], "expected external stylesheet version certifies its identity")
assert.equal(externalOnlyPresentation.generatedPresentationIdentityCount, 1)

// Mixed inline and external delivery contributes both identities exactly once.
const inlineIdentity = "4".repeat(64)
assert.deepEqual(summarizeEditorPresentation({
canvasDocumentType: "iframe",
iframeCount: 1,
stylesheetUrls: [`https://example.test/a.css?ver=${externalIdentityA}`, `https://example.test/b.css?ver=${externalIdentityB}`],
inlineStyleContents: [`:root{--blocks-engine-presentation:${inlineIdentity};}`],
}, [externalIdentityA, externalIdentityB]).generatedPresentationIdentities, [externalIdentityA, externalIdentityB, inlineIdentity].sort(), "inline and external identities combine")

// Fail closed: an unrequested hash, a non-hash version, and a missing expected
// set never manufacture an observed identity from a URL alone.
assert.deepEqual(summarizeEditorPresentation({
canvasDocumentType: "iframe",
iframeCount: 1,
stylesheetUrls: [
`https://example.test/a.css?ver=${unrequestedIdentity}`,
"https://example.test/b.css?ver=6.7.1",
"https://example.test/c.css",
],
inlineStyleContents: [],
}, [externalIdentityA]).generatedPresentationIdentities, [], "unrequested and non-hash versions are not identities")
assert.deepEqual(summarizeEditorPresentation({
canvasDocumentType: "iframe",
iframeCount: 1,
stylesheetUrls: [`https://example.test/a.css?ver=${externalIdentityA}`],
inlineStyleContents: [],
}).generatedPresentationIdentities, [], "no expected set certifies no external identity")

// A delayed stylesheet that has not yet appeared leaves the expected identity
// unobserved rather than reporting it as satisfied.
assert.deepEqual(summarizeEditorPresentation({
canvasDocumentType: "iframe",
iframeCount: 1,
stylesheetUrls: [`https://example.test/a.css?ver=${externalIdentityA}`],
inlineStyleContents: [],
}, [externalIdentityA, externalIdentityB]).generatedPresentationIdentities, [externalIdentityA], "a not-yet-loaded stylesheet stays unobserved")

const idleCanvas = await captureEditorIdleCanvas({
evaluate: async (callback: () => unknown) => {
const globals = globalThis as typeof globalThis & { document?: unknown; getComputedStyle?: unknown }
Expand Down
Loading