From 3453e686ca3a189e94425491e927e888cd87c773 Mon Sep 17 00:00:00 2001 From: David Stone Date: Sat, 28 Mar 2026 10:44:47 -0600 Subject: [PATCH 1/2] test(admin): add unit tests for View_Logs_Admin_Page and Tax_Rates_Admin_Page Closes #678 - View_Logs_Admin_Page_Test: 32 tests covering page properties, get_title, get_menu_title, get_labels, get_object, init hook registration, handle_view_logs (return structure, security guard, file contents, default content), handle_save guard conditions, page_loaded, register_scripts, and output_default_widget_payload - Tax_Rates_Admin_Page_Test: 26 tests covering page properties, get_title, get_menu_title, get_submenu_title, output (action/filter firing, column keys), register_scripts (registration + enqueue), add_fields_widget (meta box registration, default position), Tax integration (get_tax_rate_types, filter extensibility), and instantiation --- .../Admin_Pages/Tax_Rates_Admin_Page_Test.php | 386 ++++++++++++++++ .../Admin_Pages/View_Logs_Admin_Page_Test.php | 412 ++++++++++++++++++ 2 files changed, 798 insertions(+) create mode 100644 tests/WP_Ultimo/Admin_Pages/Tax_Rates_Admin_Page_Test.php create mode 100644 tests/WP_Ultimo/Admin_Pages/View_Logs_Admin_Page_Test.php diff --git a/tests/WP_Ultimo/Admin_Pages/Tax_Rates_Admin_Page_Test.php b/tests/WP_Ultimo/Admin_Pages/Tax_Rates_Admin_Page_Test.php new file mode 100644 index 000000000..eb6233742 --- /dev/null +++ b/tests/WP_Ultimo/Admin_Pages/Tax_Rates_Admin_Page_Test.php @@ -0,0 +1,386 @@ +=50% coverage. + * Methods that call wp_die(), send headers, or require HTTP context are tested + * for their guard conditions and side-effects only. + */ +class Tax_Rates_Admin_Page_Test extends WP_UnitTestCase { + + /** + * @var Tax_Rates_Admin_Page + */ + private $page; + + /** + * Set up test fixtures. + */ + protected function setUp(): void { + + parent::setUp(); + $this->page = new Tax_Rates_Admin_Page(); + } + + /** + * Tear down: clean up any registered actions/filters. + */ + protected function tearDown(): void { + + parent::tearDown(); + } + + // ------------------------------------------------------------------------- + // Page properties + // ------------------------------------------------------------------------- + + public function test_page_id(): void { + + $reflection = new \ReflectionClass($this->page); + $property = $reflection->getProperty('id'); + $property->setAccessible(true); + + $this->assertEquals('wp-ultimo-tax-rates', $property->getValue($this->page)); + } + + public function test_page_type(): void { + + $reflection = new \ReflectionClass($this->page); + $property = $reflection->getProperty('type'); + $property->setAccessible(true); + + $this->assertEquals('submenu', $property->getValue($this->page)); + } + + public function test_parent_is_none(): void { + + $reflection = new \ReflectionClass($this->page); + $property = $reflection->getProperty('parent'); + $property->setAccessible(true); + + $this->assertEquals('none', $property->getValue($this->page)); + } + + public function test_highlight_menu_slug(): void { + + $reflection = new \ReflectionClass($this->page); + $property = $reflection->getProperty('highlight_menu_slug'); + $property->setAccessible(true); + + $this->assertEquals('wp-ultimo-settings', $property->getValue($this->page)); + } + + public function test_supported_panels(): void { + + $reflection = new \ReflectionClass($this->page); + $property = $reflection->getProperty('supported_panels'); + $property->setAccessible(true); + + $panels = $property->getValue($this->page); + $this->assertArrayHasKey('network_admin_menu', $panels); + $this->assertEquals('manage_network', $panels['network_admin_menu']); + } + + // ------------------------------------------------------------------------- + // get_title() / get_menu_title() / get_submenu_title() + // ------------------------------------------------------------------------- + + public function test_get_title(): void { + + $this->assertEquals('Tax Rates', $this->page->get_title()); + } + + public function test_get_menu_title(): void { + + $this->assertEquals('Tax Rates', $this->page->get_menu_title()); + } + + public function test_get_submenu_title(): void { + + $this->assertEquals('Tax Rates', $this->page->get_submenu_title()); + } + + // ------------------------------------------------------------------------- + // output() + // ------------------------------------------------------------------------- + + public function test_output_method_exists(): void { + + $this->assertTrue(method_exists($this->page, 'output')); + $this->assertTrue(is_callable([$this->page, 'output'])); + } + + /** + * output() fires the wu_load_tax_rates_list_page action. + */ + public function test_output_fires_action(): void { + + $action_fired = false; + $callback = function () use (&$action_fired) { + $action_fired = true; + }; + + add_action('wu_load_tax_rates_list_page', $callback); + + set_current_screen('dashboard-network'); + + ob_start(); + $this->page->output(); + ob_get_clean(); + + remove_action('wu_load_tax_rates_list_page', $callback); + + $this->assertTrue($action_fired); + } + + /** + * output() applies the wu_tax_rates_columns filter. + */ + public function test_output_applies_columns_filter(): void { + + $filter_called = false; + $callback = function ($columns) use (&$filter_called) { + $filter_called = true; + return $columns; + }; + + add_filter('wu_tax_rates_columns', $callback); + + set_current_screen('dashboard-network'); + + ob_start(); + $this->page->output(); + ob_get_clean(); + + remove_filter('wu_tax_rates_columns', $callback); + + $this->assertTrue($filter_called); + } + + /** + * output() columns filter receives an array with the expected keys. + */ + public function test_output_columns_filter_receives_expected_keys(): void { + + $received_columns = null; + $callback = function ($columns) use (&$received_columns) { + $received_columns = $columns; + return $columns; + }; + + add_filter('wu_tax_rates_columns', $callback); + + set_current_screen('dashboard-network'); + + ob_start(); + $this->page->output(); + ob_get_clean(); + + remove_filter('wu_tax_rates_columns', $callback); + + $this->assertIsArray($received_columns); + $this->assertArrayHasKey('title', $received_columns); + $this->assertArrayHasKey('country', $received_columns); + $this->assertArrayHasKey('state', $received_columns); + $this->assertArrayHasKey('city', $received_columns); + $this->assertArrayHasKey('tax_rate', $received_columns); + $this->assertArrayHasKey('move', $received_columns); + } + + /** + * output() columns filter can modify the columns array. + */ + public function test_output_columns_filter_can_modify_columns(): void { + + $callback = function ($columns) { + $columns['custom_col'] = 'Custom Column'; + return $columns; + }; + + add_filter('wu_tax_rates_columns', $callback); + + set_current_screen('dashboard-network'); + + ob_start(); + $this->page->output(); + $output = ob_get_clean(); + + remove_filter('wu_tax_rates_columns', $callback); + + // The filter was applied — we just verify no exception was thrown. + $this->assertTrue(true); + } + + // ------------------------------------------------------------------------- + // register_scripts() + // ------------------------------------------------------------------------- + + public function test_register_scripts_does_not_throw(): void { + + $this->page->register_scripts(); + + $this->assertTrue(true); + } + + public function test_register_scripts_registers_wu_tax_rates_script(): void { + + $this->page->register_scripts(); + + $this->assertTrue(wp_script_is('wu-tax-rates', 'registered')); + } + + public function test_register_scripts_enqueues_wu_tax_rates_script(): void { + + $this->page->register_scripts(); + + $this->assertTrue(wp_script_is('wu-tax-rates', 'enqueued')); + } + + // ------------------------------------------------------------------------- + // add_fields_widget() — protected method via reflection + // ------------------------------------------------------------------------- + + public function test_add_fields_widget_is_callable_via_reflection(): void { + + $reflection = new \ReflectionClass($this->page); + $method = $reflection->getMethod('add_fields_widget'); + $method->setAccessible(true); + + $this->assertTrue($method->isProtected()); + } + + /** + * add_fields_widget() registers a meta box with the given ID. + */ + public function test_add_fields_widget_registers_meta_box(): void { + + set_current_screen('dashboard-network'); + + $reflection = new \ReflectionClass($this->page); + $method = $reflection->getMethod('add_fields_widget'); + $method->setAccessible(true); + + $method->invoke( + $this->page, + 'test-widget', + [ + 'title' => 'Test Widget', + 'fields' => [], + 'screen' => get_current_screen(), + ] + ); + + global $wp_meta_boxes; + $screen_id = get_current_screen()->id; + + // Meta box should be registered under the screen ID. + $this->assertArrayHasKey($screen_id, $wp_meta_boxes); + } + + /** + * add_fields_widget() uses 'side' as the default position. + */ + public function test_add_fields_widget_default_position_is_side(): void { + + set_current_screen('dashboard-network'); + + $reflection = new \ReflectionClass($this->page); + $method = $reflection->getMethod('add_fields_widget'); + $method->setAccessible(true); + + $method->invoke( + $this->page, + 'test-side-widget', + [ + 'title' => 'Side Widget', + 'fields' => [], + 'screen' => get_current_screen(), + ] + ); + + global $wp_meta_boxes; + $screen_id = get_current_screen()->id; + + // Default position is 'side'. + $this->assertArrayHasKey('side', $wp_meta_boxes[$screen_id]); + } + + // ------------------------------------------------------------------------- + // Tax integration — Tax::get_instance()->get_tax_rate_types() + // ------------------------------------------------------------------------- + + public function test_tax_get_instance_returns_tax_object(): void { + + $tax = Tax::get_instance(); + + $this->assertInstanceOf(Tax::class, $tax); + } + + public function test_tax_get_tax_rate_types_returns_array(): void { + + $types = Tax::get_instance()->get_tax_rate_types(); + + $this->assertIsArray($types); + } + + public function test_tax_get_tax_rate_types_has_regular_key(): void { + + $types = Tax::get_instance()->get_tax_rate_types(); + + $this->assertArrayHasKey('regular', $types); + } + + public function test_tax_get_tax_rate_types_regular_value_is_string(): void { + + $types = Tax::get_instance()->get_tax_rate_types(); + + $this->assertIsString($types['regular']); + } + + /** + * The wu_get_tax_rate_types filter can add new types. + */ + public function test_tax_get_tax_rate_types_filter_can_add_types(): void { + + $callback = function ($types) { + $types['custom'] = 'Custom Type'; + return $types; + }; + + add_filter('wu_get_tax_rate_types', $callback); + + $types = Tax::get_instance()->get_tax_rate_types(); + + remove_filter('wu_get_tax_rate_types', $callback); + + $this->assertArrayHasKey('custom', $types); + $this->assertEquals('Custom Type', $types['custom']); + } + + // ------------------------------------------------------------------------- + // Instantiation + // ------------------------------------------------------------------------- + + public function test_instantiation_creates_object(): void { + + $page = new Tax_Rates_Admin_Page(); + + $this->assertInstanceOf(Tax_Rates_Admin_Page::class, $page); + } + + public function test_page_extends_base_admin_page(): void { + + $this->assertInstanceOf(Base_Admin_Page::class, $this->page); + } +} diff --git a/tests/WP_Ultimo/Admin_Pages/View_Logs_Admin_Page_Test.php b/tests/WP_Ultimo/Admin_Pages/View_Logs_Admin_Page_Test.php new file mode 100644 index 000000000..8bb8ba98e --- /dev/null +++ b/tests/WP_Ultimo/Admin_Pages/View_Logs_Admin_Page_Test.php @@ -0,0 +1,412 @@ +=50% coverage. + * Methods that call wp_die(), send headers, or require HTTP context are tested + * for their guard conditions and side-effects only. + */ +class View_Logs_Admin_Page_Test extends WP_UnitTestCase { + + /** + * @var View_Logs_Admin_Page + */ + private $page; + + /** + * Set up test fixtures. + */ + protected function setUp(): void { + + parent::setUp(); + $this->page = new View_Logs_Admin_Page(); + } + + /** + * Tear down: clean up superglobals. + */ + protected function tearDown(): void { + + unset( + $_REQUEST['file'], + $_REQUEST['return_ascii'], + $_REQUEST['submit_button'], + $_REQUEST['log_file'], + $_GET['log_file'], + $_POST['log_file'] + ); + + parent::tearDown(); + } + + // ------------------------------------------------------------------------- + // Page properties + // ------------------------------------------------------------------------- + + public function test_page_id(): void { + + $reflection = new \ReflectionClass($this->page); + $property = $reflection->getProperty('id'); + $property->setAccessible(true); + + $this->assertEquals('wp-ultimo-view-logs', $property->getValue($this->page)); + } + + public function test_page_type(): void { + + $reflection = new \ReflectionClass($this->page); + $property = $reflection->getProperty('type'); + $property->setAccessible(true); + + $this->assertEquals('submenu', $property->getValue($this->page)); + } + + public function test_parent_is_none(): void { + + $reflection = new \ReflectionClass($this->page); + $property = $reflection->getProperty('parent'); + $property->setAccessible(true); + + $this->assertEquals('none', $property->getValue($this->page)); + } + + public function test_highlight_menu_slug(): void { + + $reflection = new \ReflectionClass($this->page); + $property = $reflection->getProperty('highlight_menu_slug'); + $property->setAccessible(true); + + $this->assertEquals('wp-ultimo-events', $property->getValue($this->page)); + } + + public function test_badge_count_is_zero(): void { + + $reflection = new \ReflectionClass($this->page); + $property = $reflection->getProperty('badge_count'); + $property->setAccessible(true); + + $this->assertEquals(0, $property->getValue($this->page)); + } + + public function test_supported_panels(): void { + + $reflection = new \ReflectionClass($this->page); + $property = $reflection->getProperty('supported_panels'); + $property->setAccessible(true); + + $panels = $property->getValue($this->page); + $this->assertArrayHasKey('network_admin_menu', $panels); + $this->assertEquals('manage_network', $panels['network_admin_menu']); + } + + // ------------------------------------------------------------------------- + // get_title() / get_menu_title() + // ------------------------------------------------------------------------- + + public function test_get_title(): void { + + $this->assertEquals('View Log', $this->page->get_title()); + } + + public function test_get_menu_title(): void { + + $this->assertEquals('View Log', $this->page->get_menu_title()); + } + + // ------------------------------------------------------------------------- + // get_labels() + // ------------------------------------------------------------------------- + + public function test_get_labels_returns_array(): void { + + $labels = $this->page->get_labels(); + + $this->assertIsArray($labels); + } + + public function test_get_labels_has_edit_label(): void { + + $labels = $this->page->get_labels(); + + $this->assertArrayHasKey('edit_label', $labels); + $this->assertEquals('View Log', $labels['edit_label']); + } + + public function test_get_labels_has_add_new_label(): void { + + $labels = $this->page->get_labels(); + + $this->assertArrayHasKey('add_new_label', $labels); + $this->assertEquals('View Log', $labels['add_new_label']); + } + + public function test_get_labels_has_title_placeholder(): void { + + $labels = $this->page->get_labels(); + + $this->assertArrayHasKey('title_placeholder', $labels); + } + + public function test_get_labels_has_title_description(): void { + + $labels = $this->page->get_labels(); + + $this->assertArrayHasKey('title_description', $labels); + } + + public function test_get_labels_has_delete_button_label(): void { + + $labels = $this->page->get_labels(); + + $this->assertArrayHasKey('delete_button_label', $labels); + $this->assertEquals('Delete Log File', $labels['delete_button_label']); + } + + public function test_get_labels_has_delete_description(): void { + + $labels = $this->page->get_labels(); + + $this->assertArrayHasKey('delete_description', $labels); + } + + // ------------------------------------------------------------------------- + // get_object() + // ------------------------------------------------------------------------- + + public function test_get_object_returns_empty_array(): void { + + $object = $this->page->get_object(); + + $this->assertIsArray($object); + $this->assertEmpty($object); + } + + // ------------------------------------------------------------------------- + // init() — hook registration + // ------------------------------------------------------------------------- + + public function test_init_registers_ajax_action(): void { + + $this->page->init(); + + $this->assertGreaterThan( + 0, + has_action('wp_ajax_wu_handle_view_logs', [$this->page, 'handle_view_logs']) + ); + } + + // ------------------------------------------------------------------------- + // handle_view_logs() — non-AJAX path + // ------------------------------------------------------------------------- + + /** + * handle_view_logs() returns an array with the required keys when called + * outside of an AJAX context (wp_doing_ajax() returns false in tests). + */ + public function test_handle_view_logs_returns_array_with_required_keys(): void { + + $result = $this->page->handle_view_logs(); + + $this->assertIsArray($result); + $this->assertArrayHasKey('file', $result); + $this->assertArrayHasKey('file_name', $result); + $this->assertArrayHasKey('contents', $result); + $this->assertArrayHasKey('logs_list', $result); + } + + public function test_handle_view_logs_logs_list_is_array(): void { + + $result = $this->page->handle_view_logs(); + + $this->assertIsArray($result['logs_list']); + } + + public function test_handle_view_logs_contents_is_string(): void { + + $result = $this->page->handle_view_logs(); + + $this->assertIsString($result['contents']); + } + + public function test_handle_view_logs_file_name_is_string(): void { + + $result = $this->page->handle_view_logs(); + + $this->assertIsString($result['file_name']); + } + + /** + * When no log files exist, logs_list should contain the "No log files found" entry. + */ + public function test_handle_view_logs_empty_logs_list_has_placeholder(): void { + + // Ensure the logs folder exists but is empty (or has no .log files). + // We can't easily guarantee an empty folder, so we just verify the + // structure is correct regardless of whether files exist. + $result = $this->page->handle_view_logs(); + + $this->assertIsArray($result['logs_list']); + $this->assertNotEmpty($result['logs_list']); + } + + /** + * When a specific file is requested via wu_request('file'), it should be + * reflected in the response — but only if it's within the logs folder. + * We test the security guard: a file outside the logs folder triggers wp_die(). + */ + public function test_handle_view_logs_security_check_dies_for_external_file(): void { + + $_REQUEST['file'] = '/etc/passwd'; + + $this->expectException(\WPDieException::class); + + $this->page->handle_view_logs(); + } + + /** + * When return_ascii is 'no', the default content should be the translated string. + */ + public function test_handle_view_logs_no_ascii_default_content(): void { + + $_REQUEST['return_ascii'] = 'no'; + + $result = $this->page->handle_view_logs(); + + // If no file is found, contents should be the "No log entries found." string. + // This only applies when there are no log files at all. + $this->assertIsString($result['contents']); + } + + /** + * When a valid log file is requested, its contents are returned. + */ + public function test_handle_view_logs_returns_file_contents_for_valid_file(): void { + + // Create a temporary log file inside the logs folder. + $logs_folder = Logger::get_logs_folder(); + $tmp_file = $logs_folder . '/test-unit-test.log'; + + file_put_contents($tmp_file, 'test log content'); + + $_REQUEST['file'] = $tmp_file; + + try { + $result = $this->page->handle_view_logs(); + + $this->assertEquals('test log content', $result['contents']); + $this->assertEquals($tmp_file, $result['file']); + $this->assertEquals('/test-unit-test.log', $result['file_name']); + } finally { + @unlink($tmp_file); + unset($_REQUEST['file']); + } + } + + /** + * When the requested file does not exist, contents fall back to default. + */ + public function test_handle_view_logs_nonexistent_file_returns_default_content(): void { + + $logs_folder = Logger::get_logs_folder(); + $_REQUEST['file'] = $logs_folder . '/nonexistent-file-xyz.log'; + + $result = $this->page->handle_view_logs(); + + // File doesn't exist, so contents should be the default (ascii badge or "No log entries found."). + $this->assertIsString($result['contents']); + $this->assertNotEmpty($result['contents']); + + unset($_REQUEST['file']); + } + + // ------------------------------------------------------------------------- + // handle_save() — guard conditions + // ------------------------------------------------------------------------- + + /** + * handle_save() with action 'none' adds an error notice and returns early. + */ + public function test_handle_save_with_no_action_adds_error_notice(): void { + + // submit_button defaults to 'none' when not set. + unset($_REQUEST['submit_button']); + + // Should not throw — just adds a notice and returns. + $this->page->handle_save(); + + $this->assertTrue(true); + } + + /** + * handle_save() with a non-existent file adds an error notice and returns early. + */ + public function test_handle_save_with_nonexistent_file_adds_error_notice(): void { + + $_REQUEST['submit_button'] = 'download'; + $_REQUEST['log_file'] = '/tmp/nonexistent-wu-log-file-xyz.log'; + + $this->page->handle_save(); + + $this->assertTrue(true); + + unset($_REQUEST['submit_button'], $_REQUEST['log_file']); + } + + // ------------------------------------------------------------------------- + // page_loaded() + // ------------------------------------------------------------------------- + + public function test_page_loaded_does_not_throw(): void { + + $this->page->page_loaded(); + + $this->assertTrue(true); + } + + // ------------------------------------------------------------------------- + // register_scripts() + // ------------------------------------------------------------------------- + + public function test_register_scripts_does_not_throw(): void { + + $this->page->register_scripts(); + + $this->assertTrue(true); + } + + // ------------------------------------------------------------------------- + // output_default_widget_payload() + // ------------------------------------------------------------------------- + + public function test_output_default_widget_payload_is_callable(): void { + + $this->assertTrue(is_callable([$this->page, 'output_default_widget_payload'])); + } + + public function test_output_default_widget_payload_outputs_html(): void { + + ob_start(); + $this->page->output_default_widget_payload( + null, + [ + 'args' => [ + 'contents' => 'test log line', + ], + ] + ); + $output = ob_get_clean(); + + $this->assertIsString($output); + } +} From 26c3d923fed3ee940102d5db7b94807ee35049a0 Mon Sep 17 00:00:00 2001 From: David Stone Date: Sat, 28 Mar 2026 10:53:21 -0600 Subject: [PATCH 2/2] test(admin): address CodeRabbit review findings - Tax_Rates: wrap add_action/add_filter calls in try/finally for hook isolation resilience; fix test_output_columns_filter_can_modify_columns to capture filtered columns via closure reference and assert mutation; fix misleading tearDown() docstring - View_Logs: use tempnam() for unique temp log file to avoid parallel-run collisions; strengthen output_default_widget_payload assertion to check non-empty output and payload content; replace no-op assertTrue(true) in register_scripts test with wp_script_is() assertion --- .../Admin_Pages/Tax_Rates_Admin_Page_Test.php | 59 +++++++++++-------- .../Admin_Pages/View_Logs_Admin_Page_Test.php | 18 ++++-- 2 files changed, 47 insertions(+), 30 deletions(-) diff --git a/tests/WP_Ultimo/Admin_Pages/Tax_Rates_Admin_Page_Test.php b/tests/WP_Ultimo/Admin_Pages/Tax_Rates_Admin_Page_Test.php index eb6233742..19edb4f1a 100644 --- a/tests/WP_Ultimo/Admin_Pages/Tax_Rates_Admin_Page_Test.php +++ b/tests/WP_Ultimo/Admin_Pages/Tax_Rates_Admin_Page_Test.php @@ -34,7 +34,7 @@ protected function setUp(): void { } /** - * Tear down: clean up any registered actions/filters. + * Tear down test fixtures. */ protected function tearDown(): void { @@ -135,11 +135,13 @@ public function test_output_fires_action(): void { set_current_screen('dashboard-network'); - ob_start(); - $this->page->output(); - ob_get_clean(); - - remove_action('wu_load_tax_rates_list_page', $callback); + try { + ob_start(); + $this->page->output(); + ob_get_clean(); + } finally { + remove_action('wu_load_tax_rates_list_page', $callback); + } $this->assertTrue($action_fired); } @@ -159,11 +161,13 @@ public function test_output_applies_columns_filter(): void { set_current_screen('dashboard-network'); - ob_start(); - $this->page->output(); - ob_get_clean(); - - remove_filter('wu_tax_rates_columns', $callback); + try { + ob_start(); + $this->page->output(); + ob_get_clean(); + } finally { + remove_filter('wu_tax_rates_columns', $callback); + } $this->assertTrue($filter_called); } @@ -183,11 +187,13 @@ public function test_output_columns_filter_receives_expected_keys(): void { set_current_screen('dashboard-network'); - ob_start(); - $this->page->output(); - ob_get_clean(); - - remove_filter('wu_tax_rates_columns', $callback); + try { + ob_start(); + $this->page->output(); + ob_get_clean(); + } finally { + remove_filter('wu_tax_rates_columns', $callback); + } $this->assertIsArray($received_columns); $this->assertArrayHasKey('title', $received_columns); @@ -203,8 +209,10 @@ public function test_output_columns_filter_receives_expected_keys(): void { */ public function test_output_columns_filter_can_modify_columns(): void { - $callback = function ($columns) { + $received_columns = null; + $callback = function ($columns) use (&$received_columns) { $columns['custom_col'] = 'Custom Column'; + $received_columns = $columns; return $columns; }; @@ -212,14 +220,17 @@ public function test_output_columns_filter_can_modify_columns(): void { set_current_screen('dashboard-network'); - ob_start(); - $this->page->output(); - $output = ob_get_clean(); - - remove_filter('wu_tax_rates_columns', $callback); + try { + ob_start(); + $this->page->output(); + ob_get_clean(); + } finally { + remove_filter('wu_tax_rates_columns', $callback); + } - // The filter was applied — we just verify no exception was thrown. - $this->assertTrue(true); + $this->assertIsArray($received_columns); + $this->assertArrayHasKey('custom_col', $received_columns); + $this->assertEquals('Custom Column', $received_columns['custom_col']); } // ------------------------------------------------------------------------- diff --git a/tests/WP_Ultimo/Admin_Pages/View_Logs_Admin_Page_Test.php b/tests/WP_Ultimo/Admin_Pages/View_Logs_Admin_Page_Test.php index 8bb8ba98e..9f7827b7a 100644 --- a/tests/WP_Ultimo/Admin_Pages/View_Logs_Admin_Page_Test.php +++ b/tests/WP_Ultimo/Admin_Pages/View_Logs_Admin_Page_Test.php @@ -293,9 +293,11 @@ public function test_handle_view_logs_no_ascii_default_content(): void { */ public function test_handle_view_logs_returns_file_contents_for_valid_file(): void { - // Create a temporary log file inside the logs folder. + // Create a unique temporary log file inside the logs folder to avoid + // cross-test collisions in parallel/sharded runs. $logs_folder = Logger::get_logs_folder(); - $tmp_file = $logs_folder . '/test-unit-test.log'; + $tmp_file = tempnam($logs_folder, 'wu-log-test-'); + $this->assertNotFalse($tmp_file); file_put_contents($tmp_file, 'test log content'); @@ -306,9 +308,11 @@ public function test_handle_view_logs_returns_file_contents_for_valid_file(): vo $this->assertEquals('test log content', $result['contents']); $this->assertEquals($tmp_file, $result['file']); - $this->assertEquals('/test-unit-test.log', $result['file_name']); + $this->assertStringEndsWith(basename($tmp_file), $result['file_name']); } finally { - @unlink($tmp_file); + if (is_string($tmp_file) && is_file($tmp_file)) { + unlink($tmp_file); + } unset($_REQUEST['file']); } } @@ -378,11 +382,11 @@ public function test_page_loaded_does_not_throw(): void { // register_scripts() // ------------------------------------------------------------------------- - public function test_register_scripts_does_not_throw(): void { + public function test_register_scripts_enqueues_wu_view_log_script(): void { $this->page->register_scripts(); - $this->assertTrue(true); + $this->assertTrue(wp_script_is('wu-view-log', 'enqueued')); } // ------------------------------------------------------------------------- @@ -408,5 +412,7 @@ public function test_output_default_widget_payload_outputs_html(): void { $output = ob_get_clean(); $this->assertIsString($output); + $this->assertNotSame('', trim($output)); + $this->assertStringContainsString('test log line', $output); } }