diff --git a/.github/actions/codeception/action.yml b/.github/actions/codeception/action.yml index ff7ecc8d..f509b670 100644 --- a/.github/actions/codeception/action.yml +++ b/.github/actions/codeception/action.yml @@ -67,36 +67,6 @@ runs: WP_VERSION: ${{ inputs.wordpress }} PHP_VERSION: ${{ inputs.php }} - - name: Run Acceptance Tests w/ Docker - working-directory: ${{ inputs.working-directory }} - shell: bash - run: | - docker exec \ - --env DEBUG=${{ env.DEBUG }} \ - --env SKIP_TESTS_CLEANUP=${{ env.SKIP_TESTS_CLEANUP }} \ - --env SUITES=acceptance \ - $(docker compose ps -q wordpress) \ - bash -c "cd wp-content/plugins/$(basename $(echo ${{ inputs.working-directory }} | sed 's:/*$::')) && bin/run-codeception.sh" - env: - DEBUG: ${{ env.ACTIONS_STEP_DEBUG }} - SKIP_TESTS_CLEANUP: "true" - continue-on-error: true - - - name: Run Functional Tests w/ Docker - working-directory: ${{ inputs.working-directory }} - shell: bash - run: | - docker exec \ - --env DEBUG=${{ env.DEBUG }} \ - --env SKIP_TESTS_CLEANUP=${{ env.SKIP_TESTS_CLEANUP }} \ - --env SUITES=functional \ - $(docker compose ps -q wordpress) \ - bash -c "cd wp-content/plugins/$(basename ${{ inputs.working-directory }}) && bin/run-codeception.sh" - env: - DEBUG: ${{ env.ACTIONS_STEP_DEBUG }} - SKIP_TESTS_CLEANUP: "true" - continue-on-error: true - - name: Run WPUnit Tests w/ Docker working-directory: ${{ inputs.working-directory }} shell: bash diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 45afe165..02e93cea 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -26,6 +26,14 @@ jobs: plugin=$(git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.sha }} | grep '^plugins/' | head -1 | cut -d/ -f2) echo "slug=$plugin" >> $GITHUB_OUTPUT + # We should at least have a phpcs.xml file to run code quality checks + - name: Validate phpcs.xml + run: | + if [ ! -f "plugins/${{ steps.plugin.outputs.slug }}/phpcs.xml" ]; then + echo "Exiting as no phpcs.xml file found for /${{ steps.plugin.outputs.slug }}" + exit 1 + fi + - name: PHP Code Quality uses: ./.github/actions/code-quality with: diff --git a/.github/workflows/codeception.yml b/.github/workflows/codeception.yml index 577f8ec2..d296b04b 100644 --- a/.github/workflows/codeception.yml +++ b/.github/workflows/codeception.yml @@ -1,4 +1,4 @@ -name: Codeception +name: Testing Integration on: push: @@ -43,10 +43,18 @@ jobs: plugin=$(git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.sha }} | grep '^plugins/' | head -1 | cut -d/ -f2) echo "slug=$plugin" >> $GITHUB_OUTPUT + - name: Validate codeception.dist.yml + run: | + if [ ! -f "plugins/${{ steps.plugin.outputs.slug }}/codeception.dist.yml" ]; then + echo "Exiting as no codeception file found for this plugin - /${{ steps.plugin.outputs.slug }}" + exit 1 + fi + - name: Validate composer.json run: | if [ ! -f "plugins/${{ steps.plugin.outputs.slug }}/composer.json" ]; then - echo "Warning: composer.json missing in plugins/${{ steps.plugin.outputs.slug }}" + echo "Exiting as no composer file found for this plugin - ${{ steps.plugin.outputs.slug }}" + exit 1 fi - name: Run Codeception Tests diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index ca6e29b9..77fddb49 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -1,4 +1,4 @@ -name: Playwright End-to-End Tests +name: End-to-End Tests on: push: diff --git a/.github/workflows/plugin-artifact-for-pr.yml b/.github/workflows/plugin-artifact-for-pr.yml index 0ed36382..a9305b01 100644 --- a/.github/workflows/plugin-artifact-for-pr.yml +++ b/.github/workflows/plugin-artifact-for-pr.yml @@ -30,6 +30,7 @@ jobs: PLUGIN_SLUG: ${{ steps.plugin.outputs.slug }} with: slug: ${{ env.PLUGIN_SLUG }} + composer-options: '--no-progress --optimize-autoloader --no-dev' - name: Comment with artifact link uses: actions/github-script@v7 diff --git a/plugins/hwp-previews/ACTIONS_AND_FILTERS.md b/plugins/hwp-previews/ACTIONS_AND_FILTERS.md index ac6f3b3b..33642bcb 100644 --- a/plugins/hwp-previews/ACTIONS_AND_FILTERS.md +++ b/plugins/hwp-previews/ACTIONS_AND_FILTERS.md @@ -8,23 +8,33 @@ ## PHP Filters +## Admin + +- `hwp_previews_settings_init` - Allows a user to modify the `Settings` instance +- `hwp_previews_settings_form_manager_init` - Allows a user to modify the `Settings_Form_Manager` instance and update fields and post types +- `hwp_previews_settings_fields` - Allows a user to register, modify, or remove settings fields for the settings page +- `hwp_previews_settings_group_option_key` - Filter to modify the settings group option key. Default is HWP_PREVIEWS_SETTINGS_KEY +- `hwp_previews_settings_group_settings_group` - Filter to modify the settings group name. Default is HWP_PREVIEWS_SETTINGS_GROUP + + + - `hwp_previews_register_parameters` - Allows modification of the URL parameters used for previews for the class `Preview_Parameter_Registry` - `hwp_previews_template_path` - To use our own template for iframe previews - `hwp_previews_core` - Register or unregister URL parameters, and adjust types/statuses - `hwp_previews_filter_available_post_types` - Filter to modify the available post types for Previews. -- `hwp_previews_settings_group_option_key` - Filter to modify the settings group option key. Default is HWP_PREVIEWS_SETTINGS_KEY -- `hwp_previews_settings_group_settings_group` - Filter to modify the settings group name. Default is HWP_PREVIEWS_SETTINGS_GROUP +- `hwp_previews_filter_available_post_statuses` - Filter for post statuses for previews for Previews +- `hwp_previews_filter_available_parent_post_statuses` - Filter for parent post statuses for Previews - `hwp_previews_settings_group_settings_config` - Filter to modify the settings array. See `Settings_Group` - `hwp_previews_settings_group_cache_groups` - Filter to modify cache groups for `Settings_Group` - `hwp_previews_get_post_types_config` - Filter for generating the instance of `Post_Types_Config_Interface` -- `hwp_previews_hooks_post_type_config` - Filter for post type config service for the Hook class - `hwp_previews_hooks_post_status_config` - Filter for post status config service for the Hook class - `hwp_previews_hooks_preview_link_service` - Filter for preview link service for the Hook class -- `hwp_previews_hooks_post_statuses` - Filter for post statuses for previews for the Hook Class -- `hwp_previews_settings_fields` - Allows a user to register, modify, or remove settings fields for the settings page ## Usage Examples +@TODO - Redo + + ### Filter: Post Types List Modify which post types appear in the settings UI: diff --git a/plugins/hwp-previews/CHANGELOG.md b/plugins/hwp-previews/CHANGELOG.md index d4945185..ffc75348 100644 --- a/plugins/hwp-previews/CHANGELOG.md +++ b/plugins/hwp-previews/CHANGELOG.md @@ -1,5 +1,5 @@ # HWP Previews -## 0.0.1 +## 0.0.1-beta - Proof of concept. diff --git a/plugins/hwp-previews/README.md b/plugins/hwp-previews/README.md index ff727373..aa05c0a3 100644 --- a/plugins/hwp-previews/README.md +++ b/plugins/hwp-previews/README.md @@ -1,11 +1,26 @@ # HWP Previews -**Headless Previews** solution for WordPress: fully configurable preview URLs via the settings page. +**Headless Previews** solution for WordPress: fully configurable preview URLs via the settings page which is framework agnostic. + +* [Join the Headless WordPress community on Discord.](https://discord.gg/headless-wordpress-836253505944813629) +* [Documentation](#getting-started) + + +----- + +[![Version](https://img.shields.io/badge/version-0.0.1-beta)]() +[![License](https://img.shields.io/badge/license-GPLv2%2B-green)]() +![GitHub forks](https://img.shields.io/github/forks/wpengine/hwptoolkit?style=social) +![GitHub stars](https://img.shields.io/github/stars/wpengine/hwptoolkit?style=social) +[![Testing Integration](https://github.com/wpengine/hwptoolkit/workflows/Testing%20Integration/badge.svg)](https://github.com/wpengine/hwptoolkit/actions?query=workflow%3A%22Testing+Integration%22) +[![Code Quality](https://github.com/wpengine/hwptoolkit/workflows/Code%20Quality/badge.svg)](https://github.com/wpengine/hwptoolkit/actions?query=workflow%3A%22Code+Quality%22) +[![End-to-End Tests](https://github.com/wpengine/hwptoolkit/workflows/End-to-End%20Tests/badge.svg)](https://github.com/wpengine/hwptoolkit/actions?query=workflow%3A%22End-to-End+Tests%22) +----- + -[![Version](https://img.shields.io/badge/version-0.0.1-blue)]() [![License](https://img.shields.io/badge/license-GPLv2%2B-lightgrey)]() > [!CAUTION] -> This plugin is currently in an alpha state. It's still under active development, so you may encounter bugs or incomplete features. Updates will be rolled out regularly. Use with caution and provide feedback if possible. +> This plugin is currently in an beta state. It's still under active development, so you may encounter bugs or incomplete features. Updates will be rolled out regularly. Use with caution and provide feedback if possible. You can create an issue at [https://github.com/wpengine/hwptoolkit/issues](https://github.com/wpengine/hwptoolkit/issues) --- @@ -26,6 +41,11 @@ HWP Previews is a robust and extensible WordPress plugin that centralizes all pr It empowers site administrators and developers to tailor preview behaviors for each public post type independently, facilitating seamless headless or decoupled workflows. With HWP Previews, you can define dynamic URL templates, enforce unique slugs for drafts, allow all post statuses be used as parent and extend functionality through flexible hooks and filters, ensuring a consistent and conflict-free preview experience across diverse environments. + + +>[!IMPORTANT] +> For Faust users, HWP Previews integrates seamlessly, automatically configuring settings to match Faust's preview system. This allows you to maintain your existing preview workflow without additional setup. + --- ## Features @@ -34,8 +54,8 @@ With HWP Previews, you can define dynamic URL templates, enforce unique slugs fo - **Custom URL Templates**: Define preview URLs using placeholder tokens for dynamic content. - **Parent Status**: Allow posts of **all** statuses to be used as parent within hierarchical post types. - **Highly Customizable**: Extend core behavior with a comprehensive set of actions and filters. +- **Faust Compatibility**: The plugin is compatible with [Faust.js](https://faustjs.org/) and the [FaustWP plugin](https://github.com/wpengine/faustjs/tree/canary/plugins/faustwp). ---- ## Getting Started @@ -49,6 +69,28 @@ This guide will help you set up your first headless preview link for the "Posts" --- +## Project Structure + +```text +hwp-previews/ +├── src/ # Main plugin source code +│ ├── Admin/ # Admin settings, menu, and settings page logic +│ ├── Hooks/ # WordPress hooks and filters +│ ├── Integration/ # Integrations (e.g. Faust) +│ ├── Preview/ # Preview URL logic, template resolver, helpers +│ ├── Plugin.php # Main plugin class (entry point) +│ └── Autoload.php # PSR-4 autoloader +├── tests/ # All test suites +│ ├── wpunit/ # WPBrowser/Codeception unit +├── [hwp-previews.php] +├── [activation.php] +├── [composer.json] +├── [deactivation.php] +├── [ACTIONS_AND_FILTERS.md] +├── [TESTING.md] +├── [README.md] +``` + ## Configuration HWP Previews configuration located at **Settings > HWP Previews** page in your WP Admin. The settings are organized by post type. @@ -114,11 +156,7 @@ This out-of-the-box configuration allows your existing preview workflow to conti --- -## Extending the Functionality - -The plugin's behavior can be extended using its PHP hooks. Developers can control which post types are configurable in the settings via the `hwp_previews_filter_available_post_types` filter. The `hwp_previews_core` action allows for registering new URL parameters or unregistering default ones. Additionally, the `hwp_previews_template_path` filter can be used to replace the default preview iframe with a custom PHP template. - -### Actions & Filters +## Actions & Filters See the [Actions & Filters documentation](ACTIONS_AND_FILTERS.md) for a comprehensive list of available hooks and how to use them. @@ -127,3 +165,28 @@ See the [Actions & Filters documentation](ACTIONS_AND_FILTERS.md) for a comprehe ## Testing See [Testing.md](TESTING.md) for details on how to test the plugin. + + +## Screenshots + +
+Click to expand screenshots + +![Custom Post Type Preview](./screenshots/settings_page.png) +*Preview settings page.* + +![Custom Post Type Preview](./screenshots/cpt_preview.png) +*Preview settings for a custom post type.* + +![Post Preview](./screenshots/post_preview.png) +*Preview button in the WordPress editor.* + +![Post Preview in Iframe](./screenshots/post_preview_iframe.png) +*Preview loaded inside the WordPress editor using an iframe.* + +![Preview Token](./screenshots/preview_token.png) +*Preview token parameter for secure preview URLs.* + +![App Password](./screenshots/app_password.png) +*App password setup for authentication.* +
diff --git a/plugins/hwp-previews/TESTING.md b/plugins/hwp-previews/TESTING.md index c855772e..3ac32f38 100644 --- a/plugins/hwp-previews/TESTING.md +++ b/plugins/hwp-previews/TESTING.md @@ -33,6 +33,7 @@ The plugin provides scripts to set up a local WordPress environment for testing, 1. **Copy and configure environment variables:** ```bash + @TODO cp .env.dist .env # Edit .env as needed for your local setup ``` @@ -54,44 +55,29 @@ The plugin provides scripts to set up a local WordPress environment for testing, ## Running Tests -### Unit Tests +Currently the plugin has the following suite of tests -Run unit tests (no WordPress loaded): - -```bash -composer run test:unit -# or -vendor/bin/codecept run unit -``` +1. WP Unit Tests - (Unit and Integration Tests) +2. E2E Tests - Playright tests ### WPUnit (WordPress-aware Unit/Integration) Tests Run WPUnit tests (WordPress loaded): ```bash -composer run test:wpunit -# or -vendor/bin/codecept run wpunit +sh bin/local/run-unit-tests.sh coverage ``` -### Functional Tests +> [!IMPORTANT] +> You can also add coverage e.g. `sh bin/local/run-unit-tests.sh coverage --coverage-html` and the output will be saved in [tests/_output/coverage/dashboard.html](tests/_output/coverage/dashboard.html) -Run functional tests (simulate web requests): -```bash -composer run test:functional -# or -vendor/bin/codecept run functional -``` - -### Acceptance Tests +### E2WTests Run browser-based acceptance tests: ```bash -composer run test:acceptance -# or -vendor/bin/codecept run acceptance +sh bin/local/run-e2e-tests.sh coverage ``` ### All Tests @@ -142,13 +128,7 @@ tests/ ├── _envs/ # Environment configs ├── _output/ # Test output (logs, coverage) ├── _support/ # Helper classes, modules -├── acceptance/ # Acceptance test cases -├── functional/ # Functional test cases -├── unit/ # Unit test cases ├── wpunit/ # WPUnit (WordPress-aware unit/integration) test cases -├── acceptance.suite.dist.yml -├── functional.suite.dist.yml -├── unit.suite.dist.yml ├── wpunit.suite.dist.yml └── wpunit/ └── bootstrap.php # Bootstrap for WPUnit tests diff --git a/plugins/hwp-previews/activation.php b/plugins/hwp-previews/activation.php index e4c1883d..b48c276f 100644 --- a/plugins/hwp-previews/activation.php +++ b/plugins/hwp-previews/activation.php @@ -3,6 +3,8 @@ * Activation Hook * * @package HWP\Previews + * + * @since 0.0.1 */ declare(strict_types=1); diff --git a/plugins/hwp-previews/bin/local/run-e2e-tests.sh b/plugins/hwp-previews/bin/local/run-e2e-tests.sh new file mode 100755 index 00000000..f0e260a8 --- /dev/null +++ b/plugins/hwp-previews/bin/local/run-e2e-tests.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash + +# Run Playwright E2E tests for GitHub Actions + +set -e + +# Install dependencies if needed +npm ci + +# Install composer dependencies +composer install + +# Install Playwright browsers +npx playwright install --with-deps + +# Start wp-env +npm run wp-env start + +# Run Playwright tests +npm run test:e2e + +# Stop wp-env +npm run wp-env stop diff --git a/plugins/hwp-previews/codeception.dist.yml b/plugins/hwp-previews/codeception.dist.yml index cf0411f5..002a5ff9 100644 --- a/plugins/hwp-previews/codeception.dist.yml +++ b/plugins/hwp-previews/codeception.dist.yml @@ -31,13 +31,15 @@ extensions: - lucatume\WPBrowser\Command\MonkeyCachePath - lucatume\WPBrowser\Command\RunAll - lucatume\WPBrowser\Command\RunOriginal +exclude: + - src/Templates + - src/Templates/* coverage: enabled: true remote: false c3_url: "%WORDPRESS_URL%/wp-content/plugins/hwp-previews/hwp-previews.php" include: - src/* - - /access-functions.php - /activation.php - /deactivation.php exclude: @@ -46,8 +48,8 @@ coverage: - /node_modules/* - /packages/* - /tests/* - - /vendor-prefixed/* - /vendor/* + - /src/Templates/* show_only_summary: false modules: config: diff --git a/plugins/hwp-previews/composer.json b/plugins/hwp-previews/composer.json index 005b1c76..efcb0de3 100644 --- a/plugins/hwp-previews/composer.json +++ b/plugins/hwp-previews/composer.json @@ -2,7 +2,8 @@ "name": "hwp/previews", "type": "wordpress-plugin", "description": "A WordPress plugin for headless previews.", - "license": "GPL-3.0-or-later", + "license": "GPL-2.0", + "version": "0.0.1-beta", "authors": [ { "name": "WP Engine Headless OSS Development Team", @@ -13,7 +14,11 @@ "keywords": [ "package", "dependency", - "autoload" + "autoload", + "headless", + "wordpress", + "plugin", + "previews" ], "require": { "php": "^7.4 || ^8.0" @@ -21,6 +26,7 @@ "minimum-stability": "dev", "prefer-stable": true, "require-dev": { + "10up/wp_mock": "^1.1", "automattic/vipwpcs": "^3.0", "codeception/lib-innerbrowser": "^1.0", "codeception/module-asserts": "^1.0", @@ -89,7 +95,13 @@ ".env.dist", "c3.php", "codeception.dist.yml", - "tests" + "tests", + "artifacts", + "package.json", + "package-lock.json", + "node_modules/", + "/TESTING.md", + "/screenshots" ] }, "autoload": { @@ -128,7 +140,16 @@ ], "php:psalm": "psalm", "php:psalm:info": "psalm --show-info=true", - "php:psalm:fix": "psalm --alter" + "php:psalm:fix": "psalm --alter", + "qa": "sh bin/local/run-qa.sh", + "test": [ + "sh bin/local/run-unit-tests.sh", + "sh bin/local/run-e2e-tests.sh" + ], + "test:unit": "sh bin/local/run-unit-tests.sh", + "test:unit:coverage": "sh bin/local/run-unit-tests.sh coverage", + "test:unit:coverage-html": "sh bin/local/run-unit-tests.sh coverage --coverage-html", + "test:e2e": "sh bin/local/run-e2e-tests.sh" }, "support": { "docs": "https://github.com/wpengine/hwptoolkit/tree/main/docs", diff --git a/plugins/hwp-previews/composer.lock b/plugins/hwp-previews/composer.lock index d1103157..65914656 100644 --- a/plugins/hwp-previews/composer.lock +++ b/plugins/hwp-previews/composer.lock @@ -4,9 +4,63 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "6a6d9ee1f52084304a1ff6e820c6e2e1", + "content-hash": "14b234f7f446cb76feed308e262a287e", "packages": [], "packages-dev": [ + { + "name": "10up/wp_mock", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/10up/wp_mock.git", + "reference": "f25b5895ed31bf5e7036fe0c666664364ae011c2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/10up/wp_mock/zipball/f25b5895ed31bf5e7036fe0c666664364ae011c2", + "reference": "f25b5895ed31bf5e7036fe0c666664364ae011c2", + "shasum": "" + }, + "require": { + "antecedent/patchwork": "^2.1", + "mockery/mockery": "^1.6", + "php": ">=7.4 < 9", + "phpunit/phpunit": "^9.6" + }, + "require-dev": { + "behat/behat": "^v3.11.0", + "dealerdirect/phpcodesniffer-composer-installer": "^0.7", + "friendsofphp/php-cs-fixer": "^3.4", + "php-coveralls/php-coveralls": "^v2.7", + "php-stubs/wordpress-globals": "^0.2", + "php-stubs/wordpress-stubs": "^6.3", + "phpcompatibility/php-compatibility": "^9.3", + "phpstan/phpstan": "^1.10", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-phpunit": "^1.3", + "sebastian/comparator": "^4.0.8", + "sempro/phpunit-pretty-print": "^1.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "WP_Mock\\": "./php/WP_Mock" + }, + "classmap": [ + "php/WP_Mock.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "A mocking library to take the pain out of unit testing for WordPress", + "support": { + "issues": "https://github.com/10up/wp_mock/issues", + "source": "https://github.com/10up/wp_mock/tree/1.1.0" + }, + "time": "2025-03-12T00:36:13+00:00" + }, { "name": "amphp/amp", "version": "v2.6.4", @@ -167,6 +221,54 @@ ], "time": "2024-04-13T18:00:56+00:00" }, + { + "name": "antecedent/patchwork", + "version": "2.2.1", + "source": { + "type": "git", + "url": "https://github.com/antecedent/patchwork.git", + "reference": "1bf183a3e1bd094f231a2128b9ecc5363c269245" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/antecedent/patchwork/zipball/1bf183a3e1bd094f231a2128b9ecc5363c269245", + "reference": "1bf183a3e1bd094f231a2128b9ecc5363c269245", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpunit/phpunit": ">=4" + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignas Rudaitis", + "email": "ignas.rudaitis@gmail.com" + } + ], + "description": "Method redefinition (monkey-patching) functionality for PHP.", + "homepage": "https://antecedent.github.io/patchwork/", + "keywords": [ + "aop", + "aspect", + "interception", + "monkeypatching", + "redefinition", + "runkit", + "testing" + ], + "support": { + "issues": "https://github.com/antecedent/patchwork/issues", + "source": "https://github.com/antecedent/patchwork/tree/2.2.1" + }, + "time": "2024-12-11T10:19:54+00:00" + }, { "name": "automattic/vipwpcs", "version": "3.0.1", diff --git a/plugins/hwp-previews/deactivation.php b/plugins/hwp-previews/deactivation.php index a1827df2..ffe8d0fa 100644 --- a/plugins/hwp-previews/deactivation.php +++ b/plugins/hwp-previews/deactivation.php @@ -6,6 +6,8 @@ * Deactivation Hook * * @package HWP\Previews + * + * @since 0.0.1 */ /** diff --git a/plugins/hwp-previews/hwp-previews.php b/plugins/hwp-previews/hwp-previews.php index 1b332cb3..f9c6ba73 100644 --- a/plugins/hwp-previews/hwp-previews.php +++ b/plugins/hwp-previews/hwp-previews.php @@ -7,7 +7,7 @@ * Author: WPEngine Headless OSS Team * Author URI: https://github.com/wpengine * Update URI: https://github.com/wpengine/hwptoolkit - * Version: 0.0.1 + * Version: 0.0.1-beta * Text Domain: hwp-previews * Domain Path: /languages * Requires at least: 6.0 @@ -46,88 +46,92 @@ register_deactivation_hook( __FILE__, 'hwp_previews_deactivation_callback' ); } -/** - * Define plugin constants. - * - * phpcs:disable Generic.Metrics.CyclomaticComplexity.TooHigh - * phpcs:disable SlevomatCodingStandard.Complexity.Cognitive.ComplexityTooHigh - */ -function hwp_previews_constants(): void { - if ( ! defined( 'HWP_PREVIEWS_VERSION' ) ) { - define( 'HWP_PREVIEWS_VERSION', '0.0.1' ); - } - - if ( ! defined( 'HWP_PREVIEWS_PLUGIN_DIR' ) ) { - define( 'HWP_PREVIEWS_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); - } - - if ( ! defined( 'HWP_PREVIEWS_PLUGIN_URL' ) ) { - define( 'HWP_PREVIEWS_PLUGIN_URL', plugin_dir_url( __FILE__ ) ); - } - - if ( ! defined( 'HWP_PREVIEWS_PLUGIN_FILE' ) ) { - define( 'HWP_PREVIEWS_PLUGIN_FILE', __FILE__ ); - } - - if ( ! defined( 'HWP_PREVIEWS_AUTOLOAD' ) ) { - define( 'HWP_PREVIEWS_AUTOLOAD', true ); - } - - if ( ! defined( 'HWP_PREVIEWS_SETTINGS_GROUP' ) ) { - define( 'HWP_PREVIEWS_SETTINGS_GROUP', 'hwp_previews_settings_group' ); - } - if ( ! defined( 'HWP_PREVIEWS_SETTINGS_KEY' ) ) { - define( 'HWP_PREVIEWS_SETTINGS_KEY', 'hwp_previews_settings' ); - } +// phpcs:enable Generic.Metrics.CyclomaticComplexity.TooHigh +// phpcs:enable SlevomatCodingStandard.Complexity.Cognitive.ComplexityTooHigh - if ( ! defined( 'HWP_PREVIEWS_TEXT_DOMAIN' ) ) { - define( 'HWP_PREVIEWS_TEXT_DOMAIN', 'hwp-previews' ); +if ( ! function_exists( 'hwp_previews_init' ) ) { + /** + * Initializes plugin. + */ + function hwp_previews_init(): void { + hwp_previews_constants(); + hwp_previews_plugin_init(); + hwp_previews_plugin_admin_notice(); } +} - // Plugin Template Directory. - if ( ! defined( 'HWP_PREVIEWS_TEMPLATE_DIR' ) ) { - define( 'HWP_PREVIEWS_TEMPLATE_DIR', trailingslashit( HWP_PREVIEWS_PLUGIN_DIR ) . '/src/Admin/Settings/Templates/' ); +if ( ! function_exists( 'hwp_previews_constants' ) ) { + /** + * Define plugin constants. + */ + function hwp_previews_constants(): void { + if ( ! defined( 'HWP_PREVIEWS_VERSION' ) ) { + define( 'HWP_PREVIEWS_VERSION', '0.0.1-beta' ); + } + + if ( ! defined( 'HWP_PREVIEWS_PLUGIN_DIR' ) ) { + define( 'HWP_PREVIEWS_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); + } + + if ( ! defined( 'HWP_PREVIEWS_PLUGIN_URL' ) ) { + define( 'HWP_PREVIEWS_PLUGIN_URL', plugin_dir_url( __FILE__ ) ); + } + + if ( ! defined( 'HWP_PREVIEWS_SETTINGS_GROUP' ) ) { + define( 'HWP_PREVIEWS_SETTINGS_GROUP', 'hwp_previews_settings_group' ); + } + + if ( ! defined( 'HWP_PREVIEWS_SETTINGS_KEY' ) ) { + define( 'HWP_PREVIEWS_SETTINGS_KEY', 'hwp_previews_settings' ); + } } } -// phpcs:enable Generic.Metrics.CyclomaticComplexity.TooHigh -// phpcs:enable SlevomatCodingStandard.Complexity.Cognitive.ComplexityTooHigh - -/** - * Initializes plugin. - */ -function hwp_previews_init(): void { - hwp_previews_constants(); - - if ( defined( 'HWP_PREVIEWS_PLUGIN_DIR' ) ) { +if ( ! function_exists( 'hwp_previews_plugin_init' ) ) { + /** + * Initialize the HWP Previews plugin. + */ + function hwp_previews_plugin_init(): ?Plugin { + if ( ! defined( 'HWP_PREVIEWS_PLUGIN_DIR' ) ) { + return null; + } require_once HWP_PREVIEWS_PLUGIN_DIR . 'src/Plugin.php'; - Plugin::instance(); - - return; + return Plugin::init(); } +} - add_action( - 'admin_notices', - static function (): void { - ?> -
-

