From ac68f3d79dc08972dc75fff8af436cf480837633 Mon Sep 17 00:00:00 2001
From: Adam G-H <32250-phenaproxima@users.noreply.drupalcode.org>
Date: Tue, 25 Feb 2025 17:42:51 +0000
Subject: [PATCH] Issue #3508334 by phenaproxima, tim.plunkett:
 ProjectBrowserUiTest is order-sensitive when it shouldn't be

---
 .../ProjectBrowserUiTest.php                  | 141 ++++++++++--------
 .../ProjectBrowserUiTestTrait.php             |  38 -----
 2 files changed, 82 insertions(+), 97 deletions(-)

diff --git a/tests/src/FunctionalJavascript/ProjectBrowserUiTest.php b/tests/src/FunctionalJavascript/ProjectBrowserUiTest.php
index c79c0cc5a..28180b0ab 100644
--- a/tests/src/FunctionalJavascript/ProjectBrowserUiTest.php
+++ b/tests/src/FunctionalJavascript/ProjectBrowserUiTest.php
@@ -130,19 +130,8 @@ class ProjectBrowserUiTest extends WebDriverTestBase {
     $e_commerce = $assert_session->waitForField('E-commerce');
     $e_commerce?->check();
 
-    // @todo Move this into a trait for testing Project Browser's UI in a
-    //   stable, consistent fashion.
-    $assert_category_filters_applied = function (array $expected_categories): void {
-      $selector = '.filter-applied__label';
-      $this->assertElementIsVisible('css', $selector);
-      $applied_categories = array_map(
-        fn ($element) => $element->getText(),
-        $this->getSession()->getPage()->findAll('css', $selector),
-      );
-      $this->assertSame($expected_categories, $applied_categories);
-    };
     // Make sure the 'E-commerce' module category filter is applied.
-    $assert_category_filters_applied(['E-commerce']);
+    $this->assertSame(['E-commerce'], $this->getSelectedCategories());
 
     // This call has the second argument, `$reload`, set to TRUE due to it
     // failing on ~2% of GitLabCI test runs. It is not entirely clear why this
@@ -157,7 +146,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase {
       '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');
@@ -177,7 +166,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase {
     $e_commerce?->check();
 
     // Make sure the 'Media' module category filter is applied.
-    $assert_category_filters_applied(['Media', 'E-commerce']);
+    $this->assertSame(['Media', 'E-commerce'], $this->getSelectedCategories());
     // Assert that only media and administration module categories are shown.
     $this->assertProjectsVisible([
       'Jazz',
@@ -481,7 +470,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase {
       'Grapefruit',
       'Helvetica',
       'Ice Ice',
-    ]);
+    ], in_order: TRUE);
 
     // Select 'Z-A' sorting order.
     $this->sortBy('z_a');
@@ -499,7 +488,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase {
       'Mad About You',
       'Looper',
       'Kangaroo',
-    ]);
+    ], in_order: TRUE);
 
     // Select 'Active installs' option.
     $this->sortBy('usage_total');
@@ -518,7 +507,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase {
       'Mad About You',
       'Dancing Queen',
       'Kangaroo',
-    ]);
+    ], in_order: TRUE);
 
     // Select 'Newest First' option.
     $this->sortBy('created');
