From c7935f08f2bd2997fec5720e3d2feebfabf4bc16 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Mon, 6 Jul 2026 23:13:18 -0400 Subject: [PATCH] Preflight DOM box provider for fixture matrix --- figma-transformer/README.md | 10 +++++ .../scripts/figma-fixture-matrix.php | 41 +++++++++++++++++++ .../tests/contract/FixtureMatrixContract.php | 12 ++++++ php-transformer/tools/visual-parity/README.md | 3 ++ .../visual-parity/bin/dom-box-provider.mjs | 20 ++++++++- 5 files changed, 85 insertions(+), 1 deletion(-) diff --git a/figma-transformer/README.md b/figma-transformer/README.md index e677eae6..37833dab 100644 --- a/figma-transformer/README.md +++ b/figma-transformer/README.md @@ -114,6 +114,16 @@ Run a single local fixture: homeboy bench --rig figma-transformer-fixture-matrix --profile fixture-matrix --setting bench_env.FIGMA_FIXTURE_MATRIX_FIXTURE=/path/to/fixture.fig --path /path/to/blocks-engine ``` +Run with DOM-box capture evidence: + +```sh +npm ci --prefix php-transformer/tools/visual-parity +npm --prefix php-transformer/tools/visual-parity run install:browsers +homeboy bench --rig figma-transformer-fixture-matrix --profile fixture-matrix --setting-json bench_env='{"FIGMA_FIXTURE_MATRIX_FIXTURE":"/path/to/fixture.fig","FIGMA_FIXTURE_MATRIX_ARGS":["--capture-dom-boxes","--dom-box-provider-command=node php-transformer/tools/visual-parity/bin/dom-box-provider.mjs"]}' --path /path/to/blocks-engine +``` + +The matrix runner preflights Homeboy and the canonical DOM-box provider before fixture transforms start. Missing provider configuration, missing `node_modules`, or missing Playwright Chromium exits with an actionable setup command before any partial fixture run. + Run a fixture corpus manually: ```sh diff --git a/figma-transformer/scripts/figma-fixture-matrix.php b/figma-transformer/scripts/figma-fixture-matrix.php index 368d7d40..6b28c44b 100755 --- a/figma-transformer/scripts/figma-fixture-matrix.php +++ b/figma-transformer/scripts/figma-fixture-matrix.php @@ -85,6 +85,7 @@ if ( $captureDomBoxes && ! $dryRun ) { matrix_preflight_homeboy_command($homeboyCommand); + matrix_preflight_dom_box_provider_command($domBoxProviderCommand, $root); } $summary = array( @@ -392,6 +393,7 @@ function matrix_print_help(): void php scripts/figma-fixture-matrix.php --dry-run --fixture=/tmp/patched-fixtures/home.fig php scripts/figma-fixture-matrix.php --dry-run --fixture-dir=/tmp/fixture-corpus php scripts/figma-fixture-matrix.php --dry-run --fixture=/tmp/home.fig --include-fixture-dir + php scripts/figma-fixture-matrix.php --fixture=/tmp/home.fig --capture-dom-boxes --dom-box-provider-command='node php-transformer/tools/visual-parity/bin/dom-box-provider.mjs' HELP; } @@ -686,6 +688,45 @@ function matrix_preflight_homeboy_command(string $homeboyCommand): void exit(1); } +function matrix_preflight_dom_box_provider_command(string $domBoxProviderCommand, string $root): void +{ + $domBoxProviderCommand = trim($domBoxProviderCommand); + if ( '' === $domBoxProviderCommand ) { + fwrite(STDERR, "DOM box capture requires a provider command. Set --dom-box-provider-command, --dom-box-command, or HOMEBOY_DOM_BOX_CAPTURE_COMMAND.\n"); + fwrite(STDERR, "Canonical provider: --dom-box-provider-command='node php-transformer/tools/visual-parity/bin/dom-box-provider.mjs'\n"); + fwrite(STDERR, "Install provider dependencies first: npm ci --prefix php-transformer/tools/visual-parity && npm --prefix php-transformer/tools/visual-parity run install:browsers\n"); + exit(1); + } + + if ( ! matrix_dom_box_provider_supports_preflight($domBoxProviderCommand) ) { + return; + } + + $previousDirectory = getcwd(); + if ( false === $previousDirectory || ! chdir($root) ) { + fwrite(STDERR, "Unable to preflight DOM box provider from repository root: {$root}\n"); + exit(1); + } + + exec($domBoxProviderCommand . ' --preflight 2>&1', $output, $exitCode); + chdir($previousDirectory); + + if ( 0 === $exitCode ) { + return; + } + + fwrite(STDERR, "DOM box provider preflight failed before fixture transforms started.\n"); + fwrite(STDERR, "Provider command: {$domBoxProviderCommand}\n"); + fwrite(STDERR, implode("\n", $output) . "\n"); + fwrite(STDERR, "Install provider dependencies first: npm ci --prefix php-transformer/tools/visual-parity && npm --prefix php-transformer/tools/visual-parity run install:browsers\n"); + exit(1); +} + +function matrix_dom_box_provider_supports_preflight(string $domBoxProviderCommand): bool +{ + return str_contains($domBoxProviderCommand, 'dom-box-provider.mjs') || str_contains($domBoxProviderCommand, 'blocks-engine-dom-box-provider'); +} + function matrix_inspect_command(string $figmaRoot, string $fixturePath, string $resultPath, string $zstdCommand, int $inspectLimit): string { $parts = array( diff --git a/figma-transformer/tests/contract/FixtureMatrixContract.php b/figma-transformer/tests/contract/FixtureMatrixContract.php index 7bf7b06f..44520a6a 100644 --- a/figma-transformer/tests/contract/FixtureMatrixContract.php +++ b/figma-transformer/tests/contract/FixtureMatrixContract.php @@ -432,6 +432,13 @@ function blocks_engine_figma_transformer_run_fixture_matrix_contract(callable $a . ' --homeboy-command=' . escapeshellarg($matrixFixtureDir . '/missing-homeboy') . ' 2>&1'; exec($missingHomeboyCommand, $missingHomeboyOutput, $missingHomeboyExitCode); + $missingProviderOutput = array(); + $missingProviderCommand = escapeshellarg(PHP_BINARY) + . ' ' . escapeshellarg(__DIR__ . '/../../scripts/figma-fixture-matrix.php') + . ' --capture-dom-boxes --fixture-dir=' . escapeshellarg($matrixFixtureDir) + . ' --homeboy-command=' . escapeshellarg(PHP_BINARY) + . ' 2>&1'; + exec($missingProviderCommand, $missingProviderOutput, $missingProviderExitCode); $matrixHelpOutput = array(); $matrixHelpCommand = escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg(__DIR__ . '/../../scripts/figma-fixture-matrix.php') @@ -528,6 +535,11 @@ function blocks_engine_figma_transformer_run_fixture_matrix_contract(callable $a $missingHomeboyMessage = implode("\n", $missingHomeboyOutput); $assert(str_contains($missingHomeboyMessage, 'DOM box capture requires a runnable Homeboy command'), 'fixture-matrix-capture-preflight-missing-homeboy-message'); $assert(str_contains($missingHomeboyMessage, 'Set --homeboy-command, --homeboy-bin, or HOMEBOY_COMMAND'), 'fixture-matrix-capture-preflight-homeboy-remediation'); + $assert(0 !== $missingProviderExitCode, 'fixture-matrix-capture-preflight-missing-provider-fails'); + $missingProviderMessage = implode("\n", $missingProviderOutput); + $assert(str_contains($missingProviderMessage, 'DOM box capture requires a provider command'), 'fixture-matrix-capture-preflight-missing-provider-message'); + $assert(str_contains($missingProviderMessage, "--dom-box-provider-command='node php-transformer/tools/visual-parity/bin/dom-box-provider.mjs'"), 'fixture-matrix-capture-preflight-provider-remediation'); + $assert(str_contains($missingProviderMessage, 'npm ci --prefix php-transformer/tools/visual-parity'), 'fixture-matrix-capture-preflight-provider-install-command'); $assert(0 === $matrixHelpExitCode, 'fixture-matrix-help-exits-zero'); $matrixHelpMessage = implode("\n", $matrixHelpOutput); $assert(str_contains($matrixHelpMessage, 'Usage:'), 'fixture-matrix-help-usage'); diff --git a/php-transformer/tools/visual-parity/README.md b/php-transformer/tools/visual-parity/README.md index fed44134..b93409e8 100644 --- a/php-transformer/tools/visual-parity/README.md +++ b/php-transformer/tools/visual-parity/README.md @@ -55,6 +55,7 @@ Install the tool dependencies and Chromium browser once per checkout: ```sh npm ci --prefix php-transformer/tools/visual-parity npm --prefix php-transformer/tools/visual-parity run install:browsers +node php-transformer/tools/visual-parity/bin/dom-box-provider.mjs --preflight ``` Capture DOM-box evidence for a generated static HTML artifact root: @@ -93,6 +94,8 @@ homeboy tunnel artifact-origin dom-boxes \ The report is repeatable when the artifact root, entrypoint, browser version, viewport defaults, and node identity attributes stay fixed. Attach the JSON report to the Homeboy run, issue, or PR evidence surface so the next operator can compare generated HTML structure and positions without re-running the full transform. +The fixture matrix runner preflights this provider before running transforms when DOM-box capture is enabled, so missing Node dependencies or Playwright browser installs fail fast with the install command above instead of producing partial matrix artifacts. + ```sh HOMEBOY_DOM_BOX_BASE_URL=https://example.test \ HOMEBOY_DOM_BOX_PAGE_PATHS_JSON='["/index.html"]' \ diff --git a/php-transformer/tools/visual-parity/bin/dom-box-provider.mjs b/php-transformer/tools/visual-parity/bin/dom-box-provider.mjs index f998d809..42426af8 100755 --- a/php-transformer/tools/visual-parity/bin/dom-box-provider.mjs +++ b/php-transformer/tools/visual-parity/bin/dom-box-provider.mjs @@ -60,6 +60,11 @@ async function main() { return; } + if (process.argv.includes('--preflight')) { + await preflight(); + return; + } + const cli = parseCliArgs(process.argv.slice(2)); const baseUrl = requiredEnv('HOMEBOY_DOM_BOX_BASE_URL').replace(/\/+$/, ''); const pagePaths = parsePagePaths(requiredEnv('HOMEBOY_DOM_BOX_PAGE_PATHS_JSON')); @@ -100,6 +105,19 @@ async function main() { } } +async function preflight() { + const { chromium } = await loadPlaywright(); + let browser; + try { + browser = await chromium.launch(); + } catch (error) { + throw withPlaywrightSetupHelp(error); + } finally { + await browser?.close(); + } + process.stdout.write(`${JSON.stringify({ status: 'ok', provider: 'blocks-engine-dom-box-provider' }, null, 2)}\n`); +} + async function loadPlaywright() { try { return await import('playwright'); @@ -209,7 +227,7 @@ function resolveNodeNameAttrs(cli) { } function printHelp() { - process.stdout.write(`Capture DOM boxes for Homeboy artifact-origin dom-boxes.\n\nNode identity is keyed off a configurable attribute so the tool is product-neutral.\nThe figma-transformer's data-figma-* attributes remain the backward-compatible default.\n\nEnvironment:\n HOMEBOY_DOM_BOX_BASE_URL Static artifact origin base URL.\n HOMEBOY_DOM_BOX_PAGE_PATHS_JSON JSON array of page paths to capture.\n HOMEBOY_DOM_BOX_TEXT_SAMPLE_LIMIT Optional positive integer, default 160.\n HOMEBOY_DOM_BOX_NODE_ID_ATTR Node identity attribute, default ${DEFAULT_NODE_ID_ATTR}.\n HOMEBOY_DOM_BOX_NODE_NAME_ATTR Comma-separated node name attributes, default ${DEFAULT_NODE_NAME_ATTRS.join(',')} (aria-label is always a final fallback).\n\nFlags (override the matching environment variable):\n --node-id-attr= Node identity attribute used for enumeration, selectors, and id reads.\n --node-name-attr=[,...] Node name attributes, tried in order before aria-label.\n\nOutput:\n JSON browser payload on stdout for Homeboy to shape as homeboy/static-artifact-dom-boxes/v1.\n`); + process.stdout.write(`Capture DOM boxes for Homeboy artifact-origin dom-boxes.\n\nNode identity is keyed off a configurable attribute so the tool is product-neutral.\nThe figma-transformer's data-figma-* attributes remain the backward-compatible default.\n\nEnvironment:\n HOMEBOY_DOM_BOX_BASE_URL Static artifact origin base URL.\n HOMEBOY_DOM_BOX_PAGE_PATHS_JSON JSON array of page paths to capture.\n HOMEBOY_DOM_BOX_TEXT_SAMPLE_LIMIT Optional positive integer, default 160.\n HOMEBOY_DOM_BOX_NODE_ID_ATTR Node identity attribute, default ${DEFAULT_NODE_ID_ATTR}.\n HOMEBOY_DOM_BOX_NODE_NAME_ATTR Comma-separated node name attributes, default ${DEFAULT_NODE_NAME_ATTRS.join(',')} (aria-label is always a final fallback).\n\nFlags (override the matching environment variable):\n --node-id-attr= Node identity attribute used for enumeration, selectors, and id reads.\n --node-name-attr=[,...] Node name attributes, tried in order before aria-label.\n --preflight Verify Playwright and Chromium are installed, then exit.\n\nOutput:\n JSON browser payload on stdout for Homeboy to shape as homeboy/static-artifact-dom-boxes/v1.\n`); } async function extractElements(page, pagePath, textSampleLimit, nodeIdAttr, nodeNameAttrs) {