Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v4 - load theme from arbitrary paths #727

Merged
merged 4 commits into from
May 24, 2024
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
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ This project adheres to [Semantic Versioning](http://semver.org/).

### Added

- Allow plugins to be loaded from arbitrary paths in the `WPLoader` module.
- Allow plugins to be loaded from arbitrary absolute or relative paths in the `WPLoader` module.
- Allow themes to be loaded from arbitrary absolute or relative paths in the `WPLoader` module.
- Support an array argument for the `theme` configuration parameter in the `WPLoader` module to define `[parent-theme, child-theme]` pairs.

## [4.1.9] 2024-05-18;

Expand Down
1 change: 0 additions & 1 deletion bin/setup-wp.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
use lucatume\WPBrowser\WordPress\InstallationState\EmptyDir;
use lucatume\WPBrowser\WordPress\InstallationState\InstallationStateInterface;
use lucatume\WPBrowser\WordPress\InstallationState\Scaffolded;

$dockerComposeEnvFile = escapeshellarg(dirname(__DIR__) . '/tests/.env');
`docker compose --env-file $dockerComposeEnvFile up --wait`;

Expand Down
36 changes: 23 additions & 13 deletions docs/modules/WPLoader.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,9 @@ When used in this mode, the module supports the following configuration paramete
during normal activation and are known to work correctly when activated silently. Plugin paths can be specified
following the same format of the `plugins` parameter.
* `bootstrapActions` - a list of actions or callables to call **after** WordPress is loaded and before the tests run.
* `theme` - the theme to activate and load in the WordPress installation. The theme must be specified in slug format
like `twentytwentythree`.
* `theme` - the theme to activate and load in the WordPress installation. The theme can be specified in slug format,
e.g., `twentytwentythree`, to load it from the WordPress installation themes directory. Alternatively, the theme can
be specified as an absolute or relative path to a theme folder, e.g., `/home/themes/my-theme` or `vendor/acme/vendor-theme`. To use both a parent and ha child theme from arbitrary absolute or relative paths, define the `theme` parameter as an array of theme paths, e.g., `['/home/themes/parent-theme', '.']`.
* `AUTH_KEY` - the `AUTH_KEY` constant value to use when loading WordPress. If the `wpRootFolder` path points at a
configured installation, containing the `wp-config.php` file, then the value of the constant in the configuration file
will be used, else it will be randomly generated.
Expand Down Expand Up @@ -128,11 +129,15 @@ modules:
adminEmail: admin@wordpress.test
title: 'Integration Tests'
plugins:
- hello.php # This plugin will be loaded from the WordPress installation plugins directory.
- /home/plugins/woocommerce/woocommerce.php # This plugin will be loaded from an arbitrary absolute path.
- vendor/acme/project/plugin.php # This plugin will be loaded from an arbitrary relative path inside the project root folder.
- my-plugin.php # This plugin will be loaded from the project root folder.
theme: twentytwentythree
# This plugin will be loaded from the WordPress installation plugins directory.
- hello.php
# This plugin will be loaded from an arbitrary absolute path.
- /home/plugins/woocommerce/woocommerce.php
# This plugin will be loaded from an arbitrary relative path inside the project root folder.
- vendor/acme/project/plugin.php
# This plugin will be loaded from the project root folder.
- my-plugin.php
theme: twentytwentythree # Load the theme from the WordPress installation themes directory.
```

The following configuration uses [dynamic configuration parameters][3] to set the module configuration:
Expand All @@ -151,11 +156,12 @@ modules:
adminEmail: '%WP_ADMIN_EMAIL%'
title: '%WP_TITLE%'
plugins:
- hello.php # This plugin will be loaded from the WordPress installation plugins directory.
- /home/plugins/woocommerce/woocommerce.php # This plugin will be loaded from an arbitrary absolute path.
- my-plugin.php # This plugin will be loaded from the project root folder.
- vendor/acme/project/plugin.php # This plugin will be loaded from an arbitrary relative path inside the project root folder.
theme: twentytwentythree
- hello.php
- /home/plugins/woocommerce/woocommerce.php
- my-plugin.php
- vendor/acme/project/plugin.php
# Parent theme from the WordPress installation themes directory, child theme from absolute path.
theme: [twentytwentythree, /home/themes/my-theme]
```

The following example configuration uses a SQLite database and loads a database fixture before the tests run:
Expand All @@ -181,7 +187,11 @@ modules:
- hello.php
- woocommerce/woocommerce.php
- my-plugin/my-plugin.php
theme: twentytwentythree
theme:
# Parent theme from relative path.
- vendor/acme/parent-theme
# Child theme from the current working directory.
- .
```

The follow example configuration prevents the backup of globals and static attributes in all the tests of the suite that
Expand Down
6 changes: 4 additions & 2 deletions includes/core-phpunit/wp-tests-config.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,12 @@
}

$abspath = rtrim($wpLoaderConfig['wpRootFolder'], '\\/') . '/';
$themes = (array)$wpLoaderConfig['theme'];
$stylesheet = end($themes);

foreach ([
'ABSPATH' => $abspath,
'WP_DEFAULT_THEME' => $wpLoaderConfig['theme'],
'WP_DEFAULT_THEME' => $stylesheet,
'WP_TESTS_MULTISITE' => $wpLoaderConfig['multisite'],
'WP_DEBUG' => true,
'DB_NAME' => $wpLoaderConfig['dbName'],
Expand Down Expand Up @@ -91,7 +93,7 @@
define($const, $value);
}
}
unset($const);
unset($const, $themes, $stylesheet);

$table_prefix = $wpLoaderConfig['tablePrefix'];

Expand Down
90 changes: 76 additions & 14 deletions src/Module/WPLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
use lucatume\WPBrowser\Utils\CorePHPUnit;
use lucatume\WPBrowser\Utils\Db as DbUtils;
use lucatume\WPBrowser\Utils\Filesystem as FS;
use lucatume\WPBrowser\Utils\Property;
use lucatume\WPBrowser\Utils\Random;
use lucatume\WPBrowser\WordPress\CodeExecution\CodeExecutionFactory;
use lucatume\WPBrowser\WordPress\Database\DatabaseInterface;
Expand Down Expand Up @@ -105,7 +106,7 @@ class WPLoader extends Module
* plugins: string[],
* silentlyActivatePlugins: string[],
* bootstrapActions: string|string[],
* theme: string,
* theme: string|string[],
* AUTH_KEY: string,
* SECURE_AUTH_KEY: string,
* LOGGED_IN_KEY: string,
Expand Down Expand Up @@ -228,11 +229,13 @@ protected function validateConfig(): void

$this->config['theme'] = $this->config['WP_TESTS_MULTISITE'] ?? $this->config['theme'] ?? '';

if (!is_string($this->config['theme'])) {
if (!(
is_string($this->config['theme'])
|| (is_array($this->config['theme']) && Arr::hasShape($this->config['theme'], ['string', 'string'])))
) {
throw new ModuleConfigException(
__CLASS__,
"The `theme` configuration parameter must be a string.\n" .
"For child themes, use the child theme slug."
"The `theme` configuration parameter must be either a string, or an array of two strings."
);
}

Expand Down Expand Up @@ -349,7 +352,7 @@ public function _initialize(): void
* plugins: string[],
* silentlyActivatePlugins: string[],
* bootstrapActions: string|string[],
* theme: string,
* theme: string|string[],
* AUTH_KEY: string,
* SECURE_AUTH_KEY: string,
* LOGGED_IN_KEY: string,
Expand Down Expand Up @@ -589,7 +592,7 @@ public function getPluginsFolder(string $path = ''): string
}

/**
* Returns the absolute path to the themes directory.
* Returns the absolute path to the themes' directory.
*
* @example
* ```php
Expand Down Expand Up @@ -656,6 +659,11 @@ private function installAndBootstrapInstallation(): void

$silentPlugins = $this->config['silentlyActivatePlugins'];
$this->includeAllPlugins(array_merge($plugins, $silentPlugins), $isMultisite);
if (!empty($this->config['theme'])) {
/** @var string|array{string,string} $theme */
$theme = $this->config['theme'];
$this->switchThemeFromFile($theme);
}
$this->includeCorePHPUniteSuiteBootstrapFile();

Dispatcher::dispatch(self::EVENT_AFTER_INSTALL, $this);
Expand Down Expand Up @@ -701,15 +709,15 @@ static function (string $plugin, bool $silent) use ($closuresFactory, $multisite
)
);

