nightwatch
nightwatch copied to clipboard
"stale element" for .expect API, regression over V1 and inconsistency with commands
Description of the bug/issue
When I have React web app which replaces whole elements with instances with new ID I cannot use .expect API for ie. attribute checks. We are migrating from NW v1.7 where it worked differently and in a correct manner I think.
Out app could dynamically disable buttons if the data are invalid. For synchronization we have custom command
export function waitForAndClick(selector, name) {
this.waitForElementVisible(selector, name ? `Located ${name}.` : void 0);
this.expect
.element(selector)
.not.to.have.attribute(
'disabled',
name ? `${name} to be enabled.` : void 0
);
return this.click(selector, result => {
if (result.status !== 0) {
this.assert.fail(JSON.stringify(result));
}
});
}
We could probably omit the waitForElementVisible, but that's not the point. In v1.7 when you call:
this.expect
.element(selector)
.not.to.have.attribute(
'disabled',
name ? `${name} to be enabled.` : void 0
);
And the element with disabled attribute is replaced with a new one using React, the NW traverse whole PO sections, but in V2 I got
Request GET /session/b45cd4aebcd8a6771bbe93a7e88cd364/element/a60024b9-a1de-4c01-983a-3c34522de31c/attribute/disabled
Response 404 GET /session/b45cd4aebcd8a6771bbe93a7e88cd364/element/a60024b9-a1de-4c01-983a-3c34522de31c/attribute/disabled (14ms)
{
value: {
error: 'stale element reference',
message: 'stale element reference: element is not attached to the page document\n' +
' (Session info: chrome=110.0.5481.178)',
stacktrace: ''
}
}
And that's the end of the story. Funny thing is that if I try to click the disabled element directly. I got constant error:
Error while running .clickElement() protocol action: element click intercepted: Element <button class="pendo-create-model Button Button_container Button_type-primary Button_variant-button " disabled="" type="butto
n">...</button> is not clickable at point (1797, 159). Other element would receive the click: <span class="Tooltip_wrap">...</span>
(Session info: chrome=110.0.5481.178)
But when the element gets replaced and finally clickable, it clicks. But there are those ugly errors before.
Steps to reproduce
No response
Sample test
No response
Command to run
No response
Verbose Output
√ Located New model button.
→ Completed command: waitForElementVisible ({name, __index, __selector, locateStrategy, pseudoSelector, parent, resolvedElement}, 'Located New model button.') (22ms)
→ Running command: expect.element ({name, __index, __selector, locateStrategy, pseudoSelector, parent, resolvedElement})
Request POST /session/b45cd4aebcd8a6771bbe93a7e88cd364/elements
{ using: 'css selector', value: '.DashboardLayout_nav-bar' }
Response 200 POST /session/b45cd4aebcd8a6771bbe93a7e88cd364/elements (4ms)
{
value: [
{
'element-6066-11e4-a52e-4f735466cecf': '0a813bc9-0919-427e-83f2-347f105de48f'
}
]
}
Request POST /session/b45cd4aebcd8a6771bbe93a7e88cd364/element/0a813bc9-0919-427e-83f2-347f105de48f/elements
{
using: 'xpath',
value: ".//*[contains(concat(' ', normalize-space(@class), ' '), ' Button_container ') and .//*[contains(concat(' ', normalize-space(@class), ' '), ' plus ')]]"
}
Response 200 POST /session/b45cd4aebcd8a6771bbe93a7e88cd364/element/0a813bc9-0919-427e-83f2-347f105de48f/elements (5ms)
{
value: [
{
'element-6066-11e4-a52e-4f735466cecf': 'a60024b9-a1de-4c01-983a-3c34522de31c'
}
]
}
Request GET /session/b45cd4aebcd8a6771bbe93a7e88cd364/element/a60024b9-a1de-4c01-983a-3c34522de31c/attribute/disabled
Response 200 GET /session/b45cd4aebcd8a6771bbe93a7e88cd364/element/a60024b9-a1de-4c01-983a-3c34522de31c/attribute/disabled (2ms)
{ value: 'true' }
Request GET /session/b45cd4aebcd8a6771bbe93a7e88cd364/element/a60024b9-a1de-4c01-983a-3c34522de31c/attribute/disabled
Response 200 GET /session/b45cd4aebcd8a6771bbe93a7e88cd364/element/a60024b9-a1de-4c01-983a-3c34522de31c/attribute/disabled (10ms)
{ value: 'true' }
Request GET /session/b45cd4aebcd8a6771bbe93a7e88cd364/element/a60024b9-a1de-4c01-983a-3c34522de31c/attribute/disabled
Response 200 GET /session/b45cd4aebcd8a6771bbe93a7e88cd364/element/a60024b9-a1de-4c01-983a-3c34522de31c/attribute/disabled (4ms)
{ value: 'true' }
Request GET /session/b45cd4aebcd8a6771bbe93a7e88cd364/element/a60024b9-a1de-4c01-983a-3c34522de31c/attribute/disabled
Response 200 GET /session/b45cd4aebcd8a6771bbe93a7e88cd364/element/a60024b9-a1de-4c01-983a-3c34522de31c/attribute/disabled (8ms)
{ value: 'true' }
Request GET /session/b45cd4aebcd8a6771bbe93a7e88cd364/element/a60024b9-a1de-4c01-983a-3c34522de31c/attribute/disabled
Response 404 GET /session/b45cd4aebcd8a6771bbe93a7e88cd364/element/a60024b9-a1de-4c01-983a-3c34522de31c/attribute/disabled (14ms)
{
value: {
error: 'stale element reference',
message: 'stale element reference: element is not attached to the page document\n' +
' (Session info: chrome=110.0.5481.178)',
stacktrace: ''
}
}
Request GET /session/b45cd4aebcd8a6771bbe93a7e88cd364/element/a60024b9-a1de-4c01-983a-3c34522de31c/attribute/disabled
Response 404 GET /session/b45cd4aebcd8a6771bbe93a7e88cd364/element/a60024b9-a1de-4c01-983a-3c34522de31c/attribute/disabled (10ms)
{
value: {
error: 'stale element reference',
message: 'stale element reference: element is not attached to the page document\n' +
' (Session info: chrome=110.0.5481.178)',
stacktrace: ''
}
}
And that's the death loop.
On the other hand clicking that elements makes...
→ Completed command: waitForElementVisible ({name, __index, __selector, locateStrategy, pseudoSelector, parent, resolvedElement}, 'Located New model button.') (27ms)
→ Running command: click ({name, __index, __selector, locateStrategy, pseudoSelector, parent, resolvedElement}, [Function])
Request POST /session/a16d257631686fa0e125a38ebb98c651/elements
{ using: 'css selector', value: '.DashboardLayout_nav-bar' }
Response 200 POST /session/a16d257631686fa0e125a38ebb98c651/elements (5ms)
{
value: [
{
'element-6066-11e4-a52e-4f735466cecf': '34ba22a1-7eae-4924-a6cd-c545020d4190'
}
]
}
Request POST /session/a16d257631686fa0e125a38ebb98c651/element/34ba22a1-7eae-4924-a6cd-c545020d4190/elements
{
using: 'xpath',
value: ".//*[contains(concat(' ', normalize-space(@class), ' '), ' Button_container ') and .//*[contains(concat(' ', normalize-space(@class), ' '), ' plus ')]]"
}
Response 200 POST /session/a16d257631686fa0e125a38ebb98c651/element/34ba22a1-7eae-4924-a6cd-c545020d4190/elements (5ms)
{
value: [
{
'element-6066-11e4-a52e-4f735466cecf': '27859129-5165-487d-a7c4-8e743769cd50'
}
]
}
Request POST /session/a16d257631686fa0e125a38ebb98c651/element/27859129-5165-487d-a7c4-8e743769cd50/click
{}
Response 400 POST /session/a16d257631686fa0e125a38ebb98c651/element/27859129-5165-487d-a7c4-8e743769cd50/click (1084ms)
{
value: {
error: 'element click intercepted',
message: 'element click intercepted: Element <button class="pendo-create-model Button Button_container Button_type-primary Button_variant-button " disabled="" type="button">...</button> is not clickable at point (1797,
159). Other element would receive the click: <span class="Tooltip_wrap">...</span>\n' +
' (Session info: chrome=110.0.5481.178)',
stacktrace: ''
}
}
Error Error while running .clickElement() protocol action: element click intercepted: Element <button class="pendo-create-model Button Button_container Button_type-primary Button_variant-button " disabled="" type="butto
n">...</button> is not clickable at point (1797, 159). Other element would receive the click: <span class="Tooltip_wrap">...</span>
(Session info: chrome=110.0.5481.178)
Request POST /session/a16d257631686fa0e125a38ebb98c651/elements
{ using: 'css selector', value: '.DashboardLayout_nav-bar' }
Response 200 POST /session/a16d257631686fa0e125a38ebb98c651/elements (14ms)
{
value: [
{
'element-6066-11e4-a52e-4f735466cecf': '34ba22a1-7eae-4924-a6cd-c545020d4190'
}
]
}
Request POST /session/a16d257631686fa0e125a38ebb98c651/element/34ba22a1-7eae-4924-a6cd-c545020d4190/elements
{
using: 'xpath',
value: ".//*[contains(concat(' ', normalize-space(@class), ' '), ' Button_container ') and .//*[contains(concat(' ', normalize-space(@class), ' '), ' plus ')]]"
}
Response 200 POST /session/a16d257631686fa0e125a38ebb98c651/element/34ba22a1-7eae-4924-a6cd-c545020d4190/elements (12ms)
{
value: [
{
'element-6066-11e4-a52e-4f735466cecf': '27859129-5165-487d-a7c4-8e743769cd50'
}
]
}
Request POST /session/a16d257631686fa0e125a38ebb98c651/element/27859129-5165-487d-a7c4-8e743769cd50/click
{}
Response 400 POST /session/a16d257631686fa0e125a38ebb98c651/element/27859129-5165-487d-a7c4-8e743769cd50/click (1083ms)
{
value: {
error: 'element click intercepted',
message: 'element click intercepted: Element <button class="pendo-create-model Button Button_container Button_type-primary Button_variant-button " disabled="" type="button">...</button> is not clickable at point (1797,
159). Other element would receive the click: <span class="Tooltip_wrap">...</span>\n' +
' (Session info: chrome=110.0.5481.178)',
stacktrace: ''
}
}
Error Error while running .clickElement() protocol action: element click intercepted: Element <button class="pendo-create-model Button Button_container Button_type-primary Button_variant-button " disabled="" type="butto
n">...</button> is not clickable at point (1797, 159). Other element would receive the click: <span class="Tooltip_wrap">...</span>
(Session info: chrome=110.0.5481.178)
Request POST /session/a16d257631686fa0e125a38ebb98c651/element/27859129-5165-487d-a7c4-8e743769cd50/click
{}
Response 404 POST /session/a16d257631686fa0e125a38ebb98c651/element/27859129-5165-487d-a7c4-8e743769cd50/click (9ms)
{
value: {
error: 'stale element reference',
message: 'stale element reference: element is not attached to the page document\n' +
' (Session info: chrome=110.0.5481.178)',
stacktrace: ''
}
}
Error Error while running .clickElement() protocol action: stale element reference: element is not attached to the page document
(Session info: chrome=110.0.5481.178)
Request POST /session/a16d257631686fa0e125a38ebb98c651/elements
{ using: 'css selector', value: '.DashboardLayout_nav-bar' }
Response 200 POST /session/a16d257631686fa0e125a38ebb98c651/elements (14ms)
{
value: [
{
'element-6066-11e4-a52e-4f735466cecf': '34ba22a1-7eae-4924-a6cd-c545020d4190'
}
]
}
Request POST /session/a16d257631686fa0e125a38ebb98c651/element/34ba22a1-7eae-4924-a6cd-c545020d4190/elements
{
using: 'xpath',
value: ".//*[contains(concat(' ', normalize-space(@class), ' '), ' Button_container ') and .//*[contains(concat(' ', normalize-space(@class), ' '), ' plus ')]]"
}
Response 200 POST /session/a16d257631686fa0e125a38ebb98c651/element/34ba22a1-7eae-4924-a6cd-c545020d4190/elements (12ms)
{
value: [
{
'element-6066-11e4-a52e-4f735466cecf': '98188909-1600-4105-baf0-408dc798fc19'
}
]
}
Request POST /session/a16d257631686fa0e125a38ebb98c651/element/98188909-1600-4105-baf0-408dc798fc19/click
{}
Response 200 POST /session/a16d257631686fa0e125a38ebb98c651/element/98188909-1600-4105-baf0-408dc798fc19/click (57ms)
{ value: null }
→ Completed command: click ({name, __index, __selector, locateStrategy, pseudoSelector, parent, resolvedElement}, [Function]) (3327ms)
Nightwatch Configuration
No response
Nightwatch.js Version
2.6.15
Node Version
18.13.0
Browser
chrome 110
Operating System
Windows 10
Additional Information
No response
Have you got a test case that we can look at so that we can reproduce the issue? a simple component?
Can you maybe try to use waitUntil
to wait until the element is replaced (until the element corresponding to the selector passed becomes enabled again) instead of expect
? It seems that for expect, Nightwatch does not send request for the element id again and again, but for click it does.
Have you got a test case that we can look at so that we can reproduce the issue? a simple component?
Well, I would have to fabricate also UI to reproduce that which I probably won't make it anytime sooner.
waitUntil Seems like a nice workaround. Since we are migrating from 1.7 I did not know about it. I will try it to make it wait for active element.
In fact, I would be happier if .expect api would just fail right away instead of polling for the assert. Since React and similar frameworks just replaces the element as a whole, it would never work well with polling. The fact it that it works like that in v1.7 and for us it is a regression. In fact there are so many changes which are not mentioned in the migration steps.
@ldziadkowiec Ok, so in v1.7 it was retrieving a fresh element id for each retry? Then I think we should do it that way in v2/v3. it was implemented like this as a potential performance improvement, but I guess it is not compatible with React apps (and possibly other modern frontend frameworks).
@ldziadkowiec Ok, so in v1.7 it was retrieving a fresh element id for each retry? Then I think we should do it that way in v2/v3. it was implemented like this as a potential performance improvement, but I guess it is not compatible with React apps (and possibly other modern frontend frameworks).
Exactly. I have done partial workaround for some parts when this background update is possible using suggested waitUntil like that:
export function waitForElementExpectAttributeNotPresent(
selector,
attribute,
name
) {
const { waitForConditionTimeout, waitForConditionPollInterval } = this.api.globals;
this.waitForElementVisible(selector, name ? `Located ${name}...` : undefined);
this.api.waitUntil(
async () => {
const attributeValue = await new Promise(resolve => {
this.getAttribute(selector, attribute, ({ value }) => {
resolve(value);
});
});
return attributeValue === null;
},
waitForConditionTimeout,
waitForConditionPollInterval,
name ? `${name} attribute "${attribute}" not present.` : undefined
);
return this;
}
But I have found another random issues on native .waitForElementVisible() command using some hardcore script injection which I do not understand. waitForElementVisible() seems to be using two-part query, one for webElementId and the seconds injects script to query the visibility ? I believe that this command should be really fool-proof. I have found an execution when the query for webElementId went through, than probably it has been switched with new element and the NW keeps sending the script but with: "stale element reference: element is not attached to the page document" until it timeouts out.
Screen from BS session. At 1:06 the Element is switched.
This race-condition is extremely rare and I'm only able to simulate it by running my tests in parallel, which is a normal case, but I can't turn --verbose on, since it overflows the buffer length. But the selenium commands goes as follows:
Request POST /session/d7925ff6b0656861bd6242ef600910c0/element/cb3ee80b-c9af-4919-bcb9-7f42d6206c9c/elements
{
using: 'css selector',
value: '.DataStatus_container.DataStatus_type-success'
}
Response 200 POST /session/d7925ff6b0656861bd6242ef600910c0/element/cb3ee80b-c9af-4919-bcb9-7f42d6206c9c/elements (8ms)
{
value: [
{
'element-6066-11e4-a52e-4f735466cecf': '0507f834-9bc7-4d34-a79e-c64f91c7e23d'
}
]
}
Now the element is replaced.
Request POST /session/d7925ff6b0656861bd6242ef600910c0/execute/sync
{
script: 'return (function(){return (function(){var k=this||self;function aa(a){return"string"==typeof a}function ba(a,b){a=a.split(".");var c=k;a[0]in c||"undefined"==typeof c.execScript||c.execScript("var "+a... (44027 char
acters)',
args: [
{
'element-6066-11e4-a52e-4f735466cecf': '0507f834-9bc7-4d34-a79e-c64f91c7e23d',
ELEMENT: '0507f834-9bc7-4d34-a79e-c64f91c7e23d' - NOT PRESENT ANYMORE
}
]
}
We have upgraded to nwjs v3 recently and this issue is also valid in v3
Any chance to have this bug to be fixed ? It's happenning quite frequently.
Any chance to have this resolved soon ? We suffer about this bug in every test run. Some tests are so "well timed" that even two suite retries are too low. Otherwise we would have to rewrite several commands to be immune from ID caching and use only custom commands.
I'll try to look into this.
BTW, any reason for using waitForElementVisible
instead of waitForElementPresent
? The former checks if the element is visible in the viewport, while the latter just checks if the element is present in the DOM or not. And looking at your test, waitForElementPresent
should also suffice.
But I agree that this issue also needs to be fixed.
I was just about to find some workaround to this thing since we had a lot of failing tests lately. I have first reproduced this bug by founding nice use-case which would like trigger in 75% of runtime.
After that I have searched for new nwjs and found a 3.3.4 with no changelog and release notes yet. From my surprise I found out that the
error: 'stale element reference'
Seemed to be retried nicely. The nwjs seems to backoff and search from the root of page object as it should.
Request POST /session/3c667eab12da57df15a545dbaba422ac/element/BB87EA67C88D4B37A093C47B6CF3857C_element_69/click
{}
Response 404 POST /session/3c667eab12da57df15a545dbaba422ac/element/BB87EA67C88D4B37A093C47B6CF3857C_element_69/click (106ms)
{
value: {
error: 'stale element reference',
message: 'stale element reference: stale element not found\n' +
' (Session info: chrome=120.0.6099.71)',
stacktrace: ''
}
}
Error Error while running .clickElement() protocol action: stale element reference: stale element not found
(Session info: chrome=120.0.6099.71)
Request POST /session/3c667eab12da57df15a545dbaba422ac/elements
{
using: 'css selector',
value: '.SegmentationControlsLayout_container, .TableControlsLayout_container'
}
Response 200 POST /session/3c667eab12da57df15a545dbaba422ac/elements (7ms)
{ value: [] }
But :) I have found out, that beside "stale element" there could be also error:
Request POST /session/455fc1fe406a588aa8d485f6eebf899e/element/19CB21F8C6BD9185F987EFFC377816BF_element_67/click
{}
Response 404 POST /session/455fc1fe406a588aa8d485f6eebf899e/element/19CB21F8C6BD9185F987EFFC377816BF_element_67/click (10ms)
{
value: {
error: 'no such element',
message: 'no such element: No node with given id found\n' +
' (Session info: chrome=120.0.6099.71)',
stacktrace: ''
}
}
Which keeps to repeat as the "stale element" did.
Meanwhile I have written an ugly workaround for repeatable click which seemed to be working just fine, but it's ugly and with long timeout.
this.api.waitUntil(
async () => {
return await new Promise(resolve => {
this.click(selector, result => {
resolve(result.status === 0);
});
});
},
this.api.globals.waitForConditionTimeout * 5
);
Any chance to fix also the "error: 'no such element'," ? I'll paste broather log with verbose on. That log indicated that there were "stale element" at first which were retried and after that "no such element". This was really a lucky catch :) nwjs_issue_3639.txt
Thank you :)
Hey, I'm out of office for a few days, but I'll look into it once I come back. Thanks for your patience :)
@ldziadkowiec The .click()
command was retrying correctly on stale elements before also (there was nothing new introduced in versions near 3.3.4
), the only problem was with the .waitForElementVisible()
which should be fixed now with v3.3.5
, so the code you posted here should work correctly now.
As for the error related to 'no such element'
, good catch! This seems like a bug and we'll have look into what's causing it.