We use Chrome recorder and export the recordings as puppeteer Javascript files, which are in CommonJS. They run fine with node.
Changing the puppeteer import to ESM, we get the error: TypeError: Cannot read properties of undefined (reading 'race')
.
What is the reason and how can this be fixed?
// const puppeteer = require('puppeteer'); // CommonJS
import puppeteer from 'puppeteer-core'; // ESM
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
const timeout = 30000;
page.setDefaultTimeout(timeout);
{
const targetPage = page;
await targetPage.setViewport({
width: 1920,
height: 1200
})
}
{
const targetPage = page;
const promises = [];
const startWaitingForEvents = () => {
promises.push(targetPage.waitForNavigation());
}
startWaitingForEvents();
await targetPage.goto('http://localhost:4200/');
await Promise.all(promises);
}
{
const targetPage = page;
await puppeteer.Locator.race([ // <--------------------
targetPage.locator('app-header li:nth-of-type(1) > a'),
targetPage.locator('::-p-xpath(/html/body/app-root/app-layout/app-header/nav/div/div/ul/li[1]/a)'),
targetPage.locator(':scope >>> app-header li:nth-of-type(1) > a'),
targetPage.locator('::-p-aria( Override User)'),
targetPage.locator('::-p-text(Override User)')
])
.setTimeout(timeout)
.click({
offset: {
x: 68.199951171875,
y: 11.625,
},
});
}
We would like to use mocha, and adapt the exported recordings as less as possible. We could instead of puppeteer.Locator.race use Promise.race. But the following is NOT an option for us,as we would need to adapt the whole file:
const elementPromises = [
page.waitForSelector('app-header li:nth-of-type(1) > a'),
page.waitForSelector('::-p-xpath(/html/body/app-root/app-layout/app-header/nav/div/div/ul/li[1]/a)'),
page.waitForSelector(':scope >>> app-header li:nth-of-type(1) > a'),
page.waitForSelector('::-p-aria( Override User)'),
page.waitForSelector('::-p-text(Override User)')
];
const firstElement = await Promise.race(elementPromises);
await firstElement.click({
offset: { x: 68.199951171875, y: 11.625 }
});
You can use a destructured import:
import puppeteer, {Locator} from "puppeteer"; // ^23.3.0
console.log(!!Locator.race); // => true
or a namespaced import:
import * as puppeteer from "puppeteer";
console.log(!!puppeteer.Locator.race); // => true
or use the CSS comma "OR" operator:
await targetPage
.locator(
`app-header li:nth-of-type(1) > a,
::-p-xpath(/html/body/app-root/app-layout/app-header/nav/div/div/ul/li[1]/a),
:scope >>> app-header li:nth-of-type(1) > a,
::-p-aria( Override User),
::-p-text(Override User)`
)
.setTimeout(timeout)
.click({
offset: {
x: 68.199951171875,
y: 11.625,
},
});
Minimal, reproducible proof that the comma operator works across multiple lines:
import puppeteer from "puppeteer";
const ps = [..."ABC"].sort(() => Math.random() - 0.5);
const html = `${ps.map(e => `<p>${e}</p>`).join("")}
<script>
document.querySelectorAll("p").forEach(e => {
e.addEventListener("click", event => {
document.body.textContent = event.target.textContent;
});
});
</script>`;
let browser;
(async () => {
browser = await puppeteer.launch();
const [page] = await browser.pages();
await page.setContent(html);
console.log(await page.$$eval("p", els => els.map(el => el.textContent)));
await page.locator(`
::-p-text(A),
::-p-text(B),
::-p-text(C)
`).click();
console.log(await page.$eval("body", el => el.textContent));
})()
.catch(err => console.error(err))
.finally(() => browser?.close());
As a nitpick aside, ::-p-xpath(/html/body/app-root/app-layout/app-header/nav/div/div/ul/li[1]/a),
is brittle and liable to break, as described in the Playwright docs (yes, different framework, but still holds true) and my blog post on Puppeteer antipatterns. If a single element in that hierarchy changes, the whole edifice falls apart.