/** @var string $stylesheet */
$stylesheet = $this->config['theme'];
if ($stylesheet) {
$jobs['stylesheet::' . $stylesheet] = $closuresFactory->toSwitchTheme($stylesheet, $multisite);
$themes = (array)$this->config['theme'];
foreach ($themes as $theme) {
$jobs['theme::' . basename($theme)] = $closuresFactory->toSwitchTheme($theme, $multisite);
}

$pluginsList = implode(', ', $plugins);
if ($stylesheet) {
codecept_debug('Activating plugins: ' . $pluginsList . ' and switching theme: ' . $stylesheet);
if ($themes) {
codecept_debug('Activating plugins: ' . $pluginsList
. ' and switching theme(s): ' . implode(', ', array_map('basename', $themes)));
} else {
codecept_debug('Activating plugins: ' . $pluginsList);
}
Expand All @@ -731,7 +739,7 @@ static function (string $plugin, bool $silent) use ($closuresFactory, $multisite
: $result->getStdoutBuffer();
$message = $type === 'plugin' ?
"Failed to activate plugin $name. $reason"
: "Failed to switch theme $name. $reason";
: "Failed to switch to theme $name. $reason";
throw new ModuleException(__CLASS__, $message);
}
}
Expand Down Expand Up @@ -1056,8 +1064,9 @@ private function muActivatePluginsTheme(array $plugins): array
$database = $this->db;

if ($this->config['theme']) {
$themes = (array)$this->config['theme'];
// Refresh the theme related options.
update_site_option('allowedthemes', [$this->config['theme'] => true]);
update_site_option('allowedthemes', array_combine($themes, array_fill(0, count($themes), true)));
if ($database === null) {
throw new ModuleException(
__CLASS__,
Expand Down Expand Up @@ -1152,4 +1161,57 @@ private function includeAllPlugins(array $plugins, bool $isMultisite): void
}
}, -100000);
}

