Problem/Motivation

Drupal's JavaScript tests can fail when the test runner has enough resources to be faster than expected. This is often seen when a link is expected to be clickable but it is not or a field value should be settable but it is not.

For example, if you run Drupal's JS tests on DrupalCI with a concurrency of one it will fail due to this. Also the Layout Builder tests are prone to failing locally due to this.

Follow up to #2946294: Fix race conditions in OffCanvasTestBase

This was solving for the random test failure the produces and error like

1) Drupal\Tests\system\FunctionalJavascript\OffCanvasTest::testOffCanvasLinks with data set "stark" ('stark')
WebDriver\Exception\UnknownError: unknown error: Element ... is not clickable at point (516, 87). Other element would receive the click: ...
(Session info: headless chrome=62.0.3202.94)
(Driver info: chromedriver=2.33.506092 (733a02544d189eeb751fe0d7ddca79a0ee28cce4),platform=Linux 4.9.0-0.bpo.6-amd64 x86_64)

/var/www/html/vendor/instaclick/php-webdriver/lib/WebDriver/Exception.php:155
/var/www/html/vendor/instaclick/php-webdriver/lib/WebDriver/AbstractWebDriver.php:157
/var/www/html/vendor/instaclick/php-webdriver/lib/WebDriver/AbstractWebDriver.php:218
/var/www/html/vendor/instaclick/php-webdriver/lib/WebDriver/Container.php:224
/var/www/html/vendor/behat/mink-selenium2-driver/src/Selenium2Driver.php:775
/var/www/html/vendor/behat/mink-selenium2-driver/src/Selenium2Driver.php:763
/var/www/html/vendor/behat/mink/src/Element/NodeElement.php:153
/var/www/html/vendor/behat/mink/src/Element/TraversableElement.php:73
/var/www/html/core/modules/system/tests/src/FunctionalJavascript/OffCanvasTest.php:99

Example

This seems to be a problem where the test browser is trying to click an element but other element is moving on the page and the element to be clicked is obstructed.

Proposed resolution

Generalize the solution in #2946294: Fix race conditions in OffCanvasTestBase

Option 1: Override existing methods of clicking

If would be nice if
$this->getSession()->getPage()->clickLink()
and
$this->getSession()->getPage()->fillField()
were fault tolerant for JS tests.

Therefore we extend \Drupal\FunctionalJavascriptTests\DrupalSelenium2Driver to add this to the underlying driver methods.

Option 2: Provide a new way to click elements

Add new method on WebDriverTestBase something like safeClick

Remaining tasks

Open follow-ups or find existing issues for the tests that are still failing regularly with a concurrency of one:

  • Drupal\Tests\layout_builder\FunctionalJavascript\ContentPreviewToggleTest
  • Drupal\Tests\layout_builder\FunctionalJavascript\LayoutBuilderTest
  • Drupal\Tests\layout_builder\FunctionalJavascript\AjaxBlockTest

User interface changes

None

API changes

None

Data model changes

None

Release notes snippet

N/a

CommentFileSizeAuthor
#86 3032275-86.patch19 KBalexpott
#86 81-86-interdiff.txt1.23 KBalexpott
#82 3032275-82.full-local-phpunit-output.txt5.06 KBdww
#81 3032275-81.patch18.86 KBalexpott
#81 78-81-interdiff.txt11.78 KBalexpott
#78 3032275.77_78.interdiff.txt8.27 KBdww
#78 3032275-78.patch17.27 KBdww
#77 3032275.74_77.interdiff.txt3.59 KBdww
#77 3032275-77.patch17.09 KBdww
#74 3032275-74.patch17.33 KBalexpott
#74 73-74-interdiff.txt1.72 KBalexpott
#73 3032275.71_73.interdiff.txt1.26 KBdww
#73 3032275-73.patch18.32 KBdww
#72 3032275-71.patch18.4 KBalexpott
#72 69-71-interdiff.txt756 bytesalexpott
#69 3032275-69.patch18.33 KBalexpott
#69 3032275-69-concurrency-one.patch20.52 KBalexpott
#69 3246891-4.test-only.patch2.19 KBalexpott
#69 67-69-interdiff.txt2.36 KBalexpott
#67 3032275-67.patch17.87 KBalexpott
#67 3032275-67-concurrency-one.patch20.06 KBalexpott
#67 64-67-interdiff.txt1.78 KBalexpott
#64 3032275-64.patch17.6 KBalexpott
#64 3032275-64-concurrency-one.patch19.79 KBalexpott
#64 62-64-interdiff.txt2.08 KBalexpott
#62 3032275-62.patch15.52 KBalexpott
#62 3032275-62-concurrency-one.patch17.71 KBalexpott
#62 58-62-interdiff.txt11.03 KBalexpott
#58 3032275-58.patch20.01 KBalexpott
#58 3032275-58-concurrency-one.patch22.21 KBalexpott
#58 57-58-interdiff.txt2.13 KBalexpott
#57 3032275-57.patch19.6 KBalexpott
#57 3032275-57-concurrency-one.patch21.8 KBalexpott
#57 53-57-interdiff.txt3.76 KBalexpott
#53 3032275-53-concurrency-one.patch21.78 KBalexpott
#53 3246891-4.test-only.patch2.19 KBalexpott
#53 3032275-53.patch19.59 KBalexpott
#53 49-53-interdiff.txt10.83 KBalexpott
#49 3032275-49.patch14.35 KBtedbow
#44 3032275.29_44.interdiff.txt851 bytesdww
#44 3032275-44.patch14.23 KBdww
#44 3032275-44.test-only.patch7.06 KBdww
#42 3032275-42.test-only-100.patch9.19 KBdww
#42 3032275-42.test-only-50.patch9.19 KBdww
#42 3032275-42.test-only-10.patch9.19 KBdww
#39 3032275-39.test-only-400.patch9.17 KBdww
#39 3032275-39.test-only-300.patch9.17 KBdww
#39 3032275-39.test-only-200.patch9.17 KBdww
#33 3032275-33.test-only-2000.patch7.04 KBdww
#33 3032275-33.test-only-1500.patch7.04 KBdww
#33 3032275-33.test-only-1000.patch7.04 KBdww
#33 3032275-33.test-only-500.patch7.04 KBdww
#31 3032275-29-testonly.patch7.04 KBbendeguz.csirmaz
#29 3032275-29.patch14.2 KBbendeguz.csirmaz
#29 interdiff-3032275-26-29.txt2.4 KBbendeguz.csirmaz
#27 3032275.21_26.interdiff.txt8.95 KBdww
#26 3032275-26.patch14.29 KBbendeguz.csirmaz
#21 3032275-21-testonly.patch4.14 KBbendeguz.csirmaz
#21 3032275-21.patch11.23 KBbendeguz.csirmaz
#17 3032275-17-testonly.patch4.07 KBbendeguz.csirmaz
#17 3032275-17.patch11.17 KBbendeguz.csirmaz
#4 3032275-4.patch6.79 KBtedbow
#4 3032275-4_x30_offcanvas.patch8.68 KBtedbow
#11 interdiff-3032275-4-11.txt2.25 KBbendeguz.csirmaz
#11 3032275-11.patch7.1 KBbendeguz.csirmaz
#14 3032275-14.patch11.15 KBbendeguz.csirmaz
#14 3032275-14-testonly.patch4.05 KBbendeguz.csirmaz
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

