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
39 changes: 28 additions & 11 deletions packages/runtime-playground/src/editor-command-runners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -966,11 +966,30 @@ async function captureExpectedEditorPresentationIdentities(
): Promise<{ identities: string[]; complete: boolean } | undefined> {
if (target.kind !== "post" || !target.postId) return undefined
const response = await runPlaygroundCommand("wordpress.editor-open.capture-presentation-contract", server, {
code: bootstrapPhpCode(runtimeSpec, `
$post = get_post(${target.postId});
code: bootstrapPhpCode(runtimeSpec, editorPresentationContractPhpCode(target.postId), []),
})
assertPlaygroundResponseOk("wordpress.editor-open.capture-presentation-contract", response)
const value = parseEditorPresentationContract(response.text) as { identities?: unknown; complete?: unknown }
if (!Array.isArray(value.identities) || value.complete !== true || !value.identities.every((identity) => typeof identity === "string" && /^[a-f0-9]{64}$/.test(identity))) {
throw new Error("wordpress.editor-open presentation contract returned an invalid identity set")
}
return { identities: [...new Set(value.identities)].sort(), complete: true }
}

export function editorPresentationContractPhpCode(postId: number): string {
return `
$post = get_post(${postId});
if ( ! $post instanceof WP_Post ) { throw new RuntimeException( 'Editor target post is unavailable.' ); }
wp_styles();
$GLOBALS['post'] = $post;
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 @@ -979,17 +998,15 @@ 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 ); }
}
$identities = array_values( array_unique( $identities ) );
sort( $identities, SORT_STRING );
echo PHP_EOL . '${EDITOR_PRESENTATION_CONTRACT_MARKER}' . base64_encode( (string) wp_json_encode( array( 'identities' => $identities, 'complete' => true ) ) ) . PHP_EOL;
`, []),
})
assertPlaygroundResponseOk("wordpress.editor-open.capture-presentation-contract", response)
const value = parseEditorPresentationContract(response.text) as { identities?: unknown; complete?: unknown }
if (!Array.isArray(value.identities) || value.complete !== true || !value.identities.every((identity) => typeof identity === "string" && /^[a-f0-9]{64}$/.test(identity))) {
throw new Error("wordpress.editor-open presentation contract returned an invalid identity set")
}
return { identities: [...new Set(value.identities)].sort(), complete: true }
`
}

export function parseEditorPresentationContract(output: string): unknown {
Expand Down
71 changes: 71 additions & 0 deletions tests/editor-presentation-contract.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import assert from "node:assert/strict"
import { editorPresentationContractPhpCode, parseEditorPresentationContract } from "../packages/runtime-playground/src/editor-command-runners.js"
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 }> {
const php = `
define( 'ABSPATH', '/tmp/' );
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 set_current_screen( $screen ) {}
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 );
}
}
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)}
`
const output = await runCommandText("php", ["-r", php])
return parseEditorPresentationContract(output) as { identities: string[]; complete: boolean }
}

assert.deepEqual(await captureContract([`:root{--blocks-engine-presentation:${inlineIdentity};}`], []), {
identities: [inlineIdentity.toLowerCase()],
complete: true,
}, "inline-only delivery remains in the expected contract")

assert.deepEqual(await captureContract([], [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()]), {
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]), {
identities: [],
complete: true,
}, "non-hash versions and unrelated prequeued stylesheets are ignored")
Loading