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
11 changes: 6 additions & 5 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -147,18 +147,19 @@ jobs:
- name: Place the commit under test where the container can reach it
run: cp -r theme site/theme-under-test

# --expect-blocks is deliberately not passed: the blocks have to reach
# the registry on both framework generations. They did not until the
# theme's BlocksServiceProvider stopped registering them from boot(),
# before WordPress accepts block registrations at all.
# v13.4.0 registers the theme's blocks under WP-CLI only, never in a web
# request, and it is tagged: --expect-blocks=no writes that down, and
# the check says so the day it changes. dev-main runs a framework that
# registers resources/views/blocks itself (13.32.0-beta.7 and later).
- name: Install check
working-directory: site
run: |
ddev exec bash -c '
php theme-under-test/bin/ci/check-install.php \
--url=https://pollora-theme.ddev.site \
--slug=ci-theme \
--source=/var/www/html/theme-under-test
--source=/var/www/html/theme-under-test \
--expect-blocks=${{ matrix.skeleton == 'v13.4.0' && 'no' || 'yes' }}
'

- name: Laravel log
Expand Down
35 changes: 0 additions & 35 deletions app/Providers/BlocksServiceProvider.php

This file was deleted.

205 changes: 163 additions & 42 deletions bin/ci/check-install.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,12 @@
$slug = (string) ($options['slug'] ?? 'ci-theme');
$source = isset($options['source']) ? rtrim((string) $options['source'], '/') : null;

// Whether this framework discovers blocks from resources/views/blocks.
//
// Measured, not assumed: on framework 13.4 the theme's BlocksServiceProvider
// boots before WordPress has defined register_block_type(), so
// BlockRegistrar::registerDirectory() returns early and the blocks never
// arrive. The caller says which behaviour it expects rather than the script
// skipping when it does not find them — a check that quietly turns itself off
// is how a broken fix shipped behind a green suite.
// Whether this framework registers the theme's resources/views/blocks in a web
// request. Measured, not assumed: 13.32.0-beta.7 does; 13.4 never did — its
// registration only ever happened under WP-CLI. The caller says which
// behaviour it expects rather than the script skipping when it does not find
// them — a check that quietly turns itself off is how a broken fix shipped
// behind a green suite.
$expectBlocks = ($options['expect-blocks'] ?? 'yes') !== 'no';

$host = parse_url($baseUrl, PHP_URL_HOST) ?: '';
Expand Down Expand Up @@ -218,7 +216,104 @@ function overlaySource(string $source, string $themeDir, string $slug): void
$copied++;
}

echo " \033[2m→ overlaid {$copied} files from the commit under test\033[0m\n";
// A copy only adds and overwrites. A file the commit under test deletes
// would survive from the tag, and CI would measure a theme that still has
// it — BlocksServiceProvider was the first such removal. Limited to the
// source directories: the build and node_modules are not in the repository.
$removed = 0;

foreach (['app', 'config', 'resources'] as $directory) {
if (! is_dir($themeDir.'/'.$directory)) {
continue;
}

$generated = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($themeDir.'/'.$directory, FilesystemIterator::SKIP_DOTS)
);

foreach ($generated as $file) {
$relative = substr($file->getPathname(), strlen($themeDir) + 1);

if (! file_exists($source.'/'.$relative)) {
unlink($file->getPathname());
$removed++;
}
}
}

echo " \033[2m→ overlaid {$copied} files from the commit under test, removed {$removed} it deletes\033[0m\n";
}

/**
* A logged-in session, for what only an editor may read: the REST API's block
* types. The REST API accepts the session's cookie once it is sent with the
* nonce that goes with it.
*/
final class Session
{
private readonly string $jar;

/** @var list<string> */
private array $headers = [];

public function __construct()
{
$this->jar = tempnam(sys_get_temp_dir(), 'theme-ci-').'.cookies';
}

public function __destruct()
{
@unlink($this->jar);
}

public function logIn(string $loginUrl, string $adminUrl, string $user, string $password): void
{
$this->visit($loginUrl, ['log' => $user, 'pwd' => $password, 'wp-submit' => 'Log In', 'testcookie' => '1']);

// Handed out to a logged-in session only.
$nonce = trim($this->visit($adminUrl.'/admin-ajax.php?action=rest-nonce')['body']);

if (preg_match('/^[a-f0-9]{10}$/', $nonce) !== 1) {
throw new \RuntimeException('no REST nonce — the login did not establish a session');
}

$this->headers = ['X-WP-Nonce: '.$nonce];
}

/**
* @param array<string, string> $post
* @return array{status: int, body: string}
*/
public function visit(string $url, array $post = []): array
{
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false,
CURLOPT_TIMEOUT => 45,
CURLOPT_COOKIEJAR => $this->jar,
CURLOPT_COOKIEFILE => $this->jar,
CURLOPT_HTTPHEADER => $this->headers,
]);

if ($post !== []) {
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post));
}

$body = curl_exec($ch);
$status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);

if ($error !== '') {
throw new \RuntimeException("{$url}: {$error}");
}

return ['status' => $status, 'body' => is_string($body) ? $body : ''];
}
}

echo "\n\033[1m=== theme-default — install check ===\033[0m\n";
Expand Down Expand Up @@ -425,44 +520,70 @@ function overlaySource(string $source, string $themeDir, string $slug): void
return $wrong === [] ? true : implode(', ', $wrong);
});

