Problem/Motivation
While looking into the problems with the random fails in JavascriptTestBase most of the found problems are with an incorrect wait for something to happen.
Issues with waiting problems:
#2832672: [PP-1] Upgrade jcalderonzumba/* for better test performance
#2830485: \Drupal\Tests\outside_in\FunctionalJavascript\OutsideInBlockFormTest fails randomly
#2831603: Improve JSWebAssert::waitOnAutocomplete
Currently a lot of tests wait for specific JavaScript action to complete, for example ajax requests and autocomplete actions. This needs to change, the tests should be able to wait for the expected element to mimic the UX as much as possible.
Proposed resolution
Implement helper methods to wait for various types of elements, the list matches the find...() methods.
For the edge cases that are left we need to fix both the assertWaitOnAjaxRequest() and waitOnAutocomplete().
waitOnAutocomplete waits for a fixed time and then for an Ajax request. This must be changed to wait for the expected UI change.
assertWaitOnAjaxRequest needs to wait for the full Drupal controlled AJAX requests to complete, including all the follow-up actions.
Remaining tasks
- DONEAgree on the best approach (IS updated based on the comments)
- DONEImprove current API methods
- DONEImplement new wait..() API methods
- DONEWrite test that covers each type of wait?
- Update JTB documentation and describe the best way to handle waiting for elements
API changes
Discouraging:
- Drupal\FunctionalJavascriptTests\JSWebAssert::assertWaitOnAjaxRequest
- Drupal\FunctionalJavascriptTests\JSWebAssert::waitOnAutocomplete
Adding:
- Drupal\FunctionalJavascriptTests\JSWebAssert::waitForElement
- Drupal\FunctionalJavascriptTests\JSWebAssert::waitForElementVisible
- Drupal\FunctionalJavascriptTests\JSWebAssert::waitForButton
- Drupal\FunctionalJavascriptTests\JSWebAssert::waitForLink
- Drupal\FunctionalJavascriptTests\JSWebAssert::waitForField
- Drupal\FunctionalJavascriptTests\JSWebAssert::waitForId
Comments
Comment #2
michielnugter CreditAttribution: michielnugter as a volunteer and at Synetic commentedComment #3
michielnugter CreditAttribution: michielnugter as a volunteer and at Synetic commentedComment #4
michielnugter CreditAttribution: michielnugter as a volunteer and at Synetic commentedComment #5
michielnugter CreditAttribution: michielnugter as a volunteer and at Synetic commentedComment #6
michielnugter CreditAttribution: michielnugter as a volunteer and at Synetic commentedComment #7
michielnugter CreditAttribution: michielnugter as a volunteer and at Synetic commentedFirst iteration, based on previous work.
This adds support for:
- jQuery ajax requests
- Animation
- Debounce (new)
TODO:
- Drupal response processing
- Ajax requests on load
Comment #8
michielnugter CreditAttribution: michielnugter as a volunteer and at Synetic commentedComment #9
michielnugter CreditAttribution: michielnugter as a volunteer and at Synetic commentedAnd now with support for Drupal response processing, was easier than expected.
I'm very much in doubt about this point for this issue:
This would require a lot of overwriting (see https://www.drupal.org/node/2830485#comment-11821833) and I seriously question if it should be done.
Any opinions on this would be great.
Comment #10
xjmThanks @michielnugter.
Given the number of issues we've had related to this issue, this is a major task.
Comment #11
LendudeLets see how this does.
Comment #13
jibranCan we move this to ajax.js?
Comment #14
michielnugter CreditAttribution: michielnugter as a volunteer and at Synetic commented@lendude
Looks cleaner, the fail is related to the debounce check though I think. The autocomplete fails and this has a build in 300 ms wait for the debounce.
@jibran
Could be done, I want to leave this to the javascript maintainers in what they prefer. Both options could work, for now I'd say adding it in the test would be enough. Maybe a bigger Drupal.isBusy() containing all the checks would be better suited.
Hope to be able to look into this more later today or at least tomorrow.
Comment #15
LendudeThis takes care of the fail locally. Not sure how Drupal can be undefined though, but it is.
Comment #16
jibranIt is a good idea to ask JS maintainers. +1 for moving it to
Drupal
. How aboutDrupal.waitForResponse()
orDrupal.processingRequest()
?Thanks @Lendude. This looks good.
We should add Drupal and jQuery checks on the same level.
Comment #17
michielnugter CreditAttribution: michielnugter as a volunteer and at Synetic commentedSmall cleanup to make the Drupal === 'undefined' work similar to the jQuery check. Also shortened the IsNotAjaxing method for better overall readability.
Runs fine for me as well locally. Hope the testbot can test this soon!
Comment #18
Lendude@michielnugter nice cleanup, bit of nitpicking:
The second sentence seems to belong to the $message @param.
The whole condition could probably use some comments about what we are doing and why.
Trailing whitespace (PHPStorm doesn't clean up trailing whitespace in HEREDOCs, at least mine doesn't)
And +1 for moving the 'isAjaxing' test to Drupal. but maybe in a follow-up? Would be a shame to put the test improvement on hold while sorting that out.
Comment #20
michielnugter CreditAttribution: michielnugter as a volunteer and at Synetic commentedEven more cleanup, thanks @lendude for your feedback! You know I love the nitpicks (I actually do!).
Also added comments for the JS code.
@lendude: I agree on the followup. Let's get this in as soon as possible to reduce the likeliness of random fails.
I also think that this item: "Ajax requests fired on load (drupalGet / form submit) to finish" should be adressed in a followup, I think it's a different problem than this issue fixes. This issue just provides us with a better way of waiting for everything javascripty and shouldn't change much more. Trying to keep the scope small so we can move forward.
Comment #22
michielnugter CreditAttribution: michielnugter as a volunteer and at Synetic commentedFail seems unrelated(?). Adding others tests as well.
Comment #23
LendudeSome more nitpicks (missed a couple the first time):
'or' should be 'of'
completed => complete (or 'be completed')
ajax => AJAX
whitespace snuck in again
Comment #24
michielnugter CreditAttribution: michielnugter as a volunteer and at Synetic commentedAll fixed!
Comment #25
droplet CreditAttribution: droplet commentedNot a good idea. if it's a must, we could override it in test driver only. We didn't force our code to use Drupal.debounce also. It can be _.debounce
the JS engine simulated real behavior. The debounce works never based on another counter. We must see the feedback on the browser. The JS test engine should catch the result rather than a hidden counter we never used in our codebase.
Something like these helpers:
http://codeception.com/docs/modules/WebDriver#waitForElement
In Drupal's test driver is:
About the anime condition:
this condition isn't required but it could help for testing performance. For example, we could set 30 second timeout here. And shotern
$this->session->wait(3000, CONDITION)
.If networking & anime is stopping after 5 sec, it seldom waiting for another 3s for further rendering. If it does, a BIG UX problem.
Comment #27
michielnugter CreditAttribution: michielnugter as a volunteer and at Synetic commentedMoving the debounce() changes to the test only is no problem, I don't know how yet though. The changes should be added immediately and before anything uses debounce(). Tips/example would be very much appreciated :)
My view on the debounce() check:
There are different situations where debounce() is used. It's not only used for autocomplete but in other ways as well.
Waiting for the expected result would be best but in that line of thinking we should never use any of these methods in the first place. If that's the solution I'm fine with that too but that would make writing tests a little more complex.
This method (and the original methods it was based on) would mainly serve to make writing tests easier and less error prone. Not everyone will know that a debounce() will cause a semi-random delay of about 300 ms before the expected task is performed.
My view on the anime check and shorter timeouts:
The anime check was a copy-paste form the assertWaitOnAjaxRequest(). I assumed these checks were well thought out. Also: splitting this method into different parts will be difficult, we don't know the exact order in which we should wait.
Shortening the timeout to 3000ms will cause random failures on slower computers and I don't really see the use for it. I agree though that anything not rendering within 1000 ms is a big UX issue. I'm not sure though we can catch that in a reliable way in a test. Performance is very random up till now (random failures are evidence to that).
Comment #28
jibranWe'll not exactly. We have it in drupal-extension so it was copied from there. It got added in https://github.com/jhedstrom/drupalextension/pull/30 by @grasmash so I think we should ping him.
Comment #29
klausiShould be "@deprecated Scheduled for removal in Drupal 9.0.0. Use XYZ instead"
Having so much JS code as inline string is hard to read. Please put it in a dedicated JS file for better maintainability.
Overall I think that we should avoid calling these ajax completion assertion. Instead, we should test for UI elements as they show up. Example: if you test an autocomplete then you wait until the list of suggestions shows up in the DOM. You don't care that AJAX stuff or debounce stuff is going on.
However, there are some interesting edge cases as experienced in #2832672: [PP-1] Upgrade jcalderonzumba/* for better test performance. How do you wait for a link to be associated with some JS behavior before you are allowed to click it?
Comment #30
michielnugter CreditAttribution: michielnugter as a volunteer and at Synetic commentedInteresting development, I had some time to think about it and I agree with you both that checking the UI is far better than waiting for anything you shouldn't really have to know about but it has downsides as well.
I think there are 2 ways of solving this problem:
Option 1 would mean:
- More complex test writing, you need to check for the availabily of the expected UI. Sometimes for the execution of behaviors (@klausi's last point)
- More precise testing, you actually check that what should happen, happens
- Complex reviews because the reviewer needs to know if the tests properly waits for the UI
- Higher risk for random fails because of forgotten waits().
Option 2 would mean:
- Easier test writing, *magic* takes care that the expected UI is ready for use.
- Easier reviewing, less deep knowledge required. You can review what you see.
- Might cause unnecessary delays although not too much I think.
- Black box behavior
- No or at least a low number of fails due to timing issues.
- Overriding various bits of the PhantomJS driver.
EDIT: @klausi: I haven't fixed your code reviews yet, let's wait with the patches untill a decision is made on the direction for this issue.
Comment #31
michielnugter CreditAttribution: michielnugter as a volunteer and at Synetic commentedI thought of a third option:
- Remove the debounce() check, this should always be checked in th UI
- Don't add a new function but improve the existing methods:
-- waitOnAjax: Add the wait for Drupal.ajax isAjaxing
-- waitOnAutocomplete: Remove the 300ms delay and change the validation to a UI check.
- We could promote the use of UI checks instead of the wait() methods in code reviews (and update the documentation)
This would make the scope for this issue significantly smaller because only existing api methods will be improved and made more robust.
Comment #32
michielnugter CreditAttribution: michielnugter as a volunteer and at Synetic commentedQuickly made version for option 3.
Comment #34
michielnugter CreditAttribution: michielnugter as a volunteer and at Synetic commentedThis should pass.
Still to be determined:
- Which direction to take, is this patch the best solution?
- If the current patch is it if the javascript should be moved to an external file. The amount is less now and a bit in line with other usages of inline javascript in JTB.
Comment #36
michielnugter CreditAttribution: michielnugter as a volunteer and at Synetic commentedFail seems unrelated.
Comment #37
michielnugter CreditAttribution: michielnugter as a volunteer and at Synetic commentedIf we are going to rely on waiting for the UI some API methods for this should be available I think. I created a base method and a method for each specific find...() method. This way we have a similar interface for finding elements and waiting for them. As a 'bonus' I returned the requested element so the test can just call this one line.
Curious to see what you all think of these methods and option 3..
Comment #38
michielnugter CreditAttribution: michielnugter as a volunteer and at Synetic commentedComment #40
michielnugter CreditAttribution: michielnugter as a volunteer and at Synetic commentedComment #41
michielnugter CreditAttribution: michielnugter as a volunteer and at Synetic commentedShould this issue be promoted to critical? In my opinion it's blocking all JTB tests from getting committed because this will decide an essential part of these tests, waiting for various things..
Comment #42
alexpott+1 on the latest direction it looks neat and self-contained.
Here are some nits and patch attached that fixes them:
Needs to be fully qualified. so
\Behat\Mink\Selector\Xpath\Manipulator
plus should have a description.Space at the end of the line
Should be just
@see \Behat\Mink\Element\ElementInterface::findAll()
@see does not support additional text.
Should be a full sentence - ie. start with a Capital and finish with a fullstop. Plus this one could say
See ElementInterface::findAll() for the supported selectors.
Which would then compliment the @see below.Should be
\Behat\Mink\Element\NodeElement
. @return's need a sentence description too.Let's make the spacing after commas consistent. Also this line would be simpler to read if the JS strings used
'
instead of"
so there's no need for escaping the double quotes\"
.Needs to be a one line summary.
Waits...
Comment #43
michielnugter CreditAttribution: michielnugter as a volunteer and at Synetic commentedNice cleanup, thanks for that and for the +1 on the direction. I updated the IS to reflect the current direction of the issue.
I assume the Needs tests should be a JTB test? Are the currently tests covering the custom JTB code?
update: Discussed with alexpott on IRC. Working on a JTB test for JSWebAssert. I'll post a patch when I have something.
Comment #44
michielnugter CreditAttribution: michielnugter as a volunteer and at Synetic commentedUpdate with partial coverage for JSWebAssert. All new / changed methods have coverage, existing methods not (yet).
Based on droplet's reply: https://www.drupal.org/node/2831603#comment-11871156 I deprecated waitOnAutocomplete(), I applied his patch from this post and merged it in this patch as I agree with him. Credits to droplet for the change on that. Might be needed to split this off again if it gets committed in another issue.
Comment #46
alexpottLet's open a followup to add coverage for existing methods - that might be might out-of-scope here.
Patch attached fixes coding standards and the
Drupal\system\Tests\Module\InstallUninstallTest
- test modules need to be in the Testing package.Comment #47
michielnugter CreditAttribution: michielnugter as a volunteer and at Synetic commented@alexpott Thanks for the improvements, I figured as much on the test module. Hope it passes now.
Btw: once this is in I'll update the documentation on this subject to describe the usage of these methods in the waiting section.
Comment #48
alexpottAnother little nit.
I think we're good to go here - there's test coverage and evidence on the issue that the changes improve things. I've run the new test locally over 100 times - no fails.
I consider my contributions here all tidy ups so I'm in a good position to rtbc.
Comment #49
michielnugter CreditAttribution: michielnugter as a volunteer and at Synetic commentedAnother nit.
Also: @klausi provided feedback on the changes in EntityReferenceAutocompleteWidgetTest.php
https://www.drupal.org/node/2831603#comment-11873723
I think we need to adres this feedback first?
Comment #50
klausiThose are unrelated fixes from #2831603: Improve JSWebAssert::waitOnAutocomplete. I think we don't need them for this patch?
I think we have too many inline JS lines here, which makes it very hard to read and maintain this code. I think we should put this in a dedicated JS file and use file_get_contents() here to load it.
This should have instructions what people should use instead. Are they supposed to use for example waitForElement() now?
This should use waitForElement() now, right?
Comment #51
Wim LeersThe remaining tasks in the issue summary still has some things not marked as done. It sounds like they're actually done? :)
These are stale comments.
"result count" sounds very generic.
What about
assertAutocompleteResultCount
?Unnecessary capitalization. And probably also unnecessary comments.
Woah, clever!
Comment #52
droplet CreditAttribution: droplet commentedAny good reason to make these shortcuts? Can't it just passing the CSS selectors? KISS :)
Personally, I love the Codeception API design
http://codeception.com/docs/modules/WebDriver#waitForElement
Notable:
waitForElement
Appeared in HTML
waitForElementVisible
Appeared in HTML and HUMAN eye
Theoretically, we should remove `assertWaitOnAjaxRequest()` and promote to use waitForElement. ( assertWaitOnAjaxRequest only useful if it improves test performance. And if so, it can merge into waitForElement with a simple condition)
failure of Autocomplete may be a good example. After Ajax is completed, there're other factors affected. We must verify and give time PhantomJS to render the page and execute JS ..etc
Comment #53
michielnugter CreditAttribution: michielnugter as a volunteer and at Synetic commentedAdressed 1, 3 and 4 on #50.
1: Removed the changes
3: It's not deprecated anymore now. I think #2831603: Improve JSWebAssert::waitOnAutocomplete should handle that I think
4: I changed it to use waitForElement. Interesting thing here is that :visible is not supported by selectorToXpath(). The conversion does not explicitly test the visibility. Is this a problem?
2: Not sure yet on how to proceed here. Asked alexpott on IRC. Might post another update based on his suggestions.
Adressed #1 in #51. Other issues are now not relevant anymore as the code has been removed from the patch.
Comment #54
jibranReally great work @michielnugter. Great to see the patch is ready. We are going to need a change notice.
Let's move this to JS.
Comment #55
michielnugter CreditAttribution: michielnugter as a volunteer and at Synetic commented@droplet:
With the previous test the test passes for the autocomplete. Of course I'm not sure yet on random failures.
We could add a waitForElementVisible which waits again untill the element is visible. I attached a proposal for this, I changed waitForAutocomplete to use this variant.
Comment #56
michielnugter CreditAttribution: michielnugter as a volunteer and at Synetic commentedComment #57
alexpottRe #54 and #50.2 let's not do that here. There is plenty of existing javascript in JsWebAssert - we should try and handle that altogether in the same issue.
Comment #58
droplet CreditAttribution: droplet commentedI bet that came from PhantomJS. I tried diff way to delay the responses about 4 ~ 8 secs and it still passed in my local tests.
(jQuery.active === 0 && jQuery(':animated').length === 0))
We may add this before waitforElement. A modal can be shown but not clickable if it's animating. (I hit this problem with my other apps.)
Comment #59
alexpottNeeds tests.
$element might be null
I think this timeout needs to be converted to microseconds so that'd be
$end = $start + ($timeout * 1000)
no?Comment #60
michielnugter CreditAttribution: michielnugter as a volunteer and at Synetic commented1. True, I'll work on that.
2. True, I'll fix that.
3. It's a copy-paste from the PhantomJS/Mink driver. Didn't completely think it through yet.
Follow-ups created:
#2844580: Add JavaScriptTestBase coverage for methods missing it in JSWebAssert
#2844582: Move inline javascript in JSWebAssert into a separate javascript file
Comment #61
michielnugter CreditAttribution: michielnugter as a volunteer and at Synetic commentedAdressed 1 and 2.
3: It's a conversion from milliseconds to microseconds. I think it's correct like this.
Comment #62
droplet CreditAttribution: droplet commentedPhantomJSDriver has an API for visible:
Can we test if it works on CSS based anime or not. (It's useful for OutsideIn and Contrib modules).
For example, the Codeception engine, it uses webdriver. their `isVisible` function checking CSS anime also.
Example code:
(diff engine but background theory are close)
CSS based anime. Use Bootstrap modal.
FAIL:
SUCCESS:
Comment #63
michielnugter CreditAttribution: michielnugter as a volunteer and at Synetic commentedLeftover code cleanup, sorry about that.
@droplet: not sure what your mean in this case. I am using the $element->isVisible() but only after waiting for the element to be available in the DOM.
Comment #64
alexpottRe #59.3 - microtime(TRUE) return a float in seconds so we need to convert milliseconds to seconds so dividing by 1000 is correct. See https://secure.php.net/manual/en/function.microtime.php
Comment #65
michielnugter CreditAttribution: michielnugter as a volunteer and at Synetic commented@droplet:
I reread your comment. Do you mean that the current isVisible() check is not good enough and also needs checking on animation? If so: I propose to open up a seperate follow-up to add this improvement to isVisible() as I think it would be out of scope for this issue.
Comment #66
droplet CreditAttribution: droplet commentedSorry for my picky. I'm a bit worried on the new shortcuts.
Take the LINK as example,
What if we see code like this? We looking for "added_link" but it could be matching "added_link_wrong" (alt) in our testing.
Shouldn't we make it more specified for Drupal?
Another example, It always has unique ID or NAME from Drupal's FORM API.
-------
Attached a patch to update the waitFor-* functions. Less custom code :)
Comment #67
droplet CreditAttribution: droplet commentedSorry, I made a mistake in above patch. Hoping our test for test able to catch the error.
it should be TRUE
Use Array.some() to aviod test every instances.
Thanks.
Comment #69
droplet CreditAttribution: droplet commentedComment #71
droplet CreditAttribution: droplet commentedAhh right. Some instances are NULL.
Comment #72
michielnugter CreditAttribution: michielnugter as a volunteer and at Synetic commentedWas posting another patch at the same time, you won :) Hope this passes.
About #68
I think this should be correctly implemented in every test, yes it's a pitfall but I'm not sure if we should fix it on the Drupal level. The new API methods use the exact same implementations as the find...() methods. If we want to make them more specific it's a huge change because it might mean we have to update all existing JTB tests.
Comment #74
droplet CreditAttribution: droplet commentedI will fix the patch later. It returns wrong $element.
--------
Above error hinted me that the test is not so right:
Fix:
--------
I ran into a new error in my local test:
Looks like the PhantomJS on my local env rendered ajax faster than assertEmpty
Comment #75
michielnugter CreditAttribution: michielnugter as a volunteer and at Synetic commented@droplet:
Wow, fast machine :) Tried the test 100x locally and never had the problem. I can understand however it might cause random failures by itself.
Does the check really make the test better or can we do without? If we need it we should add a delay in the ajax methods to force a delayed response, 200ms should be enough I think?
Comment #76
droplet CreditAttribution: droplet commented@michielnugter,
Hope so!! Maybe too slow on my Windows machine also :(
findField spent more time to match the result than others. I change it to `find('css', '...')`, then works!
On my testing, it required an extra 600ms delay anyway.
I think we can add 1 sec delay on `waitForElement` test. Other watiFor-* inherited from `watiForElement` can be tested loosely.
And on the tests:
thoughts?
-------
extra, each waitFor-* must add a 2nd parameter to accept $timeout. Ideally, expose $timeout to a global var also
Comment #77
michielnugter CreditAttribution: michielnugter as a volunteer and at Synetic commentedI updated the test with the changed order of checking and added a specific check on the returned result.
This fixes the random fail if the click() was too fast for the find...() and the incorrect return of $element.
Also: the test failed on an asssertion in EntityReferenceAutocompleteWidgetTest which had me surprised. The autocomplete searched for Test[space] and the result was 2 hits (Test page and Page test, the search is case insensitive and apperently ignoring the space. The test however asserted that only 1 result should be available.
Without this patch the test fails as well for me, the problem is not specific to this patch it seems.
I changed the input but I'm not sure this is in scope for this issue and a proper fix.
Comment #78
droplet CreditAttribution: droplet commentedThanks. Good to go. Just notice one thing on my above change:
$timeout / 1000
diff time scale
Comment #79
michielnugter CreditAttribution: michielnugter as a volunteer and at Synetic commentedDid some further testing on EntityReferenceAutocompleteWidgetTest.php, it keeps failing for me and I still think it's weird. It fails with and without this patch. Restarted PhantomJS in between to make sure.
Attached a patch without the change to EntityReferenceAutocompleteWidgetTest.php to see if it fails on the testbot as well. Might just be my local dev that's messed up.
Includes the fix for the timeout conversion mentioned above by @droplet
Comment #80
michielnugter CreditAttribution: michielnugter as a volunteer and at Synetic commentedRight, time to check my dev environment :) Adding other tests to make sure..
With the latest fix for the timeout: is it ready for RTBC?
Comment #81
droplet CreditAttribution: droplet commentedIt's good I think but the last patch missing #78
Comment #82
alexpottHere's a fix for #78 plus a couple of coding standards fixes.
Comment #83
jibranYay! This is ready. We just need a change record and quick IS update.
Comment #84
alexpottCreated CR https://www.drupal.org/node/2846936
Comment #85
alexpottUpdated issue summary.
Comment #86
alexpottComment #87
alexpottAll my fixes on this issue have been nits and coding standards related. Therefore I think it is okay for me to commit this massive improvement to JavascriptTestBase. Also as it applies to 8.2.x I think we should commit this there too.
Committed and pushed 3af641f to 8.3.x and ca1117f to 8.2.x. Thanks!
Comment #90
michielnugter CreditAttribution: michielnugter as a volunteer and at Synetic commentedThanks for the commit! When I find the time I'll look into updating the documentation on this point.
Comment #91
michielnugter CreditAttribution: michielnugter as a volunteer and at Synetic commentedDocumentation has been updated. Not at my best at the moment so the text might need some refinement sometime..
Comment #92
droplet CreditAttribution: droplet commentedWhere's the documentation? Maybe we could post the link here also. It's handy for someone looking on this issue to make further changes or something. Thank You.