/**
* @param string|array{string,string} $theme
*/
private function switchThemeFromFile(string|array $theme):void
{
[$template, $stylesheet] = is_array($theme) ? $theme : [$theme, $theme];
$templateRealpath = realpath($template);
$stylesheetRealpath = realpath($stylesheet);
$include = 0;

if ($templateRealpath) {
$include |= 1;
}

if ($stylesheetRealpath) {
$include |= 2;
}

if ($include === 0) {
return;
}

/** @var string $templateRealpath */
/** @var string $stylesheetRealpath */

PreloadFilters::addFilter('after_setup_theme', static function () use (
$include,
$templateRealpath,
$stylesheetRealpath
) {
global $wp_stylesheet_path, $wp_template_path, $wp_theme_directories;
($include & 1) && $wp_template_path = $templateRealpath;
($include & 2) && $wp_stylesheet_path = $stylesheetRealpath;
($include & 1) && ($wp_theme_directories[] = dirname($templateRealpath));
($include & 2) && ($wp_theme_directories[] = dirname($stylesheetRealpath));
$wp_theme_directories = array_values(array_unique($wp_theme_directories));
// Stylesheet first, template second.
(($include & 2) && ($stylesheetRealpath !== $templateRealpath))
&& include $stylesheetRealpath . '/functions.php';
($include & 1) && include $templateRealpath . '/functions.php';
}, -100000);

$templateName = basename($templateRealpath);
$templateRoot = dirname($templateRealpath);
$stylesheetName = basename($stylesheetRealpath);
$stylesheetRoot = dirname($stylesheetRealpath);

PreloadFilters::addFilter('pre_option_template', static fn() => $templateName);
PreloadFilters::addFilter('pre_option_template_root', static fn() => $templateRoot);
PreloadFilters::addFilter('pre_option_stylesheet', static fn() => $stylesheetName);
PreloadFilters::addFilter('pre_option_stylesheet_root', static fn() => $stylesheetRoot);
}
}
5 changes: 3 additions & 2 deletions src/Template/Wpbrowser.php
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,12 @@ private function createIntegrationSuite(ProjectInterface $project): void
{
$plugins = '';
if ($project instanceof PluginProject) {
$plugins = "'{$project->getActivationString()}'";
$basename = basename($project->getActivationString());
$plugins = "'." . DIRECTORY_SEPARATOR . "{$basename}'";
}
$theme = '';
if ($project instanceof ThemeProject) {
$theme = $project->getActivationString();
$theme = '.';
}

$suiteConfig = <<<EOF
Expand Down
26 changes: 24 additions & 2 deletions src/WordPress/CodeExecution/ThemeSwitchAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,21 @@ private function switchTheme(string $stylesheet, bool $multisite): void
{
// The `switch_theme` function will not complain about a missing theme: check it now.
$theme = wp_get_theme($stylesheet);
if (!($theme instanceof WP_Theme && $theme->exists())) {
throw new InstallationException("Theme $stylesheet does not exist.");

if (!($theme instanceof WP_Theme && $theme->exists() && !$theme->errors())) {
$themeRealPath = realpath($stylesheet);

if ($themeRealPath && is_dir($themeRealPath) && is_file($themeRealPath . '/style.css')) {
$this->loadThemeFromFile($themeRealPath, $multisite);
return;
}

$message = "Errors with theme $stylesheet.";
if ($theme->errors()) {
$message = implode(', ', $theme->errors()->get_error_messages());
}

throw new InstallationException($message);
}

if ($multisite) {
Expand All @@ -53,4 +66,13 @@ public function getClosure(): Closure
return $request->execute();
};
}

private function loadThemeFromFile(string $themeRealPath, bool $multisite): void
{
include_once $themeRealPath . '/functions.php';
$basename = basename($themeRealPath);
update_option('template', $basename);
update_option('stylesheet', $basename);
do_action('after_setup_theme');
}
}
1 change: 0 additions & 1 deletion tests/_data/themes/dummy/style.css
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
/*
Theme Name: Dummy
Description: Dummy theme.
Template: dummy
Version: 0.1.0
*/
4 changes: 2 additions & 2 deletions tests/_support/_generated/WploaderTesterActions.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<?php //[STAMP] b4b2d5c74a9a68974589fd65502547ce
<?php //[STAMP] a9a237b1518f3878f1c2f5e7920998b6
// phpcs:ignoreFile
namespace _generated;

Expand Down Expand Up @@ -67,7 +67,7 @@ public function getPluginsFolder(string $path = ""): string {
/**
* [!] Method is generated. Documentation taken from corresponding module.
*
* Returns the absolute path to the themes directory.
* Returns the absolute path to the themes' directory.
*
* @example
* ```php
Expand Down
29 changes: 28 additions & 1 deletion tests/unit/Codeception/Template/WpbrowserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,15 @@ public function should_scaffold_for_theme_correctly(): void
$projectDir = FS::tmpDir('setup_', [
'theme_23' => [
'style.css' => "/*\nTheme Name: Theme 23\n*/",
'functions.php' => <<< PHP
<?php
function theme_23_some_function() {
return 'test-test-test';
}

add_action('after_setup_theme', 'theme_23_some_function');
PHP,
'index.php' => '<?php // This file is required for the theme to work. ?>',
'composer.json' => $composerFileCode,
'vendor' => [
'bin' => [
Expand Down Expand Up @@ -392,9 +401,18 @@ public function should_scaffold_for_child_theme_correctly(): void
'style.css' => <<< EOT
/*
Theme Name: Theme 23
Template: twentytwenty
Template: twentytwentyfour
*/
EOT,
'functions.php' => <<< PHP
<?php
function theme_23_some_function() {
return 'test-test-test';
}

add_action('after_setup_theme', 'theme_23_some_function');
PHP,
'index.php' => '<?php // This file is required for the theme to work. ?>',
'composer.json' => $composerFileCode,
'vendor' => [
'bin' => [
Expand Down Expand Up @@ -464,6 +482,15 @@ public function should_scaffold_for_theme_custom_correctly(): void
Theme Name: Theme 23
*/
EOT,
'functions.php' => <<< PHP
<?php
function theme_23_some_function() {
return 'test-test-test';
}

add_action('after_setup_theme', 'theme_23_some_function');
PHP,
'index.php' => '<?php // This file is required for the theme to work. ?>',
'composer.json' => $composerFileCode,
'vendor' => [
'bin' => [
Expand Down
Loading
Loading