@@ -538,7 +527,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase {
       'Soup',
       'Octopus',
       'Tooth Fairy',
-    ]);
+    ], in_order: TRUE);
   }
 
   /**
@@ -699,8 +688,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase {
     ]);
     $this->assertPageHasText('15 Results');
 
-    $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'));
+    $this->assertSame(['E-commerce', 'Media'], $this->getSelectedCategories());
 
     $this->clickWithWait('[aria-label="First page"]');
     $this->assertProjectsVisible([
@@ -716,10 +704,9 @@ 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'));
+    $this->assertSame(['E-commerce', 'Media'], $this->getSelectedCategories());
   }
 
   /**
@@ -748,7 +735,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');
@@ -810,22 +797,19 @@ class ProjectBrowserUiTest extends WebDriverTestBase {
     $this->assertNotSame($second_tab_text, $assert_session->buttonExists('random_data')->getText());
     $this->assertSame('1 category selected', $page->find('css', '.pb-filter__multi-dropdown__label')->getText());
     // Save the filter applied in second tab.
-    $applied_filter = $this->getElementText('p.filter-applied:nth-child(1) .filter-applied__label');
+    [$applied_filter] = $this->getSelectedCategories();
     // Save the number of results.
     $results_before = count($page->findAll('css', '#project-browser .pb-project.list'));
 
     // Switch back to first tab.
     $this->pressWithWait('project_browser_test_mock');
     $this->assertSame('2 categories selected', $page->find('css', '.pb-filter__multi-dropdown__label')->getText());
-    $first_filter_element = $page->find('css', 'p.filter-applied:nth-child(1)');
-    $this->assertEquals('E-commerce', $first_filter_element->find('css', '.filter-applied__label')->getText());
-    $second_filter_element = $page->find('css', 'p.filter-applied:nth-child(2)');
-    $this->assertEquals('Media', $second_filter_element->find('css', '.filter-applied__label')->getText());
+    $this->assertSame(['E-commerce', 'Media'], $this->getSelectedCategories());
 
     // Again switch to second tab.
     $this->pressWithWait('random_data');
     // Assert that the filters persist.
-    $this->assertEquals($applied_filter, $this->getElementText('p.filter-applied:nth-child(1) .filter-applied__label'));
+    $this->assertSame($applied_filter, $this->getSelectedCategories()[0]);
     $this->assertSame('1 category selected', $page->find('css', '.pb-filter__multi-dropdown__label')->getText());
 
     // Assert that the number of results is the same.
@@ -947,36 +931,22 @@ class ProjectBrowserUiTest extends WebDriverTestBase {
 
   /**
    * Tests the visibility of categories in list and grid view.
+   *
+   * @testWith ["Grid"]
+   *           ["List"]
    */
-  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);
+    $this->drupalGet('admin/modules/browse/project_browser_test_mock');
+    $this->assertSession()->waitForButton($display_type)?->press();
 
-    // 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);
-    }
+    $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', $astronaut_simulator_categories[1]->getText());
   }
 
   /**
@@ -1055,7 +1025,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());
@@ -1166,7 +1136,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase {
    */
   public function testSourcePluginRoutes(): void {
     // 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();
@@ -1322,4 +1292,57 @@ class ProjectBrowserUiTest extends WebDriverTestBase {
     $this->assertEquals($search_field->getValue(), '');
   }
 
+  /**
+   * Asserts that a given list of project titles are visible on the page.
+   *
+   * @param array $project_titles
+   *   An array of expected titles.
+   * @param int $timeout
+   *   (optional) How many seconds to wait before giving up. Defaults to 10.
+   * @param bool $in_order
+   *   (optional) If TRUE, assert that the projects are visible on the page
+   *   in the same order as $project_titles. Defaults to FALSE.
+   */
+  private function assertProjectsVisible(array $project_titles, int $timeout = 10, bool $in_order = FALSE): void {
+    $page = $this->getSession()->getPage();
+
+    $list_visible_projects = function () use ($page): array {
+      return array_map(
+        fn (NodeElement $element) => $element->getText(),
+        $page->findAll('css', '#project-browser .pb-project h3 button'),
+      );
+    };
+
+    $missing = [];
+    $success = $this->getSession()
+      ->getPage()
+      ->waitFor($timeout, function () use ($project_titles, &$missing, $list_visible_projects): bool {
+        $missing = array_diff($project_titles, $list_visible_projects());
+        return empty($missing);
+      });
+
+    $this->assertTrue(
+      $success,
+      sprintf('The following projects should have appeared, but did not: %s', implode(', ', $missing)),
+    );
+    if ($in_order) {
+      $this->assertSame($project_titles, $list_visible_projects());
+    }
+  }
+
+  /**
+   * Returns the currently selected category names.
+   *
+   * @return string[]
+   *   The names of the currently selected categories, in the order they appear
+   *   as lozenges in the search area.
+   */
+  private function getSelectedCategories(): array {
+    $elements = $this->getSession()
+      ->getPage()
+      ->findAll('css', 'p.filter-applied .filter-applied__label');
+
+    return array_map(fn (NodeElement $element) => $element->getText(), $elements);
+  }
+
 }
diff --git a/tests/src/FunctionalJavascript/ProjectBrowserUiTestTrait.php b/tests/src/FunctionalJavascript/ProjectBrowserUiTestTrait.php
index 4b0488eb4..d24f35955 100644
--- a/tests/src/FunctionalJavascript/ProjectBrowserUiTestTrait.php
+++ b/tests/src/FunctionalJavascript/ProjectBrowserUiTestTrait.php
@@ -123,44 +123,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