From 0b8e605a1ad1e8ba985862bb2a646043f70d4cf0 Mon Sep 17 00:00:00 2001 From: Pamela Barone <31375-pameeela@users.noreply.drupalcode.org> Date: Tue, 7 Jan 2025 20:35:37 +0000 Subject: [PATCH] Issue #3492262 by phenaproxima, pameeela, poker10, chrisfromredfin, catch, fjgarlin, greggles: Allow isMaintained and security coverage and number of installs to differentiate between NULL and a value --- .../ProjectBrowserSource/RandomDataPlugin.php | 2 +- .../ProjectBrowserSourceExample.php | 2 - src/DevelopmentStatus.php | 45 -------- src/Element/ProjectBrowser.php | 52 ++------- src/MaintenanceStatus.php | 45 -------- .../ProjectBrowserSource/DrupalCore.php | 4 +- .../DrupalDotOrgJsonApi.php | 17 +-- src/Plugin/ProjectBrowserSource/Recipes.php | 6 +- src/ProjectBrowser/Project.php | 25 +++-- src/SecurityStatus.php | 45 -------- sveltejs/public/build/bundle.js | Bin 297515 -> 290374 bytes sveltejs/public/build/bundle.js.map | Bin 275109 -> 269718 bytes sveltejs/src/DetailModal.svelte | 2 +- sveltejs/src/MultipleChoiceFilter.svelte | 9 +- sveltejs/src/Project/Project.svelte | 26 +++-- sveltejs/src/ProjectBrowser.svelte | 49 ++++----- sveltejs/src/Search/BooleanFilter.svelte | 4 +- sveltejs/src/Search/FilterApplied.svelte | 35 +++--- sveltejs/src/Search/Search.svelte | 101 ++++++++---------- sveltejs/src/constants.js | 13 +-- sveltejs/src/stores.js | 30 ++---- .../ProjectBrowserTestMock.php | 76 +++---------- .../ProjectBrowserInstallerUiTest.php | 7 +- .../ProjectBrowserPluginTest.php | 8 +- .../ProjectBrowserUiTest.php | 9 +- .../ProjectBrowserUiTestJsonApi.php | 10 +- tests/src/Kernel/RecipeActivatorTest.php | 2 - 27 files changed, 163 insertions(+), 461 deletions(-) delete mode 100644 src/DevelopmentStatus.php delete mode 100644 src/MaintenanceStatus.php delete mode 100644 src/SecurityStatus.php diff --git a/modules/project_browser_devel/src/Plugin/ProjectBrowserSource/RandomDataPlugin.php b/modules/project_browser_devel/src/Plugin/ProjectBrowserSource/RandomDataPlugin.php index 7b16a795f..3fb484262 100644 --- a/modules/project_browser_devel/src/Plugin/ProjectBrowserSource/RandomDataPlugin.php +++ b/modules/project_browser_devel/src/Plugin/ProjectBrowserSource/RandomDataPlugin.php @@ -145,7 +145,7 @@ class RandomDataPlugin extends ProjectBrowserSourceBase { NULL, ); $filters['developmentStatus'] = new BooleanFilter( - TRUE, + FALSE, $this->t('Show projects under active development'), $this->t('Show all'), $this->t('Development status'), 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 a8c47418a..c7c429eab 100644 --- a/modules/project_browser_source_example/src/Plugin/ProjectBrowserSource/ProjectBrowserSourceExample.php +++ b/modules/project_browser_source_example/src/Plugin/ProjectBrowserSource/ProjectBrowserSourceExample.php @@ -140,7 +140,6 @@ class ProjectBrowserSourceExample extends ProjectBrowserSourceBase { isCompatible: TRUE, isMaintained: TRUE, isCovered: TRUE, - projectUsageTotal: 0, machineName: $project_from_source['unique_name'], body: [ 'summary' => $project_from_source['short_description'], @@ -161,7 +160,6 @@ class ProjectBrowserSourceExample extends ProjectBrowserSourceBase { isCompatible: TRUE, isMaintained: TRUE, isCovered: TRUE, - projectUsageTotal: 0, machineName: $project_from_source['unique_name'] . '2', body: [ 'summary' => $project_from_source['short_description'] . ' (different commands)', diff --git a/src/DevelopmentStatus.php b/src/DevelopmentStatus.php deleted file mode 100644 index 577db57d0..000000000 --- a/src/DevelopmentStatus.php +++ /dev/null @@ -1,45 +0,0 @@ -<?php - -declare(strict_types=1); - -namespace Drupal\project_browser; - -use Drupal\Core\StringTranslation\TranslatableMarkup; - -/** - * The development statuses available to the project browser. - */ -enum DevelopmentStatus: string { - - case Active = '1'; - case All = '0'; - - /** - * Represents this enum as a set of options. - * - * @return array<string, \Drupal\Core\StringTranslation\TranslatableMarkup> - * The cases in this enum. The keys are the backing values, and the values - * are the translatable labels. - */ - public static function asOptions(): array { - $options = []; - foreach (self::cases() as $case) { - $options[$case->value] = $case->label(); - } - return $options; - } - - /** - * Returns a translatable label for the current case. - * - * @return \Drupal\Core\StringTranslation\TranslatableMarkup - * A translatable label. - */ - public function label(): TranslatableMarkup { - return match ($this) { - self::Active => t('Show projects under active development'), - self::All => t('Show all'), - }; - } - -} diff --git a/src/Element/ProjectBrowser.php b/src/Element/ProjectBrowser.php index 184179ef8..2a30bd885 100644 --- a/src/Element/ProjectBrowser.php +++ b/src/Element/ProjectBrowser.php @@ -11,12 +11,9 @@ use Drupal\Core\Render\Attribute\RenderElement; use Drupal\Core\Render\Element; use Drupal\Core\Render\Element\ElementInterface; use Drupal\Core\Render\Element\RenderElementBase; -use Drupal\project_browser\DevelopmentStatus; use Drupal\project_browser\EnabledSourceHandler; use Drupal\project_browser\InstallReadiness; -use Drupal\project_browser\MaintenanceStatus; use Drupal\project_browser\Plugin\ProjectBrowserSourceInterface; -use Drupal\project_browser\SecurityStatus; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -116,12 +113,12 @@ final class ProjectBrowser implements ElementInterface, ContainerFactoryPluginIn * An array of Drupal settings. */ private function getDrupalSettings(string $source, ?int $max_selections = NULL): array { + $source = $this->enabledSourceHandler->getCurrentSources()[$source]; + assert($source instanceof ProjectBrowserSourceInterface); + if (is_int($max_selections) && $max_selections <= 0) { throw new \InvalidArgumentException('$max_selections must be a positive integer or NULL.'); } - $current_sources = [ - $source => $this->enabledSourceHandler->getCurrentSources()[$source], - ]; $package_manager = [ 'available' => (bool) $this->configFactory->get('project_browser.admin_settings')->get('allow_ui_install'), @@ -137,51 +134,16 @@ final class ProjectBrowser implements ElementInterface, ContainerFactoryPluginIn } return [ - 'active_plugins' => array_map( - fn (ProjectBrowserSourceInterface $source) => $source->getPluginDefinition()['label'], - $current_sources, - ), + 'active_plugin' => $source->getPluginDefinition()['label'], 'module_path' => $this->moduleHandler->getModule('project_browser')->getPath(), - 'special_ids' => $this->getSpecialIds(), - 'sort_options' => array_map( - fn (ProjectBrowserSourceInterface $source) => array_values($source->getSortOptions()), - $current_sources, - ), - 'maintenance_options' => MaintenanceStatus::asOptions(), - 'security_options' => SecurityStatus::asOptions(), - 'development_options' => DevelopmentStatus::asOptions(), - 'default_plugin_id' => reset($current_sources)->getPluginId(), + 'sort_options' => array_values($source->getSortOptions()), + 'default_plugin_id' => $source->getPluginId(), 'package_manager' => $package_manager, - 'filters' => array_map( - fn (ProjectBrowserSourceInterface $source) => $source->getFilterDefinitions(), - $current_sources, - ), + 'filters' => (object) $source->getFilterDefinitions(), 'max_selections' => $max_selections, ]; } - /** - * Return special IDs for some vocabularies. - * - * @return array - * List of special IDs per vocabulary. - */ - private static function getSpecialIds(): array { - $maintained = MaintenanceStatus::Maintained; - $covered = SecurityStatus::Covered; - return [ - 'maintenance_status' => [ - 'id' => $maintained->value, - 'name' => $maintained->label(), - ], - 'security_coverage' => [ - 'id' => $covered->value, - 'name' => $covered->label(), - ], - 'all_values' => MaintenanceStatus::All->value, - ]; - } - /** * {@inheritdoc} */ diff --git a/src/MaintenanceStatus.php b/src/MaintenanceStatus.php deleted file mode 100644 index 42cae92aa..000000000 --- a/src/MaintenanceStatus.php +++ /dev/null @@ -1,45 +0,0 @@ -<?php - -declare(strict_types=1); - -namespace Drupal\project_browser; - -use Drupal\Core\StringTranslation\TranslatableMarkup; - -/** - * The maintenance statuses available to the project browser. - */ -enum MaintenanceStatus: string { - - case Maintained = '1'; - case All = '0'; - - /** - * Represents this enum as a set of options. - * - * @return array<string, \Drupal\Core\StringTranslation\TranslatableMarkup> - * The cases in this enum. The keys are the backing values, and the values - * are the translatable labels. - */ - public static function asOptions(): array { - $options = []; - foreach (self::cases() as $case) { - $options[$case->value] = $case->label(); - } - return $options; - } - - /** - * Returns a translatable label for the current case. - * - * @return \Drupal\Core\StringTranslation\TranslatableMarkup - * A translatable label. - */ - public function label(): TranslatableMarkup { - return match ($this) { - self::Maintained => t('Show actively maintained projects'), - self::All => t('Show all'), - }; - } - -} diff --git a/src/Plugin/ProjectBrowserSource/DrupalCore.php b/src/Plugin/ProjectBrowserSource/DrupalCore.php index 6a4d64e5f..12aa0cb49 100644 --- a/src/Plugin/ProjectBrowserSource/DrupalCore.php +++ b/src/Plugin/ProjectBrowserSource/DrupalCore.php @@ -9,7 +9,6 @@ use Drupal\Core\Site\Settings; use Drupal\project_browser\Plugin\ProjectBrowserSourceBase; use Drupal\project_browser\ProjectBrowser\Project; use Drupal\project_browser\ProjectBrowser\ProjectsResultsPage; -use Drupal\project_browser\SecurityStatus; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\RequestStack; @@ -128,7 +127,7 @@ class DrupalCore extends ProjectBrowserSourceBase { } // Filter by coverage. - if (!empty($query['security_advisory_coverage']) && $query['security_advisory_coverage'] === SecurityStatus::Covered->value) { + if (!empty($query['security_advisory_coverage'])) { $projects = array_filter($projects, fn(Project $project) => $project->isCovered); } @@ -191,7 +190,6 @@ class DrupalCore extends ProjectBrowserSourceBase { isCompatible: TRUE, isMaintained: TRUE, isCovered: $module->info['package'] !== 'Core (Experimental)', - projectUsageTotal: -1, machineName: $module_name, body: [ 'summary' => $module->info['description'], diff --git a/src/Plugin/ProjectBrowserSource/DrupalDotOrgJsonApi.php b/src/Plugin/ProjectBrowserSource/DrupalDotOrgJsonApi.php index 70e1496b7..45fccfb8a 100644 --- a/src/Plugin/ProjectBrowserSource/DrupalDotOrgJsonApi.php +++ b/src/Plugin/ProjectBrowserSource/DrupalDotOrgJsonApi.php @@ -9,14 +9,11 @@ use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Extension\ExtensionVersion; use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\Core\Url; -use Drupal\project_browser\DevelopmentStatus; -use Drupal\project_browser\MaintenanceStatus; use Drupal\project_browser\Plugin\ProjectBrowserSourceBase; use Drupal\project_browser\ProjectBrowser\Filter\BooleanFilter; use Drupal\project_browser\ProjectBrowser\Filter\MultipleChoiceFilter; use Drupal\project_browser\ProjectBrowser\Project; use Drupal\project_browser\ProjectBrowser\ProjectsResultsPage; -use Drupal\project_browser\SecurityStatus; use GuzzleHttp\ClientInterface; use GuzzleHttp\Exception\GuzzleException; use Psr\Log\LoggerInterface; @@ -265,7 +262,7 @@ class DrupalDotOrgJsonApi extends ProjectBrowserSourceBase { NULL, ); $filters['developmentStatus'] = new BooleanFilter( - TRUE, + FALSE, $this->t('Show projects under active development'), $this->t('Show all'), $this->t('Development status'), @@ -672,27 +669,21 @@ class DrupalDotOrgJsonApi extends ProjectBrowserSourceBase { // Maintenance options. $maintenance = NULL; if (!empty($query['maintenance_status'])) { - if ($query['maintenance_status'] == MaintenanceStatus::Maintained->value) { - $maintenance = implode(',', $maintained_values); - } + $maintenance = implode(',', $maintained_values); } $query['maintenance_status'] = $maintenance; // Development options. $development = NULL; if (!empty($query['development_status'])) { - if ($query['development_status'] == DevelopmentStatus::Active->value) { - $development = implode(',', $active_values); - } + $development = implode(',', $active_values); } $query['development_status'] = $development; // Security options. $security = NULL; if (!empty($query['security_advisory_coverage'])) { - if ($query['security_advisory_coverage'] == SecurityStatus::Covered->value) { - $security = implode(',', self::COVERED_VALUES); - } + $security = implode(',', self::COVERED_VALUES); } $query['security_advisory_coverage'] = $security; diff --git a/src/Plugin/ProjectBrowserSource/Recipes.php b/src/Plugin/ProjectBrowserSource/Recipes.php index 967ab4b2d..c10a934a3 100644 --- a/src/Plugin/ProjectBrowserSource/Recipes.php +++ b/src/Plugin/ProjectBrowserSource/Recipes.php @@ -19,7 +19,6 @@ use Drupal\project_browser\Plugin\ProjectBrowserSourceBase; use Drupal\project_browser\ProjectBrowser\Project; use Drupal\project_browser\ProjectBrowser\ProjectsResultsPage; use Drupal\project_browser\ProjectType; -use Drupal\project_browser\SecurityStatus; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\Finder\Finder; @@ -112,9 +111,6 @@ class Recipes extends ProjectBrowserSourceBase { ]), ], isCompatible: TRUE, - isMaintained: TRUE, - isCovered: TRUE, - projectUsageTotal: 0, machineName: basename($path), body: $description ? ['value' => $description] : [], title: $recipe['name'], @@ -133,7 +129,7 @@ class Recipes extends ProjectBrowserSourceBase { } // Filter by coverage. - if (!empty($query['security_advisory_coverage']) && $query['security_advisory_coverage'] === SecurityStatus::Covered->value) { + if (!empty($query['security_advisory_coverage'])) { $projects = array_filter($projects, fn(Project $project) => $project->isCovered); } diff --git a/src/ProjectBrowser/Project.php b/src/ProjectBrowser/Project.php index 01e9dda26..e03bdea3e 100644 --- a/src/ProjectBrowser/Project.php +++ b/src/ProjectBrowser/Project.php @@ -70,10 +70,6 @@ class Project implements \JsonSerializable { * Logo of the project. * @param bool $isCompatible * Whether the project is compatible with the current version of Drupal. - * @param bool $isCovered - * Whether the project is considered to be covered or not. - * @param int $projectUsageTotal - * Total usage of the project. * @param string $machineName * Value of project_machine_name of the project. * @param array $body @@ -84,8 +80,15 @@ class Project implements \JsonSerializable { * Author of the project in array format. * @param string $packageName * The Composer package name of this project, e.g. `drupal/project_browser`. - * @param bool $isMaintained - * Whether the project is considered to be maintained or not. + * @param int|null $projectUsageTotal + * (optional) Total number of sites known to be using this project, or NULL + * if this information is not known. Defaults to NULL. + * @param bool|null $isCovered + * (optional) Whether or not the project is covered by security advisories, + * or NULL if this information is not known. Defaults to NULL. + * @param bool|null $isMaintained + * (optional) Whether or not the project is considered maintained, or NULL + * if this information is not known. Defaults to NULL. * @param \Drupal\Core\Url|null $url * URL of the project, if any. Defaults to NULL. * @param array $categories @@ -107,14 +110,14 @@ class Project implements \JsonSerializable { public function __construct( public array $logo, public bool $isCompatible, - public bool $isCovered, - public int $projectUsageTotal, public string $machineName, private array $body, public string $title, public array $author, public string $packageName, - public bool $isMaintained = FALSE, + public ?int $projectUsageTotal = NULL, + public ?bool $isCovered = NULL, + public ?bool $isMaintained = NULL, public ?Url $url = NULL, public array $categories = [], public array $images = [], @@ -124,6 +127,10 @@ class Project implements \JsonSerializable { ) { $this->setSummary($body); + if (is_int($projectUsageTotal) && $projectUsageTotal < 0) { + throw new \InvalidArgumentException('The $projectUsageTotal argument cannot be a negative number.'); + } + if (is_string($type)) { // If the $type can't be mapped to a ProjectType case, use it as-is. $type = ProjectType::tryFrom($type) ?? $type; diff --git a/src/SecurityStatus.php b/src/SecurityStatus.php deleted file mode 100644 index 955975d82..000000000 --- a/src/SecurityStatus.php +++ /dev/null @@ -1,45 +0,0 @@ -<?php - -declare(strict_types=1); - -namespace Drupal\project_browser; - -use Drupal\Core\StringTranslation\TranslatableMarkup; - -/** - * The security coverage statuses available to the project browser. - */ -enum SecurityStatus: string { - - case Covered = '1'; - case All = '0'; - - /** - * Represents this enum as a set of options. - * - * @return array<string, \Drupal\Core\StringTranslation\TranslatableMarkup> - * The cases in this enum. The keys are the backing values, and the values - * are the translatable labels. - */ - public static function asOptions(): array { - $options = []; - foreach (self::cases() as $case) { - $options[$case->value] = $case->label(); - } - return $options; - } - - /** - * Returns a translatable label for the current case. - * - * @return \Drupal\Core\StringTranslation\TranslatableMarkup - * A translatable label. - */ - public function label(): TranslatableMarkup { - return match ($this) { - self::Covered => t('Show projects covered by a security policy'), - self::All => t('Show all'), - }; - } - -} diff --git a/sveltejs/public/build/bundle.js b/sveltejs/public/build/bundle.js index 01d1f1bf8badee2edff3b1f1df82cd51a0e46bde..f88935469ce32d580ef7621bca8538d94951cb69 100644 GIT binary patch delta 4402 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 E10uMlzW@LL delta 7361 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 O(16HM-X0KxVgC#5Vt={- diff --git a/sveltejs/public/build/bundle.js.map b/sveltejs/public/build/bundle.js.map index 7ff6140054aa9bcb6584a688d5dd76ff2429c12b..549b45caa54dda0bdfd51ecebcc69fc89310bd5c 100644 GIT binary patch delta 5741 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 b*=oCd{~kpJUe|>=waR@gC?Bm=;@JNH*mvm6 delta 8888 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 Wwj?te=!1lO>Dy$Nwm)B5ME(a|JLMt( diff --git a/sveltejs/src/DetailModal.svelte b/sveltejs/src/DetailModal.svelte index aa1795d0b..8a80fe2cf 100644 --- a/sveltejs/src/DetailModal.svelte +++ b/sveltejs/src/DetailModal.svelte @@ -79,7 +79,7 @@ {Drupal.t('Not compatible with your Drupal installation')} </div> {/if} - {#if project.project_usage_total !== -1} + {#if typeof project.project_usage_total === 'number'} <div class="pb-detail-modal__sidebar_element"> <ProjectIcon type="usage" diff --git a/sveltejs/src/MultipleChoiceFilter.svelte b/sveltejs/src/MultipleChoiceFilter.svelte index 90a5bbe02..647c69288 100644 --- a/sveltejs/src/MultipleChoiceFilter.svelte +++ b/sveltejs/src/MultipleChoiceFilter.svelte @@ -4,10 +4,9 @@ moduleCategoryFilter, moduleCategoryVocabularies, activeTab, - sourceFilters, } from './stores'; import { normalizeOptions, shallowCompare } from './util'; - import { BASE_URL } from './constants'; + import { BASE_URL, FILTERS } from './constants'; const { Drupal } = window; const dispatch = createEventDispatcher(); @@ -86,13 +85,13 @@ return; } // Tab without shift moves to next filter. - const keys = Object.keys($sourceFilters); - const filterMap = Object.fromEntries(Object.entries($sourceFilters)); + const keys = Object.keys(FILTERS); + const filterMap = Object.fromEntries(Object.entries(FILTERS)); const indexOfCategories = keys.indexOf('categories'); if (indexOfCategories !== -1 && indexOfCategories + 1 < keys.length) { const nextKey = keys[indexOfCategories + 1]; - const nextElement = $sourceFilters[nextKey]; + const nextElement = FILTERS[nextKey]; const nextElementKey = Object.keys(filterMap).find( (key) => filterMap[key] === nextElement, ); diff --git a/sveltejs/src/Project/Project.svelte b/sveltejs/src/Project/Project.svelte index 6f45f8eae..f5550fd85 100644 --- a/sveltejs/src/Project/Project.svelte +++ b/sveltejs/src/Project/Project.svelte @@ -75,18 +75,16 @@ <ProjectIcon type="maintained" /> </span> {/if} - {#if toggleView === 'Grid' && project.project_usage_total !== -1} - {#if project.project_usage_total !== 0} - <div class="pb-project__install-count-container"> - <span class="pb-project__install-count"> - {Drupal.formatPlural( - project.project_usage_total, - `${numberFormatter.format(1)} install`, - `${numberFormatter.format(project.project_usage_total)} installs`, - )} - </span> - </div> - {/if} + {#if toggleView === 'Grid' && typeof project.project_usage_total === 'number' && project.project_usage_total > 0} + <div class="pb-project__install-count-container"> + <span class="pb-project__install-count"> + {Drupal.formatPlural( + project.project_usage_total, + `${numberFormatter.format(1)} install`, + `${numberFormatter.format(project.project_usage_total)} installs`, + )} + </span> + </div> {/if} {#if project.warnings && project.warnings.length > 0} {#each project.warnings as warning} @@ -96,12 +94,12 @@ </span> {/each} {/if} - {#if toggleView === 'List' && project.project_usage_total !== -1} + {#if toggleView === 'List' && typeof project.project_usage_total === 'number'} <div class="pb-project__project-usage-container"> <div class="pb-project__image pb-project__image--{displayMode}"> <ProjectIcon type="usage" variant="project-listing" /> </div> - {#if project.project_usage_total !== 0} + {#if project.project_usage_total > 0} <div class="pb-project__active-installs-text"> {Drupal.formatPlural( project.project_usage_total, diff --git a/sveltejs/src/ProjectBrowser.svelte b/sveltejs/src/ProjectBrowser.svelte index c7636e424..224135af9 100644 --- a/sveltejs/src/ProjectBrowser.svelte +++ b/sveltejs/src/ProjectBrowser.svelte @@ -24,13 +24,10 @@ } from './stores'; import MediaQuery from './MediaQuery.svelte'; import { - ACTIVELY_MAINTAINED_ID, - COVERED_ID, - ALL_VALUES_ID, DEFAULT_SOURCE_ID, BASE_URL, FULL_MODULE_PATH, - ACTIVE_PLUGINS, + ACTIVE_PLUGIN, PACKAGE_MANAGER, MAX_SELECTIONS, } from './constants'; @@ -100,14 +97,23 @@ // If no category filter is set, reset the tracking for the active tab. $categoryCheckedTrack[$activeTab] = []; } - if ($filters.developmentStatus && $filters.developmentStatus.length) { - searchParams.set('development_status', $filters.developmentStatus); + if ('developmentStatus' in $filters) { + searchParams.set( + 'development_status', + Number($filters.developmentStatus).toString(), + ); } - if ($filters.maintenanceStatus && $filters.maintenanceStatus.length) { - searchParams.set('maintenance_status', $filters.maintenanceStatus); + if ('maintenanceStatus' in $filters) { + searchParams.set( + 'maintenance_status', + Number($filters.maintenanceStatus).toString(), + ); } - if ($filters.securityCoverage && $filters.securityCoverage.length) { - searchParams.set('security_advisory_coverage', $filters.securityCoverage); + if ('securityCoverage' in $filters) { + searchParams.set( + 'security_advisory_coverage', + Number($filters.securityCoverage).toString(), + ); } if (Object.keys($categoryCheckedTrack).length !== 0) { searchParams.set( @@ -157,19 +163,6 @@ loading = false; } - async function filterRecommended() { - // Show recommended projects on initial page load only when no filters are applied. - if ( - $filters.developmentStatus.length === 0 && - $filters.maintenanceStatus.length === 0 && - $filters.securityCoverage.length === 0 - ) { - $filters.maintenanceStatus = ACTIVELY_MAINTAINED_ID; - $filters.securityCoverage = COVERED_ID; - $filters.developmentStatus = ALL_VALUES_ID; - } - } - /** * Load remote data when the Svelte component is mounted. */ @@ -191,8 +184,6 @@ projectId = null; } - await filterRecommended(); - await load($page); const focus = element ? document.getElementById(element) : false; if (focus) { @@ -236,11 +227,7 @@ await load(0); page.set(0); } - async function onAdvancedFilter(event) { - $filters.developmentStatus = event.detail.developmentStatus; - $filters.maintenanceStatus = event.detail.maintenanceStatus; - $filters.securityCoverage = event.detail.securityCoverage; - + async function onAdvancedFilter() { await load(0); page.set(0); } @@ -269,7 +256,7 @@ Drupal.t('@count Results for @active_tab, Sorted by @sortText', { '@count': numberFormatter.format($rowsCount), '@sortText': sortText, - '@active_tab': ACTIVE_PLUGINS[$activeTab], + '@active_tab': ACTIVE_PLUGIN, }), ); }, 210); diff --git a/sveltejs/src/Search/BooleanFilter.svelte b/sveltejs/src/Search/BooleanFilter.svelte index 5dd984b52..906cf43cc 100644 --- a/sveltejs/src/Search/BooleanFilter.svelte +++ b/sveltejs/src/Search/BooleanFilter.svelte @@ -16,7 +16,7 @@ bind:value={$filters[type]} on:change={changeHandler} > - <option value="1">{onLabel}</option> - <option value="0">{offLabel}</option> + <option value={true}>{onLabel}</option> + <option value={false}>{offLabel}</option> </select> </div> diff --git a/sveltejs/src/Search/FilterApplied.svelte b/sveltejs/src/Search/FilterApplied.svelte index 7524e1e8f..80295a470 100644 --- a/sveltejs/src/Search/FilterApplied.svelte +++ b/sveltejs/src/Search/FilterApplied.svelte @@ -1,27 +1,24 @@ <script> - import { ALL_VALUES_ID, FULL_MODULE_PATH } from '../constants'; + import { FULL_MODULE_PATH } from '../constants'; - export let id; export let label; export let clickHandler; const { Drupal } = window; </script> -{#if id !== ALL_VALUES_ID} - <p class="filter-applied"> - <span class="filter-applied__label">{label}</span> - <button - type="button" - on:click={clickHandler} - class="filter-applied__button-close" - > - {#if label} - <span class="visually-hidden" - >{Drupal.t('Remove @filter', { '@filter': label })}</span - > - {/if} - <img src="{FULL_MODULE_PATH}/images/chip-close-icon.svg" alt="" /> - </button> - </p> -{/if} +<p class="filter-applied"> + <span class="filter-applied__label">{label}</span> + <button + type="button" + on:click={clickHandler} + class="filter-applied__button-close" + > + {#if label} + <span class="visually-hidden" + >{Drupal.t('Remove @filter', { '@filter': label })}</span + > + {/if} + <img src="{FULL_MODULE_PATH}/images/chip-close-icon.svg" alt="" /> + </button> +</p> diff --git a/sveltejs/src/Search/Search.svelte b/sveltejs/src/Search/Search.svelte index e588b8c8a..d24d3fb2b 100644 --- a/sveltejs/src/Search/Search.svelte +++ b/sveltejs/src/Search/Search.svelte @@ -1,14 +1,11 @@ <script> - import { createEventDispatcher, getContext, onMount } from 'svelte'; + import { createEventDispatcher, getContext } from 'svelte'; import FilterApplied from './FilterApplied.svelte'; import BooleanFilter from './BooleanFilter.svelte'; - import { normalizeOptions, shallowCompare } from '../util'; import MultipleChoiceFilter from '../MultipleChoiceFilter.svelte'; import SearchSort from './SearchSort.svelte'; import { - sourceFilters, filters, - filtersVocabularies, moduleCategoryFilter, categoryCheckedTrack, moduleCategoryVocabularies, @@ -16,16 +13,7 @@ searchString, sortCriteria, } from '../stores'; - import { - COVERED_ID, - ACTIVELY_MAINTAINED_ID, - MAINTENANCE_OPTIONS, - DEVELOPMENT_OPTIONS, - SECURITY_OPTIONS, - ALL_VALUES_ID, - FULL_MODULE_PATH, - DARK_COLOR_SCHEME, - } from '../constants'; + import { FULL_MODULE_PATH, DARK_COLOR_SCHEME, FILTERS } from '../constants'; const { Drupal } = window; const dispatch = createEventDispatcher(); @@ -51,21 +39,6 @@ let sortText = sortMatch.text; let filterComponent; - const updateVocabularies = (vocabulary, value) => { - const normalizedValue = normalizeOptions(value); - const storedValue = JSON.parse(localStorage.getItem(`pb.${vocabulary}`)); - if (storedValue === null || !shallowCompare(normalizedValue, storedValue)) { - $filtersVocabularies[vocabulary] = normalizedValue; - localStorage.setItem(`pb.${vocabulary}`, JSON.stringify(normalizedValue)); - } - }; - - onMount(() => { - updateVocabularies('developmentStatus', DEVELOPMENT_OPTIONS); - updateVocabularies('maintenanceStatus', MAINTENANCE_OPTIONS); - updateVocabularies('securityCoverage', SECURITY_OPTIONS); - }); - export async function onSearch(event) { const state = stateContext.getState(); const detail = { @@ -79,7 +52,9 @@ rows: state.filteredRows, }; dispatch('search', detail); - filterComponent.setModuleCategoryVocabulary(); + if (filterComponent) { + filterComponent.setModuleCategoryVocabulary(); + } if (detail.preventDefault !== true) { if (detail.searchText.length === 0) { stateContext.setRows(state.rows); @@ -96,12 +71,19 @@ } const onAdvancedFilter = async (event) => { + if (event) { + const filterName = event.target.name; + + if (FILTERS[filterName]._type === 'boolean') { + $filters[filterName] = event.target.value === 'true'; + } else { + $filters[filterName] = event.target.value; + } + } + const state = stateContext.getState(); const detail = { originalEvent: event, - developmentStatus: $filters.developmentStatus, - maintenanceStatus: $filters.maintenanceStatus, - securityCoverage: $filters.securityCoverage, page: state.page, pageIndex: state.pageIndex, pageSize: state.pageSize, @@ -134,20 +116,30 @@ document.getElementById('pb-text').focus(); } + const filterDefinitions = Object.entries(FILTERS); + /** - * Actions performed when clicking filter resets such as "recommended" - * @param {string} maintenanceId - * ID of the selected maintenance status. - * @param {string} developmentId - * ID of the selected development status. - * @param {string} securityId - * ID of the selected security status. + * Resets the filters to the initial values provided by the source. + * + * @param {boolean} clear + * Whether to clear all filter values (i.e., not reset them to their defaults, + * but actually negate them all). */ - const filterResets = (maintenanceId, developmentId, securityId) => { - $filters.maintenanceStatus = maintenanceId; - $filters.developmentStatus = developmentId; - $filters.securityCoverage = securityId; - $filters = $filters; + const resetFilters = (clear) => { + $filters = {}; + filterDefinitions.forEach(([name, definition]) => { + let value; + if (clear) { + if (definition._type === 'boolean') { + value = false; + } else if (definition._type === 'multiple_choice') { + value = []; + } + } else { + value = definition.value; + } + $filters[name] = value; + }); $moduleCategoryFilter = []; $categoryCheckedTrack = {}; onAdvancedFilter(); @@ -209,10 +201,10 @@ </button> </div> </div> - {#if $sourceFilters.length !== 0} + {#if filterDefinitions.length !== 0} <div class="search__form-filters-container"> <div class="search__form-filters"> - {#each Object.entries($sourceFilters) as [filterType, filter]} + {#each filterDefinitions as [filterType, filter]} {#if filter._type === 'boolean'} <BooleanFilter name={filter.name} @@ -239,7 +231,6 @@ <div class="search__results-count"> {#each $moduleCategoryFilter as category} <FilterApplied - id={category} label={$moduleCategoryVocabularies[category]} clickHandler={() => { $moduleCategoryFilter.splice( @@ -252,26 +243,20 @@ /> {/each} - {#if $filters.securityCoverage !== ALL_VALUES_ID || $filters.maintenanceStatus !== ALL_VALUES_ID || $filters.developmentStatus !== ALL_VALUES_ID || $moduleCategoryFilter.length} + {#if $filters.securityCoverage || $filters.maintenanceStatus || $filters.developmentStatus || $moduleCategoryFilter.length} <button class="search__filter-button" type="button" - on:click|preventDefault={() => - filterResets(ALL_VALUES_ID, ALL_VALUES_ID, ALL_VALUES_ID)} + on:click|preventDefault={() => resetFilters(true)} > {Drupal.t('Clear filters')} </button> {/if} - {#if !($filters.maintenanceStatus === ACTIVELY_MAINTAINED_ID && $filters.securityCoverage === COVERED_ID && $filters.developmentStatus === ALL_VALUES_ID && $moduleCategoryFilter.length === 0)} + {#if !($filters.maintenanceStatus && $filters.securityCoverage && !$filters.developmentStatus && $moduleCategoryFilter.length === 0)} <button class="search__filter-button" type="button" - on:click|preventDefault={() => - filterResets( - ACTIVELY_MAINTAINED_ID, - ALL_VALUES_ID, - COVERED_ID, - )} + on:click|preventDefault={() => resetFilters()} > {Drupal.t('Recommended filters')} </button> diff --git a/sveltejs/src/constants.js b/sveltejs/src/constants.js index dd3ab86e9..bd2219ade 100644 --- a/sveltejs/src/constants.js +++ b/sveltejs/src/constants.js @@ -1,15 +1,4 @@ -export const MAINTENANCE_OPTIONS = - drupalSettings.project_browser.maintenance_options; -export const SECURITY_OPTIONS = drupalSettings.project_browser.security_options; -export const DEVELOPMENT_OPTIONS = - drupalSettings.project_browser.development_options; export const SORT_OPTIONS = drupalSettings.project_browser.sort_options; -export const ACTIVELY_MAINTAINED_ID = - drupalSettings.project_browser.special_ids.maintenance_status.id; -export const COVERED_ID = - drupalSettings.project_browser.special_ids.security_coverage.id; -export const ALL_VALUES_ID = - drupalSettings.project_browser.special_ids.all_values; export const DEFAULT_SOURCE_ID = drupalSettings.project_browser.default_plugin_id; export const BASE_URL = `${window.location.protocol}//${window.location.host}${drupalSettings.path.baseUrl + drupalSettings.path.pathPrefix}`; @@ -17,7 +6,7 @@ export const FULL_MODULE_PATH = `${BASE_URL}${drupalSettings.project_browser.mod export const DARK_COLOR_SCHEME = matchMedia('(forced-colors: active)').matches && matchMedia('(prefers-color-scheme: dark)').matches; -export const ACTIVE_PLUGINS = drupalSettings.project_browser.active_plugins; +export const ACTIVE_PLUGIN = drupalSettings.project_browser.active_plugin; export const PACKAGE_MANAGER = drupalSettings.project_browser.package_manager; export const FILTERS = drupalSettings.project_browser.filters || {}; export const MAX_SELECTIONS = drupalSettings.project_browser.max_selections; diff --git a/sveltejs/src/stores.js b/sveltejs/src/stores.js index 8ec141381..771967409 100644 --- a/sveltejs/src/stores.js +++ b/sveltejs/src/stores.js @@ -7,28 +7,16 @@ import { // Store the selected tab. const storedActiveTab = DEFAULT_SOURCE_ID; -let activeFilters = {}; -if (storedActiveTab in FILTERS) { - activeFilters = FILTERS[storedActiveTab]; -} -export const sourceFilters = writable(activeFilters); // Store for applied advanced filters. -const storedFilters = { - developmentStatus: '', - maintenanceStatus: '', - securityCoverage: '' -}; -export const filters = writable(storedFilters); +const defaultFilters = {}; +Object.entries(FILTERS).forEach(([name, definition]) => { + defaultFilters[name] = definition.value; +}); +export const filters = writable(defaultFilters); export const rowsCount = writable(0); -export const filtersVocabularies = writable({ - developmentStatus: JSON.parse(localStorage.getItem('pb.developmentStatus')) || [], - maintenanceStatus: JSON.parse(localStorage.getItem('pb.maintenanceStatus')) || [], - securityCoverage: JSON.parse(localStorage.getItem('pb.securityCoverage')) || [] -}); - // Store for applied category filters. const storedModuleCategoryFilter = []; export const moduleCategoryFilter = writable(storedModuleCategoryFilter); @@ -38,10 +26,6 @@ const storedModuleCategoryVocabularies = JSON.parse(localStorage.getItem('module export const moduleCategoryVocabularies = writable(storedModuleCategoryVocabularies); moduleCategoryVocabularies.subscribe((val) => localStorage.setItem('moduleCategoryVocabularies', JSON.stringify(val))); -// Store used to check if the page has loaded once already. -const storedIsFirstLoad = true; -export const isFirstLoad = writable(storedIsFirstLoad); - // Store the page the user is on. const storedPage = 0; export const page = writable(storedPage); @@ -49,7 +33,7 @@ export const page = writable(storedPage); export const activeTab = writable(storedActiveTab); // Store the current sort selected. -const storedSort = SORT_OPTIONS[storedActiveTab][0].id; +const storedSort = SORT_OPTIONS[0].id; export const sort = writable(storedSort); // Store tab-wise checked categories. @@ -65,7 +49,7 @@ const storedSearchString = ''; export const searchString = writable(storedSearchString); // Store for sort criteria. -const storedSortCriteria = SORT_OPTIONS[storedActiveTab]; +const storedSortCriteria = SORT_OPTIONS; export const sortCriteria = writable(storedSortCriteria); // Store the selected toggle view. 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 081570f20..bfd5fea19 100644 --- a/tests/modules/project_browser_test/src/Plugin/ProjectBrowserSource/ProjectBrowserTestMock.php +++ b/tests/modules/project_browser_test/src/Plugin/ProjectBrowserSource/ProjectBrowserTestMock.php @@ -9,14 +9,11 @@ use Drupal\Core\Database\Connection; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\State\StateInterface; use Drupal\Core\Url; -use Drupal\project_browser\DevelopmentStatus; -use Drupal\project_browser\MaintenanceStatus; use Drupal\project_browser\Plugin\ProjectBrowserSourceBase; use Drupal\project_browser\ProjectBrowser\Filter\BooleanFilter; use Drupal\project_browser\ProjectBrowser\Filter\MultipleChoiceFilter; use Drupal\project_browser\ProjectBrowser\Project; use Drupal\project_browser\ProjectBrowser\ProjectsResultsPage; -use Drupal\project_browser\SecurityStatus; use Psr\Log\LoggerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -218,23 +215,10 @@ class ProjectBrowserTestMock extends ProjectBrowserSourceBase { */ protected function convertMaintenance(array &$query) { if (!empty($query['maintenance_status'])) { - $options_available = MaintenanceStatus::asOptions(); - if (!in_array($query['maintenance_status'], array_keys($options_available))) { - unset($query['maintenance_status']); - } - else { - // Valid value. - switch ($query['maintenance_status']) { - case MaintenanceStatus::Maintained->value: - $query['maintenance_status'] = self::MAINTAINED_VALUES; - break; - - case MaintenanceStatus::All->value: - unset($query['maintenance_status']); - break; - - } - } + $query['maintenance_status'] = self::MAINTAINED_VALUES; + } + else { + unset($query['maintenance_status']); } } @@ -246,23 +230,10 @@ class ProjectBrowserTestMock extends ProjectBrowserSourceBase { */ protected function convertDevelopment(array &$query) { if (!empty($query['development_status'])) { - $options_available = DevelopmentStatus::asOptions(); - if (!in_array($query['development_status'], array_keys($options_available))) { - unset($query['development_status']); - } - else { - // Valid value. - switch ($query['development_status']) { - case DevelopmentStatus::Active->value: - $query['development_status'] = self::ACTIVE_VALUES; - break; - - case DevelopmentStatus::All->value: - unset($query['development_status']); - break; - - } - } + $query['development_status'] = self::ACTIVE_VALUES; + } + else { + unset($query['development_status']); } } @@ -274,28 +245,13 @@ class ProjectBrowserTestMock extends ProjectBrowserSourceBase { */ protected function convertSecurity(array &$query) { if (!empty($query['security_advisory_coverage'])) { - $options_available = SecurityStatus::asOptions(); - if (!in_array($query['security_advisory_coverage'], array_keys($options_available))) { - unset($query['security_advisory_coverage']); - } - else { - // Valid value. - switch ($query['security_advisory_coverage']) { - case SecurityStatus::Covered->value: - $query['security_advisory_coverage'] = self::COVERED_VALUES; - break; - - case SecurityStatus::All->value: - $keys = []; - $options = $this->getSecurityCoverages(); - foreach ($options as $option) { - $keys[] = $option['id']; - } - $query['security_advisory_coverage'] = $keys; - break; - - } - } + $query['security_advisory_coverage'] = self::COVERED_VALUES; + } + else { + $query['security_advisory_coverage'] = array_column( + $this->getSecurityCoverages(), + 'id' + ); } } @@ -367,7 +323,7 @@ class ProjectBrowserTestMock extends ProjectBrowserSourceBase { NULL, ); $filters['developmentStatus'] = new BooleanFilter( - TRUE, + FALSE, $this->t('Show projects under active development'), $this->t('Show all'), $this->t('Development status'), diff --git a/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php b/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php index 7b32c45e5..16225bf5f 100644 --- a/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php +++ b/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php @@ -57,8 +57,11 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase { $this->installState = $this->container->get(InstallState::class); - $this->config('project_browser.admin_settings')->set('enabled_sources', ['project_browser_test_mock'])->save(TRUE); - $this->config('project_browser.admin_settings')->set('allow_ui_install', TRUE)->save(); + $this->config('project_browser.admin_settings') + ->set('enabled_sources', ['project_browser_test_mock']) + ->set('allow_ui_install', TRUE) + ->set('max_selections', 1) + ->save(); $this->drupalLogin($this->drupalCreateUser([ 'administer modules', 'administer site configuration', diff --git a/tests/src/FunctionalJavascript/ProjectBrowserPluginTest.php b/tests/src/FunctionalJavascript/ProjectBrowserPluginTest.php index 013f583bf..252acbd10 100644 --- a/tests/src/FunctionalJavascript/ProjectBrowserPluginTest.php +++ b/tests/src/FunctionalJavascript/ProjectBrowserPluginTest.php @@ -107,13 +107,11 @@ class ProjectBrowserPluginTest extends WebDriverTestBase { $this->assertEquals('Show actively maintained projects', $this->getElementText(self::MAINTENANCE_OPTION_SELECTOR . self::OPTION_CHECKED)); $this->assertEquals('Show all', $this->getElementText(self::DEVELOPMENT_OPTION_SELECTOR . self::OPTION_CHECKED)); + $page = $this->getSession()->getPage(); // Clear the security covered filter. - $this->clickWithWait(self::SECURITY_OPTION_SELECTOR . self::OPTION_LAST_CHILD); - $this->assertEquals('Show all', $this->getElementText(self::SECURITY_OPTION_SELECTOR . self::OPTION_CHECKED)); - + $page->selectFieldOption('securityCoverage', 'Show all'); // Set the development status filter. - $this->clickWithWait(self::DEVELOPMENT_OPTION_SELECTOR . self::OPTION_FIRST_CHILD); - $this->assertEquals('Show projects under active development', $this->getElementText(self::DEVELOPMENT_OPTION_SELECTOR . self::OPTION_CHECKED)); + $page->selectFieldOption('developmentStatus', 'Show projects under active development'); // Clear all filters. $this->pressWithWait('Clear filters'); diff --git a/tests/src/FunctionalJavascript/ProjectBrowserUiTest.php b/tests/src/FunctionalJavascript/ProjectBrowserUiTest.php index 1cb47bf99..1d6d87faf 100644 --- a/tests/src/FunctionalJavascript/ProjectBrowserUiTest.php +++ b/tests/src/FunctionalJavascript/ProjectBrowserUiTest.php @@ -70,9 +70,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase { $this->getSession()->resizeWindow(1250, 1000); $this->drupalGet('admin/modules/browse/project_browser_test_mock'); $this->svelteInitHelper('css', '.pb-project.pb-project--grid'); - $assert_session->waitForElementVisible('css', '#pb-project-browser .pb-display__button[value="Grid"]'); - $grid_text = $this->getElementText('#project-browser .pb-display__button[value="Grid"]'); - $this->assertEquals('Grid', $grid_text); + $this->assertNotEmpty($assert_session->waitForButton('Grid')); $this->svelteInitHelper('text', '10 Results'); $assert_session->elementsCount('css', '#project-browser .pb-project.pb-project--grid', 10); $this->assertTrue($assert_session->waitForText('Results')); @@ -380,10 +378,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase { } // Click the Active filter. - $this->clickWithWait(self::DEVELOPMENT_OPTION_SELECTOR . self::OPTION_FIRST_CHILD); - - // Make sure the correct filter was applied. - $this->assertEquals('Show projects under active development', $this->getElementText(self::DEVELOPMENT_OPTION_SELECTOR . self::OPTION_CHECKED)); + $page->selectFieldOption('developmentStatus', 'Show projects under active development'); $this->assertProjectsVisible([ 'Jazz', diff --git a/tests/src/FunctionalJavascript/ProjectBrowserUiTestJsonApi.php b/tests/src/FunctionalJavascript/ProjectBrowserUiTestJsonApi.php index 5d65055a7..85a692f6b 100644 --- a/tests/src/FunctionalJavascript/ProjectBrowserUiTestJsonApi.php +++ b/tests/src/FunctionalJavascript/ProjectBrowserUiTestJsonApi.php @@ -228,9 +228,7 @@ class ProjectBrowserUiTestJsonApi extends WebDriverTestBase { $assert_session->pageTextNotContains(' 0 Results'); // Make sure the second filter applied is the security covered filter. - $option = $assert_session->optionExists('securityCoverage', '1'); - $this->assertSame('Show projects covered by a security policy', $option->getText()); - $this->assertTrue($option->isSelected()); + $this->assertTrue($assert_session->optionExists('securityCoverage', 'Show projects covered by a security policy')->isSelected()); // Clear the security covered filter. $this->clickWithWait(self::SECURITY_OPTION_SELECTOR . self::OPTION_LAST_CHILD); @@ -309,11 +307,9 @@ class ProjectBrowserUiTestJsonApi extends WebDriverTestBase { $this->pressWithWait('Recommended filters'); // Check that the actively maintained tag is present. - $maintenance_checked_option = $this->assertSession()->optionExists('maintenanceStatus', '1'); - $this->assertTrue($maintenance_checked_option->isSelected()); - $this->assertEquals('Show actively maintained projects', $maintenance_checked_option->getText()); + $this->assertTrue($assert_session->optionExists('maintenanceStatus', 'Show actively maintained projects')->isSelected()); // Make sure the second filter applied is the security covered filter. - $assert_session->fieldValueEquals('securityCoverage', '1'); + $this->assertTrue($assert_session->optionExists('securityCoverage', 'Show projects covered by a security policy')->isSelected()); $this->assertTrue($assert_session->waitForText(' Results')); $assert_session->pageTextNotContains(' 0 Results'); } diff --git a/tests/src/Kernel/RecipeActivatorTest.php b/tests/src/Kernel/RecipeActivatorTest.php index 74adbddce..c832f0d1c 100644 --- a/tests/src/Kernel/RecipeActivatorTest.php +++ b/tests/src/Kernel/RecipeActivatorTest.php @@ -49,8 +49,6 @@ class RecipeActivatorTest extends KernelTestBase { $project = new Project( logo: [], isCompatible: TRUE, - isCovered: TRUE, - projectUsageTotal: 0, machineName: 'My Project', body: [], title: '', -- GitLab