diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e33f10d5..eb1773cfd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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; diff --git a/bin/setup-wp.php b/bin/setup-wp.php index 2c389586c..62fa0f39a 100644 --- a/bin/setup-wp.php +++ b/bin/setup-wp.php @@ -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`; diff --git a/docs/modules/WPLoader.md b/docs/modules/WPLoader.md index 99309a6c8..021ceb56b 100644 --- a/docs/modules/WPLoader.md +++ b/docs/modules/WPLoader.md @@ -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. @@ -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: @@ -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: @@ -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 diff --git a/includes/core-phpunit/wp-tests-config.php b/includes/core-phpunit/wp-tests-config.php index 0e64ab9b4..e944a1143 100644 --- a/includes/core-phpunit/wp-tests-config.php +++ b/includes/core-phpunit/wp-tests-config.php @@ -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'], @@ -91,7 +93,7 @@ define($const, $value); } } -unset($const); +unset($const, $themes, $stylesheet); $table_prefix = $wpLoaderConfig['tablePrefix']; diff --git a/src/Module/WPLoader.php b/src/Module/WPLoader.php index 650b8d0d4..66c16f3be 100644 --- a/src/Module/WPLoader.php +++ b/src/Module/WPLoader.php @@ -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; @@ -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, @@ -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." ); } @@ -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, @@ -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 @@ -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); @@ -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); } @@ -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); } } @@ -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__, @@ -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); + } } diff --git a/src/Template/Wpbrowser.php b/src/Template/Wpbrowser.php index 693411849..84dfafcd9 100644 --- a/src/Template/Wpbrowser.php +++ b/src/Template/Wpbrowser.php @@ -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 = <<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) { @@ -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'); + } } diff --git a/tests/_data/themes/dummy/style.css b/tests/_data/themes/dummy/style.css index b1cb6ff28..47c310536 100755 --- a/tests/_data/themes/dummy/style.css +++ b/tests/_data/themes/dummy/style.css @@ -1,6 +1,5 @@ /* Theme Name: Dummy Description: Dummy theme. -Template: dummy Version: 0.1.0 */ diff --git a/tests/_support/_generated/WploaderTesterActions.php b/tests/_support/_generated/WploaderTesterActions.php index 78c4ba56a..4b032a83d 100644 --- a/tests/_support/_generated/WploaderTesterActions.php +++ b/tests/_support/_generated/WploaderTesterActions.php @@ -1,4 +1,4 @@ - [ 'style.css' => "/*\nTheme Name: Theme 23\n*/", + 'functions.php' => <<< PHP + '', 'composer.json' => $composerFileCode, 'vendor' => [ 'bin' => [ @@ -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 + '', 'composer.json' => $composerFileCode, 'vendor' => [ 'bin' => [ @@ -464,6 +482,15 @@ public function should_scaffold_for_theme_custom_correctly(): void Theme Name: Theme 23 */ EOT, + 'functions.php' => <<< PHP + '', 'composer.json' => $composerFileCode, 'vendor' => [ 'bin' => [ diff --git a/tests/unit/Codeception/Template/__snapshots__/WpbrowserTest__should_scaffold_for_child_theme_correctly__0.snapshot b/tests/unit/Codeception/Template/__snapshots__/WpbrowserTest__should_scaffold_for_child_theme_correctly__0.snapshot index 2a777b596..d3065ab51 100644 --- a/tests/unit/Codeception/Template/__snapshots__/WpbrowserTest__should_scaffold_for_child_theme_correctly__0.snapshot +++ b/tests/unit/Codeception/Template/__snapshots__/WpbrowserTest__should_scaffold_for_child_theme_correctly__0.snapshot @@ -1,3 +1,16 @@ +>>> /functions.php >>> +>> /index.php >>> + +<<< /index.php <<< + >>> /tests/EndToEnd.suite.yml >>> # Integration suite configuration # @@ -125,7 +138,7 @@ modules: adminEmail: 'admin@%WORDPRESS_DOMAIN%' title: 'Integration Tests' plugins: [] - theme: 'theme_23' + theme: '.' <<< /tests/Integration.suite.yml <<< >>> /tests/Support/IntegrationTester.php >>> @@ -257,8 +270,8 @@ TEST_TABLE_PREFIX=test_ WORDPRESS_TABLE_PREFIX=wp_ # The URL and domain of the WordPress site used in end-to-end tests. -WORDPRESS_URL=http://localhost:39249 -WORDPRESS_DOMAIN=localhost:39249 +WORDPRESS_URL=http://localhost:59860 +WORDPRESS_DOMAIN=localhost:59860 WORDPRESS_ADMIN_PATH=/wp-admin # The username and password of the administrator user of the WordPress site used in end-to-end tests. @@ -267,10 +280,10 @@ WORDPRESS_ADMIN_PASSWORD=password # The host and port of the ChromeDriver server that will be used in end-to-end tests. CHROMEDRIVER_HOST=localhost -CHROMEDRIVER_PORT=43995 +CHROMEDRIVER_PORT=21551 # The port on which the PHP built-in server will serve the WordPress installation. -BUILTIN_SERVER_PORT=39249 +BUILTIN_SERVER_PORT=59860 <<< /tests/.env <<< @@ -328,7 +341,7 @@ extensions: >>> /style.css >>> /* Theme Name: Theme 23 -Template: twentytwenty +Template: twentytwentyfour */ <<< /style.css <<< diff --git a/tests/unit/Codeception/Template/__snapshots__/WpbrowserTest__should_scaffold_for_plugin_with_non_plugin_php_file__0.snapshot b/tests/unit/Codeception/Template/__snapshots__/WpbrowserTest__should_scaffold_for_plugin_with_non_plugin_php_file__0.snapshot index 94aa0c6c0..4e155a908 100644 --- a/tests/unit/Codeception/Template/__snapshots__/WpbrowserTest__should_scaffold_for_plugin_with_non_plugin_php_file__0.snapshot +++ b/tests/unit/Codeception/Template/__snapshots__/WpbrowserTest__should_scaffold_for_plugin_with_non_plugin_php_file__0.snapshot @@ -124,7 +124,7 @@ modules: domain: '%WORDPRESS_DOMAIN%' adminEmail: 'admin@%WORDPRESS_DOMAIN%' title: 'Integration Tests' - plugins: ['plugin_89/main-file.php'] + plugins: ['./main-file.php'] theme: '' <<< /tests/Integration.suite.yml <<< @@ -265,8 +265,8 @@ TEST_TABLE_PREFIX=test_ WORDPRESS_TABLE_PREFIX=wp_ # The URL and domain of the WordPress site used in end-to-end tests. -WORDPRESS_URL=http://localhost:53890 -WORDPRESS_DOMAIN=localhost:53890 +WORDPRESS_URL=http://localhost:43626 +WORDPRESS_DOMAIN=localhost:43626 WORDPRESS_ADMIN_PATH=/wp-admin # The username and password of the administrator user of the WordPress site used in end-to-end tests. @@ -275,10 +275,10 @@ WORDPRESS_ADMIN_PASSWORD=password # The host and port of the ChromeDriver server that will be used in end-to-end tests. CHROMEDRIVER_HOST=localhost -CHROMEDRIVER_PORT=27014 +CHROMEDRIVER_PORT=35449 # The port on which the PHP built-in server will serve the WordPress installation. -BUILTIN_SERVER_PORT=53890 +BUILTIN_SERVER_PORT=43626 <<< /tests/.env <<< diff --git a/tests/unit/Codeception/Template/__snapshots__/WpbrowserTest__should_scaffold_for_plugin_with_non_plugin_php_file_custom__0.snapshot b/tests/unit/Codeception/Template/__snapshots__/WpbrowserTest__should_scaffold_for_plugin_with_non_plugin_php_file_custom__0.snapshot index 8f0699f8f..77f9d15dd 100644 --- a/tests/unit/Codeception/Template/__snapshots__/WpbrowserTest__should_scaffold_for_plugin_with_non_plugin_php_file_custom__0.snapshot +++ b/tests/unit/Codeception/Template/__snapshots__/WpbrowserTest__should_scaffold_for_plugin_with_non_plugin_php_file_custom__0.snapshot @@ -89,7 +89,7 @@ modules: domain: '%WORDPRESS_DOMAIN%' adminEmail: 'admin@%WORDPRESS_DOMAIN%' title: 'Integration Tests' - plugins: ['plugin_89/main.php'] + plugins: ['./main.php'] theme: '' <<< /tests/Integration.suite.yml <<< diff --git a/tests/unit/Codeception/Template/__snapshots__/WpbrowserTest__should_scaffold_for_plugin_with_plugin_php_file__0.snapshot b/tests/unit/Codeception/Template/__snapshots__/WpbrowserTest__should_scaffold_for_plugin_with_plugin_php_file__0.snapshot index 56be82bd8..28262e513 100644 --- a/tests/unit/Codeception/Template/__snapshots__/WpbrowserTest__should_scaffold_for_plugin_with_plugin_php_file__0.snapshot +++ b/tests/unit/Codeception/Template/__snapshots__/WpbrowserTest__should_scaffold_for_plugin_with_plugin_php_file__0.snapshot @@ -129,7 +129,7 @@ modules: domain: '%WORDPRESS_DOMAIN%' adminEmail: 'admin@%WORDPRESS_DOMAIN%' title: 'Integration Tests' - plugins: ['plugin_89/plugin.php'] + plugins: ['./plugin.php'] theme: '' <<< /tests/Integration.suite.yml <<< @@ -270,8 +270,8 @@ TEST_TABLE_PREFIX=test_ WORDPRESS_TABLE_PREFIX=wp_ # The URL and domain of the WordPress site used in end-to-end tests. -WORDPRESS_URL=http://localhost:19501 -WORDPRESS_DOMAIN=localhost:19501 +WORDPRESS_URL=http://localhost:14326 +WORDPRESS_DOMAIN=localhost:14326 WORDPRESS_ADMIN_PATH=/wp-admin # The username and password of the administrator user of the WordPress site used in end-to-end tests. @@ -280,10 +280,10 @@ WORDPRESS_ADMIN_PASSWORD=password # The host and port of the ChromeDriver server that will be used in end-to-end tests. CHROMEDRIVER_HOST=localhost -CHROMEDRIVER_PORT=13899 +CHROMEDRIVER_PORT=34581 # The port on which the PHP built-in server will serve the WordPress installation. -BUILTIN_SERVER_PORT=19501 +BUILTIN_SERVER_PORT=14326 <<< /tests/.env <<< diff --git a/tests/unit/Codeception/Template/__snapshots__/WpbrowserTest__should_scaffold_for_plugin_with_plugin_php_file_custom__0.snapshot b/tests/unit/Codeception/Template/__snapshots__/WpbrowserTest__should_scaffold_for_plugin_with_plugin_php_file_custom__0.snapshot index 3eb25f223..0734db2f2 100644 --- a/tests/unit/Codeception/Template/__snapshots__/WpbrowserTest__should_scaffold_for_plugin_with_plugin_php_file_custom__0.snapshot +++ b/tests/unit/Codeception/Template/__snapshots__/WpbrowserTest__should_scaffold_for_plugin_with_plugin_php_file_custom__0.snapshot @@ -89,7 +89,7 @@ modules: domain: '%WORDPRESS_DOMAIN%' adminEmail: 'admin@%WORDPRESS_DOMAIN%' title: 'Integration Tests' - plugins: ['plugin_89/plugin.php'] + plugins: ['./plugin.php'] theme: '' <<< /tests/Integration.suite.yml <<< diff --git a/tests/unit/Codeception/Template/__snapshots__/WpbrowserTest__should_scaffold_for_theme_correctly__0.snapshot b/tests/unit/Codeception/Template/__snapshots__/WpbrowserTest__should_scaffold_for_theme_correctly__0.snapshot index 82c54f0aa..7e4d61fd6 100644 --- a/tests/unit/Codeception/Template/__snapshots__/WpbrowserTest__should_scaffold_for_theme_correctly__0.snapshot +++ b/tests/unit/Codeception/Template/__snapshots__/WpbrowserTest__should_scaffold_for_theme_correctly__0.snapshot @@ -1,3 +1,16 @@ +>>> /functions.php >>> +>> /index.php >>> + +<<< /index.php <<< + >>> /tests/EndToEnd.suite.yml >>> # Integration suite configuration # @@ -125,7 +138,7 @@ modules: adminEmail: 'admin@%WORDPRESS_DOMAIN%' title: 'Integration Tests' plugins: [] - theme: 'theme_23' + theme: '.' <<< /tests/Integration.suite.yml <<< >>> /tests/Support/IntegrationTester.php >>> @@ -257,8 +270,8 @@ TEST_TABLE_PREFIX=test_ WORDPRESS_TABLE_PREFIX=wp_ # The URL and domain of the WordPress site used in end-to-end tests. -WORDPRESS_URL=http://localhost:61144 -WORDPRESS_DOMAIN=localhost:61144 +WORDPRESS_URL=http://localhost:8431 +WORDPRESS_DOMAIN=localhost:8431 WORDPRESS_ADMIN_PATH=/wp-admin # The username and password of the administrator user of the WordPress site used in end-to-end tests. @@ -267,10 +280,10 @@ WORDPRESS_ADMIN_PASSWORD=password # The host and port of the ChromeDriver server that will be used in end-to-end tests. CHROMEDRIVER_HOST=localhost -CHROMEDRIVER_PORT=17476 +CHROMEDRIVER_PORT=44085 # The port on which the PHP built-in server will serve the WordPress installation. -BUILTIN_SERVER_PORT=61144 +BUILTIN_SERVER_PORT=8431 <<< /tests/.env <<< diff --git a/tests/unit/Codeception/Template/__snapshots__/WpbrowserTest__should_scaffold_for_theme_custom_correctly__0.snapshot b/tests/unit/Codeception/Template/__snapshots__/WpbrowserTest__should_scaffold_for_theme_custom_correctly__0.snapshot index 6e8c18c1f..6cee5d442 100644 --- a/tests/unit/Codeception/Template/__snapshots__/WpbrowserTest__should_scaffold_for_theme_custom_correctly__0.snapshot +++ b/tests/unit/Codeception/Template/__snapshots__/WpbrowserTest__should_scaffold_for_theme_custom_correctly__0.snapshot @@ -1,3 +1,16 @@ +>>> /functions.php >>> +>> /index.php >>> + +<<< /index.php <<< + >>> /composer >>> #!/bin/sh touch composer.lock @@ -92,7 +105,7 @@ modules: adminEmail: 'admin@%WORDPRESS_DOMAIN%' title: 'Integration Tests' plugins: [] - theme: 'theme_23' + theme: '.' <<< /tests/Integration.suite.yml <<< >>> /tests/Support/IntegrationTester.php >>> diff --git a/tests/unit/lucatume/WPBrowser/Module/WPLoaderArbitraryPluginLocationTest.php b/tests/unit/lucatume/WPBrowser/Module/WPLoaderArbitraryPluginLocationTest.php new file mode 100644 index 000000000..84c7c1bf3 --- /dev/null +++ b/tests/unit/lucatume/WPBrowser/Module/WPLoaderArbitraryPluginLocationTest.php @@ -0,0 +1,338 @@ +mockModuleContainer = new ModuleContainer(new Di(), $moduleContainerConfig); + return new WPLoader($this->mockModuleContainer, ($moduleConfig ?? $this->config)); + } + + /** + * It should allow loading a plugin from an arbitrary path + * + * @test + */ + public function should_allow_loading_a_plugin_from_an_arbitrary_path(): void + { + $myPluginCode = <<< PHP + $myPluginCode, + + 'var' => [ + 'wordpress' => [] + ], + 'vendor' => [ + 'acme' => [ + + ] + ] + ]); + $wpRootDir = $projectDir . '/var/wordpress'; + $installation = Installation::scaffold($wpRootDir); + $dbName = Random::dbName(); + $dbHost = Env::get('WORDPRESS_DB_HOST'); + $dbUser = Env::get('WORDPRESS_DB_USER'); + $dbPassword = Env::get('WORDPRESS_DB_PASSWORD'); + $installationDb = new MysqlDatabase($dbName, $dbUser, $dbPassword, $dbHost, 'wp_'); + // Copy WooCommerce from the main installation to a temporary directory. + $tmpDir = sys_get_temp_dir(); + $mainWPInstallationRootDir = Env::get('WORDPRESS_ROOT_DIR'); + if (!FS::recurseCopy( + $mainWPInstallationRootDir . '/wp-content/plugins/woocommerce', + $tmpDir . '/external-woocommerce' + )) { + throw new \RuntimeException('Could not copy plugin woocommerce'); + } + $externalAbsolutePathPluginDir = $tmpDir . '/external-woocommerce'; + $this->assertFileExists($externalAbsolutePathPluginDir . '/woocommerce.php'); + if (!FS::recurseCopy( + codecept_data_dir('plugins/some-external-plugin'), + $projectDir . '/vendor/acme/some-external-plugin' + )) { + throw new \RuntimeException('Could not copy plugin some-external-plugin'); + } + + $hash = md5(microtime()); + $externalExplodingPlugin = sys_get_temp_dir() . '/' . $hash . '/exploding-plugin'; + if (!(mkdir($externalExplodingPlugin, 0777, true) && is_dir($externalExplodingPlugin))) { + throw new \RuntimeException('Could not create exploding plugin directory'); + } + if (!copy(codecept_data_dir('plugins/exploding-plugin/main.php'), $externalExplodingPlugin . '/main.php')) { + throw new \RuntimeException('Could not copy exploding plugin file'); + } + $testPluginFileContents = <<< PHP +config = [ + 'wpRootFolder' => $wpRootDir, + 'dbUrl' => $installationDb->getDbUrl(), + 'tablePrefix' => 'test_', + 'plugins' => [ + 'test.php', // From the WordPress installation plugins directory. + $externalAbsolutePathPluginDir . '/woocommerce.php', // Absolute path. + 'vendor/acme/some-external-plugin/some-plugin.php', // Relative path to the project root folder. + 'my-plugin.php' // Relative path to the project root folder, development plugin file. + ], + 'silentlyActivatePlugins' => [ + $externalExplodingPlugin . '/main.php' // Absolute path. + ] + ]; + + $wpLoader = $this->module(); + $projectDirname = basename($projectDir); + + $this->assertInIsolation( + static function () use ($wpLoader, $projectDir) { + chdir($projectDir); + $projectDirname = basename($projectDir); + + $wpLoader->_initialize(); + + Assert::assertEquals([ + 'test.php', + 'external-woocommerce/woocommerce.php', + 'some-external-plugin/some-plugin.php', + "$projectDirname/my-plugin.php", + 'exploding-plugin/main.php' + ], get_option('active_plugins')); + + // Test plugin from the WordPress installation plugins directory. + Assert::assertEquals('1', get_option('test_plugin_activated')); + Assert::assertTrue(function_exists('test_plugin_main')); + + // WooCommerce from the absolute path. + Assert::assertTrue(function_exists('wc_get_product')); + Assert::assertTrue(class_exists('WC_Product')); + $product = new \WC_Product(); + $product->set_name('Test Product'); + $product->set_price(10); + $product->set_status('publish'); + $product->save(); + Assert::assertInstanceOf(\WC_Product::class, $product); + Assert::assertInstanceOf(\WC_Product::class, wc_get_product($product->get_id())); + + // Some external plugin from the relative path. + Assert::assertTrue(function_exists('some_plugin_main')); + Assert::assertEquals('1', get_option('some_plugin_activated')); + + // My plugin from the relative path. + Assert::assertTrue(function_exists('my_plugin_main')); + Assert::assertEquals('1', get_option('my_plugin_activated')); + + // Exploding plugin from the absolute path. + Assert::assertTrue(function_exists('exploding_plugin_main')); + Assert::assertEquals('', get_option('exploding_plugin_activated')); + } + ); + } + + /** + * It should allow loading a plugin from an arbitrary path in multisite + * + * @test + */ + public function should_allow_loading_a_plugin_from_an_arbitrary_path_in_multisite(): void + { + $myPluginCode = <<< PHP + $myPluginCode, + + 'var' => [ + 'wordpress' => [] + ], + 'vendor' => [ + 'acme' => [ + + ] + ] + ]); + $wpRootDir = $projectDir . '/var/wordpress'; + $installation = Installation::scaffold($wpRootDir); + $dbName = Random::dbName(); + $dbHost = Env::get('WORDPRESS_DB_HOST'); + $dbUser = Env::get('WORDPRESS_DB_USER'); + $dbPassword = Env::get('WORDPRESS_DB_PASSWORD'); + $installationDb = new MysqlDatabase($dbName, $dbUser, $dbPassword, $dbHost, 'wp_'); + // Copy WooCommerce from the main installation to a temporary directory. + $tmpDir = sys_get_temp_dir(); + $mainWPInstallationRootDir = Env::get('WORDPRESS_ROOT_DIR'); + if (!FS::recurseCopy( + $mainWPInstallationRootDir . '/wp-content/plugins/woocommerce', + $tmpDir . '/external-woocommerce' + )) { + throw new \RuntimeException('Could not copy plugin woocommerce'); + } + $externalAbsolutePathPluginDir = $tmpDir . '/external-woocommerce'; + $this->assertFileExists($externalAbsolutePathPluginDir . '/woocommerce.php'); + if (!FS::recurseCopy( + codecept_data_dir('plugins/some-external-plugin'), + $projectDir . '/vendor/acme/some-external-plugin' + )) { + throw new \RuntimeException('Could not copy plugin some-external-plugin'); + } + + $hash = md5(microtime()); + $externalExplodingPlugin = sys_get_temp_dir() . '/' . $hash . '/exploding-plugin'; + if (!(mkdir($externalExplodingPlugin, 0777, true) && is_dir($externalExplodingPlugin))) { + throw new \RuntimeException('Could not create exploding plugin directory'); + } + if (!copy(codecept_data_dir('plugins/exploding-plugin/main.php'), $externalExplodingPlugin . '/main.php')) { + throw new \RuntimeException('Could not copy exploding plugin file'); + } + $testPluginFileContents = <<< PHP +config = [ + 'multisite' => true, + 'wpRootFolder' => $wpRootDir, + 'dbUrl' => $installationDb->getDbUrl(), + 'tablePrefix' => 'test_', + 'plugins' => [ + 'test.php', // From the WordPress installation plugins directory. + $externalAbsolutePathPluginDir . '/woocommerce.php', // Absolute path. + 'vendor/acme/some-external-plugin/some-plugin.php', // Relative path to the project root folder. + 'my-plugin.php' // Relative path to the project root folder, development plugin file. + ], + 'silentlyActivatePlugins' => [ + $externalExplodingPlugin . '/main.php' // Absolute path. + ] + ]; + + $wpLoader = $this->module(); + + $this->assertInIsolation( + static function () use ($wpLoader, $projectDir) { + chdir($projectDir); + $projectDirname = basename($projectDir); + + $wpLoader->_initialize(); + + Assert::assertEquals([ + 'test.php', + 'external-woocommerce/woocommerce.php', + 'some-external-plugin/some-plugin.php', + "$projectDirname/my-plugin.php", + 'exploding-plugin/main.php' + ], array_keys(get_site_option('active_sitewide_plugins'))); + + // Test plugin from the WordPress installation plugins directory. + Assert::assertEquals('1', get_option('test_plugin_activated')); + Assert::assertTrue(function_exists('test_plugin_main')); + + // WooCommerce from the absolute path. + Assert::assertTrue(function_exists('wc_get_product')); + Assert::assertTrue(class_exists('WC_Product')); + $product = new \WC_Product(); + $product->set_name('Test Product'); + $product->set_price(10); + $product->set_status('publish'); + $product->save(); + Assert::assertInstanceOf(\WC_Product::class, $product); + Assert::assertInstanceOf(\WC_Product::class, wc_get_product($product->get_id())); + + // Some external plugin from the relative path. + Assert::assertTrue(function_exists('some_plugin_main')); + Assert::assertEquals('1', get_option('some_plugin_activated')); + + // My plugin from the relative path. + Assert::assertTrue(function_exists('my_plugin_main')); + Assert::assertEquals('1', get_option('my_plugin_activated')); + + // Exploding plugin from the absolute path. + Assert::assertTrue(function_exists('exploding_plugin_main')); + Assert::assertEquals('', get_option('exploding_plugin_activated')); + } + ); + } +} diff --git a/tests/unit/lucatume/WPBrowser/Module/WPLoaderArbitraryThemeLocationTest.php b/tests/unit/lucatume/WPBrowser/Module/WPLoaderArbitraryThemeLocationTest.php new file mode 100644 index 000000000..bf51da28b --- /dev/null +++ b/tests/unit/lucatume/WPBrowser/Module/WPLoaderArbitraryThemeLocationTest.php @@ -0,0 +1,752 @@ + <<< CSS + /* + Theme Name: My Theme + Author: My Author + Description: My Description + Version: 1.0.0 + Tested up to: 5.9 + Requires PHP: 7.0 + License: GPLv2 or later + License URI: https://www.gnu.org/licenses/gpl-2.0.html + */ + CSS, + 'functions.php' => << '', + 'var' => [ + 'wordpress' => [] + ], + 'vendor' => [ + 'acme' => [ + 'some-theme' => [ + 'style.css' => <<< CSS + /* + Theme Name: Acme Some Theme + Author: My Author + Description: My Description + Version: 1.0.0 + Tested up to: 5.9 + Requires PHP: 7.0 + License: GPLv2 or later + License URI: https://www.gnu.org/licenses/gpl-2.0.html + */ + CSS, + 'functions.php' => <<< PHP + '', + ] + ] + ] + ]); + $wpRootDir = $themeProjectDir . '/var/wordpress'; + Installation::scaffold($wpRootDir, '6.1.1'); + $dbName = Random::dbName(); + $dbHost = Env::get('WORDPRESS_DB_HOST'); + $dbUser = Env::get('WORDPRESS_DB_USER'); + $dbPassword = Env::get('WORDPRESS_DB_PASSWORD'); + $db = new MysqlDatabase($dbName, $dbUser, $dbPassword, $dbHost, 'test_'); + $this->config = [ + 'wpRootFolder' => $wpRootDir, + 'dbUrl' => $db->getDbUrl(), + 'tablePrefix' => 'test_', + 'theme' => $themeProjectDir, + ]; + + $wpLoader = $this->module(); + + $this->assertInIsolation( + static function () use ($wpLoader, $themeProjectDir) { + chdir($themeProjectDir); + $themeName = basename($themeProjectDir); + + $wpLoader->_initialize(); + + Assert::assertEquals($themeName, get_option('stylesheet')); + Assert::assertEquals($themeName, get_option('template')); + Assert::assertTrue(function_exists('my_theme_setup')); + Assert::assertEquals('1', get_option('my_theme_setup_ran')); + global $wp_stylesheet_path, $wp_template_path; + Assert::assertEquals($themeProjectDir, $wp_stylesheet_path); + Assert::assertEquals($themeProjectDir, $wp_template_path); + $theme = wp_get_theme(); + Assert::assertInstanceOf(\WP_Theme::class, $theme); + Assert::assertEquals('My Theme', $theme->get('Name')); + Assert::assertEmpty($theme->errors()); + Assert::assertTrue($theme->exists()); + } + ); + + // Reconfigure the module to use a relative path. + $this->config = [ + 'wpRootFolder' => $wpRootDir, + 'dbUrl' => $db->getDbUrl(), + 'tablePrefix' => 'test_', + 'theme' => '.', + ]; + + $wpLoader = $this->module(); + + $this->assertInIsolation( + static function () use ($wpLoader, $themeProjectDir) { + chdir($themeProjectDir); + $projectDirname = basename($themeProjectDir); + + $wpLoader->_initialize(); + Assert::assertEquals($projectDirname, get_option('stylesheet')); + Assert::assertEquals($projectDirname, get_option('template')); + Assert::assertTrue(function_exists('my_theme_setup')); + Assert::assertEquals('1', get_option('my_theme_setup_ran')); + global $wp_stylesheet_path, $wp_template_path; + Assert::assertEquals($themeProjectDir, $wp_stylesheet_path); + Assert::assertEquals($themeProjectDir, $wp_template_path); + $theme = wp_get_theme(); + Assert::assertInstanceOf(\WP_Theme::class, $theme); + Assert::assertEquals('My Theme', $theme->get('Name')); + Assert::assertEmpty($theme->errors()); + Assert::assertTrue($theme->exists()); + } + ); + + // Reconfigure to use a theme from an absolute path. + $this->config = [ + 'wpRootFolder' => $wpRootDir, + 'dbUrl' => $db->getDbUrl(), + 'tablePrefix' => 'test_', + 'theme' => $themeProjectDir . '/vendor/acme/some-theme', + ]; + + $wpLoader = $this->module(); + + $this->assertInIsolation( + static function () use ($wpLoader, $themeProjectDir) { + chdir($themeProjectDir); + + $wpLoader->_initialize(); + + Assert::assertEquals('some-theme', get_option('stylesheet')); + Assert::assertEquals('some-theme', get_option('stylesheet')); + Assert::assertTrue(function_exists('acme_some_theme_setup')); + Assert::assertEquals('1', get_option('acme_some_theme_setup_ran')); + global $wp_stylesheet_path, $wp_template_path; + Assert::assertEquals($themeProjectDir . '/vendor/acme/some-theme', $wp_stylesheet_path); + Assert::assertEquals($themeProjectDir . '/vendor/acme/some-theme', $wp_template_path); + $theme = wp_get_theme(); + Assert::assertInstanceOf(\WP_Theme::class, $theme); + Assert::assertEquals('Acme Some Theme', $theme->get('Name')); + Assert::assertEmpty($theme->errors()); + Assert::assertTrue($theme->exists()); + } + ); + + // Reconfigure to use a theme from a relative path. + $this->config = [ + 'wpRootFolder' => $wpRootDir, + 'dbUrl' => $db->getDbUrl(), + 'tablePrefix' => 'test_', + 'theme' => 'vendor/acme/some-theme', + ]; + + $wpLoader = $this->module(); + + $this->assertInIsolation( + static function () use ($wpLoader, $themeProjectDir) { + chdir($themeProjectDir); + + $wpLoader->_initialize(); + + Assert::assertEquals('some-theme', get_option('stylesheet')); + Assert::assertEquals('some-theme', get_option('stylesheet')); + Assert::assertTrue(function_exists('acme_some_theme_setup')); + Assert::assertEquals('1', get_option('acme_some_theme_setup_ran')); + global $wp_stylesheet_path, $wp_template_path; + Assert::assertEquals($themeProjectDir . '/vendor/acme/some-theme', $wp_stylesheet_path); + Assert::assertEquals($themeProjectDir . '/vendor/acme/some-theme', $wp_template_path); + } + ); + } + + private function module(array $moduleContainerConfig = [], ?array $moduleConfig = null): WPLoader + { + $this->mockModuleContainer = new ModuleContainer(new Di(), $moduleContainerConfig); + return new WPLoader($this->mockModuleContainer, ($moduleConfig ?? $this->config)); + } + + /** + * It should allow loading theme from arbitrary location in multisite + * + * @test + */ + public function should_allow_loading_theme_from_arbitrary_location_in_multisite(): void + { + $themeProjectDir = FS::tmpDir('wploader_', [ + 'style.css' => <<< CSS + /* + Theme Name: My Theme + Author: My Author + Description: My Description + Version: 1.0.0 + Tested up to: 5.9 + Requires PHP: 7.0 + License: GPLv2 or later + License URI: https://www.gnu.org/licenses/gpl-2.0.html + */ + CSS, + 'functions.php' => << '', + 'var' => [ + 'wordpress' => [] + ], + 'vendor' => [ + 'acme' => [ + 'some-theme' => [ + 'style.css' => <<< CSS + /* + Theme Name: Acme Some Theme + Author: My Author + Description: My Description + Version: 1.0.0 + Tested up to: 5.9 + Requires PHP: 7.0 + License: GPLv2 or later + License URI: https://www.gnu.org/licenses/gpl-2.0.html + */ + CSS, + 'functions.php' => <<< PHP + '', + ] + ] + ] + ]); + $wpRootDir = $themeProjectDir . '/var/wordpress'; + Installation::scaffold($wpRootDir, '6.1.1'); + $dbName = Random::dbName(); + $dbHost = Env::get('WORDPRESS_DB_HOST'); + $dbUser = Env::get('WORDPRESS_DB_USER'); + $dbPassword = Env::get('WORDPRESS_DB_PASSWORD'); + $db = new MysqlDatabase($dbName, $dbUser, $dbPassword, $dbHost, 'test_'); + $this->config = [ + 'multisite' => true, + 'wpRootFolder' => $wpRootDir, + 'dbUrl' => $db->getDbUrl(), + 'tablePrefix' => 'test_', + 'theme' => $themeProjectDir, + ]; + + $wpLoader = $this->module(); + + $this->assertInIsolation( + static function () use ($wpLoader, $themeProjectDir) { + chdir($themeProjectDir); + $themeName = basename($themeProjectDir); + + $wpLoader->_initialize(); + + Assert::assertEquals($themeName, get_option('stylesheet')); + Assert::assertEquals($themeName, get_option('template')); + Assert::assertTrue(function_exists('my_theme_setup')); + Assert::assertEquals('1', get_option('my_theme_setup_ran')); + global $wp_stylesheet_path, $wp_template_path; + Assert::assertEquals($themeProjectDir, $wp_stylesheet_path); + Assert::assertEquals($themeProjectDir, $wp_template_path); + $theme = wp_get_theme(); + Assert::assertInstanceOf(\WP_Theme::class, $theme); + Assert::assertEquals('My Theme', $theme->get('Name')); + Assert::assertEmpty($theme->errors()); + Assert::assertTrue($theme->exists()); + } + ); + + // Reconfigure the module to use a relative path. + $this->config = [ + 'multisite' => true, + 'wpRootFolder' => $wpRootDir, + 'dbUrl' => $db->getDbUrl(), + 'tablePrefix' => 'test_', + 'theme' => '.', + ]; + + $wpLoader = $this->module(); + + $this->assertInIsolation( + static function () use ($wpLoader, $themeProjectDir) { + chdir($themeProjectDir); + $projectDirname = basename($themeProjectDir); + + $wpLoader->_initialize(); + Assert::assertEquals($projectDirname, get_option('stylesheet')); + Assert::assertEquals($projectDirname, get_option('template')); + Assert::assertTrue(function_exists('my_theme_setup')); + Assert::assertEquals('1', get_option('my_theme_setup_ran')); + global $wp_stylesheet_path, $wp_template_path; + Assert::assertEquals($themeProjectDir, $wp_stylesheet_path); + Assert::assertEquals($themeProjectDir, $wp_template_path); + $theme = wp_get_theme(); + Assert::assertInstanceOf(\WP_Theme::class, $theme); + Assert::assertEquals('My Theme', $theme->get('Name')); + Assert::assertEmpty($theme->errors()); + Assert::assertTrue($theme->exists()); + } + ); + + // Reconfigure to use a theme from an absolute path. + $this->config = [ + 'multisite' => true, + 'wpRootFolder' => $wpRootDir, + 'dbUrl' => $db->getDbUrl(), + 'tablePrefix' => 'test_', + 'theme' => $themeProjectDir . '/vendor/acme/some-theme', + ]; + + $wpLoader = $this->module(); + + $this->assertInIsolation( + static function () use ($wpLoader, $themeProjectDir) { + chdir($themeProjectDir); + + $wpLoader->_initialize(); + + Assert::assertEquals('some-theme', get_option('stylesheet')); + Assert::assertEquals('some-theme', get_option('stylesheet')); + Assert::assertTrue(function_exists('acme_some_theme_setup')); + Assert::assertEquals('1', get_option('acme_some_theme_setup_ran')); + global $wp_stylesheet_path, $wp_template_path; + Assert::assertEquals($themeProjectDir . '/vendor/acme/some-theme', $wp_stylesheet_path); + Assert::assertEquals($themeProjectDir . '/vendor/acme/some-theme', $wp_template_path); + $theme = wp_get_theme(); + Assert::assertInstanceOf(\WP_Theme::class, $theme); + Assert::assertEquals('Acme Some Theme', $theme->get('Name')); + Assert::assertEmpty($theme->errors()); + Assert::assertTrue($theme->exists()); + } + ); + + // Reconfigure to use a theme from a relative path. + $this->config = [ + 'multisite' => true, + 'wpRootFolder' => $wpRootDir, + 'dbUrl' => $db->getDbUrl(), + 'tablePrefix' => 'test_', + 'theme' => 'vendor/acme/some-theme', + ]; + + $wpLoader = $this->module(); + + $this->assertInIsolation( + static function () use ($wpLoader, $themeProjectDir) { + chdir($themeProjectDir); + + $wpLoader->_initialize(); + + Assert::assertEquals('some-theme', get_option('stylesheet')); + Assert::assertEquals('some-theme', get_option('stylesheet')); + Assert::assertTrue(function_exists('acme_some_theme_setup')); + Assert::assertEquals('1', get_option('acme_some_theme_setup_ran')); + global $wp_stylesheet_path, $wp_template_path; + Assert::assertEquals($themeProjectDir . '/vendor/acme/some-theme', $wp_stylesheet_path); + Assert::assertEquals($themeProjectDir . '/vendor/acme/some-theme', $wp_template_path); + } + ); + } + +public function invalidThemeConfigurationDataProvider(): array + { + return [ + 'int' => [1], + 'float' => [1.1], + 'bool' => [true], + 'object' => [new \stdClass()], + 'array' => [[]], + 'associative array' => [['foo' => 'bar']], + ]; + } + + /** + * It should throw if theme parameter configured with not an array of two strings + * + * @test + * @dataProvider invalidThemeConfigurationDataProvider + */ + public function should_throw_if_theme_parameter_configured_with_not_an_array_of_two_strings($theme): void + { + $this->expectException(ModuleConfigException::class); + + $this->config = [ + 'multisite' => true, + 'wpRootFolder' => __DIR__, + 'dbUrl' => 'mysql://root:root@mysql:3306/wordpress', + 'tablePrefix' => 'test_', + 'theme' => $theme + ]; + + $this->module(); + } + + /** + * It should allow loading parent and child theme from arbitrary paths + * + * @test + */ + public function should_allow_loading_parent_and_child_theme_from_arbitrary_paths(): void + { + $childThemeDir = FS::tmpDir('wploader_', [ + 'style.css' => <<< CSS + /* + Theme Name: My Child Theme + Template: parent-theme + Author: My Author + Description: My Description + Version: 1.0.0 + Tested up to: 5.9 + Requires PHP: 7.0 + License: GPLv2 or later + License URI: https://www.gnu.org/licenses/gpl-2.0.html + */ + CSS, + 'functions.php' => << '', + 'var' => [ + 'wordpress' => [] + ], + 'vendor' => [ + 'acme' => [ + 'parent-theme' => [ + 'style.css' => <<< CSS + /* + Theme Name: Acme Parent Theme + Author: My Author + Description: My Description + Version: 1.0.0 + Tested up to: 5.9 + Requires PHP: 7.0 + License: GPLv2 or later + License URI: https://www.gnu.org/licenses/gpl-2.0.html + */ + CSS, + 'functions.php' => <<< PHP + '', + ] + ] + ] + ]); + $wpRootDir = $childThemeDir . '/var/wordpress'; + $parentThemeDir = $childThemeDir . '/vendor/acme/parent-theme'; + Installation::scaffold($wpRootDir, '6.1.1'); + $dbName = Random::dbName(); + $dbHost = Env::get('WORDPRESS_DB_HOST'); + $dbUser = Env::get('WORDPRESS_DB_USER'); + $dbPassword = Env::get('WORDPRESS_DB_PASSWORD'); + $db = new MysqlDatabase($dbName, $dbUser, $dbPassword, $dbHost, 'test_'); + $this->config = [ + 'wpRootFolder' => $wpRootDir, + 'dbUrl' => $db->getDbUrl(), + 'tablePrefix' => 'test_', + 'theme' => ['vendor/acme/parent-theme', $childThemeDir] + ]; + + $wpLoader = $this->module(); + + $this->assertInIsolation( + static function () use ($wpLoader, $parentThemeDir, $childThemeDir) { + chdir($childThemeDir); + $parentThemeName = basename($parentThemeDir); + $childThemeName = basename($childThemeDir); + + $wpLoader->_initialize(); + + Assert::assertEquals($parentThemeName, get_option('template')); + Assert::assertEquals($childThemeName, get_option('stylesheet')); + Assert::assertTrue(function_exists('acme_parent_theme_setup')); + Assert::assertTrue(function_exists('my_child_theme_setup')); + Assert::assertEquals('1', get_option('acme_parent_theme_setup_ran')); + Assert::assertEquals('1', get_option('my_child_theme_setup_ran')); + global $wp_template_path, $wp_stylesheet_path; + Assert::assertEquals($parentThemeDir, $wp_template_path); + Assert::assertEquals($childThemeDir, $wp_stylesheet_path); + $theme = wp_get_theme(); + Assert::assertInstanceOf(\WP_Theme::class, $theme); + Assert::assertEquals('My Child Theme', $theme->get('Name')); + Assert::assertEquals('parent-theme', $theme->get('Template')); + Assert::assertEmpty($theme->errors()); + Assert::assertTrue($theme->exists()); + } + ); + + // Reconfigure and load the child theme from the current directory. + $this->config = [ + 'wpRootFolder' => $wpRootDir, + 'dbUrl' => $db->getDbUrl(), + 'tablePrefix' => 'test_', + 'theme' => ['vendor/acme/parent-theme', '.'] + ]; + + $wpLoader = $this->module(); + + $this->assertInIsolation( + static function () use ($wpLoader, $parentThemeDir, $childThemeDir) { + chdir($childThemeDir); + $parentThemeName = basename($parentThemeDir); + $childThemeName = basename($childThemeDir); + + $wpLoader->_initialize(); + + Assert::assertEquals($parentThemeName, get_option('template')); + Assert::assertEquals($childThemeName, get_option('stylesheet')); + Assert::assertTrue(function_exists('acme_parent_theme_setup')); + Assert::assertTrue(function_exists('my_child_theme_setup')); + Assert::assertEquals('1', get_option('acme_parent_theme_setup_ran')); + Assert::assertEquals('1', get_option('my_child_theme_setup_ran')); + global $wp_template_path, $wp_stylesheet_path; + Assert::assertEquals($parentThemeDir, $wp_template_path); + Assert::assertEquals($childThemeDir, $wp_stylesheet_path); + $theme = wp_get_theme(); + Assert::assertInstanceOf(\WP_Theme::class, $theme); + Assert::assertEquals('My Child Theme', $theme->get('Name')); + Assert::assertEquals('parent-theme', $theme->get('Template')); + Assert::assertEmpty($theme->errors()); + Assert::assertTrue($theme->exists()); + } + ); + } + + /** + * It should allow loading parent and child theme from arbitrary paths in multisite + * + * @test + */ + public function should_allow_loading_parent_and_child_theme_from_arbitrary_paths_in_multisite(): void + { + $childThemeDir = FS::tmpDir('wploader_', [ + 'style.css' => <<< CSS + /* + Theme Name: My Child Theme + Template: parent-theme + Author: My Author + Description: My Description + Version: 1.0.0 + Tested up to: 5.9 + Requires PHP: 7.0 + License: GPLv2 or later + License URI: https://www.gnu.org/licenses/gpl-2.0.html + */ + CSS, + 'functions.php' => << '', + 'var' => [ + 'wordpress' => [] + ], + 'vendor' => [ + 'acme' => [ + 'parent-theme' => [ + 'style.css' => <<< CSS + /* + Theme Name: Acme Parent Theme + Author: My Author + Description: My Description + Version: 1.0.0 + Tested up to: 5.9 + Requires PHP: 7.0 + License: GPLv2 or later + License URI: https://www.gnu.org/licenses/gpl-2.0.html + */ + CSS, + 'functions.php' => <<< PHP + '', + ] + ] + ] + ]); + $wpRootDir = $childThemeDir . '/var/wordpress'; + $parentThemeDir = $childThemeDir . '/vendor/acme/parent-theme'; + Installation::scaffold($wpRootDir, '6.1.1'); + $dbName = Random::dbName(); + $dbHost = Env::get('WORDPRESS_DB_HOST'); + $dbUser = Env::get('WORDPRESS_DB_USER'); + $dbPassword = Env::get('WORDPRESS_DB_PASSWORD'); + $db = new MysqlDatabase($dbName, $dbUser, $dbPassword, $dbHost, 'test_'); + $this->config = [ + 'multisite' => true, + 'wpRootFolder' => $wpRootDir, + 'dbUrl' => $db->getDbUrl(), + 'tablePrefix' => 'test_', + 'theme' => ['vendor/acme/parent-theme', $childThemeDir] + ]; + + $wpLoader = $this->module(); + + $this->assertInIsolation( + static function () use ($wpLoader, $parentThemeDir, $childThemeDir) { + chdir($childThemeDir); + $parentThemeName = basename($parentThemeDir); + $childThemeName = basename($childThemeDir); + + $wpLoader->_initialize(); + + Assert::assertEquals($parentThemeName, get_option('template')); + Assert::assertEquals($childThemeName, get_option('stylesheet')); + Assert::assertTrue(function_exists('acme_parent_theme_setup')); + Assert::assertTrue(function_exists('my_child_theme_setup')); + Assert::assertEquals('1', get_option('acme_parent_theme_setup_ran')); + Assert::assertEquals('1', get_option('my_child_theme_setup_ran')); + global $wp_template_path, $wp_stylesheet_path; + Assert::assertEquals($parentThemeDir, $wp_template_path); + Assert::assertEquals($childThemeDir, $wp_stylesheet_path); + $theme = wp_get_theme(); + Assert::assertInstanceOf(\WP_Theme::class, $theme); + Assert::assertEquals('My Child Theme', $theme->get('Name')); + Assert::assertEquals('parent-theme', $theme->get('Template')); + Assert::assertEmpty($theme->errors()); + Assert::assertTrue($theme->exists()); + } + ); + + // Reconfigure and load the child theme from the current directory. + $this->config = [ + 'multisite' => true, + 'wpRootFolder' => $wpRootDir, + 'dbUrl' => $db->getDbUrl(), + 'tablePrefix' => 'test_', + 'theme' => ['vendor/acme/parent-theme', '.'] + ]; + + $wpLoader = $this->module(); + + $this->assertInIsolation( + static function () use ($wpLoader, $parentThemeDir, $childThemeDir) { + chdir($childThemeDir); + $parentThemeName = basename($parentThemeDir); + $childThemeName = basename($childThemeDir); + + $wpLoader->_initialize(); + + Assert::assertEquals($parentThemeName, get_option('template')); + Assert::assertEquals($childThemeName, get_option('stylesheet')); + Assert::assertTrue(function_exists('acme_parent_theme_setup')); + Assert::assertTrue(function_exists('my_child_theme_setup')); + Assert::assertEquals('1', get_option('acme_parent_theme_setup_ran')); + Assert::assertEquals('1', get_option('my_child_theme_setup_ran')); + global $wp_template_path, $wp_stylesheet_path; + Assert::assertEquals($parentThemeDir, $wp_template_path); + Assert::assertEquals($childThemeDir, $wp_stylesheet_path); + $theme = wp_get_theme(); + Assert::assertInstanceOf(\WP_Theme::class, $theme); + Assert::assertEquals('My Child Theme', $theme->get('Name')); + Assert::assertEquals('parent-theme', $theme->get('Template')); + Assert::assertEmpty($theme->errors()); + Assert::assertTrue($theme->exists()); + } + ); + } +} diff --git a/tests/unit/lucatume/WPBrowser/Module/WPLoaderTest.php b/tests/unit/lucatume/WPBrowser/Module/WPLoaderTest.php index cb5e9f4c0..88bdb64f0 100644 --- a/tests/unit/lucatume/WPBrowser/Module/WPLoaderTest.php +++ b/tests/unit/lucatume/WPBrowser/Module/WPLoaderTest.php @@ -1281,7 +1281,7 @@ public function should_throw_if_there_is_an_error_while_switching_theme(): void Installation::scaffold($wpRootDir, 'latest'); $this->expectException(ModuleException::class); - $this->expectExceptionMessage('Theme some-theme does not exist.'); + $this->expectExceptionMessage('The theme directory "some-theme" does not exist'); $wpLoader = $this->module(); $this->assertInIsolation(static function () use ($wpLoader) { @@ -1317,41 +1317,7 @@ public function should_throw_if_there_is_an_error_while_switching_theme_in_multi Installation::scaffold($wpRootDir, 'latest'); $this->expectException(ModuleException::class); - $this->expectExceptionMessage('Theme some-theme does not exist.'); - - $wpLoader = $this->module(); - $this->assertInIsolation(static function () use ($wpLoader) { - $wpLoader->_initialize(); - }); - } - - /** - * It should throw if theme is an array - * - * @test - */ - public function should_throw_if_theme_is_an_array(): void - { - $wpRootDir = FS::tmpDir('wploader_'); - $dbName = Random::dbName(); - $dbHost = Env::get('WORDPRESS_DB_HOST'); - $dbUser = Env::get('WORDPRESS_DB_USER'); - $dbPassword = Env::get('WORDPRESS_DB_PASSWORD'); - $this->config = [ - 'wpRootFolder' => $wpRootDir, - 'dbName' => $dbName, - 'dbHost' => $dbHost, - 'dbUser' => $dbUser, - 'dbPassword' => $dbPassword, - 'configFile' => [ - codecept_data_dir('files/test_file_001.php'), - codecept_data_dir('files/test_file_002.php'), - ], - 'theme' => ['some-template', 'some-stylesheet'], - ]; - Installation::scaffold($wpRootDir, 'latest'); - - $this->expectException(ModuleConfigException::class); + $this->expectExceptionMessage('The theme directory "some-theme" does not exist'); $wpLoader = $this->module(); $this->assertInIsolation(static function () use ($wpLoader) { @@ -1566,16 +1532,17 @@ public function should_rethrow_on_failure_to_load_a_dump_file(): void $dbHost = Env::get('WORDPRESS_DB_HOST'); $dbUser = Env::get('WORDPRESS_DB_USER'); $dbPassword = Env::get('WORDPRESS_DB_PASSWORD'); + $dumpFiles = [ + codecept_data_dir('files/test-dump-001.sql'), + codecept_data_dir('files/test-dump-002.sql'), + ]; $this->config = [ 'wpRootFolder' => $wpRootDir, 'dbName' => $dbName, 'dbHost' => $dbHost, 'dbUser' => $dbUser, 'dbPassword' => $dbPassword, - 'dump' => [ - codecept_data_dir('files/test-dump-001.sql'), - codecept_data_dir('files/test-dump-002.sql'), - ] + 'dump' => $dumpFiles ]; Installation::scaffold($wpRootDir); @@ -1583,8 +1550,10 @@ public function should_rethrow_on_failure_to_load_a_dump_file(): void $this->expectException(ModuleException::class); - $this->assertInIsolation(static function () use ($wpLoader) { - uopz_set_return('fopen', false); + $this->assertInIsolation(static function () use ($wpLoader, $dumpFiles) { + uopz_set_return('fopen', function (string $file, ...$args)use($dumpFiles) { + return in_array($file, $dumpFiles, true) ? false : fopen($file, ...$args); + }, true); $wpLoader->_initialize(); }); } @@ -2739,287 +2708,4 @@ static function () use ($wpLoader) { } ); } - - /** - * It should allow loading a plugin from an arbitrary path - * - * @test - */ - public function should_allow_loading_a_plugin_from_an_arbitrary_path(): void - { - $myPluginCode = <<< PHP - $myPluginCode, - - 'var' => [ - 'wordpress' => [] - ], - 'vendor' => [ - 'acme' => [ - - ] - ] - ]); - $wpRootDir = $projectDir. '/var/wordpress'; - $installation = Installation::scaffold($wpRootDir); - $dbName = Random::dbName(); - $dbHost = Env::get('WORDPRESS_DB_HOST'); - $dbUser = Env::get('WORDPRESS_DB_USER'); - $dbPassword = Env::get('WORDPRESS_DB_PASSWORD'); - $installationDb = new MysqlDatabase($dbName, $dbUser, $dbPassword, $dbHost, 'wp_'); - // Copy WooCommerce from the main installation to a temporary directory. - $tmpDir = sys_get_temp_dir(); - $mainWPInstallationRootDir = Env::get('WORDPRESS_ROOT_DIR'); - if (!FS::recurseCopy( - $mainWPInstallationRootDir . '/wp-content/plugins/woocommerce', - $tmpDir . '/external-woocommerce' - )) { - throw new \RuntimeException('Could not copy plugin woocommerce'); - } - $externalAbsolutePathPluginDir = $tmpDir . '/external-woocommerce'; - $this->assertFileExists($externalAbsolutePathPluginDir . '/woocommerce.php'); - if(!FS::recurseCopy( - codecept_data_dir('plugins/some-external-plugin'), - $projectDir . '/vendor/acme/some-external-plugin' - )){ - throw new \RuntimeException('Could not copy plugin some-external-plugin'); - } - - $hash = md5(microtime()); - $externalExplodingPlugin = sys_get_temp_dir() . '/' . $hash . '/exploding-plugin'; - if(!(mkdir($externalExplodingPlugin, 0777, true) && is_dir($externalExplodingPlugin))){ - throw new \RuntimeException('Could not create exploding plugin directory'); - } - if(!copy(codecept_data_dir('plugins/exploding-plugin/main.php'),$externalExplodingPlugin . '/main.php' )){ - throw new \RuntimeException('Could not copy exploding plugin file'); - } - $testPluginFileContents = <<< PHP -config = [ - 'wpRootFolder' => $wpRootDir, - 'dbUrl' => $installationDb->getDbUrl(), - 'tablePrefix' => 'test_', - 'plugins' => [ - 'test.php', // From the WordPress installation plugins directory. - $externalAbsolutePathPluginDir . '/woocommerce.php', // Absolute path. - 'vendor/acme/some-external-plugin/some-plugin.php', // Relative path to the project root folder. - 'my-plugin.php' // Relative path to the project root folder, development plugin file. - ], - 'silentlyActivatePlugins' => [ - $externalExplodingPlugin . '/main.php' // Absolute path. - ] - ]; - - $wpLoader = $this->module(); - $projectDirname = basename($projectDir); - - $this->assertInIsolation( - static function () use ($wpLoader, $projectDir) { - chdir($projectDir); - $projectDirname = basename($projectDir); - - $wpLoader->_initialize(); - - Assert::assertEquals([ - 'test.php', - 'external-woocommerce/woocommerce.php', - 'some-external-plugin/some-plugin.php', - "$projectDirname/my-plugin.php", - 'exploding-plugin/main.php' - ], get_option('active_plugins')); - - // Test plugin from the WordPress installation plugins directory. - Assert::assertEquals('1', get_option('test_plugin_activated')); - Assert::assertTrue(function_exists('test_plugin_main')); - - // WooCommerce from the absolute path. - Assert::assertTrue(function_exists('wc_get_product')); - Assert::assertTrue(class_exists('WC_Product')); - $product = new \WC_Product(); - $product->set_name('Test Product'); - $product->set_price(10); - $product->set_status('publish'); - $product->save(); - Assert::assertInstanceOf(\WC_Product::class, $product); - Assert::assertInstanceOf(\WC_Product::class, wc_get_product($product->get_id())); - - // Some external plugin from the relative path. - Assert::assertTrue(function_exists('some_plugin_main')); - Assert::assertEquals('1', get_option('some_plugin_activated')); - - // My plugin from the relative path. - Assert::assertTrue(function_exists('my_plugin_main')); - Assert::assertEquals('1', get_option('my_plugin_activated')); - - // Exploding plugin from the absolute path. - Assert::assertTrue(function_exists('exploding_plugin_main')); - Assert::assertEquals('', get_option('exploding_plugin_activated')); - } - ); - } - - /** - * It should allow loading a plugin from an arbitrary path in multisite - * - * @test - */ - public function should_allow_loading_a_plugin_from_an_arbitrary_path_in_multisite(): void - { - $myPluginCode = <<< PHP - $myPluginCode, - - 'var' => [ - 'wordpress' => [] - ], - 'vendor' => [ - 'acme' => [ - - ] - ] - ]); - $wpRootDir = $projectDir. '/var/wordpress'; - $installation = Installation::scaffold($wpRootDir); - $dbName = Random::dbName(); - $dbHost = Env::get('WORDPRESS_DB_HOST'); - $dbUser = Env::get('WORDPRESS_DB_USER'); - $dbPassword = Env::get('WORDPRESS_DB_PASSWORD'); - $installationDb = new MysqlDatabase($dbName, $dbUser, $dbPassword, $dbHost, 'wp_'); - // Copy WooCommerce from the main installation to a temporary directory. - $tmpDir = sys_get_temp_dir(); - $mainWPInstallationRootDir = Env::get('WORDPRESS_ROOT_DIR'); - if (!FS::recurseCopy( - $mainWPInstallationRootDir . '/wp-content/plugins/woocommerce', - $tmpDir . '/external-woocommerce' - )) { - throw new \RuntimeException('Could not copy plugin woocommerce'); - } - $externalAbsolutePathPluginDir = $tmpDir . '/external-woocommerce'; - $this->assertFileExists($externalAbsolutePathPluginDir . '/woocommerce.php'); - if(!FS::recurseCopy( - codecept_data_dir('plugins/some-external-plugin'), - $projectDir . '/vendor/acme/some-external-plugin' - )){ - throw new \RuntimeException('Could not copy plugin some-external-plugin'); - } - - $hash = md5(microtime()); - $externalExplodingPlugin = sys_get_temp_dir() . '/' . $hash . '/exploding-plugin'; - if(!(mkdir($externalExplodingPlugin, 0777, true) && is_dir($externalExplodingPlugin))){ - throw new \RuntimeException('Could not create exploding plugin directory'); - } - if(!copy(codecept_data_dir('plugins/exploding-plugin/main.php'),$externalExplodingPlugin . '/main.php' )){ - throw new \RuntimeException('Could not copy exploding plugin file'); - } - $testPluginFileContents = <<< PHP -config = [ - 'multisite' => true, - 'wpRootFolder' => $wpRootDir, - 'dbUrl' => $installationDb->getDbUrl(), - 'tablePrefix' => 'test_', - 'plugins' => [ - 'test.php', // From the WordPress installation plugins directory. - $externalAbsolutePathPluginDir . '/woocommerce.php', // Absolute path. - 'vendor/acme/some-external-plugin/some-plugin.php', // Relative path to the project root folder. - 'my-plugin.php' // Relative path to the project root folder, development plugin file. - ], - 'silentlyActivatePlugins' => [ - $externalExplodingPlugin . '/main.php' // Absolute path. - ] - ]; - - $wpLoader = $this->module(); - $projectDirname = basename($projectDir); - - $this->assertInIsolation( - static function () use ($wpLoader, $projectDir) { - chdir($projectDir); - $projectDirname = basename($projectDir); - - $wpLoader->_initialize(); - - Assert::assertEquals([ - 'test.php', - 'external-woocommerce/woocommerce.php', - 'some-external-plugin/some-plugin.php', - "$projectDirname/my-plugin.php", - 'exploding-plugin/main.php' - ], array_keys(get_site_option('active_sitewide_plugins'))); - - // Test plugin from the WordPress installation plugins directory. - Assert::assertEquals('1', get_option('test_plugin_activated')); - Assert::assertTrue(function_exists('test_plugin_main')); - - // WooCommerce from the absolute path. - Assert::assertTrue(function_exists('wc_get_product')); - Assert::assertTrue(class_exists('WC_Product')); - $product = new \WC_Product(); - $product->set_name('Test Product'); - $product->set_price(10); - $product->set_status('publish'); - $product->save(); - Assert::assertInstanceOf(\WC_Product::class, $product); - Assert::assertInstanceOf(\WC_Product::class, wc_get_product($product->get_id())); - - // Some external plugin from the relative path. - Assert::assertTrue(function_exists('some_plugin_main')); - Assert::assertEquals('1', get_option('some_plugin_activated')); - - // My plugin from the relative path. - Assert::assertTrue(function_exists('my_plugin_main')); - Assert::assertEquals('1', get_option('my_plugin_activated')); - - // Exploding plugin from the absolute path. - Assert::assertTrue(function_exists('exploding_plugin_main')); - Assert::assertEquals('', get_option('exploding_plugin_activated')); - } - ); - } }