Stabilize FunctionalJavascript testing AJAX: add ::assertExpectedAjaxRequest()

Created on 19 October 2022, over 2 years ago
Updated 7 July 2024, 8 months ago

Problem/Motivation

Observed in #3315319-9: Random fails in Drupal\Tests\ckeditor5\FunctionalJavascript\AdminUiTest and Drupal\Tests\ckeditor5\FunctionalJavascript\CKEditor5Test ā†’ and in #3315490-24: Random fail in Drupal\Tests\layout_builder\FunctionalJavascript\InlineBlockPrivateFilesTest ā†’ : many tests wait too little (not for enough AJAX responses to arrive) or too much (waiting for an AJAX response that will never come because no AJAX request was triggered, hence waiting for the default timeout of 10 seconds for no reason at all).

Proposed resolution

Add a new ::assertExpectedAjaxRequest() method with a $count parameter to allow explicit testing of the number of AJAX responses any test item should trigger.

Make ::assertWaitOnAjaxRequest() use this new method, but without the assertion.

If either method is called when there are zero ajax requests made, a deprecation error will be fired.

Remaining tasks

User interface changes

None.

API changes

TBD

Data model changes

None.

Release notes snippet

TBD

šŸ“Œ Task
Status

Fixed

Version

10.2 āœØ

Component
PHPUnitĀ  ā†’

Last updated 4 days ago

Created by

šŸ‡§šŸ‡ŖBelgium wim leers Ghent šŸ‡§šŸ‡ŖšŸ‡ŖšŸ‡ŗ

Live updates comments and jobs are added and updated live.
  • JavaScript

    Affects the content, performance, or handling of Javascript.

Sign in to follow issues

Comments & Activities

Not all content is available!

