nightwatch icon indicating copy to clipboard operation
nightwatch copied to clipboard

"stale element" for .expect API, regression over V1 and inconsistency with commands

Open ldziadkowiec opened this issue 1 year ago • 13 comments

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

ldziadkowiec avatar Mar 10 '23 20:03 ldziadkowiec

Have you got a test case that we can look at so that we can reproduce the issue? a simple component?

AutomatedTester avatar Mar 14 '23 11:03 AutomatedTester

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.

garg3133 avatar Mar 14 '23 19:03 garg3133

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 avatar Mar 20 '23 09:03 ldziadkowiec

@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).

beatfactor avatar Mar 20 '23 11:03 beatfactor

@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. obrazek

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
       }
     ]
  }

ldziadkowiec avatar Mar 22 '23 09:03 ldziadkowiec

We have upgraded to nwjs v3 recently and this issue is also valid in v3

ldziadkowiec avatar Jul 18 '23 09:07 ldziadkowiec

Any chance to have this bug to be fixed ? It's happenning quite frequently. obrazek

ldziadkowiec avatar Oct 19 '23 11:10 ldziadkowiec

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.

ldziadkowiec avatar Dec 12 '23 07:12 ldziadkowiec

I'll try to look into this.

garg3133 avatar Dec 12 '23 10:12 garg3133

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.

garg3133 avatar Dec 12 '23 11:12 garg3133

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 :)

ldziadkowiec avatar Dec 13 '23 12:12 ldziadkowiec

Hey, I'm out of office for a few days, but I'll look into it once I come back. Thanks for your patience :)

garg3133 avatar Dec 14 '23 16:12 garg3133

@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.

garg3133 avatar Dec 21 '23 17:12 garg3133