diff --git a/modules/project_browser_devel/src/Plugin/ProjectBrowserSource/RandomDataPlugin.php b/modules/project_browser_devel/src/Plugin/ProjectBrowserSource/RandomDataPlugin.php index 82fb3614876a706e573999c65fc9218da1beb45b..82088a988c39b2eb72a7c9b2dd602e06692b88da 100644 --- a/modules/project_browser_devel/src/Plugin/ProjectBrowserSource/RandomDataPlugin.php +++ b/modules/project_browser_devel/src/Plugin/ProjectBrowserSource/RandomDataPlugin.php @@ -5,6 +5,8 @@ namespace Drupal\project_browser_devel\Plugin\ProjectBrowserSource; use Drupal\Component\Utility\Random; use Drupal\Core\Cache\CacheBackendInterface; 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 Symfony\Component\DependencyInjection\ContainerInterface; @@ -114,6 +116,38 @@ class RandomDataPlugin extends ProjectBrowserSourceBase { return $categories; } + /** + * {@inheritdoc} + */ + public function getFilterDefinitions(): array { + $filters = []; + + $categories = $this->getCategories(); + $choices = array_combine( + array_column($categories, 'id'), + array_column($categories, 'name'), + ); + $filters['categories'] = new MultipleChoiceFilter($choices, [], $this->t('Categories'), NULL); + + $filters['maintained'] = new BooleanFilter( + TRUE, + $this->t('Only show actively maintained projects'), + NULL, + ); + $filters['security_advisories'] = new BooleanFilter( + TRUE, + $this->t('Only show projects covered by a security policy'), + NULL, + ); + $filters['developed'] = new BooleanFilter( + TRUE, + $this->t('Only show projects under active development'), + NULL, + ); + + return $filters; + } + /** * {@inheritdoc} */ diff --git a/src/Element/ProjectBrowser.php b/src/Element/ProjectBrowser.php index 2646cf1b050f19f6c8bf71599d3aad455e529502..78b822d8b6be395cbea991dab4a1612388122360 100644 --- a/src/Element/ProjectBrowser.php +++ b/src/Element/ProjectBrowser.php @@ -141,11 +141,11 @@ final class ProjectBrowser implements ElementInterface, ContainerFactoryPluginIn $active_plugins[$source->getPluginId()] = $source->getPluginDefinition()['label']; } - return [ - 'active_plugins' => $active_plugins, + $settings = [ + 'active_plugins' => [], 'module_path' => $this->moduleHandler->getModule('project_browser')->getPath(), - 'special_ids' => static::getSpecialIds(), - 'sort_options' => $sort_options, + 'special_ids' => $this->getSpecialIds(), + 'sort_options' => [], 'maintenance_options' => MaintenanceStatus::asOptions(), 'security_options' => SecurityStatus::asOptions(), 'development_options' => DevelopmentStatus::asOptions(), @@ -153,6 +153,13 @@ final class ProjectBrowser implements ElementInterface, ContainerFactoryPluginIn 'current_sources_keys' => $current_sources_keys, 'package_manager' => $package_manager, ]; + + foreach ($current_sources as $plugin_id => $source) { + $settings['sort_options'][$plugin_id] = array_values($source->getSortOptions()); + $settings['active_plugins'][$plugin_id] = $source->getPluginDefinition()['label']; + $settings['filters'][$plugin_id] = $source->getFilterDefinitions(); + } + return $settings; } /** diff --git a/src/Plugin/ProjectBrowserSource/DrupalDotOrgJsonApi.php b/src/Plugin/ProjectBrowserSource/DrupalDotOrgJsonApi.php index 527329d4db0957f26604ee350a4a39117aef7414..2865b4bb85493e885308b72d9d04385b6424702a 100644 --- a/src/Plugin/ProjectBrowserSource/DrupalDotOrgJsonApi.php +++ b/src/Plugin/ProjectBrowserSource/DrupalDotOrgJsonApi.php @@ -9,6 +9,8 @@ use Drupal\Core\StringTranslation\StringTranslationTrait; 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; @@ -248,37 +250,35 @@ class DrupalDotOrgJsonApi extends ProjectBrowserSourceBase { } /** - * Get a list of values for the "Development Status" vocabulary. - * - * @return array[] - * List of terms (id and name) for the vocabulary. + * {@inheritdoc} */ - protected function getDevelopmentStatuses(): array { - return $this->getVocabularyData('development_status'); - } + public function getFilterDefinitions(): array { + $filters = []; - /** - * Get a list of values for the "Maintenance Status" vocabulary. - * - * @return array[] - * List of terms (id and name) for the vocabulary. - */ - protected function getMaintenanceStatuses(): array { - return $this->getVocabularyData('maintenance_status'); - } + $categories = $this->getCategories(); + $choices = array_combine( + array_column($categories, 'id'), + array_column($categories, 'name'), + ); + $filters['categories'] = new MultipleChoiceFilter($choices, [], $this->t('Categories'), NULL); - /** - * Get a list of values for the "Security Coverage" vocabulary. - * - * @return array[] - * List of terms (id and name) for the vocabulary. - */ - protected function getSecurityCoverages(): array { - // There is an additional 'revoked' value, but we do NOT want those modules. - return [ - ['id' => 'covered', 'name' => $this->t('Covered')], - ['id' => 'not-covered', 'name' => $this->t('Not covered')], - ]; + $filters['maintained'] = new BooleanFilter( + TRUE, + $this->t('Only show actively maintained projects'), + NULL, + ); + $filters['security_advisories'] = new BooleanFilter( + TRUE, + $this->t('Only show projects covered by a security policy'), + NULL, + ); + $filters['developed'] = new BooleanFilter( + TRUE, + $this->t('Only show projects under active development'), + NULL, + ); + + return $filters; } /** diff --git a/src/Plugin/ProjectBrowserSourceBase.php b/src/Plugin/ProjectBrowserSourceBase.php index 6d41c41b35650cefd165310b241aa7475c419c88..d544e7cf288f86c08562583e2d21fd134553edb6 100644 --- a/src/Plugin/ProjectBrowserSourceBase.php +++ b/src/Plugin/ProjectBrowserSourceBase.php @@ -5,6 +5,7 @@ namespace Drupal\project_browser\Plugin; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; 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; @@ -30,6 +31,23 @@ abstract class ProjectBrowserSourceBase extends PluginBase implements ProjectBro ); } + /** + * {@inheritdoc} + */ + public function getFilterDefinitions(): array { + $filters = []; + + $categories = $this->getCategories(); + if ($categories) { + $choices = array_combine( + array_column($categories, 'id'), + array_column($categories, 'name'), + ); + $filters['categories'] = new MultipleChoiceFilter($choices, [], $this->t('Categories'), NULL); + } + return $filters; + } + /** * Returns the available sort options that plugins will parse. * diff --git a/src/Plugin/ProjectBrowserSourceInterface.php b/src/Plugin/ProjectBrowserSourceInterface.php index c9cb3766afda7a85a7d1158cf290adb0e8d2f980..87cb55bac99e73f1ad282ef145ba54ed91b305a7 100644 --- a/src/Plugin/ProjectBrowserSourceInterface.php +++ b/src/Plugin/ProjectBrowserSourceInterface.php @@ -44,6 +44,15 @@ interface ProjectBrowserSourceInterface { */ public function getCategories(): array; + /** + * Defines the filters that this source will respect. + * + * @return \Drupal\project_browser\ProjectBrowser\Filter\FilterBase[] + * The filters that this source will respect when querying for projects, + * keyed by machine name. + */ + public function getFilterDefinitions(): array; + /** * Returns the available sort options that plugins will parse. * diff --git a/src/ProjectBrowser/Filter/BooleanFilter.php b/src/ProjectBrowser/Filter/BooleanFilter.php new file mode 100644 index 0000000000000000000000000000000000000000..88738ef10bc7b5c78ea826e53780e1b49cf8dfab --- /dev/null +++ b/src/ProjectBrowser/Filter/BooleanFilter.php @@ -0,0 +1,16 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\project_browser\ProjectBrowser\Filter; + +/** + * Defines a filter that can either be on, or off. + */ +final class BooleanFilter extends FilterBase { + + public function __construct(public bool $value, mixed ...$arguments) { + parent::__construct(...$arguments); + } + +} diff --git a/src/ProjectBrowser/Filter/FilterBase.php b/src/ProjectBrowser/Filter/FilterBase.php new file mode 100644 index 0000000000000000000000000000000000000000..a48129bdc272c6aa6dfe58246d67b3dddde9bee9 --- /dev/null +++ b/src/ProjectBrowser/Filter/FilterBase.php @@ -0,0 +1,29 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\project_browser\ProjectBrowser\Filter; + +/** + * A base class for all filters that can be defined by source plugins. + */ +abstract class FilterBase implements \JsonSerializable { + + public function __construct( + public readonly string|\Stringable $name, + public readonly string|\Stringable|null $group, + ) {} + + /** + * {@inheritdoc} + */ + final public function jsonSerialize(): array { + return [ + '_type' => match (static::class) { + BooleanFilter::class => 'boolean', + MultipleChoiceFilter::class => 'multiple_choice', + }, + ] + get_object_vars($this); + } + +} diff --git a/src/ProjectBrowser/Filter/MultipleChoiceFilter.php b/src/ProjectBrowser/Filter/MultipleChoiceFilter.php new file mode 100644 index 0000000000000000000000000000000000000000..50c01e9397c33ca8f88d0f90182a46277fcb8da4 --- /dev/null +++ b/src/ProjectBrowser/Filter/MultipleChoiceFilter.php @@ -0,0 +1,23 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\project_browser\ProjectBrowser\Filter; + +/** + * Defines a filter to choose any number of options from a list. + */ +final class MultipleChoiceFilter extends FilterBase { + + public function __construct( + public readonly array $choices, + public readonly array $value, + mixed ...$arguments, + ) { + // Everything $value should be present in $choices. + assert(array_diff($value, array_keys($choices)) === []); + + parent::__construct(...$arguments); + } + +} diff --git a/sveltejs/public/build/bundle.js b/sveltejs/public/build/bundle.js index 716ae1595f609b2bce6ade934e8a4e93a1e58c92..72b2e9d392a7047b50fb6ce67a8300ce721d3002 100644 Binary files a/sveltejs/public/build/bundle.js and b/sveltejs/public/build/bundle.js differ diff --git a/sveltejs/public/build/bundle.js.map b/sveltejs/public/build/bundle.js.map index 46a61313dd45984c8ad3945b14051e91c978c23a..c73c1212d862fbebbe51c2a4f74655ab390e8e39 100644 Binary files a/sveltejs/public/build/bundle.js.map and b/sveltejs/public/build/bundle.js.map differ diff --git a/sveltejs/src/ProjectBrowser.svelte b/sveltejs/src/ProjectBrowser.svelte index 648f03c4136392d4c2915ae273d6cb2b2dd550e1..ced684c64919976d35ebcf2aeea1ba032c4d5f09 100644 --- a/sveltejs/src/ProjectBrowser.svelte +++ b/sveltejs/src/ProjectBrowser.svelte @@ -7,6 +7,7 @@ import Tabs from './Tabs.svelte'; import { numberFormatter } from './util'; import { + sourceFilters, filters, rowsCount, moduleCategoryFilter, @@ -23,6 +24,7 @@ } from './stores'; import MediaQuery from './MediaQuery.svelte'; import { + FILTERS, ACTIVELY_MAINTAINED_ID, COVERED_ID, ALL_VALUES_ID, @@ -250,6 +252,9 @@ $categoryCheckedTrack[$activeTab] = $moduleCategoryFilter; $moduleCategoryFilter = []; $activeTab = event.detail.pluginId; + if ($activeTab in FILTERS) { + $sourceFilters = FILTERS[$activeTab]; + } $moduleCategoryFilter = typeof $categoryCheckedTrack[$activeTab] !== 'undefined' ? $categoryCheckedTrack[$activeTab] @@ -260,7 +265,6 @@ if (typeof sortMatch === 'undefined') { $sort = $sortCriteria[0].id; } - // Move to page 0 when switching sources as there's no guarantee the new // source has enough results to reach whatever the current page is. page.set(0); diff --git a/sveltejs/src/Search/Search.svelte b/sveltejs/src/Search/Search.svelte index 9f846b1c26f7d076c6a1143a578319791438fd3a..227bd434eefe6baef8cc5ab0b136d5b1b4b956e2 100644 --- a/sveltejs/src/Search/Search.svelte +++ b/sveltejs/src/Search/Search.svelte @@ -6,6 +6,7 @@ import { Filter } from '../ProjectGrid.svelte'; import SearchSort from './SearchSort.svelte'; import { + sourceFilters, filters, filtersVocabularies, moduleCategoryFilter, @@ -207,96 +208,113 @@ /> </div> </div> - <div class="search__form-filters-container"> - <div class="search__form-filters"> - <Filter - on:selectCategory={onSelectCategory} - bind:this={filterComponent} - /> - <FilterGroup - filterTitle={Drupal.t('Security Advisory Coverage')} - filterData={SECURITY_OPTIONS} - filterType="securityCoverage" - changeHandler={onAdvancedFilter} - let:id - let:label - /> - <FilterGroup - filterTitle={Drupal.t('Maintenance Status')} - filterData={MAINTENANCE_OPTIONS} - filterType="maintenanceStatus" - changeHandler={onAdvancedFilter} - let:id - let:label - > - <label - slot="label" - class="search__checkbox-label" - for={`maintenanceStatus${id}`} - > - {label} - </label> - </FilterGroup> - <FilterGroup - filterTitle={Drupal.t('Development Status')} - filterData={DEVELOPMENT_OPTIONS} - filterType="developmentStatus" - changeHandler={onAdvancedFilter} - let:id - let:label + {#if $sourceFilters.length !== 0} + <div class="search__form-filters-container"> + <div class="search__form-filters"> + {#if 'categories' in $sourceFilters} + <Filter + on:selectCategory={onSelectCategory} + bind:this={filterComponent} + /> + {/if} + {#if 'security_advisories' in $sourceFilters} + <FilterGroup + filterTitle={Drupal.t('Security Advisory Coverage')} + filterData={SECURITY_OPTIONS} + filterType="securityCoverage" + changeHandler={onAdvancedFilter} + let:id + let:label + /> + {/if} + {#if 'maintained' in $sourceFilters} + <FilterGroup + filterTitle={Drupal.t('Maintenance Status')} + filterData={MAINTENANCE_OPTIONS} + filterType="maintenanceStatus" + changeHandler={onAdvancedFilter} + let:id + let:label + > + <label + slot="label" + class="search__checkbox-label" + for={`maintenanceStatus${id}`} + > + {label} + </label> + </FilterGroup> + {/if} + {#if 'developed' in $sourceFilters} + <FilterGroup + filterTitle={Drupal.t('Development Status')} + filterData={DEVELOPMENT_OPTIONS} + filterType="developmentStatus" + changeHandler={onAdvancedFilter} + let:id + let:label + > + <label + slot="label" + class="search__checkbox-label" + for={`developmentStatus${id}`} + > + {label} + </label> + </FilterGroup> + {/if} + </div> + <div + class="search__form-sort js-form-item js-form-type-select form-type--select js-form-item-type form-item--type" > - <label - slot="label" - class="search__checkbox-label" - for={`developmentStatus${id}`} + <section + class="search__filters" + aria-label={Drupal.t('Search results')} > - {label} - </label> - </FilterGroup> - </div> - <div - class="search__form-sort js-form-item js-form-type-select form-type--select js-form-item-type form-item--type" - > - <section class="search__filters" aria-label={Drupal.t('Search results')}> - <div class="search__results-count"> - {#each $moduleCategoryFilter as category} - <FilterApplied - id={category} - label={$moduleCategoryVocabularies[category]} - clickHandler={() => { - $moduleCategoryFilter.splice( - $moduleCategoryFilter.indexOf(category), - 1, - ); - $moduleCategoryFilter = $moduleCategoryFilter; - onSelectCategory(); - }} - /> - {/each} + <div class="search__results-count"> + {#each $moduleCategoryFilter as category} + <FilterApplied + id={category} + label={$moduleCategoryVocabularies[category]} + clickHandler={() => { + $moduleCategoryFilter.splice( + $moduleCategoryFilter.indexOf(category), + 1, + ); + $moduleCategoryFilter = $moduleCategoryFilter; + onSelectCategory(); + }} + /> + {/each} - {#if $filters.securityCoverage !== ALL_VALUES_ID || $filters.maintenanceStatus !== ALL_VALUES_ID || $filters.developmentStatus !== ALL_VALUES_ID || $moduleCategoryFilter.length} - <button - class="search__filter-button" - type="button" - on:click|preventDefault={() => - filterResets(ALL_VALUES_ID, ALL_VALUES_ID, ALL_VALUES_ID)} - > - {Drupal.t('Clear filters')} - </button> - {/if} - {#if !($filters.maintenanceStatus === ACTIVELY_MAINTAINED_ID && $filters.securityCoverage === COVERED_ID && $filters.developmentStatus === ALL_VALUES_ID && $moduleCategoryFilter.length === 0)} - <button - class="search__filter-button" - type="button" - on:click|preventDefault={() => - filterResets(ACTIVELY_MAINTAINED_ID, ALL_VALUES_ID, COVERED_ID)} - > - {Drupal.t('Recommended filters')} - </button> - {/if} - </div> - </section> - <SearchSort on:sort bind:sortText refresh={refreshLiveRegion} /> + {#if $filters.securityCoverage !== ALL_VALUES_ID || $filters.maintenanceStatus !== ALL_VALUES_ID || $filters.developmentStatus !== ALL_VALUES_ID || $moduleCategoryFilter.length} + <button + class="search__filter-button" + type="button" + on:click|preventDefault={() => + filterResets(ALL_VALUES_ID, ALL_VALUES_ID, ALL_VALUES_ID)} + > + {Drupal.t('Clear filters')} + </button> + {/if} + {#if !($filters.maintenanceStatus === ACTIVELY_MAINTAINED_ID && $filters.securityCoverage === COVERED_ID && $filters.developmentStatus === ALL_VALUES_ID && $moduleCategoryFilter.length === 0)} + <button + class="search__filter-button" + type="button" + on:click|preventDefault={() => + filterResets( + ACTIVELY_MAINTAINED_ID, + ALL_VALUES_ID, + COVERED_ID, + )} + > + {Drupal.t('Recommended filters')} + </button> + {/if} + </div> + </section> + <SearchSort on:sort bind:sortText refresh={refreshLiveRegion} /> + </div> </div> - </div> + {/if} </form> diff --git a/sveltejs/src/constants.js b/sveltejs/src/constants.js index c70fba462c5f55e49efaf2baeb8f9759d9e041a3..3f5ede0781e97954a5f416d314973b0edf084f2b 100644 --- a/sveltejs/src/constants.js +++ b/sveltejs/src/constants.js @@ -21,3 +21,4 @@ export const DARK_COLOR_SCHEME = matchMedia('(prefers-color-scheme: dark)').matches; export const ACTIVE_PLUGINS = drupalSettings.project_browser.active_plugins; export const PACKAGE_MANAGER = drupalSettings.project_browser.package_manager; +export const FILTERS = drupalSettings.project_browser.filters || {}; diff --git a/sveltejs/src/stores.js b/sveltejs/src/stores.js index 46858cd0195f440367740bfd1ac9c27dc0aecf6f..d02dc0f539ffe894d456d447af4d29ff50d318ed 100644 --- a/sveltejs/src/stores.js +++ b/sveltejs/src/stores.js @@ -2,9 +2,21 @@ import { writable } from 'svelte/store'; import { - DEFAULT_SOURCE_ID, SORT_OPTIONS, + DEFAULT_SOURCE_ID, SORT_OPTIONS, FILTERS, } from './constants'; +// Store the selected tab. +const storedActiveTab = JSON.parse(sessionStorage.getItem('activeTab')) || DEFAULT_SOURCE_ID; +let activeFilters = {}; +if (sessionStorage.getItem('sourceFilters')){ + activeFilters = JSON.parse(sessionStorage.getItem('sourceFilters')); +} +else if (storedActiveTab in FILTERS) { + activeFilters = FILTERS[storedActiveTab]; +} +export const sourceFilters = writable(activeFilters); +sourceFilters.subscribe((val) => sessionStorage.setItem('sourceFilters', JSON.stringify(val))); + // Store for applied advanced filters. const storedFilters = JSON.parse(sessionStorage.getItem('advancedFilter')) || { developmentStatus: '', @@ -42,8 +54,6 @@ const storedPage = JSON.parse(sessionStorage.getItem('page')) || 0; export const page = writable(storedPage); page.subscribe((val) => sessionStorage.setItem('page', JSON.stringify(val))); -// Store the selected tab. -const storedActiveTab = JSON.parse(sessionStorage.getItem('activeTab')) || DEFAULT_SOURCE_ID; export const activeTab = writable(storedActiveTab); activeTab.subscribe((val) => sessionStorage.setItem('activeTab', JSON.stringify(val))); 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 bdf1b6716348b86b1c6276b3c13f4dabe77beebb..712022a82740e3b62a7f52fb51ced20c390d4dba 100644 --- a/tests/modules/project_browser_test/src/Plugin/ProjectBrowserSource/ProjectBrowserTestMock.php +++ b/tests/modules/project_browser_test/src/Plugin/ProjectBrowserSource/ProjectBrowserTestMock.php @@ -11,6 +11,8 @@ use Drupal\Core\State\StateInterface; 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; @@ -333,6 +335,48 @@ class ProjectBrowserTestMock extends ProjectBrowserSourceBase { return $categories; } + /** + * {@inheritdoc} + */ + public function getFilterDefinitions(): array { + $filters = []; + + $categories = $this->getCategories(); + $choices = array_combine( + array_column($categories, 'id'), + array_column($categories, 'name'), + ); + $filters['categories'] = new MultipleChoiceFilter($choices, [], $this->t('Categories'), NULL); + + $filters['maintained'] = new BooleanFilter( + TRUE, + $this->t('Only show actively maintained projects'), + NULL, + ); + $filters['security_advisories'] = new BooleanFilter( + TRUE, + $this->t('Only show projects covered by a security policy'), + NULL, + ); + $filters['developed'] = new BooleanFilter( + TRUE, + $this->t('Only show projects under active development'), + NULL, + ); + + $filters_to_define = $this->state->get('filters_to_define'); + if ($filters_to_define !== NULL) { + // Only keep those filters which needs to be defined according to + // $filters_to_define. + foreach ($filters as $filter_key => $filter_value) { + if (!in_array($filter_key, $filters_to_define, TRUE)) { + unset($filters[$filter_key]); + } + } + } + return $filters; + } + /** * {@inheritdoc} */ diff --git a/tests/src/FunctionalJavascript/ProjectBrowserUiTestJsonApi.php b/tests/src/FunctionalJavascript/ProjectBrowserUiTestJsonApi.php index 97371ef5570e97b13a80509d77438149e923954d..04b2cc4228111401de9ed7c394670b74f1bb7c53 100644 --- a/tests/src/FunctionalJavascript/ProjectBrowserUiTestJsonApi.php +++ b/tests/src/FunctionalJavascript/ProjectBrowserUiTestJsonApi.php @@ -446,6 +446,50 @@ class ProjectBrowserUiTestJsonApi extends WebDriverTestBase { $this->assertEquals($results_before, $results_after); } + /** + * Tests filters are displayed if they are defined by source. + */ + public function testFiltersShownIfDefinedBySource(): void { + if (version_compare(\Drupal::VERSION, '10.3', '<')) { + $this->markTestSkipped('This test requires Drupal 10.3 or later.'); + } + $assert_session = $this->assertSession(); + $page = $this->getSession()->getPage(); + // Enable module for extra source plugin. + $this->container->get('module_installer')->install(['project_browser_devel']); + $this->config('project_browser.admin_settings') + ->set('enabled_sources', ['recipes', 'project_browser_test_mock']) + ->save(); + + $this->drupalGet('admin/modules/browse'); + $this->assertTrue($assert_session->waitForText('Recipes')); + $page->pressButton('Recipes'); + // Recipes doesn't define any filters so no filters are displayed. + $this->assertNull($assert_session->waitForElementVisible('css', '.search__form-filters-container')); + + // Set the names of filters which will be defined by the test mock. + // @see \Drupal\project_browser_test\Plugin\ProjectBrowserSource\ProjectBrowserTestMock::getFilterDefinitions() + $filters_to_define = ['maintained', 'security_advisories']; + \Drupal::state()->set('filters_to_define', $filters_to_define); + + $this->drupalGet('admin/modules/browse'); + $this->assertTrue($assert_session->waitForText('Project Browser Mock Plugin')); + $page->pressButton('Project Browser Mock Plugin'); + // Drupal.org test mock defines only two filters (actively maintained filter + // and security coverage filter). + $assert_session->waitForElementVisible('css', '.search__form-filters-container'); + $this->assertTrue($assert_session->waitForText('Maintenance Status')); + $assert_session->waitForElementVisible('css', self::MAINTENANCE_OPTION_SELECTOR); + $this->assertTrue($assert_session->waitForText('Security Advisory Coverage')); + $assert_session->waitForElementVisible('css', self::SECURITY_OPTION_SELECTOR); + // Make sure no other filters are displayed. + $this->assertFalse($assert_session->waitForText('Development Status')); + $this->assertNull($assert_session->waitForElementVisible('css', self::DEVELOPMENT_OPTION_SELECTOR)); + $this->assertFalse($assert_session->waitForText('Filter by category')); + // Make sure category filter element is not visible. + $this->assertNull($assert_session->waitForElementVisible('css', 'div.search__form-filters-container > div.search__form-filters > section > fieldset > div')); + } + /** * Tests the view mode toggle keeps its state. */