'How can I prove that two HTML pages look identical?
E.g., I have this:
<pre>
sun<br/>
mercury <br/>
venus <br/>
earth <br/>
mars <br/>
jupiter <br/>
saturn <br/>
</pre>
and this:
<div style="font-family:monospace">
<div style="text-indent: 0">sun</div> <br/>
<div style="text-indent: 4ch">mercury</div> <br/>
<div style="text-indent: 4ch">venus</div> <br/>
<div style="text-indent: 8ch">earth</div> <br/>
<div style="text-indent: 8ch">mars</div> <br/>
<div style="text-indent: 12ch">jupiter</div> <br/>
<div style="text-indent: 4ch">saturn</div> <br/>
</div>
And I want the second one to look exactly like the first.
I believe those look identical, but my only proof was to use the old "switch back and forth between windows real quick and eyeball it" technique. (Astronomers call this a "blink comparator" -- https://en.wikipedia.org/wiki/Blink_comparator ). I made sure the windows were the same size and in the same position. But if the rendered HTML didn't fit on the screen this might have been too difficult.
Is there a more definitive tool or method to do this comparison?
I looked at these in both Chrome 77.0.3865.120 and Firefox 69.0.3.
I know for instance that with the browser Acid tests that were originally part of the Web Standards Project -- https://www.acidtests.org/ -- pixel perfect rendering was the benchmark.
(Extra Credit: The HTML for the second code snippet is probably adequate for my needs; if you care to suggest improvements those would be welcome.)
EDIT: My question compares two small HTML samples, which can be rendered to fit on the visible portion of the browser. But in general I would like to know the answer for HTML that could be quite long.
Solution 1:[1]
I have done something similar when developing a CSS related webiste. I had to compare an output produced by a HTML/CSS with an image that was previously generated with a HTML/CSS.
I used dom-to-image, which converts code to a base64-encoded image. I place this image inside a canvas and then use pixelmatch to compare between both images.
Here is an example to illustrate:
var node1 = document.querySelector("pre");
var node2 = document.querySelector(".div");
var canvas1 = document.querySelector(".first");
var canvas2 = document.querySelector(".second");
var ctx1 = canvas1.getContext("2d");
var ctx2 = canvas2.getContext("2d");
/* Change both code to Image and put inside Canvas */
domtoimage.toPng(node1)
.then(function(dataUrl) {
var image = new Image();
image.onload = function() {
ctx1.drawImage(this, 0, 0);
};
image.src = dataUrl;
})
domtoimage.toPng(node2)
.then(function(dataUrl) {
var image = new Image();
image.onload = function() {
ctx2.drawImage(this, 0, 0);
};
image.src = dataUrl;
})
/* Run the pixel matching*/
setTimeout(function() {
var im_r = ctx1.getImageData(0, 0, 300, 300).data;
var im_o = ctx2.getImageData(0, 0, 300, 300).data;
var pixDiff = pixelmatch(im_r, im_o, false, 280, 280, {
threshold: 0.1
});
console.log(pixDiff);
}, 3000);
canvas {
border: 1px solid;
}
pre,
.div {
border: 2px solid red;
width: 300px;
height: 300px;
box-sizing: border-box;
margin: 0;
}
<script type="text/javascript" src="https://css-challenges.com/wp-content/themes/ronneby_child/js/dom-to-image.js"></script>
<script type="text/javascript" src="https://css-challenges.com/wp-content/themes/ronneby_child/js/pixel.js"></script>
<pre>
sun<br/>
mercury <br/>
venus <br/>
earth <br/>
mars <br/>
jupiter <br/>
saturn <br/>
</pre>
<div class="div" style="font-family:monospace">
<div style="text-indent: 0">sun</div> <br/>
<div style="text-indent: 4ch">mercury</div> <br/>
<div style="text-indent: 4ch">venus</div> <br/>
<div style="text-indent: 8ch">earth</div> <br/>
<div style="text-indent: 8ch">mars</div> <br/>
<div style="text-indent: 12ch">jupiter</div> <br/>
<div style="text-indent: 4ch">saturn</div> <br/>
</div>
<canvas width="300" height="300" class="first"></canvas>
<canvas width="300" height="300" class="second"></canvas>
In the above code, we have our 2 HTML blocks and 2 canvases where we will paint our blocks. As you can see, the JS is quite simple. The code at the end runs pixel matching and shows how many different pixels both canvases have. I added a delay to make sure both images are loaded (you can optimize later with some events)
You can also consider a third canvas to highlight the difference between both images and obtain your visual difference:
var node1 = document.querySelector("pre");
var node2 = document.querySelector(".div");
var canvas1 = document.querySelector(".first");
var canvas2 = document.querySelector(".second");
var canvas3 = document.querySelector(".result");
var ctx1 = canvas1.getContext("2d");
var ctx2 = canvas2.getContext("2d");
var ctx3 = canvas3.getContext("2d");
/* Change both code to Image and put inside Canvas */
domtoimage.toPng(node1)
.then(function(dataUrl) {
var image = new Image();
image.onload = function() {
ctx1.drawImage(this, 0, 0);
};
image.src = dataUrl;
})
domtoimage.toPng(node2)
.then(function(dataUrl) {
var image = new Image();
image.onload = function() {
ctx2.drawImage(this, 0, 0);
};
image.src = dataUrl;
})
/* Run the pixel matching*/
setTimeout(function() {
var im_r = ctx1.getImageData(0, 0, 300, 300).data;
var im_o = ctx2.getImageData(0, 0, 300, 300).data;
var diff = ctx3.createImageData(300, 300);
var pixDiff = pixelmatch(im_r, im_o, diff.data, 300, 300, {
threshold: 0.1
});
ctx3.putImageData(diff, 0, 0);
console.log(pixDiff);
}, 3000);
canvas {
border: 1px solid;
}
pre,
.div {
border: 2px solid red;
width: 300px;
height: 300px;
box-sizing: border-box;
margin: 0;
}
<script type="text/javascript" src="https://css-challenges.com/wp-content/themes/ronneby_child/js/dom-to-image.js"></script>
<script type="text/javascript" src="https://css-challenges.com/wp-content/themes/ronneby_child/js/pixel.js"></script>
<pre>
sun<br/>
mercury <br/>
venus <br/>
earth <br/>
mars <br/>
jupiter <br/>
saturn <br/>
</pre>
<div class="div" style="font-family:monospace">
<div style="text-indent: 0">sun</div> <br/>
<div style="text-indent: 4ch">mercury</div> <br/>
<div style="text-indent: 4ch">venus</div> <br/>
<div style="text-indent: 8ch">earth</div> <br/>
<div style="text-indent: 8ch">mars</div> <br/>
<div style="text-indent: 12ch">jupiter</div> <br/>
<div style="text-indent: 4ch">saturn</div> <br/>
</div>
<canvas width="300" height="300" class="first"></canvas>
<canvas width="300" height="300" class="second"></canvas>
<canvas width="300" height="300" class="result"></canvas>
Let's change the content to see some difference:
var node1 = document.querySelector("pre");
var node2 = document.querySelector(".div");
var canvas1 = document.querySelector(".first");
var canvas2 = document.querySelector(".second");
var canvas3 = document.querySelector(".result");
var ctx1 = canvas1.getContext("2d");
var ctx2 = canvas2.getContext("2d");
var ctx3 = canvas3.getContext("2d");
/* Change both code to Image and put inside Canvas */
domtoimage.toPng(node1)
.then(function(dataUrl) {
var image = new Image();
image.onload = function() {
ctx1.drawImage(this, 0, 0);
};
image.src = dataUrl;
})
domtoimage.toPng(node2)
.then(function(dataUrl) {
var image = new Image();
image.onload = function() {
ctx2.drawImage(this, 0, 0);
};
image.src = dataUrl;
})
/* Run the pixel matching*/
setTimeout(function() {
var im_r = ctx1.getImageData(0, 0, 300, 300).data;
var im_o = ctx2.getImageData(0, 0, 300, 300).data;
var diff = ctx3.createImageData(300, 300);
var pixDiff = pixelmatch(im_r, im_o, diff.data, 300, 300, {
threshold: 0.1
});
ctx3.putImageData(diff, 0, 0);
console.log(pixDiff);
}, 3000);
canvas {
border: 1px solid;
}
pre,
.div {
border: 2px solid red;
width: 300px;
height: 300px;
box-sizing: border-box;
margin: 0;
}
<script type="text/javascript" src="https://css-challenges.com/wp-content/themes/ronneby_child/js/dom-to-image.js"></script>
<script type="text/javascript" src="https://css-challenges.com/wp-content/themes/ronneby_child/js/pixel.js"></script>
<pre>
sun<br/>
mercury <br/>
venus <br/>
earth <br/>
mars <br/>
jupiter <br/>
saturn <br/>
</pre>
<div class="div" style="font-family:monospace">
<div style="text-indent: 0">sun</div> <br/>
<div style="text-indent: 4ch">mercury</div> <br/>
<div style="text-indent: 4ch">venus</div> <br/>
<div style="text-indent: 8ch">earth</div> <br/>
<div style="text-indent: 8ch">april</div> <br/>
<div style="text-indent: 12ch">jupiter</div> <br/>
<div style="text-indent: 4ch">saturn</div> <br/>
</div>
<canvas width="300" height="300" class="first"></canvas>
<canvas width="300" height="300" class="second"></canvas>
<canvas width="300" height="300" class="result"></canvas>
You can read more about how the two plugins I used work and find more interesting options.
I am fixing the sizes to 300x300 to make the demonstration easy inside the snippet, but you can consider bigger height and width.
Update
Here is a more realistic example to compare between two layouts that produce the same result. The width/height of the canvas will be dynamic and based on the content. I will show only the last canvas with the difference.
var node1 = document.querySelector(".flex");
var node2 = document.querySelector(".grid");
var canvas1 = document.querySelector(".first");
var canvas2 = document.querySelector(".second");
var canvas3 = document.querySelector(".result");
canvas1.height= node1.offsetHeight;
canvas2.height= node2.offsetHeight;
canvas3.height= node1.offsetHeight;
canvas1.width= node1.offsetWidth;
canvas2.width= node2.offsetWidth;
canvas3.width= node1.offsetWidth;
var ctx1 = canvas1.getContext("2d");
var ctx2 = canvas2.getContext("2d");
var ctx3 = canvas3.getContext("2d");
domtoimage.toPng(node1)
.then(function(dataUrl) {
var image = new Image();
image.onload = function() {
ctx1.drawImage(this, 0, 0);
};
image.src = dataUrl;
})
domtoimage.toPng(node2)
.then(function(dataUrl) {
var image = new Image();
image.onload = function() {
ctx2.drawImage(this, 0, 0);
};
image.src = dataUrl;
})
setTimeout(function() {
var im_r = ctx1.getImageData(0, 0, canvas1.width, canvas1.height).data;
var im_o = ctx2.getImageData(0, 0, canvas1.width, canvas1.height).data;
var diff = ctx3.createImageData(canvas1.width, canvas1.height);
var pixDiff = pixelmatch(im_r, im_o, diff.data, canvas1.width, canvas1.height, {
threshold: 0.2
});
ctx3.putImageData(diff, 0, 0);
console.log(pixDiff);
}, 3000);
.grid {
display:grid;
grid-template-columns:repeat(3,minmax(0,1fr));
border:2px solid red;
}
h1 {
text-align:center;
grid-column:1/-1;
flex-basis:100%;
}
.flex {
display:flex;
flex-wrap:wrap;
border:2px solid red;
}
.flex > div {
flex-grow:1;
flex-basis:0;
}
<script type="text/javascript" src="https://css-challenges.com/wp-content/themes/ronneby_child/js/dom-to-image.js"></script>
<script type="text/javascript" src="https://css-challenges.com/wp-content/themes/ronneby_child/js/pixel.js"></script>
<div class="grid">
<h1>A title here</h1>
<div>
<img src="https://picsum.photos/id/10/200/200" >
</div>
<div>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus aliquam condimentum mollis. Phasellus faucibus diam quis lorem efficitur, id egestas neque malesuada</div>
<div> Maecenas sollicitudin lacinia finibus. Integer vel varius eros. Morbi et ante eget est mollis sollicitudin.</div>
</div>
<div class="flex">
<h1>A title here</h1>
<div>
<img src="https://picsum.photos/id/10/200/200" >
</div>
<div>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus aliquam condimentum mollis. Phasellus faucibus diam quis lorem efficitur, id egestas neque malesuada</div>
<div> Maecenas sollicitudin lacinia finibus. Integer vel varius eros. Morbi et ante eget est mollis sollicitudin.</div>
</div>
<canvas width="300" height="300" class="first" style="display:none;"></canvas>
<canvas width="300" height="300" class="second" style="display:none;"></canvas>
<canvas width="300" height="300" class="result"></canvas>
Let's use a different image:
var node1 = document.querySelector(".flex");
var node2 = document.querySelector(".grid");
var canvas1 = document.querySelector(".first");
var canvas2 = document.querySelector(".second");
var canvas3 = document.querySelector(".result");
canvas1.height= node1.offsetHeight;
canvas2.height= node2.offsetHeight;
canvas3.height= node1.offsetHeight;
canvas1.width= node1.offsetWidth;
canvas2.width= node2.offsetWidth;
canvas3.width= node1.offsetWidth;
var ctx1 = canvas1.getContext("2d");
var ctx2 = canvas2.getContext("2d");
var ctx3 = canvas3.getContext("2d");
domtoimage.toPng(node1)
.then(function(dataUrl) {
var image = new Image();
image.onload = function() {
ctx1.drawImage(this, 0, 0);
};
image.src = dataUrl;
})
domtoimage.toPng(node2)
.then(function(dataUrl) {
var image = new Image();
image.onload = function() {
ctx2.drawImage(this, 0, 0);
};
image.src = dataUrl;
})
setTimeout(function() {
var im_r = ctx1.getImageData(0, 0, canvas1.width, canvas1.height).data;
var im_o = ctx2.getImageData(0, 0, canvas1.width, canvas1.height).data;
var diff = ctx3.createImageData(canvas1.width, canvas1.height);
var pixDiff = pixelmatch(im_r, im_o, diff.data, canvas1.width, canvas1.height, {
threshold: 0.2
});
ctx3.putImageData(diff, 0, 0);
console.log(pixDiff);
}, 3000);
.grid {
display:grid;
grid-template-columns:repeat(3,minmax(0,1fr));
border:2px solid red;
}
h1 {
text-align:center;
grid-column:1/-1;
flex-basis:100%;
}
.flex {
display:flex;
flex-wrap:wrap;
border:2px solid red;
}
.flex > div {
flex-grow:1;
flex-basis:0;
}
<script type="text/javascript" src="https://css-challenges.com/wp-content/themes/ronneby_child/js/dom-to-image.js"></script>
<script type="text/javascript" src="https://css-challenges.com/wp-content/themes/ronneby_child/js/pixel.js"></script>
<div class="grid">
<h1>A title here</h1>
<div>
<img src="https://picsum.photos/id/10/200/200" >
</div>
<div>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus aliquam condimentum mollis. Phasellus faucibus diam quis lorem efficitur, id egestas neque malesuada</div>
<div> Maecenas sollicitudin lacinia finibus. Integer vel varius eros. Morbi et ante eget est mollis sollicitudin.</div>
</div>
<div class="flex">
<h1>A title here</h1>
<div>
<img src="https://picsum.photos/id/12/200/200" >
</div>
<div>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus aliquam condimentum mollis. Phasellus faucibus diam quis lorem efficitur, id egestas neque malesuada</div>
<div> Maecenas sollicitudin lacinia finibus. Integer vel varius eros. Morbi et ante eget est mollis sollicitudin.</div>
</div>
<canvas width="300" height="300" class="first" style="display:none;"></canvas>
<canvas width="300" height="300" class="second" style="display:none;"></canvas>
<canvas width="300" height="300" class="result"></canvas>
Solution 2:[2]
Here's an idea to actually do some measuring with the DOM - I just replaced the text in question with divs that have a class that can be queried. Then you can print the offset of all the nodes.
From what I measure the character indents are indeed the same as the
.
var nodes = document.getElementsByClassName('measure');
for (var n of nodes) {
console.log(n.offsetLeft);
}
.measure {
display: inline-block;
background: red;
width: 50px;
height: 5px;
}
<pre>
<div class="measure"></div><br/>
<div class="measure"></div><br/>
<div class="measure"></div><br/>
<div class="measure"></div><br/>
<div class="measure"></div><br/>
<div class="measure"></div><br/>
<div class="measure"></div><br/>
</pre>
var nodes = document.getElementsByClassName('measure');
for (var n of nodes) {
console.log(n.offsetLeft);
}
.measure {
display: inline-block;
background: red;
width: 50px;
height: 5px;
}
<div style="font-family:monospace">
<div style="text-indent: 0"><div class="measure"></div></div> <br/>
<div style="text-indent: 4ch"><div class="measure"></div></div> <br/>
<div style="text-indent: 4ch"><div class="measure"></div></div> <br/>
<div style="text-indent: 8ch"><div class="measure"></div></div> <br/>
<div style="text-indent: 8ch"><div class="measure"></div></div> <br/>
<div style="text-indent: 12ch"><div class="measure"></div></div> <br/>
<div style="text-indent: 4ch"><div class="measure"></div></div> <br/>
</div>
Solution 3:[3]
Print the screens of the two pages and compare the pixels.
You can use a webdriver like selenium, render the two screens in a image file (png, jpg, etc) - selenium has a method to do this- and write a software to read the pixels of the two to compare for similarity.
The webdriver is a browser you can controll with code. And you can find software that compare images in the web.
You can find more info in this link: https://selenium.dev/
Solution 4:[4]
First we have to capture images
Capture Method 1
Use the print screen button on your keyboard (or other screenshot tool).
For pages that are to long or wide to fit onscreen, take multiple screenshots of each page and piece them together in your photo editor. You can take overlapping screenshots of a large page, put them on separate layers, then use the overlapping portions to position the pieces precisely before merging layers to create a composite of the entire page.
Capture Method 2
You can use a browser extension such as fireshot to capture the entire page at once.
Second we compare the images
Method 1
Open photoshop or go to photopea.
Paste screenshot into a layer.
Repeat for second page, pasting into a different layer.
Toggle layer visibility and any changes will be obvious. Or adjust the opacity of the top layer (and/or set different blend modes) to compare.
Method 2
Use a browser extension such as pixel-perfect-2 which allows overlaying semi-transparent images within your browser. This is a useful extension for checking alignment and spacing as well, because you can overlay pages with a grid image within your browser.
Method 3
Use an image diff tool. A quick google search will turn up too many to list here. There are image diff plugins for some code editors, command line tools, Python scripts, and online services such as diffchecker.
Solution 5:[5]
You can place one above another in the same document with position:absolute; And maybe zoom in for closer look.
#a, #b {
position:absolute;
left:0;
top:0;
margin:0;
padding:0;
color:green;
opacity:0.5;
}
#a {
color:red;
}
#b {
color:green;
}
<pre id="a">
sun<br/>
mercury <br/>
venus <br/>
earth <br/>
mars <br/>
jupiter <br/>
saturn <br/>
</pre>
<div style="font-family:monospace" id="b">
<div style="text-indent: 0">sun</div> <br/>
<div style="text-indent: 4ch">mercury</div> <br/>
<div style="text-indent: 4ch">venus</div> <br/>
<div style="text-indent: 8ch">earth</div> <br/>
<div style="text-indent: 8ch">mars</div> <br/>
<div style="text-indent: 12ch">jupiter</div> <br/>
<div style="text-indent: 4ch">saturn</div> <br/>
</div>
Solution 6:[6]
Overlay the two pages on top of each other using iframes. This should allow you to see any differences between the two pages.
main.html:
<iframe src="doc1.html"></iframe>
<iframe src="doc2.html" style="opacity:0.5"></iframe>
<style>
html, body {
margin:0;
height:100%;
}
iframe {
height:100%;
width:100%;
position:absolute;
top:0;
left:0;
}
</style>
doc1.html:
<pre>
sun<br/>
mercury <br/>
venus <br/>
earth <br/>
mars <br/>
jupiter <br/>
saturn <br/>
</pre>
doc2.html
<div style="font-family:monospace">
<div style="text-indent: 0">sun</div> <br/>
<div style="text-indent: 4ch">mercury</div> <br/>
<div style="text-indent: 4ch">venus</div> <br/>
<div style="text-indent: 8ch">earth</div> <br/>
<div style="text-indent: 8ch">mars</div> <br/>
<div style="text-indent: 12ch">jupiter</div> <br/>
<div style="text-indent: 4ch">saturn</div> <br/>
</div>
(I suspect there is a way to do this using Stack Snippets. Feel free to enlighten me)
Solution 7:[7]
There are some really complex and clever code methods in here, so here is a simple one,
Take a screenshot of one, open it in an image editor that supports transparency Like photoshop,
Past it in, then take screen show off your CSS way and paste it in set the second pasted image to an opacity of 50%, and see if they line up.
Solution 8:[8]
As I've seen so far, few people are mentioning actual testing tools.
There are plenty tools for your usecase (often part of bigger frameworks):
To name a few taken from this list. Maybe look for yourself and choose one fitting your environment.
If you want to build a sustainable automation don't rely on hacks. It may 'only' be QA but can seriously save your a** in future releases.
Solution 9:[9]
Temani's answer is fantastic. I suggest reading it if you haven't already since it shows pixelmatch usage and the overall workflow. But I have a couple of issues with it, hence this supplementary answer.
The main issue is that I wasn't able to get dom-to-image to work consistently for me. It was chopping off the last child node in a <div>
even on a simple example. Other users ran into issues as well, and a short visit to GitHub showed long threads such as this one with multiple users suggesting switching to html2canvas. As advertised, html2canvas turned out to be more reliable for me.
The secondary issue is that I'd prefer to use promises rather than setTimeout
with an arbitrary delay, even for examples. Luckily, switching to html2canvas simplifies the workflow since it returns a canvas rather than a base64 string, ready to pass to pixelmatch.
Here's a minimal example (not a snippet due to cross-origin issues):
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://bundle.run/[email protected]"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
<style>
p, canvas {
border: 1px solid red;
}
</style>
</head>
<body>
<p>Foobar</p>
<p>Foobaz</p>
<script>
(async () => {
const [aEl, bEl] = document.querySelectorAll("p");
const aCanvas = await html2canvas(aEl);
const bCanvas = await html2canvas(bEl);
const aCtx = aCanvas.getContext("2d");
const bCtx = bCanvas.getContext("2d");
const {width: w, height: h} = aCanvas;
const diffCanvas = document.createElement("canvas");
document.body.appendChild(diffCanvas);
diffCanvas.width = w;
diffCanvas.height = h;
const diffCtx = diffCanvas.getContext("2d");
const diffImageData = diffCtx.createImageData(w, h);
const mismatchedPixels = pixelmatch(
aCtx.getImageData(0, 0, w, h).data,
bCtx.getImageData(0, 0, bCanvas.width, bCanvas.height).data,
diffImageData.data,
w, h, {threshold: 0.1}
);
diffCtx.putImageData(diffImageData, 0, 0);
console.log(mismatchedPixels);
})();
</script>
</body>
</html>
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Katelyn Kim |
Solution 2 | |
Solution 3 | |
Solution 4 | Community |
Solution 5 | V.Volkov |
Solution 6 | |
Solution 7 | Barkermn01 |
Solution 8 | Nils K |
Solution 9 |