For those looking for a quick answer, here's the main code:
await Promise.all([page.waitForNavigation(), el.click()]);
...where el
is a link that points to another page in the SPA and click
can be any event that causes navigation. See below for details.
I agree that waitFor
isn't too helpful if you can't rely on page content. Even if you can, in most cases it seems like a less desirable approach than naturally reacting to the navigation. Luckily, page.waitForNavigation
does work on SPAs. Here's a minimal, complete example of navigating between pages using a click event on a link (the same should work for a form submission) on a tiny vanilla SPA mockup which uses the history API (index.html
below). I used Node 10 and Puppeteer 5.4.1.
index.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
</head>
<body>
<script>
const nav = `<a href="/">Home</a> | <a href="/about">About</a> |
<a href="/contact">Contact</a>`;
const routes = {
"/": `<h1>Home</h1>${nav}<p>Welcome home!</p>`,
"/about": `<h1>About</h1>${nav}<p>This is a tiny SPA</p>`,
};
const render = path => {
document.body.innerHTML = routes[path] || `<h1>404</h1>${nav}`;
document.querySelectorAll('[href^="/"]').forEach(el =>
el.addEventListener("click", evt => {
evt.preventDefault();
const {pathname: path} = new URL(evt.target.href);
window.history.pushState({path}, path, path);
render(path);
})
);
};
window.addEventListener("popstate", e =>
render(new URL(window.location.href).pathname)
);
render("/");
</script>
</body>
</html>
index.js
:
const puppeteer = require("puppeteer");
let browser;
(async () => {
browser = await puppeteer.launch();
const page = await browser.newPage();
// navigate to the home page for the SPA and print the contents
await page.goto("http://localhost:8000");
console.log(page.url());
console.log(await page.$eval("p", el => el.innerHTML));
// navigate to the about page via the link
const [el] = await page.$x('//a[text()="About"]');
await Promise.all([page.waitForNavigation(), el.click()]);
// show proof that we're on the about page
console.log(page.url());
console.log(await page.$eval("p", el => el.innerHTML));
})()
.catch(err => console.error(err))
.finally(async () => await browser.close())
;
Sample run:
$ python3 -m http.server &
$ node index.js
http://localhost:8000/
Welcome home!
http://localhost:8000/about
This is a tiny SPA
If the await Promise.all([page.waitForNavigation(), el.click()]);
pattern seems strange, see this issue thread which explains the gotcha that the intuitive
await page.waitForNavigation();
await el.click();
causes a race condition.
The same thing as the Promise.all
shown above can be done with:
const navPromise = page.waitForNavigation({timeout: 1000});
await el.click();
await navPromise;
See this related answer for more on navigating SPAs with Puppeteer including hash routers.