How do I image snapshot test a React component using HTML5 canvas? - react-testing-library

I'm attempting to get image snapshot testing working (i.e., not mocked) with a React component that renders an HTML5 canvas. I'm using Jest, React Testing Library, Node Canvas, Puppeteer, and Jest Image Snapshot.
Given the following React component's render():
public render(): React.ReactElement<TestCanvas> {
const { innerWidth, innerHeight } = window;
return (
<div id="canvas" style={{ height: `${innerHeight}px`, width: `${innerWidth}px` }}>
<canvas ref={this.canvasRef} />
</div>
);
}
Here's what a Jest test might look like:
it('should render a <TestCanvas/> component', async () => {
const { container } = render(<TestCanvas />);
const page: puppeteer.Page = await browser.newPage();
await page.setContent(container.outerHTML);
const image: string = await page.screenshot();
expect(image).toMatchImageSnapshot();
});
However, this test generates an empty, white, 800x600 PNG image as the baseline.
If, however, I change the test to this:
it('should render a <TestCanvas/> component', async () => {
const { container } = render(<TestCanvas />);
const canvas: HTMLCanvasElement = container.querySelector('canvas') as HTMLCanvasElement;
const img = document.createElement('img');
img.src = canvas.toDataURL();
const page: puppeteer.Page = await browser.newPage();
await page.setContent(img.outerHTML);
const image: string = await page.screenshot();
expect(image).toMatchImageSnapshot();
});
It generates the baseline PNG snapshot based on my React component just fine.
I'm currently trying to debug where in the pipeline things are going screwy.

I have done html5 canvas image snapshot with a method that does not use puppeteer, but the puppeteer method is interesting. Here is the method I used
test('canvas image snapshot', async () => {
const { getByTestId } = render(
<MyComponent />,
)
const canvas = await waitForElement(() =>
getByTestId('mycanvas'),
)
const img = canvas.toDataURL()
const data = img.replace(/^data:image\/\w+;base64,/, '')
const buf = Buffer.from(data, 'base64')
// may need to do fuzzy image comparison because, at least for me, on
// travis-ci it was sometimes 2 pixel diff or more for font related stuff
expect(buf).toMatchImageSnapshot({
failureThreshold: 0.5,
failureThresholdType: 'percent',
})
})
Uses https://github.com/americanexpress/jest-image-snapshot for toMatchImageSnapshot

The answer from #Colin above is what one needs to do. Namely, strip off the image encoding from the canvas URL because [apparently] it's meant for a browser. Here's what we did:
it('should scroll down', () => {
const { getByTestId } = render(<PaneDrawerTestWrapper />);
const mouseHandler = getByTestId(mouseHandlerId);
act(() => {
fireEvent.wheel(mouseHandler, { deltaY: 100 });
});
const canvas = getByTestId(canvasId) as HTMLCanvasElement;
const image = stripEncoding(canvas.toDataURL());
expect(image).toMatchImageSnapshot();
});
Where stripEncoding looks like:
export function stripEncoding(canvasImageUrl: string): string {
return canvasImageUrl.replace(/^data:image\/(png|jpg);base64,/, '');
}

Related

html2canvas not rendering on safari

I'm trying to save image from canvas drawing, it works 2-3 times giving me datatourl i save in an hidden input field, then it stops going thru for some reason, it's not rendered and i can't get base64 in input field anymore.
function downloadClick() {
self.resetTransformer();
setTimeout(async function () {
const url = await createImage();
if (url) downloadImage(url, "draw.jpg");
if (url) await saveImage(url);
}, 200)
}
async function createImage() {
const {default: html2canvas} = await import('html2canvas');
self.resetTransformer();
return html2canvas(self.container.querySelector('.konvajs-content'), {
backgroundColor: null,
scrollY: -window.pageYOffset,
logging: true,
useCORS: true
})
.then(canvas => {
return canvas.toDataURL("image/png");
})
.catch(error => console.error(error));
}
function downloadImage(url, name) {
const link = document.createElement('a');
link.download = name;
link.href = url;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
async function saveImage(img) {
let imgForm = document.querySelector('#drawImage');
let hiddenInput = document.querySelector('#game-image');
hiddenInput.value = img;
imgForm.setAttribute('crossorigin', 'anonymous');
imgForm.setAttribute('data-src', img);
imgForm.setAttribute('src', img);
}
}

React app with axios inside useEffect() is rendering twice

