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)
+
+
+-----
+
+[]()
+[]()
+
+
+[](https://github.com/wpengine/hwptoolkit/actions?query=workflow%3A%22Testing+Integration%22)
+[](https://github.com/wpengine/hwptoolkit/actions?query=workflow%3A%22Code+Quality%22)
+[](https://github.com/wpengine/hwptoolkit/actions?query=workflow%3A%22End-to-End+Tests%22)
+-----
+
-[]() []()
> [!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
+
+
+*Preview settings page.*
+
+
+*Preview settings for a custom post type.*
+
+
+*Preview button in the WordPress editor.*
+
+
+*Preview loaded inside the WordPress editor using an iframe.*
+
+
+*Preview token parameter for secure preview URLs.*
+
+
+*App password setup for authentication.*
+
- -
-+ +
+
';
+ $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
+ );
+ }
+}