问题
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);
}
回答1:
Making any of the elements exists
You can use querySelectorAll
and waitFor
together to solve this problem. Using all selectors with comma will return all nodes that matches any of the selector.
await page.waitFor(() =>
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.
回答2:
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
回答3:
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
}
回答4:
Puppeteer methods might throw errors if they are unable to fufill a request. For example, page.waitForSelector(selector[, options]) might fail if the selector doesn't match any nodes during the given timeframe.
For certain types of errors Puppeteer uses specific error classes. These classes are available via require('puppeteer/Errors').
List of supported classes:
TimeoutError
An example of handling a timeout error:
const {TimeoutError} = require('puppeteer/Errors');
// ...
try {
await page.waitForSelector('.foo');
} catch (e) {
if (e instanceof TimeoutError) {
// Do something if this is a timeout.
}
}
回答5:
Combining some elements from above into a helper method, I've built a command that allows me to create multiple possible selector outcomes and have the first to resolve be handled.
/**
* @typedef {import('puppeteer').ElementHandle} PuppeteerElementHandle
* @typedef {import('puppeteer').Page} PuppeteerPage
*/
/** Description of the function
@callback OutcomeHandler
@async
@param {PuppeteerElementHandle} element matched element
@returns {Promise<*>} can return anything, will be sent to handlePossibleOutcomes
*/
/**
* @typedef {Object} PossibleOutcome
* @property {string} selector The selector to trigger this outcome
* @property {OutcomeHandler} handler handler will be called if selector is present
*/
/**
* Waits for a number of selectors (Outcomes) on a Puppeteer page, and calls the handler on first to appear,
* Outcome Handlers should be ordered by preference, as if multiple are present, only the first occuring handler
* will be called.
* @param {PuppeteerPage} page Puppeteer page object
* @param {[PossibleOutcome]} outcomes each possible selector, and the handler you'd like called.
* @returns {Promise<*>} returns the result from outcome handler
*/
async function handlePossibleOutcomes(page, outcomes)
{
var outcomeSelectors = outcomes.map(outcome => {
return outcome.selector;
}).join(', ');
return page.waitFor(outcomeSelectors)
.then(_ => {
let awaitables = [];
outcomes.forEach(outcome => {
let await = page.$(outcome.selector)
.then(element => {
if (element) {
return [outcome, element];
}
return null;
});
awaitables.push(await);
});
return Promise.all(awaitables);
})
.then(checked => {
let found = null;
checked.forEach(check => {
if(!check) return;
if(found) return;
let outcome = check[0];
let element = check[1];
let p = outcome.handler(element);
found = p;
});
return found;
});
}
To use it, you just have to call and provide an array of Possible Outcomes and their selectors / handlers:
await handlePossibleOutcomes(page, [
{
selector: '#headerNavUserButton',
handler: element => {
console.log('Logged in',element);
loggedIn = true;
return true;
}
},
{
selector: '#email-login-password_error',
handler: element => {
console.log('password error',element);
return false;
}
}
]).then(result => {
if (result) {
console.log('Logged in!',result);
} else {
console.log('Failed :(');
}
})
来源:https://stackoverflow.com/questions/49946728/puppeteer-waitforselector-on-multiple-selectors