I've been trying to figure out why my function that draws a BPMN diagram is rendering the diagram twice. I have looked at various examples using axios inside useEffect but cannot figure out why it is happening. The url is returning a valid xml for the viewer.
Can anyone give me some guidance?
Here is the entire function
function RenderBPMN(pathDefinition) {
const [diagram, setDiagram] = useState("");
const container = document.getElementById("container");
const msg = JSON.stringify(pathDefinition);
const url = `http://localhost:9090/xml?path=${msg}`;
useEffect(() => {
const fetchData = async () => {
axios
.get(url)
.then((resp) => {
setDiagram(resp.data);
})
.catch ((error) => {
console.log(error);
});
};
fetchData();
}, [url]);
if (diagram.length > 0) {
const viewer = new Viewer({
container,
keyboard: {
bindTo: document
}
});
viewer
.importXML(diagram)
.then(({ warnings }) => {
if (warnings.length) {
console.log("Warnings", warnings);
}
viewer.get('canvas').zoom('fit-viewport');
})
.catch((err) => {
console.log("error", err.message);
});
}
return (
<div
id="container"
style={{
border: "1px solid #010101",
height: "50vh",
width: "70vw",
margin: "auto"
}}
></div>
);
}
export default RenderBPMN;
Remove url from useEffect array. With empty array it will by called only once when component will be mounted.
useEffect( () => {
// your code
}, [ /* empty */ ])

access document.documentElement from puppeteer

I can get access to the entire HTML for any URL by opening dev-tools and typing:
document.documentElement
I am trying to replicate the same behavior using puppeteer, however, the snippet below returns {}
const puppeteer = require('puppeteer'); // v 1.1.0
const iPhone = puppeteer.devices['Pixel 2 XL'];
async function start(canonical_url) {
const browserURL = 'http://127.0.0.1:9222';
const browser = await puppeteer.connect({browserURL});
const page = await browser.newPage();
await page.emulate(iPhone);
await page.goto(canonical_url, {
waitUntil: 'networkidle2',
});
const data = await page.evaluate(() => document.documentElement);
console.log(data);
}
returns:
{}
Any idea on what I could be doing wrong here?

fetching data from api and shows on Render Method