test(
$expectBlocks
? 'Every block reaches the WordPress block registry'
: 'No block reaches the registry — this framework cannot discover them',
function () use ($manifests, $expectBlocks, $slug) {
if ($manifests === []) {
return 'no block.json reached the generated theme';
}

$registered = explode(',', wpEval(
'echo implode(",", array_keys(WP_Block_Type_Registry::get_instance()->get_all_registered()));'
));

$found = [];
$absent = [];
// Asked of the REST API over HTTP, not of WP-CLI. WP-CLI boots WordPress in
// an order no web request uses: on framework 13.4 the theme's
// BlocksServiceProvider registered the blocks there and nowhere else, so a
// `wp eval` found them while the editor, the page and the REST API had none.
// Since 13.32.0-beta.7 the framework registers resources/views/blocks itself
// and the provider is gone.
$blockAccount = 'theme-ci-blocks';
$blockPassword = bin2hex(random_bytes(12));

run('wp user delete '.escapeshellarg($blockAccount).' --yes');

try {
test(
$expectBlocks
? 'Every block reaches the REST API'
: 'No block reaches the REST API — this framework does not register them in a web request',
function () use ($manifests, $expectBlocks, $slug, $blockAccount, $blockPassword) {
if ($manifests === []) {
return 'no block.json reached the generated theme';
}

foreach ($manifests as $manifest) {
$name = (string) (json_decode((string) file_get_contents($manifest), true)['name'] ?? '');
wpOrFail('user create '.escapeshellarg($blockAccount).' theme-ci-blocks@example.test'
.' --role=administrator --user_pass='.escapeshellarg($blockPassword).' --porcelain');

$session = new Session;
$session->logIn(
trim(wpEval('echo wp_login_url();')),
rtrim(trim(wpEval('echo admin_url();')), '/'),
$blockAccount,
$blockPassword
);

$restUrl = trim(wpEval('echo rest_url();'));
$found = [];
$absent = [];

foreach ($manifests as $manifest) {
$name = (string) (json_decode((string) file_get_contents($manifest), true)['name'] ?? '');
$status = $session->visit($restUrl.'wp/v2/block-types/'.$name)['status'];

match ($status) {
200 => $found[] = $name,
404 => $absent[] = $name,
default => throw new \RuntimeException("the REST API answered {$status} for {$name}"),
};
}

in_array($name, $registered, true) ? $found[] = $name : $absent[] = $name;
}
if ($expectBlocks) {
return $absent === []
? true
: implode(', ', $absent).' did not reach the REST API';
}

if ($expectBlocks) {
return $absent === []
// Asserted in the negative on purpose: v13.4.0 is tagged and
// cannot change, so what it does is written down. The day a
// backport changes it, this says so instead of passing either way.
return $found === []
? true
: implode(', ', $absent).' did not reach the registry';
: implode(', ', $found)." now registers on this framework; drop --expect-blocks=no for {$slug}";
}

// Asserted in the negative on purpose. Framework 13.4 boots the
// theme's providers before WordPress defines register_block_type(),
// so registerDirectory() returns early. Recording that as an
// expectation means the day it changes — a fix, a backport — this
// says so, instead of a skip quietly passing either way.
return $found === []
? true
: implode(', ', $found)." now registers on this framework; drop --expect-blocks=no for {$slug}";
}
);
);
} finally {
run('wp user delete '.escapeshellarg($blockAccount).' --yes');
}

section('Login screen — the theme\'s design, or WordPress\'s, and which one it is');

Expand Down
29 changes: 0 additions & 29 deletions bin/tests/contract.php
Original file line number Diff line number Diff line change
Expand Up @@ -217,35 +217,6 @@ function checkNoLeakedCodeName(): void
: 'style.css lost '.implode(', ', $missing).' — generated themes will carry this template\'s own header';
});

// Cheap guard for an expensive lesson. v1.4.0 registered the blocks
// straight from the provider's boot(), which runs before WordPress has
// defined register_block_type(); the registrar answers that by returning
// immediately, so the blocks simply never existed and nothing said so.
// Only the install job can prove they arrive — this says where the call
// has to live, in a second.
test('Blocks are registered on init, not from the provider boot', function () {
$provider = themePath('app/Providers/BlocksServiceProvider.php');

if (! is_file($provider)) {
return 'app/Providers/BlocksServiceProvider.php is gone — the blocks have nothing registering them';
}

$source = (string) file_get_contents($provider);

// The call, not the word: the comment above it names the method too.
$call = strpos($source, '->registerDirectory(');

if ($call === false) {
return 'the provider no longer calls registerDirectory()';
}

$hook = strpos($source, "add_action('init'");

return $hook !== false && $hook < $call
? true
: 'registerDirectory() is called before WordPress accepts block registrations, so no block will exist';
});

test('Blocks name themselves after the generated theme, not the template', function () {
$blocks = glob(themePath('resources/views/blocks/*/block.json')) ?: [];

Expand Down
5 changes: 4 additions & 1 deletion resources/views/blocks/hero/edit.jsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { useBlockProps } from '@wordpress/block-editor';
import { __ } from '@wordpress/i18n';

/**
* Mirrors save.jsx, so the editor shows the markup the page will hold.
*/
export default function Edit() {
const blockProps = useBlockProps();

return (
<div {...blockProps}>
<p>{__('Hero – Block Editor', '%theme_name%')}</p>
<p>{__('Hero', '%theme_name%')}</p>
</div>
);
}
Loading