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
30 changes: 19 additions & 11 deletions packages/runtime-playground/src/editor-command-runners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -853,6 +853,7 @@ interface EditorPresentationCapture {
canvasDocumentType: "iframe" | "parent"
iframeCount: number
stylesheetUrls: string[]
loadedStylesheetUrls: string[]
inlineStyleContents: string[]
}

Expand All @@ -872,12 +873,14 @@ function externalStylesheetPresentationIdentity(url: string): string | undefined

export function summarizeEditorPresentation(capture: EditorPresentationCapture, expectedIdentities: readonly string[] = []): BrowserEditorPresentationSummary {
const iframeStylesheetUrls = [...new Set(capture.stylesheetUrls.map((url) => url.trim()).filter(Boolean))].sort()
const loadedStylesheetUrls = new Set(capture.loadedStylesheetUrls.map((url) => url.trim()).filter(Boolean))
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) => {
if (!loadedStylesheetUrls.has(url)) return []
const identity = externalStylesheetPresentationIdentity(url)
return identity && expected.has(identity) ? [identity] : []
})
Expand Down Expand Up @@ -928,6 +931,9 @@ export async function captureEditorPresentation(page: import("playwright").Page,
canvasDocumentType,
iframeCount,
stylesheetUrls: stylesheets.flatMap((stylesheet) => stylesheet.href ? [stylesheet.href] : []),
// A link is not proof of delivery: failed stylesheet requests leave
// sheet unset and must not satisfy an expected presentation identity.
loadedStylesheetUrls: stylesheets.flatMap((stylesheet) => stylesheet.href && stylesheet.sheet ? [stylesheet.href] : []),
inlineStyleContents: Array.from(document.querySelectorAll("style"), (style) => style.textContent ?? ""),
}
}, { canvasDocumentType, iframeCount: canvasDocumentType === "iframe" ? 1 : 0 }).catch(() => null) as (EditorPresentationCapture & { documentIdentity: string; documentAgeMs: number }) | null
Expand Down Expand Up @@ -995,13 +1001,6 @@ if ( ! function_exists( 'set_current_screen' ) ) {
require_once ABSPATH . 'wp-admin/includes/screen.php';
}
set_current_screen( 'post' );
$wp_styles = wp_styles();
wp_scripts();
$queued_before_editor_assets = $wp_styles->queue;
do_action( 'enqueue_block_assets' );
$editor_style_handles = array_values( array_diff( $wp_styles->queue, $queued_before_editor_assets ) );
$wp_styles->all_deps( $editor_style_handles );
$editor_style_handles = array_values( array_unique( array_merge( $editor_style_handles, $wp_styles->to_do ) ) );
$settings = get_block_editor_settings( array(), new WP_Block_Editor_Context( array( 'post' => $post ) ) );
$identities = array();
foreach ( (array) ( $settings['styles'] ?? array() ) as $style ) {
Expand All @@ -1010,10 +1009,19 @@ foreach ( (array) ( $settings['styles'] ?? array() ) as $style ) {
foreach ( $matches[1] as $identity ) { $identities[] = strtolower( $identity ); }
}
}
foreach ( $editor_style_handles as $handle ) {
$style = $wp_styles->registered[$handle] ?? null;
$version = is_object( $style ) ? ( $style->ver ?? null ) : null;
if ( is_string( $version ) && preg_match( '/^[a-f0-9]{64}$/iD', $version ) ) { $identities[] = strtolower( $version ); }
// WordPress collects iframe assets in an isolated styles instance, so the
// resolved asset HTML is the authoritative external stylesheet contract.
$resolved_styles = (string) ( $settings['__unstableResolvedAssets']['styles'] ?? '' );
if ( preg_match_all( '~<link\\b[^>]*\\bhref=([^[:space:]>]+)~i', $resolved_styles, $links ) ) {
foreach ( $links[1] as $href ) {
$url = trim( html_entity_decode( $href, ENT_QUOTES, 'UTF-8' ), chr(34) . chr(39) );
$query = wp_parse_url( $url, PHP_URL_QUERY );
if ( ! is_string( $query ) ) { continue; }
parse_str( $query, $parameters );
$version = $parameters['ver'] ?? null;
if ( is_array( $version ) ) { continue; }
if ( is_string( $version ) && preg_match( '/^[a-f0-9]{64}$/iD', $version ) ) { $identities[] = strtolower( $version ); }
}
}
$identities = array_values( array_unique( $identities ) );
sort( $identities, SORT_STRING );
Expand Down
18 changes: 14 additions & 4 deletions tests/editor-actions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ const styledPresentation = summarizeEditorPresentation({
canvasDocumentType: "iframe",
iframeCount: 2,
stylesheetUrls: ["https://example.test/styles/editor.css", "https://example.test/styles/editor.css", "https://example.test/styles/theme.css"],
loadedStylesheetUrls: ["https://example.test/styles/editor.css", "https://example.test/styles/theme.css"],
inlineStyleContents: [
"/* blocks-engine-presentation:ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789 */",
"/* blocks-engine-presentation:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789 */",
Expand All @@ -86,7 +87,7 @@ assert.deepEqual(styledPresentation, {
generatedPresentationIdentityCount: 1,
generatedPresentationIdentities: ["abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"],
})
assert.deepEqual(summarizeEditorPresentation({ canvasDocumentType: "parent", iframeCount: 0, stylesheetUrls: [], inlineStyleContents: [".editor { color: black; }"] }), {
assert.deepEqual(summarizeEditorPresentation({ canvasDocumentType: "parent", iframeCount: 0, stylesheetUrls: [], loadedStylesheetUrls: [], inlineStyleContents: [".editor { color: black; }"] }), {
schema: "wp-codebox/editor-presentation/v1",
canvasDocumentType: "parent",
iframeCount: 0,
Expand All @@ -106,6 +107,7 @@ const externalOnlyPresentation = summarizeEditorPresentation({
canvasDocumentType: "iframe",
iframeCount: 1,
stylesheetUrls: [`https://example.test/wp-content/themes/fixture/assets/a.css?ver=${externalIdentityA.toUpperCase()}`],
loadedStylesheetUrls: [`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")
Expand All @@ -117,6 +119,7 @@ assert.deepEqual(summarizeEditorPresentation({
canvasDocumentType: "iframe",
iframeCount: 1,
stylesheetUrls: [`https://example.test/a.css?ver=${externalIdentityA}`, `https://example.test/b.css?ver=${externalIdentityB}`],
loadedStylesheetUrls: [`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")

Expand All @@ -130,23 +133,30 @@ assert.deepEqual(summarizeEditorPresentation({
"https://example.test/b.css?ver=6.7.1",
"https://example.test/c.css",
],
loadedStylesheetUrls: [
`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}`],
loadedStylesheetUrls: [`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.
// A failed stylesheet stays visible in the evidence but cannot certify an
// expected identity.
assert.deepEqual(summarizeEditorPresentation({
canvasDocumentType: "iframe",
iframeCount: 1,
stylesheetUrls: [`https://example.test/a.css?ver=${externalIdentityA}`],
loadedStylesheetUrls: [],
inlineStyleContents: [],
}, [externalIdentityA, externalIdentityB]).generatedPresentationIdentities, [externalIdentityA], "a not-yet-loaded stylesheet stays unobserved")
}, [externalIdentityA, externalIdentityB]).generatedPresentationIdentities, [], "a failed stylesheet stays unobserved")

const idleCanvas = await captureEditorIdleCanvas({
evaluate: async (callback: () => unknown) => {
Expand Down
41 changes: 10 additions & 31 deletions tests/editor-presentation-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@ import { runCommandText } from "../scripts/test-kit.js"

const inlineIdentity = "A".repeat(64)
const externalIdentity = "b".repeat(64)
const unrelatedIdentity = "c".repeat(64)

async function captureContract(inlineCss: string[], editorVersions: unknown[], prequeuedVersions: unknown[] = []): Promise<{ identities: string[]; complete: boolean }> {
async function captureContract(inlineCss: string[], resolvedStylesheetUrls: string[]): Promise<{ identities: string[]; complete: boolean }> {
const root = await mkdtemp(join(tmpdir(), "wp-codebox-editor-presentation-"))
const includes = join(root, "wp-admin", "includes")
await mkdir(includes, { recursive: true })
Expand All @@ -20,33 +19,13 @@ define( 'ABSPATH', ${JSON.stringify(`${root}/`)} );
class WP_Post {}
class WP_Block_Editor_Context { public function __construct( public array $context ) {} }
$fixture_inline_css = ${JSON.stringify(inlineCss)};
$fixture_editor_versions = ${JSON.stringify(editorVersions)};
$fixture_prequeued_versions = ${JSON.stringify(prequeuedVersions)};
$fixture_styles = new class {
public array $queue = array();
public array $registered = array();
public array $to_do = array();
public function all_deps( $handles ) { $this->to_do = $handles; }
};
foreach ( $fixture_prequeued_versions as $index => $version ) {
$handle = 'unrelated-' . $index;
$fixture_styles->queue[] = $handle;
$fixture_styles->registered[$handle] = (object) array( 'ver' => $version );
}
function get_post( $post_id ) { return new WP_Post(); }
function wp_styles() { return $GLOBALS['fixture_styles']; }
function wp_scripts() {}
function do_action( $hook ) {
if ( 'enqueue_block_assets' !== $hook ) return;
foreach ( $GLOBALS['fixture_editor_versions'] as $index => $version ) {
$handle = 'editor-' . $index;
$GLOBALS['fixture_styles']->queue[] = $handle;
$GLOBALS['fixture_styles']->registered[$handle] = (object) array( 'ver' => $version );
$fixture_resolved_stylesheet_urls = ${JSON.stringify(resolvedStylesheetUrls)};
function get_post( $post_id ) { return new WP_Post(); }
function wp_parse_url( $url, $component = -1 ) { return parse_url( $url, $component ); }
function get_block_editor_settings( $settings, $context ) {
$links = implode( '', array_map( static fn ( $url ) => '<link rel="stylesheet" href="' . htmlspecialchars( $url, ENT_QUOTES, 'UTF-8' ) . '">', $GLOBALS['fixture_resolved_stylesheet_urls'] ) );
return array( 'styles' => array_map( static fn ( $css ) => array( 'css' => $css ), $GLOBALS['fixture_inline_css'] ), '__unstableResolvedAssets' => array( 'styles' => $links ) );
}
}
function get_block_editor_settings( $settings, $context ) {
return array( 'styles' => array_map( static fn ( $css ) => array( 'css' => $css ), $GLOBALS['fixture_inline_css'] ) );
}
function wp_json_encode( $value ) { return json_encode( $value ); }
${editorPresentationContractPhpCode(17)}
`
Expand All @@ -63,20 +42,20 @@ assert.deepEqual(await captureContract([`:root{--blocks-engine-presentation:${in
complete: true,
}, "inline-only delivery remains in the expected contract")

assert.deepEqual(await captureContract([], [externalIdentity.toUpperCase()]), {
assert.deepEqual(await captureContract([], [`https://example.test/editor.css?ver=${externalIdentity.toUpperCase()}`]), {
identities: [externalIdentity],
complete: true,
}, "external-only editor delivery contributes its canonical version identity")

assert.deepEqual(await captureContract([
`/* --blocks-engine-presentation:${inlineIdentity} */`,
`/* --blocks-engine-presentation:${externalIdentity} */`,
], [externalIdentity.toUpperCase()]), {
], [`https://example.test/editor.css?ver=${externalIdentity.toUpperCase()}`]), {
identities: [inlineIdentity.toLowerCase(), externalIdentity].sort(),
complete: true,
}, "mixed delivery is normalized, deduplicated, and sorted")

assert.deepEqual(await captureContract([], ["6.7.1", "not-a-hash", ` ${externalIdentity}`, `${externalIdentity}0`, null], [unrelatedIdentity]), {
assert.deepEqual(await captureContract([], ["https://example.test/a.css?ver=6.7.1", "https://example.test/b.css?ver=not-a-hash", `https://example.test/c.css?ver=%20${externalIdentity}`, `https://example.test/d.css?ver=${externalIdentity}0`]), {
identities: [],
complete: true,
}, "non-hash versions and unrelated prequeued stylesheets are ignored")
Loading