Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Puppeteer waitForSelector on multiple selectors

I have Puppeteer controlling a website with a lookup form that can either return a result or a "No records found" message. How can I tell which was returned? waitForSelector seems to wait for only one at a time, while waitForNavigation doesn't seem to work because it is returned using Ajax. I am using a try catch, but it is tricky to get right and slows everything way down.

try {
    await page.waitForSelector(SELECTOR1,{timeout:1000}); 
}
catch(err) { 
    await page.waitForSelector(SELECTOR2);
}
like image 330
Jon Wilson Avatar asked Apr 20 '18 17:04

Jon Wilson


7 Answers

Making any of the elements exists

You can use querySelectorAll and waitForFunction together to solve this problem. Using all selectors with comma will return all nodes that matches any of the selector.

await page.waitForFunction(() => 
  document.querySelectorAll('Selector1, Selector2, Selector3').length
);

Now this will only return true if there is some element, it won't return which selector matched which elements.

like image 154
Md. Abu Taher Avatar answered Oct 25 '22 23:10

Md. Abu Taher


how about using Promise.race() like something I did in the below code snippet, and don't forget the { visible: true } option in page.waitForSelector() method.

public async enterUsername(username:string) : Promise<void> {
    const un = await Promise.race([
        this.page.waitForSelector(selector_1, { timeout: 4000, visible: true })
        .catch(),
        this.page.waitForSelector(selector_2, { timeout: 4000, visible: true })
        .catch(),
    ]);

    await un.focus();
    await un.type(username);
}
like image 41
Alferd Nobel Avatar answered Oct 25 '22 23:10

Alferd Nobel


An alternative and simple solution would be to approach this from a more CSS perspective. waitForSelector seems to follow the CSS selector list rules. So essentially you can select multiple CSS elements by just using a comma.

try {    
    await page.waitForSelector('.selector1, .selector2',{timeout:1000})
} catch (error) {
    // handle error
}
like image 20
Jon Black Avatar answered Oct 25 '22 23:10

Jon Black


Using Md. Abu Taher's suggestion, I ended up with this:

// One of these SELECTORs should appear, we don't know which
await page.waitForFunction((sel) => { 
    return document.querySelectorAll(sel).length;
},{timeout:10000},SELECTOR1 + ", " + SELECTOR2); 

// Now see which one appeared:
try {
    await page.waitForSelector(SELECTOR1,{timeout:10});
}
catch(err) {
    //check for "not found" 
    let ErrMsg = await page.evaluate((sel) => {
        let element = document.querySelector(sel);
        return element? element.innerHTML: null;
    },SELECTOR2);
    if(ErrMsg){
        //SELECTOR2 found
    }else{
        //Neither found, try adjusting timeouts until you never get this...
    }
};
//SELECTOR1 found
like image 36
Jon Wilson Avatar answered Oct 26 '22 01:10

Jon Wilson


I had a similar issue and went for this simple solution:

helpers.waitForAnySelector = (page, selectors) => new Promise((resolve, reject) => {
  let hasFound = false
  selectors.forEach(selector => {
    page.waitFor(selector)
      .then(() => {
        if (!hasFound) {
          hasFound = true
          resolve(selector)
        }
      })
      .catch((error) => {
        // console.log('Error while looking up selector ' + selector, error.message)
      })
  })
})

And then to use it:

const selector = await helpers.waitForAnySelector(page, [
  '#inputSmsCode', 
  '#buttonLogOut'
])

if (selector === '#inputSmsCode') {
  // We need to enter the 2FA sms code. 
} else if (selector === '#buttonLogOut') {
  // We successfully logged in
}
like image 24
Gabriel Morin Avatar answered Oct 25 '22 23:10

Gabriel Morin


In puppeteer you can simply use multiple selectors separated by coma like this:

const foundElement = await page.waitForSelector('.class_1, .class_2');

The returned element will be an elementHandle of the first element found in the page.

Next if you want to know which element was found you can get the class name like so:

const className = await page.evaluate(el => el.className, foundElement);

in your case a code similar to this should work:

const foundElement = await page.waitForSelector([SELECTOR1,SELECTOR2].join(','));
const responseMsg = await page.evaluate(el => el.innerText, foundElement);
if (responseMsg == "No records found"){ // Your code here }
like image 31
Andyba Avatar answered Oct 26 '22 01:10

Andyba


One step further using Promise.race() by wrapping it and just check index for further logic:

// Typescript
export async function racePromises(promises: Promise<any>[]): Promise<number> {
  const indexedPromises: Array<Promise<number>> = promises.map((promise, index) => new Promise<number>((resolve) => promise.then(() => resolve(index))));
  return Promise.race(indexedPromises);
}
// Javascript
export async function racePromises(promises) {
  const indexedPromises = promises.map((promise, index) => new Promise((resolve) => promise.then(() => resolve(index))));
  return Promise.race(indexedPromises);
}

Usage:

const navOutcome = await racePromises([
  page.waitForSelector('SELECTOR1'),
  page.waitForSelector('SELECTOR2')
]);
if (navigationOutcome === 0) {
  //logic for 'SELECTOR1'
} else if (navigationOutcome === 1) {
  //logic for 'SELECTOR2'
}


like image 34
LeOn - Han Li Avatar answered Oct 25 '22 23:10

LeOn - Han Li