From 1dcb67d0b3677c5df7ed1979c18c99e1a54d7eb1 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Tue, 27 Oct 2026 12:16:18 -0400 Subject: [PATCH 1/2] Preserve native text/video grids and editable Media Library videos --- .../wp/__tests__/video-grid-layout.test.ts | 43 ++++++++++ .../video-grid-save-validity.test.ts | 49 +++++++++++ php-transformer/composer.json | 1 + .../src/HtmlToBlocks/BlockFactory.php | 54 +----------- .../Patterns/MediaTextPattern.php | 52 ++---------- .../HtmlToBlocks/Style/EngineSupportCss.php | 5 ++ .../Style/SourceBlockAttributeProjector.php | 9 ++ .../WordPressSitePlanResolver.php | 5 +- .../fixtures/parity/html-media-text.json | 13 +-- .../tests/fixtures/text-video-grid.html | 9 ++ .../tests/unit/media-text-block-factory.php | 32 -------- .../tests/unit/media-text-pattern.php | 82 ++++--------------- .../tests/unit/text-video-grid.php | 56 +++++++++++++ 13 files changed, 208 insertions(+), 202 deletions(-) create mode 100644 packages/blocks-engine/src/wp/__tests__/video-grid-layout.test.ts create mode 100644 packages/blocks-engine/src/wp/__tests__/video-grid-save-validity.test.ts create mode 100644 php-transformer/tests/fixtures/text-video-grid.html create mode 100644 php-transformer/tests/unit/text-video-grid.php diff --git a/packages/blocks-engine/src/wp/__tests__/video-grid-layout.test.ts b/packages/blocks-engine/src/wp/__tests__/video-grid-layout.test.ts new file mode 100644 index 000000000..2803feaa3 --- /dev/null +++ b/packages/blocks-engine/src/wp/__tests__/video-grid-layout.test.ts @@ -0,0 +1,43 @@ +import { expect, it } from 'vitest'; +import { chromium } from 'playwright'; +import { execFileSync } from 'node:child_process'; +import { readFileSync } from 'node:fs'; +import { createRequire } from 'node:module'; +import { fileURLToPath } from 'node:url'; + +const require = createRequire(import.meta.url); +const root = fileURLToPath(new URL('../../../../../php-transformer/', import.meta.url)); + +it('retains source text/video grid geometry at 390, 768 and 1440px', async () => { + const source = readFileSync(`${root}/tests/fixtures/text-video-grid.html`, 'utf8'); + const output = JSON.parse(execFileSync('php', ['-r', ` + require 'vendor/autoload.php'; + $r = (new Automattic\\BlocksEngine\\PhpTransformer\\HtmlToBlocks\\HtmlTransformer())->transform(file_get_contents('tests/fixtures/text-video-grid.html'))->toArray(); + echo json_encode(['html' => $r['serialized_blocks'], 'css' => implode("\\n", array_column($r['assets'], 'content'))]); + `], { cwd: root, encoding: 'utf8' })) as { html: string; css: string }; + const coreCss = readFileSync(require.resolve('@wordpress/block-library/build-style/style.css'), 'utf8'); + const browser = await chromium.launch({ headless: true }); + try { + const page = await browser.newPage(); + for (const width of [390, 768, 1440]) { + await page.setViewportSize({ width, height: 1000 }); + const measure = async (html: string) => { + await page.setContent(html); + return page.evaluate(() => ['h2', 'video'].map((selector) => { + const box = document.querySelector(selector)!.getBoundingClientRect(); + return { x: box.x, y: box.y, width: box.width, height: box.height }; + })); + }; + const before = await measure(source); + const after = await measure(`${output.html}`); + console.log(width, { source: before, imported: after }); + for (const [index, box] of after.entries()) { + for (const key of ['x', 'y', 'width', 'height'] as const) { + expect(Math.abs(box[key] - before[index][key]), `${width}px ${index} ${key}`).toBeLessThan(1); + } + } + } + } finally { + await browser.close(); + } +}, 30000); diff --git a/packages/blocks-engine/src/wp/__tests__/video-grid-save-validity.test.ts b/packages/blocks-engine/src/wp/__tests__/video-grid-save-validity.test.ts new file mode 100644 index 000000000..bc223ee59 --- /dev/null +++ b/packages/blocks-engine/src/wp/__tests__/video-grid-save-validity.test.ts @@ -0,0 +1,49 @@ +import { beforeAll, describe, expect, it, vi } from 'vitest'; +import { execFileSync } from 'node:child_process'; +import { createRequire } from 'node:module'; +import { fileURLToPath } from 'node:url'; +import { setupDomGlobals } from '../dom-globals.js'; + +const require = createRequire(import.meta.url); +type Block = { name: string; innerBlocks: Block[]; attributes: Record }; +type Wp = { + parse(markup: string): Block[]; + validateBlock(block: Block): [boolean, unknown[]]; +}; +const transformer = fileURLToPath(new URL('../../../../../php-transformer/', import.meta.url)); + +describe('text/video author grid Gutenberg save validity', () => { + let wp: Wp; + beforeAll(() => { + setupDomGlobals(); + wp = require('@wordpress/blocks') as Wp; + require('@wordpress/block-library').registerCoreBlocks(); + }); + + it('detects the former media-text video attribute mismatch', () => { + const error = vi.spyOn(console, 'error').mockImplementation(() => undefined); + const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + try { + const blocks = wp.parse('
'); + expect(wp.validateBlock(blocks[0])[0]).toBe(false); + } finally { + error.mockRestore(); + warn.mockRestore(); + } + }); + + it.each([true, false])('validates the public transformer output (controls=%s)', (controls) => { + const markup = execFileSync('php', ['-r', ` + require 'vendor/autoload.php'; + $html = file_get_contents('tests/fixtures/text-video-grid.html'); + if (${controls ? 'false' : 'true'}) $html = str_replace(' controls', '', $html); + echo (new Automattic\\BlocksEngine\\PhpTransformer\\HtmlToBlocks\\HtmlTransformer())->transform($html)->toArray()['serialized_blocks']; + `], { cwd: transformer, encoding: 'utf8' }); + const flatten = (blocks: Block[]): Block[] => blocks.flatMap((block) => [block, ...flatten(block.innerBlocks)]); + const blocks = flatten(wp.parse(markup)); + expect(blocks.map((block) => wp.validateBlock(block)[0])).toEqual(blocks.map(() => true)); + expect(blocks.some((block) => block.name === 'core/media-text')).toBe(false); + const video = blocks.find((block) => block.name === 'core/video'); + expect(video?.attributes).toMatchObject({ src: 'clip.mp4', poster: 'poster.jpg', preload: 'metadata', controls, loop: true, muted: true, playsInline: true }); + }); +}); diff --git a/php-transformer/composer.json b/php-transformer/composer.json index af3efab57..6ddd21300 100644 --- a/php-transformer/composer.json +++ b/php-transformer/composer.json @@ -477,6 +477,7 @@ "php tests/unit/embed-author-height-overrides-attribute.php", "php tests/unit/media-text-block-factory.php", "php tests/unit/video-element-conversion.php", + "php tests/unit/text-video-grid.php", "php tests/contract/native-video-poster.php", "php tests/contract/native-progress-meter.php", "php tests/contract/small-fixed-size-image-beside-text.php", diff --git a/php-transformer/src/HtmlToBlocks/BlockFactory.php b/php-transformer/src/HtmlToBlocks/BlockFactory.php index b8df1fc9e..b70455c23 100644 --- a/php-transformer/src/HtmlToBlocks/BlockFactory.php +++ b/php-transformer/src/HtmlToBlocks/BlockFactory.php @@ -227,23 +227,6 @@ private function commentAttrs(string $name, array $attrs): array // core's save() never reads it back, so it never belongs in the // serialized comment. unset($attrs['mediaFigureClassName']); - // Internal-only: consumed by mediaTextVideoAttrs() to build the - // video pane's