Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
91cb15a
chore: point the submodule at documentation's merged main
ogorzalka Sep 24, 2026
a2ba78a
Merge pull request #328 from Pollora/chore/documentation-submodule-bl…
ogorzalka Sep 24, 2026
05ef284
test: expect theme-default's hero preview to match the page
ogorzalka Sep 25, 2026
2270203
Merge pull request #329 from Pollora/test/hero-preview-matches-the-page
ogorzalka Sep 25, 2026
50966d1
test: cover the template hierarchy in the browser tests
ogorzalka Sep 25, 2026
75e8f5c
style: declare strict types in the hierarchy fixture scripts
ogorzalka Sep 25, 2026
a4ad625
refactor: type the fixture routes' closures
ogorzalka Sep 25, 2026
6aec1c5
Merge pull request #330 from Pollora/test/e2e-template-hierarchy
ogorzalka Sep 25, 2026
cc9f68b
test: cover the framework's features in the browser tests
ogorzalka Sep 25, 2026
436eb5d
Merge pull request #331 from Pollora/test/e2e-framework-features
ogorzalka Sep 25, 2026
8030b6d
test: check that __() sends text domains to WordPress in the browser …
ogorzalka Sep 25, 2026
83715ef
ci: run the browser tests in Firefox and WebKit every night
ogorzalka Sep 25, 2026
1ea8e96
test: load the fr_FR catalogue as the current locale's
ogorzalka Sep 25, 2026
72426c7
Merge pull request #332 from Pollora/test/e2e-i18n-and-nightly-browsers
ogorzalka Sep 25, 2026
9bba9be
chore: gather the v13.32.0-beta.8 entries under the version being tagged
ogorzalka Sep 25, 2026
84ae006
Merge pull request #333 from Pollora/release/v13.32.0-beta.8
ogorzalka Sep 25, 2026
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
12 changes: 11 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ on:
branches: [main, develop, 'feature/**', 'hotfix/**', 'release/**']
pull_request:
branches: [main, develop]
# Nightly, on the default branch: the browser tests in Firefox and WebKit as well as
# Chromium, which pull requests run alone to stay fast.
schedule:
- cron: '30 3 * * *'
workflow_dispatch:

