From af4bcd3198810eef4a255aab57f3092c8a1bdd1e Mon Sep 17 00:00:00 2001 From: David Stone Date: Fri, 27 Mar 2026 16:14:05 -0600 Subject: [PATCH] test(default-content): write unit tests for Default_Content_Installer - Add 42 tests covering all public/protected methods: - get_steps: structure, keys, count, checked flag, titles - done_creating_products/checkout_forms/emails/login_page: guard logic - ensure_currency_defaults: sets missing values, preserves existing, preserves zero-decimal precision, replaces empty-string precision - _install_create_products: creates 3 products with correct types/amounts - _install_create_checkout: creates form, registration page, block content - _install_create_login_page: creates page, sets settings, shortcode - _install_create_emails: delegates to Email_Manager - _install_create_template_site: multisite guard - all_done / handle: inherited Base_Installer methods - Fix sprintf ValueError in _install_create_checkout: escape literal % in column width values (25%%, 50%%) so sprintf only substitutes the checkout form slug placeholder (%s). PHP 8.x raises ValueError on unknown format specifiers like %". Closes #590 --- .../class-default-content-installer.php | 12 +- .../Default_Content_Installer_Test.php | 848 ++++++++++++++++++ 2 files changed, 854 insertions(+), 6 deletions(-) create mode 100644 tests/WP_Ultimo/Installers/Default_Content_Installer_Test.php diff --git a/inc/installers/class-default-content-installer.php b/inc/installers/class-default-content-installer.php index 3f6964e8b..504e67b73 100644 --- a/inc/installers/class-default-content-installer.php +++ b/inc/installers/class-default-content-installer.php @@ -414,16 +414,16 @@ public function _install_create_checkout(): void { * customise in the block editor without touching shortcode syntax. */ $post_content = ' -
-
+
+
- -
+ +
- -
+ +
'; diff --git a/tests/WP_Ultimo/Installers/Default_Content_Installer_Test.php b/tests/WP_Ultimo/Installers/Default_Content_Installer_Test.php new file mode 100644 index 000000000..55b0637b5 --- /dev/null +++ b/tests/WP_Ultimo/Installers/Default_Content_Installer_Test.php @@ -0,0 +1,848 @@ +installer = Default_Content_Installer::get_instance(); + } + + /** + * Tear down: clean up any products/checkout forms/emails created during tests. + */ + public function tearDown(): void { + // Clean up products created during tests. + $products = wu_get_products(); + foreach ( $products as $product ) { + if ( in_array( $product->get_slug(), [ 'free', 'premium', 'seo' ], true ) ) { + $product->delete(); + } + } + + // Clean up checkout forms. + $forms = wu_get_checkout_forms( [ 'slug' => 'main-form' ] ); + foreach ( $forms as $form ) { + $form->delete(); + } + + // Clean up pages created during tests. + $pages = get_posts( [ + 'post_type' => 'page', + 'post_status' => 'publish', + 'post_name__in' => [ 'register', 'login' ], + 'numberposts' => -1, + ] ); + foreach ( $pages as $page ) { + wp_delete_post( $page->ID, true ); + } + + // Reset settings touched by tests. + wu_save_setting( 'default_login_page', 0 ); + wu_save_setting( 'default_registration_page', 0 ); + wu_save_setting( 'enable_custom_login_page', 0 ); + + parent::tearDown(); + } + + // ----------------------------------------------------------------------- + // Singleton + // ----------------------------------------------------------------------- + + /** + * Test that get_instance returns a Default_Content_Installer. + */ + public function test_get_instance_returns_correct_type(): void { + $this->assertInstanceOf( Default_Content_Installer::class, $this->installer ); + } + + /** + * Test that get_instance returns the same object on repeated calls. + */ + public function test_get_instance_is_singleton(): void { + $a = Default_Content_Installer::get_instance(); + $b = Default_Content_Installer::get_instance(); + $this->assertSame( $a, $b ); + } + + // ----------------------------------------------------------------------- + // get_steps + // ----------------------------------------------------------------------- + + /** + * Test that get_steps returns an array with the expected keys. + */ + public function test_get_steps_returns_expected_keys(): void { + $steps = $this->installer->get_steps(); + + $this->assertIsArray( $steps ); + $this->assertArrayHasKey( 'create_template_site', $steps ); + $this->assertArrayHasKey( 'create_products', $steps ); + $this->assertArrayHasKey( 'create_checkout', $steps ); + $this->assertArrayHasKey( 'create_emails', $steps ); + $this->assertArrayHasKey( 'create_login_page', $steps ); + } + + /** + * Test that each step has the required fields. + */ + public function test_get_steps_each_step_has_required_fields(): void { + $steps = $this->installer->get_steps(); + + $required_fields = [ 'done', 'title', 'description', 'pending', 'installing', 'success', 'help', 'checked' ]; + + foreach ( $steps as $key => $step ) { + foreach ( $required_fields as $field ) { + $this->assertArrayHasKey( + $field, + $step, + "Step '{$key}' is missing field '{$field}'" + ); + } + } + } + + /** + * Test that get_steps returns exactly 5 steps. + */ + public function test_get_steps_returns_five_steps(): void { + $steps = $this->installer->get_steps(); + $this->assertCount( 5, $steps ); + } + + /** + * Test that each step's 'checked' field is true by default. + */ + public function test_get_steps_checked_is_true(): void { + $steps = $this->installer->get_steps(); + + foreach ( $steps as $key => $step ) { + $this->assertTrue( $step['checked'], "Step '{$key}' should have checked=true" ); + } + } + + /** + * Test that step titles are non-empty strings. + */ + public function test_get_steps_titles_are_non_empty(): void { + $steps = $this->installer->get_steps(); + + foreach ( $steps as $key => $step ) { + $this->assertIsString( $step['title'], "Step '{$key}' title should be a string" ); + $this->assertNotEmpty( $step['title'], "Step '{$key}' title should not be empty" ); + } + } + + // ----------------------------------------------------------------------- + // done_creating_products + // ----------------------------------------------------------------------- + + /** + * Test done_creating_products returns false when no products exist. + */ + public function test_done_creating_products_false_when_empty(): void { + // Ensure no plans exist. + $plans = wu_get_plans(); + foreach ( $plans as $plan ) { + $plan->delete(); + } + + $ref = new \ReflectionClass( $this->installer ); + $method = $ref->getMethod( 'done_creating_products' ); + $method->setAccessible( true ); + + $result = $method->invoke( $this->installer ); + $this->assertFalse( $result ); + } + + /** + * Test done_creating_products returns true when plans exist. + */ + public function test_done_creating_products_true_when_plans_exist(): void { + // Create a plan. + $product = wu_create_product( [ + 'name' => 'Test Plan', + 'slug' => 'test-plan-done-check', + 'type' => 'plan', + 'pricing_type' => 'free', + 'amount' => 0, + 'currency' => 'USD', + 'active' => 1, + ] ); + + $this->assertNotWPError( $product ); + + $ref = new \ReflectionClass( $this->installer ); + $method = $ref->getMethod( 'done_creating_products' ); + $method->setAccessible( true ); + + $result = $method->invoke( $this->installer ); + $this->assertTrue( $result ); + + // Cleanup. + $product->delete(); + } + + // ----------------------------------------------------------------------- + // done_creating_checkout_forms + // ----------------------------------------------------------------------- + + /** + * Test done_creating_checkout_forms returns false when no user-created forms exist. + * + * Note: wu_get_checkout_forms() may return built-in forms (wu-checkout etc.) + * so we test the method's return value reflects the actual state. + */ + public function test_done_creating_checkout_forms_returns_bool(): void { + $ref = new \ReflectionClass( $this->installer ); + $method = $ref->getMethod( 'done_creating_checkout_forms' ); + $method->setAccessible( true ); + + $result = $method->invoke( $this->installer ); + $this->assertIsBool( $result ); + } + + /** + * Test done_creating_checkout_forms returns true after creating a form. + */ + public function test_done_creating_checkout_forms_true_after_create(): void { + $form = wu_create_checkout_form( [ + 'name' => 'Test Form', + 'slug' => 'test-form-done-check', + ] ); + + $this->assertNotWPError( $form ); + + $ref = new \ReflectionClass( $this->installer ); + $method = $ref->getMethod( 'done_creating_checkout_forms' ); + $method->setAccessible( true ); + + $result = $method->invoke( $this->installer ); + $this->assertTrue( $result ); + + // Cleanup. + $form->delete(); + } + + // ----------------------------------------------------------------------- + // done_creating_emails + // ----------------------------------------------------------------------- + + /** + * Test done_creating_emails returns a boolean. + */ + public function test_done_creating_emails_returns_bool(): void { + $ref = new \ReflectionClass( $this->installer ); + $method = $ref->getMethod( 'done_creating_emails' ); + $method->setAccessible( true ); + + $result = $method->invoke( $this->installer ); + $this->assertIsBool( $result ); + } + + // ----------------------------------------------------------------------- + // done_creating_login_page + // ----------------------------------------------------------------------- + + /** + * Test done_creating_login_page returns false when no page ID is set. + */ + public function test_done_creating_login_page_false_when_no_setting(): void { + wu_save_setting( 'default_login_page', 0 ); + + $ref = new \ReflectionClass( $this->installer ); + $method = $ref->getMethod( 'done_creating_login_page' ); + $method->setAccessible( true ); + + $result = $method->invoke( $this->installer ); + $this->assertFalse( $result ); + } + + /** + * Test done_creating_login_page returns false when page ID points to non-existent post. + */ + public function test_done_creating_login_page_false_when_page_missing(): void { + wu_save_setting( 'default_login_page', 999999 ); + + $ref = new \ReflectionClass( $this->installer ); + $method = $ref->getMethod( 'done_creating_login_page' ); + $method->setAccessible( true ); + + $result = $method->invoke( $this->installer ); + $this->assertFalse( $result ); + + wu_save_setting( 'default_login_page', 0 ); + } + + /** + * Test done_creating_login_page returns true when page exists. + */ + public function test_done_creating_login_page_true_when_page_exists(): void { + $page_id = wp_insert_post( [ + 'post_title' => 'Test Login Page', + 'post_status' => 'publish', + 'post_type' => 'page', + ] ); + + wu_save_setting( 'default_login_page', $page_id ); + + $ref = new \ReflectionClass( $this->installer ); + $method = $ref->getMethod( 'done_creating_login_page' ); + $method->setAccessible( true ); + + $result = $method->invoke( $this->installer ); + $this->assertTrue( $result ); + + // Cleanup. + wp_delete_post( $page_id, true ); + wu_save_setting( 'default_login_page', 0 ); + } + + // ----------------------------------------------------------------------- + // ensure_currency_defaults + // ----------------------------------------------------------------------- + + /** + * Test ensure_currency_defaults sets defaults when settings are missing. + */ + public function test_ensure_currency_defaults_sets_missing_values(): void { + // Clear the currency settings. + wu_save_setting( 'currency_symbol', false ); + wu_save_setting( 'currency_position', false ); + wu_save_setting( 'decimal_separator', false ); + wu_save_setting( 'thousand_separator', false ); + wu_save_setting( 'precision', false ); + + $ref = new \ReflectionClass( $this->installer ); + $method = $ref->getMethod( 'ensure_currency_defaults' ); + $method->setAccessible( true ); + $method->invoke( $this->installer ); + + $this->assertSame( 'USD', wu_get_setting( 'currency_symbol' ) ); + $this->assertSame( '%s %v', wu_get_setting( 'currency_position' ) ); + $this->assertSame( '.', wu_get_setting( 'decimal_separator' ) ); + $this->assertSame( ',', wu_get_setting( 'thousand_separator' ) ); + $this->assertSame( 2, wu_get_setting( 'precision' ) ); + } + + /** + * Test ensure_currency_defaults does not overwrite existing values. + */ + public function test_ensure_currency_defaults_does_not_overwrite_existing(): void { + wu_save_setting( 'currency_symbol', 'EUR' ); + wu_save_setting( 'precision', 3 ); + + $ref = new \ReflectionClass( $this->installer ); + $method = $ref->getMethod( 'ensure_currency_defaults' ); + $method->setAccessible( true ); + $method->invoke( $this->installer ); + + $this->assertSame( 'EUR', wu_get_setting( 'currency_symbol' ) ); + $this->assertSame( 3, wu_get_setting( 'precision' ) ); + } + + /** + * Test ensure_currency_defaults preserves precision=0 (zero-decimal currencies). + */ + public function test_ensure_currency_defaults_preserves_zero_precision(): void { + wu_save_setting( 'precision', 0 ); + + $ref = new \ReflectionClass( $this->installer ); + $method = $ref->getMethod( 'ensure_currency_defaults' ); + $method->setAccessible( true ); + $method->invoke( $this->installer ); + + // 0 is a valid stored value and must NOT be overwritten with 2. + $this->assertSame( 0, wu_get_setting( 'precision' ) ); + } + + /** + * Test ensure_currency_defaults replaces empty-string precision with default. + */ + public function test_ensure_currency_defaults_replaces_empty_string_precision(): void { + wu_save_setting( 'precision', '' ); + + $ref = new \ReflectionClass( $this->installer ); + $method = $ref->getMethod( 'ensure_currency_defaults' ); + $method->setAccessible( true ); + $method->invoke( $this->installer ); + + $this->assertSame( 2, wu_get_setting( 'precision' ) ); + } + + // ----------------------------------------------------------------------- + // _install_create_products + // ----------------------------------------------------------------------- + + /** + * Test _install_create_products creates three products. + */ + public function test_install_create_products_creates_three_products(): void { + // Ensure no conflicting products exist. + foreach ( [ 'free', 'premium', 'seo' ] as $slug ) { + $existing = wu_get_product_by_slug( $slug ); + if ( $existing ) { + $existing->delete(); + } + } + + // Ensure currency defaults are set so validation passes. + wu_save_setting( 'currency_symbol', 'USD' ); + wu_save_setting( 'precision', 2 ); + + $this->installer->_install_create_products(); + + $free = wu_get_product_by_slug( 'free' ); + $premium = wu_get_product_by_slug( 'premium' ); + $seo = wu_get_product_by_slug( 'seo' ); + + $this->assertNotFalse( $free, 'Free plan should have been created' ); + $this->assertNotFalse( $premium, 'Premium plan should have been created' ); + $this->assertNotFalse( $seo, 'SEO service should have been created' ); + } + + /** + * Test _install_create_products sets correct product types. + */ + public function test_install_create_products_correct_types(): void { + foreach ( [ 'free', 'premium', 'seo' ] as $slug ) { + $existing = wu_get_product_by_slug( $slug ); + if ( $existing ) { + $existing->delete(); + } + } + + wu_save_setting( 'currency_symbol', 'USD' ); + wu_save_setting( 'precision', 2 ); + + $this->installer->_install_create_products(); + + $free = wu_get_product_by_slug( 'free' ); + $premium = wu_get_product_by_slug( 'premium' ); + $seo = wu_get_product_by_slug( 'seo' ); + + $this->assertSame( 'plan', $free->get_type() ); + $this->assertSame( 'plan', $premium->get_type() ); + $this->assertSame( 'service', $seo->get_type() ); + } + + /** + * Test _install_create_products sets correct pricing types. + */ + public function test_install_create_products_correct_pricing_types(): void { + foreach ( [ 'free', 'premium', 'seo' ] as $slug ) { + $existing = wu_get_product_by_slug( $slug ); + if ( $existing ) { + $existing->delete(); + } + } + + wu_save_setting( 'currency_symbol', 'USD' ); + wu_save_setting( 'precision', 2 ); + + $this->installer->_install_create_products(); + + $free = wu_get_product_by_slug( 'free' ); + $premium = wu_get_product_by_slug( 'premium' ); + + $this->assertSame( 'free', $free->get_pricing_type() ); + $this->assertSame( 'paid', $premium->get_pricing_type() ); + } + + /** + * Test _install_create_products sets correct amounts. + */ + public function test_install_create_products_correct_amounts(): void { + foreach ( [ 'free', 'premium', 'seo' ] as $slug ) { + $existing = wu_get_product_by_slug( $slug ); + if ( $existing ) { + $existing->delete(); + } + } + + wu_save_setting( 'currency_symbol', 'USD' ); + wu_save_setting( 'precision', 2 ); + + $this->installer->_install_create_products(); + + $free = wu_get_product_by_slug( 'free' ); + $premium = wu_get_product_by_slug( 'premium' ); + $seo = wu_get_product_by_slug( 'seo' ); + + $this->assertSame( 0.0, (float) $free->get_amount() ); + $this->assertSame( 29.99, (float) $premium->get_amount() ); + $this->assertSame( 9.99, (float) $seo->get_amount() ); + } + + /** + * Test _install_create_products calls ensure_currency_defaults. + * + * Verifies that after calling _install_create_products with empty precision, + * the precision is set to the default (2). + */ + public function test_install_create_products_ensures_currency_defaults(): void { + foreach ( [ 'free', 'premium', 'seo' ] as $slug ) { + $existing = wu_get_product_by_slug( $slug ); + if ( $existing ) { + $existing->delete(); + } + } + + // Set precision to empty string to simulate the bug scenario. + wu_save_setting( 'precision', '' ); + wu_save_setting( 'currency_symbol', 'USD' ); + + $this->installer->_install_create_products(); + + // After the call, precision should be set to 2. + $this->assertSame( 2, wu_get_setting( 'precision' ) ); + } + + // ----------------------------------------------------------------------- + // _install_create_checkout + // ----------------------------------------------------------------------- + + /** + * Test _install_create_checkout creates a checkout form with slug 'main-form'. + */ + public function test_install_create_checkout_creates_form(): void { + // Ensure no existing main-form. + $existing = wu_get_checkout_form_by_slug( 'main-form' ); + if ( $existing ) { + $existing->delete(); + } + + wu_save_setting( 'currency_symbol', 'USD' ); + wu_save_setting( 'precision', 2 ); + + $this->installer->_install_create_checkout(); + + $form = wu_get_checkout_form_by_slug( 'main-form' ); + $this->assertNotFalse( $form, 'main-form checkout form should have been created' ); + $this->assertInstanceOf( \WP_Ultimo\Models\Checkout_Form::class, $form ); + } + + /** + * Test _install_create_checkout creates a registration page. + */ + public function test_install_create_checkout_creates_registration_page(): void { + $existing = wu_get_checkout_form_by_slug( 'main-form' ); + if ( $existing ) { + $existing->delete(); + } + + wu_save_setting( 'currency_symbol', 'USD' ); + wu_save_setting( 'precision', 2 ); + + $this->installer->_install_create_checkout(); + + $page_id = wu_get_setting( 'default_registration_page', 0 ); + $this->assertGreaterThan( 0, $page_id, 'default_registration_page setting should be set' ); + + $page = get_post( $page_id ); + $this->assertNotNull( $page, 'Registration page post should exist' ); + $this->assertSame( 'publish', $page->post_status ); + } + + /** + * Test _install_create_checkout sets default_registration_page setting. + */ + public function test_install_create_checkout_sets_registration_page_setting(): void { + $existing = wu_get_checkout_form_by_slug( 'main-form' ); + if ( $existing ) { + $existing->delete(); + } + + wu_save_setting( 'currency_symbol', 'USD' ); + wu_save_setting( 'precision', 2 ); + wu_save_setting( 'default_registration_page', 0 ); + + $this->installer->_install_create_checkout(); + + $page_id = wu_get_setting( 'default_registration_page', 0 ); + $this->assertGreaterThan( 0, $page_id ); + } + + /** + * Test _install_create_checkout page content includes checkout block. + */ + public function test_install_create_checkout_page_has_checkout_block(): void { + $existing = wu_get_checkout_form_by_slug( 'main-form' ); + if ( $existing ) { + $existing->delete(); + } + + wu_save_setting( 'currency_symbol', 'USD' ); + wu_save_setting( 'precision', 2 ); + + $this->installer->_install_create_checkout(); + + $page_id = wu_get_setting( 'default_registration_page', 0 ); + $page = get_post( $page_id ); + + $this->assertStringContainsString( + 'wp-ultimo/checkout', + $page->post_content, + 'Registration page should contain the checkout block' + ); + } + + /** + * Test _install_create_checkout calls ensure_currency_defaults. + */ + public function test_install_create_checkout_ensures_currency_defaults(): void { + $existing = wu_get_checkout_form_by_slug( 'main-form' ); + if ( $existing ) { + $existing->delete(); + } + + wu_save_setting( 'precision', '' ); + wu_save_setting( 'currency_symbol', 'USD' ); + + $this->installer->_install_create_checkout(); + + $this->assertSame( 2, wu_get_setting( 'precision' ) ); + } + + // ----------------------------------------------------------------------- + // _install_create_login_page + // ----------------------------------------------------------------------- + + /** + * Test _install_create_login_page creates a page. + */ + public function test_install_create_login_page_creates_page(): void { + $this->installer->_install_create_login_page(); + + $page_id = wu_get_setting( 'default_login_page', 0 ); + $this->assertGreaterThan( 0, $page_id, 'default_login_page setting should be set' ); + + $page = get_post( $page_id ); + $this->assertNotNull( $page ); + $this->assertSame( 'publish', $page->post_status ); + } + + /** + * Test _install_create_login_page sets enable_custom_login_page setting. + */ + public function test_install_create_login_page_enables_custom_login(): void { + wu_save_setting( 'enable_custom_login_page', 0 ); + + $this->installer->_install_create_login_page(); + + $this->assertSame( 1, wu_get_setting( 'enable_custom_login_page' ) ); + } + + /** + * Test _install_create_login_page sets default_login_page setting. + */ + public function test_install_create_login_page_sets_page_setting(): void { + wu_save_setting( 'default_login_page', 0 ); + + $this->installer->_install_create_login_page(); + + $page_id = wu_get_setting( 'default_login_page', 0 ); + $this->assertGreaterThan( 0, $page_id ); + } + + /** + * Test _install_create_login_page page contains login shortcode. + */ + public function test_install_create_login_page_has_login_shortcode(): void { + $this->installer->_install_create_login_page(); + + $page_id = wu_get_setting( 'default_login_page', 0 ); + $page = get_post( $page_id ); + + $this->assertStringContainsString( + 'wu_login_form', + $page->post_content, + 'Login page should contain the wu_login_form shortcode' + ); + } + + /** + * Test done_creating_login_page returns true after _install_create_login_page. + */ + public function test_done_creating_login_page_true_after_install(): void { + $this->installer->_install_create_login_page(); + + $ref = new \ReflectionClass( $this->installer ); + $method = $ref->getMethod( 'done_creating_login_page' ); + $method->setAccessible( true ); + + $result = $method->invoke( $this->installer ); + $this->assertTrue( $result ); + } + + // ----------------------------------------------------------------------- + // _install_create_emails + // ----------------------------------------------------------------------- + + /** + * Test _install_create_emails delegates to Email_Manager. + * + * Verifies that calling _install_create_emails results in system emails + * being present (Email_Manager::create_all_system_emails is called). + */ + public function test_install_create_emails_creates_system_emails(): void { + // Delete any existing system emails. + $existing = wu_get_all_system_emails(); + foreach ( $existing as $email ) { + $email->delete(); + } + + $this->installer->_install_create_emails(); + + $emails = wu_get_all_system_emails(); + $this->assertNotEmpty( $emails, 'System emails should have been created' ); + } + + // ----------------------------------------------------------------------- + // _install_create_template_site (multisite only) + // ----------------------------------------------------------------------- + + /** + * Test _install_create_template_site creates a site on multisite. + * + * @group ms-required + */ + public function test_install_create_template_site_creates_site(): void { + if ( ! is_multisite() ) { + $this->markTestSkipped( 'Requires multisite.' ); + } + + // Check if template site already exists and skip if so. + $ref = new \ReflectionClass( $this->installer ); + $method = $ref->getMethod( 'done_creating_template_site' ); + $method->setAccessible( true ); + + if ( $method->invoke( $this->installer ) ) { + $this->markTestSkipped( 'Template site already exists.' ); + } + + $this->installer->_install_create_template_site(); + + // After creation, done_creating_template_site should return a truthy value + // (domain_exists returns int blog_id or false). + $result = $method->invoke( $this->installer ); + $this->assertNotEmpty( $result, 'Template site should exist after creation' ); + } + + /** + * Test done_creating_template_site returns false on single-site. + */ + public function test_done_creating_template_site_false_on_single_site(): void { + if ( is_multisite() ) { + $this->markTestSkipped( 'This test is for single-site only.' ); + } + + $ref = new \ReflectionClass( $this->installer ); + $method = $ref->getMethod( 'done_creating_template_site' ); + $method->setAccessible( true ); + + $result = $method->invoke( $this->installer ); + $this->assertFalse( $result ); + } + + /** + * Test done_creating_template_site runs without exception on multisite. + * + * domain_exists() returns int|false|null depending on WordPress version. + * We verify the method completes without throwing. + * + * @group ms-required + */ + public function test_done_creating_template_site_runs_without_exception_on_multisite(): void { + if ( ! is_multisite() ) { + $this->markTestSkipped( 'Requires multisite.' ); + } + + $ref = new \ReflectionClass( $this->installer ); + $method = $ref->getMethod( 'done_creating_template_site' ); + $method->setAccessible( true ); + + // Should not throw — just verify it completes. + $result = $method->invoke( $this->installer ); + + // The result is truthy when the template site exists, falsy otherwise. + $this->assertNotInstanceOf( \WP_Error::class, $result ); + } + + // ----------------------------------------------------------------------- + // all_done (inherited from Base_Installer) + // ----------------------------------------------------------------------- + + /** + * Test all_done returns false when at least one step is not done. + */ + public function test_all_done_returns_false_when_steps_pending(): void { + // Ensure at least one step is not done (no login page set). + wu_save_setting( 'default_login_page', 0 ); + + $result = $this->installer->all_done(); + $this->assertFalse( $result ); + } + + /** + * Test all_done returns a boolean. + */ + public function test_all_done_returns_bool(): void { + $result = $this->installer->all_done(); + $this->assertIsBool( $result ); + } + + // ----------------------------------------------------------------------- + // handle (inherited from Base_Installer) + // ----------------------------------------------------------------------- + + /** + * Test handle returns WP_Error when installer throws an exception. + */ + public function test_handle_returns_wp_error_on_exception(): void { + // Use a mock installer name that has no corresponding _install_ method. + // Base_Installer::handle returns $status unchanged when no callable found. + $result = $this->installer->handle( true, 'nonexistent_step', null ); + $this->assertTrue( $result, 'handle should return original status when no callable found' ); + } + + /** + * Test handle returns original status when no installer method exists. + */ + public function test_handle_returns_status_when_no_callable(): void { + $result = $this->installer->handle( 'original_status', 'no_such_installer', null ); + $this->assertSame( 'original_status', $result ); + } +}