I am fetched data from api but the problem is
when i am show data in render method then it showing "Undefine"
Please Help me to fix it
This is my code:-
var ProductData=''
export default class ApiProduct extends Component {
FetchProduct=()=>{
fetch('https://drawtopic.in/projects/wordpress/wp- json/wc/v2/products?consumer_key=ck_044491712632ef889ec13c75daff5879a8291674&consumer_secret=cs_a8e16c732e1812017e15d278e1dce2765a88c49b',{
method:'GET',
})
.then((response) => response.json())
.then((res) =>{
ProductData= res;
})
}
render() {
{this.FetchProduct()}
{console.warn(ProductData)}
return (
<View/>
)}
i Want to Show All data in render method
Here is a quick Expo example that should show you how to render a simple list. It is not a good idea to call fetch inside the render method, as every re-render will call the fetch.
Here is an expo snack https://snack.expo.io/S1-LKIyQE
import React from 'react';
import { Text, View, StyleSheet, FlatList, SafeAreaView } from 'react-native';
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = {
productData: []
}
}
async componentDidMount () {
await this.getData();
}
async getData() {
try {
let url ='https://drawtopic.in/projects/wordpress/wp-json/wc/v2/products?consumer_key=ck_044491712632ef889ec13c75daff5879a8291674&consumer_secret=cs_a8e16c732e1812017e15d278e1dce2765a88c49b'
let response = await fetch(url, { method:'GET' });
let responseJson = await response.json();
this.setState({productData: responseJson});
} catch (err) {
console.warn(err);
}
}
renderItem = ({item}) => {
return (
<View style={styles.mainItem}>
<Text>{item.name}</Text>
</View>
);
}
keyExtractor = (item, index) => {
return index.toString();
}
render() {
return (
<SafeAreaView style={styles.container}>
<FlatList
extraData={this.state}
data={this.state.productData}
keyExtractor={this.keyExtractor}
renderItem={this.renderItem}
/>
</SafeAreaView>
)
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'white'
},
mainItem: {
width:200,
height: 80,
justifyContent: 'center',
alignItems: 'center',
margin: 10,
backgroundColor: 'yellow',
borderColor: 'black',
borderWidth: 1
},
});
Here I have used async/await as it can make for much cleaner and clearer code. This is a great article on the differences between promises and async/await https://medium.com/#bluepnume/learn-about-promises-before-you-start-using-async-await-eb148164a9c8.
I have also given you a quick example on how to use a FlatList to display your data. You should check the docs on how to use it properly https://facebook.github.io/react-native/docs/flatlist
If you want to edit how each item is displayed on the screen then you need to update the renderItem method.
Try this way, if you have a question of how it works makes me to know.
let self;
export default class ApiProduct extends Component {
constructor(){
super();
self = this;
this.state = {
productData: null;
};
}
FetchProduct=()=>{
fetch('https://drawtopic.in/projects/wordpress/wp- json/wc/v2/products?consumer_key=ck_044491712632ef889ec13c75daff5879a8291674&consumer_secret=cs_a8e16c732e1812017e15d278e1dce2765a88c49b',{
method:'GET',
})
.then((response) => response.json())
.then((res) =>{
self.setState({ productData: res});
});
}
render() {
this.FetchProduct();
console.warn(self.state.productData);
return (
<View/>
);
}
I'll try to make order in your code.
Fetching data in the render method is not a good idea, it's better to use lifecycle methods, like componentDidMount. In order to handle your request result, set a state field and in your render read data from that field. So:
export default class ApiProduct extends Component {
constructor(){
super();
this.state = {
productData: undefined;
};
}
async componentDidMount(){
await this.fetchProduct();
}
fetchProduct = () => {
fetch('https://drawtopic.in/projects/wordpress/wp- json/wc/v2/products?consumer_key=ck_044491712632ef889ec13c75daff5879a8291674&consumer_secret=cs_a8e16c732e1812017e15d278e1dce2765a88c49b',{
method:'GET',
})
.then((response) => response.json())
.then((res) =>{
this.setState({
productData: res
})
})
}
render() {
const {productData} = this.state;
console.log(productData);
return (
<View/> // add here your code to render data properly
)
}}

Puppeteer Generate PDF from multiple HTML strings

I am using Puppeteer to generate PDF files from HTML strings.
Reading the documentation, I found two ways of generating the PDF files:
First, passing an url and call the goto method as follows:
page.goto('https://example.com');
page.pdf({format: 'A4'});
The second one, which is my case, calling the method setContent as follows:
page.setContent('<p>Hello, world!</p>');
page.pdf({format: 'A4'});
The thing is that I have 3 different HTML strings that are sent from the client and I want to generate a single PDF file with 3 pages (in case I have 3 HTML strings).
I wonder if there exists a way of doing this with Puppeteer? I accept other suggestions, but I need to use chrome-headless.
I was able to do this by doing the following:
Generate 3 different PDFs with puppeteer. You have the option of saving the file locally or to store it in a variable.
I saved the files locally, because all the PDF Merge plugins that I found only accept URLs and they don't accept buffers for instance. After generating synchronously the PDFs locally, I merged them using PDF Easy Merge.
The code is like this:
const page1 = '<h1>HTML from page1</h1>';
const page2 = '<h1>HTML from page2</h1>';
const page3 = '<h1>HTML from page3</h1>';
const browser = await puppeteer.launch();
const tab = await browser.newPage();
await tab.setContent(page1);
await tab.pdf({ path: './page1.pdf' });
await tab.setContent(page2);
await tab.pdf({ path: './page2.pdf' });
await tab.setContent(page3);
await tab.pdf({ path: './page3.pdf' });
await browser.close();
pdfMerge([
'./page1.pdf',
'./page2.pdf',
'./page3.pdf',
],
path.join(__dirname, `./mergedFile.pdf`), async (err) => {
if (err) return console.log(err);
console.log('Successfully merged!');
})
I was able to generate multiple PDF from multiple URLs from below code:
package.json
{
............
............
"dependencies": {
"puppeteer": "^1.1.1",
"easy-pdf-merge": "0.1.3"
}
..............
..............
}
index.js
const puppeteer = require('puppeteer');
const merge = require('easy-pdf-merge');
var pdfUrls = ["http://www.google.com","http://www.yahoo.com"];
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
var pdfFiles=[];
for(var i=0; i<pdfUrls.length; i++){
await page.goto(pdfUrls[i], {waitUntil: 'networkidle2'});
var pdfFileName = 'sample'+(i+1)+'.pdf';
pdfFiles.push(pdfFileName);
await page.pdf({path: pdfFileName, format: 'A4'});
}
await browser.close();
await mergeMultiplePDF(pdfFiles);
})();
const mergeMultiplePDF = (pdfFiles) => {
return new Promise((resolve, reject) => {
merge(pdfFiles,'samplefinal.pdf',function(err){
if(err){
console.log(err);
reject(err)
}
console.log('Success');
resolve()
});
});
};
RUN Command: node index.js
pdf-merger-js is another option. page.setContent should work just the same as a drop-in replacement for page.goto below:
const PDFMerger = require("pdf-merger-js"); // 3.4.0
const puppeteer = require("puppeteer"); // 14.1.1
const urls = [
"https://news.ycombinator.com",
"https://en.wikipedia.org",
"https://www.example.com",
// ...
];
const filename = "merged.pdf";
let browser;
(async () => {
browser = await puppeteer.launch();
const [page] = await browser.pages();
const merger = new PDFMerger();
for (const url of urls) {
await page.goto(url);
merger.add(await page.pdf());
}
await merger.save(filename);
})()
.catch(err => console.error(err))
.finally(() => browser?.close())
;