tedbow created an issue. See original summary.

dww’s picture

Component: other » phpunit
Issue tags: +Random test failure

I'm torn. Overriding the existing methods (option 1) is sort of a mess, since we'd have to intercept a lot of possible choices, and that probably gets ugly quickly. I'm also not exactly clear on the mechanism to try to intercept those methods, but I haven't looked that closely.

However, expecting test writers to know when to use clickLink() vs. safeClick() (option 2) seems somewhat unreasonable. Yes, it'd be great to have a generic solution, but it's not going to do much good if people don't know it exists and know when to use it. I'm barely getting started with FunctionalJS tests, and I sure as hell don't feel qualified to understand when a given command is likely to introduce a race condition or not. It's mostly all magic to me at this point. Maybe I (and others) would simply adopt the "always use the 'safe' option" approach, and maybe that's fine.

But yes, we clearly have a general race condition problem in our JS tests, and I'm glad we now have an issue to try to track it. Moving to the phpunit component (which seems more appropriate) and tagging for 'Random test failure' (since that's the class of issues this one is trying to solve.

xjm’s picture

Title: Create a fault tolerant method for clicking links in Javascript tests. » Create a fault-tolerant method for clicking links in Javascript tests.
tedbow’s picture

I agree that option 1 could be a mess but also should every click be a safeClick 😜

So here is a try at Option 1. If this actually works it is not too bad.

  1. I wrote a test prove that all the cases of getPage, find and findAll return our custom elements.
  2. It seems for making sure we are always dealing with our elements we just have to override findAll() because all other find* methods end up there.
  3. I think pretty much all click* methods ultimitally use \Behat\Mink\Element\NodeElement::click() so I think we just have to override that.
  4. I am including a x30 for OffCanvasTest to prove it would fix that test.

Status: Needs review » Needs work

The last submitted patch, 4: 3032275-4_x30_offcanvas.patch, failed testing. View results

tedbow’s picture

Status: Needs work » Needs review

\Drupal\Tests\UiHelperTrait::click()
\Drupal\Tests\content_translation\Functional\ContentTranslationRevisionTranslationDeletionTest::doTestOverview(in a call)

Both call $this->getSession()->getDriver()->click()

We should probably change \Drupal\Tests\UiHelperTrait::click() to use $this->getSession()->getPage()->find('css', $css_selector)->click()
I am not sure why it is not doing so now.

Krzysztof Domański’s picture

A similar problem in the Media and Media Library.

https://www.drupal.org/pift-ci-job/1196544

1) Drupal\Tests\media_library\FunctionalJavascript\MediaLibraryTest::testWidgetUpload
WebDriver\Exception\UnknownError: unknown error: Element ... is not clickable at point (628, 434). Other element would receive the click: ...

https://www.drupal.org/pift-ci-job/1190550

1) Drupal\Tests\media\FunctionalJavascript\MediaStandardProfileTest::testMediaSources
WebDriver\Exception\UnknownError: unknown error: Element  is not clickable at point (44, 588). Other element would receive the click: ...
Krzysztof Domański’s picture

Priority: Normal » Major

Many random test failure.

Lendude’s picture

Pretty cool that this works, but....

