I am currently trying to figure out how to render my React app within the puppeteer browser so that I could take a screenshot and perform visual regression testing in the future. Most of the ways I've seen this achieved was by starting up an express server and serving up the html page when a certain endpoint is requested. However, I'd like to avoid starting up a server if possible and would like to perhaps use something like ReactDOM.render within the puppeteer browser to render it like how React would.
So far, I've tried creating a build of the React app and then using puppeteer's page.setContent api to render out the build's index.html to the puppeteer browser directly. However, when I do this, the browser doesn't render anything on screen and does not make network requests for the static files.
This is what it looks like in puppeteer: puppeteer browser
This is the method with page.setContent:
const buildHTML = await fs.readFileSync('build/index.html', 'utf8');
// create a new browser tab
const page = await browser.newPage();
await page.setViewport({ width: 1920, height: 1080 });
// set page's html content with build's index.html
await page.setContent(buildHTML);
// take a screenshot
const contentLoadingPageScreenshot = await page.screenshot({fullPage: true});
Also, if anyone knows of a better way to run an image snapshot test without having to start up a server, please let me know. Thank you.
I am currently trying to figure out how to render my React app within the puppeteer browser so that I could take a screenshot and perform visual regression testing in the future. Most of the ways I've seen this achieved was by starting up an express server and serving up the html page when a certain endpoint is requested. However, I'd like to avoid starting up a server if possible and would like to perhaps use something like ReactDOM.render within the puppeteer browser to render it like how React would.
So far, I've tried creating a build of the React app and then using puppeteer's page.setContent api to render out the build's index.html to the puppeteer browser directly. However, when I do this, the browser doesn't render anything on screen and does not make network requests for the static files.
This is what it looks like in puppeteer: puppeteer browser
This is the method with page.setContent:
const buildHTML = await fs.readFileSync('build/index.html', 'utf8');
// create a new browser tab
const page = await browser.newPage();
await page.setViewport({ width: 1920, height: 1080 });
// set page's html content with build's index.html
await page.setContent(buildHTML);
// take a screenshot
const contentLoadingPageScreenshot = await page.screenshot({fullPage: true});
Also, if anyone knows of a better way to run an image snapshot test without having to start up a server, please let me know. Thank you.
Share Improve this question edited Aug 7, 2019 at 14:07 user11885987 asked Aug 5, 2019 at 18:36 user11885987user11885987 411 silver badge3 bronze badges 2- Maybe the document.write used by setContent is not triggering the event (document load?) your app is using to render the HTML? – hardkoded Commented Aug 5, 2019 at 18:40
-
It doesn't make sense to
await
a sync function, soawait fs.readFileSync('build/index.html', 'utf8');
should probably beawait fs.readFile('build/index.html', 'utf8');
with the import asimport fs from "node:fs/promises";
. Also, without the HTML, it's not clear that there are images in the app. This would benefit from a minimal reproducible example. Thanks. – ggorlen Commented Mar 17, 2023 at 14:04
2 Answers
Reset to default 4With ref: puppeteer doc, you can put options for setContent to wait for the network request
await page.setContent(reuslt, {
waitUntil: ["load","networkidle0"]
});
This problem reduces to Puppeteer wait until page is pletely loaded in many respects, so I suggest giving that thread a read.
networkidle0
is a reasonable answer and easy to code, but has the downside of being "one size fits all". After all the network requests resolve, the goto
promise won't resolve for another 500ms. Not exactly an eternity, but also unnecessary overhead that adds up if you have many requests to handle.
If you're in control of the build/index.html
file and know which resources it's requesting, a more verbose but faster approach is to use waitForResponse
to block on each resource. Once all resources have arrived, you know the precise moment you can capture a screenshot (keeping in mind the next paragraph).
The same approach can be applied to page.waitForSelector
and page.waitForFunction
. These can help ensure the page is in the precise state, in addition to resource responses arriving.
This may sound like a good deal of work, but for most sites, it probably es down to a couple of simple lines.
Here's a somewhat contrived example for illustration.
index.html:
<!doctype html>
<html lang="en">
<head>
<!-- sure, we could use fetch but this makes it more interesting -->
<script src="https://cdnjs.cloudflare./ajax/libs/axios/1.7.2/axios.min.js"></script>
<link
rel="stylesheet"
href="https://unpkg./[email protected]/dist/dark-grey.css"
/>
</head>
<body class="dark-grey">
<img src="https://picsum.photos/100/100" alt="accessibility" />
<script>
axios
.get("https://jsonplaceholder.typicode./users/1")
.then(({ data }) => {
const p = document.createElement("p");
p.textContent = data.name;
document.body.appendChild(p);
});
</script>
</body>
</html>
This example page has a script, a JSON file, an image and a stylesheet that need to be block for before it's stable. There's also a selector that's added after page load we can block for as well. Here's how:
const fs = require("node:fs/promises");
const puppeteer = require("puppeteer"); // ^22.10.0
let browser;
(async () => {
browser = await puppeteer.launch();
const [page] = await browser.pages();
const html = await fs.readFile("index.html", "utf-8");
const loaded = Promise.all([
page.waitForResponse(e =>
e.url() === "https://unpkg./[email protected]/dist/dark-grey.css"
),
page.waitForResponse(e =>
e.url() === "https://picsum.photos/100/100"
),
page.waitForResponse(e =>
e.url() === "https://jsonplaceholder.typicode./users/1"
),
page.waitForFunction("!!window.axios"),
page.waitForSelector("p::-p-text(Leanne Graham)"),
]);
await page.setContent(html, {waitUntil: "domcontentloaded"});
await loaded;
await page.evaluate(`
Promise.all(
[...document.querySelectorAll("img")].map(e => e.decode())
)
`);
await page.screenshot({path: "index.png"});
})()
.catch(err => console.error(err))
.finally(() => browser?.close());
This is overzealous for illustration. We can simplify this to just wait for the image load, the stylesheet and the text. For those to resolve, it's guaranteed that axios and the JSON file have loaded as well and can be omitted.
Rough speed parison between the code above:
real 0m1.355s
user 0m0.616s
sys 0m0.139s
and networkidle0 on its own:
real 0m2.540s
user 0m0.633s
sys 0m0.134s