From 2ec7f9297b4f1ef6ddc2c748b54c1915f7801ee3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dami=C3=A1n=20Su=C3=A1rez?= Date: Fri, 17 Jul 2026 09:33:50 +0100 Subject: [PATCH 1/9] add declarative metadata fields to Widget_Type --- .../src/class-widget-type.php | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/projects/packages/premium-analytics/src/class-widget-type.php b/projects/packages/premium-analytics/src/class-widget-type.php index c0ef1c5448eb..5c98791a8521 100644 --- a/projects/packages/premium-analytics/src/class-widget-type.php +++ b/projects/packages/premium-analytics/src/class-widget-type.php @@ -72,6 +72,57 @@ class Widget_Type { */ public $presentation = null; + /** + * Widget types are grouped into categories to help users browse and + * discover them. Static and declarative; not a user-editable attribute. + * + * Null when the widget did not declare the field. + * + * @var string|null + */ + public $category = null; + + /** + * Human-readable title that names the widget type. Translated + * at registration time using the widget's text domain. + * + * Null when the widget did not declare the field. + * + * @var string|null + */ + public $title = null; + + /** + * Human-readable description of what the widget type does. + * Translated at registration time using the widget's text domain. + * + * Null when the widget did not declare the field. + * + * @var string|null + */ + public $description = null; + + /** + * Contextual help note: `content` plus optional `links`. + * Translated at registration time using the widget's text domain. + * + * Null when the widget did not declare the field. + * + * @var array|null + */ + public $help = null; + + /** + * Alternative terms used to match the widget type when searching, + * e.g. "calendar" for an events widget. Translated at registration + * time using the widget's text domain. + * + * Null when the widget did not declare the field. + * + * @var string[]|null + */ + public $keywords = null; + /** * Constructor. * From 149cc7879edd1344d3841b4d03b6dab97fed88c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dami=C3=A1n=20Su=C3=A1rez?= Date: Fri, 17 Jul 2026 09:40:13 +0100 Subject: [PATCH 2/9] port widget metadata i18n pipeline from upstream translate, sanitize and register the new manifest fields --- .../premium-analytics/src/widget-i18n.json | 9 ++ .../premium-analytics/src/widget-types.php | 97 ++++++++++++++++++- 2 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 projects/packages/premium-analytics/src/widget-i18n.json diff --git a/projects/packages/premium-analytics/src/widget-i18n.json b/projects/packages/premium-analytics/src/widget-i18n.json new file mode 100644 index 000000000000..859fa3ef5dc8 --- /dev/null +++ b/projects/packages/premium-analytics/src/widget-i18n.json @@ -0,0 +1,9 @@ +{ + "title": "widget title", + "description": "widget description", + "help": { + "content": "widget help content", + "links": [ { "label": "widget help link label" } ] + }, + "keywords": [ "widget keyword" ] +} diff --git a/projects/packages/premium-analytics/src/widget-types.php b/projects/packages/premium-analytics/src/widget-types.php index 3acc82219790..4743c0b17ea4 100644 --- a/projects/packages/premium-analytics/src/widget-types.php +++ b/projects/packages/premium-analytics/src/widget-types.php @@ -4,7 +4,8 @@ * * Copies the wp-build manifest (`jpa_get_registered_widget_modules()`) into the * in-memory Widget_Type_Registry, so the plugin queries the registry instead - * of re-parsing the manifest. + * of re-parsing the manifest. On the way in, user-facing metadata strings are + * translated (per the widget-i18n.json schema) and the `help` note sanitized. * * This is the problem-agnostic "core" layer (a PA-namespaced copy of the * experimental Gutenberg API): it exposes the hooks a consumer uses to scope @@ -33,6 +34,93 @@ */ const WIDGET_TYPES_FILTER = 'jetpack_premium_analytics_widget_types'; +/** + * Returns the i18n schema describing which widget metadata fields are + * translatable and the gettext context to use for each. + * + * Read once from widget-i18n.json and memoized for the rest of the request. + * Decoded as objects, not associative arrays: that is how + * `translate_settings_using_i18n_schema()` tells keyed maps apart from lists. + * + * @return object Map of translatable field name to gettext context. + */ +function get_widget_metadata_i18n_schema() { + static $i18n_schema = null; + + if ( null === $i18n_schema ) { + $schema = wp_json_file_decode( __DIR__ . '/widget-i18n.json' ); + $i18n_schema = is_object( $schema ) ? $schema : new \stdClass(); + } + + return $i18n_schema; +} + +/** + * Translates a widget's user-facing metadata strings. + * + * Runs `title`, `description`, `help`, and `keywords` through the widget + * i18n schema, leaving every other key untouched. Unlike the upstream copy, + * a widget with no `textdomain` falls back to the package text domain + * instead of skipping translation: every bundled widget shares it. + * + * @param array $widget Widget data from the build manifest. + * @return array Widget data with its translatable strings localized. + */ +function translate_widget_metadata( $widget ) { + $textdomain = ! empty( $widget['textdomain'] ) ? $widget['textdomain'] : 'jetpack-premium-analytics'; + $i18n_schema = get_widget_metadata_i18n_schema(); + + foreach ( array( 'title', 'description', 'help', 'keywords' ) as $field ) { + if ( isset( $widget[ $field ] ) && isset( $i18n_schema->$field ) ) { + $widget[ $field ] = translate_settings_using_i18n_schema( $i18n_schema->$field, $widget[ $field ], $textdomain ); + } + } + + return $widget; +} + +/** + * Constrains a widget help note to its allowed shape: `content` keeps + * only `em`/`strong` markup, and links missing a `label` or `href` are + * dropped. + * + * @param array|null $help Help note from the build manifest. + * @return array|null Sanitized help note, or null when there is no content. + */ +function sanitize_widget_help( $help ) { + if ( ! is_array( $help ) || empty( $help['content'] ) || ! is_string( $help['content'] ) ) { + return null; + } + + $sanitized = array( + 'content' => wp_kses( + $help['content'], + array( + 'em' => array(), + 'strong' => array(), + ) + ), + ); + + if ( ! empty( $help['links'] ) && is_array( $help['links'] ) ) { + $links = array(); + foreach ( $help['links'] as $link ) { + if ( is_array( $link ) && ! empty( $link['label'] ) && ! empty( $link['href'] ) ) { + $links[] = array( + 'label' => $link['label'], + 'href' => $link['href'], + ); + } + } + + if ( $links ) { + $sanitized['links'] = $links; + } + } + + return $sanitized; +} + /** * Hydrates the widget type registry from the build manifest. * @@ -68,12 +156,19 @@ function register_widget_types() { continue; } + $widget = translate_widget_metadata( $widget ); + $registry->register( $widget['name'], array( 'render_module' => $widget['render_module'] ?? null, 'widget_module' => $widget['widget_module'] ?? null, 'presentation' => $widget['presentation'] ?? null, + 'category' => $widget['category'] ?? null, + 'title' => $widget['title'] ?? null, + 'description' => $widget['description'] ?? null, + 'help' => sanitize_widget_help( $widget['help'] ?? null ), + 'keywords' => $widget['keywords'] ?? null, ) ); } From 8c20a98905466a99b511a720fa844fdb768df426 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dami=C3=A1n=20Su=C3=A1rez?= Date: Fri, 17 Jul 2026 09:42:01 +0100 Subject: [PATCH 3/9] expose widget metadata in widget-modules route --- projects/packages/premium-analytics/src/widget-modules.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/projects/packages/premium-analytics/src/widget-modules.php b/projects/packages/premium-analytics/src/widget-modules.php index 8c591f7e0d43..9d201d1ac51a 100644 --- a/projects/packages/premium-analytics/src/widget-modules.php +++ b/projects/packages/premium-analytics/src/widget-modules.php @@ -46,6 +46,11 @@ function get_widget_modules_response() { 'render_module' => $widget_type->render_module, 'widget_module' => $widget_type->widget_module, 'presentation' => $widget_type->presentation, + 'category' => $widget_type->category, + 'title' => $widget_type->title, + 'description' => $widget_type->description, + 'help' => $widget_type->help, + 'keywords' => $widget_type->keywords, ); } From c76b9c690d85b6145b7f2fb70f4fbb92d350c808 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dami=C3=A1n=20Su=C3=A1rez?= Date: Fri, 17 Jul 2026 10:26:59 +0100 Subject: [PATCH 4/9] add widget metadata tests and changelog entry --- .../update-pa-widget-metadata-pipeline | 4 + .../tests/php/Widget_Metadata_Test.php | 187 ++++++++++++++++++ 2 files changed, 191 insertions(+) create mode 100644 projects/packages/premium-analytics/changelog/update-pa-widget-metadata-pipeline create mode 100644 projects/packages/premium-analytics/tests/php/Widget_Metadata_Test.php diff --git a/projects/packages/premium-analytics/changelog/update-pa-widget-metadata-pipeline b/projects/packages/premium-analytics/changelog/update-pa-widget-metadata-pipeline new file mode 100644 index 000000000000..6b413aa325dc --- /dev/null +++ b/projects/packages/premium-analytics/changelog/update-pa-widget-metadata-pipeline @@ -0,0 +1,4 @@ +Significance: minor +Type: added + +Widget types: carry the declarative widget.json metadata (category, title, description, help, keywords) from the build manifest through the registry into the widget-modules REST record, translated and sanitized at registration. diff --git a/projects/packages/premium-analytics/tests/php/Widget_Metadata_Test.php b/projects/packages/premium-analytics/tests/php/Widget_Metadata_Test.php new file mode 100644 index 000000000000..483ea83e94f2 --- /dev/null +++ b/projects/packages/premium-analytics/tests/php/Widget_Metadata_Test.php @@ -0,0 +1,187 @@ + 'jpa/hello-world', + 'title' => 'Hello world', + 'description' => 'A friendly greeting.', + 'keywords' => array( 'greeting' ), + ) + ); + + remove_filter( 'gettext_with_context', $callback ); + + $this->assertSame( 'Hello world', $widget['title'], 'Untranslated strings pass through unchanged.' ); + $this->assertContains( array( 'Hello world', 'widget title', 'jetpack-premium-analytics' ), $calls, 'The title is translated under the package domain.' ); + $this->assertContains( array( 'A friendly greeting.', 'widget description', 'jetpack-premium-analytics' ), $calls, 'The description is translated under the package domain.' ); + $this->assertContains( array( 'greeting', 'widget keyword', 'jetpack-premium-analytics' ), $calls, 'Each keyword is translated under the package domain.' ); + } + + /** + * A widget-declared textdomain wins over the package default. + */ + public function test_translate_widget_metadata_honors_declared_textdomain() { + $calls = array(); + $callback = static function ( $translation, $text, $context, $domain ) use ( &$calls ) { + $calls[] = array( $text, $context, $domain ); + return $translation; + }; + add_filter( 'gettext_with_context', $callback, 10, 4 ); + + translate_widget_metadata( + array( + 'title' => 'Hello world', + 'textdomain' => 'my-widget-pack', + ) + ); + + remove_filter( 'gettext_with_context', $callback ); + + $this->assertContains( array( 'Hello world', 'widget title', 'my-widget-pack' ), $calls, 'The declared textdomain is used for translation.' ); + } + + /** + * A help note without usable string content sanitizes to null. + */ + public function test_sanitize_widget_help_requires_string_content() { + $this->assertNull( sanitize_widget_help( null ), 'Null input stays null.' ); + $this->assertNull( sanitize_widget_help( array() ), 'A help note without content is dropped.' ); + $this->assertNull( sanitize_widget_help( array( 'content' => '' ) ), 'Empty content is dropped.' ); + $this->assertNull( sanitize_widget_help( array( 'content' => 42 ) ), 'Non-string content is dropped.' ); + } + + /** + * Help content keeps only `em`/`strong` markup. + */ + public function test_sanitize_widget_help_keeps_only_emphasis_markup() { + $help = sanitize_widget_help( + array( 'content' => 'Use bold, emphasis and nothing else.' ) + ); + + $this->assertSame( 'Use bold, emphasis and nothing else.', $help['content'], 'Only em/strong markup survives sanitization.' ); + } + + /** + * Links missing a label or href are dropped; surviving links are reduced + * to exactly label + href. + */ + public function test_sanitize_widget_help_drops_incomplete_links() { + $help = sanitize_widget_help( + array( + 'content' => 'Read the docs.', + 'links' => array( + array( + 'label' => 'Docs', + 'href' => 'https://example.com/docs', + 'target' => '_blank', + ), + array( 'label' => 'No href' ), + array( 'href' => 'https://example.com/no-label' ), + 'not-a-link', + ), + ) + ); + + $this->assertSame( + array( + array( + 'label' => 'Docs', + 'href' => 'https://example.com/docs', + ), + ), + $help['links'], + 'Only complete links survive, reduced to label + href.' + ); + } + + /** + * When no link survives, the `links` key is omitted entirely. + */ + public function test_sanitize_widget_help_omits_links_when_none_survive() { + $help = sanitize_widget_help( + array( + 'content' => 'Plain.', + 'links' => array( array( 'label' => 'No href' ) ), + ) + ); + + $this->assertArrayNotHasKey( 'links', $help, 'The links key is omitted when no link survives.' ); + } + + /** + * A registered widget type's metadata reaches the widget-modules REST + * record intact. + */ + public function test_widget_modules_record_carries_metadata() { + $registry = Widget_Type_Registry::get_instance(); + $registry->register( + 'test/metadata-sentinel', + array( + 'render_module' => 'test/render', + 'widget_module' => 'test/widget', + 'presentation' => 'framed', + 'category' => 'stats', + 'title' => 'Sentinel', + 'description' => 'Metadata carrier.', + 'help' => array( 'content' => 'Helpful.' ), + 'keywords' => array( 'sentinel' ), + ) + ); + + $records = get_widget_modules_response()->get_data(); + + $registry->unregister( 'test/metadata-sentinel' ); + + $record = null; + foreach ( $records as $candidate ) { + if ( 'test/metadata-sentinel' === $candidate['name'] ) { + $record = $candidate; + } + } + + $this->assertNotNull( $record, 'The registered widget type appears in the REST record list.' ); + $this->assertSame( 'stats', $record['category'], 'The category reaches the record.' ); + $this->assertSame( 'Sentinel', $record['title'], 'The title reaches the record.' ); + $this->assertSame( 'Metadata carrier.', $record['description'], 'The description reaches the record.' ); + $this->assertSame( array( 'content' => 'Helpful.' ), $record['help'], 'The help note reaches the record.' ); + $this->assertSame( array( 'sentinel' ), $record['keywords'], 'The keywords reach the record.' ); + } +} From 3eab8eaddf1f0847a13506561e0baa7c4df24ab3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dami=C3=A1n=20Su=C3=A1rez?= Date: Fri, 17 Jul 2026 11:34:34 +0100 Subject: [PATCH 5/9] fix phan nullable array access in metadata tests --- .../tests/php/Widget_Metadata_Test.php | 61 ++++++++++--------- 1 file changed, 33 insertions(+), 28 deletions(-) diff --git a/projects/packages/premium-analytics/tests/php/Widget_Metadata_Test.php b/projects/packages/premium-analytics/tests/php/Widget_Metadata_Test.php index 483ea83e94f2..ce618966a10e 100644 --- a/projects/packages/premium-analytics/tests/php/Widget_Metadata_Test.php +++ b/projects/packages/premium-analytics/tests/php/Widget_Metadata_Test.php @@ -92,11 +92,13 @@ public function test_sanitize_widget_help_requires_string_content() { * Help content keeps only `em`/`strong` markup. */ public function test_sanitize_widget_help_keeps_only_emphasis_markup() { - $help = sanitize_widget_help( - array( 'content' => 'Use bold, emphasis and nothing else.' ) + $this->assertSame( + array( 'content' => 'Use bold, emphasis and nothing else.' ), + sanitize_widget_help( + array( 'content' => 'Use bold, emphasis and nothing else.' ) + ), + 'Only em/strong markup survives sanitization, and no other key rides along.' ); - - $this->assertSame( 'Use bold, emphasis and nothing else.', $help['content'], 'Only em/strong markup survives sanitization.' ); } /** @@ -104,31 +106,32 @@ public function test_sanitize_widget_help_keeps_only_emphasis_markup() { * to exactly label + href. */ public function test_sanitize_widget_help_drops_incomplete_links() { - $help = sanitize_widget_help( + $this->assertSame( array( 'content' => 'Read the docs.', 'links' => array( array( - 'label' => 'Docs', - 'href' => 'https://example.com/docs', - 'target' => '_blank', + 'label' => 'Docs', + 'href' => 'https://example.com/docs', ), - array( 'label' => 'No href' ), - array( 'href' => 'https://example.com/no-label' ), - 'not-a-link', ), - ) - ); - - $this->assertSame( - array( + ), + sanitize_widget_help( array( - 'label' => 'Docs', - 'href' => 'https://example.com/docs', - ), + 'content' => 'Read the docs.', + 'links' => array( + array( + 'label' => 'Docs', + 'href' => 'https://example.com/docs', + 'target' => '_blank', + ), + array( 'label' => 'No href' ), + array( 'href' => 'https://example.com/no-label' ), + 'not-a-link', + ), + ) ), - $help['links'], - 'Only complete links survive, reduced to label + href.' + 'Only complete links survive, reduced to exactly label + href.' ); } @@ -136,14 +139,16 @@ public function test_sanitize_widget_help_drops_incomplete_links() { * When no link survives, the `links` key is omitted entirely. */ public function test_sanitize_widget_help_omits_links_when_none_survive() { - $help = sanitize_widget_help( - array( - 'content' => 'Plain.', - 'links' => array( array( 'label' => 'No href' ) ), - ) + $this->assertSame( + array( 'content' => 'Plain.' ), + sanitize_widget_help( + array( + 'content' => 'Plain.', + 'links' => array( array( 'label' => 'No href' ) ), + ) + ), + 'The links key is omitted when no link survives.' ); - - $this->assertArrayNotHasKey( 'links', $help, 'The links key is omitted when no link survives.' ); } /** From 5f910b8597ee48be16b6c5b7d221aa060559edd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dami=C3=A1n=20Su=C3=A1rez?= Date: Fri, 17 Jul 2026 11:42:13 +0100 Subject: [PATCH 6/9] sanitize widget help link hrefs drop links whose href fails esc_url_raw --- .../premium-analytics/src/widget-types.php | 16 +++++---- .../tests/php/Widget_Metadata_Test.php | 34 +++++++++++++++++++ 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/projects/packages/premium-analytics/src/widget-types.php b/projects/packages/premium-analytics/src/widget-types.php index 4743c0b17ea4..575dee4b4fc6 100644 --- a/projects/packages/premium-analytics/src/widget-types.php +++ b/projects/packages/premium-analytics/src/widget-types.php @@ -81,8 +81,8 @@ function translate_widget_metadata( $widget ) { /** * Constrains a widget help note to its allowed shape: `content` keeps - * only `em`/`strong` markup, and links missing a `label` or `href` are - * dropped. + * only `em`/`strong` markup, and links are dropped unless they carry a + * `label` and an `href` that survives `esc_url_raw()`. * * @param array|null $help Help note from the build manifest. * @return array|null Sanitized help note, or null when there is no content. @@ -106,10 +106,14 @@ function sanitize_widget_help( $help ) { $links = array(); foreach ( $help['links'] as $link ) { if ( is_array( $link ) && ! empty( $link['label'] ) && ! empty( $link['href'] ) ) { - $links[] = array( - 'label' => $link['label'], - 'href' => $link['href'], - ); + $href = esc_url_raw( $link['href'] ); + + if ( $href ) { + $links[] = array( + 'label' => $link['label'], + 'href' => $href, + ); + } } } diff --git a/projects/packages/premium-analytics/tests/php/Widget_Metadata_Test.php b/projects/packages/premium-analytics/tests/php/Widget_Metadata_Test.php index ce618966a10e..108c644d0c93 100644 --- a/projects/packages/premium-analytics/tests/php/Widget_Metadata_Test.php +++ b/projects/packages/premium-analytics/tests/php/Widget_Metadata_Test.php @@ -135,6 +135,40 @@ public function test_sanitize_widget_help_drops_incomplete_links() { ); } + /** + * Each link href goes through esc_url_raw(): a disallowed protocol drops + * the whole link, and safe URLs pass through unchanged. + */ + public function test_sanitize_widget_help_rejects_unsafe_link_protocols() { + $this->assertSame( + array( + 'content' => 'Read the docs.', + 'links' => array( + array( + 'label' => 'Docs', + 'href' => 'https://example.com/docs', + ), + ), + ), + sanitize_widget_help( + array( + 'content' => 'Read the docs.', + 'links' => array( + array( + 'label' => 'Bad', + 'href' => 'javascript:alert(1)', + ), + array( + 'label' => 'Docs', + 'href' => 'https://example.com/docs', + ), + ), + ) + ), + 'A link whose href does not survive esc_url_raw() is dropped; safe links pass unchanged.' + ); + } + /** * When no link survives, the `links` key is omitted entirely. */ From 54e8eab6a104183bc40c3b611aa38a3d4ddd7fe9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dami=C3=A1n=20Su=C3=A1rez?= Date: Fri, 17 Jul 2026 17:28:51 +0100 Subject: [PATCH 7/9] add hydration coverage for widget metadata --- .../tests/php/Widget_Metadata_Test.php | 62 +++++++++++++++++++ .../php/fixtures/widget-modules-manifest.php | 23 +++++++ 2 files changed, 85 insertions(+) create mode 100644 projects/packages/premium-analytics/tests/php/fixtures/widget-modules-manifest.php diff --git a/projects/packages/premium-analytics/tests/php/Widget_Metadata_Test.php b/projects/packages/premium-analytics/tests/php/Widget_Metadata_Test.php index 108c644d0c93..0bf7583c682f 100644 --- a/projects/packages/premium-analytics/tests/php/Widget_Metadata_Test.php +++ b/projects/packages/premium-analytics/tests/php/Widget_Metadata_Test.php @@ -13,13 +13,16 @@ require_once __DIR__ . '/../../src/widget-types.php'; require_once __DIR__ . '/../../src/widget-modules.php'; +require_once __DIR__ . '/fixtures/widget-modules-manifest.php'; /** + * @covers ::Automattic\Jetpack\PremiumAnalytics\register_widget_types * @covers ::Automattic\Jetpack\PremiumAnalytics\translate_widget_metadata * @covers ::Automattic\Jetpack\PremiumAnalytics\sanitize_widget_help * @covers ::Automattic\Jetpack\PremiumAnalytics\get_widget_metadata_i18n_schema * @covers ::Automattic\Jetpack\PremiumAnalytics\get_widget_modules_response */ +#[CoversFunction( 'Automattic\Jetpack\PremiumAnalytics\register_widget_types' )] #[CoversFunction( 'Automattic\Jetpack\PremiumAnalytics\translate_widget_metadata' )] #[CoversFunction( 'Automattic\Jetpack\PremiumAnalytics\sanitize_widget_help' )] #[CoversFunction( 'Automattic\Jetpack\PremiumAnalytics\get_widget_metadata_i18n_schema' )] @@ -78,6 +81,65 @@ public function test_translate_widget_metadata_honors_declared_textdomain() { $this->assertContains( array( 'Hello world', 'widget title', 'my-widget-pack' ), $calls, 'The declared textdomain is used for translation.' ); } + /** + * register_widget_types() hydrates the registry from the manifest with + * metadata translated, the help note sanitized, and every field mapped. + */ + public function test_register_widget_types_hydrates_metadata_from_manifest() { + $GLOBALS['jpa_test_widget_manifest'] = array( + array( + 'name' => 'test/hydration-sentinel', + 'render_module' => 'test/hydration/render', + 'widget_module' => 'test/hydration/widget', + 'presentation' => 'framed', + 'category' => 'stats', + 'title' => 'Hydration sentinel', + 'description' => 'Carries metadata through hydration.', + 'help' => array( + 'content' => 'Read this .', + 'links' => array( + array( + 'label' => 'Docs', + 'href' => 'https://example.com/docs', + ), + ), + ), + 'keywords' => array( 'sentinel' ), + ), + ); + + try { + register_widget_types(); + + $registered = get_registered_widget_types(); + $this->assertArrayHasKey( 'test/hydration-sentinel', $registered, 'The manifest candidate is registered.' ); + + $widget_type = $registered['test/hydration-sentinel']; + $this->assertSame( 'test/hydration/render', $widget_type->render_module, 'The render module is mapped.' ); + $this->assertSame( 'framed', $widget_type->presentation, 'The presentation is mapped.' ); + $this->assertSame( 'stats', $widget_type->category, 'The category is mapped.' ); + $this->assertSame( 'Hydration sentinel', $widget_type->title, 'The title is mapped.' ); + $this->assertSame( 'Carries metadata through hydration.', $widget_type->description, 'The description is mapped.' ); + $this->assertSame( array( 'sentinel' ), $widget_type->keywords, 'The keywords are mapped.' ); + $this->assertSame( + array( + 'content' => 'Read this carefully.', + 'links' => array( + array( + 'label' => 'Docs', + 'href' => 'https://example.com/docs', + ), + ), + ), + $widget_type->help, + 'The help note is sanitized during hydration.' + ); + } finally { + Widget_Type_Registry::get_instance()->unregister( 'test/hydration-sentinel' ); + unset( $GLOBALS['jpa_test_widget_manifest'] ); + } + } + /** * A help note without usable string content sanitizes to null. */ diff --git a/projects/packages/premium-analytics/tests/php/fixtures/widget-modules-manifest.php b/projects/packages/premium-analytics/tests/php/fixtures/widget-modules-manifest.php new file mode 100644 index 000000000000..a1f8f380f468 --- /dev/null +++ b/projects/packages/premium-analytics/tests/php/fixtures/widget-modules-manifest.php @@ -0,0 +1,23 @@ + Date: Mon, 20 Jul 2026 08:19:04 +0100 Subject: [PATCH 8/9] update comment --- .../premium-analytics/tests/php/Widget_Metadata_Test.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/projects/packages/premium-analytics/tests/php/Widget_Metadata_Test.php b/projects/packages/premium-analytics/tests/php/Widget_Metadata_Test.php index 0bf7583c682f..244dd25c1de9 100644 --- a/projects/packages/premium-analytics/tests/php/Widget_Metadata_Test.php +++ b/projects/packages/premium-analytics/tests/php/Widget_Metadata_Test.php @@ -82,8 +82,8 @@ public function test_translate_widget_metadata_honors_declared_textdomain() { } /** - * register_widget_types() hydrates the registry from the manifest with - * metadata translated, the help note sanitized, and every field mapped. + * Hydration registers manifest candidates with metadata translated, + * the help note sanitized, and every field mapped onto the type. */ public function test_register_widget_types_hydrates_metadata_from_manifest() { $GLOBALS['jpa_test_widget_manifest'] = array( From 66c47ee8847b91ff2d7f73fd3ed1a468cde4ea25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dami=C3=A1n=20Su=C3=A1rez?= Date: Mon, 20 Jul 2026 11:35:59 +0100 Subject: [PATCH 9/9] remove unused phan suppression in widget-types test fixture declares the function, so phan no longer flags it --- projects/packages/premium-analytics/src/widget-types.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/projects/packages/premium-analytics/src/widget-types.php b/projects/packages/premium-analytics/src/widget-types.php index 575dee4b4fc6..624de7e6cd92 100644 --- a/projects/packages/premium-analytics/src/widget-types.php +++ b/projects/packages/premium-analytics/src/widget-types.php @@ -140,7 +140,8 @@ function register_widget_types() { $registry = Widget_Type_Registry::get_instance(); - // @phan-suppress-next-line PhanUndeclaredFunction -- Generated by wp-build into build/widgets.php, outside Phan's analysis scope. The function_exists() guard above protects the call at runtime. + // Generated by wp-build into build/widgets.php, outside Phan's analysis scope. + // The function_exists() guard above protects the call at runtime. $jetpack_widget_modules = jpa_get_registered_widget_modules(); /**