The issue I see here, is that this is just solving one very small fraction of possible causes of these random fails. There are an X number of reasons you need to wait during a javascript test (and even more for Nightwatch tests, which doesn't even pause on page loads). So are we going to try to catch ALL the reasons that can cause this, or are people going to need to learn that you need to waitForX a lot when writing these types of tests?
Annoying? Maybe, but it's the reality of writing javascript tests and catching one (or some) of the causes of this will not make them clearer to use or understand I think.

We don't want to get to a state where we are just debouncing EVERYTHING by 100ms like what was done in the MinkPhantomJSDriver (see https://github.com/jcalderonzumba/MinkPhantomJSDriver/pull/68). Taking away the 100ms led to a number of fails in our tests IIRC, so having that in was a solution to some of our random fails (just not a very good one I feel).

tedbow’s picture

@Lendude I understand your concerns here

So are we going to try to catch ALL the reasons that can cause this, or are people going to need to learn that you need to waitForX a lot when writing these types of tests?
Annoying? Maybe, but it's the reality of writing javascript tests and catching one (or some) of the causes of this will not make them clearer to use or understand I think.

But if you look at this patch I was testing with here with a issue that is having this type of random fail

   $this->waitForNoElement('#drupal-off-canvas');
    $assert_session->assertWaitOnAjaxRequest();
    $assert_session->linkExists('Save Layout');
    $this->assertNotEmpty($assert_session->waitForElementVisible('css', "a:contains('Save Layout')"));
    $page->clickLink('Save Layout');

Here I am waiting for just about everything I can think of. The line directly before the clickLink() checks if the link is visible.
But still this randomly fails here.

With the fix here this line does not randomly fail.

bendeguz.csirmaz’s picture

I just corrected some nitpicks:
- Fixed coding standards errors, except in the test. I think it doesn't need a doc comment (I couldn't come up with one).
- Minor modification to the JavascriptSession to be more similar to the parent Session class.

How do you test if it is working though?

tedbow’s picture

@bendeguz.csirmaz thanks!

Yeah testing is tricky.

Here is idea I had.

  1. Create a test page that will have a link, the target link, but have another element, the blocker element, obstruct that link.
  2. Create another link, the trigger link, on the page that when clicked will remove the blocker element after a 2 second wait.

Without the fix if you did this

$this->clickLink('Trigger Link');
$this->clickLink('Target Link');

It should fail because the Target Link would be obstructed by the Blocker element

With the fix the same code should pass because JavascriptNodeElement::click will try to click repeatedly 10 seconds to click until it is clickable.

Of course this assumes that $this->clickLink('Trigger Link'); will get the error JavascriptNodeElement::click was made to handle. but this should prove it.

andypost’s picture

bendeguz.csirmaz’s picture

#12
Added the test.
I'm not sure if creating a new hidden testing module in the modules folder is okay?

The last submitted patch, 14: 3032275-14-testonly.patch, failed testing. View results

The last submitted patch, 14: 3032275-14.patch, failed testing. View results

bendeguz.csirmaz’s picture

Adding @group annotation to the test.

Status: Needs review » Needs work

The last submitted patch, 17: 3032275-17-testonly.patch, failed testing. View results

bendeguz.csirmaz’s picture

#14
Apparently it's not okay: ComposerIntegrationTest fails.

tedbow’s picture

  1. --- /dev/null
    +++ b/core/modules/testing_js_click/src/Controller/JSClickTestController.php
    

    Test modules should live under /core/[module_name]/tests/modules/

    Since this is functionality not for a particular module it should be under /core/system/tests/modules/

    You can look at anyone of the test modules there like core/modules/system/tests/modules/cache_test for examples.

  2. +++ b/core/modules/testing_js_click/testing_js_click.info.yml
    @@ -0,0 +1,7 @@
    +hidden: true
    

    Once you move the module it does not need to be hidden. It should not have the hidden key

bendeguz.csirmaz’s picture

Status: Needs work » Needs review
FileSize
11.23 KB
4.14 KB

Thanks! Here's the fixed version.

Status: Needs review » Needs work

The last submitted patch, 21: 3032275-21-testonly.patch, failed testing. View results

bendeguz.csirmaz’s picture

Status: Needs work » Needs review
tedbow’s picture

Status: Needs review » Needs work
  1. +++ b/core/modules/testing_js_click/src/Controller/JSClickTestController.php
    @@ -0,0 +1,69 @@
    +    $onclick = <<<'JS'
    +event.preventDefault();
    +setTimeout(function () {
    +  document.getElementById('blocker-element').remove();
    +}, 2000);
    

    I think we should just use regular JS file in the test module rather than include JS from the server-side. It is little more work but I think it is clearer what is going on.

  2. +++ b/core/modules/testing_js_click/src/Controller/JSClickTestController.php
    @@ -0,0 +1,69 @@
    +          'style' => implode([
    +            // Resize the box to cover the target link.
    +            'width: 100px;',
    +            'height: 40px;',
    +            // Position the box over the target link.
    +            'position: relative;',
    +            'top: -30px;',
    +            'left: -5px;',
    +            // Make the blocker element visible.
    +            'background-color: black;',
    +            'opacity: 0.5;',
    +          ]),
    

    Likewise I think we should just have CSS files in the test module.

dww’s picture

+1 to the points in #24. When I was looking at #21 I thought the same thing.

