Skip to content

Commit

Permalink
feat(WPLoader) load theme from arbitrary path
Browse files Browse the repository at this point in the history
  • Loading branch information
lucatume committed May 23, 2024
1 parent cfc712d commit 008a1e3
Show file tree
Hide file tree
Showing 9 changed files with 1,219 additions and 317 deletions.
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);
}
}
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
Loading

0 comments on commit 008a1e3

Please sign in to comment.