I have a React component with some componentDidMount logic:
export default class MyComponent {
componentDidMount() {
// some changes to DOM done here by a library
}
render() {
return (
<div>{props.data}</div>
);
}
}
Is it possible to pass this component with props so that everything in componentDidMount() gets executed, somehow to puppeteer in order to take a screenshot? Something along these lines:
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
const html = ReactDOMServer.renderToString(<MyComponent data='' />); <-- but this skips the componentDidMount logic
await page.setContent(html);
await page.screenshot({ path: 'screenshot.png' });
I know I could use page.goto()
, but I have some complex login logic that I would like to avoid with a shortcut like this and instead pass all the needed props just directly to the component?
Jest and Puppeteer are a combination that can't go wrong when it comes to testing React apps.
You can pass a component as props in React by using the built-in children prop. All elements you pass between the opening and closing tags of a component get assigned to the children prop. Copied!
Now you can control the browser (running on the server) form the client-side with a puppeteer bundle for the client.
You want to use JSX inside your props You can simply use {} to cause JSX to parse the parameter. The only limitation is the same as for every JSX element: It must return only one root element.
I answered this question here. Let's try the same here.
Install babel, webpack and puppeteer packages.
{
"name": "react-puppeteer",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"compile": "webpack",
"build": "webpack -p",
"start": "webpack && node pup.js"
},
"devDependencies": {
"babel-core": "^6.26.0",
"babel-loader": "^7.1.2",
"babel-preset-env": "^1.6.1",
"babel-preset-es2015": "^6.24.1",
"babel-preset-react": "^6.24.1",
"react": "^16.2.0",
"react-dom": "^16.2.0",
"webpack": "^3.10.0",
"webpack-dev-middleware": "^2.0.3"
},
"dependencies": {
"puppeteer": "^0.13.0"
}
}
Prepare webpack config,
const webpack = require('webpack');
const loaders = [
{
test: /\.jsx?$/,
exclude: /node_modules/,
loader: 'babel-loader',
query: {
presets: ['babel-preset-es2015', 'babel-preset-react'],
plugins: []
}
}
];
module.exports = {
entry: './entry.js',
output: {
path: __dirname,
filename: 'bundle.js',
libraryTarget: 'umd'
},
module: {
loaders: loaders
}
};
Create entry file, On this file, instead of mounting the element directly, export it to window so that we can access it later.
import React from 'react';
import { render } from 'react-dom';
class Hello extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
// accept a name for example and a domNode where to render
function renderIt(name, domNode) {
render(<Hello name={name} />, domNode);
}
window.renderIt = renderIt;
When we run webpack
, it's going to produce a bundle.js file. We can use it on puppeteer.
They have deprecated the injectFile function on puppeteer, so we are going to resurrect it. Here is a sample repo for that, you can yarn add it.
https://github.com/entrptaher/puppeteer-inject-file
Now, lets create a puppeteer script.
const puppeteer = require('puppeteer');
const injectFile = require('puppeteer-inject-file');
(async () => {
const browser = await puppeteer.launch({ headless: false });
const page = await browser.newPage();
await page.goto('https://github.com');
await injectFile(page, require.resolve('./bundle.js'));
await page.evaluate(() => {
renderIt("Someone", document.querySelector('div.jumbotron.jumbotron-codelines > div > div > div > h1'));
});
await page.screenshot({ path: 'example.png' });
await browser.close();
})();
And when we run this, we get following result,
If we added a componentDidMount()
call, we could have done that too. But if we are trying to do more modification, we have to make the puppeteer script wait for that which have been discussed many times in other questions.
Say we have a state now which will return something once component is loaded.
class Hello extends React.Component {
state = {
jokes: null
};
componentDidMount() {
const self = this;
const jokesUrl = `http://api.icndb.com/jokes/random?firstName=John&lastName=Doe`;
fetch(jokesUrl)
.then(data => data.json())
.then(data => {
self.setState({
jokes: data.value.joke
});
});
}
render() {
if(!!this.state.jokes){
return <p id='quote'>{this.state.jokes}</p>
}
return <h1>Hello, {this.props.name}</h1>;
}
}
On puppeteer, I can wait for the element like this,
...
await injectFile(page, require.resolve('./bundle.js'));
await page.evaluate(() => {
renderIt("Someone", document.querySelector('div'));
});
await page.waitFor('p#quote');
...
We might need babel-preset-stage-2 but I'll leave that to you. And here is the result,
Figure the rest of the problem yourself :) ...
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With