diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..d99a6ae92 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,22 @@ +# Progress Planner + +Progress Planner (PP) is being withdrawn from wordpress.org. It reaches users as a Composer dependency of **pp-hosts** (private repo `ProgressPlanner/pp-hosts`, which requires this repo's `pp-hosts` branch). A change here is only done once it works inside pp-hosts. + +## Branches + +- `develop`: development. `main`: stable. `pp-hosts`: what pp-hosts installs (merge `develop` into it to release to pp-hosts). + +## pp-hosts contract + +`tests/contract/pp-hosts-contract.json` lists the PP API surface pp-hosts depends on: hooks it listens to, parent classes it extends (overridden methods, used properties and constants), `progress_planner()->get_*()` services and the methods called on them, constants, dashboard widget IDs and assets. + +- `tests/phpunit/test-pp-hosts-contract.php` runs on every PR (in the normal PHPUnit suite) and fails if PP stops providing any of it. +- The same check runs without WordPress or a database: `php tests/contract/check-pp-hosts-contract.php [contract.json]`. +- **If it fails:** prefer keeping the old API (deprecate it; don't remove it). If the break is intended, change pp-hosts in the same release. Then regenerate the contract in pp-hosts (`php bin/build-pp-contract.php`) and copy it here. +- **Don't edit the JSON by hand.** It's generated from pp-hosts. `known_issues` in it comes from pp-hosts `tests/contract/known-issues.json`. + +## Testing a PP change inside pp-hosts + +From a pp-hosts checkout, run `bin/pp-integration.sh `. It builds pp-hosts with that PP ref and runs these gates: contract, lint, PHPStan and Playwright e2e. Add `--ai` for a Claude impact report and a browser smoke test. See pp-hosts `CLAUDE.md`. + +Run it after merging notable PRs to `develop`, and always before merging `develop` into `pp-hosts`. diff --git a/tests/contract/check-pp-hosts-contract.php b/tests/contract/check-pp-hosts-contract.php new file mode 100644 index 000000000..5a83374d4 --- /dev/null +++ b/tests/contract/check-pp-hosts-contract.php @@ -0,0 +1,77 @@ +check(); +$prpl_known = $prpl_checker->get_known(); + +if ( $prpl_json ) { + echo \json_encode( + [ + 'failures' => (object) $prpl_failures, + 'known' => (object) $prpl_known, + ], + \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES + ) . "\n"; + exit( empty( $prpl_failures ) ? 0 : 1 ); +} + +foreach ( $prpl_known as $prpl_message => $prpl_reason ) { + echo "known issue: {$prpl_message}\n ({$prpl_reason})\n"; +} + +if ( empty( $prpl_failures ) ) { + echo "pp-hosts contract: OK\n"; +} else { + echo "pp-hosts contract: FAILED\n"; + foreach ( $prpl_failures as $prpl_section => $prpl_messages ) { + echo "\n[{$prpl_section}]\n"; + foreach ( $prpl_messages as $prpl_message ) { + echo " - {$prpl_message}\n"; + } + } +} + +exit( empty( $prpl_failures ) ? 0 : 1 ); diff --git a/tests/contract/class-pp-hosts-contract-checker.php b/tests/contract/class-pp-hosts-contract-checker.php new file mode 100644 index 000000000..71932ab21 --- /dev/null +++ b/tests/contract/class-pp-hosts-contract-checker.php @@ -0,0 +1,399 @@ +pp_dir = \rtrim( $pp_dir, '/' ); + + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Local file. + $contract = \json_decode( (string) \file_get_contents( $contract_file ), true ); + if ( ! \is_array( $contract ) ) { + throw new \RuntimeException( "Could not read contract file: {$contract_file}" ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- CLI/test output. + } + $this->contract = $contract; + } + + /** + * Failures matching an entry in the contract's known_issues. + * + * @var array Failure message => reason. + */ + private $known = []; + + /** + * Run all checks. + * + * Failures that mention a key from the contract's `known_issues` map are + * not returned; they are available via get_known() instead, so already + * tracked breakage doesn't fail the build but stays visible. + * + * @return array Failures grouped by section; empty sections are omitted. + */ + public function check() { + $failures = [ + 'hooks' => $this->check_hooks(), + 'classes' => $this->check_classes(), + 'services' => $this->check_services(), + 'constants' => $this->check_constants(), + 'meta_boxes' => $this->check_meta_boxes(), + 'assets' => $this->check_assets(), + ]; + + $this->known = []; + $known = $this->contract['known_issues'] ?? []; + foreach ( $failures as $section => $messages ) { + foreach ( $messages as $index => $message ) { + foreach ( $known as $key => $reason ) { + if ( false !== \strpos( $message, (string) $key ) ) { + $this->known[ $message ] = $reason; + unset( $failures[ $section ][ $index ] ); + break; + } + } + } + $failures[ $section ] = \array_values( $failures[ $section ] ); + } + + return \array_filter( $failures ); + } + + /** + * Known issues hit by the last check() run. + * + * @return array Failure message => reason. + */ + public function get_known() { + return $this->known; + } + + /** + * Hooks pp-hosts listens to must still be fired by Progress Planner. + * + * @return string[] + */ + private function check_hooks() { + $failures = []; + foreach ( $this->contract['hooks']['listens'] ?? [] as $hook ) { + $pattern = '/\b(?:do_action|do_action_ref_array|apply_filters|apply_filters_ref_array)\(\s*[\'"]' . \preg_quote( $hook, '/' ) . '[\'"]/'; + if ( ! \preg_match( $pattern, $this->get_source() ) ) { + $failures[] = "Hook '{$hook}' is no longer fired (no do_action/apply_filters found)."; + } + } + return $failures; + } + + /** + * Classes pp-hosts extends must keep the members pp-hosts overrides or uses. + * + * @return string[] + */ + private function check_classes() { + $failures = []; + foreach ( $this->contract['classes'] ?? [] as $consumer => $spec ) { + $parent_name = $spec['extends']; + if ( ! \class_exists( $parent_name ) && ! \interface_exists( $parent_name ) ) { + $failures[] = "{$consumer}: parent class {$parent_name} no longer exists."; + continue; + } + $parent = new \ReflectionClass( $parent_name ); + + if ( $parent->isFinal() ) { + $failures[] = "{$consumer}: parent class {$parent_name} is now final."; + } + + foreach ( $spec['overrides'] ?? [] as $method => $signature ) { + $failures = \array_merge( $failures, $this->check_override( $consumer, $parent, $method, $signature ) ); + } + + foreach ( $spec['calls'] ?? [] as $method ) { + if ( ! $parent->hasMethod( $method ) ) { + $failures[] = "{$consumer}: calls {$parent_name}::{$method}(), which no longer exists."; + } elseif ( $parent->getMethod( $method )->isPrivate() ) { + $failures[] = "{$consumer}: calls {$parent_name}::{$method}(), which is now private."; + } + } + + foreach ( $spec['properties'] ?? [] as $property => $visibility ) { + if ( ! $parent->hasProperty( $property ) ) { + $failures[] = "{$consumer}: relies on {$parent_name}::\${$property}, which no longer exists."; + continue; + } + $parent_property = $parent->getProperty( $property ); + if ( $parent_property->isPrivate() ) { + $failures[] = "{$consumer}: relies on {$parent_name}::\${$property}, which is now private."; + } elseif ( $parent_property->isPublic() && \in_array( $visibility, [ 'protected', 'private' ], true ) ) { + $failures[] = "{$consumer}: redeclares public {$parent_name}::\${$property} as {$visibility} (fatal)."; + } + } + + foreach ( $spec['constants'] ?? [] as $constant ) { + if ( ! $parent->hasConstant( $constant ) ) { + $failures[] = "{$consumer}: relies on {$parent_name}::{$constant}, which no longer exists."; + } + } + + // A new abstract method in the parent makes concrete pp-hosts subclasses fatal. + if ( empty( $spec['abstract'] ) ) { + foreach ( $parent->getMethods( \ReflectionMethod::IS_ABSTRACT ) as $abstract_method ) { + if ( ! \in_array( $abstract_method->getName(), $spec['chain_methods'] ?? [], true ) ) { + $failures[] = "{$consumer}: does not implement new abstract method {$abstract_method->class}::{$abstract_method->getName()}()."; + } + } + } + } + return $failures; + } + + /** + * Check that an overridden method is still compatible with its parent. + * + * @param string $consumer The pp-hosts class. + * @param \ReflectionClass $parent_class The Progress Planner parent class. + * @param string $method The method name. + * @param array $signature The pp-hosts signature: params, has_return_type, static. + * + * @return string[] + */ + private function check_override( $consumer, $parent_class, $method, $signature ) { + $label = "{$consumer}::{$method}() overrides {$parent_class->getName()}::{$method}()"; + + if ( ! $parent_class->hasMethod( $method ) ) { + return [ "{$label}, which no longer exists (the override is now dead code)." ]; + } + + $parent_method = $parent_class->getMethod( $method ); + $failures = []; + + if ( $parent_method->isFinal() ) { + $failures[] = "{$label}, which is now final (fatal)."; + } + if ( $parent_method->isPrivate() ) { + $failures[] = "{$label}, which is now private."; + } + if ( $parent_method->isStatic() !== (bool) ( $signature['static'] ?? false ) ) { + $failures[] = "{$label}, but static-ness differs (fatal)."; + } + if ( $parent_method->getNumberOfParameters() > (int) $signature['params'] ) { + $failures[] = "{$label}, which now takes {$parent_method->getNumberOfParameters()} parameter(s); pp-hosts accepts {$signature['params']} (fatal)."; + } + if ( $parent_method->hasReturnType() && empty( $signature['has_return_type'] ) ) { + $failures[] = "{$label}, which now declares a return type; pp-hosts does not (fatal)."; + } + + return $failures; + } + + /** + * Service getters on progress_planner() and the methods called on them. + * + * @return string[] + */ + private function check_services() { + $failures = []; + $base = new \ReflectionClass( 'Progress_Planner\Base' ); + + if ( ! \preg_match( '/function progress_planner\(\)/', $this->get_source() ) ) { + $failures[] = 'The progress_planner() function is no longer defined.'; + } + + foreach ( $this->contract['services'] ?? [] as $getter => $methods ) { + // Real methods on Base (e.g. the_asset, get_file_version). + if ( $base->hasMethod( $getter ) ) { + continue; + } + + $class_name = self::getter_to_class( $getter ); + if ( null === $class_name ) { + $failures[] = "progress_planner()->{$getter}() no longer resolves to a class."; + continue; + } + + foreach ( $methods as $method ) { + if ( ! \method_exists( $class_name, $method ) ) { + $failures[] = "progress_planner()->{$getter}()->{$method}(): {$class_name}::{$method}() no longer exists."; + } elseif ( ! ( new \ReflectionMethod( $class_name, $method ) )->isPublic() ) { + $failures[] = "progress_planner()->{$getter}()->{$method}(): {$class_name}::{$method}() is no longer public."; + } + } + } + return $failures; + } + + /** + * Resolve a magic getter name to a class, mirroring Base::__call(). + * + * @param string $getter The getter, e.g. get_ui__branding. + * + * @return string|null + */ + public static function getter_to_class( $getter ) { + $name = \substr( $getter, 4 ); + $class_name = \implode( '\\', \explode( '__', $name ) ); + $class_name = 'Progress_Planner\\' . \implode( '_', \array_map( 'ucfirst', \explode( '_', $class_name ) ) ); + + if ( \class_exists( $class_name ) ) { + return ( new \ReflectionClass( $class_name ) )->getName(); + } + + $deprecations = \Progress_Planner\Utils\Deprecations::BASE_METHODS; + if ( isset( $deprecations[ $getter ] ) ) { + return self::getter_to_class( $deprecations[ $getter ][0] ); + } + + return null; + } + + /** + * Constants pp-hosts reads must still be defined, and constants pp-hosts + * defines for Progress Planner must still be read by it. + * + * @return string[] + */ + private function check_constants() { + $failures = []; + foreach ( $this->contract['constants']['reads'] ?? [] as $constant ) { + if ( ! \preg_match( '/define\(\s*[\'"]' . \preg_quote( $constant, '/' ) . '[\'"]/', $this->get_source() ) ) { + $failures[] = "Constant {$constant} is no longer defined."; + } + } + foreach ( $this->contract['constants']['provides'] ?? [] as $constant ) { + if ( false === \strpos( $this->get_source(), $constant ) ) { + $failures[] = "Constant {$constant} (set by pp-hosts) is no longer read by Progress Planner."; + } + } + return $failures; + } + + /** + * Dashboard widgets pp-hosts removes must still exist under the same ID. + * + * @return string[] + */ + private function check_meta_boxes() { + $failures = []; + foreach ( $this->contract['meta_boxes'] ?? [] as $id ) { + if ( false !== \strpos( $this->get_source(), "'{$id}'" ) || $this->is_dashboard_widget( $id ) ) { + continue; + } + $failures[] = "Dashboard widget '{$id}' no longer exists (pp-hosts removes it)."; + } + return $failures; + } + + /** + * Whether a widget ID is registered by Admin\Dashboard_Widget, which builds + * it as "progress_planner_dashboard_widget_{$this->id}". + * + * @param string $id The widget ID, e.g. progress_planner_dashboard_widget_score. + * + * @return bool + */ + private function is_dashboard_widget( $id ) { + $prefix = 'progress_planner_dashboard_widget_'; + if ( 0 !== \strpos( $id, $prefix ) || false === \strpos( $this->get_source(), "\"{$prefix}{\$this->id}\"" ) ) { + return false; + } + + $suffix = \substr( $id, \strlen( $prefix ) ); + $class_name = 'Progress_Planner\\Admin\\Dashboard_Widget_' . \implode( '_', \array_map( 'ucfirst', \explode( '_', $suffix ) ) ); + if ( ! \class_exists( $class_name ) ) { + return false; + } + + $defaults = ( new \ReflectionClass( $class_name ) )->getDefaultProperties(); + return isset( $defaults['id'] ) && $suffix === $defaults['id']; + } + + /** + * Assets pp-hosts enqueues or prints from Progress Planner must still exist. + * + * @return string[] + */ + private function check_assets() { + $failures = []; + foreach ( $this->contract['assets'] ?? [] as $asset ) { + if ( ! \file_exists( $this->pp_dir . '/' . \ltrim( $asset, '/' ) ) ) { + $failures[] = "Asset {$asset} no longer exists."; + } + } + return $failures; + } + + /** + * Get all Progress Planner PHP source (excluding tests and dependencies). + * + * @return string + */ + private function get_source() { + if ( null !== $this->source ) { + return $this->source; + } + + $this->source = ''; + $iterator = new \RecursiveIteratorIterator( + new \RecursiveCallbackFilterIterator( + new \RecursiveDirectoryIterator( $this->pp_dir, \FilesystemIterator::SKIP_DOTS ), + static function ( $file ) { + return ! \in_array( $file->getFilename(), [ 'vendor', 'node_modules', 'tests', '.git' ], true ); + } + ) + ); + foreach ( $iterator as $file ) { + if ( 'php' === $file->getExtension() ) { + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Local file. + $this->source .= \file_get_contents( $file->getPathname() ) . "\n"; + } + } + + return $this->source; + } +} diff --git a/tests/contract/pp-hosts-contract.json b/tests/contract/pp-hosts-contract.json new file mode 100644 index 000000000..567fda938 --- /dev/null +++ b/tests/contract/pp-hosts-contract.json @@ -0,0 +1,581 @@ +{ + "_comment": "Generated by pp-hosts/bin/build-pp-contract.php. Do not edit by hand. Copy to progress-planner/tests/contract/pp-hosts-contract.json.", + "hooks": { + "listens": [ + "progress_planner_admin_page_after_widgets", + "progress_planner_admin_widgets", + "progress_planner_dashboard_sidebar", + "progress_planner_data_collectors", + "progress_planner_get_badge", + "progress_planner_latest_completed_badge", + "progress_planner_popover_after_badge_groups", + "progress_planner_show_onboarding", + "progress_planner_suggested_task_completed", + "progress_planner_suggested_tasks_in_rest_format", + "progress_planner_suggested_tasks_providers", + "progress_planner_task_activity_category" + ], + "fires": [ + "progress_planner_get_badge", + "progress_planner_guided_tours" + ] + }, + "classes": { + "PP_Hosts\\First_Run\\Badges_Dashboard\\Badges\\Badge_Flying_Start": { + "extends": "Progress_Planner\\Badges\\Badge", + "abstract": false, + "overrides": { + "get_description": { + "params": 0, + "has_return_type": false, + "static": false + }, + "get_name": { + "params": 0, + "has_return_type": false, + "static": false + }, + "progress_callback": { + "params": 1, + "has_return_type": false, + "static": false + } + }, + "calls": [ + "get_saved", + "save_progress" + ], + "properties": { + "background": "protected", + "id": "protected" + }, + "constants": [], + "chain_methods": [ + "get_description", + "get_name", + "progress_callback" + ] + }, + "PP_Hosts\\First_Run\\Badges_Dashboard\\Badges\\Badge_Go_To_Publish": { + "extends": "Progress_Planner\\Badges\\Badge", + "abstract": false, + "overrides": { + "get_description": { + "params": 0, + "has_return_type": false, + "static": false + }, + "get_name": { + "params": 0, + "has_return_type": false, + "static": false + }, + "progress_callback": { + "params": 1, + "has_return_type": false, + "static": false + } + }, + "calls": [ + "get_saved", + "save_progress" + ], + "properties": { + "background": "protected", + "id": "protected" + }, + "constants": [], + "chain_methods": [ + "get_description", + "get_name", + "progress_callback" + ] + }, + "PP_Hosts\\First_Run\\Badges_Dashboard\\Badges\\Badge_Onboarding": { + "extends": "Progress_Planner\\Badges\\Badge", + "abstract": true, + "overrides": {}, + "calls": [], + "properties": { + "background": "protected" + }, + "constants": [], + "chain_methods": [] + }, + "PP_Hosts\\First_Run\\Badges_Dashboard\\Tasks\\Task_Check_About_Page": { + "extends": "Progress_Planner\\Suggested_Tasks\\Providers\\Tasks", + "abstract": false, + "overrides": { + "get_description": { + "params": 0, + "has_return_type": false, + "static": false + }, + "get_title": { + "params": 0, + "has_return_type": false, + "static": false + } + }, + "calls": [], + "properties": { + "priority": "protected" + }, + "constants": [ + "PROVIDER_ID" + ], + "chain_methods": [ + "get_description", + "get_provider_id", + "get_title", + "should_add_task" + ] + }, + "PP_Hosts\\First_Run\\Badges_Dashboard\\Tasks\\Task_Check_Categories": { + "extends": "Progress_Planner\\Suggested_Tasks\\Providers\\Tasks", + "abstract": false, + "overrides": { + "get_description": { + "params": 0, + "has_return_type": false, + "static": false + }, + "get_title": { + "params": 0, + "has_return_type": false, + "static": false + } + }, + "calls": [], + "properties": { + "priority": "protected" + }, + "constants": [ + "PROVIDER_ID" + ], + "chain_methods": [ + "get_description", + "get_provider_id", + "get_title", + "should_add_task" + ] + }, + "PP_Hosts\\First_Run\\Badges_Dashboard\\Tasks\\Task_Check_Contact_Info": { + "extends": "Progress_Planner\\Suggested_Tasks\\Providers\\Tasks", + "abstract": false, + "overrides": { + "get_description": { + "params": 0, + "has_return_type": false, + "static": false + }, + "get_title": { + "params": 0, + "has_return_type": false, + "static": false + } + }, + "calls": [], + "properties": { + "priority": "protected" + }, + "constants": [ + "PROVIDER_ID" + ], + "chain_methods": [ + "get_description", + "get_provider_id", + "get_title", + "should_add_task" + ] + }, + "PP_Hosts\\First_Run\\Badges_Dashboard\\Tasks\\Task_Check_Generated_Pages": { + "extends": "Progress_Planner\\Suggested_Tasks\\Providers\\Tasks", + "abstract": false, + "overrides": { + "get_description": { + "params": 0, + "has_return_type": false, + "static": false + }, + "get_title": { + "params": 0, + "has_return_type": false, + "static": false + } + }, + "calls": [], + "properties": { + "priority": "protected" + }, + "constants": [ + "PROVIDER_ID" + ], + "chain_methods": [ + "get_description", + "get_provider_id", + "get_title", + "should_add_task" + ] + }, + "PP_Hosts\\First_Run\\Badges_Dashboard\\Tasks\\Task_Check_Generated_Posts": { + "extends": "Progress_Planner\\Suggested_Tasks\\Providers\\Tasks", + "abstract": false, + "overrides": { + "get_description": { + "params": 0, + "has_return_type": false, + "static": false + }, + "get_title": { + "params": 0, + "has_return_type": false, + "static": false + } + }, + "calls": [], + "properties": { + "priority": "protected" + }, + "constants": [ + "PROVIDER_ID" + ], + "chain_methods": [ + "get_description", + "get_provider_id", + "get_title", + "should_add_task" + ] + }, + "PP_Hosts\\First_Run\\Badges_Dashboard\\Tasks\\Task_Check_Homepage": { + "extends": "Progress_Planner\\Suggested_Tasks\\Providers\\Tasks", + "abstract": false, + "overrides": { + "get_description": { + "params": 0, + "has_return_type": false, + "static": false + }, + "get_title": { + "params": 0, + "has_return_type": false, + "static": false + } + }, + "calls": [], + "properties": { + "priority": "protected" + }, + "constants": [ + "PROVIDER_ID" + ], + "chain_methods": [ + "get_description", + "get_provider_id", + "get_title", + "should_add_task" + ] + }, + "PP_Hosts\\First_Run\\Badges_Dashboard\\Tasks\\Task_Check_Reviews": { + "extends": "Progress_Planner\\Suggested_Tasks\\Providers\\Tasks", + "abstract": false, + "overrides": { + "get_description": { + "params": 0, + "has_return_type": false, + "static": false + }, + "get_title": { + "params": 0, + "has_return_type": false, + "static": false + } + }, + "calls": [], + "properties": { + "priority": "protected" + }, + "constants": [ + "PROVIDER_ID" + ], + "chain_methods": [ + "get_description", + "get_provider_id", + "get_title", + "should_add_task" + ] + }, + "PP_Hosts\\First_Run\\Badges_Dashboard\\Tasks\\Task_Guided_Tour": { + "extends": "Progress_Planner\\Suggested_Tasks\\Providers\\Tasks", + "abstract": true, + "overrides": { + "get_provider_id": { + "params": 0, + "has_return_type": false, + "static": false + }, + "should_add_task": { + "params": 0, + "has_return_type": false, + "static": false + } + }, + "calls": [], + "properties": { + "is_dismissable": "protected" + }, + "constants": [ + "IS_ONBOARDING_TASK", + "PROVIDER_ID" + ], + "chain_methods": [ + "get_provider_id", + "should_add_task" + ] + }, + "PP_Hosts\\First_Run\\Badges_Dashboard\\Tasks\\Task_Write_First_Post": { + "extends": "Progress_Planner\\Suggested_Tasks\\Providers\\Tasks", + "abstract": false, + "overrides": { + "get_description": { + "params": 0, + "has_return_type": false, + "static": false + }, + "get_title": { + "params": 0, + "has_return_type": false, + "static": false + } + }, + "calls": [], + "properties": { + "priority": "protected" + }, + "constants": [ + "PROVIDER_ID" + ], + "chain_methods": [ + "get_description", + "get_provider_id", + "get_title", + "should_add_task" + ] + }, + "PP_Hosts\\First_Run\\Badges_Dashboard\\Widgets\\Widget_Desktop_Tour_Prompt": { + "extends": "Progress_Planner\\Admin\\Widgets\\Widget", + "abstract": false, + "overrides": { + "enqueue_scripts": { + "params": 0, + "has_return_type": false, + "static": false + }, + "enqueue_styles": { + "params": 0, + "has_return_type": false, + "static": false + }, + "render": { + "params": 0, + "has_return_type": false, + "static": false + } + }, + "calls": [], + "properties": { + "id": "protected", + "width": "protected" + }, + "constants": [], + "chain_methods": [ + "enqueue_scripts", + "enqueue_styles", + "render" + ] + }, + "PP_Hosts\\First_Run\\Badges_Dashboard\\Widgets\\Widget_Flying_Start": { + "extends": "Progress_Planner\\Admin\\Widgets\\Widget", + "abstract": false, + "overrides": { + "enqueue_scripts": { + "params": 0, + "has_return_type": false, + "static": false + }, + "enqueue_styles": { + "params": 0, + "has_return_type": false, + "static": false + }, + "render": { + "params": 0, + "has_return_type": false, + "static": false + } + }, + "calls": [], + "properties": { + "id": "protected", + "width": "protected" + }, + "constants": [], + "chain_methods": [ + "enqueue_scripts", + "enqueue_styles", + "render" + ] + }, + "PP_Hosts\\First_Run\\Badges_Dashboard\\Widgets\\Widget_Go_To_Publish": { + "extends": "Progress_Planner\\Admin\\Widgets\\Widget", + "abstract": false, + "overrides": { + "enqueue_scripts": { + "params": 0, + "has_return_type": false, + "static": false + }, + "enqueue_styles": { + "params": 0, + "has_return_type": false, + "static": false + }, + "render": { + "params": 0, + "has_return_type": false, + "static": false + } + }, + "calls": [], + "properties": { + "id": "protected", + "width": "protected" + }, + "constants": [], + "chain_methods": [ + "enqueue_scripts", + "enqueue_styles", + "render" + ] + }, + "PP_Hosts\\First_Run\\Badges_Dashboard\\Widgets\\Widget_Onboarding_Gauge": { + "extends": "Progress_Planner\\Admin\\Widgets\\Widget", + "abstract": false, + "overrides": { + "__construct": { + "params": 1, + "has_return_type": false, + "static": false + }, + "enqueue_scripts": { + "params": 0, + "has_return_type": false, + "static": false + }, + "enqueue_styles": { + "params": 0, + "has_return_type": false, + "static": false + }, + "render": { + "params": 0, + "has_return_type": false, + "static": false + } + }, + "calls": [], + "properties": { + "force_last_column": "protected", + "id": "protected", + "width": "protected" + }, + "constants": [], + "chain_methods": [ + "__construct", + "enqueue_scripts", + "enqueue_styles", + "get_badge", + "render" + ] + }, + "PP_Hosts\\First_Run\\Badges_Dashboard\\Widgets\\Widget_Onboarding_Info": { + "extends": "Progress_Planner\\Admin\\Widgets\\Widget", + "abstract": false, + "overrides": { + "enqueue_scripts": { + "params": 0, + "has_return_type": false, + "static": false + }, + "enqueue_styles": { + "params": 0, + "has_return_type": false, + "static": false + }, + "render": { + "params": 0, + "has_return_type": false, + "static": false + } + }, + "calls": [], + "properties": { + "id": "protected", + "width": "protected" + }, + "constants": [], + "chain_methods": [ + "enqueue_scripts", + "enqueue_styles", + "render" + ] + } + }, + "services": { + "get_admin__enqueue": [ + "enqueue_script", + "enqueue_style" + ], + "get_file_version": [], + "get_plugin_upgrade_tasks": [ + "add_upgrade_tasks_popover" + ], + "get_settings": [ + "get", + "set" + ], + "get_ui__branding": [ + "enqueue_inline_css", + "get_branding_id", + "get_widget_title" + ], + "the_asset": [] + }, + "constants": { + "reads": [ + "PROGRESS_PLANNER_DIR", + "PROGRESS_PLANNER_URL" + ], + "provides": [ + "PROGRESS_PLANNER_BRANDING_ID" + ] + }, + "meta_boxes": [ + "progress_planner_dashboard_widget_score", + "progress_planner_dashboard_widget_todo" + ], + "assets": [ + "assets/css/page-widgets/badge-streak-content.css", + "assets/css/page-widgets/suggested-tasks.css", + "assets/css/suggested-task.css", + "assets/images/icon_info.svg", + "assets/js/web-components/prpl-badge.js", + "assets/js/web-components/prpl-gauge.js", + "assets/js/widgets/suggested-tasks.js" + ], + "known_issues": { + "assets/css/page-widgets/badge-streak-content.css": "Enqueued by first-run/badges-dashboard/widgets/class-widget-onboarding-gauge.php, but it only ever existed on the reverted split-widgets branch. Enqueue::enqueue_style() silently skips it.", + "progress_planner_dashboard_sidebar": "pp-hosts first-run/guided-tour/class-integration.php hooks this, but PP never fires it: it was only added in an unmerged PP commit (244b8eb9d \"hello guided tour\"). render_dashboard_launcher() never runs from this hook.", + "progress_planner_show_onboarding": "PP removed this filter in f82660b13 (2025-12-26). hosts/shared/class-yourhosting-base.php still filters it to disable onboarding for YourHosting partners, which now does nothing." + } +} diff --git a/tests/phpunit/test-pp-hosts-contract.php b/tests/phpunit/test-pp-hosts-contract.php new file mode 100644 index 000000000..c17e479fb --- /dev/null +++ b/tests/phpunit/test-pp-hosts-contract.php @@ -0,0 +1,41 @@ +check(); + + $message = ''; + foreach ( $failures as $section => $messages ) { + $message .= "\n[{$section}]\n - " . \implode( "\n - ", $messages ); + } + + $this->assertEmpty( $failures, "Progress Planner no longer satisfies the pp-hosts contract:{$message}" ); + } +}