jobs:
tests:
Expand Down Expand Up @@ -238,6 +243,11 @@ jobs:
# authoritative: a deployment dumped with --classmap-authoritative answers
# from the classmap alone. Theme and plugin classes are autoloaded at
# runtime, and every front-end page once answered 404 there (#320).
# Chromium on pushes and pull requests; all three engines on the nightly and on a
# manual run.
env:
E2E_BROWSERS: ${{ (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && 'chromium,firefox,webkit' || 'chromium' }}

strategy:
fail-fast: false
matrix:
Expand Down Expand Up @@ -328,7 +338,7 @@ jobs:
working-directory: packages/framework/tests/e2e
run: |
npm ci
npx playwright install --with-deps chromium
npx playwright install --with-deps ${E2E_BROWSERS//,/ }

# DDEV serves HTTPS with a mkcert certificate when mkcert is present.
# Say which case the runner is in rather than guessing.
Expand Down
11 changes: 10 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,16 @@ All notable changes to the Pollora framework will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased](https://github.com/Pollora/framework/compare/v13.32.0-beta.7...develop)
## [Unreleased](https://github.com/Pollora/framework/compare/v13.32.0-beta.8...develop)

## [v13.32.0-beta.8](https://github.com/Pollora/framework/compare/v13.32.0-beta.7...v13.32.0-beta.8) - 2026-09-25

No change to the framework's code: this beta ships the browser test suite that now guards it, 73 tests per browser.

### Added
- Browser tests of the template hierarchy. Two fixture themes: one has a template for every case, the other `index.blade.php` alone. Each URL is read in the framework's template marker, in the template's own output and in the HTTP status: `front-page`, `home`, `page-{slug}`, `page-{id}`, a custom page template, `page`, `single`, `single-{type}`, `archive-{type}`, `archive`, `taxonomy-{tax}-{term}`, `taxonomy-{tax}`, `category-{slug}`, `tag`, `author-{nicename}`, `date`, `search`, and `404` with a 404 status; a Blade view over a PHP template of the same name; `Route::wp()` and Laravel routes over the hierarchy. With `index` alone every case falls back to it, and an unknown URL still answers 404. Replayed: without the 404 view, or with the marker disabled, the suite fails
- Browser tests of the framework's features, through a fixture plugin that declares each by attribute and checks it by its effect: `#[Filter]` and `#[Action]`, a `#[PostType]` (REST type, archive, admin menu), `#[WpRestRoute]` (a public route; an `IsAdmin` one that refuses a visitor and answers an administrator), `#[Ajax]` for visitors and logged-in users, a script enqueued through the `Asset` facade, every script and stylesheet of the home page loading with nothing over plain `http://`, `get_theme_file_uri()` giving the active theme's built entry a URL that answers, no server path in the page a visitor gets, the theme's menu locations and login screen, and `__()` sending a text domain to WordPress's catalogues and replacements to Laravel. Replayed: with the v13.32.0-beta.6 theme URI fix undone, or with WordPress's side of `__()` disabled, the suite fails
- The browser tests run in Firefox and WebKit as well as Chromium every night, and on a manual run of the workflow (`E2E_BROWSERS`). Pull requests keep Chromium alone. The schedule runs from `main`, so it starts with this release

## [v13.32.0-beta.7](https://github.com/Pollora/framework/compare/v13.32.0-beta.6...v13.32.0-beta.7) - 2026-09-24

Expand Down
2 changes: 1 addition & 1 deletion documentation
22 changes: 22 additions & 0 deletions tests/e2e/bin/fixtures.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@
# - when theme-default is the active theme, a dynamic block made in it
# by pollora:make:block. Any other theme is left alone: it may be
# someone's work in progress.
# - the e2e-features plugin, committed in fixtures/plugins: framework
# features declared by attribute (hooks, a post type, REST routes,
# an Ajax action) and a script enqueued through the Asset facade;
# - the template hierarchy themes, e2e-full and e2e-index, copied into
# themes/. They are not activated here: the hierarchy spec activates
# each in turn and gives the site its own theme back.
# down remove all of it, and give modules_statuses.json back as it was
set -euo pipefail

Expand Down Expand Up @@ -69,6 +75,17 @@ case "${1:-}" in
build "$MODULE_DIR"
php artisan module:enable "$MODULE" --no-interaction

# Framework features plugin
rm -rf "public/content/plugins/e2e-features"
cp -r "$FIXTURES/plugins/e2e-features" "public/content/plugins/e2e-features"
wp plugin activate e2e-features

# Template hierarchy themes
for theme in e2e-full e2e-index; do
rm -rf "themes/$theme"
cp -r "$FIXTURES/themes/$theme" "themes/$theme"
done

# Theme, only when it is theme-default
if [ "$(wp theme list --status=active --field=name)" = "$THEME" ]; then
php artisan pollora:make:block "$THEME_BLOCK" --theme="$THEME" --title="Theme Card" --force --no-interaction
Expand All @@ -87,6 +104,11 @@ case "${1:-}" in
rm -f "$STATUSES_BACKUP"
fi

wp plugin deactivate e2e-features 2>/dev/null || true
rm -rf public/content/plugins/e2e-features

rm -rf themes/e2e-full themes/e2e-index

# The theme block is only ever made in theme-default, and removed from it
rm -rf "$THEME_DIR/resources/views/blocks/$THEME_BLOCK"
;;
Expand Down
39 changes: 39 additions & 0 deletions tests/e2e/fixtures/hierarchy/cleanup.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

declare(strict_types=1);

/**
* Remove everything seed.php creates, whether or not it all exists: it also
* runs before seeding, to clear what an interrupted run left behind. Run with
* e2e-full active, so the e2e_genre terms can still be deleted.
*/
$posts = get_posts([
'post_type' => 'any',
'post_status' => 'any',
'posts_per_page' => -1,
'post_name__in' => [
'e2e-hierarchy-front', 'e2e-hierarchy-blog', 'e2e-hierarchy-about', 'e2e-hierarchy-by-id',
'e2e-hierarchy-plain', 'e2e-hierarchy-landing', 'e2e-hierarchy-routed', 'e2e-hierarchy-laravel',
'e2e-hierarchy-php', 'e2e-hierarchy-post', 'e2e-hierarchy-book', 'e2e-hierarchy-note',
],
'fields' => 'ids',
]);

foreach ($posts as $id) {
wp_delete_post($id, true);
}

foreach ([['category', 'e2e-hierarchy-cat'], ['post_tag', 'e2e-hierarchy-tag'], ['e2e_genre', 'e2e-rock'], ['e2e_genre', 'e2e-jazz']] as [$taxonomy, $slug]) {
$term = get_term_by('slug', $slug, $taxonomy);

if ($term) {
wp_delete_term($term->term_id, $taxonomy);
}
}

$author = get_user_by('login', 'e2e-hierarchy-author');

if ($author) {
require_once ABSPATH.'wp-admin/includes/user.php';
wp_delete_user($author->ID);
}
98 changes: 98 additions & 0 deletions tests/e2e/fixtures/hierarchy/seed.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<?php

declare(strict_types=1);

/**
* The content the template hierarchy spec resolves, run through `wp eval` with
* e2e-full active (its post types and taxonomy must be registered).
*
* Slugs are fixed because the fixture templates are named after them
* (page-e2e-hierarchy-about, category-e2e-hierarchy-cat, …). Prints the URL of
* every case, and the ids the spec needs, as JSON.
*/
$page = static fn (string $slug, array $extra = []): int => wp_insert_post([
'post_type' => 'page',
'post_status' => 'publish',
'post_title' => $slug,
'post_name' => $slug,
...$extra,
], true);

$authorId = wp_insert_user([
'user_login' => 'e2e-hierarchy-author',
'user_nicename' => 'e2e-hierarchy-author',
'user_email' => 'e2e-hierarchy-author@example.test',
'user_pass' => wp_generate_password(24),
'role' => 'author',
]);

$ids = [
'front' => $page('e2e-hierarchy-front'),
'blog' => $page('e2e-hierarchy-blog'),
'about' => $page('e2e-hierarchy-about'),
'byId' => $page('e2e-hierarchy-by-id'),
'plain' => $page('e2e-hierarchy-plain'),
'landing' => $page('e2e-hierarchy-landing', ['meta_input' => ['_wp_page_template' => 'templates/landing.blade.php']]),
'routed' => $page('e2e-hierarchy-routed'),
'laravel' => $page('e2e-hierarchy-laravel'),
'php' => $page('e2e-hierarchy-php'),
];

$category = wp_insert_term('E2E hierarchy category', 'category', ['slug' => 'e2e-hierarchy-cat']);
$rock = wp_insert_term('E2E rock', 'e2e_genre', ['slug' => 'e2e-rock']);
$jazz = wp_insert_term('E2E jazz', 'e2e_genre', ['slug' => 'e2e-jazz']);

$ids['post'] = wp_insert_post([
'post_type' => 'post',
'post_status' => 'publish',
'post_title' => 'e2e-hierarchy-post',
'post_name' => 'e2e-hierarchy-post',
'post_author' => $authorId,
'post_date' => '2020-01-15 10:00:00',
'post_category' => [$category['term_id']],
'tags_input' => ['e2e-hierarchy-tag'],
], true);

$ids['book'] = wp_insert_post(['post_type' => 'e2e_book', 'post_status' => 'publish', 'post_title' => 'e2e-hierarchy-book', 'post_name' => 'e2e-hierarchy-book'], true);
wp_set_object_terms($ids['book'], [$rock['term_id'], $jazz['term_id']], 'e2e_genre');
$ids['note'] = wp_insert_post(['post_type' => 'e2e_note', 'post_status' => 'publish', 'post_title' => 'e2e-hierarchy-note', 'post_name' => 'e2e-hierarchy-note'], true);

foreach ([$authorId, $category, $rock, $jazz, ...array_values($ids)] as $created) {
if (is_wp_error($created)) {
fwrite(STDERR, $created->get_error_message()."\n");
exit(1);
}
}

// A static front page and a posts page, so front-page and home are both reachable.
update_option('show_on_front', 'page');
update_option('page_on_front', $ids['front']);
update_option('page_for_posts', $ids['blog']);

echo json_encode([
'ids' => $ids,
'urls' => [
'front' => home_url('/'),
'blog' => get_permalink($ids['blog']),
'about' => get_permalink($ids['about']),
'byId' => get_permalink($ids['byId']),
'plain' => get_permalink($ids['plain']),
'landing' => get_permalink($ids['landing']),
'routed' => get_permalink($ids['routed']),
'laravel' => get_permalink($ids['laravel']),
'php' => get_permalink($ids['php']),
'post' => get_permalink($ids['post']),
'book' => get_permalink($ids['book']),
'bookArchive' => get_post_type_archive_link('e2e_book'),
'note' => get_permalink($ids['note']),
'noteArchive' => get_post_type_archive_link('e2e_note'),
'rock' => get_term_link('e2e-rock', 'e2e_genre'),
'jazz' => get_term_link('e2e-jazz', 'e2e_genre'),
'category' => get_category_link($category['term_id']),
'tag' => get_term_link('e2e-hierarchy-tag', 'post_tag'),
'author' => get_author_posts_url($authorId),
'date' => get_month_link(2020, 1),
'search' => home_url('/?s=e2e-hierarchy'),
'notFound' => home_url('/e2e-hierarchy-nothing-here'),
],
]);
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace Plugin\E2eFeatures\Endpoints;

use Pollora\Attributes\WpRestRoute;
use Pollora\Attributes\WpRestRoute\Method;
use Pollora\WpRest\Permissions\IsAdmin;

/**
* Administrators only; the id comes from the route.
*/
#[WpRestRoute('e2e/v1', '/admin/(?P<id>\d+)', permissionCallback: IsAdmin::class)]
class AdminStatus
{
/** @return array{route: string, id: int} */
#[Method('GET')]
public function show(string $id): array
{
return ['route' => 'admin', 'id' => (int) $id];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

declare(strict_types=1);

namespace Plugin\E2eFeatures\Endpoints;

use Pollora\Attributes\WpRestRoute;
use Pollora\Attributes\WpRestRoute\Method;

/**
* No permission declared: the route is public.
*/
#[WpRestRoute('e2e/v1', '/public')]
class PublicStatus
{
/** @return array{route: string} */
#[Method('GET')]
public function show(): array
{
return ['route' => 'public'];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace Plugin\E2eFeatures\Endpoints;

use Pollora\Attributes\WpRestRoute;
use Pollora\Attributes\WpRestRoute\Method;

/**
* What `__()` answers, on each side of the helper that shares the name: WordPress keeps
* its own as `__wp()`, and `__()` sends a call with a text domain to WordPress and a call
* with replacements to Laravel. This plugin's fr_FR catalogue is what WordPress finds,
* whatever the site's language.
*/
#[WpRestRoute('e2e/v1', '/translations')]
class Translations
{
/** @return array{wordpress: string, untranslated: string, laravel: string, wpNative: string} */
#[Method('GET')]
public function show(): array
{
// Loaded as the catalogue of the current locale, whatever the site's language:
// since WordPress 6.5 translations are kept per locale, so loading it as fr_FR
// on an en_US site would leave __() nothing to find.
unload_textdomain('e2e-features');
load_textdomain('e2e-features', WP_PLUGIN_DIR.'/e2e-features/languages/e2e-features-fr_FR.mo', determine_locale());

return [
'wordpress' => __('E2E greeting', 'e2e-features'),
'untranslated' => __('E2E string outside the catalogue', 'e2e-features'),
'laravel' => __('Shipping :brand', ['brand' => 'Example']),
'wpNative' => __wp('E2E greeting', 'e2e-features'),
];
}
}
19 changes: 19 additions & 0 deletions tests/e2e/fixtures/plugins/e2e-features/app/Event.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace Plugin\E2eFeatures;

use Pollora\Attributes\PostType;
use Pollora\Attributes\PostType\HasArchive;
use Pollora\Attributes\PostType\PublicPostType;
use Pollora\Attributes\PostType\ShowInRest;

/**
* A post type declared by a plugin, not by the theme.
*/
#[PostType('e2e_event', singular: 'E2E Event', plural: 'E2E Events')]
#[PublicPostType]
#[HasArchive]
#[ShowInRest]
class Event {}
Loading
Loading