It's likely this issue predates Contrib.social: some issue and comment data are missing.

  • @catch opened merge request.
  • šŸ‡¬šŸ‡§United Kingdom catch

    MR back to green.

    Looking at my earlier review point that ended up stalling this:

    What if we added an $expected_count parameter here instead of adding the int return?

    Totally possible, but ā€¦ it'd be more disruptive: any contrib/custom subclass of ::assertWaitOnAjaxRequest() would need to be updated. More importantly still, you'd need to pass in an explicit timeout and message before you could pass that third new parameter ā€¦ which seems pretty awful and verbose

    @Wim is correct. The only thing I can think of is adding this as the first parameter to assertWaitOnAjaxRequest() - either add a new method with the new signature like assertAjaxRequestCount() or change the existing API, which would mean trying to divine whether an integer is a count or a timeout value and issuing deprecations from there etc.

  • Status changed to Needs review over 1 year ago
  • šŸ‡¬šŸ‡§United Kingdom catch
  • šŸ‡«šŸ‡·France nod_ Lille

    Option one just seems terrible, I'd go for option 2 that way we unlock the situation with the return and have a path forward

  • šŸ‡¬šŸ‡§United Kingdom longwave UK

    +1 to option 2. Overloading the existing API doesn't feel like a good idea as there is no clean way to do it and the method name doesn't imply that it can also count requests so discoverability of this is hard. Adding a new method explicitly for counting the requests as well makes sense to me.

  • šŸ‡¬šŸ‡§United Kingdom alexpott šŸ‡ŖšŸ‡ŗšŸŒ

    I agree with @nod_ +1 for option 2 as a way forward.

  • Status changed to Needs work over 1 year ago
  • The Needs Review Queue Bot ā†’ tested this issue. It fails the Drupal core commit checks. Therefore, this issue status is now "Needs work".

    This does not mean that the patch needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.

    Consult the Drupal Contributor Guide ā†’ to find step-by-step guides for working with issues.

  • šŸ‡«šŸ‡·France nod_ Lille

    all good on the js side for me, thanks

  • šŸ‡¬šŸ‡§United Kingdom catch

    I've added a new method, public function assertExpectedAjaxRequest(?int $count = NULL, $timeout = 10000, $message = 'Unable to complete AJAX request.'): void {

    This optionally expects a count, and will run the assertion internally if one is provided, otherwise it's exactly the same as the old method.

    https://git.drupalcode.org/project/drupal/-/merge_requests/5367/diffs?co... shows the change. I also updated FrameworkTest.

    If this looks OK, I can update all of the calls here which are doing assertSame() on the return value.

    We could also entirely deprecate the old method and move everything over - don't particularly mind doing that if it helps this land smoothly, but it'd also be a straightforward follow-up.

  • Status changed to Needs review over 1 year ago
  • Status changed to RTBC over 1 year ago
  • šŸ‡«šŸ‡·France nod_ Lille

    All looks good to me

  • last update over 1 year ago
    Patch Failed to Apply
  • šŸ‡§šŸ‡ŖBelgium wim leers Ghent šŸ‡§šŸ‡ŖšŸ‡ŖšŸ‡ŗ

    Looks really good. šŸ¤©

    Posted one question. But that could be a follow-up too ā€” it's so clear that this is such a huge improvement to functional JS test reliability thanks to:

    
       * @throws \LogicException
       *   When an unnecessary invocation to this method is detected.
    

    šŸ‘† Gone will be the false sense of security of having written a test, when actually that test is incorrect at worst or undeterministic at best!

    Note: this will impact contrib, this is a theoretical BC break. Some contrib tests will stop working, because they'll run into this exception. But ā€¦ that'll actually be a good thing, because each of those will be for a wrong "AJAX wait"! They'll be forced to fix it, and they (and d.o's test infra) will be better for it šŸ‘ Tagging and because neither the API change is listed nor do we have a release note ready for module maintainers.

  • Status changed to Needs work over 1 year ago
  • šŸ‡¬šŸ‡§United Kingdom alexpott šŸ‡ŖšŸ‡ŗšŸŒ

    Can we stop this from breaking contrib and custom tests unless they are doing deprecation testing?

    I.e. can we only throw the new error if the test is a core test. And if not trigger a deprecation or something. If someone has a large JS test suite that is running fine on their infrastructure causing it to fail and requiring them to remediate immediately feels not great.

    Setting to needs work for #97

  • šŸ‡§šŸ‡ŖBelgium wim leers Ghent šŸ‡§šŸ‡ŖšŸ‡ŖšŸ‡ŗ

    I.e. can we only throw the new error if the test is a core test. And if not trigger a deprecation or something. If someone has a large JS test suite that is running fine on their infrastructure causing it to fail and requiring them to remediate immediately feels not great.

    Yes, using something like \Drupal\Core\Config\Schema\SchemaCheckTrait::isContribViolation():

      /**
       * Whether the current test is for a contrib module.
       *
       * @return bool
       */
      private function isContribViolation(): bool {
        $test_file_name = (new \ReflectionClass($this))->getFileName();
        $root = dirname(__DIR__, 6);
        return !str_starts_with($test_file_name, $root . DIRECTORY_SEPARATOR . 'core');
      }
    
  • šŸ‡¬šŸ‡§United Kingdom catch

    I.e. can we only throw the new error if the test is a core test. And if not trigger a deprecation or something.

    This is a docs issue - the code triggers a deprecation, the phpdoc appears to have been pre-emptive (or maybe a holdover from before this triggered a deprecation.

  • Status changed to RTBC over 1 year ago
  • šŸ‡¬šŸ‡§United Kingdom catch
  • šŸ‡§šŸ‡ŖBelgium wim leers Ghent šŸ‡§šŸ‡ŖšŸ‡ŖšŸ‡ŗ

    I see ā€” my bad! šŸ˜…

  • šŸ‡¬šŸ‡§United Kingdom catch
  • šŸ‡¬šŸ‡§United Kingdom catch

    Updated the IS and the CR.

  • šŸ‡¬šŸ‡§United Kingdom catch

    Removing the 'needs release note' tag since I think the change record is enough here.

  • šŸ‡¬šŸ‡§United Kingdom catch

    Above wasn't true for the RuntimeException so I have made that conditional on $count being passed in, we can change that issue a deprecation when $count isn't passed in later.

  • Status changed to Needs work over 1 year ago
  • šŸ‡¬šŸ‡§United Kingdom longwave UK

    Latest run has a fail that looks relevant:

    Drupal\FunctionalJavascriptTests\AjaxWaitTest::testUntrackedXhr
    Failed asserting that exception message 'Unable to complete AJAX request.' contains '0 XHR requests through jQuery, but 1 observed in the browser ā€” this requires js_testing_ajax_request_test.js to be updated.'.
    

    Rerunning the pipeline but this is one of the new tests in AjaxWaitTest getting the opposite error message.

  • šŸ‡¬šŸ‡§United Kingdom catch

    AjaxWaitTest needed updating to use ::assertExpectedAjaxRequest() with a $count parameter, otherwise the error message it's looking for doesn't get triggered (due to tweaks here to make it more bc-friendly and not trigger the new exceptions). Pushed a commit that does that.

  • Status changed to RTBC over 1 year ago
  • šŸ‡¬šŸ‡§United Kingdom catch

    Since the fix was just converting the test to use the new API with the stricter behaviour, after relaxing the behaviour for the old API to maintain bc, moving back to RTBC.

    • lauriii ā†’ committed 2a95e3fd on 11.x
      Issue #3316274 by Wim Leers, catch, nod_, alexpott, longwave, bnjmnm:...
    • lauriii ā†’ committed a49e4141 on 10.2.x
      Issue #3316274 by Wim Leers, catch, nod_, alexpott, longwave, bnjmnm:...
  • Status changed to Fixed over 1 year ago
  • šŸ‡«šŸ‡®Finland lauriii Finland

    Committed 2a95e3f and pushed to 11.x. Also cherry-picked to 10.2.x. Thanks!

  • šŸ‡¬šŸ‡§United Kingdom alexpott šŸ‡ŖšŸ‡ŗšŸŒ

    This has caused \Drupal\Tests\settings_tray\FunctionalJavascript\SettingsTrayBlockFormTest::testBlocks() to fail locally 100% of the time and really really often on gitlabci.

  • šŸ‡¬šŸ‡§United Kingdom alexpott šŸ‡ŖšŸ‡ŗšŸŒ
  • šŸ‡ŖšŸ‡øSpain fjgarlin

    I got the same last week too. For example: https://git.drupalcode.org/project/drupal/-/merge_requests/5451/pipelines where the only things that were changed in that MR were "test-only" job code. I tried re-running some of the jobs multiple times and kept getting fails.

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

  • Status changed to Fixed about 1 year ago
  • šŸ‡¦šŸ‡ŗAustralia acbramley

    Just FYI for anyone else that finds this issue because their assertWaitOnAjaxRequest() calls started failing, you need to now enable js_testing_ajax_request_test for any JS test that uses this function.

    js_testing_ajax_request_test provides test JS code that set window.drupalActiveXhrCount and window.drupalCumulativeXhrCount. Those variables are required to have a successful assertWaitOnAjaxRequest call.

  • šŸ‡·šŸ‡ŗRussia zniki.ru

    Addition to previous comment. You don't need to add js_testing_ajax_request_test if you extends WebDriverTestBase.

    Many contrib modules affected by this change:

    And in most cases nobody understand the reason of the fail.
    Changing from assertWaitOnAjaxRequest() to assertExpectedAjaxRequest() doesn't help at all.
    I would love to have instruction how to handle common fail cases, e.g:
    RuntimeException: Unable to complete AJAX request.

  • šŸ‡®šŸ‡±Israel jsacksick

    I'm confused by this change... Commerce & Commerce shipping tests are affected as well... Even though the failing tests extend the CommerceWebDriverTestBase class (which extends the WebDriverTestBase).
    What am I missing? Do we need to not rely on assertWaitOnAjaxRequest() anymore?

  • šŸ‡ŗšŸ‡øUnited States bnjmnm Ann Arbor, MI

    assertWaitOnAjaxRequest() now fails if during the wait window.drupalCumulativeXhrCount never exceeds 0, so any uses of assertWaitOnAjaxRequest() that weren't actually needed will now fail.

    So for tests that were previously passing and now fail on assertWaitOnAjaxRequest(), the first thing to try is removing the call entirely. It was very common to add these calls in places where they were not needed, where they were used for waiting on a JS operation to complete that wasn't necessarily something that used the Drupal AJAX API.

    It is possible removing assertWaitOnAjaxRequest() will result a different test failure. Calling assertWaitOnAjaxRequest() when no Ajax calls are present still added a 10000ms wait before proceeding, and tests may have depended on that wait. Ideally, if the assertWaitOnAjaxRequest() removal results in a fail, it should be replaced with a wait for the specific expected change.

    One of the reasons for this change was due to every unnecessary assertWaitOnAjaxRequest() adding 10000ms per-call to each test run. The more of these removed, the faster you can expect your tests to run.

  • šŸ‡®šŸ‡±Israel jsacksick

    @bnjmnm: Thanks a lot for this thorough explanation! This is helpful :).

  • šŸ‡¦šŸ‡ŗAustralia jordan.jamous

    Is this going to land in 10.3.x?

  • šŸ‡¦šŸ‡ŗAustralia acbramley

    @jordan.jamous it is in 10.3.0.

Production build 0.71.5 2024