Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to execute one element then and then another in protractor

This following code behaving randomly sometime it works fine and sometime it throws error like this Stale Element Reference Exception

what i want is i want to get this below executed first

element(by.id('FiltItemTransDocNo')).sendKeys(grno);

after above i want this to get executed this below

element.all(by.name('chkGrd')).first().click();

i have tried this way but it didnt seems to work

element(by.id('FiltItemTransDocNo')).sendKeys(grno).then(function(el){
     element.all(by.name('chkGrd')).first().click();
});

help me out with this

i have attached the image i am sending keys to Purchase Rquisition field and according i came up with result which will show only one result and i want to click and if i will put condition for visiblity it will always be true then i will lead to same issue enter image description here

like image 728
Rao Avatar asked Dec 17 '18 10:12

Rao


3 Answers

A quick note on sendKeys and e1

A quick note that sendKeys does not return a WebElement or ElementFinder. This means that e1 in the example above is probably undefined.

Stale elements and the DOM changing

Quick note on assumptions: The answer has an assumption that sending text to a filter would change the row count or number of elements on the screen. If there are the same number of elements on the screen after you send in the text, then this would not work. I would look at Florent's comment below on the stale reference error.

Stale elements usually happen when the DOM has changed. If you are using some structural directive in Angular, the DOM will change if you are using *ngFor or *ngIf. My guess is that after you filter the the element, you are getting the elements in the DOM during or before the DOM actually changes based on your filter. This would result in a stale referenced web element. In my examplex below, I am using async / await with the control flow off.

Explicit wait during the filter

You could explicitly set a sleep so the DOM will update before you make a call to click the first element. This could lead to potentially flaky tests because the timeout is unknown based on the environment you will run in.

it('should do something', async () => {
  const filterItem = element(by.id('FiltItemTransDocNo'));
  await filterItem.sendKeys(grno);
  await browser.sleep(1000);  // or some other waits
  await element.all(by.name('chkGrd')).first().click();
});

Compare rows for element.all

Alternatively you could do a comparison check for the amount of element.all items you have before and after the click and only move on when things are updated.

it('should do something', async () => {
  const filterItem = element(by.id('FiltItemTransDocNo'));
  const table = element.all(by.name('chkGrd'));
  const length = await table.count();

  await filterItem.sendKeys(grno);

  // After the filter keys are sent, check to see if the current table
  // count is not equal to the `table.count()`.
  let updated = false;
  await browser.wait(async () => {
    updated = length !== await table.count();
    return updated;
  }, 5000);

  // So if we use the entire 5 seconds and the count has not changed,
  // we should probably fail before clicking on stuff.
  expect(updated).toBeTruthy();

  // now we can click on the next element.
  await element.all(by.name('chkGrd')).first().click();
});

Why does calling length !== await table.count() work? This is because the table represents a promise to get the web element. When you call the count method, it executes the action by first resolving the web element. This could potentially be different depending if the DOM changes. We then compare the current count to the previous one.

Making sure you are using async / await

In your configuration file, you will need to specify that you are off the control flow:

exports.config = {
    // In this case, I plan to use a selenium standalone server
    // at http://127.0.0.1:4444/wd/hub. You could also use other 
    // methods like direct connect or starting it up with 'local'.
    seleniumAddress: 'http://127.0.0.1:4444/wd/hub',

    // Required flag to tell Protractor you do not want to use the
    // control flow. This means that you will have to either chain
    // your promises or async / await your actions. Historically
    // jasminewd node module would resolve promises for you. This
    // package will no longer be used in future releases since the
    // control flow is deprecated.
    SELENIUM_PROMISE_MANAGER: false,

    // The rest of your config...
}

Hope that helps.

like image 76
cnishina Avatar answered Nov 17 '22 04:11

cnishina


there is several solutions for stale element reference.

First:

    let filterItem = element(by.id('FiltItemTransDocNo'));
    browser.wait(ExpectedConditions.visibilityOf(filterItem), 5000, 'Element is not visible.');
    filterItem.sendKeys('some random text');

    let elementToClick = element.all(by.name('chkGrd')).first();
    browser.wait(ExpectedConditions.elementToBeClickable(elementToClick), 5000, 'Element is not clickable.');
    elementToClick.click();

You can also chain them:

browser.wait(ExpectedConditions.visibilityOf(filterItem), 5000, 'Element is not visible.').then( () => {
                filterItem.sendKeys('some random text');

                browser.wait(ExpectedConditions.elementToBeClickable(elementToClick), 5000, 'Element is not clickable.').then( () => {
                    elementToClick.click();
                });
            });

Or second way, to refresh the element on stale error:

let filterItem = element(by.id('FiltItemTransDocNo'));

            try {
                filterItem.sendKeys('some random text');
            } catch (e) {
                if (e instanceof StaleElementReferenceError) {
                    filterItem = element(by.id('FiltItemTransDocNo'));
                    filterItem.sendKeys('text');
                }
            }
like image 29
Infern0 Avatar answered Nov 17 '22 03:11

Infern0


As I explained in an answer to another question, Protractor and the tools it uses are all changing to use native JavaScript promises, with the expectation that everyone will migrate their code to a fully async style of coding. Unfortunately, they are not making it easy because you cannot mix the old style code with the new style. So first things first, make sure you entire suite is written async style and you have set SELENIUM_PROMISE_MANAGER: false in your config.

What happens in response to the sendKeys? If that triggers an AJAX call and the response to that call changes the DOM, then you are probably going to have to do what @cnishina suggests and poll the DOM to wait for the change to land. If, however, only Angular client-side changes happen, then this code should work:

it('should do something', async () => {
  await element(by.id('FiltItemTransDocNo')).sendKeys(grno);
  await element.all(by.name('chkGrd')).first().click();
});

Both element calls synchronize with Angular to make sure that Angular updates are finished before the locators proceed, so you should be OK as far as Angular is concerned.

like image 40
Old Pro Avatar answered Nov 17 '22 04:11

Old Pro