diff --git a/modules/project_browser_devel/project_browser_devel.install b/modules/project_browser_devel/project_browser_devel.install index b86aaffb0a053f2708eb645211966389c20a9c26..ffc19ab2361e07482a630b2f55629a1493548bda 100644 --- a/modules/project_browser_devel/project_browser_devel.install +++ b/modules/project_browser_devel/project_browser_devel.install @@ -10,7 +10,7 @@ use Drupal\project_browser\Plugin\ProjectBrowserSourceManager; /** * Implements hook_install(). */ -function project_browser_devel_install() { +function project_browser_devel_install(): void { // Set the new random data generator as plugin and keep the current one. $configFactory = \Drupal::configFactory(); $current_source_plugin = $configFactory->getEditable('project_browser.admin_settings') @@ -27,7 +27,7 @@ function project_browser_devel_install() { /** * Implements hook_uninstall(). */ -function project_browser_devel_uninstall() { +function project_browser_devel_uninstall(): void { // Set the previous plugin. $admin_settings = \Drupal::configFactory()->getEditable('project_browser.admin_settings'); $enabled_sources = $admin_settings->get('enabled_sources'); diff --git a/modules/project_browser_devel/src/Plugin/ProjectBrowserSource/RandomDataPlugin.php b/modules/project_browser_devel/src/Plugin/ProjectBrowserSource/RandomDataPlugin.php index 3fb484262afee880487f9dec31de2bd5f4297d56..7b4a9cc65add3fd579482ae48dc33bcc9219a3d7 100644 --- a/modules/project_browser_devel/src/Plugin/ProjectBrowserSource/RandomDataPlugin.php +++ b/modules/project_browser_devel/src/Plugin/ProjectBrowserSource/RandomDataPlugin.php @@ -25,21 +25,21 @@ use Symfony\Component\DependencyInjection\ContainerInterface; * local_task = {} * ) */ -class RandomDataPlugin extends ProjectBrowserSourceBase { +final class RandomDataPlugin extends ProjectBrowserSourceBase { /** * Utility to create random data. * * @var \Drupal\Component\Utility\Random */ - protected $randomGenerator; + protected Random $randomGenerator; /** * ProjectBrowser cache bin. * * @var \Drupal\Core\Cache\CacheBackendInterface */ - protected $cacheBin; + protected CacheBackendInterface $cacheBin; /** * Constructs a MockDrupalDotOrg object. @@ -62,7 +62,7 @@ class RandomDataPlugin extends ProjectBrowserSourceBase { /** * {@inheritdoc} */ - public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static { return new static( $configuration, $plugin_id, @@ -80,7 +80,7 @@ class RandomDataPlugin extends ProjectBrowserSourceBase { * @return array * Array of random IDs and names. */ - protected function getRandomIdsAndNames($array_length = 4): array { + protected function getRandomIdsAndNames(int $array_length = 4): array { $data = []; for ($i = 0; $i < $array_length; $i++) { $data[] = [ @@ -98,7 +98,7 @@ class RandomDataPlugin extends ProjectBrowserSourceBase { * @return int * Random timestamp. */ - protected function getRandomDate() { + protected function getRandomDate(): int { return rand(strtotime('2 years ago'), strtotime('today')); } @@ -163,17 +163,17 @@ class RandomDataPlugin extends ProjectBrowserSourceBase { // Filter by project machine name. if (!empty($query['machine_name'])) { - $projects = array_filter($projects, fn(Project $project) => $project->machineName === $query['machine_name']); + $projects = array_filter($projects, fn(Project $project): bool => $project->machineName === $query['machine_name']); } // Filter by categories. if (!empty($query['categories'])) { - $projects = array_filter($projects, fn(Project $project) => array_intersect(array_column($project->categories, 'id'), explode(',', $query['categories']))); + $projects = array_filter($projects, fn(Project $project): bool => empty(array_intersect(array_column($project->categories, 'id'), explode(',', $query['categories'])))); } // Filter by search text. if (!empty($query['search'])) { - $projects = array_filter($projects, fn(Project $project) => stripos($project->title, $query['search']) !== FALSE); + $projects = array_filter($projects, fn(Project $project): bool => stripos($project->title, $query['search']) !== FALSE); } return $this->createResultsPage($projects); @@ -181,6 +181,9 @@ class RandomDataPlugin extends ProjectBrowserSourceBase { /** * Gets the project data from cache if available, or builds it if not. + * + * @return \Drupal\project_browser\ProjectBrowser\Project[] + * An array of projects. */ private function getProjectData(): array { $stored_projects = $this->cacheBin->get('RandomData:projects'); @@ -199,14 +202,14 @@ class RandomDataPlugin extends ProjectBrowserSourceBase { if ($i !== 0) { $project_images[] = [ 'file' => [ - 'uri' => str_replace(4, 5, $good_image), + 'uri' => str_replace('4', '5', $good_image), 'resource' => 'image', ], 'alt' => $machine_name . ' something', ]; $project_images[] = [ 'file' => [ - 'uri' => str_replace(4, 6, $good_image), + 'uri' => str_replace('4', '6', $good_image), 'resource' => 'image', ], 'alt' => $machine_name . ' another thing', diff --git a/modules/project_browser_source_example/project_browser_source_example.install b/modules/project_browser_source_example/project_browser_source_example.install index 19a515978151549377bf3046f7e04d474a1d4475..c999f3663bd927c61c2e5990258eaaa712050382 100644 --- a/modules/project_browser_source_example/project_browser_source_example.install +++ b/modules/project_browser_source_example/project_browser_source_example.install @@ -10,7 +10,7 @@ /** * Implements hook_install(). */ -function project_browser_source_example_install() { +function project_browser_source_example_install(): void { $configFactory = \Drupal::configFactory(); $current_source_plugin = $configFactory->getEditable('project_browser.admin_settings') ->get('enabled_sources'); @@ -23,7 +23,7 @@ function project_browser_source_example_install() { /** * Implements hook_uninstall(). */ -function project_browser_source_example_uninstall() { +function project_browser_source_example_uninstall(): void { $admin_settings = \Drupal::configFactory()->getEditable('project_browser.admin_settings'); $enabled_sources = $admin_settings->get('enabled_sources'); if (($key = array_search('project_browser_source_example', $enabled_sources)) !== FALSE) { diff --git a/modules/project_browser_source_example/src/Plugin/ProjectBrowserSource/ProjectBrowserSourceExample.php b/modules/project_browser_source_example/src/Plugin/ProjectBrowserSource/ProjectBrowserSourceExample.php index c7c429eab83e7126891f89e65665681d1f9a73f5..facbb1891d87a2a2f3ad756820ca528b66488135 100644 --- a/modules/project_browser_source_example/src/Plugin/ProjectBrowserSource/ProjectBrowserSourceExample.php +++ b/modules/project_browser_source_example/src/Plugin/ProjectBrowserSource/ProjectBrowserSourceExample.php @@ -2,7 +2,6 @@ namespace Drupal\project_browser_source_example\Plugin\ProjectBrowserSource; -use Drupal\Core\Extension\ModuleExtensionList; use Drupal\project_browser\Plugin\ProjectBrowserSourceBase; use Drupal\project_browser\ProjectBrowser\Project; use Drupal\project_browser\ProjectBrowser\ProjectsResultsPage; @@ -18,7 +17,7 @@ use Symfony\Component\HttpFoundation\RequestStack; * description = @Translation("Example source plugin for Project Browser."), * ) */ -class ProjectBrowserSourceExample extends ProjectBrowserSourceBase { +final class ProjectBrowserSourceExample extends ProjectBrowserSourceBase { /** * Constructor for example plugin. @@ -31,15 +30,12 @@ class ProjectBrowserSourceExample extends ProjectBrowserSourceBase { * The plugin implementation definition. * @param \Symfony\Component\HttpFoundation\RequestStack $requestStack * The request from the browser. - * @param \Drupal\Core\Extension\ModuleExtensionList $moduleExtensionList - * The module extension list. */ public function __construct( array $configuration, $plugin_id, $plugin_definition, protected readonly RequestStack $requestStack, - protected ModuleExtensionList $moduleExtensionList, ) { parent::__construct($configuration, $plugin_id, $plugin_definition); } @@ -47,13 +43,12 @@ class ProjectBrowserSourceExample extends ProjectBrowserSourceBase { /** * {@inheritdoc} */ - public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static { return new static( $configuration, $plugin_id, $plugin_definition, $container->get(RequestStack::class), - $container->get(ModuleExtensionList::class), ); } @@ -85,7 +80,7 @@ class ProjectBrowserSourceExample extends ProjectBrowserSourceBase { 'short_description' => 'Quick summary to show in the cards.', 'long_description' => 'Extended project information to show in the detail page', 'author' => 'Jane Doe', - 'logo' => $request->getSchemeAndHttpHost() . '/core/misc/logo/drupal-logo.svg', + 'logo' => $request?->getSchemeAndHttpHost() . '/core/misc/logo/drupal-logo.svg', 'created_at' => strtotime('1 year ago'), 'updated_at' => strtotime('1 month ago'), 'categories' => ['cat_1:Category 1'], @@ -152,7 +147,6 @@ class ProjectBrowserSourceExample extends ProjectBrowserSourceBase { // Images: Array of images using the same structure as $logo, above. images: [], ); - $pb_path = $this->moduleExtensionList->getPath('project_browser'); $projects[] = new Project( logo: $logo, // Maybe the source won't have all fields, but we still need to diff --git a/phpstan.neon b/phpstan.neon index ea077c431dbf5420f0c8903bed143a42c30743c7..76dde2088bba117a8d58282f01024deb0a8e92a3 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,7 +1,11 @@ parameters: - level: 1 + level: 8 + universalObjectCratesClasses: + - Drupal\Core\Extension\Extension reportUnmatchedIgnoredErrors: true excludePaths: + # The scripts directory does not contain runtime code. + - scripts # The node_modules contains some PHP to ignore. - sveltejs # The recipe form contains a couple of errors that cannot be ignored. @@ -9,9 +13,60 @@ parameters: - src/Form/RecipeForm.php # Every ignore should be documented. ignoreErrors: + # Ignore errors when using `array` as a return type. - - # @see https://www.drupal.org/docs/develop/development-tools/phpstan/handling-unsafe-usage-of-new-static#s-ignoring-the-issue - identifier: new.static + identifier: missingType.iterableValue + reportUnmatched: false + + ### Core testing suite + # Caused by missing return type on \Drupal\FunctionalJavascriptTests\WebDriverTestBase::assertSession(). + - + message: "#^Call to an undefined method Drupal\\\\Tests\\\\WebAssert\\:\\:assert[a-zA-Z]+\\(\\)\\.$#" + paths: + - tests/src/FunctionalJavascript + reportUnmatched: false + # Caused by missing return type on \Drupal\FunctionalJavascriptTests\WebDriverTestBase::assertSession(). + - + message: "#^Call to an undefined method Drupal\\\\Tests\\\\WebAssert\\:\\:wait[a-zA-Z]+\\(\\)\\.$#" + paths: + - tests/src/FunctionalJavascript + reportUnmatched: false + # Caused by \Drupal\KernelTests\KernelTestBase::$container having the wrong type. + - + message: "#^Property Drupal\\\\KernelTests\\\\KernelTestBase\\:\\:\\$container \\(Drupal\\\\Core\\\\DependencyInjection\\\\ContainerBuilder\\) does not accept Drupal\\\\Component\\\\DependencyInjection\\\\ContainerInterface\\.$#" + paths: + - tests/src/Kernel/DatabaseTablesTest.php + reportUnmatched: false + # Caused by \Drupal\Tests\user\Traits\UserCreationTrait::createUser() returning FALSE instead of throwing an exception. + - + message: "#^Parameter \\#1 \\$account of method Drupal\\\\Tests\\\\BrowserTestBase\\:\\:drupalLogin\\(\\) expects Drupal\\\\Core\\\\Session\\\\AccountInterface, Drupal\\\\user\\\\Entity\\\\User\\|false given\\.$#" + paths: + - tests/src/Functional + - tests/src/FunctionalJavascript + reportUnmatched: false + + ### Package Manager + # @todo Remove after resolving https://www.drupal.org/i/3501836. + # Caused by using self instead of static as a return type in \Drupal\fixture_manipulator\FixtureManipulator. + - + message: "#^Method Drupal\\\\fixture_manipulator\\\\FixtureManipulator\\:\\:commitChanges\\(\\) invoked with 0 parameters, 1 required\\.$#" + paths: + - tests/src/Kernel/InstallerTest.php + - tests/src/Kernel/CoreNotUpdatedValidatorTest.php + - tests/src/Kernel/PackageNotInstalledValidatorTest.php + reportUnmatched: false + # Caused by missing return type on \Drupal\Tests\package_manager\Traits\FixtureManipulatorTrait::getStageFixtureManipulator(). + - + message: "#^Call to an undefined method object\\:\\:setCorePackageVersion\\(\\)\\.$#" + paths: + - tests/src/Kernel/CoreNotUpdatedValidatorTest.php + reportUnmatched: false + # Caused by missing @throws on \Drupal\package_manager\StageBase::apply(). + - + message: "#^Dead catch \\- Drupal\\\\package_manager\\\\Exception\\\\StageEventException is never thrown in the try block\\.$#" + paths: + - tests/src/Kernel/CoreNotUpdatedValidatorTest.php + reportUnmatched: false # @todo: Remove the following rules when support is dropped for Drupal 10.2, which does not have recipes. - diff --git a/project_browser.install b/project_browser.install index 3cf1624f53adcea74973739ae87bbdd73155092f..8c5e62d7a085793c0883e5fad9d24f201be4b71f 100644 --- a/project_browser.install +++ b/project_browser.install @@ -13,7 +13,7 @@ use Drupal\Core\Recipe\Recipe; * Populates the project_browser_projects using a fixture with PHP serialized * items. */ -function project_browser_install() { +function project_browser_install(): void { if (class_exists(Recipe::class)) { $config = \Drupal::configFactory() ->getEditable('project_browser.admin_settings'); @@ -26,7 +26,7 @@ function project_browser_install() { /** * Implements hook_update_last_removed(). */ -function project_browser_update_last_removed() { +function project_browser_update_last_removed(): int { return 9017; } @@ -35,7 +35,7 @@ function project_browser_update_last_removed() { * * Remove disable_add_new_module setting. */ -function project_browser_update_9018() { +function project_browser_update_9018(): void { $config_factory = \Drupal::configFactory(); $config_factory->getEditable('project_browser.admin_settings') ->clear('disable_add_new_module') @@ -47,7 +47,7 @@ function project_browser_update_9018() { * * Remove the Drupal.org(Mock API) setting and tables. */ -function project_browser_update_9019() { +function project_browser_update_9019(): void { // Remove the mock from the enabled_sources, if present. $config = \Drupal::configFactory()->getEditable('project_browser.admin_settings'); $enabled_sources = $config->get('enabled_sources'); diff --git a/project_browser.module b/project_browser.module index a823a5531ffb805cf64b7cfa3fd5f1b66d4d1c67..211b4d17c3c9e7e5013aed3bf79cb811b1d359bc 100644 --- a/project_browser.module +++ b/project_browser.module @@ -13,10 +13,10 @@ use Drupal\project_browser\Plugin\ProjectBrowserSource\Recipes; /** * Implements hook_help(). */ -function project_browser_help($route_name, RouteMatchInterface $route_match) { +function project_browser_help(string $route_name, RouteMatchInterface $route_match): string { + $output = ''; switch ($route_name) { case 'help.page.project_browser': - $output = ''; $output .= '<h3>' . t('About') . '</h3>'; $output .= '<p>' . t("The Project Browser module allows users to easily search for available Drupal modules from your site. Enhanced filtering is provided so you can find what you need.") . '</p>'; $output .= '<p>' . t('For more information, see the <a href=":project_browser">online documentation for the Project Browser module</a>.', [':project_browser' => 'https://www.drupal.org/docs/contributed-modules/project-browser']) . '</p>'; @@ -28,14 +28,14 @@ function project_browser_help($route_name, RouteMatchInterface $route_match) { $output .= '<dd>' . t('Users who have the <em>Administer site configuration</em> permission can select where to search for modules from the <a href=":project_browser_settings">Project Browser settings page</a>. This can include the modules already on your site as well as contributed modules on Drupal.org', [':project_browser_settings' => Url::fromRoute('project_browser.settings')->toString()]) . '</dd>'; $output .= '</dl>'; - return $output; } + return $output; } /** * Implements hook_theme(). */ -function project_browser_theme() { +function project_browser_theme(): array { return [ 'project_browser_main_app' => [ 'variables' => [], diff --git a/src/Activator.php b/src/Activator.php index 19245c186ca33fb80026afc2e28d7b9789adac28..0e363bf816a19a8c3f9768dfdb88d1c8d5532246 100644 --- a/src/Activator.php +++ b/src/Activator.php @@ -69,7 +69,8 @@ final class Activator implements ActivatorInterface { */ public function supports(Project $project): bool { try { - return $this->getActivatorForProject($project) instanceof ActivatorInterface; + $this->getActivatorForProject($project); + return TRUE; } catch (\InvalidArgumentException) { return FALSE; diff --git a/src/Controller/InstallerController.php b/src/Controller/InstallerController.php index db85b666bf19b813e0ca2526e525103af8416c3e..78ba9e795fd78252d52ae562f9aee220154025cf 100644 --- a/src/Controller/InstallerController.php +++ b/src/Controller/InstallerController.php @@ -13,16 +13,18 @@ use Drupal\project_browser\ComposerInstaller\Installer; use Drupal\project_browser\EnabledSourceHandler; use Drupal\project_browser\InstallState; use Drupal\project_browser\ProjectBrowser\Project; +use Drupal\project_browser_test\Plugin\ProjectBrowserSource\ProjectBrowserTestMock; use Psr\Log\LoggerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; /** * Defines a controller to install projects via UI. */ -class InstallerController extends ControllerBase { +final class InstallerController extends ControllerBase { /** * No require or install in progress for a given module. @@ -64,7 +66,11 @@ class InstallerController extends ControllerBase { /** * {@inheritdoc} */ - public static function create(ContainerInterface $container) { + public static function create(ContainerInterface $container): static { + // @see \Drupal\project_browser\ProjectBrowserServiceProvider. + assert($container->get(Installer::class) instanceof Installer); + assert($container->get(InstallState::class) instanceof InstallState); + return new static( $container->get(Installer::class), $container->get(EnabledSourceHandler::class), @@ -78,7 +84,7 @@ class InstallerController extends ControllerBase { /** * Checks if UI install is enabled on the site. */ - public function access() :AccessResult { + public function access(): AccessResult { $ui_install = $this->config('project_browser.admin_settings')->get('allow_ui_install'); return AccessResult::allowedIf((bool) $ui_install); } @@ -131,7 +137,7 @@ class InstallerController extends ControllerBase { /** * Provides a JSON response for a given error. * - * @param \Exception $e + * @param \Throwable $e * The error that occurred. * @param string $phase * The phase the error occurred in. @@ -139,7 +145,7 @@ class InstallerController extends ControllerBase { * @return \Symfony\Component\HttpFoundation\JsonResponse * Provides an error message to be displayed by the Project Browser UI. */ - private function errorResponse(\Exception $e, string $phase = ''): JsonResponse { + private function errorResponse(\Throwable $e, string $phase = ''): JsonResponse { $exception_type_short = (new \ReflectionClass($e))->getShortName(); $exception_message = $e->getMessage(); $response_body = ['message' => "$exception_type_short: $exception_message"]; @@ -213,7 +219,7 @@ class InstallerController extends ControllerBase { // accessed after. This final check ensures a destroy is not attempted // during apply. if ($this->installer->isApplying()) { - throw new StageException('Another project is being added. Try again in a few minutes.'); + throw new StageException($this->installer, 'Another project is being added. Try again in a few minutes.'); } // Adding the TRUE parameter to destroy is dangerous, but we provide it @@ -351,6 +357,7 @@ class InstallerController extends ControllerBase { if ($source === NULL) { return new JsonResponse(['message' => "Cannot download $project->id from any available source"], 500); } + assert($source instanceof ProjectBrowserTestMock); if (!$source->isProjectSafe($project)) { return new JsonResponse(['message' => "$project->machineName is not safe to add because its security coverage has been revoked"], 500); } @@ -439,10 +446,10 @@ class InstallerController extends ControllerBase { * @param \Symfony\Component\HttpFoundation\Request $request * The request. * - * @return \Symfony\Component\HttpFoundation\JsonResponse + * @return \Symfony\Component\HttpFoundation\Response * Status message. */ - public function activate(Request $request): JsonResponse { + public function activate(Request $request): Response { foreach ($request->toArray() as $project) { $project = $this->enabledSourceHandler->getStoredProject($project); $this->installState->setState($project, 'activating'); diff --git a/src/Controller/ProjectBrowserEndpointController.php b/src/Controller/ProjectBrowserEndpointController.php index b39b59498468583d1ea204b4daa8fb7f1fe2421c..ad6666740318e990e2089e4110753ef8c2a66020 100644 --- a/src/Controller/ProjectBrowserEndpointController.php +++ b/src/Controller/ProjectBrowserEndpointController.php @@ -12,7 +12,7 @@ use Symfony\Component\HttpFoundation\Response; /** * Controller for the proxy layer. */ -class ProjectBrowserEndpointController extends ControllerBase { +final class ProjectBrowserEndpointController extends ControllerBase { /** * Constructor for endpoint controller. @@ -27,7 +27,7 @@ class ProjectBrowserEndpointController extends ControllerBase { /** * {@inheritdoc} */ - public static function create(ContainerInterface $container) { + public static function create(ContainerInterface $container): static { return new static( $container->get(EnabledSourceHandler::class), ); @@ -44,9 +44,10 @@ class ProjectBrowserEndpointController extends ControllerBase { * @return \Symfony\Component\HttpFoundation\JsonResponse * Typically a project listing. */ - public function getAllProjects(Request $request) { + public function getAllProjects(Request $request): JsonResponse { $id = $request->query->get('id'); if ($id) { + assert(is_string($id)); return new JsonResponse($this->enabledSource->getStoredProject($id)); } @@ -125,7 +126,7 @@ class ProjectBrowserEndpointController extends ControllerBase { /** * Returns a list of categories. */ - public function getAllCategories() { + public function getAllCategories(): JsonResponse { $current_sources = $this->enabledSource->getCurrentSources(); if (!$current_sources) { return new JsonResponse([], Response::HTTP_ACCEPTED); diff --git a/src/Drush/Commands/ProjectBrowserCommands.php b/src/Drush/Commands/ProjectBrowserCommands.php index 34e89aa41fa3013c213c89ea7c48c5aba5b41ed5..89d132808382962c068e41a4f3759d8b0b2c179c 100644 --- a/src/Drush/Commands/ProjectBrowserCommands.php +++ b/src/Drush/Commands/ProjectBrowserCommands.php @@ -28,7 +28,7 @@ final class ProjectBrowserCommands extends DrushCommands { #[Usage(name: 'project-browser:storage-clear', description: 'Clear stored Project Browser data')] public function storageClear(): void { $this->enabledSourceHandler->clearStorage(); - $this->logger()->success(dt('Stored data from Project Browser sources have been cleared.')); + $this->logger()?->success(dt('Stored data from Project Browser sources have been cleared.')); } } diff --git a/src/Element/ProjectBrowser.php b/src/Element/ProjectBrowser.php index eff2aa0c07e777b290efe2e2d7026648d5de6a8f..757a955cd7a2c6165f9b6d4193454c606cbb5901 100644 --- a/src/Element/ProjectBrowser.php +++ b/src/Element/ProjectBrowser.php @@ -51,10 +51,13 @@ final class ProjectBrowser implements ElementInterface, ContainerFactoryPluginIn * {@inheritdoc} */ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static { + $install_readiness = $container->get(InstallReadiness::class, ContainerInterface::NULL_ON_INVALID_REFERENCE); + assert(is_null($install_readiness) || $install_readiness instanceof InstallReadiness); + return new static( $plugin_id, $plugin_definition, - $container->get(InstallReadiness::class, ContainerInterface::NULL_ON_INVALID_REFERENCE), + $install_readiness, $container->get(ModuleHandlerInterface::class), $container->get(ConfigFactoryInterface::class), ); @@ -123,7 +126,7 @@ final class ProjectBrowser implements ElementInterface, ContainerFactoryPluginIn ]; // @todo Fix https://www.drupal.org/node/3494512 to avoid adding // hard-coded values. #techdebt - if ($source->getPluginId() !== 'recipes' && $package_manager['available']) { + if ($source->getPluginId() !== 'recipes' && $package_manager['available'] && is_object($this->installReadiness)) { $package_manager = array_merge($package_manager, $this->installReadiness->validatePackageManager()); $package_manager['status_checked'] = TRUE; } diff --git a/src/Form/SettingsForm.php b/src/Form/SettingsForm.php index 30a5a0e490998f7037fe5835a774435e1f5938b3..3429dbd4b629f6723b61ab008c5d3d259709f9eb 100644 --- a/src/Form/SettingsForm.php +++ b/src/Form/SettingsForm.php @@ -16,7 +16,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface; /** * Settings form for Project Browser. */ -class SettingsForm extends ConfigFormBase { +final class SettingsForm extends ConfigFormBase { public function __construct( ConfigFactoryInterface $config_factory, @@ -32,7 +32,7 @@ class SettingsForm extends ConfigFormBase { /** * {@inheritdoc} */ - public static function create(ContainerInterface $container) { + public static function create(ContainerInterface $container): static { return new static( $container->get(ConfigFactoryInterface::class), $container->get(TypedConfigManagerInterface::class), @@ -109,6 +109,7 @@ class SettingsForm extends ConfigFormBase { '#attributes' => [ 'id' => 'project_browser', ], + '#tabledrag' => [], ]; $options = [ 'enabled' => $this->t('Enabled'), @@ -117,6 +118,7 @@ class SettingsForm extends ConfigFormBase { if (count($source_plugins) > 1) { $form['#attached']['library'][] = 'project_browser/tabledrag'; foreach ($options as $status => $title) { + assert(is_array($table['#tabledrag'])); $table['#tabledrag'][] = [ 'action' => 'match', 'relationship' => 'sibling', @@ -135,11 +137,11 @@ class SettingsForm extends ConfigFormBase { 'class' => ['status-title', 'status-title-' . $status], 'no_striping' => TRUE, ], - ]; - $table['status-' . $status]['title'] = [ - '#plain_text' => $title, - '#wrapper_attributes' => [ - 'colspan' => 4, + 'title' => [ + '#plain_text' => $title, + '#wrapper_attributes' => [ + 'colspan' => 4, + ], ], ]; @@ -204,7 +206,7 @@ class SettingsForm extends ConfigFormBase { /** * {@inheritdoc} */ - public function validateForm(array &$form, FormStateInterface $form_state) { + public function validateForm(array &$form, FormStateInterface $form_state): void { $all_plugins = $form_state->getValue('enabled_sources'); if (!array_key_exists('enabled', array_count_values(array_column($all_plugins, 'status')))) { $form_state->setErrorByName('enabled_sources', $this->t('At least one source plugin must be enabled.')); @@ -214,10 +216,10 @@ class SettingsForm extends ConfigFormBase { /** * {@inheritdoc} */ - public function submitForm(array &$form, FormStateInterface $form_state) { + public function submitForm(array &$form, FormStateInterface $form_state): void { $settings = $this->config('project_browser.admin_settings'); $all_plugins = $form_state->getValue('enabled_sources'); - $enabled_plugins = array_filter($all_plugins, fn($source) => $source['status'] === 'enabled'); + $enabled_plugins = array_filter($all_plugins, fn($source): bool => $source['status'] === 'enabled'); $settings ->set('enabled_sources', array_keys($enabled_plugins)) ->set('allow_ui_install', $form_state->getValue('allow_ui_install')) diff --git a/src/ModuleActivator.php b/src/ModuleActivator.php index 6517f7795ad941e7f6ca7bb690b133293e849616..f329fe3ab0a74f00181ad3be4b0e10ff177b813d 100644 --- a/src/ModuleActivator.php +++ b/src/ModuleActivator.php @@ -17,16 +17,13 @@ use Symfony\Component\HttpFoundation\Response; */ final class ModuleActivator implements ActivatorInterface { - use ActivationInstructionsTrait { - __construct as traitConstruct; - } + use ActivationInstructionsTrait; public function __construct( private readonly ModuleInstallerInterface $moduleInstaller, - ModuleExtensionList $moduleList, - FileUrlGeneratorInterface $fileUrlGenerator, + protected readonly ModuleExtensionList $moduleList, + protected readonly FileUrlGeneratorInterface $fileUrlGenerator, ) { - $this->traitConstruct($moduleList, $fileUrlGenerator); } /** @@ -60,7 +57,7 @@ final class ModuleActivator implements ActivatorInterface { /** * {@inheritdoc} */ - public function getInstructions(Project $project): string|Url|null { + public function getInstructions(Project $project): string|Url { if ($this->getStatus($project) === ActivationStatus::Present) { return Url::fromRoute('system.modules_list', options: [ 'fragment' => 'module-' . str_replace('_', '-', $project->machineName), diff --git a/src/Plugin/Derivative/LocalTaskDeriver.php b/src/Plugin/Derivative/LocalTaskDeriver.php index 11e8b20cec79db4ed494ad66093a6477d2fc04c1..25bfc6e4638a779c63c72e45a0a957f76e99efca 100644 --- a/src/Plugin/Derivative/LocalTaskDeriver.php +++ b/src/Plugin/Derivative/LocalTaskDeriver.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Drupal\project_browser\Plugin\Derivative; use Drupal\Component\Plugin\Derivative\DeriverBase; +use Drupal\Component\Plugin\PluginBase; use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\project_browser\EnabledSourceHandler; @@ -24,7 +25,7 @@ final class LocalTaskDeriver extends DeriverBase implements ContainerDeriverInte /** * {@inheritdoc} */ - public static function create(ContainerInterface $container, $base_plugin_id) { + public static function create(ContainerInterface $container, $base_plugin_id): static { return new static( $container->get(EnabledSourceHandler::class), ); @@ -50,7 +51,7 @@ final class LocalTaskDeriver extends DeriverBase implements ContainerDeriverInte $local_task['route_parameters'] = [ 'source' => $source_id, ]; - $derivative_id = str_replace($source::DERIVATIVE_SEPARATOR, '__', $source_id); + $derivative_id = str_replace(PluginBase::DERIVATIVE_SEPARATOR, '__', $source_id); $this->derivatives[$derivative_id] = $local_task; } } diff --git a/src/Plugin/ProjectBrowserSource/DrupalCore.php b/src/Plugin/ProjectBrowserSource/DrupalCore.php index 12aa0cb490320db4ae4a600511478306c7201a81..f8a48728608221ec90463cb15ff7377ae8780535 100644 --- a/src/Plugin/ProjectBrowserSource/DrupalCore.php +++ b/src/Plugin/ProjectBrowserSource/DrupalCore.php @@ -21,7 +21,7 @@ use Symfony\Component\HttpFoundation\RequestStack; * description = @Translation("Modules included in Drupal core"), * ) */ -class DrupalCore extends ProjectBrowserSourceBase { +final class DrupalCore extends ProjectBrowserSourceBase { /** * All core modules are covered under security policy. @@ -74,7 +74,7 @@ class DrupalCore extends ProjectBrowserSourceBase { /** * {@inheritdoc} */ - public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static { return new static( $configuration, $plugin_id, @@ -92,10 +92,10 @@ class DrupalCore extends ProjectBrowserSourceBase { * The array containing core modules, keyed by module machine name. */ protected function getCoreModules() { - $projects = array_filter($this->moduleExtensionList->reset()->getList(), fn(Extension $project) => $project->origin === 'core'); + $projects = array_filter($this->moduleExtensionList->reset()->getList(), fn(Extension $project): bool => $project->origin === 'core'); $include_tests = Settings::get('extension_discovery_scan_tests') || drupal_valid_test_ua(); if (!$include_tests) { - $projects = array_filter($projects, fn(Extension $project) => empty($project->info['hidden']) && $project->info['package'] !== 'Testing'); + $projects = array_filter($projects, fn(Extension $project): bool => empty($project->info['hidden']) && $project->info['package'] !== 'Testing'); } return $projects; } @@ -123,22 +123,22 @@ class DrupalCore extends ProjectBrowserSourceBase { // Filter by project machine name. if (!empty($query['machine_name'])) { - $projects = array_filter($projects, fn(Project $project) => $project->machineName === $query['machine_name']); + $projects = array_filter($projects, fn(Project $project): bool => $project->machineName === $query['machine_name']); } // Filter by coverage. if (!empty($query['security_advisory_coverage'])) { - $projects = array_filter($projects, fn(Project $project) => $project->isCovered); + $projects = array_filter($projects, fn(Project $project): bool => $project->isCovered ?? FALSE); } // Filter by categories. if (!empty($query['categories'])) { - $projects = array_filter($projects, fn(Project $project) => array_intersect(array_column($project->categories, 'id'), explode(',', $query['categories']))); + $projects = array_filter($projects, fn(Project $project): bool => empty(array_intersect(array_column($project->categories, 'id'), explode(',', $query['categories'])))); } // Filter by search text. if (!empty($query['search'])) { - $projects = array_filter($projects, fn(Project $project) => stripos($project->title, $query['search']) !== FALSE); + $projects = array_filter($projects, fn(Project $project): bool => stripos($project->title, $query['search']) !== FALSE); } // Filter by sorting criterion. @@ -181,7 +181,7 @@ class DrupalCore extends ProjectBrowserSourceBase { $returned_list[] = new Project( logo: [ 'file' => [ - 'uri' => $request->getSchemeAndHttpHost() . '/core/misc/logo/drupal-logo.svg', + 'uri' => $request?->getSchemeAndHttpHost() . '/core/misc/logo/drupal-logo.svg', 'resource' => 'image', ], 'alt' => '', diff --git a/src/Plugin/ProjectBrowserSource/DrupalDotOrgJsonApi.php b/src/Plugin/ProjectBrowserSource/DrupalDotOrgJsonApi.php index 8eff2b332aba2a20f4bc14b99d47ce46ccd2a2a9..3b8e26f0dfc65c2bd6c4ebfdc92d4c6ca4e50772 100644 --- a/src/Plugin/ProjectBrowserSource/DrupalDotOrgJsonApi.php +++ b/src/Plugin/ProjectBrowserSource/DrupalDotOrgJsonApi.php @@ -33,7 +33,7 @@ use Symfony\Component\HttpFoundation\Response; * } * ) */ -class DrupalDotOrgJsonApi extends ProjectBrowserSourceBase { +final class DrupalDotOrgJsonApi extends ProjectBrowserSourceBase { use StringTranslationTrait; @@ -98,7 +98,7 @@ class DrupalDotOrgJsonApi extends ProjectBrowserSourceBase { /** * {@inheritdoc} */ - public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static { return new static( $configuration, $plugin_id, @@ -159,7 +159,7 @@ class DrupalDotOrgJsonApi extends ProjectBrowserSourceBase { $response_data = Json::decode($response->getBody()->getContents()); $result['data'] = array_merge($result['data'], $response_data['data']); - if (!empty($response_data['included'])) { + if (!empty($response_data['included']) && !empty($result['included'])) { $result['included'] = array_merge($result['included'], $response_data['included']); } $iterations++; @@ -296,13 +296,13 @@ class DrupalDotOrgJsonApi extends ProjectBrowserSourceBase { } $api_response = $this->fetchProjects($query); - if (!is_array($api_response) || $api_response['code'] !== Response::HTTP_OK) { + if ($api_response['code'] !== Response::HTTP_OK) { $error_message = $api_response['message'] ?? $this->t('Error querying data.'); return $this->createResultsPage([], 0, $error_message); } $returned_list = []; - if (is_array($api_response) && !empty($api_response['list'])) { + if (!empty($api_response['list'])) { $related = !empty($api_response['related']) ? $api_response['related'] : NULL; $current_drupal_version = $this->getNumericSemverVersion(\Drupal::VERSION); $maintained_values = $filter_values['maintained'] ?? []; @@ -313,7 +313,7 @@ class DrupalDotOrgJsonApi extends ProjectBrowserSourceBase { $uid_info = $project['relationships']['uid']['data']; $maintenance_status = $project['relationships']['field_maintenance_status']['data'] ?? []; - if (!empty($maintenance_status)) { + if (is_array($maintenance_status)) { $maintenance_status = [ 'id' => $maintenance_status['id'], 'name' => $related[$maintenance_status['type']][$maintenance_status['id']]['name'], @@ -328,8 +328,8 @@ class DrupalDotOrgJsonApi extends ProjectBrowserSourceBase { ]; } - $module_categories = $project['relationships']['field_module_categories']['data'] ?? []; - if (!empty($module_categories)) { + $module_categories = $project['relationships']['field_module_categories']['data'] ?? NULL; + if (is_array($module_categories)) { $categories = []; foreach ($module_categories as $module_category) { $categories[] = [ @@ -340,8 +340,8 @@ class DrupalDotOrgJsonApi extends ProjectBrowserSourceBase { $module_categories = $categories; } - $project_images = $project['relationships']['field_project_images']['data'] ?? []; - if (!empty($project_images)) { + $project_images = $project['relationships']['field_project_images']['data'] ?? NULL; + if (is_array($project_images)) { $images = []; foreach ($project_images as $image) { $uri = self::DRUPAL_ORG_ENDPOINT . $related[$image['type']][$image['id']]['uri']['url']; @@ -400,8 +400,8 @@ class DrupalDotOrgJsonApi extends ProjectBrowserSourceBase { 'name' => $related[$uid_info['type']][$uid_info['id']]['name'], ], packageName: $project['attributes']['field_composer_namespace'] ?? 'drupal/' . $machine_name, - categories: $module_categories, - images: $project_images, + categories: $module_categories ?? [], + images: $project_images ?? [], url: Url::fromUri('https://www.drupal.org/project/' . $machine_name), ); $returned_list[] = $project_object; @@ -565,7 +565,7 @@ class DrupalDotOrgJsonApi extends ProjectBrowserSourceBase { if ($extra = $version_object->getVersionExtra()) { $version = str_replace("-$extra", '', $version); } - $minor_version = $version_object->getMinorVersion() ?? 0; + $minor_version = $version_object->getMinorVersion() ?? '0'; $patch_version = explode('.', $version)[2] ?? '0'; return (int) ( diff --git a/src/Plugin/ProjectBrowserSource/Recipes.php b/src/Plugin/ProjectBrowserSource/Recipes.php index 36602ff31aaf2931a7b0732436794d6dd6a489bc..a658c1ea0ce075ee5f88ede6da1475674f4c33d6 100644 --- a/src/Plugin/ProjectBrowserSource/Recipes.php +++ b/src/Plugin/ProjectBrowserSource/Recipes.php @@ -25,7 +25,7 @@ use Symfony\Component\Finder\Finder; /** * A source plugin that exposes recipes installed locally. */ -class Recipes extends ProjectBrowserSourceBase { +final class Recipes extends ProjectBrowserSourceBase { public function __construct( private readonly FileSystemInterface $fileSystem, @@ -43,6 +43,7 @@ class Recipes extends ProjectBrowserSourceBase { * {@inheritdoc} */ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static { + assert(is_string($container->getParameter('app.root'))); return new static( $container->get(FileSystemInterface::class), $container->get('cache.project_browser'), @@ -89,6 +90,7 @@ class Recipes extends ProjectBrowserSourceBase { } else { $package = file_get_contents($path . '/composer.json'); + assert(is_string($package)); $package = Json::decode($package); $package_name = $package['name']; @@ -125,22 +127,22 @@ class Recipes extends ProjectBrowserSourceBase { // Filter by project machine name. if (!empty($query['machine_name'])) { - $projects = array_filter($projects, fn(Project $project) => $project->machineName === $query['machine_name']); + $projects = array_filter($projects, fn(Project $project): bool => $project->machineName === $query['machine_name']); } // Filter by coverage. if (!empty($query['security_advisory_coverage'])) { - $projects = array_filter($projects, fn(Project $project) => $project->isCovered); + $projects = array_filter($projects, fn(Project $project): bool => $project->isCovered ?? FALSE); } // Filter by categories. if (!empty($query['categories'])) { - $projects = array_filter($projects, fn(Project $project) => array_intersect(array_column($project->categories, 'id'), explode(',', $query['categories']))); + $projects = array_filter($projects, fn(Project $project): bool => empty(array_intersect(array_column($project->categories, 'id'), explode(',', $query['categories'])))); } // Filter by search text. if (!empty($query['search'])) { - $projects = array_filter($projects, fn(Project $project) => stripos($project->title, $query['search']) !== FALSE); + $projects = array_filter($projects, fn(Project $project): bool => stripos($project->title, $query['search']) !== FALSE); } $total = count($projects); @@ -181,7 +183,9 @@ class Recipes extends ProjectBrowserSourceBase { $contrib_recipe_names = InstalledVersions::getInstalledPackagesByType(Recipe::COMPOSER_PROJECT_TYPE); if ($contrib_recipe_names) { $path = InstalledVersions::getInstallPath($contrib_recipe_names[0]); + assert(is_string($path)); $path = $this->fileSystem->realpath($path); + assert(is_string($path)); $search_in[] = dirname($path); } diff --git a/src/Plugin/ProjectBrowserSourceBase.php b/src/Plugin/ProjectBrowserSourceBase.php index afe348c5e664ddab2f0ae7b3387783c0a4ba3789..6a481821ec4856158944f518221645173d204a63 100644 --- a/src/Plugin/ProjectBrowserSourceBase.php +++ b/src/Plugin/ProjectBrowserSourceBase.php @@ -7,7 +7,6 @@ use Drupal\Core\Plugin\PluginBase; use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\project_browser\ProjectBrowser\Filter\MultipleChoiceFilter; use Drupal\project_browser\ProjectBrowser\ProjectsResultsPage; -use Symfony\Component\DependencyInjection\ContainerInterface; /** * Defines an abstract base class for a Project Browser source. @@ -20,17 +19,6 @@ abstract class ProjectBrowserSourceBase extends PluginBase implements ProjectBro use StringTranslationTrait; - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { - return new static( - $configuration, - $plugin_id, - $plugin_definition, - ); - } - /** * {@inheritdoc} */ @@ -102,4 +90,13 @@ abstract class ProjectBrowserSourceBase extends PluginBase implements ProjectBro ); } + /** + * {@inheritdoc} + */ + public function getPluginDefinition(): array { + $definition = parent::getPluginDefinition(); + assert(is_array($definition)); + return $definition; + } + } diff --git a/src/Plugin/ProjectBrowserSourceInterface.php b/src/Plugin/ProjectBrowserSourceInterface.php index 9bb2d2703d5d3735450127b5d60b40f34696d5b1..482e153be2b5618d275a1611b642b8ad9ae9c8a0 100644 --- a/src/Plugin/ProjectBrowserSourceInterface.php +++ b/src/Plugin/ProjectBrowserSourceInterface.php @@ -62,4 +62,9 @@ interface ProjectBrowserSourceInterface extends PluginInspectionInterface { */ public function getSortOptions(): array; + /** + * {@inheritdoc} + */ + public function getPluginDefinition(): array; + } diff --git a/src/Plugin/ProjectBrowserSourceManager.php b/src/Plugin/ProjectBrowserSourceManager.php index 7e913bb29bc0c909cf12fff22ebff02c4fd3b620..e7b10717fd78741b8bc7cfa7afb7c81eaba372d6 100644 --- a/src/Plugin/ProjectBrowserSourceManager.php +++ b/src/Plugin/ProjectBrowserSourceManager.php @@ -39,4 +39,16 @@ class ProjectBrowserSourceManager extends DefaultPluginManager { $this->setCacheBackend($cache_backend, 'project_browser_source_info_plugins'); } + /** + * {@inheritdoc} + * + * @return \Drupal\project_browser\Plugin\ProjectBrowserSourceInterface + * The source plugin. + */ + public function createInstance($plugin_id, array $configuration = []) { + $instance = parent::createInstance($plugin_id, $configuration); + assert($instance instanceof ProjectBrowserSourceInterface); + return $instance; + } + } diff --git a/src/ProjectBrowser/Filter/FilterBase.php b/src/ProjectBrowser/Filter/FilterBase.php index fa7f1ce47d4e44c788cfc93ad7972978022fc5a6..a84a7ede9ca5397e1369ee463dfb513291411f85 100644 --- a/src/ProjectBrowser/Filter/FilterBase.php +++ b/src/ProjectBrowser/Filter/FilterBase.php @@ -22,6 +22,7 @@ abstract class FilterBase implements \JsonSerializable { '_type' => match (static::class) { BooleanFilter::class => 'boolean', MultipleChoiceFilter::class => 'multiple_choice', + default => throw new \UnhandledMatchError('Unexpected class ' . static::class), }, ] + get_object_vars($this); diff --git a/src/ProjectBrowserServiceProvider.php b/src/ProjectBrowserServiceProvider.php index 01f34d2d7f628679ca759bcacd7769dd5be2d3b4..595a6a4d922cf210a188cc7dd3bc03c330476c50 100644 --- a/src/ProjectBrowserServiceProvider.php +++ b/src/ProjectBrowserServiceProvider.php @@ -29,7 +29,8 @@ class ProjectBrowserServiceProvider extends ServiceProviderBase { /** * {@inheritdoc} */ - public function alter(ContainerBuilder $container) { + public function alter(ContainerBuilder $container): void { + assert(is_array($container->getParameter('container.modules'))); if (array_key_exists('package_manager', $container->getParameter('container.modules'))) { parent::register($container); diff --git a/src/RecipeActivator.php b/src/RecipeActivator.php index 21ef587d6497952f799eccdcdac13e81b07eb3c9..72df6d4afc7dab83bb16425fd59e9235d0c9d64e 100644 --- a/src/RecipeActivator.php +++ b/src/RecipeActivator.php @@ -22,11 +22,9 @@ use Symfony\Component\HttpFoundation\Response; /** * Applies locally installed recipes. */ -class RecipeActivator implements ActivatorInterface, EventSubscriberInterface { +final class RecipeActivator implements ActivatorInterface, EventSubscriberInterface { - use ActivationInstructionsTrait { - __construct as traitConstruct; - } + use ActivationInstructionsTrait; /** * The state key that stores the record of all applied recipes. @@ -39,11 +37,9 @@ class RecipeActivator implements ActivatorInterface, EventSubscriberInterface { private readonly string $appRoot, private readonly StateInterface $state, private readonly FileSystemInterface $fileSystem, - ModuleExtensionList $moduleList, - FileUrlGeneratorInterface $fileUrlGenerator, - ) { - $this->traitConstruct($moduleList, $fileUrlGenerator); - } + protected readonly ModuleExtensionList $moduleList, + protected readonly FileUrlGeneratorInterface $fileUrlGenerator, + ) {} /** * {@inheritdoc} @@ -97,6 +93,10 @@ class RecipeActivator implements ActivatorInterface, EventSubscriberInterface { */ public function activate(Project $project): ?Response { $path = $this->getPath($project); + if (!$path) { + return NULL; + } + $recipe = Recipe::createFromDirectory($path); // If the recipe has input, return a response that will instruct the Svelte @@ -127,7 +127,7 @@ class RecipeActivator implements ActivatorInterface, EventSubscriberInterface { /** * {@inheritdoc} */ - public function getInstructions(Project $project): string|Url|null { + public function getInstructions(Project $project): string { $instructions = '<p>' . $this->t('To apply this recipe, run the following command at the command line:') . '</p>'; $command = sprintf( @@ -174,7 +174,7 @@ class RecipeActivator implements ActivatorInterface, EventSubscriberInterface { } } - return $path ? $this->fileSystem->realpath($path) : NULL; + return $path ? ($this->fileSystem->realpath($path) ?: NULL) : NULL; } } diff --git a/src/Routing/ProjectBrowserRoutes.php b/src/Routing/ProjectBrowserRoutes.php index 0f2361d25f632fa88adeb1039372839c161f2dcf..2ca6b7859f4d6367ef3ff3bce5b52c11b5018221 100644 --- a/src/Routing/ProjectBrowserRoutes.php +++ b/src/Routing/ProjectBrowserRoutes.php @@ -16,7 +16,7 @@ use Symfony\Component\Routing\Route; * @internal * Routing callbacks are internal. */ -class ProjectBrowserRoutes implements ContainerInjectionInterface { +final class ProjectBrowserRoutes implements ContainerInjectionInterface { /** * Constructor for project browser routes. @@ -31,7 +31,7 @@ class ProjectBrowserRoutes implements ContainerInjectionInterface { /** * {@inheritdoc} */ - public static function create(ContainerInterface $container) { + public static function create(ContainerInterface $container): static { return new static( $container->get(ModuleHandlerInterface::class), ); diff --git a/tests/modules/project_browser_test/project_browser_test.install b/tests/modules/project_browser_test/project_browser_test.install index ee0e5c0713f561d163e6fa46b51c5c40dae28d49..a8600740df05188a0c89cadd2e629e929f346230 100644 --- a/tests/modules/project_browser_test/project_browser_test.install +++ b/tests/modules/project_browser_test/project_browser_test.install @@ -5,6 +5,8 @@ * Contains install and update functions for testing Project Browser. */ +declare(strict_types=1); + use Drupal\Component\Serialization\Json; use Drupal\Core\Database\Database; @@ -13,7 +15,7 @@ use Drupal\Core\Database\Database; * * Database Table storing. */ -function project_browser_test_schema() { +function project_browser_test_schema(): array { return [ 'project_browser_projects' => [ 'description' => 'Project browser project', @@ -120,7 +122,7 @@ function project_browser_test_schema() { /** * Replace Project Browser data with test data. */ -function project_browser_test_install() { +function project_browser_test_install(): void { $connection = Database::getConnection(); $connection->truncate('project_browser_projects')->execute(); $connection->truncate('project_browser_categories')->execute(); @@ -128,7 +130,9 @@ function project_browser_test_install() { $module_path = \Drupal::service('module_handler')->getModule('project_browser')->getPath(); $category_values = []; - $projects = Json::decode(file_get_contents($module_path . '/tests/fixtures/projects_fixture.json')); + $contents = file_get_contents($module_path . '/tests/fixtures/projects_fixture.json'); + assert(is_string($contents)); + $projects = Json::decode($contents); // Insert fixture data to the database. $query = $connection->insert('project_browser_projects')->fields([ 'nid', diff --git a/tests/modules/project_browser_test/project_browser_test.module b/tests/modules/project_browser_test/project_browser_test.module index faaa7233cca748a5d3f7ae94544f77514fe72065..a4bf231ed0ef8ec75a9dc62284d4b7ee774570a1 100644 --- a/tests/modules/project_browser_test/project_browser_test.module +++ b/tests/modules/project_browser_test/project_browser_test.module @@ -5,12 +5,14 @@ * For use in Project Browser tests. */ +declare(strict_types=1); + use Drupal\Core\Asset\AttachedAssetsInterface; /** * Implements hook_js_settings_alter(). */ -function project_browser_test_js_settings_alter(array &$settings, AttachedAssetsInterface $assets) { +function project_browser_test_js_settings_alter(array &$settings, AttachedAssetsInterface $assets): void { // For testing purposes, trick Project Browser into thinking Pinky and The // Brain has been downloaded but not installed. $settings['project_browser']['modules']['pinky_brain'] = 0; diff --git a/tests/modules/project_browser_test/src/Controller/TestPageController.php b/tests/modules/project_browser_test/src/Controller/TestPageController.php index 225cc4dc1bb42597569555b7d69ae3c317beac75..99372e6b151a375fd967479f80706754f9e91dbf 100644 --- a/tests/modules/project_browser_test/src/Controller/TestPageController.php +++ b/tests/modules/project_browser_test/src/Controller/TestPageController.php @@ -1,5 +1,7 @@ <?php +declare(strict_types=1); + namespace Drupal\project_browser_test\Controller; use Drupal\Core\Controller\ControllerBase; diff --git a/tests/modules/project_browser_test/src/Datetime/TestTime.php b/tests/modules/project_browser_test/src/Datetime/TestTime.php index 041bf174b59470bef30df86a4d158827e5917211..b4b4100c63f1ae26d66d3b69782a44878d6403d9 100644 --- a/tests/modules/project_browser_test/src/Datetime/TestTime.php +++ b/tests/modules/project_browser_test/src/Datetime/TestTime.php @@ -1,5 +1,7 @@ <?php +declare(strict_types=1); + namespace Drupal\project_browser_test\Datetime; use Drupal\Component\Datetime\TimeInterface; @@ -20,21 +22,21 @@ class TestTime implements TimeInterface { /** * {@inheritdoc} */ - public function getRequestMicroTime() { + public function getRequestMicroTime(): float { return $this->decorated->getRequestMicroTime(); } /** * {@inheritdoc} */ - public function getCurrentTime() { + public function getCurrentTime(): int { return $this->decorated->getCurrentTime(); } /** * {@inheritdoc} */ - public function getCurrentMicroTime() { + public function getCurrentMicroTime(): float { return $this->decorated->getCurrentMicroTime(); } @@ -44,7 +46,9 @@ class TestTime implements TimeInterface { public function getRequestTime(): int { // @phpstan-ignore-next-line if ($faked_date = \Drupal::state()->get('project_browser_test.fake_date_time')) { - return \DateTime::createFromFormat('U', $faked_date)->getTimestamp(); + if ($date_time = \DateTime::createFromFormat('U', $faked_date)) { + return $date_time->getTimestamp(); + } } return $this->decorated->getRequestTime(); } diff --git a/tests/modules/project_browser_test/src/DrupalOrgClientMiddleware.php b/tests/modules/project_browser_test/src/DrupalOrgClientMiddleware.php index b6beb005a43e1e21fbcaccc504ae81f093d029f6..4e14e69765d40e0518abfc461c9fdf07c2624dfb 100644 --- a/tests/modules/project_browser_test/src/DrupalOrgClientMiddleware.php +++ b/tests/modules/project_browser_test/src/DrupalOrgClientMiddleware.php @@ -1,5 +1,7 @@ <?php +declare(strict_types=1); + namespace Drupal\project_browser_test; use Drupal\Core\Extension\ModuleHandlerInterface; @@ -109,33 +111,34 @@ class DrupalOrgClientMiddleware { * if they were real-time API results, providing controlled and predictable * data to validate functionality. */ - public function __invoke() { + public function __invoke(): \Closure { return function ($handler) { return function (RequestInterface $request, array $options) use ($handler) { $json_response = ''; // This endpoint, when accessed in a browser, returns the JSON data // which is used to generate the fixtures used in // ProjectBrowserUiTestJsonApi test. - $actual_api_endpoint = $request->getUri(); + $actual_api_endpoint = (string) $request->getUri(); if (strpos($actual_api_endpoint, DrupalDotOrgJsonApi::JSONAPI_ENDPOINT) !== FALSE) { $relevant_path = str_replace(DrupalDotOrgJsonApi::JSONAPI_ENDPOINT, '', $actual_api_endpoint); // Remove semver query as it is core version dependent. // Processed query will act as relevant path to fixtures. - $relevant_path = preg_replace('/&filter%5Bcore_semver_minimum%5D%5Bvalue%5D=[0-9]*/', '', $relevant_path); - $relevant_path = preg_replace('/&filter%5Bcore_semver_maximum%5D%5Bvalue%5D=[0-9]*/', '', $relevant_path); + $relevant_path = (string) preg_replace('/&filter%5Bcore_semver_minimum%5D%5Bvalue%5D=[0-9]*/', '', $relevant_path); + $relevant_path = (string) preg_replace('/&filter%5Bcore_semver_maximum%5D%5Bvalue%5D=[0-9]*/', '', $relevant_path); $path_to_fixture = self::DRUPALORG_JSONAPI_ENDPOINT_TO_FIXTURE_MAP; if (isset($path_to_fixture[$relevant_path])) { $module_path = $this->moduleHandler->getModule('project_browser')->getPath(); - $data = file_get_contents($module_path . '/tests/fixtures/drupalorg_jsonapi/' . $path_to_fixture[$relevant_path]); - $json_response = new Response(200, [], $data); - return new FulfilledPromise($json_response); + if ($data = file_get_contents($module_path . '/tests/fixtures/drupalorg_jsonapi/' . $path_to_fixture[$relevant_path])) { + $json_response = new Response(200, [], $data); + return new FulfilledPromise($json_response); + } } throw new \Exception('Attempted call to the Drupal.org jsonapi endpoint that is not mocked in middleware: ' . $relevant_path); } // Other queries to the non-jsonapi endpoints. - elseif (strpos($request->getUri(), DrupalDotOrgJsonApi::DRUPAL_ORG_ENDPOINT) !== FALSE) { - $relevant_path = str_replace(DrupalDotOrgJsonApi::DRUPAL_ORG_ENDPOINT, '', $request->getUri()); + elseif (strpos($actual_api_endpoint, DrupalDotOrgJsonApi::DRUPAL_ORG_ENDPOINT) !== FALSE) { + $relevant_path = str_replace(DrupalDotOrgJsonApi::DRUPAL_ORG_ENDPOINT, '', $actual_api_endpoint); $path_to_fixture = self::DRUPALORG_ENDPOINT_TO_FIXTURE_MAP; $is_outdated = $this->state->get('project_browser:test_deprecated_api'); @@ -149,9 +152,10 @@ class DrupalOrgClientMiddleware { if (isset($path_to_fixture[$relevant_path])) { $module_path = $this->moduleHandler->getModule('project_browser')->getPath(); - $data = file_get_contents($module_path . '/tests/fixtures/drupalorg_jsonapi/' . $path_to_fixture[$relevant_path]); - $json_response = new Response(200, [], $data); - return new FulfilledPromise($json_response); + if ($data = file_get_contents($module_path . '/tests/fixtures/drupalorg_jsonapi/' . $path_to_fixture[$relevant_path])) { + $json_response = new Response(200, [], $data); + return new FulfilledPromise($json_response); + } } throw new \Exception('Attempted call to the Drupal.org endpoint that is not mocked in middleware: ' . $relevant_path); diff --git a/tests/modules/project_browser_test/src/Extension/TestModuleInstaller.php b/tests/modules/project_browser_test/src/Extension/TestModuleInstaller.php index 3043447d67ec68c98d0cffef5c9cf9c7465788cb..65ac77f652c93dfd8cb5d6b6864858cda00bbacd 100644 --- a/tests/modules/project_browser_test/src/Extension/TestModuleInstaller.php +++ b/tests/modules/project_browser_test/src/Extension/TestModuleInstaller.php @@ -1,5 +1,7 @@ <?php +declare(strict_types=1); + namespace Drupal\project_browser_test\Extension; use Drupal\Core\Extension\ModuleInstallerInterface; @@ -24,7 +26,7 @@ class TestModuleInstaller implements ModuleInstallerInterface { * @param bool $enable_dependencies * True if dependencies should be enabled. */ - public function install(array $module_list, $enable_dependencies = TRUE) { + public function install(array $module_list, $enable_dependencies = TRUE): bool { if (!empty(array_intersect(['cream_cheese', 'kangaroo'], $module_list))) { return TRUE; } @@ -34,21 +36,21 @@ class TestModuleInstaller implements ModuleInstallerInterface { /** * {@inheritdoc} */ - public function uninstall(array $module_list, $uninstall_dependents = TRUE) { + public function uninstall(array $module_list, $uninstall_dependents = TRUE): bool { return $this->decorated->uninstall($module_list, $uninstall_dependents); } /** * {@inheritdoc} */ - public function addUninstallValidator(ModuleUninstallValidatorInterface $uninstall_validator) { + public function addUninstallValidator(ModuleUninstallValidatorInterface $uninstall_validator): void { $this->decorated->addUninstallValidator($uninstall_validator); } /** * {@inheritdoc} */ - public function validateUninstall(array $module_list) { + public function validateUninstall(array $module_list): array { return $this->decorated->validateUninstall($module_list); } diff --git a/tests/modules/project_browser_test/src/Plugin/ProjectBrowserSource/ProjectBrowserTestMock.php b/tests/modules/project_browser_test/src/Plugin/ProjectBrowserSource/ProjectBrowserTestMock.php index c74a5a2995cc0e7062a3704134f0bb71121e9ff3..248149b423a04414dba27787a99009352545e9b5 100644 --- a/tests/modules/project_browser_test/src/Plugin/ProjectBrowserSource/ProjectBrowserTestMock.php +++ b/tests/modules/project_browser_test/src/Plugin/ProjectBrowserSource/ProjectBrowserTestMock.php @@ -1,10 +1,11 @@ <?php +declare(strict_types=1); + namespace Drupal\project_browser_test\Plugin\ProjectBrowserSource; use Drupal\Component\Serialization\Json; use Drupal\Component\Utility\Html; -use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Database\Connection; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\State\StateInterface; @@ -31,7 +32,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface; * } * ) */ -class ProjectBrowserTestMock extends ProjectBrowserSourceBase { +final class ProjectBrowserTestMock extends ProjectBrowserSourceBase { /** * This is what the Mock understands as "Covered" modules. @@ -76,8 +77,6 @@ class ProjectBrowserTestMock extends ProjectBrowserSourceBase { * The database connection. * @param \Drupal\Core\State\StateInterface $state * The session state. - * @param \Drupal\Core\Cache\CacheBackendInterface $cacheBin - * The back end cache interface. * @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler * The module handler. */ @@ -88,7 +87,6 @@ class ProjectBrowserTestMock extends ProjectBrowserSourceBase { private readonly LoggerInterface $logger, private readonly Connection $database, private readonly StateInterface $state, - private readonly CacheBackendInterface $cacheBin, private readonly ModuleHandlerInterface $moduleHandler, ) { parent::__construct($configuration, $plugin_id, $plugin_definition); @@ -97,7 +95,7 @@ class ProjectBrowserTestMock extends ProjectBrowserSourceBase { /** * {@inheritdoc} */ - public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static { return new static( $configuration, $plugin_id, @@ -105,7 +103,6 @@ class ProjectBrowserTestMock extends ProjectBrowserSourceBase { $container->get('logger.factory')->get('project_browser'), $container->get(Connection::class), $container->get(StateInterface::class), - $container->get('cache.project_browser'), $container->get(ModuleHandlerInterface::class), ); } @@ -119,7 +116,7 @@ class ProjectBrowserTestMock extends ProjectBrowserSourceBase { * @return array|array[] * An array with the term id, name and description. */ - protected function getStatuses(int $taxonomy_id) { + protected function getStatuses(int $taxonomy_id): array { $body = ''; // Development. if ($taxonomy_id === 46) { @@ -179,7 +176,7 @@ class ProjectBrowserTestMock extends ProjectBrowserSourceBase { * @param array $query * Query array to transform. */ - protected function convertSort(array &$query) { + protected function convertSort(array &$query): void { if (!empty($query['sort'])) { $options_available = $this->getSortOptions(); if (!in_array($query['sort'], array_keys($options_available))) { @@ -220,7 +217,7 @@ class ProjectBrowserTestMock extends ProjectBrowserSourceBase { * @param array $query * Query array to transform. */ - protected function convertMaintenance(array &$query) { + protected function convertMaintenance(array &$query): void { if (!empty($query['maintenance_status'])) { $query['maintenance_status'] = self::MAINTAINED_VALUES; } @@ -235,7 +232,7 @@ class ProjectBrowserTestMock extends ProjectBrowserSourceBase { * @param array $query * Query array to transform. */ - protected function convertDevelopment(array &$query) { + protected function convertDevelopment(array &$query): void { if (!empty($query['development_status'])) { $query['development_status'] = self::ACTIVE_VALUES; } @@ -250,7 +247,7 @@ class ProjectBrowserTestMock extends ProjectBrowserSourceBase { * @param array $query * Query array to transform. */ - protected function convertSecurity(array &$query) { + protected function convertSecurity(array &$query): void { if (!empty($query['security_advisory_coverage'])) { $query['security_advisory_coverage'] = self::COVERED_VALUES; } @@ -291,7 +288,9 @@ class ProjectBrowserTestMock extends ProjectBrowserSourceBase { */ protected function getCategoryData(): array { $module_path = $this->moduleHandler->getModule('project_browser')->getPath(); - $category_list = Json::decode(file_get_contents($module_path . '/tests/fixtures/category_list.json')) ?? []; + $contents = file_get_contents($module_path . '/tests/fixtures/category_list.json'); + assert(is_string($contents)); + $category_list = Json::decode($contents) ?? []; $categories = []; foreach ($category_list as $category) { $categories[$category['tid']] = [ @@ -366,7 +365,7 @@ class ProjectBrowserTestMock extends ProjectBrowserSourceBase { $categories = $this->getCategoryData(); $returned_list = []; - if ($api_response) { + if (is_array($api_response)) { foreach ($api_response['list'] as $project_data) { $avatar_url = 'https://git.drupalcode.org/project/' . $project_data['field_project_machine_name'] . '/-/avatar'; $logo = [ @@ -384,7 +383,7 @@ class ProjectBrowserTestMock extends ProjectBrowserSourceBase { isCompatible: TRUE, isMaintained: in_array($project_data['maintenance_status'], self::MAINTAINED_VALUES), isCovered: in_array($project_data['field_security_advisory_coverage'], self::COVERED_VALUES), - projectUsageTotal: array_reduce($project_data['project_data']['project_usage'] ?? [], fn($total, $project_usage) => $total + $project_usage) ?: 0, + projectUsageTotal: array_reduce($project_data['project_data']['project_usage'] ?? [], fn($total, $project_usage): int => $total + $project_usage) ?: 0, machineName: $project_data['field_project_machine_name'], body: $this->relativeToAbsoluteUrls($project_data['project_data']['body'], 'https://www.drupal.org'), title: $project_data['title'], @@ -392,7 +391,7 @@ class ProjectBrowserTestMock extends ProjectBrowserSourceBase { packageName: 'drupal/' . $project_data['field_project_machine_name'], url: Url::fromUri('https://www.drupal.org/project/' . $project_data['field_project_machine_name']), // Add name property to each category, so it can be rendered. - categories: array_map(fn($category) => $categories[$category['id']] ?? '', $project_data['project_data']['taxonomy_vocabulary_3'] ?? []), + categories: array_map(fn($category): array => $categories[$category['id']] ?? [], $project_data['project_data']['taxonomy_vocabulary_3'] ?? []), images: $project_data['project_data']['field_project_images'] ?? [], warnings: $this->getWarnings($project_data), id: $project_data['field_project_machine_name'], @@ -400,7 +399,7 @@ class ProjectBrowserTestMock extends ProjectBrowserSourceBase { } } - return $this->createResultsPage($returned_list, $api_response['total_results'] ?? 0, static::$resultsError); + return $this->createResultsPage($returned_list, (int) ($api_response['total_results'] ?? 0), static::$resultsError); } /** @@ -408,7 +407,7 @@ class ProjectBrowserTestMock extends ProjectBrowserSourceBase { * * Here, we're querying the local database, populated from the fixture. */ - protected function fetchProjects($query) { + protected function fetchProjects(array $query): bool|array { $query = $this->convertQueryOptions($query); try { $db_query = $this->database->select('project_browser_projects', 'pbp') @@ -462,13 +461,13 @@ class ProjectBrowserTestMock extends ProjectBrowserSourceBase { // projects. $total_results = $db_query->countQuery() ->execute() - ->fetchField(); + ?->fetchField(); $offset = $query['page'] ?? 0; $limit = $query['limit'] ?? 50; $db_query->range($limit * $offset, $limit); $result = $db_query ->execute() - ->fetchAll(); + ?->fetchAll() ?? []; $db_projects = array_map(function ($project_data) { $data = (array) $project_data; $data['project_data'] = unserialize($project_data->project_data); @@ -498,7 +497,7 @@ class ProjectBrowserTestMock extends ProjectBrowserSourceBase { * @return string[] * An array of warning messages. */ - protected function getWarnings(array $project) { + protected function getWarnings(array $project): array { // This is based on logic from Drupal.org. // @see https://git.drupalcode.org/project/drupalorg/-/blob/e31465608d1380345834/drupalorg_project/drupalorg_project.module $warnings = []; @@ -530,7 +529,7 @@ class ProjectBrowserTestMock extends ProjectBrowserSourceBase { * @return array * Body array with relative URLs converted to absolute ones. */ - protected function relativeToAbsoluteUrls(array $body, string $base_url) { + protected function relativeToAbsoluteUrls(array $body, string $base_url): array { if (empty($body['value'])) { $body['value'] = $body['summary'] ?? ''; } diff --git a/tests/modules/project_browser_test/src/ProjectBrowserTestServiceProvider.php b/tests/modules/project_browser_test/src/ProjectBrowserTestServiceProvider.php index da31f551e695f1a33d59c6fc025a485932436d8a..d70305429cc4e78c08bb7cfc00eacbb2b669b48b 100644 --- a/tests/modules/project_browser_test/src/ProjectBrowserTestServiceProvider.php +++ b/tests/modules/project_browser_test/src/ProjectBrowserTestServiceProvider.php @@ -1,5 +1,7 @@ <?php +declare(strict_types=1); + namespace Drupal\project_browser_test; use Drupal\Core\DependencyInjection\ContainerBuilder; @@ -14,7 +16,7 @@ class ProjectBrowserTestServiceProvider extends ServiceProviderBase { /** * {@inheritdoc} */ - public function alter(ContainerBuilder $container) { + public function alter(ContainerBuilder $container): void { // The InstallReadiness service is defined by ProjectBrowserServiceProvider // if Package Manager is installed. if ($container->hasDefinition(InstallReadiness::class)) { diff --git a/tests/modules/project_browser_test/src/TestInstallReadiness.php b/tests/modules/project_browser_test/src/TestInstallReadiness.php index 8dcacb2da268593501c01a447b0d2455c129b069..bc17893aee871f02adb63d39e16bbd6a7175d31b 100644 --- a/tests/modules/project_browser_test/src/TestInstallReadiness.php +++ b/tests/modules/project_browser_test/src/TestInstallReadiness.php @@ -1,5 +1,7 @@ <?php +declare(strict_types=1); + namespace Drupal\project_browser_test; use Drupal\Core\State\StateInterface; diff --git a/tests/src/Functional/ClearStorageTest.php b/tests/src/Functional/ClearStorageTest.php index 47c893922c019cb60590991f0a84ff7be8522151..d0be6f3f68a1136fb86c72673638ff12e936aed3 100644 --- a/tests/src/Functional/ClearStorageTest.php +++ b/tests/src/Functional/ClearStorageTest.php @@ -1,5 +1,7 @@ <?php +declare(strict_types=1); + namespace Drupal\Tests\project_browser\Functional; use Drupal\Core\KeyValueStore\KeyValueStoreInterface; diff --git a/tests/src/Functional/EnabledSourceHandlerTest.php b/tests/src/Functional/EnabledSourceHandlerTest.php index dd18731dbe0870c2c48e2cba4f6e50eca2892f3f..9f9a05ca1f804eaf3d813367c14ea0782d0e5b38 100644 --- a/tests/src/Functional/EnabledSourceHandlerTest.php +++ b/tests/src/Functional/EnabledSourceHandlerTest.php @@ -54,6 +54,7 @@ class EnabledSourceHandlerTest extends BrowserTestBase { $handler = $this->container->get(EnabledSourceHandler::class); $projects = $handler->getProjects('project_browser_test_mock'); + assert(!empty($projects)); $list = reset($projects)->list; $this->assertNotEmpty($list); $project = reset($list); @@ -74,6 +75,7 @@ class EnabledSourceHandlerTest extends BrowserTestBase { // and commands set. $projects = $this->container->get(EnabledSourceHandler::class) ->getProjects('project_browser_test_mock'); + assert(!empty($projects)); $list = reset($projects)->list; $this->assertNotEmpty($list); $project = reset($list); diff --git a/tests/src/Functional/InstallTest.php b/tests/src/Functional/InstallTest.php index ff9b82719908b3499348a38f54171e0924e3afb0..c4756360953de949e4abb47f524bd0ad4d6971c4 100644 --- a/tests/src/Functional/InstallTest.php +++ b/tests/src/Functional/InstallTest.php @@ -1,5 +1,7 @@ <?php +declare(strict_types=1); + namespace Drupal\Tests\project_browser\Functional; use Drupal\Tests\BrowserTestBase; @@ -40,7 +42,7 @@ class InstallTest extends BrowserTestBase { /** * Reloads services used by this test. */ - protected function reloadServices() { + protected function reloadServices(): void { $this->rebuildContainer(); $this->moduleHandler = $this->container->get('module_handler'); } @@ -48,15 +50,15 @@ class InstallTest extends BrowserTestBase { /** * Tests that the module is installable. */ - public function testInstallation() { + public function testInstallation(): void { $edit = []; $edit['modules[project_browser][enable]'] = 'project_browser'; $this->drupalGet('admin/modules'); $this->submitForm($edit, 'Install'); // @todo Convert this to pageTextContains(), and only look for `installed`, // once Drupal 10 support is dropped. - $this->assertSession()->pageTextMatches('/Module Project Browser has been (installed|enabled)\./', 'status'); - $this->assertSession()->statusCodeEquals('200'); + $this->assertSession()->pageTextMatches('/Module Project Browser has been (installed|enabled)\./'); + $this->assertSession()->statusCodeEquals(200); $this->reloadServices(); $this->assertTrue($this->moduleHandler->moduleExists('project_browser')); $this->assertFalse($this->moduleHandler->moduleExists('package_manager')); diff --git a/tests/src/Functional/InstallerControllerTest.php b/tests/src/Functional/InstallerControllerTest.php index bd43f760cf308d79b8d7dda108a02d44a4f5ffef..1bb7a0d50cddd19d4324b8deaa9999bbebf9e5da 100644 --- a/tests/src/Functional/InstallerControllerTest.php +++ b/tests/src/Functional/InstallerControllerTest.php @@ -1,5 +1,7 @@ <?php +declare(strict_types=1); + namespace Drupal\Tests\project_browser\Functional; use Drupal\Component\Serialization\Json; @@ -162,7 +164,9 @@ class InstallerControllerTest extends BrowserTestBase { ]); $query->execute(); $this->initPackageManager(); - $this->installer = $this->container->get(Installer::class); + /** @var \Drupal\project_browser\ComposerInstaller\Installer $installer */ + $installer = $this->container->get(Installer::class); + $this->installer = $installer; $this->drupalLogin($this->drupalCreateUser(['administer modules'])); $this->config('project_browser.admin_settings') ->set('enabled_sources', ['project_browser_test_mock', 'drupal_core']) @@ -178,7 +182,7 @@ class InstallerControllerTest extends BrowserTestBase { * * @covers ::access */ - public function testUiInstallUnavailableIfDisabled() { + public function testUiInstallUnavailableIfDisabled(): void { $this->config('project_browser.admin_settings')->set('allow_ui_install', FALSE)->save(); $this->drupalGet('admin/modules/project_browser/install-begin'); $this->assertSession()->statusCodeEquals(403); @@ -190,8 +194,8 @@ class InstallerControllerTest extends BrowserTestBase { * * @covers ::begin */ - public function testInstallSecurityRevokedModule() { - $this->assertSame([], $this->container->get(InstallState::class)->toArray()); + public function testInstallSecurityRevokedModule(): void { + $this->assertSame([], $this->getInstallState()->toArray()); $contents = $this->drupalGet('admin/modules/project_browser/install-begin'); $this->stageId = Json::decode($contents)['stage_id']; $response = $this->getPostResponse('project_browser.stage.require', 'project_browser_test_mock/security_revoked_module', [ @@ -206,8 +210,8 @@ class InstallerControllerTest extends BrowserTestBase { * * @covers ::require */ - public function testInstallAlreadyPresentPackage() { - $this->assertSame([], $this->container->get(InstallState::class)->toArray()); + public function testInstallAlreadyPresentPackage(): void { + $this->assertSame([], $this->getInstallState()->toArray()); // Though core is not available as a choice in project browser, it works // well for the purposes of this test as it's definitely already added // via composer. @@ -225,8 +229,8 @@ class InstallerControllerTest extends BrowserTestBase { * * @covers ::begin */ - private function doStart() { - $this->assertSame([], $this->container->get(InstallState::class)->toArray()); + private function doStart(): void { + $this->assertSame([], $this->getInstallState()->toArray()); $contents = $this->drupalGet('admin/modules/project_browser/install-begin'); $this->stageId = Json::decode($contents)['stage_id']; $this->assertSession()->statusCodeEquals(200); @@ -238,7 +242,7 @@ class InstallerControllerTest extends BrowserTestBase { * * @covers ::require */ - private function doRequire() { + private function doRequire(): void { $response = $this->getPostResponse('project_browser.stage.require', 'project_browser_test_mock/awesome_module', [ 'stage_id' => $this->stageId, ]); @@ -252,7 +256,7 @@ class InstallerControllerTest extends BrowserTestBase { * * @covers ::apply */ - private function doApply() { + private function doApply(): void { $this->drupalGet("/admin/modules/project_browser/install-apply/$this->stageId"); $expected_output = sprintf('{"phase":"apply","status":0,"stage_id":"%s"}', $this->stageId); $this->assertSame($expected_output, $this->getSession()->getPage()->getContent()); @@ -264,7 +268,7 @@ class InstallerControllerTest extends BrowserTestBase { * * @covers ::postApply */ - private function doPostApply() { + private function doPostApply(): void { $this->drupalGet("/admin/modules/project_browser/install-post_apply/$this->stageId"); $expected_output = sprintf('{"phase":"post apply","status":0,"stage_id":"%s"}', $this->stageId); $this->assertSame($expected_output, $this->getSession()->getPage()->getContent()); @@ -276,7 +280,7 @@ class InstallerControllerTest extends BrowserTestBase { * * @covers ::destroy */ - private function doDestroy() { + private function doDestroy(): void { $this->drupalGet("/admin/modules/project_browser/install-destroy/$this->stageId"); $expected_output = sprintf('{"phase":"destroy","status":0,"stage_id":"%s"}', $this->stageId); $this->assertSame($expected_output, $this->getSession()->getPage()->getContent()); @@ -286,7 +290,7 @@ class InstallerControllerTest extends BrowserTestBase { /** * Calls every endpoint needed to do a UI install and confirms they work. */ - public function testUiInstallerEndpoints() { + public function testUiInstallerEndpoints(): void { $this->doStart(); $this->doRequire(); $this->doApply(); @@ -299,7 +303,7 @@ class InstallerControllerTest extends BrowserTestBase { * * @covers ::create */ - public function testPreCreateError() { + public function testPreCreateError(): void { $message = t('This is a PreCreate error.'); $result = ValidationResult::createError([$message]); TestSubscriber::setTestResult([$result], PreCreateEvent::class); @@ -313,7 +317,7 @@ class InstallerControllerTest extends BrowserTestBase { * * @covers ::create */ - public function testPreCreateException() { + public function testPreCreateException(): void { $error = new \Exception('PreCreate did not go well.'); TestSubscriber::setException($error, PreCreateEvent::class); $contents = $this->drupalGet('admin/modules/project_browser/install-begin'); @@ -326,7 +330,7 @@ class InstallerControllerTest extends BrowserTestBase { * * @covers ::create */ - public function testPostCreateException() { + public function testPostCreateException(): void { $error = new \Exception('PostCreate did not go well.'); TestSubscriber::setException($error, PostCreateEvent::class); $contents = $this->drupalGet('admin/modules/project_browser/install-begin'); @@ -339,7 +343,7 @@ class InstallerControllerTest extends BrowserTestBase { * * @covers ::require */ - public function testPreRequireError() { + public function testPreRequireError(): void { $message = t('This is a PreRequire error.'); $result = ValidationResult::createError([$message]); $this->doStart(); @@ -356,7 +360,7 @@ class InstallerControllerTest extends BrowserTestBase { * * @covers ::require */ - public function testPreRequireException() { + public function testPreRequireException(): void { $error = new \Exception('PreRequire did not go well.'); TestSubscriber::setException($error, PreRequireEvent::class); $this->doStart(); @@ -372,7 +376,7 @@ class InstallerControllerTest extends BrowserTestBase { * * @covers ::require */ - public function testPostRequireException() { + public function testPostRequireException(): void { $error = new \Exception('PostRequire did not go well.'); TestSubscriber::setException($error, PostRequireEvent::class); $this->doStart(); @@ -388,7 +392,7 @@ class InstallerControllerTest extends BrowserTestBase { * * @covers ::apply */ - public function testPreApplyError() { + public function testPreApplyError(): void { $message = t('This is a PreApply error.'); $result = ValidationResult::createError([$message]); TestSubscriber::setTestResult([$result], PreApplyEvent::class); @@ -404,7 +408,7 @@ class InstallerControllerTest extends BrowserTestBase { * * @covers ::apply */ - public function testPreApplyException() { + public function testPreApplyException(): void { $error = new \Exception('PreApply did not go well.'); TestSubscriber::setException($error, PreApplyEvent::class); $this->doStart(); @@ -419,7 +423,7 @@ class InstallerControllerTest extends BrowserTestBase { * * @covers ::apply */ - public function testPostApplyException() { + public function testPostApplyException(): void { $error = new \Exception('PostApply did not go well.'); TestSubscriber::setException($error, PostApplyEvent::class); $this->doStart(); @@ -435,7 +439,7 @@ class InstallerControllerTest extends BrowserTestBase { * * @covers ::unlock */ - public function testInstallUnlockMessage() { + public function testInstallUnlockMessage(): void { $this->doStart(); $this->doRequire(); @@ -448,10 +452,14 @@ class InstallerControllerTest extends BrowserTestBase { $this->assertSame($expected_message, $response['message']); if ($response['unlock_url']) { - $this->assertStringEndsWith('/admin/modules/project_browser/install/unlock', parse_url($response['unlock_url'], PHP_URL_PATH)); - $query = parse_url($response['unlock_url'], PHP_URL_QUERY); - parse_str($query, $query); + $path_string = parse_url($response['unlock_url'], PHP_URL_PATH); + $this->assertIsString($path_string); + $this->assertStringEndsWith('/admin/modules/project_browser/install/unlock', $path_string); + $query_string = parse_url($response['unlock_url'], PHP_URL_QUERY); + $this->assertIsString($query_string); + parse_str($query_string, $query); $this->assertNotEmpty($query['token']); + $this->assertIsString($query['destination']); $this->assertStringEndsWith('/admin/modules/browse/project_browser_test_mock', $query['destination']); } }; @@ -466,7 +474,7 @@ class InstallerControllerTest extends BrowserTestBase { 'status' => 'requiring', ], ]; - $this->assertSame($expected, $this->container->get(InstallState::class)->toArray()); + $this->assertSame($expected, $this->getInstallState()->toArray()); $this->assertFalse($this->installer->isAvailable()); $this->assertFalse($this->installer->isApplying()); TestTime::setFakeTimeByOffset("+800 seconds"); @@ -500,7 +508,7 @@ class InstallerControllerTest extends BrowserTestBase { * * @covers ::unlock */ - public function testCanBreakLock() { + public function testCanBreakLock(): void { $this->doStart(); // Try beginning another install while one is in progress, but not yet in // the applying stage. @@ -515,8 +523,8 @@ class InstallerControllerTest extends BrowserTestBase { $json = Json::decode($content); $this->assertSame('The process for adding projects is locked, but that lock has expired. Use [+ unlock link] to unlock the process and try to add the project again.', $json['message']); $unlock_url = parse_url($json['unlock_url']); - parse_str($unlock_url['query'], $unlock_url['query']); - $unlock_content = $this->drupalGet($unlock_url['path'], ['query' => $unlock_url['query']]); + parse_str($unlock_url['query'] ?? '', $unlock_url['query']); + $unlock_content = $this->drupalGet($unlock_url['path'] ?? '', ['query' => $unlock_url['query']]); $this->assertSession()->statusCodeEquals(200); $this->assertTrue($this->installer->isAvailable()); $this->assertStringContainsString('Operation complete, you can add a new project again.', $unlock_content); @@ -529,9 +537,9 @@ class InstallerControllerTest extends BrowserTestBase { * * @covers ::unlock */ - public function testCanBreakStageWithMissingProjectBrowserLock() { + public function testCanBreakStageWithMissingProjectBrowserLock(): void { $this->doStart(); - $this->container->get(InstallState::class)->deleteAll(); + $this->getInstallState()->deleteAll(); $content = $this->drupalGet('admin/modules/project_browser/install-begin', [ 'query' => ['source' => 'project_browser_test_mock'], ]); @@ -541,8 +549,8 @@ class InstallerControllerTest extends BrowserTestBase { $json = Json::decode($content); $this->assertSame('The process for adding projects is locked, but that lock has expired. Use [+ unlock link] to unlock the process and try to add the project again.', $json['message']); $unlock_url = parse_url($json['unlock_url']); - parse_str($unlock_url['query'], $unlock_url['query']); - $unlock_content = $this->drupalGet($unlock_url['path'], ['query' => $unlock_url['query']]); + parse_str($unlock_url['query'] ?? '', $unlock_url['query']); + $unlock_content = $this->drupalGet($unlock_url['path'] ?? '', ['query' => $unlock_url['query']]); $this->assertSession()->statusCodeEquals(200); $this->assertTrue($this->installer->isAvailable()); $this->assertStringContainsString('Operation complete, you can add a new project again.', $unlock_content); @@ -585,12 +593,12 @@ class InstallerControllerTest extends BrowserTestBase { * @param string $status * The install state. */ - protected function assertInstallInProgress(string $project_id, string $source, ?string $status = NULL) { + protected function assertInstallInProgress(string $project_id, string $source, ?string $status = NULL): void { $expect_install[$project_id] = [ 'source' => $source, 'status' => $status, ]; - $this->assertSame($expect_install, $this->container->get(InstallState::class)->toArray()); + $this->assertSame($expect_install, $this->getInstallState()->toArray()); $this->drupalGet("/admin/modules/project_browser/install_in_progress/$project_id"); $this->assertSame(sprintf('{"status":1,"phase":"%s"}', $status), $this->getSession()->getPage()->getContent()); $this->drupalGet('/admin/modules/project_browser/install_in_progress/project_browser_test_mock/metatag'); @@ -624,4 +632,16 @@ class InstallerControllerTest extends BrowserTestBase { return $this->makeApiRequest('POST', $post_url, $request_options); } + /** + * Gets the install state. + * + * @return \Drupal\project_browser\InstallState + * The install state. + */ + private function getInstallState(): InstallState { + $install_state = $this->container->get(InstallState::class); + assert($install_state instanceof InstallState); + return $install_state; + } + } diff --git a/tests/src/Functional/ProjectBrowserMenuTabsTest.php b/tests/src/Functional/ProjectBrowserMenuTabsTest.php index 58336cea91ec84d54a2006f74e2e136177e3172d..01b68e4e7e34cf36aa54e39a0910f3afb26c882e 100644 --- a/tests/src/Functional/ProjectBrowserMenuTabsTest.php +++ b/tests/src/Functional/ProjectBrowserMenuTabsTest.php @@ -1,5 +1,7 @@ <?php +declare(strict_types=1); + namespace Drupal\Tests\project_browser\Functional; use Drupal\Tests\BrowserTestBase; diff --git a/tests/src/Functional/RoutingTest.php b/tests/src/Functional/RoutingTest.php index b70a55fadb8298a3aaac963c1eafb5240f948c08..d7ac5dc20f7daa167b81b7892a8dc383486aedb2 100644 --- a/tests/src/Functional/RoutingTest.php +++ b/tests/src/Functional/RoutingTest.php @@ -1,5 +1,7 @@ <?php +declare(strict_types=1); + namespace Drupal\Tests\project_browser\Functional; use Drupal\Core\Url; diff --git a/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php b/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php index 9e84a6ccd4d0af4267e3aa8501f72b1aec53f7d8..d4ea81f0a44cb8eaeded8656680a183614d142d3 100644 --- a/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php +++ b/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php @@ -55,7 +55,9 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase { $this->initPackageManager(); - $this->installState = $this->container->get(InstallState::class); + /** @var \Drupal\project_browser\InstallState $install_state */ + $install_state = $this->container->get(InstallState::class); + $this->installState = $install_state; $this->config('project_browser.admin_settings') ->set('enabled_sources', ['project_browser_test_mock']) @@ -97,7 +99,7 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase { * Browser UI is used, but could happen if the module was added differently, * such as via the terminal with Compose or a direct file addition. */ - public function testInstallModuleAlreadyInFilesystem() { + public function testInstallModuleAlreadyInFilesystem(): void { $assert_session = $this->assertSession(); $page = $this->getSession()->getPage(); $this->drupalGet('admin/modules/browse/project_browser_test_mock'); @@ -193,7 +195,7 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase { $this->assertNotEmpty($download_button); $this->assertSame('Install Cream cheese on a bagel', $download_button->getText()); $this->drupalGet('/admin/config/development/project_browser'); - $page->find('css', '#edit-allow-ui-install')->click(); + $page->find('css', '#edit-allow-ui-install')?->click(); $assert_session->checkboxNotChecked('edit-allow-ui-install'); $this->submitForm([], 'Save'); $this->assertTrue($assert_session->waitForText('The configuration options have been saved.')); @@ -210,7 +212,7 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase { * * @covers ::unlock */ - public function testCanBreakStageWithMissingProjectBrowserLock() { + public function testCanBreakStageWithMissingProjectBrowserLock(): void { $assert_session = $this->assertSession(); $page = $this->getSession()->getPage(); @@ -225,7 +227,7 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase { // the applying stage. $cream_cheese_module_selector = '#project-browser .pb-layout__main ul > li:nth-child(1)'; $cream_cheese_button = $page->find('css', "$cream_cheese_module_selector button.project__action_button"); - $cream_cheese_button->click(); + $cream_cheese_button?->click(); $this->assertTrue($assert_session->waitForText('The process for adding projects is locked, but that lock has expired. Use unlock link to unlock the process and try to add the project again.')); @@ -234,7 +236,7 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase { $this->svelteInitHelper('text', 'Cream cheese on a bagel'); // Try beginning another install after breaking lock. $cream_cheese_button = $page->find('css', "$cream_cheese_module_selector button.project__action_button"); - $cream_cheese_button->click(); + $cream_cheese_button?->click(); $installed_action = $assert_session->waitForElementVisible('css', "$cream_cheese_module_selector .project_status-indicator", 30000); $assert_session->waitForText('Cream cheese on a bagel is Installed'); $this->assertSame('Cream cheese on a bagel is Installed', $installed_action->getText()); @@ -248,7 +250,7 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase { * * @covers ::unlock */ - public function testCanBreakLock() { + public function testCanBreakLock(): void { $assert_session = $this->assertSession(); $page = $this->getSession()->getPage(); @@ -265,14 +267,14 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase { // the applying stage. $cream_cheese_module_selector = '#project-browser .pb-layout__main ul > li:nth-child(1)'; $cream_cheese_button = $page->find('css', "$cream_cheese_module_selector button.project__action_button"); - $cream_cheese_button->click(); + $cream_cheese_button?->click(); $this->assertTrue($assert_session->waitForText('The process for adding projects is locked, but that lock has expired. Use unlock link to unlock the process and try to add the project again.')); // Click Unlock Install Stage link. $this->clickWithWait('#ui-id-1 > p > a'); $this->svelteInitHelper('text', 'Cream cheese on a bagel'); // Try beginning another install after breaking lock. $cream_cheese_button = $page->find('css', "$cream_cheese_module_selector button.project__action_button"); - $cream_cheese_button->click(); + $cream_cheese_button?->click(); $installed_action = $assert_session->waitForElementVisible('css', "$cream_cheese_module_selector .project_status-indicator", 30000); $assert_session->waitForText('Cream cheese on a bagel is Installed'); $this->assertSame('Cream cheese on a bagel is Installed', $installed_action->getText()); @@ -373,7 +375,7 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase { $this->assertTrue($was_selected); $dancing_queen_button = $page->find('css', '#project-browser .pb-layout__main ul > li:nth-child(3) button'); - $this->assertFalse($dancing_queen_button->hasAttribute('disabled')); + $this->assertFalse($dancing_queen_button?->hasAttribute('disabled')); $this->assertNotEmpty($assert_session->waitForButton('Install selected projects')); @@ -413,7 +415,7 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase { /** * Tests that adding projects to queue is plugin specific. */ - public function testPluginSpecificQueue() { + public function testPluginSpecificQueue(): void { $assert_session = $this->assertSession(); $this->container->get('module_installer')->install(['project_browser_devel'], TRUE); $this->drupalGet('project-browser/project_browser_test_mock'); @@ -451,10 +453,14 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase { $this->assertSame('Install Cream cheese on a bagel', $download_button->getText()); $download_button->click(); $unlock_url = $assert_session->waitForElementVisible('css', "#unlock-link")->getAttribute('href'); - $this->assertStringEndsWith('/admin/modules/project_browser/install/unlock', parse_url($unlock_url, PHP_URL_PATH)); - $query = parse_url($unlock_url, PHP_URL_QUERY); - parse_str($query, $query); + $path_string = parse_url($unlock_url, PHP_URL_PATH); + $this->assertIsString($path_string); + $this->assertStringEndsWith('/admin/modules/project_browser/install/unlock', $path_string); + $query_string = parse_url($unlock_url, PHP_URL_QUERY); + $this->assertIsString($query_string); + parse_str($query_string, $query); $this->assertNotEmpty($query['token']); + $this->assertIsString($query['destination']); $this->assertStringEndsWith('/admin/modules/browse/project_browser_test_mock', $query['destination']); } diff --git a/tests/src/FunctionalJavascript/ProjectBrowserPluginTest.php b/tests/src/FunctionalJavascript/ProjectBrowserPluginTest.php index 252acbd106e2f8f460f7f6ea7a3999a7432f0f40..fbbc5ebbadfc979b83b57fa42bd4499ff2a1252d 100644 --- a/tests/src/FunctionalJavascript/ProjectBrowserPluginTest.php +++ b/tests/src/FunctionalJavascript/ProjectBrowserPluginTest.php @@ -91,7 +91,7 @@ class ProjectBrowserPluginTest extends WebDriverTestBase { $this->svelteInitHelper('css', '.pager__item--next'); $assert_session->elementsCount('css', '.pager__item--next', 1); - $page->find('css', 'a[aria-label="Next page"]')->click(); + $page->find('css', 'a[aria-label="Next page"]')?->click(); $this->assertNotNull($assert_session->waitForElement('css', '.pager__item--previous')); $assert_session->elementsCount('css', '.pager__item--previous', 1); } @@ -163,7 +163,7 @@ class ProjectBrowserPluginTest extends WebDriverTestBase { $assert_session->waitForElementVisible('css', '.pb-project .pb-project__title'); $first_project_selector = $page->find('css', '.pb-project .pb-project__title .pb-project__link'); - $first_project_selector->click(); + $first_project_selector?->click(); $this->assertTrue($assert_session->waitForText('sites report using this module')); } diff --git a/tests/src/FunctionalJavascript/ProjectBrowserUiTest.php b/tests/src/FunctionalJavascript/ProjectBrowserUiTest.php index 2fd9a08f96de5efc8d1413babebe8498cdf88e3d..b15114f2456ea41cf4682789dd339b2e05686587 100644 --- a/tests/src/FunctionalJavascript/ProjectBrowserUiTest.php +++ b/tests/src/FunctionalJavascript/ProjectBrowserUiTest.php @@ -152,13 +152,13 @@ class ProjectBrowserUiTest extends WebDriverTestBase { $this->clickWithWait('#104', '10 Results'); // Use blur event to close drop-down so Clear is visible. - $this->assertSession()->elementExists('css', '.pb-filter__multi-dropdown')->blur(); + $assert_session->elementExists('css', '.pb-filter__multi-dropdown')->blur(); $this->pressWithWait('Clear filters', '25 Results'); // Open category drop-down again by pressing space. - $this->assertSession()->elementExists('css', '.pb-filter__multi-dropdown')->keyDown(' '); - $this->assertSession()->waitForText('Media'); + $assert_session->elementExists('css', '.pb-filter__multi-dropdown')->keyDown(' '); + $assert_session->waitForText('Media'); // Click 'Media' checkbox. $this->clickWithWait('#67'); @@ -214,12 +214,15 @@ class ProjectBrowserUiTest extends WebDriverTestBase { // The first textarea should have the command to require the module. $this->assertSame('composer require drupal/helvetica', $command_boxes[0]->getValue()); // And the second textarea should have the command to install it. - $this->assertStringEndsWith('drush install helvetica', $command_boxes[1]->getValue()); + $value = $command_boxes[1]->getValue(); + $this->assertIsString($value); + $this->assertStringEndsWith('drush install helvetica', $value); // Tests alt text for copy command image. $download_commands = $page->findAll('css', '.command-box img'); $this->assertCount(2, $download_commands); $this->assertEquals('Copy the download command', $download_commands[0]->getAttribute('alt')); + $this->assertIsString($download_commands[1]->getAttribute('alt')); $this->assertStringStartsWith('Copy the install command', $download_commands[1]->getAttribute('alt')); } @@ -531,6 +534,8 @@ class ProjectBrowserUiTest extends WebDriverTestBase { * Tests search with strings that need URI encoding. */ public function testSearchForSpecialChar(): void { + $assert_session = $this->assertSession(); + // Clear filters. $this->drupalGet('admin/modules/browse/project_browser_test_mock'); $this->svelteInitHelper('text', '10 Results'); @@ -539,7 +544,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase { // Fill in the search field. $this->inputSearchField('', TRUE); $this->inputSearchField('&', TRUE); - $this->assertSession()->waitForElementVisible('css', ".search__search-submit")->click(); + $assert_session->waitForElementVisible('css', ".search__search-submit")?->click(); $this->assertProjectsVisible([ 'Vitamin&C;$?', 'Unwritten&:/', @@ -548,7 +553,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase { // Fill in the search field. $this->inputSearchField('', TRUE); $this->inputSearchField('n&', TRUE); - $this->assertSession()->waitForElementVisible('css', ".search__search-submit")->click(); + $assert_session->waitForElementVisible('css', ".search__search-submit")?->click(); $this->assertProjectsVisible([ 'Vitamin&C;$?', 'Unwritten&:/', @@ -556,28 +561,28 @@ class ProjectBrowserUiTest extends WebDriverTestBase { $this->inputSearchField('', TRUE); $this->inputSearchField('$', TRUE); - $this->assertSession()->waitForElementVisible('css', ".search__search-submit")->click(); + $assert_session->waitForElementVisible('css', ".search__search-submit")?->click(); $this->assertProjectsVisible([ 'Vitamin&C;$?', ]); $this->inputSearchField('', TRUE); $this->inputSearchField('?', TRUE); - $this->assertSession()->waitForElementVisible('css', ".search__search-submit")->click(); + $assert_session->waitForElementVisible('css', ".search__search-submit")?->click(); $this->assertProjectsVisible([ 'Vitamin&C;$?', ]); $this->inputSearchField('', TRUE); $this->inputSearchField('&:', TRUE); - $this->assertSession()->waitForElementVisible('css', ".search__search-submit")->click(); + $assert_session->waitForElementVisible('css', ".search__search-submit")?->click(); $this->assertProjectsVisible([ 'Unwritten&:/', ]); $this->inputSearchField('', TRUE); $this->inputSearchField('$?', TRUE); - $this->assertSession()->waitForElementVisible('css', ".search__search-submit")->click(); + $assert_session->waitForElementVisible('css', ".search__search-submit")?->click(); $this->assertProjectsVisible([ 'Vitamin&C;$?', ]); @@ -614,7 +619,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase { $assert_session->waitForButton('Close')?->click(); $assert_session->elementNotExists('xpath', '//span[contains(@class, "ui-dialog-title") and text()="Helvetica"]'); // Check that a different module modal can be opened. - $assert_session->waitForButton('Octopus')->click(); + $assert_session->waitForButton('Octopus')?->click(); $assert_session->waitForElementVisible('xpath', '//span[contains(@class, "ui-dialog-title") and text()="Octopus"]'); $assert_session->waitForButton('Close')?->click(); $assert_session->elementNotExists('xpath', '//span[contains(@class, "ui-dialog-title") and text()="Octopus"]'); @@ -628,7 +633,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase { */ public function testPersistence(): void { $this->markTestSkipped('Skipped because the persistence layer has been removed for now and needs to be rewritten.'); - + // @phpstan-ignore deadCode.unreachable $assert_session = $this->assertSession(); $this->drupalGet('admin/modules/browse/project_browser_test_mock'); $this->svelteInitHelper('text', 'Clear Filters'); @@ -642,7 +647,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase { $this->clickWithWait(self::DEVELOPMENT_OPTION_SELECTOR . self::OPTION_FIRST_CHILD); // Open category drop-down. - $assert_session->elementExists('css', '.pb-filter__multi-dropdown')->click(); + $assert_session->elementExists('css', '.pb-filter__multi-dropdown')?->click(); // Select the E-commerce filter. $assert_session->waitForElementVisible('css', '#104'); @@ -730,7 +735,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase { */ public function testMultiplePlugins(): void { $this->markTestSkipped('This test is skipped because it needs to be rewritten now that in-app tabbing and persistence is removed.'); - + // @phpstan-ignore deadCode.unreachable $page = $this->getSession()->getPage(); $assert_session = $this->assertSession(); // Enable module for extra source plugin. @@ -768,7 +773,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase { $this->assertSame($second_tab_text, $assert_session->buttonExists('random_data')->getText()); // Use blur event to close drop-down so Clear is visible. - $this->assertSession()->elementExists('css', '.pb-filter__multi-dropdown')->blur(); + $assert_session->elementExists('css', '.pb-filter__multi-dropdown')->blur(); $this->assertSame('2 categories selected', $page->find('css', '.pb-filter__multi-dropdown__label')->getText()); // Click other tab. @@ -781,7 +786,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase { $this->assertEquals($second_tab_text . ' (active tab)', $page->findButton('random_data')->getText()); // Open category drop-down again by pressing space. - $this->assertSession()->elementExists('css', '.pb-filter__multi-dropdown')->keyDown(' '); + $assert_session->elementExists('css', '.pb-filter__multi-dropdown')->keyDown(' '); // Apply the second module category filter. $second_category_filter_selector = '.pb-filter__multi-dropdown__items > div:nth-child(2) input'; @@ -822,7 +827,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase { $this->pressWithWait('project_browser_test_mock'); // Filter by search text. $this->inputSearchField('Number', TRUE); - $assert_session->waitForElementVisible('css', ".search__search-submit")->click(); + $assert_session->waitForElementVisible('css', ".search__search-submit")?->click(); $this->assertTrue($assert_session->waitForText('2 Results')); $this->assertProjectsVisible([ '9 Starts With a Higher Number', @@ -846,6 +851,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase { * Tests the view mode toggle keeps its state. */ public function testToggleViewState(): void { + $page = $this->getSession()->getPage(); $assert_session = $this->assertSession(); $viewSwitches = [ [ @@ -861,12 +867,12 @@ class ProjectBrowserUiTest extends WebDriverTestBase { foreach ($viewSwitches as $selector) { $this->drupalGet('admin/modules/browse/project_browser_test_mock'); $this->svelteInitHelper('css', $selector['selector']); - $this->getSession()->getPage()->pressButton($selector['value']); + $page->pressButton($selector['value']); $this->svelteInitHelper('text', 'Helvetica'); $assert_session->waitForButton('Helvetica')?->click(); $this->svelteInitHelper('text', 'Close'); $assert_session->waitForButton('Close')?->click(); - $this->assertSession()->elementExists('css', $selector['selector'] . '.pb-display__button--selected'); + $assert_session->elementExists('css', $selector['selector'] . '.pb-display__button--selected'); } } @@ -893,7 +899,9 @@ class ProjectBrowserUiTest extends WebDriverTestBase { $this->drupalGet('admin/config/development/project_browser'); $first_plugin = $page->find('css', '#source--project_browser_test_mock'); $second_plugin = $page->find('css', '#source--random_data'); - $first_plugin->find('css', '.handle')->dragTo($second_plugin); + $this->assertNotNull($second_plugin); + $first_plugin?->find('css', '.handle')?->dragTo($second_plugin); + $this->assertNotNull($first_plugin); $this->assertTableRowWasDragged($first_plugin); $this->submitForm([], 'Save'); @@ -905,7 +913,9 @@ class ProjectBrowserUiTest extends WebDriverTestBase { $this->drupalGet('admin/config/development/project_browser'); $enabled_row = $page->find('css', '#source--project_browser_test_mock'); $disabled_region_row = $page->find('css', '.status-title-disabled'); - $enabled_row->find('css', '.handle')->dragTo($disabled_region_row); + $this->assertNotNull($disabled_region_row); + $enabled_row?->find('css', '.handle')?->dragTo($disabled_region_row); + $this->assertNotNull($enabled_row); $this->assertTableRowWasDragged($enabled_row); $this->submitForm([], 'Save'); $assert_session->pageTextContains('The configuration options have been saved.'); @@ -930,6 +940,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase { * Tests the visibility of categories in list and grid view. */ public function testCategoriesVisibility(): void { + $page = $this->getSession()->getPage(); $assert_session = $this->assertSession(); $view_options = [ [ @@ -946,7 +957,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase { foreach ($view_options as $selector) { $this->drupalGet('admin/modules/browse/project_browser_test_mock'); $this->svelteInitHelper('css', $selector['selector']); - $this->getSession()->getPage()->pressButton($selector['value']); + $page->pressButton($selector['value']); $this->svelteInitHelper('text', 'Helvetica'); $assert_session->elementsCount('css', '#project-browser .pb-layout__main ul li:nth-child(7) .pb-project-categories ul li', 1); $grid_text = $this->getElementText('#project-browser .pb-layout__main ul li:nth-child(7) .pb-project-categories ul li:nth-child(1)'); @@ -1010,13 +1021,15 @@ class ProjectBrowserUiTest extends WebDriverTestBase { $this->svelteInitHelper('css', '.pb-project.pb-project--list'); $this->inputSearchField('inline form errors', TRUE); - $assert_session->waitForElementVisible('css', ".search__search-submit")->click(); + $assert_session->waitForElementVisible('css', ".search__search-submit")?->click(); $this->svelteInitHelper('text', 'Inline Form Errors'); $install_link = $page->find('css', '.pb-layout__main .pb-actions a'); - $this->assertStringContainsString('admin/modules#module-inline-form-errors', $install_link->getAttribute('href')); - $this->drupalGet($install_link->getAttribute('href')); + $href = $install_link?->getAttribute('href'); + $this->assertIsString($href); + $this->assertStringContainsString('admin/modules#module-inline-form-errors', $href); + $this->drupalGet($href); $assert_session->waitForElementVisible('css', "#edit-modules-inline-form-errors-enable"); $assert_session->assertVisibleInViewport('css', '#edit-modules-inline-form-errors-enable'); } @@ -1024,10 +1037,12 @@ class ProjectBrowserUiTest extends WebDriverTestBase { /** * Confirms UI install can not be enabled without Package Manager installed. */ - public function testUiInstallNeedsPackageManager() { + public function testUiInstallNeedsPackageManager(): void { + $page = $this->getSession()->getPage(); + $this->drupalGet('admin/config/development/project_browser'); - $ui_install_input = $this->getSession()->getPage()->find('css', '[data-drupal-selector="edit-allow-ui-install"]'); - $this->assertTrue($ui_install_input->getAttribute('disabled') === 'disabled'); + $ui_install_input = $page->find('css', '[data-drupal-selector="edit-allow-ui-install"]'); + $this->assertTrue($ui_install_input?->getAttribute('disabled') === 'disabled'); // @todo Remove try/catch in https://www.drupal.org/i/3349193. try { @@ -1037,14 +1052,14 @@ class ProjectBrowserUiTest extends WebDriverTestBase { $this->markTestSkipped($e->getMessage()); } $this->drupalGet('admin/config/development/project_browser'); - $ui_install_input = $this->getSession()->getPage()->find('css', '[data-drupal-selector="edit-allow-ui-install"]'); - $this->assertFalse($ui_install_input->hasAttribute('disabled')); + $ui_install_input = $page->find('css', '[data-drupal-selector="edit-allow-ui-install"]'); + $this->assertFalse($ui_install_input?->hasAttribute('disabled')); } /** * Tests that we can clear search results with one click. */ - public function testClearKeywordSearch() { + public function testClearKeywordSearch(): void { $assert_session = $this->assertSession(); $this->drupalGet('admin/modules/browse/project_browser_test_mock'); $this->svelteInitHelper('css', '.pb-search-results'); @@ -1056,7 +1071,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase { // Search for something to change it. $this->inputSearchField('abcdefghijklmnop', TRUE); - $assert_session->waitForElementVisible('css', ".search__search-submit")->click(); + $assert_session->waitForElementVisible('css', ".search__search-submit")?->click(); $this->assertTrue($results->waitFor(10, fn (NodeElement $element) => $element->getText() !== $original_text)); // Remove the search text and make sure it auto-updates. @@ -1072,13 +1087,13 @@ class ProjectBrowserUiTest extends WebDriverTestBase { */ public function testSearchClearNoTabIndex(): void { $page = $this->getSession()->getPage(); - $this->assertSession(); + $assert_session = $this->assertSession(); $this->drupalGet('admin/modules/browse/project_browser_test_mock'); $this->svelteInitHelper('css', '.pb-search-results'); // Search and confirm clear button has no focus after tabbing. $this->inputSearchField('abcdefghijklmnop', TRUE); - $this->assertSession()->waitForElementVisible('css', ".search__search-submit")->click(); + $assert_session->waitForElementVisible('css', ".search__search-submit")?->click(); $this->getSession()->getDriver()->keyPress($page->getXpath(), '9'); $has_focus_id = $this->getSession()->evaluateScript('document.activeElement.id'); @@ -1101,7 +1116,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase { $this->drupalGet('admin/modules/browse/recipes'); $this->svelteInitHelper('css', '.pb-projects-list'); $this->inputSearchField('image', TRUE); - $assert_session->waitForElementVisible('css', ".search__search-submit")->click(); + $assert_session->waitForElementVisible('css', ".search__search-submit")?->click(); // Look for a recipe that ships with core. $card = $assert_session->waitForElementVisible('css', '.pb-project:contains("Image media type")'); @@ -1150,6 +1165,8 @@ class ProjectBrowserUiTest extends WebDriverTestBase { * Tests that each source plugin has its own dedicated route. */ public function testSourcePluginRoutes(): void { + $assert_session = $this->assertSession(); + // Enable module for extra source plugin. $this->container->get('module_installer')->install(['project_browser_devel'], TRUE); $this->rebuildContainer(); @@ -1159,7 +1176,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase { foreach (array_keys($current_sources) as $plugin_id) { $this->drupalGet("/admin/modules/browse/{$plugin_id}"); - $this->assertNotNull($this->assertSession()->waitForElementVisible('css', '#project-browser .pb-project.pb-project--list')); + $this->assertNotNull($assert_session->waitForElementVisible('css', '#project-browser .pb-project.pb-project--list')); } } @@ -1168,6 +1185,8 @@ class ProjectBrowserUiTest extends WebDriverTestBase { */ public function testWrenchIcon(): void { $assert_session = $this->assertSession(); + $page = $this->getSession()->getPage(); + $this->getSession()->resizeWindow(1460, 960); $this->drupalGet('admin/modules/browse/project_browser_test_mock'); $this->svelteInitHelper('text', 'Helvetica'); @@ -1177,7 +1196,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase { $this->assertTrue($assert_session->waitForText('The module is actively maintained by the maintainers')); // This asserts that status icon is present in detail's modal. $this->assertNotNull($assert_session->waitForElementVisible('css', '.pb-detail-modal__sidebar .pb-project__status-icon-btn')); - $this->getSession()->getPage()->find('css', '.ui-dialog-titlebar-close')->click(); + $page->find('css', '.ui-dialog-titlebar-close')?->click(); $this->clickWithWait(self::MAINTENANCE_OPTION_SELECTOR . self::OPTION_LAST_CHILD); $this->assertEquals('Show all', $this->getElementText(self::MAINTENANCE_OPTION_SELECTOR . self::OPTION_LAST_CHILD)); @@ -1204,21 +1223,21 @@ class ProjectBrowserUiTest extends WebDriverTestBase { // Locate and click the Grapefruit project link. $grapefruit_link = $page->find('xpath', '//button[contains(@class, "pb-project__link") and contains(text(), "Grapefruit")]'); - $grapefruit_link->click(); + $grapefruit_link?->click(); // Verify the text for Grapefruit (singular case). $this->assertTrue($assert_session->waitForText('site reports using this module')); // Go back to the project list. $close_button = $page->find('xpath', '//button[contains(@class, "ui-dialog-titlebar-close") and contains(text(), "Close")]'); - $close_button->click(); + $close_button?->click(); // Expect Octopus to have 235 installs. $assert_session->elementExists('xpath', '//span[contains(@class, "pb-project__install-count") and text()="235 installs"]'); // Locate and click the Octopus project link. $octopus_link = $page->find('xpath', '//button[contains(@class, "pb-project__link") and contains(text(), "Octopus")]'); - $octopus_link->click(); + $octopus_link?->click(); // Verify the text for Octopus (plural case). $this->assertTrue($assert_session->waitForText('sites report using this module')); diff --git a/tests/src/FunctionalJavascript/ProjectBrowserUiTestJsonApi.php b/tests/src/FunctionalJavascript/ProjectBrowserUiTestJsonApi.php index 7e829566a26f90240df8fd7f58b6e41e92bceb4a..7154f5b6cdd85d31bbe8ea662aa502eedc100f67 100644 --- a/tests/src/FunctionalJavascript/ProjectBrowserUiTestJsonApi.php +++ b/tests/src/FunctionalJavascript/ProjectBrowserUiTestJsonApi.php @@ -418,7 +418,9 @@ class ProjectBrowserUiTestJsonApi extends WebDriverTestBase { $this->drupalGet('admin/config/development/project_browser'); $first_plugin = $page->find('css', '#source--drupalorg_jsonapi'); $second_plugin = $page->find('css', '#source--random_data'); - $first_plugin->find('css', '.tabledrag-handle')->dragTo($second_plugin); + $this->assertNotNull($second_plugin); + $first_plugin?->find('css', '.tabledrag-handle')?->dragTo($second_plugin); + $this->assertNotNull($first_plugin); $this->assertTableRowWasDragged($first_plugin); $this->submitForm([], 'Save'); @@ -432,7 +434,9 @@ class ProjectBrowserUiTestJsonApi extends WebDriverTestBase { $this->drupalGet('admin/config/development/project_browser'); $enabled_row = $page->find('css', '#source--drupalorg_jsonapi'); $disabled_region_row = $page->find('css', '.status-title-disabled'); - $enabled_row->find('css', '.handle')->dragTo($disabled_region_row); + $this->assertNotNull($disabled_region_row); + $enabled_row?->find('css', '.handle')?->dragTo($disabled_region_row); + $this->assertNotNull($enabled_row); $this->assertTableRowWasDragged($enabled_row); $this->submitForm([], 'Save'); $assert_session->pageTextContains('The configuration options have been saved.'); diff --git a/tests/src/FunctionalJavascript/ProjectBrowserUiTestTrait.php b/tests/src/FunctionalJavascript/ProjectBrowserUiTestTrait.php index 12440efeba96f33eb136f9b17220fbf00c63c0e9..b17b3420b452b1736449d5584b4df2a884d3afe0 100644 --- a/tests/src/FunctionalJavascript/ProjectBrowserUiTestTrait.php +++ b/tests/src/FunctionalJavascript/ProjectBrowserUiTestTrait.php @@ -55,7 +55,7 @@ trait ProjectBrowserUiTestTrait { try { $this->assertTrue($this->getSession()->evaluateScript($script), 'Ran:' . $script . 'Svelte did not initialize. Markup: ' . $this->getSession()->evaluateScript('document.querySelector("#project-browser").innerHTML')); } - catch (\Exception $e) { + catch (\Exception) { $this->getSession()->reload(); $this->getSession()->wait(10000, $script); } @@ -72,7 +72,7 @@ trait ProjectBrowserUiTestTrait { * @param bool $bypass_wait * When TRUE, do not wait for a rerender after entering a search string. */ - protected function inputSearchField(string $value, bool $bypass_wait = FALSE) { + protected function inputSearchField(string $value, bool $bypass_wait = FALSE): void { $search_field = $this->assertSession()->waitForElementVisible('css', '#pb-text'); if ($bypass_wait) { $search_field->setValue($value); @@ -94,7 +94,7 @@ trait ProjectBrowserUiTestTrait { * @param bool $bypass_wait * When TRUE, do not wait for a rerender after entering a search string. */ - protected function pressWithWait(string $locator, string $wait_for_text = '', bool $bypass_wait = FALSE) { + protected function pressWithWait(string $locator, string $wait_for_text = '', bool $bypass_wait = FALSE): void { if ($bypass_wait) { $this->getSession()->getPage()->pressButton($locator); } @@ -119,7 +119,7 @@ trait ProjectBrowserUiTestTrait { * @param bool $bypass_wait * When TRUE, do not wait for a rerender after entering a search string. */ - protected function clickWithWait(string|NodeElement $element, string $wait_for_text = '', bool $bypass_wait = FALSE) { + protected function clickWithWait(string|NodeElement $element, string $wait_for_text = '', bool $bypass_wait = FALSE): void { if (is_string($element)) { $element = $this->assertSession()->elementExists('css', $element); } @@ -151,9 +151,9 @@ trait ProjectBrowserUiTestTrait { /** * Opens the advanced filter element. */ - protected function openAdvancedFilter() { + protected function openAdvancedFilter(): void { $filter_icon_selector = $this->getSession()->getPage()->find('css', '.search__filter__toggle'); - $filter_icon_selector->click(); + $filter_icon_selector?->click(); $this->assertSession()->waitForElementVisible('css', '.search__filter__toggle[aria-expanded="true"]'); } @@ -165,7 +165,7 @@ trait ProjectBrowserUiTestTrait { * @param bool $bypass_wait * When TRUE, do not wait for a rerender after entering a search string. */ - protected function sortBy(string $value, bool $bypass_wait = FALSE) { + protected function sortBy(string $value, bool $bypass_wait = FALSE): void { if ($bypass_wait) { $this->getSession()->getPage()->selectFieldOption('pb-sort', $value); } @@ -179,14 +179,14 @@ trait ProjectBrowserUiTestTrait { /** * Add an attribute to a project card that will vanish after filtering. */ - protected function preFilterWait() { + protected function preFilterWait(): void { $this->getSession()->executeScript("document.querySelectorAll('.pb-project').forEach((project) => project.setAttribute('data-pre-filter', 'true'))"); } /** * Confirm the attribute added in preFilterWait() is no longer present. */ - protected function postFilterWait() { + protected function postFilterWait(): void { $this->assertSession()->assertNoElementAfterWait('css', '[data-pre-filter]'); } @@ -204,7 +204,7 @@ trait ProjectBrowserUiTestTrait { * @param int $timeout * Timeout in milliseconds, defaults to 10000. */ - protected function svelteInitHelper(string $check_type, string $check_value, int $timeout = 10000) { + protected function svelteInitHelper(string $check_type, string $check_value, int $timeout = 10000): void { if ($check_type === 'css') { if (!$this->assertSession()->waitForElement('css', $check_value, $timeout)) { $this->getSession()->reload(); @@ -232,7 +232,7 @@ trait ProjectBrowserUiTestTrait { * @return string * The trimmed text content of the element. */ - protected function getElementText($selector) { + protected function getElementText(string $selector): string { return trim($this->getSession()->evaluateScript("document.querySelector('$selector').textContent")); } @@ -274,7 +274,7 @@ trait ProjectBrowserUiTestTrait { * @param \Behat\Mink\Element\NodeElement|null $container * The container to look within. */ - protected function waitForField(string $locator, ?NodeElement $container = NULL): NodeElement { + protected function waitForField(string $locator, ?NodeElement $container = NULL): ?NodeElement { $container ??= $this->getSession()->getPage(); $this->assertTrue( $container->waitFor(10, fn ($container) => $container->findField($locator)?->isVisible()), diff --git a/tests/src/FunctionalJavascript/TranslatedSvelteAppTest.php b/tests/src/FunctionalJavascript/TranslatedSvelteAppTest.php index 73f292b4c0bc89b603f5e904d471cf9405795311..86d2864f1ff282f5035a28c8192a3daa09a6d186 100644 --- a/tests/src/FunctionalJavascript/TranslatedSvelteAppTest.php +++ b/tests/src/FunctionalJavascript/TranslatedSvelteAppTest.php @@ -38,7 +38,7 @@ class TranslatedSvelteAppTest extends WebDriverTestBase { * 90% of this is code borrowed from * \Drupal\Tests\locale\Functional\LocaleContentTest. */ - public function testTranslation() { + public function testTranslation(): void { $admin_user = $this->drupalCreateUser([ 'administer languages', 'access administration pages', diff --git a/tests/src/Kernel/CoreExperimentalLabelTest.php b/tests/src/Kernel/CoreExperimentalLabelTest.php index 61976f33191e19599dd1e63970ceb99eda4ccb7b..17c787316367deb9f0ca5c49fa40c9a0edf40eba 100644 --- a/tests/src/Kernel/CoreExperimentalLabelTest.php +++ b/tests/src/Kernel/CoreExperimentalLabelTest.php @@ -1,5 +1,7 @@ <?php +declare(strict_types=1); + namespace Drupal\Tests\project_browser\Kernel; use Drupal\KernelTests\KernelTestBase; @@ -32,10 +34,11 @@ class CoreExperimentalLabelTest extends KernelTestBase { * @covers ::getProjectData */ public function testCoreExperimentalLabel(): void { + /** @var \Drupal\project_browser\Plugin\ProjectBrowserSourceInterface $plugin_instance */ $plugin_instance = $this->container->get(ProjectBrowserSourceManager::class) ->createInstance('drupal_core'); $modules_to_test = ['Experimental Test', 'System']; - $filtered_projects = array_filter($plugin_instance->getProjects()->list, fn(Project $value) => in_array($value->title, $modules_to_test)); + $filtered_projects = array_filter($plugin_instance->getProjects()->list, fn(Project $value): bool => in_array($value->title, $modules_to_test)); $this->assertCount(2, $filtered_projects); foreach ($filtered_projects as $project) { if ($project->title === 'System') { diff --git a/tests/src/Kernel/CoreNotUpdatedValidatorTest.php b/tests/src/Kernel/CoreNotUpdatedValidatorTest.php index d2ef82e5c7fb66de30be69d739a430f5b7522151..87dc30f6724effbbcd3792649cd5a62e569184ac 100644 --- a/tests/src/Kernel/CoreNotUpdatedValidatorTest.php +++ b/tests/src/Kernel/CoreNotUpdatedValidatorTest.php @@ -1,7 +1,10 @@ <?php +declare(strict_types=1); + namespace Drupal\Tests\project_browser\Kernel; +use Drupal\package_manager\Event\PreOperationStageEvent; use Drupal\Tests\package_manager\Kernel\PackageManagerKernelTestBase; use Drupal\fixture_manipulator\ActiveFixtureManipulator; use Drupal\package_manager\Exception\StageEventException; @@ -70,8 +73,9 @@ class CoreNotUpdatedValidatorTest extends PackageManagerKernelTestBase { */ public function testPreApplyException(bool $core_updated, array $expected_results): void { if ($core_updated) { - $this->getStageFixtureManipulator()->setCorePackageVersion('9.8.1'); + $this->getStageFixtureManipulator()?->setCorePackageVersion('9.8.1'); } + /** @var \Drupal\project_browser\ComposerInstaller\Installer $installer */ $installer = $this->container->get(Installer::class); $installer->create(); $installer->require(['org/package-name']); @@ -81,6 +85,7 @@ class CoreNotUpdatedValidatorTest extends PackageManagerKernelTestBase { $this->assertEmpty($expected_results); } catch (StageEventException $e) { + assert($e->event instanceof PreOperationStageEvent); $this->assertValidationResultsEqual($expected_results, $e->event->getResults()); } } diff --git a/tests/src/Kernel/DatabaseTablesTest.php b/tests/src/Kernel/DatabaseTablesTest.php index 6f6ef2a3d380ba958f74cc6e6c7629995ad88fd0..f77d32ec9ad48a97f8aaa17d001ed6b97489597c 100644 --- a/tests/src/Kernel/DatabaseTablesTest.php +++ b/tests/src/Kernel/DatabaseTablesTest.php @@ -1,5 +1,7 @@ <?php +declare(strict_types=1); + namespace Drupal\Tests\project_browser\Kernel; use Drupal\KernelTests\KernelTestBase; @@ -36,7 +38,7 @@ class DatabaseTablesTest extends KernelTestBase { project_browser_test_install(); $this->container = \Drupal::getContainer(); - /** @var \Drupal\Core\Database\Schema $database */ + /** @var \Drupal\Core\Database\Schema $schema */ $schema = $this->container->get('database')->schema(); $this->assertTrue($schema->tableExists('project_browser_projects')); $this->assertTrue($schema->tableExists('project_browser_categories')); @@ -44,9 +46,11 @@ class DatabaseTablesTest extends KernelTestBase { // Make sure the fixture files do have data in them. /** @var \Drupal\Core\Database\Connection $database */ $database = $this->container->get('database'); - $rows = $database->select('project_browser_projects')->countQuery()->execute()->fetchCol(); + $rows = $database->select('project_browser_projects')->countQuery()->execute()?->fetchCol(); + $this->assertIsArray($rows); $this->assertGreaterThan(1, $rows[0]); - $rows = $database->select('project_browser_categories')->countQuery()->execute()->fetchCol(); + $rows = $database->select('project_browser_categories')->countQuery()->execute()?->fetchCol(); + $this->assertIsArray($rows); $this->assertGreaterThan(1, $rows[0]); $module_installer->uninstall(['project_browser_test']); diff --git a/tests/src/Kernel/InstallerTest.php b/tests/src/Kernel/InstallerTest.php index 9a25957209dcfd00798f1e5109526f06475d0f2e..04cc7ab648637dda92d847f309c95f6cd5291c10 100644 --- a/tests/src/Kernel/InstallerTest.php +++ b/tests/src/Kernel/InstallerTest.php @@ -1,5 +1,7 @@ <?php +declare(strict_types=1); + namespace Drupal\Tests\project_browser\Kernel; use Drupal\Tests\package_manager\Kernel\PackageManagerKernelTestBase; @@ -54,7 +56,7 @@ class InstallerTest extends PackageManagerKernelTestBase { /** * Data provider for testCommitException(). * - * @return \string[][] + * @return string[][] * The test cases. */ public static function providerCommitException(): array { @@ -77,14 +79,15 @@ class InstallerTest extends PackageManagerKernelTestBase { /** * Tests exception handling during calls to Composer Stager commit. * - * @param string $thrown_class + * @param class-string<\Throwable> $thrown_class * The throwable class that should be thrown by Composer Stager. - * @param string|null $expected_class + * @param class-string<\Throwable> $expected_class * The expected exception class. * * @dataProvider providerCommitException */ - public function testCommitException(string $thrown_class, ?string $expected_class = NULL): void { + public function testCommitException(string $thrown_class, string $expected_class): void { + /** @var \Drupal\project_browser\ComposerInstaller\Installer $installer */ $installer = $this->container->get(Installer::class); $installer->create(); $installer->require(['org/package-name']); @@ -111,7 +114,8 @@ class InstallerTest extends PackageManagerKernelTestBase { * * @covers ::dispatch */ - public function testInstallException() { + public function testInstallException(): void { + /** @var \Drupal\project_browser\ComposerInstaller\Installer $installer */ $installer = $this->container->get(Installer::class); $installer->create(); $installer->require(['org/package-name']); diff --git a/tests/src/Kernel/PackageNotInstalledValidatorTest.php b/tests/src/Kernel/PackageNotInstalledValidatorTest.php index 61000e4ebcb2914c0b5dcac6a72f6f8fd6c09746..eff4ae007d5b47a577f43606c0c76cfce739092e 100644 --- a/tests/src/Kernel/PackageNotInstalledValidatorTest.php +++ b/tests/src/Kernel/PackageNotInstalledValidatorTest.php @@ -1,7 +1,10 @@ <?php +declare(strict_types=1); + namespace Drupal\Tests\project_browser\Kernel; +use Drupal\package_manager\Event\PreOperationStageEvent; use Drupal\Tests\package_manager\Kernel\PackageManagerKernelTestBase; use Drupal\fixture_manipulator\ActiveFixtureManipulator; use Drupal\package_manager\Exception\StageEventException; @@ -85,6 +88,7 @@ class PackageNotInstalledValidatorTest extends PackageManagerKernelTestBase { // entry for it, so we can 'composer require' it later. ->removePackage('drupal/new_module') ->commitChanges(); + /** @var \Drupal\project_browser\ComposerInstaller\Installer $installer */ $installer = $this->container->get(Installer::class); try { $installer->create(); @@ -94,6 +98,7 @@ class PackageNotInstalledValidatorTest extends PackageManagerKernelTestBase { } catch (StageEventException $e) { $this->assertNotNull($expected_result); + assert($e->event instanceof PreOperationStageEvent); $this->assertValidationResultsEqual([$expected_result], $e->event->getResults()); } } diff --git a/tests/src/Kernel/RecipeActivatorTest.php b/tests/src/Kernel/RecipeActivatorTest.php index c832f0d1cc778c1f196a449f577adc21eb45ba10..c25952a2e60d6ca52126c2742718d062d04d5e12 100644 --- a/tests/src/Kernel/RecipeActivatorTest.php +++ b/tests/src/Kernel/RecipeActivatorTest.php @@ -56,6 +56,7 @@ class RecipeActivatorTest extends KernelTestBase { packageName: 'My Project', type: ProjectType::Recipe, ); + /** @var \Drupal\project_browser\ActivatorInterface $activator */ $activator = $this->container->get(ActivatorInterface::class); // As this project is not installed the RecipeActivator::getPath() will // return NULL in RecipeActivator::getStatus() and it will return the diff --git a/tests/src/Kernel/RecipesSourceTest.php b/tests/src/Kernel/RecipesSourceTest.php index 1ee60153ab91f973f90cd357c251fb9b99742d50..32f993ffd5eec73ef3462fcfbddf7cbeb9101a92 100644 --- a/tests/src/Kernel/RecipesSourceTest.php +++ b/tests/src/Kernel/RecipesSourceTest.php @@ -68,8 +68,7 @@ class RecipesSourceTest extends KernelTestBase { $this->setSetting('extension_discovery_scan_tests', TRUE); /** @var \Drupal\project_browser\Plugin\ProjectBrowserSourceInterface $source */ - $source = $this->container->get(ProjectBrowserSourceManager::class) - ->createInstance('recipes'); + $source = $this->container->get(ProjectBrowserSourceManager::class)->createInstance('recipes'); // Generate a fake recipe in the temporary directory. $generated_recipe_name = uniqid(); @@ -100,10 +99,7 @@ class RecipesSourceTest extends KernelTestBase { $expected_recipe_names[] = $core_recipe->getBasename(); } - /** @var \Drupal\project_browser\ProjectBrowser\ProjectsResultsPage $projects */ - $projects = $this->container->get(ProjectBrowserSourceManager::class) - ->createInstance('recipes') - ->getProjects(); + $projects = $source->getProjects(); $found_recipes = []; foreach ($projects->list as $project) { $this->assertNotEmpty($project->title); @@ -120,12 +116,13 @@ class RecipesSourceTest extends KernelTestBase { $this->assertSame($expected_recipe_names, $found_recipe_names); // Ensure the package names are properly resolved. - $this->assertSame('drupal/core', $found_recipes['standard']?->packageName); - $this->assertSame('project-browser-test/test-recipe', $found_recipes['test_recipe']?->packageName); + $this->assertArrayHasKey('standard', $found_recipes); + $this->assertSame('drupal/core', $found_recipes['standard']->packageName); + $this->assertArrayHasKey('test_recipe', $found_recipes); + $this->assertSame('project-browser-test/test-recipe', $found_recipes['test_recipe']->packageName); // The core recipes should have descriptions, which should become the body // text of the project. - $this->assertArrayHasKey('standard', $found_recipes); // The need for reflection sucks, but there's no way to introspect the body // on the backend. $body = (new \ReflectionProperty($found_recipes['standard'], 'body')) @@ -151,10 +148,9 @@ class RecipesSourceTest extends KernelTestBase { ]) ->save(); - /** @var \Drupal\project_browser\ProjectBrowser\ProjectsResultsPage $projects */ - $projects = $this->container->get(ProjectBrowserSourceManager::class) - ->createInstance('recipes') - ->getProjects(); + /** @var \Drupal\project_browser\Plugin\ProjectBrowserSourceInterface $source */ + $source = $this->container->get(ProjectBrowserSourceManager::class)->createInstance('recipes'); + $projects = $source->getProjects(); $found_recipe_names = array_column($projects->list, 'machineName'); // The `example` recipe (from core) should always be hidden, even if it's in diff --git a/tests/src/Traits/PackageManagerFixtureUtilityTrait.php b/tests/src/Traits/PackageManagerFixtureUtilityTrait.php index 7c10dfeef226e71aef553853a548da258e9b8d7f..bfef2179945266671d6cfc7d83b910cd0b6147b2 100644 --- a/tests/src/Traits/PackageManagerFixtureUtilityTrait.php +++ b/tests/src/Traits/PackageManagerFixtureUtilityTrait.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Drupal\Tests\project_browser\Traits; use Drupal\package_manager\PathLocator; +use Drupal\package_manager_bypass\MockPathLocator; use Symfony\Component\Filesystem\Filesystem; /** @@ -37,8 +38,9 @@ trait PackageManagerFixtureUtilityTrait { // unique for each test run. This will enable changing files in the // directory and not affect other tests. $active_dir = $this->copyFixtureToTempDirectory($fixture_directory); - $this->container->get(PathLocator::class) - ->setPaths($active_dir, $active_dir . '/vendor', '', NULL); + $path_locator = $this->container->get(PathLocator::class); + assert($path_locator instanceof MockPathLocator); + $path_locator->setPaths($active_dir, $active_dir . '/vendor', '', NULL); } /** diff --git a/tests/src/Unit/ProjectBrowserTestMockTest.php b/tests/src/Unit/ProjectBrowserTestMockTest.php index a8076ab1c115f8ab2f8ff09b72508fb4b86cd288..e286066d5d8be1979a445d997bbacfbb343137a8 100644 --- a/tests/src/Unit/ProjectBrowserTestMockTest.php +++ b/tests/src/Unit/ProjectBrowserTestMockTest.php @@ -1,8 +1,9 @@ <?php +declare(strict_types=1); + namespace Drupal\Tests\project_browser\Unit; -use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Database\Connection; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Tests\UnitTestCase; @@ -19,7 +20,7 @@ class ProjectBrowserTestMockTest extends UnitTestCase { /** * The plugin. * - * @var \Drupal\project_browser\Plugin\ProjectBrowserSource\ProjectBrowserTestMock + * @var \Drupal\project_browser_test\Plugin\ProjectBrowserSource\ProjectBrowserTestMock */ protected $plugin; @@ -51,13 +52,6 @@ class ProjectBrowserTestMockTest extends UnitTestCase { */ protected ModuleHandlerInterface $moduleHandler; - /** - * ProjectBrowser cache bin. - * - * @var \Drupal\Core\Cache\CacheBackendInterface - */ - protected $cacheBin; - /** * {@inheritdoc} */ @@ -66,14 +60,13 @@ class ProjectBrowserTestMockTest extends UnitTestCase { $this->logger = $this->createMock(LoggerInterface::class); $this->database = $this->createMock(Connection::class); - $this->cacheBin = $this->createMock(CacheBackendInterface::class); $this->state = $this->createMock('\Drupal\Core\State\StateInterface'); $this->moduleHandler = $this->createMock(ModuleHandlerInterface::class); $configuration = []; $plugin_id = $this->randomMachineName(); $plugin_definition = []; - $this->plugin = new ProjectBrowserTestMock($configuration, $plugin_id, $plugin_definition, $this->logger, $this->database, $this->state, $this->cacheBin, $this->moduleHandler); + $this->plugin = new ProjectBrowserTestMock($configuration, $plugin_id, $plugin_definition, $this->logger, $this->database, $this->state, $this->moduleHandler); } /** @@ -85,7 +78,7 @@ class ProjectBrowserTestMockTest extends UnitTestCase { * @return \ReflectionMethod * The accessible method. */ - protected static function getMethod($name) { + protected static function getMethod($name): \ReflectionMethod { $class = new \ReflectionClass(ProjectBrowserTestMock::class); $method = $class->getMethod($name); $method->setAccessible(TRUE); @@ -95,7 +88,7 @@ class ProjectBrowserTestMockTest extends UnitTestCase { /** * Tests relative to absolute URL conversion. */ - public function testRelativeToAbsoluteUrl() { + public function testRelativeToAbsoluteUrl(): void { // Project body with relative URLs. $project_data['body'] = ['value' => '<img src="/files/issues/123" alt="Image1" /><img src="/files/issues/321" alt="Image2" />']; // Expected Absolute URLs.