From 88b8523fda5fa4e09bcd3a62fe9b73e4136401cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Tue, 18 Feb 2025 10:17:53 -0500 Subject: [PATCH 01/12] Use xxh64 for top-level query cache --- project_browser.services.yml | 2 +- src/EnabledSourceHandler.php | 40 ++++++++++++++++++++++++++++-------- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/project_browser.services.yml b/project_browser.services.yml index 4e2e42215..3b18bdd9c 100644 --- a/project_browser.services.yml +++ b/project_browser.services.yml @@ -9,7 +9,7 @@ services: parent: default_plugin_manager Drupal\project_browser\EnabledSourceHandler: arguments: - $keyValueFactory: '@keyvalue' + $keyValueFactory: '@keyvalue.expirable' calls: - [setLogger, ['@logger.channel.project_browser']] tags: diff --git a/src/EnabledSourceHandler.php b/src/EnabledSourceHandler.php index 1f3ad4119..8b83e108b 100644 --- a/src/EnabledSourceHandler.php +++ b/src/EnabledSourceHandler.php @@ -2,12 +2,13 @@ namespace Drupal\project_browser; +use Composer\InstalledVersions; use Drupal\Component\Serialization\Json; use Drupal\Core\Config\ConfigCrudEvent; use Drupal\Core\Config\ConfigEvents; use Drupal\Core\Config\ConfigFactoryInterface; -use Drupal\Core\KeyValueStore\KeyValueFactoryInterface; -use Drupal\Core\KeyValueStore\KeyValueStoreInterface; +use Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface; +use Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface; use Drupal\project_browser\Plugin\ProjectBrowserSourceManager; use Drupal\project_browser\ProjectBrowser\Project; use Drupal\project_browser\ProjectBrowser\ProjectsResultsPage; @@ -25,7 +26,7 @@ class EnabledSourceHandler implements LoggerAwareInterface, EventSubscriberInter public function __construct( private readonly ConfigFactoryInterface $configFactory, private readonly ProjectBrowserSourceManager $pluginManager, - private readonly KeyValueFactoryInterface $keyValueFactory, + private readonly KeyValueExpirableFactoryInterface $keyValueFactory, ) {} /** @@ -34,10 +35,10 @@ class EnabledSourceHandler implements LoggerAwareInterface, EventSubscriberInter * @param string $source_id * The ID of a source plugin. * - * @return \Drupal\Core\KeyValueStore\KeyValueStoreInterface + * @return \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface * A key-value store for the specified source plugin. */ - private function keyValue(string $source_id): KeyValueStoreInterface { + private function keyValue(string $source_id): KeyValueStoreExpirableInterface { return $this->keyValueFactory->get("project_browser:$source_id"); } @@ -109,7 +110,7 @@ class EnabledSourceHandler implements LoggerAwareInterface, EventSubscriberInter */ public function getProjects(string $source_id, array $query = []): ProjectsResultsPage { // Cache only exact query, down to the page number. - $cache_key = 'query:' . md5(Json::encode($query)); + $cache_key = $this->getQueryCacheKey($query); $storage = $this->keyValue($source_id); @@ -128,19 +129,40 @@ class EnabledSourceHandler implements LoggerAwareInterface, EventSubscriberInter $storage->setIfNotExists($project->id, $project); } // If there were no query errors, store the results as a set of arguments - // to ProjectsResultsPage. + // to ProjectsResultsPage. These cached results automatically expire daily. if (empty($results->error)) { - $storage->set($cache_key, [ + $storage->setWithExpire($cache_key, [ $results->totalResults, array_column($results->list, 'id'), $results->pluginLabel, $source_id, $results->error, - ]); + ], 86400); } return $results; } + /** + * Generates a cache key for a specific query. + * + * @param array $query + * The query. + * + * @return string + * A cache key for the given query. + */ + private function getQueryCacheKey(array $query): string { + // Include a quick hash of the top-level `composer.lock` file in the hash, + // so that sources which base their queries on the state of the local site + // will be refreshed when the local site changes. + ['install_path' => $project_root] = InstalledVersions::getRootPackage(); + $lock_file = $project_root . DIRECTORY_SEPARATOR . 'composer.lock'; + $lock_file_hash = file_exists($lock_file) + ? hash_file('xxh64', $lock_file) + : ''; + return md5(Json::encode($query) . $lock_file_hash); + } + /** * Queries the specified source. * -- GitLab From a1bd72dde4eab7c53499510a0c6fd95f3ec07f20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Tue, 18 Feb 2025 10:31:54 -0500 Subject: [PATCH 02/12] Fix EnabledSourceHandlerTest --- src/EnabledSourceHandler.php | 2 +- tests/src/Functional/EnabledSourceHandlerTest.php | 15 +++++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/EnabledSourceHandler.php b/src/EnabledSourceHandler.php index 8b83e108b..e4b70c6aa 100644 --- a/src/EnabledSourceHandler.php +++ b/src/EnabledSourceHandler.php @@ -160,7 +160,7 @@ class EnabledSourceHandler implements LoggerAwareInterface, EventSubscriberInter $lock_file_hash = file_exists($lock_file) ? hash_file('xxh64', $lock_file) : ''; - return md5(Json::encode($query) . $lock_file_hash); + return 'query:' . md5(Json::encode($query) . $lock_file_hash); } /** diff --git a/tests/src/Functional/EnabledSourceHandlerTest.php b/tests/src/Functional/EnabledSourceHandlerTest.php index f0b9ce8b4..5dac6b3e8 100644 --- a/tests/src/Functional/EnabledSourceHandlerTest.php +++ b/tests/src/Functional/EnabledSourceHandlerTest.php @@ -66,20 +66,23 @@ class EnabledSourceHandlerTest extends BrowserTestBase { /** @var \Drupal\project_browser\EnabledSourceHandler $handler */ $handler = $this->container->get(EnabledSourceHandler::class); - $handler->getProjects('project_browser_test_mock'); /** @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface $storage */ - $storage = $this->container->get('keyvalue') + $storage = $this->container->get('keyvalue.expirable') ->get('project_browser:project_browser_test_mock'); + $get_query_cache_keys = function () use ($storage): array { + return preg_grep('/^query:[0-9a-f]+$/', array_keys($storage->getAll())); + }; + + $handler->getProjects('project_browser_test_mock'); // Query results should have been stored. - $query_cache_key = 'query:' . md5('[]'); - $this->assertTrue($storage->has($query_cache_key)); + $this->assertNotEmpty($get_query_cache_keys()); $handler->clearStorage(); ProjectBrowserTestMock::$resultsError = 'Nope!'; $handler->getProjects('project_browser_test_mock'); - // No query results should have been stored. - $this->assertFalse($storage->has($query_cache_key)); + // No new query results should have been stored. + $this->assertEmpty($get_query_cache_keys()); } /** -- GitLab From 3ce0195807d4205e8d980fe6257f4cd71633c11e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Tue, 18 Feb 2025 10:43:10 -0500 Subject: [PATCH 03/12] Refactor to shut PHPStan the HELL up --- tests/src/Functional/EnabledSourceHandlerTest.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/src/Functional/EnabledSourceHandlerTest.php b/tests/src/Functional/EnabledSourceHandlerTest.php index 5dac6b3e8..3207ab8cb 100644 --- a/tests/src/Functional/EnabledSourceHandlerTest.php +++ b/tests/src/Functional/EnabledSourceHandlerTest.php @@ -66,23 +66,23 @@ class EnabledSourceHandlerTest extends BrowserTestBase { /** @var \Drupal\project_browser\EnabledSourceHandler $handler */ $handler = $this->container->get(EnabledSourceHandler::class); - /** @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface $storage */ - $storage = $this->container->get('keyvalue.expirable') - ->get('project_browser:project_browser_test_mock'); - $get_query_cache_keys = function () use ($storage): array { - return preg_grep('/^query:[0-9a-f]+$/', array_keys($storage->getAll())); + $has_cached_queries = function (): bool { + /** @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface $storage */ + $storage = $this->container->get('keyvalue.expirable') + ->get('project_browser:project_browser_test_mock'); + return (bool) preg_grep('/^query:[0-9a-f]+$/', array_keys($storage->getAll())); }; $handler->getProjects('project_browser_test_mock'); // Query results should have been stored. - $this->assertNotEmpty($get_query_cache_keys()); + $this->assertTrue($has_cached_queries()); $handler->clearStorage(); ProjectBrowserTestMock::$resultsError = 'Nope!'; $handler->getProjects('project_browser_test_mock'); // No new query results should have been stored. - $this->assertEmpty($get_query_cache_keys()); + $this->assertFalse($has_cached_queries()); } /** -- GitLab From 47563e0aa9cbf84cf44e9767f288bc351d830d65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Tue, 18 Feb 2025 10:46:50 -0500 Subject: [PATCH 04/12] Fix ClearStorageTest --- tests/src/Functional/ClearStorageTest.php | 29 +++++++++++++---------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/tests/src/Functional/ClearStorageTest.php b/tests/src/Functional/ClearStorageTest.php index d0be6f3f6..98a452504 100644 --- a/tests/src/Functional/ClearStorageTest.php +++ b/tests/src/Functional/ClearStorageTest.php @@ -28,13 +28,6 @@ class ClearStorageTest extends BrowserTestBase { */ protected $defaultTheme = 'stark'; - /** - * The cache key under which project data is stored in key-value. - * - * @var string - */ - private readonly string $cacheKey; - /** * The key-value storage. * @@ -52,12 +45,12 @@ class ClearStorageTest extends BrowserTestBase { ->set('enabled_sources', ['project_browser_test_mock']) ->save(); - $this->cacheKey = 'query:' . md5('[]'); - $this->keyValue = \Drupal::service('keyvalue')->get('project_browser:project_browser_test_mock'); + $this->keyValue = \Drupal::service('keyvalue.expirable') + ->get('project_browser:project_browser_test_mock'); // Warm the project cache and confirm it is populated. \Drupal::service(EnabledSourceHandler::class)->getProjects('project_browser_test_mock'); - $this->assertNotEmpty($this->keyValue->get($this->cacheKey)); + $this->assertTrue($this->hasCachedQueries()); } /** @@ -67,7 +60,7 @@ class ClearStorageTest extends BrowserTestBase { */ public function testClearCacheDirectly(): void { \Drupal::service(EnabledSourceHandler::class)->clearStorage(); - $this->assertEmpty($this->keyValue->get($this->cacheKey)); + $this->assertFalse($this->hasCachedQueries()); } /** @@ -81,7 +74,7 @@ class ClearStorageTest extends BrowserTestBase { */ public function testClearCacheWithDrush(string $command): void { $this->drush($command); - $this->assertEmpty($this->keyValue->get($this->cacheKey)); + $this->assertFalse($this->hasCachedQueries()); } /** @@ -95,7 +88,17 @@ class ClearStorageTest extends BrowserTestBase { $assert_session = $this->assertSession(); $assert_session->buttonExists('Clear storage')->press(); $assert_session->statusMessageContains('Storage cleared.'); - $this->assertEmpty($this->keyValue->get($this->cacheKey)); + $this->assertFalse($this->hasCachedQueries()); + } + + /** + * Checks if there are any cached query results. + * + * @return bool + * Whether there are any cached query results. + */ + private function hasCachedQueries(): bool { + return (bool) preg_grep('/^query:[0-9a-f]+$/', array_keys($this->keyValue->getAll())); } } -- GitLab From 1aee84181498040a64855fea5f8dff5a8804df96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Tue, 18 Feb 2025 12:15:56 -0500 Subject: [PATCH 05/12] Refactor ProjectBrowserInstallerUiTest to not be order-senstiive --- .../ProjectBrowserInstallerUiTest.php | 89 ++++++++++++++----- 1 file changed, 68 insertions(+), 21 deletions(-) diff --git a/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php b/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php index 2e1560123..16212301d 100644 --- a/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php +++ b/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php @@ -195,10 +195,14 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase { $this->drupalGet('admin/modules/browse/project_browser_test_mock'); $this->svelteInitHelper('text', 'Pinky and the Brain'); - $cream_cheese_module_selector = '#project-browser .pb-layout__main ul > li:nth-child(1)'; - $download_button = $assert_session->waitForElementVisible('css', "$cream_cheese_module_selector button.pb__action_button"); + $cream_cheese = $this->getProjectCard('Cream cheese on a bagel'); + $download_button = $cream_cheese->waitFor( + 10, + function (NodeElement $card): ?NodeElement { + return $card->findButton('Install Cream cheese on a bagel'); + }, + ); $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(); $assert_session->checkboxNotChecked('edit-allow-ui-install'); @@ -207,9 +211,13 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase { $this->drupalGet('admin/modules/browse/project_browser_test_mock'); $this->svelteInitHelper('text', 'Cream cheese on a bagel'); - $action_button = $assert_session->waitForElementVisible('css', "$cream_cheese_module_selector button.pb__action_button"); + $action_button = $cream_cheese->waitFor( + 10, + function (NodeElement $card): ?NodeElement { + return $card->findButton('View Commands for Cream cheese on a bagel'); + }, + ); $this->assertNotEmpty($action_button); - $this->assertSame('View Commands for Cream cheese on a bagel', $action_button->getText()); } /** @@ -219,7 +227,6 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase { */ public function testCanBreakStageWithMissingProjectBrowserLock(): void { $assert_session = $this->assertSession(); - $page = $this->getSession()->getPage(); // Start install begin. $this->drupalGet('admin/modules/project_browser/install-begin', [ @@ -230,9 +237,8 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase { $this->svelteInitHelper('text', 'Cream cheese on a bagel'); // Try beginning another install while one is in progress, but not yet in // 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.pb__action_button"); - $cream_cheese_button?->click(); + $cream_cheese = $this->getProjectCard('Cream cheese on a bagel'); + $cream_cheese->find('css', "button.pb__action_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.')); @@ -240,12 +246,56 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase { $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.pb__action_button"); - $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()); + $cream_cheese->find('css', "button.pb__action_button")?->click(); + $this->waitForInstallToFinish($cream_cheese); + } + + /** + * Waits for a project to be visible and returns its list item (card). + * + * @param string $name + * The human-readable name of the project. + */ + private function getProjectCard(string $name): NodeElement { + $element = $this->assertSession() + ->elementExists('css', ".pb-project__title:contains('$name')"); + // Move upwards in the document until we find the card. + while (!$element->hasClass('pb-project')) { + $element = $element->getParent(); + } + $this->assertNotEmpty($element); + return $element; + } + + /** + * Waits for a project to finish installing. + * + * @param \Behat\Mink\Element\NodeElement $card + * The project's list item (card) element. + * @param int $timeout + * (optional) How to long to wait for the install finish, in seconds. + * Defaults to 30. + */ + private function waitForInstallToFinish(NodeElement $card, int $timeout = 30): void { + // First, wait for the status indicator element to appear at all. + $indicator = $card->waitFor( + $timeout, + function (NodeElement $card): ?NodeElement { + $indicator = $card->find('css', '.project_status-indicator'); + return $indicator?->isVisible() ? $indicator : NULL; + }, + ); + + $name = $card->find('css', '.pb-project__title')?->getText(); + $this->assertNotEmpty($name); + $was_installed = $indicator?->waitFor( + $timeout, + function (NodeElement $indicator) use ($name): bool { + return $indicator->getText() === "$name is Installed"; + }, + ); + $this->assertTrue($was_installed); } /** @@ -257,7 +307,6 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase { */ public function testCanBreakLock(): void { $assert_session = $this->assertSession(); - $page = $this->getSession()->getPage(); // Start install begin. $this->drupalGet('admin/modules/project_browser/install-begin', [ @@ -267,19 +316,17 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase { $this->svelteInitHelper('text', 'Cream cheese on a bagel'); // Try beginning another install while one is in progress, but not yet in // 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.pb__action_button"); + $cream_cheese = $this->getProjectCard('Cream cheese on a bagel'); + $cream_cheese_button = $cream_cheese->find('css', "button.pb__action_button"); $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.pb__action_button"); + $cream_cheese_button = $cream_cheese->find('css', "button.pb__action_button"); $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()); + $this->waitForInstallToFinish($cream_cheese); } /** -- GitLab From 42cf359cd938ba544dae83501a1228e1f0ae88f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Tue, 18 Feb 2025 12:28:45 -0500 Subject: [PATCH 06/12] Fix one more failure --- .../ProjectBrowserInstallerUiTest.php | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php b/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php index 16212301d..6edd48074 100644 --- a/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php +++ b/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php @@ -514,7 +514,6 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase { * Tests that the install state does not change on error. */ public function testInstallStatusUnchangedOnError(): void { - $assert_session = $this->assertSession(); $page = $this->getSession()->getPage(); // Start install begin. @@ -525,16 +524,18 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase { $this->svelteInitHelper('text', 'Cream cheese on a bagel'); // Try beginning another install while one is in progress, but not yet in // 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.pb__action_button"); - $cream_cheese_button?->click(); + $cream_cheese = $this->getProjectCard('Cream cheese on a bagel'); + $cream_cheese->find('css', "button.pb__action_button")?->click(); // Close the dialog to assert the state of install button. $page->find('css', '.ui-dialog-titlebar-close')?->click(); - $download_button = $assert_session->waitForElementVisible('css', "$cream_cheese_module_selector button.pb__action_button"); - $this->assertNotEmpty($download_button); + $download_button = $cream_cheese->waitFor( + 10, + function (NodeElement $card): ?NodeElement { + return $card->findButton('Install Cream cheese on a bagel'); + }, + ); // Assertion that the install state does not change. - $this->assertSame('Install Cream cheese on a bagel', $download_button->getText()); - + $this->assertNotEmpty($download_button?->isVisible()); } } -- GitLab From 7b737d94929e70618d810eaae56216f8c1dcd094 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Tue, 18 Feb 2025 12:44:21 -0500 Subject: [PATCH 07/12] Revert changes to JS tests because clearly something broke normal sorting --- .../ProjectBrowserInstallerUiTest.php | 106 +++++------------- 1 file changed, 29 insertions(+), 77 deletions(-) diff --git a/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php b/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php index 6edd48074..2e1560123 100644 --- a/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php +++ b/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php @@ -195,14 +195,10 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase { $this->drupalGet('admin/modules/browse/project_browser_test_mock'); $this->svelteInitHelper('text', 'Pinky and the Brain'); - $cream_cheese = $this->getProjectCard('Cream cheese on a bagel'); - $download_button = $cream_cheese->waitFor( - 10, - function (NodeElement $card): ?NodeElement { - return $card->findButton('Install Cream cheese on a bagel'); - }, - ); + $cream_cheese_module_selector = '#project-browser .pb-layout__main ul > li:nth-child(1)'; + $download_button = $assert_session->waitForElementVisible('css', "$cream_cheese_module_selector button.pb__action_button"); $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(); $assert_session->checkboxNotChecked('edit-allow-ui-install'); @@ -211,13 +207,9 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase { $this->drupalGet('admin/modules/browse/project_browser_test_mock'); $this->svelteInitHelper('text', 'Cream cheese on a bagel'); - $action_button = $cream_cheese->waitFor( - 10, - function (NodeElement $card): ?NodeElement { - return $card->findButton('View Commands for Cream cheese on a bagel'); - }, - ); + $action_button = $assert_session->waitForElementVisible('css', "$cream_cheese_module_selector button.pb__action_button"); $this->assertNotEmpty($action_button); + $this->assertSame('View Commands for Cream cheese on a bagel', $action_button->getText()); } /** @@ -227,6 +219,7 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase { */ public function testCanBreakStageWithMissingProjectBrowserLock(): void { $assert_session = $this->assertSession(); + $page = $this->getSession()->getPage(); // Start install begin. $this->drupalGet('admin/modules/project_browser/install-begin', [ @@ -237,8 +230,9 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase { $this->svelteInitHelper('text', 'Cream cheese on a bagel'); // Try beginning another install while one is in progress, but not yet in // the applying stage. - $cream_cheese = $this->getProjectCard('Cream cheese on a bagel'); - $cream_cheese->find('css', "button.pb__action_button")?->click(); + $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.pb__action_button"); + $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.')); @@ -246,56 +240,12 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase { $this->clickWithWait('#ui-id-1 > p > a'); $this->svelteInitHelper('text', 'Cream cheese on a bagel'); // Try beginning another install after breaking lock. - $cream_cheese->find('css', "button.pb__action_button")?->click(); - $this->waitForInstallToFinish($cream_cheese); - } - - /** - * Waits for a project to be visible and returns its list item (card). - * - * @param string $name - * The human-readable name of the project. - */ - private function getProjectCard(string $name): NodeElement { - $element = $this->assertSession() - ->elementExists('css', ".pb-project__title:contains('$name')"); - - // Move upwards in the document until we find the card. - while (!$element->hasClass('pb-project')) { - $element = $element->getParent(); - } - $this->assertNotEmpty($element); - return $element; - } + $cream_cheese_button = $page->find('css', "$cream_cheese_module_selector button.pb__action_button"); + $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()); - /** - * Waits for a project to finish installing. - * - * @param \Behat\Mink\Element\NodeElement $card - * The project's list item (card) element. - * @param int $timeout - * (optional) How to long to wait for the install finish, in seconds. - * Defaults to 30. - */ - private function waitForInstallToFinish(NodeElement $card, int $timeout = 30): void { - // First, wait for the status indicator element to appear at all. - $indicator = $card->waitFor( - $timeout, - function (NodeElement $card): ?NodeElement { - $indicator = $card->find('css', '.project_status-indicator'); - return $indicator?->isVisible() ? $indicator : NULL; - }, - ); - - $name = $card->find('css', '.pb-project__title')?->getText(); - $this->assertNotEmpty($name); - $was_installed = $indicator?->waitFor( - $timeout, - function (NodeElement $indicator) use ($name): bool { - return $indicator->getText() === "$name is Installed"; - }, - ); - $this->assertTrue($was_installed); } /** @@ -307,6 +257,7 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase { */ public function testCanBreakLock(): void { $assert_session = $this->assertSession(); + $page = $this->getSession()->getPage(); // Start install begin. $this->drupalGet('admin/modules/project_browser/install-begin', [ @@ -316,17 +267,19 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase { $this->svelteInitHelper('text', 'Cream cheese on a bagel'); // Try beginning another install while one is in progress, but not yet in // the applying stage. - $cream_cheese = $this->getProjectCard('Cream cheese on a bagel'); - $cream_cheese_button = $cream_cheese->find('css', "button.pb__action_button"); + $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.pb__action_button"); $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 = $cream_cheese->find('css', "button.pb__action_button"); + $cream_cheese_button = $page->find('css', "$cream_cheese_module_selector button.pb__action_button"); $cream_cheese_button?->click(); - $this->waitForInstallToFinish($cream_cheese); + $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()); } /** @@ -514,6 +467,7 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase { * Tests that the install state does not change on error. */ public function testInstallStatusUnchangedOnError(): void { + $assert_session = $this->assertSession(); $page = $this->getSession()->getPage(); // Start install begin. @@ -524,18 +478,16 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase { $this->svelteInitHelper('text', 'Cream cheese on a bagel'); // Try beginning another install while one is in progress, but not yet in // the applying stage. - $cream_cheese = $this->getProjectCard('Cream cheese on a bagel'); - $cream_cheese->find('css', "button.pb__action_button")?->click(); + $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.pb__action_button"); + $cream_cheese_button?->click(); // Close the dialog to assert the state of install button. $page->find('css', '.ui-dialog-titlebar-close')?->click(); - $download_button = $cream_cheese->waitFor( - 10, - function (NodeElement $card): ?NodeElement { - return $card->findButton('Install Cream cheese on a bagel'); - }, - ); + $download_button = $assert_session->waitForElementVisible('css', "$cream_cheese_module_selector button.pb__action_button"); + $this->assertNotEmpty($download_button); // Assertion that the install state does not change. - $this->assertNotEmpty($download_button?->isVisible()); + $this->assertSame('Install Cream cheese on a bagel', $download_button->getText()); + } } -- GitLab From 8629bdf658c47228a0c8255fae904a2735421c0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Fri, 21 Feb 2025 11:18:38 -0500 Subject: [PATCH 08/12] Fix test, hopefully --- .../ProjectBrowserUiTest.php | 97 ++++++++++++------- .../ProjectBrowserUiTestTrait.php | 38 -------- 2 files changed, 61 insertions(+), 74 deletions(-) diff --git a/tests/src/FunctionalJavascript/ProjectBrowserUiTest.php b/tests/src/FunctionalJavascript/ProjectBrowserUiTest.php index fa833827f..6f7976f79 100644 --- a/tests/src/FunctionalJavascript/ProjectBrowserUiTest.php +++ b/tests/src/FunctionalJavascript/ProjectBrowserUiTest.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Drupal\Tests\project_browser\FunctionalJavascript; +use Behat\Mink\Element\DocumentElement; use Behat\Mink\Element\NodeElement; use Drupal\Core\Extension\MissingDependencyException; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; @@ -149,14 +150,14 @@ class ProjectBrowserUiTest extends WebDriverTestBase { // Svelte app has occasional initialization problems on GitLabCI that are // reliably fixed by a page reload, so we allow that here to prevent random // failures that are not representative of real world use. - $this->assertProjectsVisible([ + $this->waitForProjects([ 'Cream cheese on a bagel', 'Dancing Queen', 'Kangaroo', '9 Starts With a Higher Number', 'Helvetica', 'Astronaut Simulator', - ], TRUE); + ]); // Clear the checkbox to verify the results revert to their initial state. $this->clickWithWait('#104', '10 Results'); @@ -178,7 +179,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase { // Make sure the 'Media' module category filter is applied. $assert_category_filters_applied(['Media', 'E-commerce']); // Assert that only media and administration module categories are shown. - $this->assertProjectsVisible([ + $this->waitForProjects([ 'Jazz', 'Eggman', 'Tooth Fairy', @@ -245,7 +246,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase { $this->drupalGet('admin/modules/browse/project_browser_test_mock'); $this->svelteInitHelper('text', '10 Results'); - $this->assertProjectsVisible([ + $this->waitForProjects([ 'Cream cheese on a bagel', 'Pinky and the Brain', 'Dancing Queen', @@ -261,7 +262,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase { $page->pressButton('Clear filters'); $this->assertTrue($assert_session->waitForText('25 Results')); - $this->assertProjectsVisible([ + $this->waitForProjects([ 'Jazz', 'Eggman', 'Tooth Fairy', @@ -279,7 +280,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase { $assert_session->elementExists('css', '.pager__item--active > .is-active[aria-label="Page 1"]'); $this->clickWithWait('[aria-label="Next page"]'); - $this->assertProjectsVisible([ + $this->waitForProjects([ '9 Starts With a Higher Number', 'Quiznos', 'Octopus', @@ -296,7 +297,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase { $this->assertPagerItems(['First', 'Previous', '1', '2', '3', 'Next', 'Last']); $this->clickWithWait('[aria-label="Next page"]'); - $this->assertProjectsVisible([ + $this->waitForProjects([ 'Astronaut Simulator', ]); $this->assertPagerItems(['First', 'Previous', '1', '2', '3']); @@ -351,7 +352,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase { $this->svelteInitHelper('text', 'Astronaut Simulator'); $this->pressWithWait('Clear filters'); $this->pressWithWait('Recommended filters'); - $this->assertProjectsVisible([ + $this->waitForProjects([ 'Cream cheese on a bagel', 'Pinky and the Brain', 'Dancing Queen', @@ -369,7 +370,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase { // Clear the security covered filter. $this->clickWithWait(self::SECURITY_OPTION_SELECTOR . self::OPTION_LAST_CHILD); - $this->assertProjectsVisible([ + $this->waitForProjects([ 'Jazz', 'Vitamin&C;$?', 'Cream cheese on a bagel', @@ -392,7 +393,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase { // Click the Active filter. $page->selectFieldOption('development_status', 'Show projects under active development'); - $this->assertProjectsVisible([ + $this->waitForProjects([ 'Jazz', 'Cream cheese on a bagel', 'Ice Ice', @@ -409,7 +410,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase { // Click the "Show all" filter for security. $this->clickWithWait(self::SECURITY_OPTION_SELECTOR . self::OPTION_LAST_CHILD, '', TRUE); - $this->assertProjectsVisible([ + $this->waitForProjects([ 'Jazz', 'Cream cheese on a bagel', 'Ice Ice', @@ -431,7 +432,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase { $this->clickWithWait(self::MAINTENANCE_OPTION_SELECTOR . self::OPTION_FIRST_CHILD); $this->assertEquals('Show actively maintained projects', $this->getElementText(self::MAINTENANCE_OPTION_SELECTOR . self::OPTION_CHECKED)); - $this->assertProjectsVisible([ + $this->waitForProjects([ 'Jazz', 'Vitamin&C;$?', 'Cream cheese on a bagel', @@ -466,7 +467,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase { $this->sortBy('a_z'); // Assert that the projects are listed in ascending order of their titles. - $this->assertProjectsVisible([ + $this->waitForProjects([ '1 Starts With a Number', '9 Starts With a Higher Number', 'Astronaut Simulator', @@ -484,7 +485,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase { // Select 'Z-A' sorting order. $this->sortBy('z_a'); - $this->assertProjectsVisible([ + $this->waitForProjects([ 'Vitamin&C;$?', 'Unwritten&:/', 'Tooth Fairy', @@ -503,7 +504,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase { $this->sortBy('usage_total'); // Assert that the projects are listed in descending order of their usage. - $this->assertProjectsVisible([ + $this->waitForProjects([ 'Jazz', 'Eggman', 'Tooth Fairy', @@ -523,7 +524,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase { // Assert that the projects are listed in descending order of their date of // creation. - $this->assertProjectsVisible([ + $this->waitForProjects([ '9 Starts With a Higher Number', 'Helvetica', 'Become a Banana', @@ -554,7 +555,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase { $this->inputSearchField('', TRUE); $this->inputSearchField('&', TRUE); $assert_session->waitForElementVisible('css', ".search__search-submit")?->click(); - $this->assertProjectsVisible([ + $this->waitForProjects([ 'Vitamin&C;$?', 'Unwritten&:/', ]); @@ -563,7 +564,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase { $this->inputSearchField('', TRUE); $this->inputSearchField('n&', TRUE); $assert_session->waitForElementVisible('css', ".search__search-submit")?->click(); - $this->assertProjectsVisible([ + $this->waitForProjects([ 'Vitamin&C;$?', 'Unwritten&:/', ]); @@ -571,28 +572,28 @@ class ProjectBrowserUiTest extends WebDriverTestBase { $this->inputSearchField('', TRUE); $this->inputSearchField('$', TRUE); $assert_session->waitForElementVisible('css', ".search__search-submit")?->click(); - $this->assertProjectsVisible([ + $this->waitForProjects([ 'Vitamin&C;$?', ]); $this->inputSearchField('', TRUE); $this->inputSearchField('?', TRUE); $assert_session->waitForElementVisible('css', ".search__search-submit")?->click(); - $this->assertProjectsVisible([ + $this->waitForProjects([ 'Vitamin&C;$?', ]); $this->inputSearchField('', TRUE); $this->inputSearchField('&:', TRUE); $assert_session->waitForElementVisible('css', ".search__search-submit")?->click(); - $this->assertProjectsVisible([ + $this->waitForProjects([ 'Unwritten&:/', ]); $this->inputSearchField('', TRUE); $this->inputSearchField('$?', TRUE); $assert_session->waitForElementVisible('css', ".search__search-submit")?->click(); - $this->assertProjectsVisible([ + $this->waitForProjects([ 'Vitamin&C;$?', ]); } @@ -667,7 +668,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase { $this->clickWithWait('#67', '', TRUE); $this->assertTrue($assert_session->waitForText('15 Results')); - $this->assertProjectsVisible([ + $this->waitForProjects([ 'Octopus', 'No Scrubs', 'Mad About You', @@ -683,7 +684,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase { ]); $this->clickWithWait('[aria-label="Next page"]'); - $this->assertProjectsVisible([ + $this->waitForProjects([ 'Astronaut Simulator', '9 Starts With a Higher Number', '1 Starts With a Number', @@ -691,7 +692,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase { $this->getSession()->reload(); // Should still be on second results page. $this->svelteInitHelper('css', '#project-browser .pb-project'); - $this->assertProjectsVisible([ + $this->waitForProjects([ 'Astronaut Simulator', '9 Starts With a Higher Number', '1 Starts With a Number', @@ -702,7 +703,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase { $this->assertEquals('Media', $this->getElementText('p.filter-applied:nth-child(2) .filter-applied__label')); $this->clickWithWait('[aria-label="First page"]'); - $this->assertProjectsVisible([ + $this->waitForProjects([ 'Octopus', 'No Scrubs', 'Mad About You', @@ -715,7 +716,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase { 'Dancing Queen', 'Cream cheese on a bagel', 'Become a Banana', - ], TRUE); + ]); $this->assertEquals('E-commerce', $this->getElementText('p.filter-applied:first-child .filter-applied__label')); $this->assertEquals('Media', $this->getElementText('p.filter-applied:nth-child(2) .filter-applied__label')); @@ -748,7 +749,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase { $page = $this->getSession()->getPage(); $assert_session = $this->assertSession(); // Enable module for extra source plugin. - $this->container->get('module_installer')->install(['project_browser_devel'], TRUE); + $this->container->get('module_installer')->install(['project_browser_devel']); // Test categories with multiple plugin enabled. $this->drupalGet('admin/modules/browse'); $this->svelteInitHelper('css', '.pb-filter__checkbox'); @@ -838,7 +839,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase { $this->inputSearchField('Number', TRUE); $assert_session->waitForElementVisible('css', ".search__search-submit")?->click(); $this->assertTrue($assert_session->waitForText('2 Results')); - $this->assertProjectsVisible([ + $this->waitForProjects([ '9 Starts With a Higher Number', '1 Starts With a Number', ]); @@ -850,7 +851,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase { $this->svelteInitHelper('css', '#project-browser .pb-project'); // Assert that the filters persist. $this->assertTrue($assert_session->waitForText('2 Results')); - $this->assertProjectsVisible([ + $this->waitForProjects([ '9 Starts With a Higher Number', '1 Starts With a Number', ]); @@ -987,7 +988,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase { $this->drupalGet('admin/modules/browse/project_browser_test_mock'); $this->pressWithWait('Clear filters'); - $this->assertProjectsVisible([ + $this->waitForProjects([ 'Jazz', 'Eggman', 'Tooth Fairy', @@ -1004,7 +1005,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase { $this->assertPagerItems(['1', '2', '3', 'Next', 'Last']); $this->clickWithWait('[aria-label="Last page"]'); - $this->assertProjectsVisible([ + $this->waitForProjects([ 'Astronaut Simulator', ]); @@ -1055,7 +1056,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase { // @todo Remove try/catch in https://www.drupal.org/i/3349193. try { - $this->container->get('module_installer')->install(['package_manager'], TRUE); + $this->container->get('module_installer')->install(['package_manager']); } catch (MissingDependencyException $e) { $this->markTestSkipped($e->getMessage()); @@ -1174,7 +1175,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase { $assert_session = $this->assertSession(); // Enable module for extra source plugin. - $this->container->get('module_installer')->install(['project_browser_devel'], TRUE); + $this->container->get('module_installer')->install(['project_browser_devel']); $this->rebuildContainer(); $current_sources = $this->container->get(EnabledSourceHandler::class)->getCurrentSources(); @@ -1308,7 +1309,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase { $search_field = $assert_session->waitForElementVisible('css', '#pb-text'); $this->inputSearchField('Tooth Fairy', TRUE); $assert_session->waitForElementVisible('css', ".search__search-submit")?->click(); - $this->assertProjectsVisible([ + $this->waitForProjects([ 'Tooth Fairy', ]); // Check search box still has search text. @@ -1321,7 +1322,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase { // Run search again. $this->inputSearchField('Tooth Fairy', TRUE); $assert_session->waitForElementVisible('css', ".search__search-submit")?->click(); - $this->assertProjectsVisible([ + $this->waitForProjects([ 'Tooth Fairy', ]); @@ -1330,4 +1331,28 @@ class ProjectBrowserUiTest extends WebDriverTestBase { $this->assertEquals($search_field->getValue(), ''); } + /** + * Asserts that a given list of project titles are visible on the page. + * + * This doesn't assert that the projects appear in any particular order, only + * that they appear somewhere in the project browser. + * + * @param array $project_titles + * An array of expected titles. + */ + private function waitForProjects(array $project_titles): void { + $missing = []; + $success = $this->getSession() + ->getPage() + ->waitFor(10, function (DocumentElement $page) use ($project_titles, &$missing): bool { + $projects_on_page = array_map( + fn (NodeElement $element) => $element->getText(), + $page->findAll('css', '#project-browser .pb-project h3 button'), + ); + $missing = array_diff($project_titles, $projects_on_page); + return empty($missing); + }); + $this->assertTrue($success, sprintf('The following projects should have appeared, but did not: %s', implode(', ', $missing))); + } + } diff --git a/tests/src/FunctionalJavascript/ProjectBrowserUiTestTrait.php b/tests/src/FunctionalJavascript/ProjectBrowserUiTestTrait.php index b7f40a120..ce6632a1c 100644 --- a/tests/src/FunctionalJavascript/ProjectBrowserUiTestTrait.php +++ b/tests/src/FunctionalJavascript/ProjectBrowserUiTestTrait.php @@ -98,44 +98,6 @@ trait ProjectBrowserUiTestTrait { $this->assertInstanceOf(NodeElement::class, $indicator); } - /** - * Asserts that a given list of project titles are visible on the page. - * - * @param array $project_titles - * An array of expected titles. - * @param bool $reload - * When TRUE, reload the page if the assertion fails and try again. - * This should typically be kept to the default value of FALSE. It only - * needs to be set to TRUE for calls that intermittently fail on GitLabCI. - */ - protected function assertProjectsVisible(array $project_titles, bool $reload = FALSE): void { - $count = count($project_titles); - - // Create a JavaScript string that checks the titles of the visible - // projects. This is done with JavaScript to avoid issues with PHP - // referencing an element that was rerendered and thus unavailable. - $script = "document.querySelectorAll('#project-browser .pb-project h3 button').length === $count"; - foreach ($project_titles as $key => $value) { - $script .= " && document.querySelectorAll('#project-browser .pb-project h3 button')[$key].textContent === '$value'"; - } - - // It can take a while for all items to render. Wait for the condition to be - // true before asserting it. - $this->getSession()->wait(10000, $script); - - if ($reload) { - try { - $this->assertTrue($this->getSession()->evaluateScript($script), 'Ran:' . $script . 'Svelte did not initialize. Markup: ' . $this->getSession()->evaluateScript('document.querySelector("#project-browser").innerHTML')); - } - catch (\Exception) { - $this->getSession()->reload(); - $this->getSession()->wait(10000, $script); - } - } - - $this->assertTrue($this->getSession()->evaluateScript($script), 'Ran:' . $script . 'Svelte did not initialize. Markup: ' . $this->getSession()->evaluateScript('document.querySelector("#project-browser").innerHTML')); - } - /** * Searches for a term in the search field. * -- GitLab From becbd94d408f98b99f68841953dae289e1c26060 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Fri, 21 Feb 2025 11:40:17 -0500 Subject: [PATCH 09/12] Make testCategoriesVisibility() non-flaky --- .../ProjectBrowserUiTest.php | 41 +++++++------------ 1 file changed, 14 insertions(+), 27 deletions(-) diff --git a/tests/src/FunctionalJavascript/ProjectBrowserUiTest.php b/tests/src/FunctionalJavascript/ProjectBrowserUiTest.php index 6f7976f79..d6df2e278 100644 --- a/tests/src/FunctionalJavascript/ProjectBrowserUiTest.php +++ b/tests/src/FunctionalJavascript/ProjectBrowserUiTest.php @@ -948,36 +948,23 @@ class ProjectBrowserUiTest extends WebDriverTestBase { /** * Tests the visibility of categories in list and grid view. + * + * @testWith ["List"] + * ["Grid"] */ - public function testCategoriesVisibility(): void { - $page = $this->getSession()->getPage(); - $assert_session = $this->assertSession(); - $view_options = [ - [ - 'selector' => '.pb-display__button[value="Grid"]', - 'value' => 'Grid', - ], [ - 'selector' => '.pb-display__button[value="List"]', - 'value' => 'List', - ], - ]; + public function testCategoriesVisibility(string $display_type): void { $this->getSession()->resizeWindow(1300, 1300); - // Check visibility of categories in each view. - foreach ($view_options as $selector) { - $this->drupalGet('admin/modules/browse/project_browser_test_mock'); - $this->svelteInitHelper('css', $selector['selector']); - $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)'); - $this->assertEquals('E-commerce', $grid_text); - $assert_session->elementsCount('css', '#project-browser .pb-layout__main ul li:nth-child(10) .pb-project-categories ul li', 2); - $grid_text = $this->getElementText('#project-browser .pb-layout__main ul li:nth-child(7) .pb-project-categories ul li:nth-child(1)'); - $this->assertEquals('E-commerce', $grid_text); - $grid_text = $this->getElementText('#project-browser .pb-layout__main ul li:nth-child(10) .pb-project-categories ul li:nth-child(2)'); - $this->assertEquals('E-commerce', $grid_text); - } + $this->drupalGet('admin/modules/browse/project_browser_test_mock'); + $this->assertSession()->waitForButton($display_type)?->press(); + + $helvetica = $this->waitForProject('Helvetica'); + $this->assertSame('E-commerce', $helvetica->find('css', '.pb-project-categories ul li')?->getText()); + + $astronaut_simulator_categories = $this->waitForProject('Astronaut Simulator') + ->findAll('css', '.pb-project-categories ul li'); + $this->assertCount(2, $astronaut_simulator_categories); + $this->assertSame('E-commerce', end($astronaut_simulator_categories)->getText()); } /** -- GitLab From 7c2c2ca04fbb0807593757c90dfd6eac93bee1a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Tue, 25 Feb 2025 13:41:46 -0500 Subject: [PATCH 10/12] Revert all tests --- tests/src/Functional/ClearStorageTest.php | 29 +++++++++---------- .../Functional/EnabledSourceHandlerTest.php | 17 +++++------ 2 files changed, 20 insertions(+), 26 deletions(-) diff --git a/tests/src/Functional/ClearStorageTest.php b/tests/src/Functional/ClearStorageTest.php index 98a452504..d0be6f3f6 100644 --- a/tests/src/Functional/ClearStorageTest.php +++ b/tests/src/Functional/ClearStorageTest.php @@ -28,6 +28,13 @@ class ClearStorageTest extends BrowserTestBase { */ protected $defaultTheme = 'stark'; + /** + * The cache key under which project data is stored in key-value. + * + * @var string + */ + private readonly string $cacheKey; + /** * The key-value storage. * @@ -45,12 +52,12 @@ class ClearStorageTest extends BrowserTestBase { ->set('enabled_sources', ['project_browser_test_mock']) ->save(); - $this->keyValue = \Drupal::service('keyvalue.expirable') - ->get('project_browser:project_browser_test_mock'); + $this->cacheKey = 'query:' . md5('[]'); + $this->keyValue = \Drupal::service('keyvalue')->get('project_browser:project_browser_test_mock'); // Warm the project cache and confirm it is populated. \Drupal::service(EnabledSourceHandler::class)->getProjects('project_browser_test_mock'); - $this->assertTrue($this->hasCachedQueries()); + $this->assertNotEmpty($this->keyValue->get($this->cacheKey)); } /** @@ -60,7 +67,7 @@ class ClearStorageTest extends BrowserTestBase { */ public function testClearCacheDirectly(): void { \Drupal::service(EnabledSourceHandler::class)->clearStorage(); - $this->assertFalse($this->hasCachedQueries()); + $this->assertEmpty($this->keyValue->get($this->cacheKey)); } /** @@ -74,7 +81,7 @@ class ClearStorageTest extends BrowserTestBase { */ public function testClearCacheWithDrush(string $command): void { $this->drush($command); - $this->assertFalse($this->hasCachedQueries()); + $this->assertEmpty($this->keyValue->get($this->cacheKey)); } /** @@ -88,17 +95,7 @@ class ClearStorageTest extends BrowserTestBase { $assert_session = $this->assertSession(); $assert_session->buttonExists('Clear storage')->press(); $assert_session->statusMessageContains('Storage cleared.'); - $this->assertFalse($this->hasCachedQueries()); - } - - /** - * Checks if there are any cached query results. - * - * @return bool - * Whether there are any cached query results. - */ - private function hasCachedQueries(): bool { - return (bool) preg_grep('/^query:[0-9a-f]+$/', array_keys($this->keyValue->getAll())); + $this->assertEmpty($this->keyValue->get($this->cacheKey)); } } diff --git a/tests/src/Functional/EnabledSourceHandlerTest.php b/tests/src/Functional/EnabledSourceHandlerTest.php index 3207ab8cb..f0b9ce8b4 100644 --- a/tests/src/Functional/EnabledSourceHandlerTest.php +++ b/tests/src/Functional/EnabledSourceHandlerTest.php @@ -66,23 +66,20 @@ class EnabledSourceHandlerTest extends BrowserTestBase { /** @var \Drupal\project_browser\EnabledSourceHandler $handler */ $handler = $this->container->get(EnabledSourceHandler::class); - $has_cached_queries = function (): bool { - /** @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface $storage */ - $storage = $this->container->get('keyvalue.expirable') - ->get('project_browser:project_browser_test_mock'); - return (bool) preg_grep('/^query:[0-9a-f]+$/', array_keys($storage->getAll())); - }; - $handler->getProjects('project_browser_test_mock'); + /** @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface $storage */ + $storage = $this->container->get('keyvalue') + ->get('project_browser:project_browser_test_mock'); // Query results should have been stored. - $this->assertTrue($has_cached_queries()); + $query_cache_key = 'query:' . md5('[]'); + $this->assertTrue($storage->has($query_cache_key)); $handler->clearStorage(); ProjectBrowserTestMock::$resultsError = 'Nope!'; $handler->getProjects('project_browser_test_mock'); - // No new query results should have been stored. - $this->assertFalse($has_cached_queries()); + // No query results should have been stored. + $this->assertFalse($storage->has($query_cache_key)); } /** -- GitLab From b8606d87ed220092c2d2384eb5d07f027c080c2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Tue, 25 Feb 2025 13:43:30 -0500 Subject: [PATCH 11/12] Revert expirable --- project_browser.services.yml | 2 +- src/EnabledSourceHandler.php | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/project_browser.services.yml b/project_browser.services.yml index 3b18bdd9c..4e2e42215 100644 --- a/project_browser.services.yml +++ b/project_browser.services.yml @@ -9,7 +9,7 @@ services: parent: default_plugin_manager Drupal\project_browser\EnabledSourceHandler: arguments: - $keyValueFactory: '@keyvalue.expirable' + $keyValueFactory: '@keyvalue' calls: - [setLogger, ['@logger.channel.project_browser']] tags: diff --git a/src/EnabledSourceHandler.php b/src/EnabledSourceHandler.php index 5c25daa9d..c223bde84 100644 --- a/src/EnabledSourceHandler.php +++ b/src/EnabledSourceHandler.php @@ -7,8 +7,8 @@ use Drupal\Component\Serialization\Json; use Drupal\Core\Config\ConfigCrudEvent; use Drupal\Core\Config\ConfigEvents; use Drupal\Core\Config\ConfigFactoryInterface; -use Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface; -use Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface; +use Drupal\Core\KeyValueStore\KeyValueFactoryInterface; +use Drupal\Core\KeyValueStore\KeyValueStoreInterface; use Drupal\project_browser\Plugin\ProjectBrowserSourceManager; use Drupal\project_browser\ProjectBrowser\Project; use Drupal\project_browser\ProjectBrowser\ProjectsResultsPage; @@ -26,7 +26,7 @@ class EnabledSourceHandler implements LoggerAwareInterface, EventSubscriberInter public function __construct( private readonly ConfigFactoryInterface $configFactory, private readonly ProjectBrowserSourceManager $pluginManager, - private readonly KeyValueExpirableFactoryInterface $keyValueFactory, + private readonly KeyValueFactoryInterface $keyValueFactory, ) {} /** @@ -35,10 +35,10 @@ class EnabledSourceHandler implements LoggerAwareInterface, EventSubscriberInter * @param string $source_id * The ID of a source plugin. * - * @return \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface + * @return \Drupal\Core\KeyValueStore\KeyValueStoreInterface * A key-value store for the specified source plugin. */ - private function keyValue(string $source_id): KeyValueStoreExpirableInterface { + private function keyValue(string $source_id): KeyValueStoreInterface { return $this->keyValueFactory->get("project_browser:$source_id"); } @@ -132,15 +132,15 @@ class EnabledSourceHandler implements LoggerAwareInterface, EventSubscriberInter $storage->setIfNotExists($project->id, $project); } // If there were no query errors, store the results as a set of arguments - // to ProjectsResultsPage. These cached results automatically expire daily. + // to ProjectsResultsPage. if (empty($results->error)) { - $storage->setWithExpire($cache_key, [ + $storage->set($cache_key, [ $results->totalResults, array_column($results->list, 'id'), $results->pluginLabel, $source_id, $results->error, - ], 86400); + ]); } return $results; } -- GitLab From aa606404908449bb8e09c6b6a63166448bfc5ea3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Tue, 25 Feb 2025 14:24:03 -0500 Subject: [PATCH 12/12] Fix functional tests --- tests/src/Functional/ClearStorageTest.php | 16 ++++------------ .../Functional/EnabledSourceHandlerTest.php | 19 ++++++++++++------- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/tests/src/Functional/ClearStorageTest.php b/tests/src/Functional/ClearStorageTest.php index d0be6f3f6..a1209bc38 100644 --- a/tests/src/Functional/ClearStorageTest.php +++ b/tests/src/Functional/ClearStorageTest.php @@ -28,13 +28,6 @@ class ClearStorageTest extends BrowserTestBase { */ protected $defaultTheme = 'stark'; - /** - * The cache key under which project data is stored in key-value. - * - * @var string - */ - private readonly string $cacheKey; - /** * The key-value storage. * @@ -52,12 +45,11 @@ class ClearStorageTest extends BrowserTestBase { ->set('enabled_sources', ['project_browser_test_mock']) ->save(); - $this->cacheKey = 'query:' . md5('[]'); $this->keyValue = \Drupal::service('keyvalue')->get('project_browser:project_browser_test_mock'); // Warm the project cache and confirm it is populated. \Drupal::service(EnabledSourceHandler::class)->getProjects('project_browser_test_mock'); - $this->assertNotEmpty($this->keyValue->get($this->cacheKey)); + $this->assertNotEmpty($this->keyValue->getAll()); } /** @@ -67,7 +59,7 @@ class ClearStorageTest extends BrowserTestBase { */ public function testClearCacheDirectly(): void { \Drupal::service(EnabledSourceHandler::class)->clearStorage(); - $this->assertEmpty($this->keyValue->get($this->cacheKey)); + $this->assertEmpty($this->keyValue->getAll()); } /** @@ -81,7 +73,7 @@ class ClearStorageTest extends BrowserTestBase { */ public function testClearCacheWithDrush(string $command): void { $this->drush($command); - $this->assertEmpty($this->keyValue->get($this->cacheKey)); + $this->assertEmpty($this->keyValue->getAll()); } /** @@ -95,7 +87,7 @@ class ClearStorageTest extends BrowserTestBase { $assert_session = $this->assertSession(); $assert_session->buttonExists('Clear storage')->press(); $assert_session->statusMessageContains('Storage cleared.'); - $this->assertEmpty($this->keyValue->get($this->cacheKey)); + $this->assertEmpty($this->keyValue->getAll()); } } diff --git a/tests/src/Functional/EnabledSourceHandlerTest.php b/tests/src/Functional/EnabledSourceHandlerTest.php index f0b9ce8b4..aea25f80f 100644 --- a/tests/src/Functional/EnabledSourceHandlerTest.php +++ b/tests/src/Functional/EnabledSourceHandlerTest.php @@ -65,21 +65,26 @@ class EnabledSourceHandlerTest extends BrowserTestBase { public function testErrorsAreNotStored(): void { /** @var \Drupal\project_browser\EnabledSourceHandler $handler */ $handler = $this->container->get(EnabledSourceHandler::class); - $handler->getProjects('project_browser_test_mock'); - /** @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface $storage */ - $storage = $this->container->get('keyvalue') - ->get('project_browser:project_browser_test_mock'); + + $has_cached_queries = function (): bool { + $items = $this->container->get('keyvalue') + ->get('project_browser:project_browser_test_mock') + ->getAll(); + return (bool) array_filter( + array_keys($items), + fn (string $key): bool => str_starts_with($key, 'query:'), + ); + }; // Query results should have been stored. - $query_cache_key = 'query:' . md5('[]'); - $this->assertTrue($storage->has($query_cache_key)); + $this->assertTrue($has_cached_queries()); $handler->clearStorage(); ProjectBrowserTestMock::$resultsError = 'Nope!'; $handler->getProjects('project_browser_test_mock'); // No query results should have been stored. - $this->assertFalse($storage->has($query_cache_key)); + $this->assertFalse($has_cached_queries()); } /** -- GitLab