- -

-
- +
+

+ +

+
+ diff --git a/plugins/hwp-previews/src/Admin/Settings/Fields/Field/Checkbox_Field.php b/plugins/hwp-previews/src/Admin/Settings/Fields/Field/Checkbox_Field.php index f73484ce..b46219f5 100644 --- a/plugins/hwp-previews/src/Admin/Settings/Fields/Field/Checkbox_Field.php +++ b/plugins/hwp-previews/src/Admin/Settings/Fields/Field/Checkbox_Field.php @@ -4,6 +4,15 @@ namespace HWP\Previews\Admin\Settings\Fields\Field; +/** + * Checkbox field class + * + * This class represents a checkbox field in the settings of the HWP Previews plugin. + * + * @package HWP\Previews + * + * @since 0.0.1 + */ class Checkbox_Field extends Abstract_Settings_Field { /** * The default value for the field. diff --git a/plugins/hwp-previews/src/Admin/Settings/Fields/Field/Text_Input_Field.php b/plugins/hwp-previews/src/Admin/Settings/Fields/Field/Text_Input_Field.php index 8d781e2b..e5576f0b 100644 --- a/plugins/hwp-previews/src/Admin/Settings/Fields/Field/Text_Input_Field.php +++ b/plugins/hwp-previews/src/Admin/Settings/Fields/Field/Text_Input_Field.php @@ -4,6 +4,15 @@ namespace HWP\Previews\Admin\Settings\Fields\Field; +/** + * Text input class + * + * This class represents a text input field in the settings of the HWP Previews plugin. + * + * @package HWP\Previews + * + * @since 0.0.1 + */ class Text_Input_Field extends Abstract_Settings_Field { /** * The default value for the field. diff --git a/plugins/hwp-previews/src/Admin/Settings/Fields/Field/URL_Input_Field.php b/plugins/hwp-previews/src/Admin/Settings/Fields/Field/URL_Input_Field.php index f9b15b89..db9baf22 100644 --- a/plugins/hwp-previews/src/Admin/Settings/Fields/Field/URL_Input_Field.php +++ b/plugins/hwp-previews/src/Admin/Settings/Fields/Field/URL_Input_Field.php @@ -4,6 +4,15 @@ namespace HWP\Previews\Admin\Settings\Fields\Field; +/** + * URL input class + * + * This class represents a url input field in the settings of the HWP Previews plugin. + * + * @package HWP\Previews + * + * @since 0.0.1 + */ class URL_Input_Field extends Text_Input_Field { /** * @param mixed $value @@ -43,7 +52,20 @@ public function get_input_type(): string { * @param string $value */ private function fix_url( string $value ): string { - // Remove HTML tags, trim, encode spaces, add protocol. + + + if ( '' === $value ) { + return ''; + } + + // Remove #is', '', $value ); + + if ( ! is_string( $value ) || '' === trim( $value ) ) { + return ''; + } + + // Remove HTML tags except curly braces, trim, encode spaces, add protocol. $value = preg_replace( '/<(?!\{)[^>]+>/', '', $value ); $value = trim( str_replace( ' ', '%20', (string) $value ) ); @@ -51,11 +73,11 @@ private function fix_url( string $value ): string { return ''; } - $has_prootocol = preg_match( '/^https?:\/\//i', $value ) === 1; - if ( $has_prootocol ) { + $has_protocol = preg_match( '/^https?:\/\//i', $value ) === 1; + if ( $has_protocol ) { return $value; } - $protocol = is_ssl() ? 'https://' : 'http://'; - return $protocol . ltrim( $value, '/' ); + $protocol = is_ssl() ? 'https://' : 'http://'; + return $protocol . ltrim( $value, '/' ); } } diff --git a/plugins/hwp-previews/src/Admin/Settings/Fields/Settings_Field_Collection.php b/plugins/hwp-previews/src/Admin/Settings/Fields/Settings_Field_Collection.php index c4a43357..399d389e 100644 --- a/plugins/hwp-previews/src/Admin/Settings/Fields/Settings_Field_Collection.php +++ b/plugins/hwp-previews/src/Admin/Settings/Fields/Settings_Field_Collection.php @@ -7,7 +7,47 @@ use HWP\Previews\Admin\Settings\Fields\Field\Checkbox_Field; use HWP\Previews\Admin\Settings\Fields\Field\URL_Input_Field; +/** + * Class Settings_Field_Collection + * + * This class manages a collection of settings fields for the HWP Previews plugin. + * It provides methods to add, remove, and retrieve fields, as well as to initialize them. + * + * @package HWP\Previews + * + * @since 0.0.1 + */ class Settings_Field_Collection { + /** + * The ID of the field that enables or disables the previews. + * + * @var string + */ + public const ENABLED_FIELD_ID = 'enabled'; + + + /** + * The ID of the field that allows using post-statuses as parent. + * + * @var string + */ + public const POST_STATUSES_AS_PARENT_FIELD_ID = 'post_statuses_as_parent'; + + + /** + * The ID of the field that indicates whether the preview is rendered in an iframe. + * + * @var string + */ + public const IN_IFRAME_FIELD_ID = 'in_iframe'; + + /** + * The ID of the field that contains the preview URL. + * + * @var string + */ + public const PREVIEW_URL_FIELD_ID = 'preview_url'; + /** * Array of fields * @@ -66,7 +106,7 @@ protected function initialize_fields(): void { $this->add_field( new Checkbox_Field( - 'enabled', + self::ENABLED_FIELD_ID, false, __( 'Enable Previews', 'hwp-previews' ), __( 'Enable previews for post type.', 'hwp-previews' ) @@ -75,7 +115,7 @@ protected function initialize_fields(): void { $this->add_field( new Checkbox_Field( - 'post_statuses_as_parent', + self::POST_STATUSES_AS_PARENT_FIELD_ID, true, __( 'Allow all post statuses in parents option', 'hwp-previews' ), __( 'By default WordPress only allows published posts to be parents. This option allows posts of all statuses to be used as parent within hierarchical post types.', 'hwp-previews' ) @@ -84,7 +124,7 @@ protected function initialize_fields(): void { $this->add_field( new Checkbox_Field( - 'in_iframe', + self::IN_IFRAME_FIELD_ID, false, __( 'Use iframe to render previews', 'hwp-previews' ), __( 'With this option enabled, headless previews will be displayed inside an iframe on the preview page, without leaving WordPress.', 'hwp-previews' ) @@ -93,7 +133,7 @@ protected function initialize_fields(): void { $this->add_field( new URL_Input_Field( - 'preview_url', + self::PREVIEW_URL_FIELD_ID, false, __( 'Preview URL', 'hwp-previews' ), __( 'Construct your preview URL using the tags on the right. You can add any parameters needed to support headless previews.', 'hwp-previews' ), diff --git a/plugins/hwp-previews/src/Admin/Settings/Menu/Menu_Page.php b/plugins/hwp-previews/src/Admin/Settings/Menu/Menu_Page.php index d23fe0f9..f6c2a2e1 100644 --- a/plugins/hwp-previews/src/Admin/Settings/Menu/Menu_Page.php +++ b/plugins/hwp-previews/src/Admin/Settings/Menu/Menu_Page.php @@ -4,6 +4,15 @@ namespace HWP\Previews\Admin\Settings\Menu; +/** + * Menu class for WordPress admin settings page. + * + * This class is responsible for creating a menu page in the WordPress admin area. + * + * @package HWP\Previews + * + * @since 0.0.1 + */ class Menu_Page { /** * The title of the page. @@ -41,13 +50,6 @@ class Menu_Page { */ protected array $args; - /** - * The name of a Dashicons helper class to use a font icon. - * - * @var string - */ - protected string $icon; - /** * Constructor. * @@ -56,22 +58,19 @@ class Menu_Page { * @param string $menu_slug The slug name to refer to this menu by. Should be unique for this menu and only include lowercase alphanumeric, dashes, and underscores characters to be compatible with sanitize_key(). * @param string $template The template that will be included in the callback. * @param array> $args The args array for the template. - * @param string $icon The name of a Dashicons helper class to use a font icon. */ public function __construct( string $page_title, string $menu_title, string $menu_slug, string $template, - array $args = [], - string $icon = 'dashicons-admin-generic' + array $args = [] ) { $this->page_title = $page_title; $this->menu_title = $menu_title; $this->menu_slug = $menu_slug; $this->template = $template; $this->args = $args; - $this->icon = $icon; } /** @@ -84,8 +83,7 @@ public function register_page(): void { $this->menu_title, 'manage_options', $this->menu_slug, - [ $this, 'registration_callback' ], - 999 + [ $this, 'registration_callback' ] ); } @@ -101,18 +99,11 @@ public function registration_callback(): void { return; } - $this->set_query_vars(); - - // phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.UsingVariable -- $this->template is validated and defined within the class - include_once $this->template; - } - - /** - * Sets the query vars for the template. - */ - protected function set_query_vars(): void { foreach ( $this->args as $query_var => $args ) { set_query_var( $query_var, $args ); } + + // phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.UsingVariable -- $this->template is validated and defined within the class + include_once $this->template; } } diff --git a/plugins/hwp-previews/src/Admin/Settings/Settings_Form_Manager.php b/plugins/hwp-previews/src/Admin/Settings/Settings_Form_Manager.php index 52825add..1c6cf5a7 100644 --- a/plugins/hwp-previews/src/Admin/Settings/Settings_Form_Manager.php +++ b/plugins/hwp-previews/src/Admin/Settings/Settings_Form_Manager.php @@ -5,7 +5,17 @@ namespace HWP\Previews\Admin\Settings; use HWP\Previews\Admin\Settings\Fields\Settings_Field_Collection; - +use HWP\Previews\Preview\Post\Post_Settings_Service; + +/** + * Settings Form Manager class for HWP Previews. + * + * This class manages the settings form for different post types, allowing for the registration and sanitization of settings fields. + * + * @package HWP\Previews + * + * @since 0.0.1 + */ class Settings_Form_Manager { /** * Array of available post-types for which settings are registered. @@ -19,15 +29,22 @@ class Settings_Form_Manager { * * @var \HWP\Previews\Admin\Settings\Fields\Settings_Field_Collection */ - protected Settings_Field_Collection $fields; + protected Settings_Field_Collection $field_collection; /** * @param array $post_types Array of post types for which settings are registered. * @param \HWP\Previews\Admin\Settings\Fields\Settings_Field_Collection $fields Collection of fields to be registered in the settings sections. */ public function __construct( array $post_types, Settings_Field_Collection $fields ) { - $this->post_types = $post_types; - $this->fields = $fields; + $this->set_post_types( $post_types ); + $this->set_field_collection( $fields ); + + /** + * Fire off init action. + * + * @param \HWP\Previews\Admin\Settings\Settings_Form_Manager $instance the instance of the settings class. + */ + do_action( 'hwp_previews_settings_form_manager_init', $this ); } /** @@ -38,7 +55,7 @@ public function __construct( array $post_types, Settings_Field_Collection $field public function render_form(): void { $this->create_tabs(); - foreach ( $this->post_types as $post_type => $label ) { + foreach ( $this->get_post_types() as $post_type => $label ) { $this->render_post_type_section( $post_type, $label ); } } @@ -52,8 +69,7 @@ public function render_form(): void { */ public function sanitize_settings( array $new_input ): array { - // @TODO - Refactor with Settings Group. - $option_name = apply_filters( 'hwp_previews_settings_key', HWP_PREVIEWS_SETTINGS_KEY ); + $option_name = $this->get_option_key(); $old_input = (array) get_option( $option_name, [] ); @@ -74,7 +90,7 @@ public function sanitize_settings( array $new_input ): array { // Sanitize the fields in the tab. $sanitized_fields = []; foreach ( $new_input[ $tab_to_sanitize ] as $key => $value ) { - $field = $this->fields->get_field( $key ); + $field = $this->get_field_collection()->get_field( $key ); if ( is_null( $field ) ) { continue; // Skip unknown fields. } @@ -88,6 +104,50 @@ public function sanitize_settings( array $new_input ): array { return $old_input; } + /** + * Get the option key for the settings group. + */ + public function get_option_key(): string { + return Post_Settings_Service::get_option_key(); + } + + /** + * Get the settings group for the options. + */ + public function get_settings_group(): string { + return Post_Settings_Service::get_settings_group(); + } + + /** + * Get the fields collection for the settings form. + */ + public function get_field_collection(): Settings_Field_Collection { + return $this->field_collection; + } + + /** + * @param \HWP\Previews\Admin\Settings\Fields\Settings_Field_Collection $field_collection + */ + public function set_field_collection( Settings_Field_Collection $field_collection ): void { + $this->field_collection = $field_collection; + } + + /** + * The available post types for which settings are registered. + * + * @return array + */ + public function get_post_types(): array { + return $this->post_types; + } + + /** + * @param array $post_types + */ + public function set_post_types( array $post_types ): void { + $this->post_types = $post_types; + } + /** * Create the settings tabs for the post-types. * @@ -95,9 +155,8 @@ public function sanitize_settings( array $new_input ): array { */ protected function create_tabs(): void { - // @TODO - Refactor with Settings Group. - $option_group = apply_filters( 'hwp_previews_settings_group', HWP_PREVIEWS_SETTINGS_GROUP ); - $option_name = apply_filters( 'hwp_previews_settings_key', HWP_PREVIEWS_SETTINGS_KEY ); + $option_group = $this->get_settings_group(); + $option_name = $this->get_option_key(); register_setting( $option_group, @@ -119,7 +178,7 @@ protected function create_tabs(): void { * @param string $label The label for the post type section. */ protected function render_post_type_section( string $post_type, string $label ): void { - $fields = $this->fields->get_fields(); + $fields = $this->get_field_collection()->get_fields(); $page_id = 'hwp_previews_section_' . $post_type; $page_uri = 'hwp-previews-' . $post_type; $is_hierarchical = is_post_type_hierarchical( $post_type ); @@ -138,7 +197,7 @@ protected function render_post_type_section( string $post_type, string $label ): [ 'post_type' => $post_type, 'label' => $label, - 'settings_key' => HWP_PREVIEWS_SETTINGS_KEY, + 'settings_key' => Post_Settings_Service::get_option_key(), ] ); } diff --git a/plugins/hwp-previews/src/Admin/Settings_Page.php b/plugins/hwp-previews/src/Admin/Settings_Page.php index 1d30c54d..18948f61 100644 --- a/plugins/hwp-previews/src/Admin/Settings_Page.php +++ b/plugins/hwp-previews/src/Admin/Settings_Page.php @@ -7,10 +7,18 @@ use HWP\Previews\Admin\Settings\Fields\Settings_Field_Collection; use HWP\Previews\Admin\Settings\Menu\Menu_Page; use HWP\Previews\Admin\Settings\Settings_Form_Manager; -use HWP\Previews\Post\Type\Contracts\Post_Types_Config_Interface; -use HWP\Previews\Post\Type\Post_Types_Config_Registry; use HWP\Previews\Preview\Parameter\Preview_Parameter_Registry; - +use HWP\Previews\Preview\Post\Post_Preview_Service; + +/** + * Settings_Page class for HWP Previews. + * + * This class handles the registration of the settings page, settings fields, and loading of scripts and styles for the plugin. + * + * @package HWP\Previews + * + * @since 0.0.1 + */ class Settings_Page { /** * @var string The slug for the plugin menu. @@ -23,9 +31,11 @@ class Settings_Page { protected Preview_Parameter_Registry $parameters; /** - * @var \HWP\Previews\Post\Type\Contracts\Post_Types_Config_Interface The post types available for previews. + * Post-preview service to get post types and statuses for the settings page. + * + * @var \HWP\Previews\Preview\Post\Post_Preview_Service */ - protected Post_Types_Config_Interface $types_config; + protected Post_Preview_Service $post_preview_service; /** * The instance of the plugin. @@ -39,21 +49,21 @@ class Settings_Page { * * Initializes the settings page, registers settings fields, and loads scripts and styles. */ - public function __construct() { - $this->parameters = Preview_Parameter_Registry::get_instance(); - $this->types_config = Post_Types_Config_Registry::get_post_type_config(); - - $this->register_settings_pages(); - $this->register_settings_fields(); - $this->load_scripts_styles(); + protected function __construct() { + $this->parameters = Preview_Parameter_Registry::get_instance(); + $this->post_preview_service = new Post_Preview_Service(); } /** * Initializes the settings page. */ - public static function init(): Settings_Page { + public static function init(): ?Settings_Page { + if ( ! is_admin() ) { + return null; + } if ( ! isset( self::$instance ) || ! ( is_a( self::$instance, self::class ) ) ) { self::$instance = new self(); + self::$instance->setup(); } /** @@ -61,50 +71,55 @@ public static function init(): Settings_Page { * * @param \HWP\Previews\Admin\Settings_Page $instance the instance of the plugin class. */ - do_action( 'hwp_previews_init', self::$instance ); + do_action( 'hwp_previews_settings_init', self::$instance ); return self::$instance; } + /** + * Sets up the settings page by registering hooks. + */ + public function setup(): void { + add_action( 'admin_menu', [ $this, 'register_settings_page' ], 10, 0 ); + add_action( 'admin_init', [ $this, 'register_settings_fields' ], 10, 0 ); + add_action( 'admin_enqueue_scripts', [ $this, 'load_scripts_styles' ], 10, 1 ); + } + /** * Registers the settings page. */ - public function register_settings_pages(): void { - add_action( 'admin_menu', function (): void { - - // Note: We didn't initalize in the constructor because we need to ensure - // the post types are registered before we can use them. - $post_types = $this->types_config->get_public_post_types(); - - $page = new Menu_Page( - __( 'HWP Previews Settings', 'hwp-previews' ), - 'HWP Previews', - self::PLUGIN_MENU_SLUG, - trailingslashit( HWP_PREVIEWS_TEMPLATE_DIR ) . 'settings-page-main.php', - [ - 'hwp_previews_main_page_config' => [ - 'tabs' => $post_types, - 'current_tab' => $this->get_current_tab( $post_types ), - 'params' => $this->parameters->get_descriptions(), - ], + public function register_settings_page(): void { + + // Note: We didn't initialise in the constructor because we need to ensure + // the post-types are registered before we can use them. + $post_types = $this->post_preview_service->get_post_types(); + + $page = new Menu_Page( + __( 'HWP Previews Settings', 'hwp-previews' ), + 'HWP Previews', + self::PLUGIN_MENU_SLUG, + trailingslashit( HWP_PREVIEWS_PLUGIN_DIR ) . 'src/Templates/admin.php', + [ + 'hwp_previews_main_page_config' => [ + 'tabs' => $post_types, + 'current_tab' => $this->get_current_tab( $post_types ), + 'params' => $this->parameters->get_descriptions(), ], - ); + ], + ); - $page->register_page(); - } ); + $page->register_page(); } /** * Registers the settings fields for each post type. */ public function register_settings_fields(): void { - add_action( 'admin_init', function (): void { - $settings_manager = new Settings_Form_Manager( - $this->types_config->get_public_post_types(), - new Settings_Field_Collection() - ); - $settings_manager->render_form(); - }, 10, 0 ); + $settings_manager = new Settings_Form_Manager( + $this->post_preview_service->get_post_types(), + new Settings_Field_Collection() + ); + $settings_manager->render_form(); } /** @@ -129,28 +144,28 @@ public function get_current_tab( array $post_types, string $tab = 'tab' ): strin /** * Enqueues the JavaScript and the CSS file for the plugin admin area. + * + * @param string $hook The current admin page hook. */ - public function load_scripts_styles(): void { - add_action( 'admin_enqueue_scripts', static function ( string $hook ): void { - - if ( 'settings_page_' . self::PLUGIN_MENU_SLUG !== $hook ) { - return; - } - - wp_enqueue_script( - 'hwp-previews-js', - trailingslashit( HWP_PREVIEWS_PLUGIN_URL ) . 'assets/js/hwp-previews.js', - [], - HWP_PREVIEWS_VERSION, - true - ); - - wp_enqueue_style( - 'hwp-previews-css', - trailingslashit( HWP_PREVIEWS_PLUGIN_URL ) . 'assets/css/hwp-previews.css', - [], - HWP_PREVIEWS_VERSION - ); - } ); + public function load_scripts_styles( string $hook ): void { + + if ( 'settings_page_' . self::PLUGIN_MENU_SLUG !== $hook ) { + return; + } + + wp_enqueue_script( + 'hwp-previews-js', + trailingslashit( HWP_PREVIEWS_PLUGIN_URL ) . 'assets/js/hwp-previews.js', + [], + HWP_PREVIEWS_VERSION, + true + ); + + wp_enqueue_style( + 'hwp-previews-css', + trailingslashit( HWP_PREVIEWS_PLUGIN_URL ) . 'assets/css/hwp-previews.css', + [], + HWP_PREVIEWS_VERSION + ); } } diff --git a/plugins/hwp-previews/src/Autoloader.php b/plugins/hwp-previews/src/Autoloader.php index 056b1a0a..1d9f0f1f 100644 --- a/plugins/hwp-previews/src/Autoloader.php +++ b/plugins/hwp-previews/src/Autoloader.php @@ -1,18 +1,15 @@ post_preview_service = new Post_Preview_Service(); + $this->post_settings_service = new Post_Settings_Service(); + } /** * Initialize the hooks for the preview functionality. */ - public static function init(): void { - self::init_class_properties(); - self::add_hook_actions(); + public static function init(): self { + if ( ! isset( self::$instance ) || ! ( is_a( self::$instance, self::class ) ) ) { + self::$instance = new self(); + self::$instance->setup(); + } + + return self::$instance; } /** * Registers the hooks for the preview functionality. */ - public static function add_hook_actions(): void { - if ( null === self::$types_config ) { - return; - } + public function setup(): void { // Enable post statuses as parent for the post types specified in the post-types config. - add_filter( 'page_attributes_dropdown_pages_args', [ self::class, 'enable_post_statuses_as_parent' ], 10, 1 ); - add_filter( 'quick_edit_dropdown_pages_args', [ self::class, 'enable_post_statuses_as_parent' ], 10, 1 ); + add_filter( 'page_attributes_dropdown_pages_args', [ $this, 'enable_post_statuses_as_parent' ], 10, 1 ); + add_filter( 'quick_edit_dropdown_pages_args', [ $this, 'enable_post_statuses_as_parent' ], 10, 1 ); - foreach ( self::$types_config->get_post_types() as $post_type ) { - if ( ! self::$types_config->gutenberg_editor_enabled( $post_type ) ) { + $post_editor_service = new Post_Editor_Service(); + $post_types = $this->post_preview_service->get_post_types(); + + // Enable post-statuses as parent for the post-types specified in the post-types config. + foreach ( $post_types as $post_type => $label ) { + if ( ! $post_editor_service->gutenberg_editor_enabled( $post_type ) ) { continue; } - // @TODO - Add unit tests for this filter. - add_filter( 'rest_' . $post_type . '_query', [ self::class, 'enable_post_statuses_as_parent' ], 10, 1 ); + + add_filter( 'rest_' . $post_type . '_query', [ $this, 'enable_post_statuses_as_parent' ], 10, 1 ); } // iframe preview functionality. - add_filter( 'template_include', [ self::class, 'add_iframe_preview_template' ], 10, 1 ); + add_filter( 'template_include', [ $this, 'add_iframe_preview_template' ], 10, 1 ); // Preview link functionality. Extra priority to ensure it runs after the Faust preview link filter. - add_filter( 'preview_post_link', [ self::class, 'update_preview_post_link' ], 1001, 2 ); + add_filter( 'preview_post_link', [ $this, 'update_preview_post_link' ], 1001, 2 ); /** * Hack Function that changes the preview link for draft articles, * this must be removed when properly fixed https://github.com/WordPress/gutenberg/issues/13998. */ - foreach ( self::$types_config->get_public_post_types() as $key => $label ) { - add_filter( 'rest_prepare_' . $key, [ self::class, 'filter_rest_prepare_link' ], 10, 2 ); + foreach ( $post_types as $post_type => $label ) { + add_filter( 'rest_prepare_' . $post_type, [ $this, 'filter_rest_prepare_link' ], 10, 2 ); } } /** - * Initializes the class properties. - */ - public static function init_class_properties(): void { - - self::$settings_helper = Settings_Helper::get_instance(); - - self::$types_config = apply_filters( - 'hwp_previews_hooks_post_type_config', - Post_Types_Config_Registry::get_post_type_config() - ); - - // Initialize the post types and statuses configurations. - self::$statuses_config = apply_filters( - 'hwp_previews_hooks_post_status_config', - ( new Post_Statuses_Config() )->set_post_statuses( self::get_post_statuses() ) - ); - - // Initialize the preview link service. - self::$link_service = apply_filters( - 'hwp_previews_hooks_preview_link_service', - new Preview_Link_Service( - // @phpstan-ignore-next-line - self::$types_config, - // @phpstan-ignore-next-line - self::$statuses_config, - new Preview_Link_Placeholder_Resolver( Preview_Parameter_Registry::get_instance() ) - ) - ); - } - - /** - * Gets a list of available post statuses for the preview functionality.. - * - * @return array - */ - public static function get_post_statuses(): array { - $post_statuses = [ - 'publish', - 'future', - 'draft', - 'pending', - 'private', - 'auto-draft', - ]; - - return apply_filters( 'hwp_previews_hooks_post_statuses', $post_statuses ); - } - - /** - * Enable post statuses as parent for the post types specified in the post types config. + * Enable post-statuses as parent for the post types specified in the post types config. * * @param array $args The arguments for the dropdown pages. * @@ -150,46 +116,58 @@ public static function get_post_statuses(): array { * * @link https://developer.wordpress.org/reference/hooks/quick_edit_dropdown_pages_args/ */ - public static function enable_post_statuses_as_parent( array $args ): array { - - if ( null === self::$settings_helper || null === self::$types_config || null === self::$statuses_config ) { + public function enable_post_statuses_as_parent( array $args ): array { + if ( ! $this->should_enable_post_statuses_as_parent( $args ) ) { return $args; } - $post_parent_manager = new Post_Parent_Manager( self::$types_config, self::$statuses_config ); + $args['post_status'] = $this->get_statuses_for_parent_post_type(); + + return $args; + } + /** + * Whether post-statuses should be enabled as parent for the given post-type. + * + * @param array $args + */ + public function should_enable_post_statuses_as_parent( array $args ): bool { if ( empty( $args['post_type'] ) ) { - return $args; + return false; } $post_type = (string) $args['post_type']; - // Check if the correspondent setting is enabled. - if ( ! self::$settings_helper->post_statuses_as_parent( $post_type ) ) { - return $args; + if ( ! is_post_type_hierarchical( $post_type ) ) { + return false; } - $post_statuses = $post_parent_manager->get_post_statuses_as_parent( $post_type ); - if ( ! empty( $post_statuses ) ) { - $args['post_status'] = $post_statuses; + $config = $this->post_settings_service->get_post_type_config( $post_type ); + if ( ! is_array( $config ) || empty( $config ) ) { + return false; } - return $args; + $field_id = Settings_Field_Collection::POST_STATUSES_AS_PARENT_FIELD_ID; + + return isset( $config[ $field_id ] ) && (bool) $config[ $field_id ]; } /** * Replace the preview link in the REST response. */ - public static function filter_rest_prepare_link( WP_REST_Response $response, WP_Post $post ): WP_REST_Response { - if ( null === self::$settings_helper ) { + public function filter_rest_prepare_link( WP_REST_Response $response, WP_Post $post ): WP_REST_Response { + + $post_type_service = new Post_Type_Service( $post, $this->post_preview_service, $this->post_settings_service ); + + if ( ! $post_type_service->is_allowed_for_previews() ) { return $response; } - if ( self::$settings_helper->in_iframe( $post->post_type ) ) { + if ( $post_type_service->is_iframe() ) { return $response; } - $preview_url = self::generate_preview_url( $post ); + $preview_url = $this->generate_preview_url( $post ); if ( ! empty( $preview_url ) ) { $response->data['link'] = $preview_url; } @@ -200,47 +178,37 @@ public static function filter_rest_prepare_link( WP_REST_Response $response, WP_ /** * Enable preview functionality in iframe. */ - public static function add_iframe_preview_template( string $template ): string { - - // Bail out if class not initialized or settings helper and link service are not set. - if ( null === self::$settings_helper || null === self::$link_service || null === self::$types_config || null === self::$statuses_config ) { - return $template; - } + public function add_iframe_preview_template( string $template ): string { if ( ! is_preview() ) { return $template; } $post = get_post(); - if ( ! $post instanceof WP_Post ) { + if ( ! ( $post instanceof WP_Post ) ) { return $template; } - // Check if the correspondent setting is enabled. - if ( ! self::$settings_helper->in_iframe( $post->post_type ) ) { + $post_type_service = new Post_Type_Service( $post, $this->post_preview_service, $this->post_settings_service ); + + if ( + ! $post_type_service->is_allowed_for_previews() || + ! $post_type_service->is_iframe() + ) { return $template; } - $template_resolver = new Preview_Template_Resolver( self::$types_config, self::$statuses_config ); - - - /** - * The filter 'hwp_previews_template_path' allows to change the template directory path. - */ - $template_dir_path = (string) apply_filters( - 'hwp_previews_template_path', - trailingslashit( HWP_PREVIEWS_TEMPLATE_DIR ) . 'hwp-preview.php', - ); - - $preview_template = $template_resolver->resolve_template_path( $post, $template_dir_path ); + $template_resolver = new Template_Resolver_Service(); + $iframe_template = $template_resolver->get_iframe_template(); + $url = self::generate_preview_url( $post ); - if ( empty( $preview_template ) ) { + if ( empty( $iframe_template ) || empty( $url ) ) { return $template; } - set_query_var( $template_resolver::HWP_PREVIEWS_IFRAME_PREVIEW_URL, self::generate_preview_url( $post ) ); + $template_resolver->set_query_variable( $url ); - return $preview_template; + return $iframe_template; } /** @@ -248,22 +216,21 @@ public static function add_iframe_preview_template( string $template ): string { * * @link https://developer.wordpress.org/reference/hooks/preview_post_link/ */ - public static function update_preview_post_link( string $preview_link, WP_Post $post ): string { + public function update_preview_post_link( string $preview_link, WP_Post $post ): string { - // Bail out if class not initialized or settings helper and link service are not set. - if ( null === self::$settings_helper || null === self::$link_service ) { + $post_type_service = new Post_Type_Service( $post, $this->post_preview_service, $this->post_settings_service ); + + if ( ! $post_type_service->is_allowed_for_previews() ) { return $preview_link; } - // @TODO - Need to do more testing and add e2e tests for this filter. - - // If iframe option is enabled, we need to resolve preview on the template redirect level. - if ( self::$settings_helper->in_iframe( $post->post_type ) ) { + // If the iframe option is enabled, we need to resolve preview on the template redirect level. + if ( $post_type_service->is_iframe() ) { return $preview_link; } - $url = self::generate_preview_url( $post ); - if ( empty( $url ) ) { + $url = $this->generate_preview_url( $post ); + if ( '' === $url ) { return $preview_link; } @@ -277,18 +244,29 @@ public static function update_preview_post_link( string $preview_link, WP_Post $ * * @return string The generated preview URL. */ - public static function generate_preview_url( WP_Post $post ): string { - if ( null === self::$settings_helper ) { + public function generate_preview_url( WP_Post $post ): string { + + $post_type_service = new Post_Type_Service( $post, $this->post_preview_service, $this->post_settings_service ); + + if ( ! $post_type_service->is_allowed_for_previews() ) { return ''; } - // Check if the correspondent setting is enabled. - $url = self::$settings_helper->url_template( $post->post_type ); - - if ( empty( $url ) || null === self::$link_service ) { + $url = $post_type_service->get_preview_url(); + if ( empty( $url ) ) { return ''; } - return self::$link_service->generate_preview_post_link( $url, $post ); + $service = new Preview_Url_Resolver_Service( Preview_Parameter_Registry::get_instance() ); + return (string) $service->resolve( $post, $url ); + } + + /** + * @return array + */ + public function get_statuses_for_parent_post_type(): array { + $parent_statuses = $this->post_preview_service->get_parent_post_statuses(); + $post_statuses = $this->post_preview_service->get_post_statuses(); + return array_intersect( $parent_statuses, $post_statuses ); } } diff --git a/plugins/hwp-previews/src/Integration/Faust_Integration.php b/plugins/hwp-previews/src/Integration/Faust_Integration.php index 70c70ecc..83393701 100644 --- a/plugins/hwp-previews/src/Integration/Faust_Integration.php +++ b/plugins/hwp-previews/src/Integration/Faust_Integration.php @@ -4,9 +4,19 @@ namespace HWP\Previews\Integration; -use HWP\Previews\Post\Type\Post_Types_Config_Registry; -use HWP\Previews\Preview\Helper\Settings_Group; - +use HWP\Previews\Admin\Settings\Fields\Settings_Field_Collection; +use HWP\Previews\Preview\Post\Post_Preview_Service; +use HWP\Previews\Preview\Post\Post_Settings_Service; + +/** + * Faust Integration class + * + * This class handles the integration with the Faust plugin for HWP Previews and updates the settings accordingly. + * + * @package HWP\Previews + * + * @since 0.0.1 + */ class Faust_Integration { /** * The key for the admin notice. @@ -18,49 +28,72 @@ class Faust_Integration { /** * Whether Faust is enabled. */ - public static bool $faust_enabled = false; + public bool $faust_enabled = false; /** - * Initialize the hooks for the preview functionality. + * Whether Faust is enabled. */ - public static function init(): void { - self::$faust_enabled = self::is_faust_enabled(); - - self::configure_faust(); - } + public bool $faust_configured = false; /** - * Configure Faust settings and remove conflicting filters. + * The instance of the Faust integration. + * + * @var \HWP\Previews\Integration\Faust_Integration|null */ - public static function configure_faust(): void { - if ( self::$faust_enabled ) { - self::set_default_faust_settings(); + protected static ?Faust_Integration $instance = null; - // Remove FaustWP post preview link filter to avoid conflicts with our custom preview link generation. - remove_filter( 'preview_post_link', 'WPE\FaustWP\Replacement\post_preview_link', 1000 ); + /** + * Faust_Integration constructor. + * + * Initializes the Faust integration if Faust is enabled. + */ + public function __construct() { + $this->faust_enabled = $this->is_faust_enabled(); + $this->configure_faust(); + } - self::display_faust_admin_notice(); + /** + * Initialize the hooks for the preview functionality. + */ + public static function init(): Faust_Integration { + if ( ! isset( self::$instance ) || ! ( is_a( self::$instance, self::class ) ) ) { + self::$instance = new self(); } + + return self::$instance; } /** * Checks if Faust is enabled. */ - public static function is_faust_enabled(): bool { - if ( function_exists( 'is_plugin_active' ) ) { - return is_plugin_active( 'faustwp/faustwp.php' ); + public function is_faust_enabled(): bool { + if ( ! function_exists( 'is_plugin_active' ) ) { + return false; } + return is_plugin_active( 'faustwp/faustwp.php' ); + } + + /** + * Get the Faust enabled status. + */ + public function get_faust_enabled(): bool { + return $this->faust_enabled; + } - return false; + /** + * Get the Faust configured status. + */ + public function get_faust_configured(): bool { + return $this->faust_configured; } /** * Returns the Faust frontend URL from settings or a default value. */ - public static function get_faust_frontend_url(): string { + public function get_faust_frontend_url(): string { $default_value = 'http://localhost:3000'; - if ( self::$faust_enabled && function_exists( '\WPE\FaustWP\Settings\faustwp_get_setting' ) ) { + if ( $this->get_faust_enabled() && function_exists( '\WPE\FaustWP\Settings\faustwp_get_setting' ) ) { $frontend_uri = \WPE\FaustWP\Settings\faustwp_get_setting( 'frontend_uri', '' ); if ( ! empty( $frontend_uri ) ) { @@ -74,53 +107,44 @@ public static function get_faust_frontend_url(): string { /** * Get default preview URL for Faust. */ - public static function get_faust_preview_url(): string { + public function get_faust_preview_url(): string { return self::get_faust_frontend_url() . '/preview?p={ID}&preview=true&previewPathname=p{ID}&typeName={type}'; } /** * Sets default Faust settings if there are no existing settings. */ - public static function set_default_faust_settings(): void { - $settings_group = Settings_Group::get_instance(); - $types_config = apply_filters( - 'hwp_previews_hooks_post_type_config', - Post_Types_Config_Registry::get_post_type_config() - ); + public function set_default_faust_settings(): void { + $settings_helper = new Post_Settings_Service(); + $plugin_settings = $settings_helper->get_settings_values(); - $plugin_settings = $settings_group->get_cached_settings(); - + // If already configured, do not overwrite. if ( ! empty( $plugin_settings ) ) { return; } - $setting_preview_key = $settings_group->get_settings_key_preview_url(); - $setting_enabled_key = $settings_group->get_settings_key_enabled(); + $post_preview_service = new Post_Preview_Service(); + $post_types = $post_preview_service->get_post_types(); - $default_settings = []; + $setting_preview_key = Settings_Field_Collection::PREVIEW_URL_FIELD_ID; + $setting_enabled_key = Settings_Field_Collection::ENABLED_FIELD_ID; - foreach ( $types_config->get_public_post_types() as $key => $label ) { - $default_settings[ $key ] = [ + $default_settings = []; + foreach ( $post_types as $type => $label ) { + $default_settings[ $type ] = [ $setting_enabled_key => true, $setting_preview_key => self::get_faust_preview_url(), ]; } - update_option( HWP_PREVIEWS_SETTINGS_KEY, $default_settings ); - } - - /** - * Dismiss the Faust admin notice. - */ - public static function dismiss_faust_admin_notice(): void { - update_user_meta( get_current_user_id(), self::FAUST_NOTICE_KEY, 1 ); + update_option( Post_Settings_Service::get_option_key(), $default_settings ); } /** * Register admin notice to inform users about Faust integration. */ - public static function register_faust_admin_notice(): void { + public function register_faust_admin_notice(): void { add_action( 'admin_notices', static function (): void { $screen = get_current_screen(); @@ -137,10 +161,10 @@ public static function register_faust_admin_notice(): void { get_faust_enabled() ) { + return; + } + $this->faust_configured = true; + + $this->set_default_faust_settings(); + + // Remove FaustWP post preview link filter to avoid conflicts with our custom preview link generation. + remove_filter( 'preview_post_link', 'WPE\FaustWP\Replacement\post_preview_link', 1000 ); + + $this->display_faust_admin_notice(); } /** * If Faust is enabled, show an admin notice about the migration on the settings page. */ - public static function display_faust_admin_notice(): void { + protected function display_faust_admin_notice(): void { $is_dismissed = get_user_meta( get_current_user_id(), self::FAUST_NOTICE_KEY, true ); // Exit if Faust is not enabled or if the notice has been dismissed. - if ( ! self::$faust_enabled || (bool) $is_dismissed ) { + if ( ! $this->get_faust_enabled() || (bool) $is_dismissed ) { return; } diff --git a/plugins/hwp-previews/src/Plugin.php b/plugins/hwp-previews/src/Plugin.php index 44f56f56..c760ce6f 100644 --- a/plugins/hwp-previews/src/Plugin.php +++ b/plugins/hwp-previews/src/Plugin.php @@ -8,76 +8,76 @@ use HWP\Previews\Hooks\Preview_Hooks; use HWP\Previews\Integration\Faust_Integration; -if ( ! class_exists( Plugin::class ) ) : - +/** + * Plugin class for HWP Previews. + * + * This class serves as the main entry point for the plugin, handling initialization, action and filter hooks. + * + * @package HWP\Previews + */ +final class Plugin { /** - * Plugin class for HWP Previews. - * - * This class serves as the main entry point for the plugin, handling initialization, action and filter hooks. + * The instance of the plugin. * - * @package HWP\Previews + * @var \HWP\Previews\Plugin|null */ - final class Plugin { - /** - * The instance of the plugin. - * - * @var \HWP\Previews\Plugin|null - */ - protected static ?Plugin $instance = null; + protected static ?Plugin $instance = null; - /** - * Constructor - */ - public static function instance(): self { - if ( ! isset( self::$instance ) || ! ( is_a( self::$instance, self::class ) ) ) { - self::$instance = new self(); - self::$instance->setup(); - } - - /** - * Fire off init action. - * - * @param self $instance the instance of the plugin class. - */ - do_action( 'hwp_previews_init', self::$instance ); + /** + * Private constructor to prevent direct instantiation. + */ + protected function __construct() { + } - return self::$instance; + /** + * Constructor + */ + public static function init(): self { + if ( ! isset( self::$instance ) || ! ( is_a( self::$instance, self::class ) ) ) { + self::$instance = new self(); + self::$instance->setup(); } /** - * Initialize the plugin admin, frontend & api functionality. + * Fire off init action. + * + * @param \HWP\Previews\Plugin $instance the instance of the plugin class. */ - public function setup(): void { - if ( is_admin() ) { - Settings_Page::init(); - } + do_action( 'hwp_previews_init', self::$instance ); - Preview_Hooks::init(); - Faust_Integration::init(); - } + return self::$instance; + } - /** - * Throw error on object clone. - * The whole idea of the singleton design pattern is that there is a single object - * therefore, we don't want the object to be cloned. - * - * @codeCoverageIgnore - * - * @return void - */ - public function __clone() { - // Cloning instances of the class is forbidden. - _doing_it_wrong( __FUNCTION__, 'The plugin Plugin class should not be cloned.', '0.0.1' ); - } + /** + * Initialize the plugin admin, frontend & api functionality. + */ + public function setup(): void { + Settings_Page::init(); + Preview_Hooks::init(); + Faust_Integration::init(); + } - /** - * Disable unserializing of the class. - * - * @codeCoverageIgnore - */ - public function __wakeup(): void { - // De-serializing instances of the class is forbidden. - _doing_it_wrong( __FUNCTION__, 'De-serializing instances of the plugin Main class is not allowed.', '0.0.1' ); - } + /** + * Throw error on object clone. + * The whole idea of the singleton design pattern is that there is a single object + * therefore, we don't want the object to be cloned. + * + * @codeCoverageIgnore + * + * @return void + */ + public function __clone() { + // Cloning instances of the class is forbidden. + _doing_it_wrong( __METHOD__, 'The plugin Plugin class should not be cloned.', '0.0.1' ); + } + + /** + * Disable unserializing of the class. + * + * @codeCoverageIgnore + */ + public function __wakeup(): void { + // De-serializing instances of the class is forbidden. + _doing_it_wrong( __METHOD__, 'De-serializing instances of the plugin Main class is not allowed.', '0.0.1' ); } -endif; +} diff --git a/plugins/hwp-previews/src/Post/Parent/Contracts/Post_Parent_Manager_Interface.php b/plugins/hwp-previews/src/Post/Parent/Contracts/Post_Parent_Manager_Interface.php deleted file mode 100644 index 31557ee1..00000000 --- a/plugins/hwp-previews/src/Post/Parent/Contracts/Post_Parent_Manager_Interface.php +++ /dev/null @@ -1,16 +0,0 @@ - - */ - public function get_post_statuses_as_parent( string $post_type ): array; -} diff --git a/plugins/hwp-previews/src/Post/Parent/Post_Parent_Manager.php b/plugins/hwp-previews/src/Post/Parent/Post_Parent_Manager.php deleted file mode 100644 index 93d95134..00000000 --- a/plugins/hwp-previews/src/Post/Parent/Post_Parent_Manager.php +++ /dev/null @@ -1,66 +0,0 @@ - - */ - public const POST_STATUSES = [ 'publish', 'future', 'draft', 'pending', 'private' ]; - - /** - * Post types configuration. - * - * @var \HWP\Previews\Post\Type\Contracts\Post_Types_Config_Interface - */ - private Post_Types_Config_Interface $post_types; - - /** - * Post statuses configuration. - * - * @var \HWP\Previews\Post\Status\Contracts\Post_Statuses_Config_Interface - */ - private Post_Statuses_Config_Interface $post_statuses; - - /** - * Post_Parent_Manager constructor. - * - * @param \HWP\Previews\Post\Type\Contracts\Post_Types_Config_Interface $post_types Post types configuration. - * @param \HWP\Previews\Post\Status\Contracts\Post_Statuses_Config_Interface $post_statuses Post statuses configuration. - */ - public function __construct( Post_Types_Config_Interface $post_types, Post_Statuses_Config_Interface $post_statuses ) { - $this->post_types = $post_types; - $this->post_statuses = $post_statuses; - } - - /** - * Get the post statuses that can be used as parent for a given post type. - * - * @param string $post_type Post Type slug. - * - * @return array - */ - public function get_post_statuses_as_parent( string $post_type ): array { - if ( - ! $this->post_types->is_post_type_applicable( $post_type ) || - ! $this->post_types->is_hierarchical( $post_type ) - ) { - return []; - } - - return array_intersect( self::POST_STATUSES, $this->post_statuses->get_post_statuses() ); - } -} diff --git a/plugins/hwp-previews/src/Post/Slug/Contracts/Post_Slug_Repository_Interface.php b/plugins/hwp-previews/src/Post/Slug/Contracts/Post_Slug_Repository_Interface.php deleted file mode 100644 index 3c056635..00000000 --- a/plugins/hwp-previews/src/Post/Slug/Contracts/Post_Slug_Repository_Interface.php +++ /dev/null @@ -1,16 +0,0 @@ -get_var( // phpcs:ignore WordPress.DB - $wpdb->prepare( - // @phpstan-ignore-next-line - "SELECT post_name FROM {$wpdb->posts} WHERE post_name = %s AND post_type = %s AND ID != %d LIMIT 1", - $slug, - $post_type, - $post_id - ) - ); - } -} diff --git a/plugins/hwp-previews/src/Post/Status/Contracts/Post_Statuses_Config_Interface.php b/plugins/hwp-previews/src/Post/Status/Contracts/Post_Statuses_Config_Interface.php deleted file mode 100644 index 1af9b622..00000000 --- a/plugins/hwp-previews/src/Post/Status/Contracts/Post_Statuses_Config_Interface.php +++ /dev/null @@ -1,28 +0,0 @@ - $post_statuses The post statuses to set. - */ - public function set_post_statuses( array $post_statuses ): self; - - /** - * Get the post statuses that are applicable for the plugin. - * - * @return array - */ - public function get_post_statuses(): array; - - /** - * Check if a given post status is applicable for the plugin. - * - * @param string $post_status Post status slug. - */ - public function is_post_status_applicable( string $post_status ): bool; -} diff --git a/plugins/hwp-previews/src/Post/Status/Post_Statuses_Config.php b/plugins/hwp-previews/src/Post/Status/Post_Statuses_Config.php deleted file mode 100644 index a5a25f58..00000000 --- a/plugins/hwp-previews/src/Post/Status/Post_Statuses_Config.php +++ /dev/null @@ -1,50 +0,0 @@ - - */ - private array $post_statuses = []; - - /** - * Sets the post statuses that are applicable for the plugin. - * - * @param array $post_statuses Post statuses that are applicable for the plugin. - * - * @return $this - */ - public function set_post_statuses( array $post_statuses ): self { - $this->post_statuses = $post_statuses; - - return $this; - } - - /** - * Get the post statuses that are applicable for the plugin. - * - * @return array Post statuses. - */ - public function get_post_statuses(): array { - return $this->post_statuses; - } - - /** - * Verifies if the post status is applicable according to the configuration. - * - * @param string $post_status Post status to check. - */ - public function is_post_status_applicable( string $post_status ): bool { - return in_array( $post_status, $this->post_statuses, true ); - } -} diff --git a/plugins/hwp-previews/src/Post/Type/Contracts/Post_Type_Inspector_Interface.php b/plugins/hwp-previews/src/Post/Type/Contracts/Post_Type_Inspector_Interface.php deleted file mode 100644 index eb21865e..00000000 --- a/plugins/hwp-previews/src/Post/Type/Contracts/Post_Type_Inspector_Interface.php +++ /dev/null @@ -1,23 +0,0 @@ - $post_types The post type to check. - */ - public function set_post_types( array $post_types ): self; - - /** - * Get the post types that are applicable for previews. - * - * @return array Post types that are applicable for previews. - */ - public function get_post_types(): array; - - /** - * Check if a post type is applicable for previews. - * - * @param string $post_type The post type to check. - */ - public function is_post_type_applicable( string $post_type ): bool; - - /** - * Check if a post type is hierarchical. - * - * @param string $post_type The post type. - */ - public function is_hierarchical( string $post_type ): bool; - - /** - * Check if a post type supports Gutenberg. - * - * @param string $post_type Post Type slug. - */ - public function gutenberg_editor_enabled( string $post_type ): bool; - - /** - * Gets all publicly available post types as key value array, where key is a post type slug and value is a label. - * - * @return array - */ - public function get_public_post_types(): array; -} diff --git a/plugins/hwp-previews/src/Post/Type/Post_Type_Inspector.php b/plugins/hwp-previews/src/Post/Type/Post_Type_Inspector.php deleted file mode 100644 index 422210f7..00000000 --- a/plugins/hwp-previews/src/Post/Type/Post_Type_Inspector.php +++ /dev/null @@ -1,49 +0,0 @@ -show_in_rest ) || - empty( $post_type->supports ) || - ! is_array( $post_type->supports ) || - ! in_array( 'editor', $post_type->supports, true ) - ) { - return false; - } - - return $post_type->show_in_rest; - } - - /** - * Checks if the post type is supported by Classic Editor. - * - * @param string $post_type Post Type slug. - */ - public function is_classic_editor_forced( string $post_type ): bool { - if ( - ! function_exists( 'is_plugin_active' ) || - ! is_plugin_active( 'classic-editor/classic-editor.php' ) - ) { - return false; - } - - // If this post type is listed in Classic Editor settings, Gutenberg is disabled. - $settings = (array) get_option( 'classic-editor-settings', [] ); - - return ! empty( $settings['post_types'] ) && - is_array( $settings['post_types'] ) && - in_array( $post_type, $settings['post_types'], true ); - } -} diff --git a/plugins/hwp-previews/src/Post/Type/Post_Types_Config.php b/plugins/hwp-previews/src/Post/Type/Post_Types_Config.php deleted file mode 100644 index 33ad2fc0..00000000 --- a/plugins/hwp-previews/src/Post/Type/Post_Types_Config.php +++ /dev/null @@ -1,109 +0,0 @@ - - */ - private array $post_types = []; - - /** - * Post type inspector. - * - * @var \HWP\Previews\Post\Type\Contracts\Post_Type_Inspector_Interface - */ - private Post_Type_Inspector_Interface $inspector; - - /** - * Class constructor. - * - * @param \HWP\Previews\Post\Type\Contracts\Post_Type_Inspector_Interface $inspector Post Type inspector. - */ - public function __construct( Post_Type_Inspector_Interface $inspector ) { - $this->inspector = $inspector; - } - - /** - * Sets the post types that are applicable for preview links. - * - * @param array $post_types Post types that are applicable for preview links. - * - * @return $this - */ - public function set_post_types( array $post_types ): self { - $this->post_types = $post_types; - - return $this; - } - - /** - * Get the post types that are applicable for preview links. - * - * @return array - */ - public function get_post_types(): array { - return $this->post_types; - } - - /** - * Check if the post type is applicable for preview links. - * - * @param string $post_type Post Type slug. - */ - public function is_post_type_applicable( string $post_type ): bool { - return in_array( $post_type, $this->post_types, true ) && post_type_exists( $post_type ); - } - - /** - * Check if the post type is hierarchical. - * - * @param string $post_type Post Type slug. - */ - public function is_hierarchical( string $post_type ): bool { - return is_post_type_hierarchical( $post_type ); - } - - /** - * Check if the post type supports Gutenberg editor and if the classic editor is not being forced. - * - * @param string $post_type Post Type slug. - */ - public function gutenberg_editor_enabled( string $post_type ): bool { - $post_type_object = get_post_type_object( $post_type ); - if ( ! $post_type_object instanceof WP_Post_Type ) { - return false; - } - - return $this->inspector->is_gutenberg_supported( $post_type_object ) && - ! $this->inspector->is_classic_editor_forced( $post_type ); - } - - /** - * Gets all publicly available post types as key value array, where key is a post type slug and value is a label. - * - * @return array - */ - public function get_public_post_types(): array { - $post_types = get_post_types( [ 'public' => true ], 'objects' ); - - $result = []; - - foreach ( $post_types as $post_type ) { - $result[ $post_type->name ] = $post_type->label; - } - - return apply_filters( 'hwp_previews_filter_available_post_types', $result ); - } -} diff --git a/plugins/hwp-previews/src/Post/Type/Post_Types_Config_Registry.php b/plugins/hwp-previews/src/Post/Type/Post_Types_Config_Registry.php deleted file mode 100644 index c5e5d2c7..00000000 --- a/plugins/hwp-previews/src/Post/Type/Post_Types_Config_Registry.php +++ /dev/null @@ -1,38 +0,0 @@ -set_post_types( Settings_Helper::get_instance()->post_types_enabled() ); - $instance = apply_filters( 'hwp_previews_get_post_types_config', $instance ); - self::$instance = $instance; - - return $instance; - } -} diff --git a/plugins/hwp-previews/src/Preview/Helper/Settings_Group.php b/plugins/hwp-previews/src/Preview/Helper/Settings_Group.php deleted file mode 100644 index 6e9b2e15..00000000 --- a/plugins/hwp-previews/src/Preview/Helper/Settings_Group.php +++ /dev/null @@ -1,176 +0,0 @@ - - */ - protected array $settings_config = []; - - /** - * The settings helper instance. - * - * @var \HWP\Previews\Preview\Helper\Settings_Group|null - */ - protected static $instance = null; - - /** - * Class initializer. - */ - public function __construct() { - $this->option_key = $this->get_option_key(); - $this->settings_group = $this->get_settings_group(); - $this->settings_config = $this->get_settings_config(); - $this->set_cache_group(); - } - - /** - * Get Singleton instance of the Settings_Group class. - */ - public static function get_instance(): self { - - $instance = self::$instance; - - if ( $instance instanceof self ) { - return $instance; - } - - self::$instance = new self(); - - return self::$instance; - } - - /** - * Setting key for the enabled status. - */ - public function get_settings_key_enabled(): string { - return 'enabled'; - } - - /** - * Setting key for post-statuses as parent. - */ - public function get_settings_key_post_statuses_as_parent(): string { - return 'post_statuses_as_parent'; - } - - /** - * Setting key for the preview URL. - */ - public function get_settings_key_preview_url(): string { - return 'preview_url'; - } - - /** - * Setting key for is the preview in an iframe. - */ - public function get_settings_key_in_iframe(): string { - return 'in_iframe'; - } - - /** - * Gets the settings configuration for the settings group. - * - * @return array The settings configuration. - */ - public function get_settings_config(): array { - return apply_filters( 'hwp_previews_settings_group_settings_config', [ - $this->get_settings_key_enabled() => 'bool', - $this->get_settings_key_post_statuses_as_parent() => 'bool', - $this->get_settings_key_preview_url() => 'string', - $this->get_settings_key_in_iframe() => 'bool', - ] ); - } - - /** - * Gets settings from the cache or database. - * - * @return array - */ - public function get_cached_settings(): array { - - $value = wp_cache_get( $this->option_key, $this->settings_group ); - if ( is_array( $value ) ) { - return $value; - } - - $value = (array) get_option( $this->option_key, [] ); - wp_cache_set( $this->option_key, $value, $this->settings_group ); - - return $value; - } - - /** - * Gets a boolean value for a specific setting in a post type. - * - * @param string $name The setting name. - * @param string $post_type The post type slug. - * @param bool $default_value The default value to return if the setting is not found. - */ - public function get_post_type_boolean_value( string $name, string $post_type, bool $default_value = false ): bool { - $settings = $this->get_cached_settings(); - $type = $this->settings_config[ $name ] ?? null; - if ( 'bool' === $type && isset( $settings[ $post_type ][ $name ] ) ) { - return (bool) $settings[ $post_type ][ $name ]; - } - - return $default_value; - } - - /** - * Gets a string value for a specific setting in a post type. - * - * @param string $name The setting name. - * @param string $post_type The post type slug. - * @param string $default_value The default value to return if the setting is not found. - */ - public function get_post_type_string_value( string $name, string $post_type, string $default_value = '' ): string { - $settings = $this->get_cached_settings(); - $type = $this->settings_config[ $name ] ?? null; - if ( 'string' === $type && isset( $settings[ $post_type ][ $name ] ) ) { - return (string) $settings[ $post_type ][ $name ]; - } - - return $default_value; - } - - /** - * Gets the option key for the settings group. - */ - protected function get_option_key(): string { - return apply_filters( 'hwp_previews_settings_group_option_key', HWP_PREVIEWS_SETTINGS_KEY ); - } - - /** - * Gets the settings group name. - */ - protected function get_settings_group(): string { - return apply_filters( 'hwp_previews_settings_group_settings_group', HWP_PREVIEWS_SETTINGS_GROUP ); - } - - /** - * Sets the cache group for the settings. - */ - protected function set_cache_group(): void { - $groups = apply_filters( 'hwp_previews_settings_group_cache_groups', [ $this->settings_group ] ); - wp_cache_add_non_persistent_groups( $groups ); - } -} diff --git a/plugins/hwp-previews/src/Preview/Helper/Settings_Helper.php b/plugins/hwp-previews/src/Preview/Helper/Settings_Helper.php deleted file mode 100644 index b0d15590..00000000 --- a/plugins/hwp-previews/src/Preview/Helper/Settings_Helper.php +++ /dev/null @@ -1,118 +0,0 @@ -settings_group = $settings_group; - } - - /** - * Get an instance of the Settings_Helper class. - * - * @return \HWP\Previews\Preview\Helper\Settings_Helper - */ - public static function get_instance(): self { - - $instance = self::$instance; - - if ( $instance instanceof self ) { - return $instance; - } - - self::$instance = new self( Settings_Group::get_instance() ); - - return self::$instance; - } - - /** - * Get the settings group. - * - * @return array - */ - public function get_settings_config(): array { - // @TODO: Remove this method when the settings group is no longer needed. - return $this->settings_group->get_settings_config(); - } - - /** - * Get all post types that are enabled in the settings. - * - * @param array $default_value Default post types to return if none are enabled. - * - * @return array - */ - public function post_types_enabled( array $default_value = [] ): array { - $settings = $this->settings_group->get_cached_settings(); - - $enabled_key = $this->settings_group->get_settings_key_enabled(); - - $enabled_post_types = []; - foreach ( $settings as $key => $item ) { - if ( (bool) ( $item[ $enabled_key ] ?? false ) ) { - $enabled_post_types[] = $key; - } - } - - if ( ! empty( $enabled_post_types ) ) { - return $enabled_post_types; - } - return $default_value; - } - - /** - * Get Post Statuses as Parent setting value for the given post type. - * - * @param string $post_type The post type to get the setting for. - * @param bool $default_value The default value to return if the setting is not set. - */ - public function post_statuses_as_parent( string $post_type, bool $default_value = false ): bool { - $key = $this->settings_group->get_settings_key_post_statuses_as_parent(); - - return $this->settings_group->get_post_type_boolean_value( $key, $post_type, $default_value ); - } - - /** - * Show In iframe value for the given post type. - * - * @param string $post_type The post type to get the setting for. - * @param bool $default_value The default value to return if the setting is not set. - */ - public function in_iframe( string $post_type, bool $default_value = false ): bool { - - $key = $this->settings_group->get_settings_key_in_iframe(); - - return $this->settings_group->get_post_type_boolean_value( $key, $post_type, $default_value ); - } - - /** - * URL template setting value for the given post type. - * - * @param string $post_type The post type to get the setting for. - * @param string $default_value The default value to return if the setting is not set. - */ - public function url_template( string $post_type, string $default_value = '' ): string { - $key = $this->settings_group->get_settings_key_preview_url(); - - return $this->settings_group->get_post_type_string_value( $key, $post_type, $default_value ); - } -} diff --git a/plugins/hwp-previews/src/Preview/Link/Preview_Link_Service.php b/plugins/hwp-previews/src/Preview/Link/Preview_Link_Service.php deleted file mode 100644 index db1cbfd2..00000000 --- a/plugins/hwp-previews/src/Preview/Link/Preview_Link_Service.php +++ /dev/null @@ -1,70 +0,0 @@ -types = $types; - $this->statuses = $statuses; - $this->resolver = $resolver; - } - - /** - * Generate a preview post link. - * - * @param string $preview_url_template Preview URL template. - * @param \WP_Post $post The post object. - */ - public function generate_preview_post_link( string $preview_url_template, WP_Post $post ): string { - if ( - empty( $preview_url_template ) || - ! $this->types->is_post_type_applicable( $post->post_type ) || - ! $this->statuses->is_post_status_applicable( $post->post_status ) - ) { - return ''; - } - - return $this->resolver->resolve_placeholders( $preview_url_template, $post ); - } -} diff --git a/plugins/hwp-previews/src/Preview/Parameter/Contracts/Preview_Parameter_Builder_Interface.php b/plugins/hwp-previews/src/Preview/Parameter/Contracts/Preview_Parameter_Builder_Interface.php deleted file mode 100644 index 6cc37ccf..00000000 --- a/plugins/hwp-previews/src/Preview/Parameter/Contracts/Preview_Parameter_Builder_Interface.php +++ /dev/null @@ -1,25 +0,0 @@ - - */ - public function build_preview_args( WP_Post $post, string $page_uri, string $token ): array; -} diff --git a/plugins/hwp-previews/src/Preview/Parameter/Contracts/Preview_Parameter_Interface.php b/plugins/hwp-previews/src/Preview/Parameter/Contracts/Preview_Parameter_Interface.php deleted file mode 100644 index 488f6ff3..00000000 --- a/plugins/hwp-previews/src/Preview/Parameter/Contracts/Preview_Parameter_Interface.php +++ /dev/null @@ -1,33 +0,0 @@ - + * @var array<\HWP\Previews\Preview\Parameter\Preview_Parameter> */ private array $parameters = []; @@ -72,9 +76,9 @@ public static function add_initial_parameters( self $instance ): self { /** * Register a parameter. * - * @param \HWP\Previews\Preview\Parameter\Contracts\Preview_Parameter_Interface $parameter The parameter object. + * @param \HWP\Previews\Preview\Parameter\Preview_Parameter $parameter The parameter object. */ - public function register( Preview_Parameter_Interface $parameter ): self { + public function register( Preview_Parameter $parameter ): self { $this->parameters[ $parameter->get_name() ] = $parameter; return $this; @@ -93,15 +97,6 @@ public function unregister( string $name ): self { return $this; } - /** - * Get all registered parameters. - * - * @return array - */ - public function get_all(): array { - return $this->parameters; - } - /** * Get all registered parameters as an array of their names and descriptions. * @@ -121,7 +116,7 @@ public function get_descriptions(): array { * * @param string $name The parameter name. */ - public function get( string $name ): ?Preview_Parameter_Interface { + public function get( string $name ): ?Preview_Parameter { return $this->parameters[ $name ] ?? null; } } diff --git a/plugins/hwp-previews/src/Preview/Post/Post_Editor_Service.php b/plugins/hwp-previews/src/Preview/Post/Post_Editor_Service.php new file mode 100644 index 00000000..ffbb81aa --- /dev/null +++ b/plugins/hwp-previews/src/Preview/Post/Post_Editor_Service.php @@ -0,0 +1,70 @@ +is_gutenberg_supported( $post_type_object ) && + ! $this->is_classic_editor_forced( $post_type ); + } + + /** + * Checks if the post-type supports Gutenberg. + * + * @param \WP_Post_Type $post_type Post Type object. + */ + public function is_gutenberg_supported( WP_Post_Type $post_type ): bool { + + if ( empty( $post_type->show_in_rest ) ) { + return false; + } + + if ( ! post_type_supports( $post_type->name, 'editor' ) ) { + return false; + } + + return $post_type->show_in_rest; + } + + /** + * Checks if the post-type is supported by Classic Editor. + */ + public function is_classic_editor_forced(string $post_type): bool { + if ( + ! function_exists( 'is_plugin_active' ) || + ! is_plugin_active( 'classic-editor/classic-editor.php' ) + ) { + return false; + } + + // If this post type is listed in Classic Editor settings, Gutenberg is disabled. + $settings = (array) get_option( 'classic-editor-settings', [] ); + + return ! empty( $settings['post_types'] ) && + is_array( $settings['post_types'] ) && + in_array( $post_type, $settings['post_types'], true ); + } +} diff --git a/plugins/hwp-previews/src/Preview/Post/Post_Preview_Service.php b/plugins/hwp-previews/src/Preview/Post/Post_Preview_Service.php new file mode 100644 index 00000000..711adec2 --- /dev/null +++ b/plugins/hwp-previews/src/Preview/Post/Post_Preview_Service.php @@ -0,0 +1,130 @@ + + */ + protected $post_types = []; + + /** + * The allowed post-statuses for previews. + * + * @var array + */ + protected $post_statuses = []; + + /** + * The allowed post-statuses for parent post-types (hierarchical). + * + * @var array + */ + protected $parent_post_statuses = []; + + /** + * Constructor for the Post_Preview_Service class. + * + * Initializes the allowed post-types and statuses for previews. + */ + public function __construct() { + $this->set_post_types(); + $this->set_post_statuses(); + $this->set_post_parent_statuses(); + } + + /** + * @return array + */ + public function get_allowed_post_types(): array { + return $this->post_types; + } + + /** + * Get the post-statuses. + * + * @return array + */ + public function get_post_statuses(): array { + return $this->post_statuses; + } + + /** + * Get the post-types. + * + * @return array + */ + public function get_post_types(): array { + return $this->post_types; + } + + /** + * Get the parent post-statuses + * + * @return array + */ + public function get_parent_post_statuses(): array { + return $this->parent_post_statuses; + } + + /** + * Sets the allowed post-types for previews. + */ + protected function set_post_types(): void { + $post_types = get_post_types( [ 'public' => true ], 'objects' ); + + $result = []; + + foreach ( $post_types as $post_type ) { + $result[ $post_type->name ] = $post_type->label; + } + + $this->post_types = apply_filters( 'hwp_previews_filter_available_post_types', $result ); + } + + /** + * Sets the allowed post-statuses for previews. + */ + protected function set_post_statuses(): void { + + $post_statuses = [ + 'publish', + 'future', + 'draft', + 'pending', + 'private', + 'auto-draft', + ]; + + $this->post_statuses = apply_filters( 'hwp_previews_filter_available_post_statuses', $post_statuses ); + } + + /** + * Sets the allowed post-statuses for parent post-types (hierarchical). + */ + protected function set_post_parent_statuses(): void { + + $post_statuses = [ + 'publish', + 'future', + 'draft', + 'pending', + 'private', + ]; + + $this->parent_post_statuses = apply_filters( 'hwp_previews_filter_available_parent_post_statuses', $post_statuses ); + } +} diff --git a/plugins/hwp-previews/src/Preview/Post/Post_Settings_Service.php b/plugins/hwp-previews/src/Preview/Post/Post_Settings_Service.php new file mode 100644 index 00000000..7b7e3453 --- /dev/null +++ b/plugins/hwp-previews/src/Preview/Post/Post_Settings_Service.php @@ -0,0 +1,81 @@ + + */ + protected $settings_values = []; + + /** + * Initialize the settings service. + */ + public function __construct() { + $this->setup(); + } + + /** + * Get the settings values. + * + * @return array + */ + public function get_settings_values(): array { + return $this->settings_values; + } + + /** + * @param string $post_type + * + * @return array|null + */ + public function get_post_type_config( string $post_type ): ?array { + return $this->settings_values[ $post_type ] ?? null; + } + + /** + * The option key for the settings group. + */ + public static function get_option_key(): string { + return (string) apply_filters( 'hwp_previews_settings_group_option_key', HWP_PREVIEWS_SETTINGS_KEY ); + } + + /** + * The settings group for the options. + */ + public static function get_settings_group(): string { + return (string) apply_filters( 'hwp_previews_settings_group_settings_group', HWP_PREVIEWS_SETTINGS_GROUP ); + } + + /** + * Set up the settings values it by retrieving them from the database or cache. + * This method is called in the constructor to ensure settings are available. + */ + protected function setup(): void { + $option_key = self::get_option_key(); + $settings_group = self::get_settings_group(); + + $value = wp_cache_get( $option_key, $settings_group ); + if ( is_array( $value ) ) { + $this->settings_values = $value; + + return; + } + + $this->settings_values = (array) get_option( $option_key, [] ); + wp_cache_set( $option_key, $value, $settings_group ); + } +} diff --git a/plugins/hwp-previews/src/Preview/Post/Post_Type_Service.php b/plugins/hwp-previews/src/Preview/Post/Post_Type_Service.php new file mode 100644 index 00000000..4a2934b9 --- /dev/null +++ b/plugins/hwp-previews/src/Preview/Post/Post_Type_Service.php @@ -0,0 +1,132 @@ +post = $post; + $this->post_preview_service = $post_preview_service; + $this->post_settings_service = $post_settings_service; + } + + /** + * Checks if the post-type is allowed for previews. + */ + public function is_allowed_for_previews(): bool { + return $this->is_enabled() && $this->is_allowed_post_type() && $this->is_allowed_post_status(); + } + + /** + * Checks if the post-type is enabled for previews. + */ + public function is_enabled(): bool { + $config = $this->post_settings_service->get_post_type_config( $this->post->post_type ); + if ( ! is_array( $config ) ) { + return false; + } + + $field_id = Settings_Field_Collection::ENABLED_FIELD_ID; + + if ( ! isset( $config[ $field_id ] ) ) { + return false; + } + + return (bool) $config[ $field_id ]; + } + + /** + * Checks if the post-type is allowed for previews. + */ + public function is_allowed_post_type(): bool { + $post_type = $this->post->post_type; + $post_types = $this->post_preview_service->get_post_types(); + + return array_key_exists( $post_type, $post_types ); + } + + /** + * Checks if the post-status is allowed for previews. + */ + public function is_allowed_post_status(): bool { + $post_status = $this->post->post_status; + $post_statuses = $this->post_preview_service->get_post_statuses(); + + return in_array( $post_status, $post_statuses, true ); + } + + /** + * Checks if the post-type is allowed for iframe previews. + */ + public function is_iframe(): bool { + $config = $this->post_settings_service->get_post_type_config( $this->post->post_type ); + if ( ! is_array( $config ) ) { + return false; + } + + $field_id = Settings_Field_Collection::IN_IFRAME_FIELD_ID; + if ( ! isset( $config[ $field_id ] ) ) { + return false; + } + + return (bool) $config[ $field_id ]; + } + + /** + * Retrieves the settings URL for the given post. + * + * @return string|null The settings URL or null if not found. + */ + public function get_preview_url(): ?string { + $config = $this->post_settings_service->get_post_type_config( $this->post->post_type ); + if ( ! is_array( $config ) ) { + return null; + } + + $field_id = Settings_Field_Collection::PREVIEW_URL_FIELD_ID; + if ( ! isset( $config[ $field_id ] ) ) { + return null; + } + + return (string) $config[ $field_id ]; + } +} diff --git a/plugins/hwp-previews/src/Preview/Template/Contracts/Preview_Template_Resolver_Interface.php b/plugins/hwp-previews/src/Preview/Template/Contracts/Preview_Template_Resolver_Interface.php deleted file mode 100644 index 24973aec..00000000 --- a/plugins/hwp-previews/src/Preview/Template/Contracts/Preview_Template_Resolver_Interface.php +++ /dev/null @@ -1,17 +0,0 @@ -types = $types; - $this->statuses = $statuses; - } - - /** - * Resolves the template path for the preview. - * - * @param \WP_Post $post The post object. - * @param string $template_path The template path. - * - * @return string The resolved template path. - */ - public function resolve_template_path( WP_Post $post, string $template_path ): string { - if ( - empty( $template_path ) || - ! $this->types->is_post_type_applicable( $post->post_type ) || - ! $this->statuses->is_post_status_applicable( $post->post_status ) || - ! is_preview() - ) { - return ''; - } - - return file_exists( $template_path ) ? $template_path : ''; - } -} diff --git a/plugins/hwp-previews/src/Preview/Template/Template_Resolver_Service.php b/plugins/hwp-previews/src/Preview/Template/Template_Resolver_Service.php new file mode 100644 index 00000000..c266af4c --- /dev/null +++ b/plugins/hwp-previews/src/Preview/Template/Template_Resolver_Service.php @@ -0,0 +1,52 @@ +registry = $registry; + public function __construct(Preview_Parameter_Registry $parameter_registry ) { + $this->parameter_registry = $parameter_registry; } /** - * Replace all {PLACEHOLDER} tokens in template string with urlencoded string values from callbacks. + * Resolves the parameters in the given URL using the post data. * - * @param string $template The string containing {KEY} placeholders. - * @param \WP_Post $post The post object to resolve the tokens against. + * @param \WP_Post $post The post object. + * @param string $url The URL containing placeholders to resolve. + * + * @return string|null The URL with resolved parameters or null if not found. */ - public function resolve_placeholders(string $template, WP_Post $post ): string { + public function resolve( WP_Post $post, string $url ): ?string { return (string) preg_replace_callback( self::PLACEHOLDER_REGEX, fn(array $matches): string => rawurlencode( $this->resolve_token( $matches[1], $post ) ), - $template + $url ); } @@ -59,9 +61,9 @@ public function resolve_placeholders(string $template, WP_Post $post ): string { * @param string $key The token key without braces. * @param \WP_Post $post Post object to resolve the token against. */ - private function resolve_token( string $key, WP_Post $post ): string { - $parameter = $this->registry->get( $key ); - if ( ! $parameter instanceof Preview_Parameter_Interface ) { + public function resolve_token( string $key, WP_Post $post ): string { + $parameter = $this->parameter_registry->get( $key ); + if ( ! $parameter instanceof Preview_Parameter ) { return self::PLACEHOLDER_NOT_FOUND; } diff --git a/plugins/hwp-previews/src/Admin/Settings/Templates/settings-page-main.php b/plugins/hwp-previews/src/Templates/admin.php similarity index 77% rename from plugins/hwp-previews/src/Admin/Settings/Templates/settings-page-main.php rename to plugins/hwp-previews/src/Templates/admin.php index f190b7bc..5c07eb96 100644 --- a/plugins/hwp-previews/src/Admin/Settings/Templates/settings-page-main.php +++ b/plugins/hwp-previews/src/Templates/admin.php @@ -1,13 +1,22 @@ @@ -32,7 +41,7 @@
@@ -61,14 +70,14 @@

- +
  • - + get_faust_enabled() ) : ?>
  • -
  • +

HWP Toolkit on GitHub

diff --git a/plugins/hwp-previews/src/Admin/Settings/Templates/hwp-preview.php b/plugins/hwp-previews/src/Templates/iframe.php similarity index 81% rename from plugins/hwp-previews/src/Admin/Settings/Templates/hwp-preview.php rename to plugins/hwp-previews/src/Templates/iframe.php index 874402be..242d93d1 100644 --- a/plugins/hwp-previews/src/Admin/Settings/Templates/hwp-preview.php +++ b/plugins/hwp-previews/src/Templates/iframe.php @@ -5,14 +5,13 @@ * This template will be used for all post/page previews * * @package HWP\Previews + * + * @since 0.0.1 */ declare(strict_types=1); -use HWP\Previews\Preview\Template\Preview_Template_Resolver; - -$hwp_previews_url_template = (string) get_query_var( Preview_Template_Resolver::HWP_PREVIEWS_IFRAME_PREVIEW_URL ); - +$hwp_previews_url_template = \HWP\Previews\Preview\Template\Template_Resolver_Service::get_query_variable(); ?> diff --git a/plugins/hwp-previews/tests/acceptance.suite.dist.yml b/plugins/hwp-previews/tests/acceptance.suite.dist.yml deleted file mode 100644 index acda1956..00000000 --- a/plugins/hwp-previews/tests/acceptance.suite.dist.yml +++ /dev/null @@ -1,23 +0,0 @@ -# Codeception Test Suite Configuration -# -# Suite for acceptance tests. -# Perform tests in browser using the WPWebDriver or WPBrowser. -# Use WPDb to set up your initial database fixture. -# If you need both WPWebDriver and WPBrowser tests - create a separate suite. - -actor: AcceptanceTester -modules: - enabled: - - Asserts - - lucatume\WPBrowser\Module\WPBrowser - - lucatume\WPBrowser\Module\WPDb - - lucatume\WPBrowser\Module\WPFilesystem - - lucatume\WPBrowser\Module\WPLoader - - REST - config: - lucatume\WPBrowser\Module\WPLoader: - loadOnly: true - plugins: - - hwp-previews/hwp-previews.php - activatePlugins: - - hwp-previews/hwp-previews.php diff --git a/plugins/hwp-previews/tests/acceptance/.gitkeep b/plugins/hwp-previews/tests/acceptance/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/plugins/hwp-previews/tests/bootstrap.php b/plugins/hwp-previews/tests/bootstrap.php index dfc6629d..a8aff209 100644 --- a/plugins/hwp-previews/tests/bootstrap.php +++ b/plugins/hwp-previews/tests/bootstrap.php @@ -4,3 +4,18 @@ * * @package Tests\HWP\Previews */ + +// Ensure proper autoloading +if (file_exists(__DIR__ . '/../../vendor/autoload.php')) { + require_once __DIR__ . '/../../vendor/autoload.php'; +} + +// Load the main plugin file to ensure all classes are available +if (file_exists(__DIR__ . '/../../hwp-previews.php')) { + require_once __DIR__ . '/../../hwp-previews.php'; +} + +// Load access functions if they exist +if (file_exists(__DIR__ . '/../../access-functions.php')) { + require_once __DIR__ . '/../../access-functions.php'; +} diff --git a/plugins/hwp-previews/tests/e2e/specs/settings.spec.js b/plugins/hwp-previews/tests/e2e/specs/settings.spec.js index b97cc778..497180c8 100644 --- a/plugins/hwp-previews/tests/e2e/specs/settings.spec.js +++ b/plugins/hwp-previews/tests/e2e/specs/settings.spec.js @@ -132,4 +132,17 @@ test.describe("HWP Previews Admin Settings", () => { expect(inputValue).toContain(buttonText.trim()); } }); + + test(`URL input rejects an incorrect URL value`, async ({ page }) => { + const urlInput = page.locator("input.hwp-previews-url"); + const incorrectUrlValue = "this is not a valid url"; + + await urlInput.fill(incorrectUrlValue); + await page.getByRole("button", { name: "Save Changes" }).click(); + + const validationMessage = await urlInput.evaluate( + (element) => element.validationMessage, + ); + expect(validationMessage).not.toBe(""); + }); }); diff --git a/plugins/hwp-previews/tests/functional.suite.dist.yml b/plugins/hwp-previews/tests/functional.suite.dist.yml deleted file mode 100644 index b27e03a2..00000000 --- a/plugins/hwp-previews/tests/functional.suite.dist.yml +++ /dev/null @@ -1,20 +0,0 @@ -# Codeception Test Suite Configuration -# -# Suite for functional tests -# Emulate web requests and make WordPress process them - -actor: FunctionalTester -modules: - enabled: - - Asserts - - lucatume\WPBrowser\Module\WPBrowser - - lucatume\WPBrowser\Module\WPDb - - lucatume\WPBrowser\Module\WPLoader - - REST - config: - lucatume\WPBrowser\Module\WPLoader: - loadOnly: true - plugins: - - hwp-previews/hwp-previews.php - activatePlugins: - - hwp-previews/hwp-previews.php diff --git a/plugins/hwp-previews/tests/functional/.gitkeep b/plugins/hwp-previews/tests/functional/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/plugins/hwp-previews/tests/unit.suite.dist.yml b/plugins/hwp-previews/tests/unit.suite.dist.yml deleted file mode 100644 index 07a7166d..00000000 --- a/plugins/hwp-previews/tests/unit.suite.dist.yml +++ /dev/null @@ -1,8 +0,0 @@ -# Codeception Test Suite Configuration -# -# Suite for unit (internal) tests. - -class_name: UnitTester -modules: - enabled: - - Asserts \ No newline at end of file diff --git a/plugins/hwp-previews/tests/wpunit.suite.dist.yml b/plugins/hwp-previews/tests/wpunit.suite.dist.yml index 5bcc7311..b0a5969b 100644 --- a/plugins/hwp-previews/tests/wpunit.suite.dist.yml +++ b/plugins/hwp-previews/tests/wpunit.suite.dist.yml @@ -5,4 +5,8 @@ actor: WpunitTester modules: enabled: - lucatume\WPBrowser\Module\WPLoader -bootstrap: bootstrap.php \ No newline at end of file + config: + lucatume\WPBrowser\Module\WPLoader: + # Enable coverage collection for this suite + coverage: true +bootstrap: bootstrap.php diff --git a/plugins/hwp-previews/tests/wpunit/Admin/Settings/Fields/Field/TextURLFieldTest.php b/plugins/hwp-previews/tests/wpunit/Admin/Settings/Fields/Field/TextURLFieldTest.php index e2aae306..48697658 100644 --- a/plugins/hwp-previews/tests/wpunit/Admin/Settings/Fields/Field/TextURLFieldTest.php +++ b/plugins/hwp-previews/tests/wpunit/Admin/Settings/Fields/Field/TextURLFieldTest.php @@ -176,4 +176,76 @@ public function test_get_setting_value_returns_value_when_set() { delete_option( $settings_key ); $this->assertEmpty( $field->get_setting_value( $settings_key, $post_type ) ); } + + public function test_fix_url_removes_html_and_scripts() { + $field = $this->field; + + // Simulate fix_url as a public/protected method for testing. + // If fix_url is private, use Reflection to access it. + $reflection = new \ReflectionClass($field); + $method = $reflection->getMethod('fix_url'); + $method->setAccessible(true); + + // Remove script tags + $input = 'https://example.com/?preview=true'; + $expected = 'https://example.com/?preview=true'; + $this->assertEquals($expected, $method->invoke($field, $input)); + + $input = ''; + $this->assertEmpty($method->invoke($field, $input)); + + // Remove HTML tags + $input = 'Boldhttps://example.com/'; + $expected = 'http://Boldhttps://example.com/'; + $this->assertEquals($expected, $method->invoke($field, $input)); + + // Remove image tags + $input = 'test'; + $expected = ''; + $this->assertEquals($expected, $method->invoke($field, $input)); + } + + public function test_fix_url_handles_encoded_urls() { + $field = $this->field; + $reflection = new \ReflectionClass($field); + $method = $reflection->getMethod('fix_url'); + $method->setAccessible(true); + + $input = 'https://example.com/?q=%3Cscript%3Ealert(1)%3C%2Fscript%3E'; + $expected = 'https://example.com/?q=%3Cscript%3Ealert(1)%3C%2Fscript%3E'; + $this->assertEquals($expected, $method->invoke($field, $input)); + } + + public function test_fix_url_preserves_placeholders() { + $field = $this->field; + $reflection = new \ReflectionClass($field); + $method = $reflection->getMethod('fix_url'); + $method->setAccessible(true); + + $input = 'https://example.com/{slug}?preview=true&post_id={ID}'; + $expected = 'https://example.com/{slug}?preview=true&post_id={ID}'; + $this->assertEquals($expected, $method->invoke($field, $input)); + } + + public function test_fix_url_handles_empty_and_invalid_input() { + $field = $this->field; + $reflection = new \ReflectionClass($field); + $method = $reflection->getMethod('fix_url'); + $method->setAccessible(true); + + $this->assertEquals('', $method->invoke($field, '')); + $this->assertEquals('', $method->invoke($field, '
')); + } + + public function test_fix_url_handles_relative_urls() { + $field = $this->field; + $reflection = new \ReflectionClass($field); + $method = $reflection->getMethod('fix_url'); + $method->setAccessible(true); + + $input = '/relative/path?foo=bar'; + $protocol = is_ssl() ? 'https://' : 'http://'; + $expected = $protocol . 'relative/path?foo=bar'; + $this->assertEquals($expected, $method->invoke($field, $input)); + } } diff --git a/plugins/hwp-previews/tests/wpunit/Admin/Settings/Menu/MenuPageTest.php b/plugins/hwp-previews/tests/wpunit/Admin/Settings/Menu/MenuPageTest.php new file mode 100644 index 00000000..20378693 --- /dev/null +++ b/plugins/hwp-previews/tests/wpunit/Admin/Settings/Menu/MenuPageTest.php @@ -0,0 +1,230 @@ + [ + 'tabs' => [ + 'post' => 'Posts', + 'page' => 'Pages' + ], + 'current_tab' => 'post', + 'params' => '' + ], + ]; + + protected Menu_Page $menu_page; + + public function setUp(): void { + parent::setUp(); + + $this->current_user_id = get_current_user_id(); + + // Create an administrator user for testing. + $this->admin_user_id = $this->factory()->user->create( [ + 'role' => 'administrator' + ] ); + wp_set_current_user( $this->admin_user_id ); + + // Create a temporary template file for testing + $this->template_file = sys_get_temp_dir() . '/test-template-' . uniqid() . '.php'; + file_put_contents( $this->template_file, '' ); + + $this->menu_page = new Menu_Page( + $this->page_title, + $this->menu_title, + $this->menu_slug, + $this->template_file, + $this->args + ); + } + + public function tearDown(): void { + // Clean up the temporary template file + if ( file_exists( $this->template_file ) ) { + unlink( $this->template_file ); + } + + // Clean up the user + wp_delete_user( $this->admin_user_id ); + wp_set_current_user( $this->current_user_id ); + + parent::tearDown(); + } + + public function test_constructor_sets_properties_correctly() { + + $page = $this->menu_page; + + $reflection = new \ReflectionClass( $page ); + + $page_title_prop = $reflection->getProperty( 'page_title' ); + $page_title_prop->setAccessible( true ); + $this->assertEquals( $this->page_title, $page_title_prop->getValue( $page ) ); + + $menu_title_prop = $reflection->getProperty( 'menu_title' ); + $menu_title_prop->setAccessible( true ); + $this->assertEquals( $this->menu_title, $menu_title_prop->getValue( $page ) ); + + $menu_slug_prop = $reflection->getProperty( 'menu_slug' ); + $menu_slug_prop->setAccessible( true ); + $this->assertEquals( $this->menu_slug, $menu_slug_prop->getValue( $page ) ); + + $template_prop = $reflection->getProperty( 'template' ); + $template_prop->setAccessible( true ); + $this->assertEquals( $this->template_file, $template_prop->getValue( $page ) ); + + $args_prop = $reflection->getProperty( 'args' ); + $args_prop->setAccessible( true ); + $this->assertEquals( $this->args, $args_prop->getValue( $page ) ); + } + + public function test_constructor_with_empty_args() { + $page = new Menu_Page( + $this->page_title, + $this->menu_title, + $this->menu_slug, + $this->template_file, + ); + + $reflection = new \ReflectionClass( $page ); + $args_prop = $reflection->getProperty( 'args' ); + $args_prop->setAccessible( true ); + $this->assertEquals( [], $args_prop->getValue( $page ) ); + } + + public function test_register_page_adds_submenu_correctly() { + global $submenu, $_registered_pages, $_parent_pages; + + $page = $this->menu_page; + + // Verify the current user has the required capability + $this->assertTrue( current_user_can( 'manage_options' ) ); + + // Capture the state before registration + $submenu_before = $submenu; + + $page->register_page(); + + // Verify that the submenu was modified + $this->assertNotEquals( $submenu_before, $submenu ); + + // Check that the submenu item was added to options-general.php + $this->assertArrayHasKey( 'options-general.php', $submenu ); + + // Find the added submenu item + $found_item = null; + foreach ( $submenu['options-general.php'] as $item ) { + if ( $item[2] === $this->menu_slug ) { + $found_item = $item; + break; + } + } + + $this->assertNotNull( $found_item, 'Should find the added submenu item' ); + $this->assertEquals( $this->menu_title, $found_item[0], 'Menu title should match' ); + $this->assertEquals( $this->menu_slug, $found_item[2], 'Menu slug should match' ); + $this->assertEquals( $this->page_title, $found_item[3], 'Page title should match' ); + $this->assertEquals( 'manage_options', $found_item[1], 'Capability should be manage_options' ); + + // Verify page was registered and parent relationship was set + $expected_hookname = get_plugin_page_hookname( $this->menu_slug, 'options-general.php' ); + $this->assertArrayHasKey( $expected_hookname, $_registered_pages ); + $this->assertEquals( 'options-general.php', $_parent_pages[$this->menu_slug] ); + } + + public function test_register_page_without_manage_options_capability() { + global $submenu, $_wp_submenu_nopriv; + + // Create a subscriber user (no manage_options capability) + $subscriber_id = $this->factory()->user->create( [ + 'role' => 'subscriber' + ] ); + + wp_set_current_user( $subscriber_id ); + + $page = $this->menu_page; + + // Verify the current user doesn't have the required capability + $this->assertFalse( current_user_can( 'manage_options' ) ); + + // Capture the state before registration + $submenu_before = $submenu; + + $page->register_page(); + + // When user lacks capability, the submenu should not be modified + // but the page should be marked as no-privilege + $this->assertEquals( $submenu_before, $submenu, 'Submenu should not be modified when user lacks capability' ); + $this->assertTrue( $_wp_submenu_nopriv['options-general.php'][$this->menu_slug], 'Should mark page as no-privilege' ); + + // Clean up + wp_delete_user( $subscriber_id ); + wp_set_current_user( $this->admin_user_id ); + } + + public function test_registration_callback_with_nonexistent_template() { + $page = new Menu_Page( + $this->page_title, + $this->menu_title, + $this->menu_slug, + '/path/to/nonexistent/template.php' + ); + + // Capture output + ob_start(); + $page->registration_callback(); + $output = ob_get_clean(); + + $this->assertStringContainsString( 'notice notice-error', $output ); + $this->assertStringContainsString( 'The HWP Previews Settings template does not exist.', $output ); + } + + public function test_registration_callback_sets_query_vars() { + $args = [ + 'test_var_1' => [ 'key1' => 'value1', 'key2' => 'value2' ], + 'test_var_2' => [ 'key3' => 'value3' ] + ]; + + $page = new Menu_Page( + $this->page_title, + $this->menu_title, + $this->menu_slug, + $this->template_file, + $args + ); + + // Clear any existing query vars + global $wp_query; + if ( isset( $wp_query->query_vars['test_var_1'] ) ) { + unset( $wp_query->query_vars['test_var_1'] ); + } + if ( isset( $wp_query->query_vars['test_var_2'] ) ) { + unset( $wp_query->query_vars['test_var_2'] ); + } + + $page->registration_callback(); + + // Verify query vars were set + $this->assertEquals( [ 'key1' => 'value1', 'key2' => 'value2' ], get_query_var( 'test_var_1' ) ); + $this->assertEquals( [ 'key3' => 'value3' ], get_query_var( 'test_var_2' ) ); + } +} diff --git a/plugins/hwp-previews/tests/wpunit/Admin/Settings/SettingsFormManagerTest.php b/plugins/hwp-previews/tests/wpunit/Admin/Settings/SettingsFormManagerTest.php new file mode 100644 index 00000000..46dc2c1e --- /dev/null +++ b/plugins/hwp-previews/tests/wpunit/Admin/Settings/SettingsFormManagerTest.php @@ -0,0 +1,91 @@ +get_post_types(), + new Settings_Field_Collection() + ); + + $fields = $form_manager->get_field_collection(); + $this->assertInstanceOf(Settings_Field_Collection::class, $fields); + $post_types = $form_manager->get_post_types(); + $this->assertIsArray( $post_types ); + $this->assertNotEmpty( $post_types ); + } + + public function test_sanitize_settings() { + + $post_preview_service = new Post_Preview_Service(); + $form_manager = new Settings_Form_Manager( + $post_preview_service->get_post_types(), + new Settings_Field_Collection() + ); + + $data = [ + "page" => [ + "enabled" => "1", + "post_statuses_as_parent" => "1", + "in_iframe" => "1", + "preview_url" => "https://localhost:3000/page?preview=true&post_id={ID}&name={slug}" + ] + ]; + + $sanitized_data = $form_manager->sanitize_settings( $data ); + $this->assertEquals( $sanitized_data, $data ); + } + + public function test_sanitize_settings_invalid_field() { + + $post_preview_service = new Post_Preview_Service(); + $form_manager = new Settings_Form_Manager( + $post_preview_service->get_post_types(), + new Settings_Field_Collection() + ); + + $data = [ + "page" => [ + "enabled" => "1", + 'not_registered_field' => "This field is not registered in the field collection so it should be removed", + "post_statuses_as_parent" => "1", + "in_iframe" => "1", + "preview_url" => "https://localhost:3000/page?preview=true&post_id={ID}&name={slug}" + ] + ]; + + $sanitized_data = $form_manager->sanitize_settings( $data ); + + // The 'not_registered_field' should be removed from the sanitized data. + unset( $data["page"]["not_registered_field"] ); + + $this->assertEquals( $sanitized_data, $data ); + } + + public function test_sanitize_settings_invalid_format() { + + $post_preview_service = new Post_Preview_Service(); + $form_manager = new Settings_Form_Manager( + $post_preview_service->get_post_types(), + new Settings_Field_Collection() + ); + + $data = [ + "enabled" => "1", + "post_statuses_as_parent" => "1", + "in_iframe" => "1", + "preview_url" => "https://localhost:3000/page?preview=true&post_id={ID}&name={slug}" + ]; + + $this->assertEmpty( $form_manager->sanitize_settings( $data ) ); + $this->assertEmpty( $form_manager->sanitize_settings([]) ); + } +} diff --git a/plugins/hwp-previews/tests/wpunit/Admin/SettingsPageTest.php b/plugins/hwp-previews/tests/wpunit/Admin/SettingsPageTest.php new file mode 100644 index 00000000..eccf0474 --- /dev/null +++ b/plugins/hwp-previews/tests/wpunit/Admin/SettingsPageTest.php @@ -0,0 +1,70 @@ +getProperty( 'instance' ); + $instanceProperty->setAccessible( true ); + $instanceProperty->setValue( null ); + + $this->assertNull( $instanceProperty->getValue() ); + $instance = Settings_Page::init(); + + $this->assertInstanceOf( Settings_Page::class, $instanceProperty->getValue() ); + $this->assertSame( $instance, $instanceProperty->getValue(), 'Settings_Page::init() should set the static instance property' ); + } + + public function test_get_current_tab() { + $_GET['attachment'] = 'attachment'; + $settings_page = Settings_Page::init(); + + $post_preview_service = new Post_Preview_Service(); + $post_types = $post_preview_service->get_post_types(); + + + $tab = $settings_page->get_current_tab( [], 'attachment' ); + $this->assertSame( '', $tab ); + + $tab = $settings_page->get_current_tab( $post_types, 'page' ); + $this->assertEquals( 'post', $tab ); + + $tab = $settings_page->get_current_tab( $post_types, 'attachment' ); + $this->assertSame( 'attachment', $tab ); + } + + public function test_register_hooks() { + $settings_page = Settings_Page::init(); + $this->assertNull( $settings_page->register_settings_page() ); + $this->assertNull( $settings_page->register_settings_fields() ); + $this->assertNull( $settings_page->load_scripts_styles( 'settings_page_hwp-previews' ) ); + $this->assertNull( $settings_page->load_scripts_styles( 'invalid-previews' ) ); + } +} diff --git a/plugins/hwp-previews/tests/wpunit/Core/ActivationTest.php b/plugins/hwp-previews/tests/wpunit/Core/ActivationTest.php new file mode 100644 index 00000000..8411011d --- /dev/null +++ b/plugins/hwp-previews/tests/wpunit/Core/ActivationTest.php @@ -0,0 +1,38 @@ +assertTrue( function_exists( 'hwp_previews_activation_callback' ) ); + } + + + public function test_custom_filter_on_hwp_previews_activate(): void { + $called = false; + + add_action( 'hwp_previews_activate', function () use ( &$called ) { + $called = true; + } ); + + $callback = hwp_previews_activation_callback(); + $callback(); + + $this->assertTrue( $called, 'Custom filter on hwp_previews_activate was not called.' ); + } +} diff --git a/plugins/hwp-previews/tests/wpunit/Core/AutoloaderTest.php b/plugins/hwp-previews/tests/wpunit/Core/AutoloaderTest.php new file mode 100644 index 00000000..2719a565 --- /dev/null +++ b/plugins/hwp-previews/tests/wpunit/Core/AutoloaderTest.php @@ -0,0 +1,123 @@ +assertInstanceOf( Autoloader::class, $autoloader ); + $this->assertTrue( method_exists( $autoloader, 'autoload' ) ); + $this->assertTrue( method_exists( $autoloader, 'get_composer_autoloader_path' ) ); + $this->assertTrue( method_exists( $autoloader, 'get_is_loaded' ) ); + + // Check composer autoloader file exists + $composer_file = $autoloader::get_composer_autoloader_path(); + $this->assertFileExists( $composer_file ); + $this->assertIsReadable( $composer_file ); + + // Autoload the composer dependencies + $result = $autoloader->autoload(); + $this->assertEquals( $result, $composer_file ); + $this->assertEquals( Autoloader::get_is_loaded(), $composer_file ); + $this->assertEquals( $result, Autoloader::get_is_loaded() ); + } + + // Additional test: Test that autoload() sets is_loaded to true when autoloader file exists and returns true. + public function test_autoload_sets_is_loaded_true_when_file_exists_and_returns_true(): void { + + // Create a temporary autoloader file that returns true + $temp_dir = sys_get_temp_dir() . '/hwp-previews-test-' . uniqid(); + mkdir( $temp_dir . '/vendor', 0755, true ); + $autoloader_path = $temp_dir . '/vendor/autoload.php'; + file_put_contents( $autoloader_path, 'getMethod( 'get_composer_autoloader_path' ); + $method->setAccessible( true ); + + // Backup original method + $original_method = $method; + + // Override method to return our temp path + $mock = $this->getMockBuilder( Autoloader::class ) + ->disableOriginalConstructor() + ->setMethods( [ 'get_composer_autoloader_path' ] ) + ->getMock(); + + // Reset static property + $property = $reflection->getProperty( 'is_loaded' ); + $property->setAccessible( true ); + $property->setValue( null, false ); + + // Call autoload and assert + $result = Autoloader::autoload(); + $this->assertTrue( $result, 'Autoload should return true when autoloader file returns true' ); + $this->assertTrue( Autoloader::get_is_loaded(), 'is_loaded should be true after successful autoload' ); + + // Clean up + unlink( $autoloader_path ); + rmdir( $temp_dir . '/vendor' ); + rmdir( $temp_dir ); + } + + /** + * Test that missing autoloader notice is displayed in admin. + */ + public function test_missing_autoloader_notice_admin(): void { + + // Call the method that should trigger the notice + $reflection = new ReflectionClass( Autoloader::class ); + $method = $reflection->getMethod( 'missing_autoloader_notice' ); + $method->setAccessible( true ); + + $method->invoke( null ); + + // Check that hooks were added + $this->assertTrue( has_action( 'admin_notices' ) ); + $this->assertTrue( has_action( 'network_admin_notices' ) ); + + // Test the actual notice output + ob_start(); + do_action( 'admin_notices' ); + $output = ob_get_clean(); + + $this->assertStringContainsString( 'HWP Previews: The Composer autoloader was not found', $output ); + $this->assertStringContainsString( 'composer install', $output ); + $this->assertStringContainsString( 'error notice', $output ); + } + + + public function test_get_composer_autoloader_path_returns_expected_path() { + hwp_previews_constants(); + $expected = HWP_PREVIEWS_PLUGIN_DIR . 'vendor/autoload.php'; + $this->assertEquals( + $expected, + Autoloader::get_composer_autoloader_path() + ); + } + + public function test_require_autoloader_returns_false_if_file_not_readable() { + $reflection = new \ReflectionClass( Autoloader::class ); + $method = $reflection->getMethod( 'require_autoloader' ); + $method->setAccessible( true ); + + // Use a non-existent file + $result = $method->invokeArgs( null, [ '/tmp/does-not-exist-' . uniqid() . '.php' ] ); + $this->assertFalse( $result ); + } +} diff --git a/plugins/hwp-previews/tests/wpunit/Core/DeactivationTest.php b/plugins/hwp-previews/tests/wpunit/Core/DeactivationTest.php new file mode 100644 index 00000000..2d978e1a --- /dev/null +++ b/plugins/hwp-previews/tests/wpunit/Core/DeactivationTest.php @@ -0,0 +1,39 @@ +assertTrue( function_exists( 'hwp_previews_deactivation_callback' ) ); + } + + + public function test_custom_filter_on_hwp_previews_deactivate(): void { + $called = false; + + add_action( 'hwp_previews_deactivate', function () use ( &$called ) { + $called = true; + } ); + + $callback = hwp_previews_deactivation_callback(); + $callback(); + + $this->assertTrue( $called, 'Custom filter on hwp_previews_deactivate was not called.' ); + } +} diff --git a/plugins/hwp-previews/tests/wpunit/Core/PluginTest.php b/plugins/hwp-previews/tests/wpunit/Core/PluginTest.php new file mode 100644 index 00000000..677fbb71 --- /dev/null +++ b/plugins/hwp-previews/tests/wpunit/Core/PluginTest.php @@ -0,0 +1,69 @@ +assertTrue( $instance instanceof Plugin ); + } + + public function test_singleton_returns_same_instance() { + $first = Plugin::init(); + $second = Plugin::init(); + $this->assertSame( $first, $second, 'Plugin::instance() should always return the same instance' ); + } + + public function test_instance_creates_and_sets_up_plugin_when_not_set() { + $reflection = new ReflectionClass( Plugin::class ); + $instanceProperty = $reflection->getProperty( 'instance' ); + $instanceProperty->setAccessible( true ); + $instanceProperty->setValue( null ); + + $this->assertNull( $instanceProperty->getValue() ); + $instance = Plugin::init(); + + $this->assertInstanceOf( Plugin::class, $instanceProperty->getValue() ); + $this->assertSame( $instance, $instanceProperty->getValue(), 'Plugin::instance() should set the static instance property' ); + } + + + public function test_clone_method_throws_error() { + // Create a fresh instance instead of using singleton + $reflection = new ReflectionClass( Plugin::class ); + $plugin = $reflection->newInstanceWithoutConstructor(); + + $this->setExpectedIncorrectUsage( 'HWP\Previews\Plugin::__clone' ); + $clone = clone $plugin; + + // Verify the clone exists to ensure the operation completed + $this->assertInstanceOf( Plugin::class, $clone ); + } + + public function test_wakeup_method_throws_error() { + $this->setExpectedIncorrectUsage( 'HWP\Previews\Plugin::__wakeup' ); + + // Create a fresh instance + $reflection = new ReflectionClass( Plugin::class ); + $plugin = $reflection->newInstanceWithoutConstructor(); + + $serialized = serialize( $plugin ); + $unserialized = unserialize( $serialized ); + + // Verify the unserialized object exists + $this->assertInstanceOf( Plugin::class, $unserialized ); + } +} diff --git a/plugins/hwp-previews/tests/wpunit/Hooks/PreviewHooksTest.php b/plugins/hwp-previews/tests/wpunit/Hooks/PreviewHooksTest.php new file mode 100644 index 00000000..2a73175f --- /dev/null +++ b/plugins/hwp-previews/tests/wpunit/Hooks/PreviewHooksTest.php @@ -0,0 +1,577 @@ +remove_all_filters(); + $this->delete_option(); + + // Set up test filters so we don't overwrite the actual plugin settings. + add_filter( 'hwp_previews_settings_group_option_key', function () { + return $this->test_option_key; + } ); + add_filter( 'hwp_previews_settings_group_settings_group', function () { + return $this->test_settings_group; + } ); + + $this->post = WPTestCase::factory()->post->create_and_get( [ + 'post_type' => 'post', + 'post_status' => 'draft', + ] ); + + + } + + public function tearDown(): void { + $this->delete_option(); + $this->remove_all_filters(); + parent::tearDown(); + } + + public function remove_all_filters() { + remove_all_filters( 'hwp_previews_settings_group_option_key' ); + remove_all_filters( 'hwp_previews_settings_group_settings_group' ); + remove_all_filters( 'hwp_previews_template_path' ); + } + + public function delete_option() { + delete_option( $this->test_option_key ); + wp_cache_flush(); + } + + public function test_preview_hooks_instance() { + + + $reflection = new ReflectionClass( Preview_Hooks::class ); + $instanceProperty = $reflection->getProperty( 'instance' ); + $instanceProperty->setAccessible( true ); + $instanceProperty->setValue( null ); + + $this->assertNull( $instanceProperty->getValue() ); + $instance = Preview_Hooks::init(); + + $this->assertInstanceOf( Preview_Hooks::class, $instanceProperty->getValue() ); + $this->assertSame( $instance, $instanceProperty->getValue(), 'Preview_Hooks::init() should set the static instance property' ); + } + + public function test_enable_post_statuses_as_parent_asserts_true() { + + $test_config = [ + 'page' => [ + Settings_Field_Collection::ENABLED_FIELD_ID => true, + Settings_Field_Collection::POST_STATUSES_AS_PARENT_FIELD_ID => true, + ] + ]; + update_option( $this->test_option_key, $test_config ); + + $args = [ + 'post_type' => 'page' + ]; + + $preview_hooks = new Preview_Hooks(); + $newArgs = $preview_hooks->enable_post_statuses_as_parent( $args ); + $this->assertArrayHasKey( 'post_type', $newArgs ); + $this->assertArrayHasKey( 'post_status', $newArgs, 'Post type is not enabled for post statuses for parent.' ); + + $this->assertEquals( $newArgs['post_type'], 'page' ); + $this->assertIsArray( $newArgs['post_status'] ); + } + + public function test_enable_post_statuses_as_parent_asserts_false_no_config_values() { + + $args = [ + 'post_type' => 'page' + ]; + + $preview_hooks = new Preview_Hooks(); + $newArgs = $preview_hooks->enable_post_statuses_as_parent( $args ); + + $this->assertArrayHasKey( 'post_type', $newArgs ); + $this->assertArrayNotHasKey( 'post_status', $newArgs ); + $this->assertEquals( $args, $newArgs ); + } + + public function test_enable_post_statuses_as_parent_asserts_false_option_not_enabled() { + + $test_config = [ + 'page' => [ + Settings_Field_Collection::ENABLED_FIELD_ID => true, + Settings_Field_Collection::POST_STATUSES_AS_PARENT_FIELD_ID => false, + ] + ]; + update_option( $this->test_option_key, $test_config ); + + $args = [ + 'post_type' => 'page' + ]; + + $preview_hooks = new Preview_Hooks(); + $newArgs = $preview_hooks->enable_post_statuses_as_parent( $args ); + + $this->assertArrayNotHasKey( 'post_status', $newArgs ); + $this->assertEquals( $args, $newArgs ); + } + + public function test_enable_post_statuses_as_parent_asserts_false_not_hierarchal_post_type() { + + $test_config = [ + 'page' => [ + Settings_Field_Collection::ENABLED_FIELD_ID => true, + Settings_Field_Collection::POST_STATUSES_AS_PARENT_FIELD_ID => false, + ], + 'post' => [ + Settings_Field_Collection::ENABLED_FIELD_ID => true, + Settings_Field_Collection::POST_STATUSES_AS_PARENT_FIELD_ID => false, // Note this doesn't appear in the admin but lets pretend so we can assert false + ] + ]; + update_option( $this->test_option_key, $test_config ); + + $args = [ + 'post_type' => 'post' + ]; + + $preview_hooks = new Preview_Hooks(); + $newArgs = $preview_hooks->enable_post_statuses_as_parent( $args ); + + $this->assertArrayNotHasKey( 'post_status', $newArgs ); + $this->assertEquals( $args, $newArgs ); + } + + public function test_enable_post_statuses_as_parent_asserts_false_not_post_type() { + + $test_config = [ + 'post' => [ + Settings_Field_Collection::ENABLED_FIELD_ID => true, + Settings_Field_Collection::POST_STATUSES_AS_PARENT_FIELD_ID => true, + ] + ]; + update_option( $this->test_option_key, $test_config ); + + $args = [ + ]; + + $preview_hooks = new Preview_Hooks(); + $newArgs = $preview_hooks->enable_post_statuses_as_parent( $args ); + + $this->assertEquals( $args, $newArgs ); + } + + public function test_should_return_iframe_template() { + + $test_config = [ + 'post' => [ + Settings_Field_Collection::ENABLED_FIELD_ID => true, + Settings_Field_Collection::IN_IFRAME_FIELD_ID => true, + Settings_Field_Collection::PREVIEW_URL_FIELD_ID => 'https://localhost:3000/post?preview=true&post_id={ID}&status={status}', + ] + ]; + update_option( $this->test_option_key, $test_config ); + + // Set is_preview to true + global $wp_query; + $wp_query->is_preview = true; + + // Set global post to a WP_Post object + $new_post = $this->post; + + global $post; + $post = $new_post; + + $expected_template = ''; + add_filter( + 'hwp_previews_template_path', + function ( $template ) use ( &$expected_template ) { + $expected_template = $template; + + return $template; + } + ); + + + $preview = new Preview_Hooks(); + $template = $preview->add_iframe_preview_template( 'default-template.php' ); + $this->assertEquals( $expected_template, $template ); + + // Assert that the query variable is set correctly in add_iframe_preview_template function + $this->assertEquals( Template_Resolver_Service::get_query_variable(), 'https://localhost:3000/post?preview=true&post_id=' . $new_post->ID . '&status=draft' ); + } + + public function test_should_return_iframe_template_return_default_not_is_preview() { + + $test_config = [ + 'post' => [ + Settings_Field_Collection::ENABLED_FIELD_ID => true, + Settings_Field_Collection::IN_IFRAME_FIELD_ID => true, + Settings_Field_Collection::PREVIEW_URL_FIELD_ID => 'https://localhost:3000/post?preview=true&post_id={ID}&status={status}', + ] + ]; + update_option( $this->test_option_key, $test_config ); + + // Set is_preview to false + global $wp_query; + $wp_query->is_preview = false; + + // Set global post to a WP_Post object + $new_post = $this->post; + + global $post; + $post = $new_post; + + $preview = new Preview_Hooks(); + $template = $preview->add_iframe_preview_template( 'default-template.php' ); + $this->assertEquals( $template, 'default-template.php' ); + } + + + public function test_should_return_iframe_template_return_default_no_post() { + + $test_config = [ + 'post' => [ + Settings_Field_Collection::ENABLED_FIELD_ID => true, + Settings_Field_Collection::IN_IFRAME_FIELD_ID => true, + Settings_Field_Collection::PREVIEW_URL_FIELD_ID => 'https://localhost:3000/post?preview=true&post_id={ID}&status={status}', + ] + ]; + update_option( $this->test_option_key, $test_config ); + + // Set is_preview to true + global $wp_query; + $wp_query->is_preview = true; + + // Set global post to a WP_Post object + $new_post = $this->post; + + // No post set + global $post; + $post = ''; + + $preview = new Preview_Hooks(); + $template = $preview->add_iframe_preview_template( 'default-template.php' ); + $this->assertEquals( $template, 'default-template.php' ); + } + + public function test_should_return_iframe_template_return_default_no_iframe_template() { + + $test_config = [ + 'post' => [ + Settings_Field_Collection::ENABLED_FIELD_ID => true, + Settings_Field_Collection::IN_IFRAME_FIELD_ID => true, + Settings_Field_Collection::PREVIEW_URL_FIELD_ID => 'https://localhost:3000/post?preview=true&post_id={ID}&status={status}', + ] + ]; + update_option( $this->test_option_key, $test_config ); + + // Set is_preview to true + global $wp_query; + $wp_query->is_preview = true; + + // Set global post to a WP_Post object + $new_post = $this->post; + + global $post; + $post = $new_post; + + // Ensure that the filter returns an empty string so that the template is not found + add_filter( + 'hwp_previews_template_path', + function ( $template ) { + return ''; + } + ); + + + $preview = new Preview_Hooks(); + $template = $preview->add_iframe_preview_template( 'default-template.php' ); + $this->assertEquals( $template, 'default-template.php' ); + } + + public function test_should_return_iframe_template_return_default_not_enabled_for_previews() { + + $test_config = [ + 'post' => [ + Settings_Field_Collection::ENABLED_FIELD_ID => false, // Not enabled for previews + Settings_Field_Collection::IN_IFRAME_FIELD_ID => true, + Settings_Field_Collection::PREVIEW_URL_FIELD_ID => 'https://localhost:3000/post?preview=true&post_id={ID}&status={status}', + ] + ]; + update_option( $this->test_option_key, $test_config ); + + // Set is_preview to true + global $wp_query; + $wp_query->is_preview = true; + + // Set global post to a WP_Post object + $new_post = $this->post; + + global $post; + $post = $new_post; + + $preview = new Preview_Hooks(); + $template = $preview->add_iframe_preview_template( 'default-template.php' ); + $this->assertEquals( $template, 'default-template.php' ); + } + + + public function test_generate_preview_url_no_url() { + + $test_config = [ + 'post' => [ + Settings_Field_Collection::ENABLED_FIELD_ID => true, + Settings_Field_Collection::IN_IFRAME_FIELD_ID => true, + Settings_Field_Collection::PREVIEW_URL_FIELD_ID => '', + ] + ]; + update_option( $this->test_option_key, $test_config ); + + $new_post = $this->post; + $preview = new Preview_Hooks(); + $url = $preview->generate_preview_url( $new_post ); + + $this->assertEquals( '', $url ); + } + + public function test_generate_preview_url_no_url_not_enabled() { + + $test_config = [ + 'post' => [ + Settings_Field_Collection::ENABLED_FIELD_ID => false, + Settings_Field_Collection::IN_IFRAME_FIELD_ID => true, + Settings_Field_Collection::PREVIEW_URL_FIELD_ID => 'https://localhost:3000/post?preview=true&post_id={ID}&status={status}', + ] + ]; + update_option( $this->test_option_key, $test_config ); + + $new_post = $this->post; + $preview = new Preview_Hooks(); + $url = $preview->generate_preview_url( $new_post ); + + $this->assertEquals( '', $url ); + } + + public function test_generate_preview_url_return_valid_url() { + + // Note: More tests in TemplateResolverTest.php + + $test_config = [ + 'post' => [ + Settings_Field_Collection::ENABLED_FIELD_ID => true, + Settings_Field_Collection::IN_IFRAME_FIELD_ID => true, + Settings_Field_Collection::PREVIEW_URL_FIELD_ID => 'https://localhost:3000/post?preview=true&post_id={ID}&status={status}', + ] + ]; + update_option( $this->test_option_key, $test_config ); + + $new_post = $this->post; + $preview = new Preview_Hooks(); + + $url = $preview->generate_preview_url( $new_post ); + $this->assertEquals( 'https://localhost:3000/post?preview=true&post_id=' . $new_post->ID . '&status=draft', $url ); + } + + public function test_update_preview_post_link_returns_generated_url() { + + $preview_link = 'https://localhost:3000/post?preview=true&post_id={ID}&status={status}'; + + $test_config = [ + 'post' => [ + Settings_Field_Collection::ENABLED_FIELD_ID => true, + Settings_Field_Collection::IN_IFRAME_FIELD_ID => false, + Settings_Field_Collection::PREVIEW_URL_FIELD_ID => $preview_link, + ] + ]; + update_option( $this->test_option_key, $test_config ); + + $new_post = $this->post; + $preview = new Preview_Hooks(); + + $url = $preview->update_preview_post_link( $preview_link, $new_post ); + $this->assertNotEquals( $url, $preview_link, 'The URL should not be the same as the preview link' ); + } + + public function test_update_preview_post_link_returns_default_previews_not_enabled() { + + $preview_link = 'https://localhost:3000/post?preview=true&post_id={ID}&status={status}'; + + $test_config = [ + 'post' => [ + Settings_Field_Collection::ENABLED_FIELD_ID => false, + Settings_Field_Collection::IN_IFRAME_FIELD_ID => false, + Settings_Field_Collection::PREVIEW_URL_FIELD_ID => $preview_link, + ] + ]; + update_option( $this->test_option_key, $test_config ); + + $new_post = $this->post; + $preview = new Preview_Hooks(); + + $url = $preview->update_preview_post_link( $preview_link, $new_post ); + $this->assertEquals( $url, $preview_link, 'The URL should be the same as the preview link as preview is not enabled for posts' ); + } + + public function test_update_preview_post_link_returns_default_iframe_enabled() { + + $preview_link = 'https://localhost:3000/post?preview=true&post_id={ID}&status={status}'; + + $test_config = [ + 'post' => [ + Settings_Field_Collection::ENABLED_FIELD_ID => true, + Settings_Field_Collection::IN_IFRAME_FIELD_ID => true, + Settings_Field_Collection::PREVIEW_URL_FIELD_ID => $preview_link, + ] + ]; + update_option( $this->test_option_key, $test_config ); + + $new_post = $this->post; + $preview = new Preview_Hooks(); + + $url = $preview->update_preview_post_link( $preview_link, $new_post ); + $this->assertEquals( $url, $preview_link, 'The URL should be the same as the preview link as iframe is enabled for posts' ); + } + + + public function test_update_preview_post_link_returns_default_no_preview_url() { + + $preview_link = 'https://localhost:3000/post?preview=true&post_id={ID}&status={status}'; + + $test_config = [ + 'post' => [ + Settings_Field_Collection::ENABLED_FIELD_ID => true, + Settings_Field_Collection::IN_IFRAME_FIELD_ID => false, + ] + ]; + update_option( $this->test_option_key, $test_config ); + + // Set global post to a WP_Post object + $new_post = $this->post; + + $preview = new Preview_Hooks(); + + $url = $preview->update_preview_post_link( $preview_link, $new_post ); + $this->assertEquals( $url, $preview_link, 'The URL should be the same as the preview link as post type was removed from allowed post types' ); + } + + + public function test_filter_rest_prepare_link_adds_link() { + + $preview_link = 'https://localhost:3000/post?preview=true&post_id={ID}&status={status}'; + + $test_config = [ + 'post' => [ + Settings_Field_Collection::ENABLED_FIELD_ID => true, + Settings_Field_Collection::IN_IFRAME_FIELD_ID => false, + Settings_Field_Collection::PREVIEW_URL_FIELD_ID => $preview_link, + ] + ]; + update_option( $this->test_option_key, $test_config ); + + $new_post = $this->post; + + $original_response = new WP_REST_Response( [ 'foo' => 'bar' ] ); + $preview = new Preview_Hooks(); + + $response = $preview->filter_rest_prepare_link( $original_response, $new_post ); + $data = $response->get_data(); + $this->assertArrayHasKey( 'link', $data ); + + $this->assertEquals( 'https://localhost:3000/post?preview=true&post_id=' . $new_post->ID . '&status=' . $new_post->post_status, $data['link'] ); + } + + public function test_filter_rest_prepare_link_no_link_iframe_enabled() { + + $preview_link = 'https://localhost:3000/post?preview=true&post_id={ID}&status={status}'; + + $test_config = [ + 'post' => [ + Settings_Field_Collection::ENABLED_FIELD_ID => true, + Settings_Field_Collection::IN_IFRAME_FIELD_ID => true, + Settings_Field_Collection::PREVIEW_URL_FIELD_ID => $preview_link, + ] + ]; + update_option( $this->test_option_key, $test_config ); + + $new_post = $this->post; + + $original_response = new WP_REST_Response( [ 'foo' => 'bar' ] ); + $preview = new Preview_Hooks(); + + $response = $preview->filter_rest_prepare_link( $original_response, $new_post ); + $data = $response->get_data(); + $this->assertArrayNotHasKey( 'link', $data ); + $this->assertEquals( $original_response, $response ); + } + + public function test_filter_rest_prepare_link_no_link_previews_not_enabled() { + + $preview_link = 'https://localhost:3000/post?preview=true&post_id={ID}&status={status}'; + + $test_config = [ + 'post' => [ + Settings_Field_Collection::ENABLED_FIELD_ID => false, + Settings_Field_Collection::IN_IFRAME_FIELD_ID => false, + Settings_Field_Collection::PREVIEW_URL_FIELD_ID => $preview_link, + ] + ]; + update_option( $this->test_option_key, $test_config ); + + $new_post = $this->post; + + $original_response = new WP_REST_Response( [ 'foo' => 'bar' ] ); + $preview = new Preview_Hooks(); + + $response = $preview->filter_rest_prepare_link( $original_response, $new_post ); + $data = $response->get_data(); + $this->assertArrayNotHasKey( 'link', $data ); + $this->assertEquals( $original_response, $response ); + } + + public function test_filter_rest_prepare_link_no_link_previews_no_preview_url() { + + $test_config = [ + 'post' => [ + Settings_Field_Collection::ENABLED_FIELD_ID => true, + Settings_Field_Collection::IN_IFRAME_FIELD_ID => false, + ] + ]; + update_option( $this->test_option_key, $test_config ); + + $new_post = $this->post; + + $original_response = new WP_REST_Response( [ 'foo' => 'bar' ] ); + $preview = new Preview_Hooks(); + + $response = $preview->filter_rest_prepare_link( $original_response, $new_post ); + $data = $response->get_data(); + $this->assertArrayNotHasKey( 'link', $data ); + $this->assertEquals( $original_response, $response ); + } +} diff --git a/plugins/hwp-previews/tests/wpunit/Integration/FaustIntegrationTest.php b/plugins/hwp-previews/tests/wpunit/Integration/FaustIntegrationTest.php new file mode 100644 index 00000000..a16f941a --- /dev/null +++ b/plugins/hwp-previews/tests/wpunit/Integration/FaustIntegrationTest.php @@ -0,0 +1,135 @@ +getProperty( 'instance' ); + $instanceProperty->setAccessible( true ); + $instanceProperty->setValue( null, null ); + } + + + public function test_instance_creates_and_sets_up_faust_integration_when_not_set() { + $reflection = new ReflectionClass( Faust_Integration::class ); + $instanceProperty = $reflection->getProperty( 'instance' ); + $instanceProperty->setAccessible( true ); + $instanceProperty->setValue( null ); + + $this->assertNull( $instanceProperty->getValue() ); + $instance = Faust_Integration::init(); + + $this->assertInstanceOf( Faust_Integration::class, $instanceProperty->getValue() ); + $this->assertSame( $instance, $instanceProperty->getValue(), 'Faust_Integration::init() should set the static instance property' ); + + $this->assertFalse( $instance->get_faust_enabled() ); + $this->assertFalse( $instance->get_faust_configured() ); + + } + + + public function test_instance_configure_faust() { + + // Mock FaustWP exists + tests_add_filter( 'pre_option_active_plugins', function ( $plugins ) { + $plugins[] = 'faustwp/faustwp.php'; + + return $plugins; + } ); + + $instance = Faust_Integration::init(); + $this->assertTrue( $instance->get_faust_enabled() ); + $this->assertTrue( $instance->get_faust_configured() ); + } + + + public function test_dismiss_faust_notice_meta_value() { + $instance = Faust_Integration::init(); + + $admin_user = WPTestCase::factory()->user->create_and_get( [ + 'role' => 'administrator', + 'meta_input' => [ + 'first_name' => 'Test', + 'last_name' => 'User', + ], + 'user_login' => 'testuser' + ] ); + + // Set the current user to the admin user + $original_user_id = get_current_user_id(); + wp_set_current_user( $admin_user->ID ); + + // Set the user meta and check + $instance::dismiss_faust_admin_notice(); + $this->assertEquals( + 1, + get_user_meta( $admin_user->ID, Faust_Integration::FAUST_NOTICE_KEY, true ) + ); + + $this->assertFalse( + get_user_meta( $original_user_id, Faust_Integration::FAUST_NOTICE_KEY, true ) + ); + + // Reset the current user + wp_set_current_user( $original_user_id ); + } + + + public function test_faust_frontend_url_default_url() { + + $instance = Faust_Integration::init(); + $this->assertEquals( $instance->get_faust_frontend_url(), 'http://localhost:3000' ); + + } + + public function test_faust_frontend_url_with_faust_setting() { + + // Mock FaustWP exists + tests_add_filter( 'pre_option_active_plugins', function ( $plugins ) { + $plugins[] = 'faustwp/faustwp.php'; + + return $plugins; + } ); + + $frontend_uri = 'https://mocked-frontend.com'; + + // We need to Mock each type so that the function can be called in different ways + WP_Mock::userFunction('\WPE\FaustWP\Settings\faustwp_get_setting', [ + 'return' => $frontend_uri + ]); + + WP_Mock::userFunction('faustwp_get_setting', [ + 'return' => $frontend_uri + ]); + + WP_Mock::userFunction('WPE\FaustWP\Settings\faustwp_get_setting', [ + 'return' => $frontend_uri + ]); + + $instance = Faust_Integration::init(); + $this->assertTrue(function_exists( '\WPE\FaustWP\Settings\faustwp_get_setting' ) ); + $this->assertEquals( $frontend_uri, $instance->get_faust_frontend_url() ); + + } +} diff --git a/plugins/hwp-previews/tests/wpunit/Preview/Parameter/PreviewParameterRegsitryTest.php b/plugins/hwp-previews/tests/wpunit/Preview/Parameter/PreviewParameterRegsitryTest.php new file mode 100644 index 00000000..572def74 --- /dev/null +++ b/plugins/hwp-previews/tests/wpunit/Preview/Parameter/PreviewParameterRegsitryTest.php @@ -0,0 +1,50 @@ +getProperty( 'instance' ); + $instanceProperty->setAccessible( true ); + $instanceProperty->setValue( null ); + + $this->assertNull( $instanceProperty->getValue() ); + $instance = Preview_Parameter_Registry::get_instance(); + + $this->assertInstanceOf( Preview_Parameter_Registry::class, $instanceProperty->getValue() ); + $this->assertSame( $instance, $instanceProperty->getValue(), 'Preview_Parameter_Registry::get_instance() should set the static instance property' ); + } + + public function test_registering_new_parameter() { + $registry = Preview_Parameter_Registry::get_instance(); + $registry->register( + new Preview_Parameter( 'test_param', static fn() => 'test_value', 'Test parameter' ) + ); + + $parameter = $registry->get( 'test_param' ); + $this->assertInstanceOf( Preview_Parameter::class, $parameter ); + $this->assertSame( 'test_param', $parameter->get_name() ); + $this->assertSame( 'test_value', $parameter->get_value( new \WP_Post( (object) [ 'ID' => 1 ] ) ) ); + + $registry->unregister( 'test_param' ); + $this->assertNull( $registry->get( 'test_param' ) ); + } + + + public function test_get_all_descriptions() { + $registry = Preview_Parameter_Registry::get_instance(); + $descriptions = $registry->get_descriptions(); + $this->assertNotEmpty( $descriptions ); + } + +} diff --git a/plugins/hwp-previews/tests/wpunit/Preview/Parameter/PreviewParameterTest.php b/plugins/hwp-previews/tests/wpunit/Preview/Parameter/PreviewParameterTest.php new file mode 100644 index 00000000..4227edf8 --- /dev/null +++ b/plugins/hwp-previews/tests/wpunit/Preview/Parameter/PreviewParameterTest.php @@ -0,0 +1,31 @@ + (string) $post->ID, 'Post ID.'); + $this->assertEquals( 'Post ID.', $preview->get_description() ); + $this->assertEquals( 'ID', $preview->get_name() ); + } + + public function test_create_instance_get_value() { + $preview = new Preview_Parameter('status', static fn( WP_Post $post ) => $post->post_status, 'The post status.'); + + $post = WPTestCase::factory()->post->create_and_get( [ + 'post_title' => 'Test Post', + 'post_status' => 'publish', + 'post_content' => 'This is a test post.', + ] ); + + $this->assertEquals($post->post_status, $preview->get_value($post)); + } +} diff --git a/plugins/hwp-previews/tests/wpunit/Preview/Post/PostEditorServiceTest.php b/plugins/hwp-previews/tests/wpunit/Preview/Post/PostEditorServiceTest.php new file mode 100644 index 00000000..c042072c --- /dev/null +++ b/plugins/hwp-previews/tests/wpunit/Preview/Post/PostEditorServiceTest.php @@ -0,0 +1,123 @@ +service = new Post_Editor_Service(); + + $this->original_settings = get_option( 'classic-editor-settings', [] ); + $this->clean_up_filter_options(); + } + + public function tearDown(): void { + $this->clean_up_filter_options(); + update_option( 'classic-editor-settings', $this->original_settings ); + parent::tearDown(); + } + + public function clean_up_filter_options() { + remove_all_filters( 'pre_option_classic-editor-settings' ); + delete_option( 'classic-editor-settings' ); + } + + + public function test_gutenberg_editor_enabled_returns_true_when_conditions_met(): void { + + $post_type = 'events'; + register_post_type( $post_type, [ + 'label' => 'Events', + 'description' => 'Custom post type for events', + 'public' => true, + 'show_in_rest' => true, // Gutenberg supported + 'supports' => array( 'title', 'editor', 'author', 'thumbnail' ), + ] ); + + $result = $this->service->gutenberg_editor_enabled( $post_type ); + + $this->assertTrue( $result ); + unregister_post_type( $post_type ); + } + + public function test_gutenberg_editor_enabled_returns_false_when_gutenberg_not_supported(): void { + $post_type = 'events_no_gutenberg'; + register_post_type($post_type, [ + 'label' => 'Events', + 'description' => 'Custom post type for events', + 'public' => true, + 'show_in_rest' => false, // Gutenberg not supported + 'supports' => ['title', 'editor'] + ]); + + $result = $this->service->gutenberg_editor_enabled($post_type); + $this->assertFalse($result); + + unregister_post_type($post_type); + } + + public function test_gutenberg_editor_enabled_returns_false_when_post_type_not_exists(): void { + $result = $this->service->gutenberg_editor_enabled( 'nonexistent_post_type' ); + + $this->assertFalse( $result ); + } + + + public function test_gutenberg_editor_enabled_returns_false_when_classic_editor_forced(): void { + $post_type = 'events_classic'; + register_post_type($post_type, [ + 'public' => true, + 'show_in_rest' => true, + 'supports' => ['title', 'editor'] + ]); + + // Mock classic editor being active and configured + tests_add_filter( 'pre_option_active_plugins', function( $plugins ) { + $plugins[] = 'classic-editor/classic-editor.php'; + return $plugins; + } ); + + + // Set classic editor settings to force this post type + update_option('classic-editor-settings', [ + 'post_types' => [$post_type] + ]); + + + $result = $this->service->gutenberg_editor_enabled($post_type); + + $this->assertFalse($result); + unregister_post_type($post_type); + } + + + public function test_is_gutenberg_supported_returns_false_when_editor_not_supported(): void { + $post_type = 'no_editor_support'; + register_post_type($post_type, [ + 'public' => true, + 'show_in_rest' => true, + 'supports' => ['title'] // No 'editor' support + ]); + + $result = $this->service->gutenberg_editor_enabled($post_type); + + $this->assertFalse($result); + unregister_post_type($post_type); + } + + + +} diff --git a/plugins/hwp-previews/tests/wpunit/Preview/Post/PostPreviewServiceTest.php b/plugins/hwp-previews/tests/wpunit/Preview/Post/PostPreviewServiceTest.php new file mode 100644 index 00000000..a023de95 --- /dev/null +++ b/plugins/hwp-previews/tests/wpunit/Preview/Post/PostPreviewServiceTest.php @@ -0,0 +1,115 @@ +remove_all_filters(); + $this->service = new Post_Preview_Service(); + } + + public function tearDown(): void { + $this->remove_all_filters(); + parent::tearDown(); + } + + public function remove_all_filters() { + remove_all_filters( 'hwp_previews_filter_available_post_types' ); + remove_all_filters( 'hwp_previews_filter_available_post_statuses' ); + } + + public function test_get_allowed_post_types_returns_array(): void { + $result = $this->service->get_allowed_post_types(); + + $this->assertIsArray( $result ); + $this->assertNotEmpty( $result ); + } + + public function test_get_post_statuses_returns_default_statuses(): void { + $result = $this->service->get_post_statuses(); + + $expected = [ + 'publish', + 'future', + 'draft', + 'pending', + 'private', + 'auto-draft', + ]; + $this->assertEquals($expected, $result); + } + + public function test_get_parent_post_statuses_returns_default_statuses(): void { + $result = $this->service->get_parent_post_statuses(); + + $expected = [ + 'publish', + 'future', + 'draft', + 'pending', + 'private' + ]; + $this->assertEquals($expected, $result); + } + + public function test_get_post_types_returns_same_as_get_allowed_post_types(): void { + $allowed_types = $this->service->get_allowed_post_types(); + $post_types = $this->service->get_post_types(); + + $this->assertEquals($allowed_types, $post_types); + } + + public function test_post_types_filter_is_applied(): void { + $custom_post_types = ['custom_post' => 'Custom Post Type']; + add_filter('hwp_previews_filter_available_post_types', function() use ($custom_post_types) { + return $custom_post_types; + }); + + $service = new Post_Preview_Service(); + $result = $service->get_post_types(); + $this->assertEquals($custom_post_types, $result); + } + + public function test_post_statuses_filter_is_applied(): void { + $custom_statuses = ['custom_status']; + add_filter('hwp_previews_filter_available_post_statuses', function() use ($custom_statuses) { + return $custom_statuses; + }); + + $service = new Post_Preview_Service(); + $result = $service->get_post_statuses(); + $this->assertEquals($custom_statuses, $result); + } + + public function test_parent_post_statuses_filter_is_applied(): void { + $custom_statuses = ['custom_status']; + add_filter('hwp_previews_filter_available_parent_post_statuses', function() use ($custom_statuses) { + return $custom_statuses; + }); + + $service = new Post_Preview_Service(); + $result = $service->get_parent_post_statuses(); + $this->assertEquals($custom_statuses, $result); + } + + public function test_constructor_initializes_post_types_and_statuses(): void { + $service = new Post_Preview_Service(); + + $this->assertIsArray($service->get_post_types()); + $this->assertIsArray($service->get_post_statuses()); + $this->assertNotEmpty($service->get_post_statuses()); + } +} diff --git a/plugins/hwp-previews/tests/wpunit/Preview/Post/PostSettingsServiceTest.php b/plugins/hwp-previews/tests/wpunit/Preview/Post/PostSettingsServiceTest.php new file mode 100644 index 00000000..70a539d0 --- /dev/null +++ b/plugins/hwp-previews/tests/wpunit/Preview/Post/PostSettingsServiceTest.php @@ -0,0 +1,163 @@ +remove_all_filters(); + $this->delete_option(); + + // Set up test filters so we don't overwrite the actual plugin settings. + add_filter( 'hwp_previews_settings_group_option_key', function () { + return $this->test_option_key; + } ); + add_filter( 'hwp_previews_settings_group_settings_group', function () { + return $this->test_settings_group; + } ); + } + + public function tearDown(): void { + $this->delete_option(); + $this->remove_all_filters(); + parent::tearDown(); + } + + public function remove_all_filters() { + remove_all_filters( 'hwp_previews_settings_group_option_key' ); + remove_all_filters( 'hwp_previews_settings_group_settings_group' ); + } + + public function delete_option() { + delete_option( $this->test_option_key ); + wp_cache_flush(); + } + + public function test_get_post_type_config_returns_config_when_exists(): void { + $test_config = [ + 'post' => [ 'enabled' => true, 'in_iframe' => false ], + 'page' => [ 'enabled' => false, 'in_iframe' => true ] + ]; + update_option( $this->test_option_key, $test_config ); + + $this->service = new Post_Settings_Service(); + $result = $this->service->get_post_type_config( 'post' ); + + + $this->assertEquals( [ 'enabled' => true, 'in_iframe' => false ], $result ); + } + + public function test_get_post_type_config_returns_null_when_not_exists(): void { + + $test_config = [ 'post' => [ 'enabled' => true ] ]; + update_option( $this->test_option_key, $test_config ); + + $this->service = new Post_Settings_Service(); + $result = $this->service->get_post_type_config( 'nonexistent_post_type' ); + + $this->assertNull( $result ); + } + + public function test_get_post_type_config_returns_null_when_no_settings(): void { + $this->service = new Post_Settings_Service(); + $result = $this->service->get_post_type_config( 'post' ); + + $this->assertNull( $result ); + } + + public function test_get_option_key_returns_filtered_value(): void { + $this->service = new Post_Settings_Service(); + $result = Post_Settings_Service::get_option_key(); + + $this->assertEquals( $this->test_option_key, $result ); + } + + public function test_get_settings_group_returns_filtered_value(): void { + $this->service = new Post_Settings_Service(); + $result = Post_Settings_Service::get_settings_group(); + + $this->assertEquals( $this->test_settings_group, $result ); + } + + public function test_constructor_loads_settings_from_cache_when_available(): void { + $cached_data = [ 'post' => [ 'enabled' => true, 'cached' => true ] ]; + wp_cache_set( $this->test_option_key, $cached_data, $this->test_settings_group ); + + $db_data = [ 'post' => [ 'enabled' => false, 'cached' => false ] ]; + update_option( $this->test_option_key, $db_data ); + + $this->service = new Post_Settings_Service(); + $result = $this->service->get_post_type_config( 'post' ); + + $this->assertEquals( [ 'enabled' => true, 'cached' => true ], $result ); + } + + public function test_constructor_loads_settings_from_database_when_cache_empty(): void { + $db_data = [ 'post' => [ 'enabled' => true, 'from_db' => true ] ]; + update_option( $this->test_option_key, $db_data ); + + wp_cache_delete( $this->test_option_key, $this->test_settings_group ); + + $this->service = new Post_Settings_Service(); + $result = $this->service->get_post_type_config( 'post' ); + + $this->assertEquals( [ 'enabled' => true, 'from_db' => true ], $result ); + } + + public function test_constructor_handles_non_array_cache_value(): void { + wp_cache_set( $this->test_option_key, 'not_an_array', $this->test_settings_group ); + $db_data = [ 'post' => [ 'enabled' => true ] ]; + update_option( $this->test_option_key, $db_data ); + + $this->service = new Post_Settings_Service(); + $result = $this->service->get_post_type_config( 'post' ); + + $this->assertEquals( [ 'enabled' => true ], $result ); + } + + public function test_constructor_handles_empty_database_option(): void { + delete_option( $this->test_option_key ); + wp_cache_delete( $this->test_option_key, $this->test_settings_group ); + + $this->service = new Post_Settings_Service(); + $result = $this->service->get_post_type_config( 'post' ); + + $this->assertNull( $result ); + } + + public function test_constructor_handles_non_array_database_option(): void { + update_option( $this->test_option_key, 'not_an_array' ); + wp_cache_delete( $this->test_option_key, $this->test_settings_group ); + + $this->service = new Post_Settings_Service(); + $result = $this->service->get_post_type_config( 'post' ); + + $this->assertNull( $result ); + } +} diff --git a/plugins/hwp-previews/tests/wpunit/Preview/Post/PostTypeServiceTest.php b/plugins/hwp-previews/tests/wpunit/Preview/Post/PostTypeServiceTest.php new file mode 100644 index 00000000..f6a89afc --- /dev/null +++ b/plugins/hwp-previews/tests/wpunit/Preview/Post/PostTypeServiceTest.php @@ -0,0 +1,222 @@ +post_id = $this->factory()->post->create( + [ + 'post_type' => 'post', + 'post_status' => 'publish', + 'post_title' => 'Test Post', + ] + ); + + $this->post = get_post( $this->post_id ); + + // Create mocks for dependencies + $this->post_preview_service_mock = $this->createMock( Post_Preview_Service::class ); + $this->post_settings_service_mock = $this->createMock( Post_Settings_Service::class ); + + $this->service = new Post_Type_Service( + $this->post, + $this->post_preview_service_mock, + $this->post_settings_service_mock + ); + } + + public function tearDown(): void { + remove_all_filters( 'hwp_previews_settings_group_option_key' ); + remove_all_filters( 'hwp_previews_settings_group_settings_group' ); + } + + public function test_is_allowed_for_previews_when_enabled_and_post_type_and_status_is_allowed(): void { + + $this->post_settings_service_mock + ->method( 'get_post_type_config' ) + ->willReturn( [ Settings_Field_Collection::ENABLED_FIELD_ID => true ] ); + + $this->post_preview_service_mock + ->method( 'get_post_types' ) + ->willReturn( [ 'post' => 'Posts' ] ); + + $this->post_preview_service_mock + ->method( 'get_post_statuses' ) + ->willReturn( [ 'publish', 'draft' ] ); + + $result = $this->service->is_allowed_for_previews(); + $this->assertTrue( $result ); + } + + public function test_is_not_allowed_for_previews_when_enabled_and_post_type_and_status_is_not_allowed(): void { + + $this->post_settings_service_mock + ->method( 'get_post_type_config' ) + ->willReturn( [ Settings_Field_Collection::ENABLED_FIELD_ID => true ] ); + + $this->post_preview_service_mock + ->method( 'get_post_types' ) + ->willReturn( [ 'post' => 'Posts' ] ); + + $this->post_preview_service_mock + ->method( 'get_post_statuses' ) + ->willReturn( [ 'publish', 'draft' ] ); + + $post = $this->post; + $post->post_type = 'draft'; + $draft_service = new Post_Type_Service( + $post, + $this->post_preview_service_mock, + $this->post_settings_service_mock + ); + + $result = $draft_service->is_allowed_for_previews(); + $this->assertFalse( $result ); + + + $post = $this->post; + $post->post_type = 'media'; + $custom_post_type_service = new Post_Type_Service( + $post, + $this->post_preview_service_mock, + $this->post_settings_service_mock + ); + + $result = $custom_post_type_service->is_allowed_for_previews(); + $this->assertFalse( $result ); + } + + public function test_is_allowed_for_previews_returns_false_when_not_enabled(): void { + $this->post_settings_service_mock + ->method( 'get_post_type_config' ) + ->willReturn( [ Settings_Field_Collection::ENABLED_FIELD_ID => false ] ); + + $result = $this->service->is_allowed_for_previews(); + + $this->assertFalse( $result ); + } + + + public function test_is_enabled_returns_false_when_config_not_array(): void { + $this->post_settings_service_mock + ->method( 'get_post_type_config' ) + ->willReturn( null ); + + $result = $this->service->is_enabled(); + $this->assertFalse( $result ); + } + + public function test_is_enabled_returns_false_when_enabled_key_missing(): void { + $this->post_settings_service_mock + ->method( 'get_post_type_config' ) + ->willReturn( [ 'other_setting' => true ] ); + + $result = $this->service->is_enabled(); + $this->assertFalse( $result ); + } + + public function test_is_allowed_post_type_returns_true_when_post_type_exists(): void { + $this->post_preview_service_mock + ->method( 'get_post_types' ) + ->willReturn( [ 'post' => 'Posts', 'page' => 'Pages' ] ); + + $result = $this->service->is_allowed_post_type(); + $this->assertTrue( $result ); + } + + public function test_is_allowed_post_type_returns_false_when_post_type_not_exists(): void { + $this->post_preview_service_mock + ->method( 'get_post_types' ) + ->willReturn( [ 'page' => 'Pages' ] ); + + $result = $this->service->is_allowed_post_type(); + $this->assertFalse( $result ); + } + + public function test_is_iframe_returns_true_when_enabled(): void { + $this->post_settings_service_mock + ->method( 'get_post_type_config' ) + ->willReturn( [ Settings_Field_Collection::IN_IFRAME_FIELD_ID => true ] ); + + $result = $this->service->is_iframe(); + $this->assertTrue( $result ); + } + + public function test_is_iframe_returns_false_when_disabled(): void { + $this->post_settings_service_mock + ->method( 'get_post_type_config' ) + ->willReturn( [ Settings_Field_Collection::IN_IFRAME_FIELD_ID => false ] ); + + $result = $this->service->is_iframe(); + $this->assertFalse( $result ); + } + + public function test_is_iframe_returns_false_when_config_not_array(): void { + $this->post_settings_service_mock + ->method( 'get_post_type_config' ) + ->willReturn( null ); + + $result = $this->service->is_iframe(); + $this->assertFalse( $result ); + } + + public function test_is_iframe_returns_false_when_iframe_key_missing(): void { + $this->post_settings_service_mock + ->method( 'get_post_type_config' ) + ->willReturn( [ Settings_Field_Collection::ENABLED_FIELD_ID => true ] ); + + $result = $this->service->is_iframe(); + $this->assertFalse( $result ); + } + + public function test_get_preview_url_returns_url_when_set(): void { + $expected_url = 'https://example.com/preview'; + $this->post_settings_service_mock + ->method('get_post_type_config') + ->willReturn([ Settings_Field_Collection::PREVIEW_URL_FIELD_ID => $expected_url ]); + + $result = $this->service->get_preview_url(); + $this->assertSame($expected_url, $result); + } + + public function test_get_preview_url_returns_null_when_config_not_array(): void { + $this->post_settings_service_mock + ->method('get_post_type_config') + ->willReturn(null); + + $result = $this->service->get_preview_url(); + $this->assertNull($result); + } + + public function test_get_preview_url_returns_null_when_field_missing(): void { + $this->post_settings_service_mock + ->method('get_post_type_config') + ->willReturn([ Settings_Field_Collection::ENABLED_FIELD_ID => true ]); + + $result = $this->service->get_preview_url(); + $this->assertNull($result); + } +} diff --git a/plugins/hwp-previews/tests/wpunit/Preview/Template/TemplateResolverServiceTest.php b/plugins/hwp-previews/tests/wpunit/Preview/Template/TemplateResolverServiceTest.php new file mode 100644 index 00000000..18835999 --- /dev/null +++ b/plugins/hwp-previews/tests/wpunit/Preview/Template/TemplateResolverServiceTest.php @@ -0,0 +1,175 @@ +get_iframe_template(); + + $this->assertEquals( $existing_file, $result ); + + // Cleanup + unlink( $existing_file ); + remove_all_filters( 'hwp_previews_template_path' ); + } + + /** + * Test get_iframe_template returns empty string when file does not exist. + */ + public function test_get_iframe_template_returns_empty_string_when_file_not_exists(): void { + $resolver = new Template_Resolver_Service(); + + // Mock the filter to return a non-existent file path + $non_existent_file = '/path/that/does/not/exist/iframe.php'; + + add_filter( 'hwp_previews_template_path', function () use ( $non_existent_file ) { + return $non_existent_file; + } ); + + $result = $resolver->get_iframe_template(); + + $this->assertEquals( '', $result ); + + // Cleanup + remove_all_filters( 'hwp_previews_template_path' ); + } + + /** + * Test get_iframe_template applies the hwp_previews_template_path filter. + */ + public function test_get_iframe_template_applies_filter(): void { + $resolver = new Template_Resolver_Service(); + + $custom_path = '/custom/template/path.php'; + $filter_called = false; + + add_filter( 'hwp_previews_template_path', function ( $path ) use ( $custom_path, &$filter_called ) { + $filter_called = true; + + return $custom_path; + } ); + + // Call the method (will return empty string since file doesn't exist) + $resolver->get_iframe_template(); + + $this->assertTrue( $filter_called ); + + // Cleanup + remove_all_filters( 'hwp_previews_template_path' ); + } + + /** + * Test set_query_variable sets the query variable correctly. + */ + public function test_set_query_variable_sets_query_var(): void { + $resolver = new Template_Resolver_Service(); + $test_url = 'https://example.com/preview'; + + $resolver->set_query_variable( $test_url ); + + $this->assertEquals( $test_url, get_query_var( Template_Resolver_Service::HWP_PREVIEWS_IFRAME_PREVIEW_URL ) ); + } + + /** + * Test set_query_variable with empty string. + */ + public function test_set_query_variable_with_empty_string(): void { + $resolver = new Template_Resolver_Service(); + + $resolver->set_query_variable( '' ); + + $this->assertEquals( '', get_query_var( Template_Resolver_Service::HWP_PREVIEWS_IFRAME_PREVIEW_URL ) ); + } + + /** + * Test get_query_variable returns the correct value. + */ + public function test_get_query_variable_returns_correct_value(): void { + $test_url = 'https://example.com/preview'; + + set_query_var( Template_Resolver_Service::HWP_PREVIEWS_IFRAME_PREVIEW_URL, $test_url ); + + $result = Template_Resolver_Service::get_query_variable(); + + $this->assertEquals( $test_url, $result ); + } + + /** + * Test get_query_variable returns empty string when not set. + */ + public function test_get_query_variable_returns_empty_string_when_not_set(): void { + // Ensure the query var is not set + set_query_var( Template_Resolver_Service::HWP_PREVIEWS_IFRAME_PREVIEW_URL, '' ); + + $result = Template_Resolver_Service::get_query_variable(); + + $this->assertEquals( '', $result ); + } + + /** + * Test get_query_variable is static and works without instance. + */ + public function test_get_query_variable_is_static(): void { + $test_url = 'https://example.com/static-test'; + + set_query_var( Template_Resolver_Service::HWP_PREVIEWS_IFRAME_PREVIEW_URL, $test_url ); + + // Call static method without creating an instance + $result = Template_Resolver_Service::get_query_variable(); + + $this->assertEquals( $test_url, $result ); + } + + /** + * Test constant is defined correctly. + */ + public function test_constant_is_defined_correctly(): void { + $this->assertEquals( 'hwp_previews_iframe_preview_url', Template_Resolver_Service::HWP_PREVIEWS_IFRAME_PREVIEW_URL ); + } + + /** + * Test integration: set and get query variable using the same constant. + */ + public function test_set_and_get_query_variable_integration(): void { + $resolver = new Template_Resolver_Service(); + $test_url = 'https://example.com/integration-test'; + + $resolver->set_query_variable( $test_url ); + $retrieved_url = Template_Resolver_Service::get_query_variable(); + + $this->assertEquals( $test_url, $retrieved_url ); + } +} diff --git a/plugins/hwp-previews/tests/wpunit/Preview/Url/PreviewUrlResolverServiceTest.php b/plugins/hwp-previews/tests/wpunit/Preview/Url/PreviewUrlResolverServiceTest.php new file mode 100644 index 00000000..49aed2ec --- /dev/null +++ b/plugins/hwp-previews/tests/wpunit/Preview/Url/PreviewUrlResolverServiceTest.php @@ -0,0 +1,160 @@ +assertInstanceOf(Preview_Url_Resolver_Service::class, $service); + } + + /** + * Main logic for preview URL resolution. + * + * @return void + */ + public function test_resolve_default_parameters(): void { + + $registry = Preview_Parameter_Registry::get_instance(); + $service = new Preview_Url_Resolver_Service($registry); + + $author = WPTestCase::factory()->user->create_and_get( [ + 'user_login' => 'test_author' + ]); + + $post = WPTestCase::factory()->post->create_and_get( [ + 'post_title' => 'Test Post', + 'post_status' => 'publish', + 'post_content' => 'This is a test post.', + 'post_type' => 'page', // Using this as it can be hierarchical + 'post_author' => $author->ID, + 'post_date' => '2023-10-01 12:00:00', + 'post_date_gmt' => '2023-10-01 12:00:00', + 'post_modified' => '2023-10-01 12:00:00', + ] ); + + $this->assertEquals( + $service->resolve($post, 'https://localhost:3000/preview={ID}' ), + 'https://localhost:3000/preview=' . $post->ID + ); + + $this->assertEquals( + $service->resolve($post, 'https://localhost:3000/preview={author_ID}' ), + 'https://localhost:3000/preview=' . $author->ID + ); + + $this->assertEquals( + $service->resolve($post, 'https://localhost:3000/preview={status}' ), + 'https://localhost:3000/preview=' . $post->post_status + ); + + $this->assertEquals( + $service->resolve($post, 'https://localhost:3000/preview={type}' ), + 'https://localhost:3000/preview=' . $post->post_type + ); + + $this->assertEquals( + $service->resolve($post, 'https://localhost:3000/preview={template}' ), + 'https://localhost:3000/preview=' . get_page_template_slug( $post ) + ); + + // Asserting the parent post ID is 0 as we do not set a parent post. + $this->assertEquals( + $service->resolve($post, 'https://localhost:3000/preview={parent_ID}' ), + 'https://localhost:3000/preview=0' + ); + + + $child_post = WPTestCase::factory()->post->create_and_get( [ + 'post_title' => 'Child Post', + 'post_status' => 'publish', + 'post_content' => 'This is a child post.', + 'post_type' => 'page', // Using this as it can be hierarchical + 'post_author' => $author->ID, + 'post_parent' => $post->ID, // Setting the parent post + 'post_date' => '2023-10-01 12:00:00', + 'post_date_gmt' => '2023-10-01 12:00:00', + 'post_modified' => '2023-10-01 12:00:00', + ] ); + + $this->assertEquals( + $service->resolve($child_post, 'https://localhost:3000/preview={parent_ID}' ), + 'https://localhost:3000/preview=' . $post->ID + ); + + } + + + public function test_custom_parameters_resolution() { + + $registry = Preview_Parameter_Registry::get_instance(); + $service = new Preview_Url_Resolver_Service($registry); + + $author = WPTestCase::factory()->user->create_and_get( [ + 'user_login' => 'test_author', + 'user_email' => uniqid( 'test_author', true ) . '@example.com' + ]); + + $post = WPTestCase::factory()->post->create_and_get( [ + 'post_title' => 'Test Post', + 'post_status' => 'publish', + 'post_content' => 'This is a test post.', + 'post_type' => 'page', // Using this as it can be hierarchical + 'post_author' => $author->ID, + 'post_date' => '2023-10-01 12:00:00', + 'post_date_gmt' => '2023-10-01 12:00:00', + 'post_modified' => '2023-10-01 12:00:00', + ] ); + + $registry->register(new Preview_Parameter('custom_param', static fn(WP_Post $post) => 'custom_value', 'A custom parameter for testing.')); + + $this->assertEquals( + $service->resolve($post, 'https://localhost:3000/preview={custom_param}' ), + 'https://localhost:3000/preview=custom_value' + ); + } + + public function test_custom_parameters_resolution_no_registered_class_returns_placeholder() { + + $registry = Preview_Parameter_Registry::get_instance(); + $service = new Preview_Url_Resolver_Service($registry); + + $author = WPTestCase::factory()->user->create_and_get( [ + 'user_login' => 'test_author', + 'user_email' => uniqid( 'test_author', true ) . '@example.com' + ]); + + $post = WPTestCase::factory()->post->create_and_get( [ + 'post_title' => 'Test Post', + 'post_status' => 'publish', + 'post_content' => 'This is a test post.', + 'post_type' => 'page', // Using this as it can be hierarchical + 'post_author' => $author->ID, + 'post_date' => '2023-10-01 12:00:00', + 'post_date_gmt' => '2023-10-01 12:00:00', + 'post_modified' => '2023-10-01 12:00:00', + ] ); + + // Ensure the custom parameter is not registered + $registry->unregister('custom_param'); + + $this->assertEquals( + $service->resolve($post, 'https://localhost:3000/preview={custom_param}' ), + 'https://localhost:3000/preview=' . $service::PLACEHOLDER_NOT_FOUND + ); + } +}