Some additional nits and concerns:

  1. +++ b/core/modules/system/tests/modules/js_click_test/js_click_test.info.yml
    @@ -0,0 +1,6 @@
    +description: 'Module for testing fault tolerant clicks.'
    

    fault-tolerant

    Also, maybe we should say "... in JavaScript tests" or something? we've got JS in the name, maybe worth being more explicit in this description, too?

  2. +++ b/core/modules/system/tests/modules/js_click_test/src/Controller/JSClickTestController.php
    @@ -0,0 +1,69 @@
    +   * after a 2 second wait.
    

    Should this 2 second wait be a route parameter or something?

  3. +++ b/core/modules/system/tests/modules/js_click_test/src/Controller/JSClickTestController.php
    @@ -0,0 +1,69 @@
    +            // Resize the box to cover the target link.
    +            'width: 100px;',
    +            'height: 40px;',
    +            // Position the box over the target link.
    +            'position: relative;',
    +            'top: -30px;',
    +            'left: -5px;',
    

    (In the new CSS file, not here, but...) don't we also want to set a z-index to force this to be in front of the target_link?

  4. +++ b/core/tests/Drupal/FunctionalJavascriptTests/FindJavascriptElementsTrait.php
    @@ -0,0 +1,29 @@
    + * Trait with finding extending NodeElements.
    

    Not sure what this means. Can we more clearly word what this trait is for? "finding extending" isn't proper, regardless.

  5. +++ b/core/tests/Drupal/FunctionalJavascriptTests/FindJavascriptElementsTrait.php
    @@ -0,0 +1,29 @@
    +  /**
    +   * The session.
    +   *
    +   * @var \Behat\Mink\Session
    +   */
    +  protected $elementSession = NULL;
    

    Unless I'm missing something, I don't see this actually being used anywhere, other than passing it around for the new constructors. Can't we remove this entirely, and majorly simplify lots of this?

  6. +++ b/core/tests/Drupal/FunctionalJavascriptTests/JavascriptNodeElement.php
    @@ -0,0 +1,49 @@
    + * Extends NodeElement to provide fault tolerant clicking.
    

    fault-tolerant.

  7. +++ b/core/tests/Drupal/FunctionalJavascriptTests/JavascriptSession.php
    @@ -0,0 +1,39 @@
    +  private $page = NULL;
    

    protected instead of private?

  8. +++ b/core/tests/Drupal/FunctionalJavascriptTests/Tests/ExtendedSessionTest.php
    @@ -0,0 +1,43 @@
    + * Tests that session is extended and all find methods return extend elements.
    

    "return extend elements" doesn't make sense. Does this mean "return extended elements."? What does "extended" mean?

bendeguz.csirmaz’s picture

Status: Needs work » Needs review
FileSize
14.29 KB

Okay, fixed everything, thanks!
The reason I added that inline code is because I thought keeping the test in one file would be simpler.

#25.5
I'm not sure how this could be simplified.

dww’s picture

FileSize
8.95 KB

Thanks, that's much improved! Attaching the interdiff for other folks to review, too.

Re: #25.5: Indeed, I was missing something. ;) I didn't notice that we pass $session to the parent constructor (I was searching for "elementSession" not "session") and I now understand why that's needed. ;) Please ignore.

Two tiny remaining nits:

  1. You added 2 new libraries for this, one for JS the other for CSS. Seems like a bit of overkill, and they could be combined into a single library. Not going to NW for this, but if you want to re-roll (or if anything else of substance turns up), maybe this could be simplified.
  2. Now that it's a URL param (thanks!) I wonder if we really need these tests to wait 2 seconds each. ;) Maybe 1000 is a better value? Just trying to look out for the throughput of the test bots, since every delay is compounded a lot by how often these tests are going to be invoked.

Thanks!
-Derek

tedbow’s picture

Status: Needs review » Needs work

I think @dww 2 points #27 in are worth fixing

bendeguz.csirmaz’s picture

Status: Needs work » Needs review
FileSize
2.4 KB
14.2 KB

Adding the fix

dww’s picture

Status: Needs review » Reviewed & tested by the community

Did you mean 100 vs 1000? If 100 is still enough to "see" the bug, that's great. Just double checking.

Otherwise, I have no more concerns. RTBC to my eyes.

Thanks!
-Derek

bendeguz.csirmaz’s picture

I think it's enough.
Uploading the testonly to prove it :)
(If it fails it can go back to RTBC)

dww’s picture

Status: Reviewed & tested by the community » Needs work

Hrm, bummer, that passed. ;)

Can we see maybe 3 test-only versions @ 500, 1000 and 2000 and then decide?

Sorry for the hassles/churn!

Thanks,
-Derek

dww’s picture

dww’s picture

Assigned: dww » Unassigned

Whoops, didn't actually mean to assign to myself.

p.s. Now that I applied locally, I saw that the test is at:
core/tests/Drupal/FunctionalJavascriptTests/Tests/JSClickTest.php
while the module it depends on is in:
core/modules/system/tests/modules/js_click_test
I definitely don't grok how we decide what tests live in system vs the core/tests/Drupal world, but I wonder if the test class should move to:
core/modules/system/tests/modules/js_click_test
core/modules/system/tests/src/FunctionalJavascript/JSClickTest.php
?

I guess this is a test for the testing framework itself, not a test for something in Drupal, so the current location makes sense. But I wanted to ask.

Thanks,
-Derek

dww’s picture

p.p.s. I regret rushing to upload the patches in #33 without also including a change to drupalci.yml to only run this test. :/ Whoops!!

