diff --git a/plugins/optimization-detective/class-od-link-collection.php b/plugins/optimization-detective/class-od-link-collection.php index 762c2aa743..10e479cc21 100644 --- a/plugins/optimization-detective/class-od-link-collection.php +++ b/plugins/optimization-detective/class-od-link-collection.php @@ -114,12 +114,18 @@ public function add_link( array $attributes, ?int $minimum_viewport_width = null * @return LinkAttributes[] Prepared links with adjacent-duplicates merged together and media attributes added. */ private function get_prepared_links(): array { + $links_by_rel = array_values( $this->links_by_rel ); + if ( count( $links_by_rel ) === 0 ) { + // This condition is needed for PHP 7.2 and PHP 7.3 in which array_merge() fails if passed a spread empty array: 'array_merge() expects at least 1 parameter, 0 given'. + return array(); + } + return array_merge( ...array_map( function ( array $links ): array { return $this->merge_consecutive_links( $links ); }, - array_values( $this->links_by_rel ) + $links_by_rel ) ); } diff --git a/plugins/optimization-detective/class-od-strict-url-metric.php b/plugins/optimization-detective/class-od-strict-url-metric.php index bf973fe50f..09a014607a 100644 --- a/plugins/optimization-detective/class-od-strict-url-metric.php +++ b/plugins/optimization-detective/class-od-strict-url-metric.php @@ -18,6 +18,8 @@ * This is used exclusively in the REST API endpoint for capturing new URL Metrics to prevent invalid additional data from being * submitted in the request. For URL Metrics which have been stored the looser OD_URL_Metric class is used instead. * + * @phpstan-import-type JSONSchema from OD_URL_Metric + * * @since 0.6.0 * @access private */ @@ -28,7 +30,7 @@ final class OD_Strict_URL_Metric extends OD_URL_Metric { * * @since 0.6.0 * - * @return array Schema. + * @return JSONSchema Schema. */ public static function get_json_schema(): array { return self::set_additional_properties_to_false( parent::get_json_schema() ); @@ -43,26 +45,28 @@ public static function get_json_schema(): array { * @since 0.6.0 * @see rest_default_additional_properties_to_false() * - * @param mixed $schema Schema. - * @return mixed Processed schema. + * @phpstan-param JSONSchema $schema + * + * @param array $schema Schema. + * @return JSONSchema Processed schema. */ - private static function set_additional_properties_to_false( $schema ) { - if ( ! isset( $schema['type'] ) ) { - return $schema; - } - + private static function set_additional_properties_to_false( array $schema ): array { $type = (array) $schema['type']; if ( in_array( 'object', $type, true ) ) { if ( isset( $schema['properties'] ) ) { foreach ( $schema['properties'] as $key => $child_schema ) { - $schema['properties'][ $key ] = self::set_additional_properties_to_false( $child_schema ); + if ( isset( $child_schema['type'] ) ) { + $schema['properties'][ $key ] = self::set_additional_properties_to_false( $child_schema ); + } } } if ( isset( $schema['patternProperties'] ) ) { foreach ( $schema['patternProperties'] as $key => $child_schema ) { - $schema['patternProperties'][ $key ] = self::set_additional_properties_to_false( $child_schema ); + if ( isset( $child_schema['type'] ) ) { + $schema['patternProperties'][ $key ] = self::set_additional_properties_to_false( $child_schema ); + } } } @@ -70,7 +74,7 @@ private static function set_additional_properties_to_false( $schema ) { } if ( in_array( 'array', $type, true ) ) { - if ( isset( $schema['items'] ) ) { + if ( isset( $schema['items'], $schema['items']['type'] ) ) { $schema['items'] = self::set_additional_properties_to_false( $schema['items'] ); } } diff --git a/plugins/optimization-detective/class-od-url-metric-group-collection.php b/plugins/optimization-detective/class-od-url-metric-group-collection.php index d5bc4b09d7..5d159e7cd6 100644 --- a/plugins/optimization-detective/class-od-url-metric-group-collection.php +++ b/plugins/optimization-detective/class-od-url-metric-group-collection.php @@ -271,9 +271,12 @@ public function add_url_metric( OD_URL_Metric $new_url_metric ): void { return; } } + // @codeCoverageIgnoreStart + // In practice this exception should never get thrown because create_groups() creates groups from a minimum of 0 to a maximum of PHP_INT_MAX. throw new InvalidArgumentException( esc_html__( 'No group available to add URL Metric to.', 'optimization-detective' ) ); + // @codeCoverageIgnoreEnd } /** @@ -296,6 +299,8 @@ public function get_group_for_viewport_width( int $viewport_width ): OD_URL_Metr return $group; } } + // @codeCoverageIgnoreStart + // In practice this exception should never get thrown because create_groups() creates groups from a minimum of 0 to a maximum of PHP_INT_MAX. throw new InvalidArgumentException( esc_html( sprintf( @@ -305,6 +310,7 @@ public function get_group_for_viewport_width( int $viewport_width ): OD_URL_Metr ) ) ); + // @codeCoverageIgnoreEnd } )(); $this->result_cache[ __FUNCTION__ ][ $viewport_width ] = $result; diff --git a/plugins/optimization-detective/class-od-url-metric.php b/plugins/optimization-detective/class-od-url-metric.php index 7b31e97bfb..dac1b8b6a9 100644 --- a/plugins/optimization-detective/class-od-url-metric.php +++ b/plugins/optimization-detective/class-od-url-metric.php @@ -45,6 +45,19 @@ * viewport: ViewportRect, * elements: ElementData[] * } + * @phpstan-type JSONSchema array{ + * type: string|string[], + * items?: mixed, + * properties?: array, + * patternProperties?: array, + * required?: bool, + * minimum?: int, + * maximum?: int, + * pattern?: non-empty-string, + * additionalProperties?: bool, + * format?: non-empty-string, + * readonly?: bool, + * } * * @since 0.1.0 * @access private @@ -161,7 +174,7 @@ public function set_group( OD_URL_Metric_Group $group ): void { * * @todo Cache the return value? * - * @return array Schema. + * @return JSONSchema Schema. */ public static function get_json_schema(): array { /* diff --git a/plugins/optimization-detective/hooks.php b/plugins/optimization-detective/hooks.php index 59707c968d..ff2908390f 100644 --- a/plugins/optimization-detective/hooks.php +++ b/plugins/optimization-detective/hooks.php @@ -12,6 +12,9 @@ } // @codeCoverageIgnoreEnd +// The addition of the following hooks is tested in Test_OD_Hooks::test_hooks_added() and Test_OD_Storage_Post_Type::test_add_hooks(). + +// @codeCoverageIgnoreStart add_action( 'init', 'od_initialize_extensions', PHP_INT_MAX ); add_filter( 'template_include', 'od_buffer_output', PHP_INT_MAX ); OD_URL_Metrics_Post_Type::add_hooks(); @@ -20,3 +23,4 @@ add_filter( 'site_status_tests', 'od_add_rest_api_availability_test' ); add_action( 'admin_init', 'od_maybe_run_rest_api_health_check' ); add_action( 'after_plugin_row_meta', 'od_render_rest_api_health_check_admin_notice_in_plugin_row', 30 ); +// @codeCoverageIgnoreEnd diff --git a/plugins/optimization-detective/storage/rest-api.php b/plugins/optimization-detective/storage/rest-api.php index 07aebc8cd2..f4b086177e 100644 --- a/plugins/optimization-detective/storage/rest-api.php +++ b/plugins/optimization-detective/storage/rest-api.php @@ -168,7 +168,7 @@ function od_handle_rest_request( WP_REST_Request $request ) { ); } catch ( InvalidArgumentException $exception ) { // Note: This should never happen because an exception only occurs if a viewport width is less than zero, and the JSON Schema enforces that the viewport.width have a minimum of zero. - return new WP_Error( 'invalid_viewport_width', $exception->getMessage() ); + return new WP_Error( 'invalid_viewport_width', $exception->getMessage() ); // @codeCoverageIgnore } if ( $url_metric_group->is_complete() ) { return new WP_Error( @@ -279,7 +279,7 @@ function od_handle_rest_request( WP_REST_Request $request ) { * @since 0.8.0 * @access private * - * @param int $cache_purge_post_id Cache purge post ID. + * @param positive-int $cache_purge_post_id Cache purge post ID. */ function od_trigger_page_cache_invalidation( int $cache_purge_post_id ): void { $post = get_post( $cache_purge_post_id ); diff --git a/plugins/optimization-detective/tests/storage/test-rest-api.php b/plugins/optimization-detective/tests/storage/test-rest-api.php index 903cb283f5..50393f1558 100644 --- a/plugins/optimization-detective/tests/storage/test-rest-api.php +++ b/plugins/optimization-detective/tests/storage/test-rest-api.php @@ -75,6 +75,8 @@ static function ( array $properties ) use ( $property_name ): array { * @covers ::od_register_endpoint * @covers ::od_handle_rest_request * @covers ::od_trigger_page_cache_invalidation + * @covers OD_Strict_URL_Metric::set_additional_properties_to_false + * @covers OD_URL_Metric_Store_Request_Context::__construct */ public function test_rest_request_good_params( Closure $set_up ): void { $stored_context = null; @@ -140,6 +142,28 @@ function ( OD_URL_Metric_Store_Request_Context $context ) use ( &$stored_context } } + /** + * Test good params. + * + * @dataProvider data_provider_to_test_rest_request_good_params + * + * @covers ::od_register_endpoint + * @covers ::od_handle_rest_request + * @covers OD_Strict_URL_Metric::set_additional_properties_to_false + */ + public function test_rest_request_good_params_but_post_save_failed( Closure $set_up ): void { + $valid_params = $set_up(); + + add_filter( 'wp_insert_post_empty_content', '__return_true' ); // Cause wp_insert_post() to return WP_Error. + + $request = $this->create_request( $valid_params ); + $response = rest_get_server()->dispatch( $request ); + + $error = $response->as_error(); + $this->assertInstanceOf( WP_Error::class, $error ); + $this->assertSame( 'unable_to_store_url_metric', $error->get_error_code() ); + } + /** * Data provider for test_rest_request_bad_params. * @@ -274,6 +298,7 @@ static function ( $params ) use ( $valid_params ) { * * @covers ::od_register_endpoint * @covers ::od_handle_rest_request + * @covers OD_Strict_URL_Metric::set_additional_properties_to_false * * @dataProvider data_provider_invalid_params * @@ -655,6 +680,22 @@ static function ( string $hook, ...$args ) use ( &$all_hook_callback_args ): voi $this->assertTrue( $found, 'Expected save_post to have been fired for the post queried object.' ); } + /** + * Test od_trigger_page_cache_invalidation() for an invalid post. + * + * @covers ::od_trigger_page_cache_invalidation + */ + public function test_od_trigger_page_cache_invalidation_invalid_post_id(): void { + wp_delete_post( 1, true ); + $before_clean_post_cache_count = did_action( 'clean_post_cache' ); + $before_transition_post_status_count = did_action( 'transition_post_status' ); + $before_save_post_count = did_action( 'save_post' ); + od_trigger_page_cache_invalidation( 1 ); + $this->assertSame( $before_clean_post_cache_count, did_action( 'clean_post_cache' ) ); + $this->assertSame( $before_transition_post_status_count, did_action( 'transition_post_status' ) ); + $this->assertSame( $before_save_post_count, did_action( 'save_post' ) ); + } + /** * Populate URL Metrics. * diff --git a/plugins/optimization-detective/tests/test-cases/preload-link/buffer.html b/plugins/optimization-detective/tests/test-cases/preload-link/buffer.html new file mode 100644 index 0000000000..2418d914bc --- /dev/null +++ b/plugins/optimization-detective/tests/test-cases/preload-link/buffer.html @@ -0,0 +1,11 @@ + + + + ... + + +
+ +
+ + diff --git a/plugins/optimization-detective/tests/test-cases/preload-link/expected.html b/plugins/optimization-detective/tests/test-cases/preload-link/expected.html new file mode 100644 index 0000000000..ba358c6ce6 --- /dev/null +++ b/plugins/optimization-detective/tests/test-cases/preload-link/expected.html @@ -0,0 +1,13 @@ + + + + ... + + + +
+ +
+ + + diff --git a/plugins/optimization-detective/tests/test-cases/preload-link/set-up.php b/plugins/optimization-detective/tests/test-cases/preload-link/set-up.php new file mode 100644 index 0000000000..d91ac2b279 --- /dev/null +++ b/plugins/optimization-detective/tests/test-cases/preload-link/set-up.php @@ -0,0 +1,24 @@ +register( + 'img-preload', + static function ( OD_Tag_Visitor_Context $context ): bool { + if ( 'IMG' === $context->processor->get_tag() ) { + $context->link_collection->add_link( + array( + 'rel' => 'preload', + 'as' => 'image', + 'href' => $context->processor->get_attribute( 'src' ), + ) + ); + } + return false; + } + ); + } + ); +}; diff --git a/plugins/optimization-detective/tests/test-class-od-element.php b/plugins/optimization-detective/tests/test-class-od-element.php index 16b51a0e03..756686277d 100644 --- a/plugins/optimization-detective/tests/test-class-od-element.php +++ b/plugins/optimization-detective/tests/test-class-od-element.php @@ -12,6 +12,7 @@ class Test_OD_Element extends WP_UnitTestCase { /** * Tests construction. * + * @covers ::__construct * @covers ::get * @covers ::get_url_metric * @covers ::get_url_metric_group diff --git a/plugins/optimization-detective/tests/test-class-od-html-tag-processor.php b/plugins/optimization-detective/tests/test-class-od-html-tag-processor.php index a778950b57..ef70ad6289 100644 --- a/plugins/optimization-detective/tests/test-class-od-html-tag-processor.php +++ b/plugins/optimization-detective/tests/test-class-od-html-tag-processor.php @@ -85,11 +85,14 @@ public function data_provider_sample_documents(): array { 2 +
+
Copyright 2025
+ ', - 'open_tags' => array( 'HTML', 'HEAD', 'BODY', 'DIV', 'SVG', 'G', 'PATH', 'CIRCLE', 'G', 'RECT', 'MATH', 'MN', 'MSPACE', 'MN' ), + 'open_tags' => array( 'HTML', 'HEAD', 'BODY', 'DIV', 'SVG', 'G', 'PATH', 'CIRCLE', 'G', 'RECT', 'MATH', 'MN', 'MSPACE', 'MN', 'MAIN', 'FOOTER', 'SCRIPT' ), 'xpath_breadcrumbs' => array( '/HTML' => array( 'HTML' ), '/HTML/HEAD' => array( 'HTML', 'HEAD' ), @@ -105,6 +108,9 @@ public function data_provider_sample_documents(): array { '/HTML/BODY/DIV[@id=\'page\']/*[2][self::MATH]/*[1][self::MN]' => array( 'HTML', 'BODY', 'DIV', 'MATH', 'MN' ), '/HTML/BODY/DIV[@id=\'page\']/*[2][self::MATH]/*[2][self::MSPACE]' => array( 'HTML', 'BODY', 'DIV', 'MATH', 'MSPACE' ), '/HTML/BODY/DIV[@id=\'page\']/*[2][self::MATH]/*[3][self::MN]' => array( 'HTML', 'BODY', 'DIV', 'MATH', 'MN' ), + '/HTML/BODY/DIV[@id=\'page\']/*[3][self::MAIN]' => array( 'HTML', 'BODY', 'DIV', 'MAIN' ), + '/HTML/BODY/DIV[@id=\'page\']/*[3][self::MAIN]/*[1][self::FOOTER]' => array( 'HTML', 'BODY', 'DIV', 'MAIN', 'FOOTER' ), // The self-closing
has no effect in HTML, so it is expected that FOOTER would be parsed as a child of MAIN. + '/HTML/BODY/DIV[@id=\'page\']/*[4][self::SCRIPT]' => array( 'HTML', 'BODY', 'DIV', 'SCRIPT' ), // TODO: This is not correct, as the breadcrumbs should be `array( 'HTML', 'BODY', 'SCRIPT' )`. This would be handled automatically by WP_HTML_Processor. See . ), ), 'closing-void-tag' => array( @@ -156,7 +162,7 @@ public function data_provider_sample_documents(): array { - +
@@ -415,15 +421,18 @@ public function data_provider_sample_documents(): array { } /** - * Test next_tag(), next_token(), and get_xpath(). + * Test next_tag(), next_token(), get_xpath(), expects_closer(). * * @covers ::next_open_tag * @covers ::next_tag * @covers ::next_token + * @covers ::expects_closer + * @covers ::is_foreign_element * @covers ::get_xpath * @covers ::get_breadcrumbs * @covers ::get_indexed_breadcrumbs * @covers ::get_disambiguating_attributes + * @covers ::warn * * @dataProvider data_provider_sample_documents * @@ -470,6 +479,28 @@ public function test_next_tag_with_query(): void { $p->next_tag( array( 'tag_name' => 'HTML' ) ); } + /** + * Test expects_closer(). + * + * @covers ::expects_closer + */ + public function test_expects_closer(): void { + $p = new OD_HTML_Tag_Processor( '
' ); + $this->assertFalse( $p->expects_closer() ); + while ( $p->next_tag() ) { + if ( 'BODY' === $p->get_tag() ) { + break; + } + } + $this->assertSame( 'BODY', $p->get_tag() ); + $this->assertFalse( $p->expects_closer( 'IMG' ) ); + $this->assertTrue( $p->expects_closer() ); + $p->next_tag(); + $this->assertSame( 'HR', $p->get_tag() ); + $this->assertFalse( $p->expects_closer() ); + $this->assertTrue( $p->expects_closer( 'DIV' ) ); + } + /** * Test both append_head_html() and append_body_html(). * @@ -523,6 +554,8 @@ public function test_append_head_and_body_html(): void { $this->assertTrue( $did_seek ); $this->assertTrue( $saw_head ); $this->assertTrue( $saw_body ); + $this->assertTrue( $processor->has_bookmark( OD_HTML_Tag_Processor::END_OF_HEAD_BOOKMARK ) ); + $this->assertTrue( $processor->has_bookmark( OD_HTML_Tag_Processor::END_OF_BODY_BOOKMARK ) ); $this->assertStringContainsString( $head_injected, $processor->get_updated_html(), 'Only expecting end-of-head injection once document was finalized.' ); $this->assertStringContainsString( $body_injected, $processor->get_updated_html(), 'Only expecting end-of-body injection once document was finalized.' ); @@ -545,6 +578,53 @@ public function test_append_head_and_body_html(): void { $this->assertSame( $expected, $processor->get_updated_html() ); } + /** + * Test get_updated_html() when running out of bookmarks. + * + * @covers ::get_updated_html + * @covers ::warn + */ + public function test_get_updated_html_when_out_of_bookmarks(): void { + $this->setExpectedIncorrectUsage( 'WP_HTML_Tag_Processor::set_bookmark' ); + $html = ' + + + + + +

Hello World

+ + + '; + $processor = new OD_HTML_Tag_Processor( $html ); + $this->assertTrue( $processor->next_tag() ); + $this->assertEquals( 'HTML', $processor->get_tag() ); + $max_bookmarks = max( WP_HTML_Processor::MAX_BOOKMARKS, WP_HTML_Tag_Processor::MAX_BOOKMARKS ); + for ( $i = 0; $i < $max_bookmarks + 1; $i++ ) { + if ( ! $processor->set_bookmark( "bookmark-$i" ) ) { + break; + } + } + $processor->append_head_html( '' ); + $processor->append_body_html( '' ); + + $saw_head = false; + $saw_body = false; + while ( $processor->next_open_tag() ) { + $tag = $processor->get_tag(); + if ( 'HEAD' === $tag ) { + $saw_head = true; + } elseif ( 'BODY' === $tag ) { + $saw_body = true; + } + } + $this->assertTrue( $saw_head ); + $this->assertTrue( $saw_body ); + $this->assertFalse( $processor->has_bookmark( OD_HTML_Tag_Processor::END_OF_HEAD_BOOKMARK ) ); + $this->assertFalse( $processor->has_bookmark( OD_HTML_Tag_Processor::END_OF_BODY_BOOKMARK ) ); + $this->assertSame( $html, $processor->get_updated_html() ); + } + /** * Test get_tag(), get_attribute(), set_attribute(), remove_attribute(), and get_updated_html(). * @@ -578,6 +658,7 @@ public function test_html_tag_processor_wrapper_methods(): void { * @covers ::set_bookmark * @covers ::seek * @covers ::release_bookmark + * @covers ::get_current_depth */ public function test_bookmarking_and_seeking(): void { $processor = new OD_HTML_Tag_Processor( @@ -689,6 +770,9 @@ public function test_bookmarking_and_seeking(): void { $processor->release_bookmark( 'FIGURE' ); $this->assertFalse( $processor->has_bookmark( 'FIGURE' ) ); + $this->assertFalse( $processor->release_bookmark( 'optimization_detective_end_of_head' ) ); + $this->assertFalse( $processor->release_bookmark( 'optimization_detective_end_of_body' ) ); + // TODO: Try adding too many bookmarks. } diff --git a/plugins/optimization-detective/tests/test-class-od-link-collection.php b/plugins/optimization-detective/tests/test-class-od-link-collection.php index 3c0ea1bd4c..da274c1763 100644 --- a/plugins/optimization-detective/tests/test-class-od-link-collection.php +++ b/plugins/optimization-detective/tests/test-class-od-link-collection.php @@ -233,6 +233,20 @@ public function data_provider_to_test_add_link(): array { 'expected_count' => 0, 'error' => 'A link with rel=preconnect must include an "href" attribute.', ), + 'bad_rel' => array( + 'links_args' => array( + array( + array( + 'rel' => 123, + 'href' => 'https://example.com/foo-400.jpg', + ), + ), + ), + 'expected_html' => '', + 'expected_header' => '', + 'expected_count' => 0, + 'error' => 'Link attributes must be strings.', + ), 'bad_preload' => array( 'links_args' => array( array( @@ -329,7 +343,10 @@ public function data_provider_to_test_add_link(): array { * * @covers ::add_link * @covers ::get_html + * @covers ::get_prepared_links + * @covers ::merge_consecutive_links * @covers ::get_response_header + * @covers ::count * * @dataProvider data_provider_to_test_add_link * @@ -346,6 +363,9 @@ public function test_add_link( array $links_args, string $expected_html, string } $collection = new OD_Link_Collection(); + + $this->assertNull( $collection->get_response_header() ); + foreach ( $links_args as $link_args ) { $collection->add_link( ...$link_args ); } diff --git a/plugins/optimization-detective/tests/test-class-od-strict-url-metric.php b/plugins/optimization-detective/tests/test-class-od-strict-url-metric.php index 33be2b8ffc..53f7cc2a9b 100644 --- a/plugins/optimization-detective/tests/test-class-od-strict-url-metric.php +++ b/plugins/optimization-detective/tests/test-class-od-strict-url-metric.php @@ -22,7 +22,13 @@ static function ( array $properties ) { 'type' => 'object', 'properties' => array( 'hex' => array( - 'type' => 'string', + 'type' => 'object', + 'properties' => array( + 'red' => array( + 'type' => 'string', + ), + ), + 'additionalProperties' => true, ), ), 'additionalProperties' => true, @@ -35,9 +41,15 @@ static function ( array $properties ) { static function ( array $properties ) { $properties['region'] = array( 'type' => 'object', - 'properties' => array( - 'continent' => array( - 'type' => 'string', + 'patternProperties' => array( + '\w+' => array( + 'type' => 'object', + 'properties' => array( + 'name' => array( + 'type' => 'string', + ), + ), + 'additionalProperties' => true, ), ), 'additionalProperties' => true, @@ -49,14 +61,41 @@ static function ( array $properties ) { $this->assertTrue( $loose_schema['additionalProperties'] ); $this->assertFalse( $loose_schema['properties']['viewport']['additionalProperties'] ); // The viewport is never extensible. Only the root and the elements are. $this->assertTrue( $loose_schema['properties']['elements']['items']['additionalProperties'] ); + $this->assertTrue( $loose_schema['properties']['colors']['properties']['hex']['additionalProperties'] ); $this->assertTrue( $loose_schema['properties']['elements']['items']['properties']['region']['additionalProperties'] ); + $this->assertTrue( $loose_schema['properties']['elements']['items']['properties']['region']['patternProperties']['\w+']['additionalProperties'] ); $this->assertTrue( $loose_schema['properties']['colors']['additionalProperties'] ); $strict_schema = OD_Strict_URL_Metric::get_json_schema(); $this->assertFalse( $strict_schema['additionalProperties'] ); $this->assertFalse( $strict_schema['properties']['viewport']['additionalProperties'] ); $this->assertFalse( $strict_schema['properties']['elements']['items']['additionalProperties'] ); + $this->assertFalse( $strict_schema['properties']['colors']['properties']['hex']['additionalProperties'] ); $this->assertFalse( $strict_schema['properties']['elements']['items']['properties']['region']['additionalProperties'] ); + $this->assertFalse( $strict_schema['properties']['elements']['items']['properties']['region']['patternProperties']['\w+']['additionalProperties'] ); $this->assertFalse( $strict_schema['properties']['colors']['additionalProperties'] ); } + + /** + * Tests get_json_schema() with missing type. + * + * @covers ::get_json_schema + */ + public function test_get_json_schema_with_missing_type(): void { + $this->setExpectedIncorrectUsage( 'Filter: 'od_url_metric_schema_root_additional_properties'' ); + add_filter( + 'od_url_metric_schema_root_additional_properties', + static function ( array $properties ) { + $properties['foo'] = array( + 'readonly' => true, + ); + return $properties; + } + ); + $loose_schema = OD_URL_Metric::get_json_schema(); + $this->assertArrayNotHasKey( 'colors', $loose_schema['properties'] ); + + $strict_schema = OD_Strict_URL_Metric::get_json_schema(); + $this->assertArrayNotHasKey( 'colors', $strict_schema['properties'] ); + } } diff --git a/plugins/optimization-detective/tests/test-class-od-tag-visitor-registry.php b/plugins/optimization-detective/tests/test-class-od-tag-visitor-registry.php index 7abd92b997..50742612fc 100644 --- a/plugins/optimization-detective/tests/test-class-od-tag-visitor-registry.php +++ b/plugins/optimization-detective/tests/test-class-od-tag-visitor-registry.php @@ -24,6 +24,7 @@ public function test(): void { // Add img visitor. $this->assertFalse( $registry->is_registered( 'img' ) ); + $this->assertNull( $registry->get_registered( 'img' ) ); $img_visitor = static function ( OD_Tag_Visitor_Context $context ) { return $context->processor->get_tag() === 'IMG'; }; diff --git a/plugins/optimization-detective/tests/test-class-od-url-metric.php b/plugins/optimization-detective/tests/test-class-od-url-metric.php index 15f34899be..c48e7caffa 100644 --- a/plugins/optimization-detective/tests/test-class-od-url-metric.php +++ b/plugins/optimization-detective/tests/test-class-od-url-metric.php @@ -266,6 +266,9 @@ static function ( $value ) { /** * Tests construction. * + * @covers ::__construct + * @covers ::prepare_data + * @covers ::get_uuid * @covers ::get_viewport * @covers ::get_viewport_width * @covers ::get_timestamp @@ -365,6 +368,13 @@ public function data_provider_to_test_constructor_with_extended_schema(): array 'boundingClientRect' => $this->get_sample_dom_rect(), ); + $data = array( + 'url' => home_url( '/' ), + 'viewport' => $viewport, + 'timestamp' => microtime( true ), + 'elements' => array( $valid_element ), + ); + return array( 'added_valid_root_property_populated' => array( 'set_up' => static function (): void { @@ -547,6 +557,112 @@ static function ( array $properties ): array { 'assert' => static function (): void {}, 'error' => 'OD_URL_Metric[elements][0][isColorful] is not of type boolean.', ), + + 'added_immutable_element_property' => array( + 'set_up' => static function (): void { + add_filter( + 'od_url_metric_schema_element_item_additional_properties', + static function ( array $properties ): array { + $properties['isLCP'] = array( + 'type' => 'string', + ); + return $properties; + } + ); + }, + 'data' => $data, + 'assert' => static function (): void {}, + 'error' => '', + 'wrong' => 'Filter: 'od_url_metric_schema_element_item_additional_properties'', + ), + + 'added_element_property_without_type' => array( + 'set_up' => static function (): void { + add_filter( + 'od_url_metric_schema_element_item_additional_properties', + static function ( array $properties ): array { + $properties['foo'] = array( + 'minimum' => 1, + ); + return $properties; + } + ); + }, + 'data' => $data, + 'assert' => function (): void { + $this->assertArrayHasKey( 'isLCP', OD_URL_Metric::get_json_schema()['properties']['elements']['items']['properties'] ); + $this->assertArrayNotHasKey( 'foo', OD_URL_Metric::get_json_schema()['properties']['elements']['items']['properties'] ); + }, + 'error' => '', + 'wrong' => 'Filter: 'od_url_metric_schema_element_item_additional_properties'', + ), + + 'added_element_property_with_invalid_type' => array( + 'set_up' => static function (): void { + add_filter( + 'od_url_metric_schema_element_item_additional_properties', + static function ( array $properties ): array { + $properties['foo'] = array( + 'type' => null, + ); + return $properties; + } + ); + }, + 'data' => $data, + 'assert' => function (): void { + $this->assertArrayHasKey( 'isLCP', OD_URL_Metric::get_json_schema()['properties']['elements']['items']['properties'] ); + $this->assertArrayNotHasKey( 'foo', OD_URL_Metric::get_json_schema()['properties']['elements']['items']['properties'] ); + }, + 'error' => '', + 'wrong' => 'Filter: 'od_url_metric_schema_element_item_additional_properties'', + ), + + 'added_element_property_with_required' => array( + 'set_up' => static function (): void { + add_filter( + 'od_url_metric_schema_element_item_additional_properties', + static function ( array $properties ): array { + $properties['foo'] = array( + 'type' => 'string', + 'required' => true, + ); + return $properties; + } + ); + }, + 'data' => $data, + 'assert' => function (): void { + $this->assertArrayHasKey( 'isLCP', OD_URL_Metric::get_json_schema()['properties']['elements']['items']['properties'] ); + $this->assertArrayHasKey( 'foo', OD_URL_Metric::get_json_schema()['properties']['elements']['items']['properties'] ); + $this->assertFalse( OD_URL_Metric::get_json_schema()['properties']['elements']['items']['properties']['foo']['required'] ); + }, + 'error' => '', + 'wrong' => 'Filter: 'od_url_metric_schema_element_item_additional_properties'', + ), + + 'added_element_property_invalid_default' => array( + 'set_up' => static function (): void { + add_filter( + 'od_url_metric_schema_element_item_additional_properties', + static function ( array $properties ): array { + $properties['foo'] = array( + 'type' => 'string', + 'default' => 'bard', + ); + return $properties; + } + ); + }, + 'data' => $data, + 'assert' => function (): void { + $this->assertArrayHasKey( 'isLCP', OD_URL_Metric::get_json_schema()['properties']['elements']['items']['properties'] ); + $this->assertArrayHasKey( 'foo', OD_URL_Metric::get_json_schema()['properties']['elements']['items']['properties'] ); + $this->assertArrayNotHasKey( 'default', OD_URL_Metric::get_json_schema()['properties']['elements']['items']['properties']['foo'] ); + }, + 'error' => '', + 'wrong' => 'Filter: 'od_url_metric_schema_element_item_additional_properties'', + ), ); } @@ -554,6 +670,7 @@ static function ( array $properties ): array { * Tests construction with extended schema. * * @covers ::get_json_schema + * @covers ::extend_schema_with_optional_properties * * @dataProvider data_provider_to_test_constructor_with_extended_schema * @@ -561,12 +678,16 @@ static function ( array $properties ): array { * @param array $data Data. * @param Closure $assert Assert. * @param string $error Error. + * @param string $wrong Expected doing it wrong. */ - public function test_constructor_with_extended_schema( Closure $set_up, array $data, Closure $assert, string $error = '' ): void { + public function test_constructor_with_extended_schema( Closure $set_up, array $data, Closure $assert, string $error = '', string $wrong = '' ): void { if ( '' !== $error ) { $this->expectException( OD_Data_Validation_Exception::class ); $this->expectExceptionMessage( $error ); } + if ( '' !== $wrong ) { + $this->setExpectedIncorrectUsage( $wrong ); + } $url_metric_sans_extended_schema = new OD_URL_Metric( $data ); $set_up(); $url_metric_with_extended_schema = new OD_URL_Metric( $data ); diff --git a/plugins/optimization-detective/tests/test-class-od-url-metrics-group-collection.php b/plugins/optimization-detective/tests/test-class-od-url-metrics-group-collection.php index 08be7f4691..4e431d031c 100644 --- a/plugins/optimization-detective/tests/test-class-od-url-metrics-group-collection.php +++ b/plugins/optimization-detective/tests/test-class-od-url-metrics-group-collection.php @@ -118,6 +118,9 @@ public function data_provider_test_construction(): array { /** * @covers ::__construct + * @covers ::get_current_etag + * @covers ::create_groups + * @covers ::count * * @dataProvider data_provider_test_construction * @@ -134,6 +137,7 @@ public function test_construction( array $url_metrics, string $current_etag, arr } $group_collection = new OD_URL_Metric_Group_Collection( $url_metrics, $current_etag, $breakpoints, $sample_size, $freshness_ttl ); $this->assertCount( count( $breakpoints ) + 1, $group_collection ); + $this->assertSame( $current_etag, $group_collection->get_current_etag() ); } /** @@ -237,6 +241,8 @@ public function test_clear_cache(): void { * Test add_url_metric(). * * @covers ::add_url_metric + * @covers OD_URL_Metric_Group::add_url_metric + * @covers ::is_any_group_populated * * @param int $sample_size Sample size. * @param int[] $breakpoints Breakpoints. @@ -248,11 +254,14 @@ public function test_clear_cache(): void { public function test_add_url_metric( int $sample_size, array $breakpoints, array $viewport_widths, array $expected_counts ): void { $current_etag = md5( '' ); $group_collection = new OD_URL_Metric_Group_Collection( array(), $current_etag, $breakpoints, $sample_size, HOUR_IN_SECONDS ); + $this->assertFalse( $group_collection->is_any_group_populated() ); + $this->assertFalse( $group_collection->is_any_group_populated() ); // To check the result cache. // Over-populate the sample size for the breakpoints by a dozen. foreach ( $viewport_widths as $viewport_width => $count ) { for ( $i = 0; $i < $count; $i++ ) { $group_collection->add_url_metric( $this->get_sample_url_metric( array( 'viewport_width' => $viewport_width ) ) ); + $this->assertTrue( $group_collection->is_any_group_populated() ); } } @@ -629,8 +638,9 @@ static function ( OD_URL_Metric_Group $group ): array { } /** - * Test is_every_group_populated() and is_every_group_complete(). + * Test is_any_group_populated(), is_every_group_populated(), and is_every_group_complete(). * + * @covers ::is_any_group_populated * @covers ::is_every_group_populated * @covers ::is_every_group_complete */ @@ -645,8 +655,12 @@ public function test_is_every_group_populated(): void { $sample_size, HOUR_IN_SECONDS ); + $this->assertFalse( $group_collection->is_any_group_populated() ); + $this->assertFalse( $group_collection->is_any_group_populated() ); // Check cached value. $this->assertFalse( $group_collection->is_every_group_populated() ); + $this->assertFalse( $group_collection->is_every_group_populated() ); // Check cached value. $this->assertFalse( $group_collection->is_every_group_complete() ); + $this->assertFalse( $group_collection->is_every_group_complete() ); // Check cached value. $group_collection->add_url_metric( $this->get_sample_url_metric( array( @@ -655,6 +669,7 @@ public function test_is_every_group_populated(): void { ) ) ); + $this->assertTrue( $group_collection->is_any_group_populated() ); $this->assertFalse( $group_collection->is_every_group_populated() ); $this->assertFalse( $group_collection->is_every_group_complete() ); $group_collection->add_url_metric( @@ -665,6 +680,7 @@ public function test_is_every_group_populated(): void { ) ) ); + $this->assertTrue( $group_collection->is_any_group_populated() ); $this->assertFalse( $group_collection->is_every_group_populated() ); $this->assertFalse( $group_collection->is_every_group_complete() ); $group_collection->add_url_metric( @@ -675,6 +691,7 @@ public function test_is_every_group_populated(): void { ) ) ); + $this->assertTrue( $group_collection->is_any_group_populated() ); $this->assertTrue( $group_collection->is_every_group_populated() ); $this->assertFalse( $group_collection->is_every_group_complete() ); @@ -862,6 +879,7 @@ public function test_get_common_lcp_element( array $url_metrics, ?string $expect $this->assertCount( 3, $group_collection ); $common_lcp_element = $group_collection->get_common_lcp_element(); + $this->assertSame( $common_lcp_element, $group_collection->get_common_lcp_element() ); // Check cached value. if ( is_string( $expected ) ) { $this->assertInstanceOf( OD_Element::class, $common_lcp_element ); $this->assertSame( $expected, $common_lcp_element->get_xpath() ); @@ -942,8 +960,11 @@ public function data_provider_element_max_intersection_ratios(): array { * Test get_all_element_max_intersection_ratios(), get_element_max_intersection_ratio(), and get_all_denormalized_elements(). * * @covers ::get_all_element_max_intersection_ratios + * @covers OD_URL_Metric_Group::get_all_element_max_intersection_ratios * @covers ::get_element_max_intersection_ratio + * @covers OD_URL_Metric_Group::get_element_max_intersection_ratio * @covers ::get_xpath_elements_map + * @covers OD_URL_Metric_Group::get_xpath_elements_map * * @dataProvider data_provider_element_max_intersection_ratios * @@ -966,6 +987,7 @@ public function test_get_all_element_max_intersection_ratios( array $url_metrics // Check get_all_denormalized_elements. $all_elements = $group_collection->get_xpath_elements_map(); + $this->assertSame( $all_elements, $group_collection->get_xpath_elements_map() ); // Check cached value. $xpath_counts = array(); foreach ( $url_metrics as $url_metric ) { foreach ( $url_metric->get_elements() as $element ) { @@ -979,8 +1001,7 @@ public function test_get_all_element_max_intersection_ratios( array $url_metrics foreach ( $all_elements as $xpath => $elements ) { foreach ( $elements as $element ) { $this->assertSame( $element->get_url_metric()->get_group(), $element->get_url_metric_group() ); - $this->assertInstanceOf( OD_Element::class, $element ); - $this->assertSame( $xpath, $element['xpath'] ); + $this->assertSame( $xpath, $element->get_xpath() ); } } } diff --git a/plugins/optimization-detective/tests/test-class-od-url-metrics-group.php b/plugins/optimization-detective/tests/test-class-od-url-metrics-group.php index bff1c56c55..4059137284 100644 --- a/plugins/optimization-detective/tests/test-class-od-url-metrics-group.php +++ b/plugins/optimization-detective/tests/test-class-od-url-metrics-group.php @@ -415,6 +415,7 @@ public function test_get_lcp_element( array $breakpoints, array $url_metrics, ar $lcp_element_xpaths_by_minimum_viewport_widths = array(); foreach ( $group_collection as $group ) { $lcp_element = $group->get_lcp_element(); + $this->assertSame( $lcp_element, $group->get_lcp_element() ); // Check cached result. $width_range = sprintf( '%d:', $group->get_minimum_viewport_width() ); if ( $group->get_maximum_viewport_width() !== PHP_INT_MAX ) { $width_range .= $group->get_maximum_viewport_width(); diff --git a/plugins/optimization-detective/tests/test-detection.php b/plugins/optimization-detective/tests/test-detection.php index e78c7231d9..cfea6df604 100644 --- a/plugins/optimization-detective/tests/test-detection.php +++ b/plugins/optimization-detective/tests/test-detection.php @@ -122,6 +122,7 @@ static function ( array $urls ): array { * Make sure the expected script is printed. * * @covers ::od_get_detection_script + * @covers ::od_get_asset_path * * @dataProvider data_provider_od_get_detection_script * diff --git a/plugins/optimization-detective/tests/test-hooks.php b/plugins/optimization-detective/tests/test-hooks.php index aa5d71ffc6..1a0ad677ed 100644 --- a/plugins/optimization-detective/tests/test-hooks.php +++ b/plugins/optimization-detective/tests/test-hooks.php @@ -10,21 +10,13 @@ class Test_OD_Hooks extends WP_UnitTestCase { /** * Make sure the hooks are added in hooks.php. * - * @see OD_Storage_Post_Type_Tests::test_add_hooks() + * @see Test_OD_Storage_Post_Type::test_add_hooks() */ public function test_hooks_added(): void { + $this->assertEquals( PHP_INT_MAX, has_action( 'init', 'od_initialize_extensions' ) ); $this->assertEquals( PHP_INT_MAX, has_filter( 'template_include', 'od_buffer_output' ) ); + $this->assertEquals( 10, has_filter( 'wp', 'od_maybe_add_template_output_buffer_filter' ) ); - $this->assertSame( - 10, - has_action( - 'init', - array( - OD_URL_Metrics_Post_Type::class, - 'register_post_type', - ) - ) - ); $this->assertEquals( 10, has_action( 'wp_head', 'od_render_generator_meta_tag' ) ); $this->assertEquals( 10, has_filter( 'site_status_tests', 'od_add_rest_api_availability_test' ) ); $this->assertEquals( 10, has_action( 'admin_init', 'od_maybe_run_rest_api_health_check' ) ); diff --git a/plugins/optimization-detective/tests/test-optimization.php b/plugins/optimization-detective/tests/test-optimization.php index 879221c1a8..5d66e7ec1a 100644 --- a/plugins/optimization-detective/tests/test-optimization.php +++ b/plugins/optimization-detective/tests/test-optimization.php @@ -335,6 +335,8 @@ public function data_provider_test_od_optimize_template_output_buffer(): array { * * @covers ::od_optimize_template_output_buffer * @covers ::od_is_response_html_content_type + * @covers OD_Tag_Visitor_Context::__construct + * @covers OD_Tag_Visitor_Context::__get * * @dataProvider data_provider_test_od_optimize_template_output_buffer * @@ -355,6 +357,16 @@ function ( OD_Tag_Visitor_Context $context ): bool { $this->assertInstanceOf( OD_HTML_Tag_Processor::class, $context->processor ); $this->assertInstanceOf( OD_Link_Collection::class, $context->link_collection ); + $error = null; + $value = ''; + try { + $value = $context->__get( 'invalid_param' ); + } catch ( Error $e ) { + $error = $e; + } + $this->assertInstanceOf( Error::class, $error ); + $this->assertSame( '', $value ); + $this->assertFalse( $context->processor->is_tag_closer() ); return $context->processor->get_tag() === 'IMG'; } diff --git a/plugins/optimization-detective/uninstall.php b/plugins/optimization-detective/uninstall.php index f5e360c6b3..d7f657d5d0 100644 --- a/plugins/optimization-detective/uninstall.php +++ b/plugins/optimization-detective/uninstall.php @@ -8,7 +8,7 @@ // If uninstall.php is not called by WordPress, bail. if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) { - exit; + exit; // @codeCoverageIgnore } require_once __DIR__ . '/storage/class-od-url-metrics-post-type.php';