The last submitted patch, 33: 3032275-33.test-only-1000.patch, failed testing. View results

The last submitted patch, 33: 3032275-33.test-only-500.patch, failed testing. View results

Status: Needs review » Needs work

The last submitted patch, 33: 3032275-33.test-only-2000.patch, failed testing. View results

dww’s picture

Cool, that's more like it. ;) Let's see about a sweet spot between 100 and 500, then.
This time, with a change to drupalci.yml to only run this particular test, but to repeat it 20 times.

The last submitted patch, 39: 3032275-39.test-only-200.patch, failed testing. View results

Status: Needs review » Needs work

The last submitted patch, 39: 3032275-39.test-only-400.patch, failed testing. View results

dww’s picture

Hrm, what about 100ms x 20 repeats? Also 50 and 10 for good measure. ;)

Meanwhile, I noticed the bot complaining about this:

var/lib/drupalci/workspace/jenkins-drupal_patches-86345/source/core/tests/Drupal/FunctionalJavascriptTests/Tests/JSClickTest.php ✗ 1 more
24	@expectedException tags should not be used, use $this->setExpectedException() or $this->expectException() instead

So I fixed that (I think). Let's see what the bot says now.

(sorry for the noise, but I think this is important to find a reasonable value for this).

Status: Needs review » Needs work

The last submitted patch, 42: 3032275-42.test-only-100.patch, failed testing. View results

dww’s picture

Hah! ;) So I guess 100 is about right, after all. LOL ;) Maybe we should bump to 150 or so. Committers could easily change the value on commit based on all these test results. But here's a full patch with the fix to using setExpectedException(), back at 100ms. Probably RTBC but I'll let @tedbow (or someone) do that.

Thanks,
-Derek

The last submitted patch, 44: 3032275-44.test-only.patch, failed testing. View results

dww’s picture

Nice! Those are the test results we were hoping for. ;)

I obviously can no longer RTBC this, but I believe it's ready for that.

Note to committers: please list @tedbow and @bendeguz.csirmaz ahead of me in the commit message. I'm only at the front of the default list as a result of uploading all these test-only patches to find/confirm the right value. Those two actually wrote all the code that matters in here.

Thanks,
-Derek

bendeguz.csirmaz’s picture

Thanks for doing this!
Since 100 randomly failed once I think it should be a greater value to be safe.

dww’s picture

Well, 100 randomly *passed* for the test-only. ;) To consistently see the test-only fail, we need more than 100, but that's not really the point. We just wanted to make sure this fix is working, and it is.

tedbow’s picture

Just a reroll

alexpott’s picture

Status: Needs review » Needs work
+++ b/core/tests/Drupal/FunctionalJavascriptTests/JavascriptNodeElement.php
@@ -0,0 +1,49 @@
+      catch (UnknownError $exception) {

We've discovered that this Exception class is not reliable in #3056536: LayoutBuilderDisableInteractionsTest randomly fails. It needs to be \Exception because on MacOS connecting directly to chromedriver that's the exception that's thrown. On Linux and via selenium-server it's a UnknownError but this will fail on many people's local testing environments. As you're checking the error message anyway I think you'll get the behaviour you want with catch (\Exception $e)

Version: 8.6.x-dev » 8.8.x-dev

Drupal 8.6.x will not receive any further development aside from security fixes. Bug reports should be targeted against the 8.8.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.9.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.7 was released on June 3, 2020 and is the final full bugfix release for the Drupal 8.8.x series. Drupal 8.8.x will not receive any further development aside from security fixes. Sites should prepare to update to Drupal 8.9.0 or Drupal 9.0.0 for ongoing support.

Bug reports should be targeted against the 8.9.x-dev branch from now on, and new development or disruptive changes should be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

alexpott’s picture

Making this critical because atm you cannot run layout builder tests locally without it failing if you have a fast machine. Plus DrupalCI can't run JS tests with a concurrency of 1 without failing. Going to mark #3246891: Layout Builder JS tests are failing locally and on DrupalCI with a concurrency of 1 as a duplicate.

alexpott’s picture

Title: Create a fault-tolerant method for clicking links in Javascript tests. » Create a fault-tolerant method for interacting with links and fields in Javascript tests.

The patch in #53 also adds protection for NodeElement::setValue() as that was also causing not interactable errors for Layout Builder tests.

The last submitted patch, 53: 3032275-53.patch, failed testing. View results

The last submitted patch, 53: 3246891-4.test-only.patch, failed testing. View results

alexpott’s picture

Fixing the tests and removing usage of the deprecated jQuery.once library.

alexpott’s picture

The last submitted patch, 57: 3032275-57-concurrency-one.patch, failed testing. View results

The last submitted patch, 58: 3032275-58-concurrency-one.patch, failed testing. View results

The last submitted patch, 58: 3032275-58-concurrency-one.patch, failed testing. View results

alexpott’s picture

Looking at this change we can move all the hackery of the wait into something we're already overriding - the DrupalSelenium2Driver.

The last submitted patch, 62: 3032275-62-concurrency-one.patch, failed testing. View results

alexpott’s picture

Hopefully this will fix the remaining errors.

alexpott’s picture

Issue summary: View changes

Updated issue summary with new information about how to cause the problem and the nature of the fix.

The last submitted patch, 64: 3032275-64-concurrency-one.patch, failed testing. View results

alexpott’s picture

Locally I can get the AjaxBlockTest is I use sqlite. The attached patch fixes this.

Status: Needs review » Needs work

The last submitted patch, 67: 3032275-67.patch, failed testing. View results

alexpott’s picture

Okay I'm going to stop trying to fix:

  • Drupal\Tests\layout_builder\FunctionalJavascript\ContentPreviewToggleTest
  • Drupal\Tests\layout_builder\FunctionalJavascript\LayoutBuilderTest
  • Drupal\Tests\layout_builder\FunctionalJavascript\AjaxBlockTest

We should file follow-ups to explore fixing those. The current approach here of making link clicking and field value setting more resilient reduces the number of random JS errors considering as we can see from the attached patches.

I've also added tests for the form value setting and renamed things in the tests so they are not so click focussed and I removed the dependency on jQuery because why not.

Here's what the different patches attached show:

  • 3246891-4.test-only.patch: this is how bad things are on HEAD when JS tests are run with a concurrency of one - should have the most fails.
  • 3032275-69-concurrency-one.patch: this shows how this change improves under the concurrency of one condition
alexpott’s picture

Issue summary: View changes

Status: Needs review » Needs work

The last submitted patch, 69: 3032275-69.patch, failed testing. View results

alexpott’s picture

Status: Needs work » Needs review
FileSize
756 bytes
18.4 KB

Yay another difference between a modern chromedriver and the one we currently test on using DrupalCI - we need to update it sometime.

dww’s picture

FileSize
18.32 KB
1.26 KB

@alexpott: Thanks for resurrecting this issue and moving it so far forward!

A few trivial docs nits fixed with attached patch. Then a few other comments / concerns. Only #3 prevents me from recommending RTBC, but I'd love feedback on the others.

  1. +++ b/core/tests/Drupal/FunctionalJavascriptTests/DrupalSelenium2Driver.php
    @@ -134,4 +137,96 @@ public function uploadFileAndGetRemoteFilePath($path) {
    +  public function click($xpath) {
    

    #2946294: Fix race conditions in OffCanvasTestBase makes me wonder if we also want to override Behat\Mink\Element\TraversableElement::clickLink(), too. But that'd require undoing much of the goodness / simplification from #62...

  2. +++ b/core/tests/Drupal/FunctionalJavascriptTests/DrupalSelenium2Driver.php
    @@ -134,4 +137,96 @@ public function uploadFileAndGetRemoteFilePath($path) {
    +   * Waits for an element(-s) to appear and returns it.
    +   *
    +   * @param int|float $timeout
    +   *   Maximal allowed waiting time in seconds.
    +   * @param callable $callback
    +   *   Callback, which result is both used as waiting condition and returned.
    +   *   Will receive reference to `this driver` as first argument.
    

    These docs are a bit clumsy and hard to follow.

    A) "element(-s)" vs. "it" - if we're going to handle single or plural, "it" should probably be "it/them".

    B) "which result is both used as a waiting condition and returned" is confusing. And maybe now wrong? Looks like the callback is expected to return TRUE if it found the thing, but we don't actually care about or return the underlying result beyond that.

    That said, this is a private helper in a dark corner of the test plumbing, so maybe it doesn't matter. ;)

  3. +++ b/core/tests/Drupal/FunctionalJavascriptTests/JavascriptSession.php
    @@ -0,0 +1,39 @@
    +class JavascriptSession extends Session {
    

    It appears that we removed the code that was registering this in WebDriverTestBase. It seems this is no longer needed. If so, we should remove it.

    Looks like how it was registered was changed in #53, but then completely removed in #62.

Thanks again!
-Derek

alexpott’s picture

Thanks for the review @dww

Re #73.1 All clicks will be more fault tolerant now. If they are being added by ajax you're going to need wait otherwise the find will fail but I think that that is okay.

#73.2 and they are copied from upstream docs. I've improved them a bit. Re part B the "which result is both used as a waiting condition and returned" is correct. The callback is expected to return something truthy to end the waiting and something falsey to continue to wait.

#73.3 yep this is dead code now.

dww’s picture

Thanks, @alexpott!

1. Great! After I posted 73, I was wondering if that’d be the case, but I didn’t have time to dig in and investigate before bed. thanks for confirmation.

2. New docs look good. Truthy or not is the key point, which it now says. Thanks.

3. Yay, glad to see it go.

I have nothing other than too many patches uploaded here stopping me from RTBC. 😉 hopefully @tedbow will RTBC soon (he mentioned in Slack he was planning to review this today).

Thanks again,
-Derek

tedbow’s picture

Status: Needs review » Needs work

Excited this getting fixed! Just a few things

  1. +++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/BlockFormMessagesTest.php
    @@ -106,34 +104,4 @@ protected function assertMessagesDisplayed() {
    -  protected function clickElementWhenClickable(NodeElement $element, $timeout = 10000) {
    

    Bittersweet to see this go 😢. Probably one of the great function names in Drupal core history 😂

  2. +++ b/core/modules/system/tests/modules/js_interaction_test/css/js-interaction-test-blocker-element.css
    @@ -0,0 +1,13 @@
    +  /* Resize the box to cover the target. */
    

    is "Resize" correct here? I feel this test is already a bit tricky to understand. We are actively removing an element and this first made wonder if we were actively resizing.

    could we just say
    /* Size the box to cover the target. */

  3. +++ b/core/modules/system/tests/modules/js_interaction_test/js_interaction_test.routing.yml
    @@ -0,0 +1,7 @@
    +  path: '/js_interaction_test/{milliseconds}'
    

    Whats the point of having {milliseconds}? We always use /js_interaction_test/100 to access the link in the test. Couldn't we either hard-code this in \Drupal\js_interaction_test\Controller\JSInteractionTestForm::buildForm()?
    or if we just hard-coded in js_interaction_test.trigger_link.es6.js we won't need to use the core/drupalSettings library at all.

  4. +++ b/core/modules/system/tests/modules/js_interaction_test/src/Controller/JSInteractionTestForm.php
    @@ -0,0 +1,97 @@
    +      'trigger_link' => [
    +        '#type' => 'link',
    +        '#url' => Url::fromRoute('<current>'),
    +        '#title' => $this->t('Trigger link'),
    

    To me the combo of this form and the JS in js_interaction_test.trigger_link.es6.js makes this test a bit hard to understand. I didn't read any of the comments first to see if the test is understandable on its own since I hadn't thought about this issue in long time.

    In the comment we describe this link as
    A link that, when clicked, removes the obstructing element after some time.

    But then we call it here "Trigger Link". To me it is a little to hard to connect the 2.

    Could just call this link "Blocker remover" or even "Link Blocker remover". I don't see any downside to be overly explicit. We could then change the class to blocker-remover-link

    Think the JS would more understandable

    const blockerRemoverLink = once('blocker-remover-link', '.blocker-remover-link').shift();
          blockerRemoverLink.addEventListener('click', (event) => {
            event.preventDefault();
            setTimeout(() => {
              document.querySelector('.blocker-element').remove();
            }, triggerInteractivity.milliseconds);
          });
    

    or something

    We could also update the comment to match
    " removes the blocking element after some time."

  5. +++ b/core/modules/system/tests/modules/js_interaction_test/src/Controller/JSInteractionTestForm.php
    @@ -0,0 +1,97 @@
    +        '#title' => $this->t('Trigger field'),
    +        '#attributes' => [
    +          'class' => ['trigger-field'],
    

    To me this would more understandable as "Field enabler" with the class field-enabler

  6. +++ b/core/tests/Drupal/FunctionalJavascriptTests/DrupalSelenium2Driver.php
    @@ -134,4 +137,97 @@ public function uploadFileAndGetRemoteFilePath($path) {
    +  public static function isExceptionNotClickable(Exception $exception): bool {
    +    return (bool) preg_match('/not (clickable|interactable|visible)/', $exception->getMessage());
    +  }
    

    Do we really want to make this a public method instead of private or protected?

    This seems like it could cause some BC headaches. For instance what if Selenium2Driver itself solves the current problem we are having themselves so that we no longer of have to have our overrides in this class. We won't need this method any longer but we couldn't remove it either because others might be calling it. We could then deprecate it but then it is just one more thing for people take care when they move to the next major version of Drupal.

  7. +++ b/core/tests/Drupal/FunctionalJavascriptTests/Tests/JSInteractionTest.php
    @@ -0,0 +1,60 @@
    +   * Assert an exception is thrown when blocker element is never removed.
    

    For the field we don't have actually use block element, correct? We just enable it after a delay

dww’s picture

Thanks for the review, @tedbow! All good points.

But first, I actually tried running the test locally while I was working on the proposed changes. Discovered we never fully addressed #50. The test was dying for me (MacOS chrome + chromedriver 94.0.4606.81) like so:

There were 2 failures:

1) Drupal\FunctionalJavascriptTests\Tests\JSInteractionTest::testNotClickable
Failed asserting that exception of type "Exception" matches expected exception "WebDriver\Exception". Message was: "element click intercepted: Element <a href="/drupal-9_1/js_interaction_test/100" data-drupal-selector="edit-target-link" id="edit-target-link">...</a> is not clickable at point (42, 17). Other element would receive the click: <div class="blocker-element" data-drupal-selector="edit-blocker-element"></div>
  (Session info: chrome=95.0.4638.69)
  (Driver info: chromedriver=94.0.4606.61 (418b78f5838ed0b1c69bb4e51ea0252171854915-refs/branch-heads/4606@{#1204}),platform=Mac OS X 10.15.7 x86_64)" at

I had to change both core/tests/Drupal/FunctionalJavascriptTests/DrupalSelenium2Driver.php and core/tests/Drupal/FunctionalJavascriptTests/Tests/JSInteractionTest.php to use \Exception not WebDriver\Exception. Hope that's cool.

Uploading that here separately, before I work on #76 so there's a separate patch / interdiff in case we want to change course on this.

dww’s picture

Title: Create a fault-tolerant method for interacting with links and fields in Javascript tests. » Create a fault-tolerant method for interacting with links and fields in Javascript tests
Issue summary: View changes
Status: Needs work » Needs review
FileSize
17.27 KB
8.27 KB

This fixes most of #76. I'm leaving point 3 for @alexpott to decide. Would like confirmation on 6, too.

  1. 👍😉
  2. Simplified to "Size".
  3. Leaving for Alex. @todo (maybe)
  4. I'm pretty used to core JS tests with "target" and "trigger" terminology. But I see your point. Per Slack, I think verb-first is more clear, so I went with "Remove Blocker Trigger"). Updated everything to match, test is still passing locally. Hopefully easier to read / understand.
  5. Went with "Enable Field Trigger".
  6. Seems like an excellent reason to be passing $this into that callback (which we're already doing). We don't need this to be static (and therefore public). Agreed that less API is better here. If we have some public need for this (hard to imagine), we can change it then.
  7. Changed to "Assert an exception is thrown when the field is never enabled."
alexpott’s picture

@dww re #77 I would love to understand why you're not getting a typed exception. Imo the change to \Exception is not correct and we need to work out what it is about the way you have things set up that results in an un-typed exception. This is because there are other tests that capture even more specific exceptions that would be broken.

alexpott’s picture

Status: Needs review » Needs work
+++ b/core/tests/Drupal/FunctionalJavascriptTests/DrupalSelenium2Driver.php
@@ -225,7 +225,7 @@
-  public static function isExceptionNotClickable(\Exception $exception): bool {
+  private function isExceptionNotClickable(\Exception $exception): bool {

We should move this elsewhere. And leave it as a public method. There are other places in the codebase that need this. Plus this should stay as static wherever it goes because it has no need for $this.

alexpott’s picture

Status: Needs work » Needs review
FileSize
11.78 KB
18.86 KB

@dww I use OSX and chromedriver and \WebDriver\Exception is what I'm getting. Can you provide a full backtrace for your exception so we can see what code is converting it to \Exception - I can not see where that is coming from and rather than converting this to the catch all I'd like to understand it. (Therefore the patch here reverts this change).

Fixed up the wait to be hard-coded - I agree that having this as a setting is OTT.

Also move the exception message tester to JsWebAssert - couldn't think of a better place and used it in the other place where it needs to be.

dww’s picture

Yeah, I don't think #77 is the right approach. Happy to see it reverted. But for reference, attaching the full phpunit output. Here's what's going on for me locally that's throwing \Exception in my case:

vendor/instaclick/php-webdriver/lib/WebDriver/Exception.php

    public static function factory($code, $message = null, $previousException = null)
    {
        // unknown error
        if (!isset(self::$errs[$code])) {
            if (trim($message) === '') {
                $message = 'Unknown Error';
            }

            return new \Exception($message, $code, $previousException);
        }
        ....
    }

I'm getting "element not interactable" but that's not in the list of exceptions that class understands. My (unmodified) composer.lock says:

            "name": "instaclick/php-webdriver",
            "version": "1.4.10",

Maybe it's a mismatch between chrome (95.0.4638.69) and chromedriver (94.0.4606.61)? Sorry, don't have time to dig deeper right now.

alexpott’s picture

@dww I have no idea how but your vendor directory is very very out-of-date. The current code for 1.4.10 is

    /**
     * Factory method to create WebDriver\Exception objects
     *
     * @param integer    $code              Code
     * @param string     $message           Message
     * @param \Exception $previousException Previous exception
     *
     * @return \Exception
     */
    public static function factory($code, $message = null, $previousException = null)
    {
        // unknown error
        if (! isset(self::$errs[$code])) {
            $code = self::UNKNOWN_ERROR;
        }

        $errorDefinition = self::$errs[$code];

        if (trim($message) === '') {
            $message = $errorDefinition[1];
        }

        if (! is_numeric($code)) {
            $code = self::W3C_WEBDRIVER_ERROR;
        }

        $className = __CLASS__ . '\\' . $errorDefinition[0];

        return new $className($message, $code, $previousException);
    }

and https://github.com/instaclick/php-webdriver/blob/1.4.10/lib/WebDriver/Ex...

dww’s picture

Status: Needs review » Reviewed & tested by the community

Yeah, my local vendor was all messed up from switching branches and not re-running composer install. Sorry about that! Confirmed that #81 is now passing locally:

Testing Drupal\FunctionalJavascriptTests\Tests\JSInteractionTest
...                                                                 3 / 3 (100%)

Time: 1.27 minutes, Memory: 4.00 MB

OK (3 tests, 11 assertions)

Although I've uploaded a lot of patches here, many of them were just trying to get the testbot to give us results. The latest code here is mostly @alexpott's additions to and rewrites of the original work by @tedbow and @bendeguz.csirmaz. So I'm gonna be bold and RTBC this. Fixing this would be a huge win for core development, so no reason to further delay.

lauriii’s picture

+++ b/core/tests/Drupal/FunctionalJavascriptTests/JSWebAssert.php
@@ -506,4 +508,18 @@ public function assertNoElementAfterWait($selector_type, $selector, $timeout = 1
+  public static function isExceptionNotClickable(\Exception $exception): bool {

Was this left as the untyped exception on purpose?

alexpott’s picture

That's a great catch @lauriii. Fixing that up. It's a minor change so leaving at rtbc.

  • lauriii committed 7ff9cbf on 9.4.x
    Issue #3032275 by alexpott, dww, bendeguz.csirmaz, tedbow: Create a...
lauriii’s picture

Thank you for the new patch @alexpott! Committed 7ff9cbf and pushed to 9.4.x. Thanks!

Leaving open for 9.3.x backport.

  • lauriii committed 88adf9c on 9.3.x
    Issue #3032275 by alexpott, dww, bendeguz.csirmaz, tedbow: Create a...
lauriii’s picture

Status: Reviewed & tested by the community » Fixed

Backporting to 9.3.x since this only impacts tests. Also got confirmation from @catch on Slack that he didn't have any concerns on backporting this.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.