blob: 839a13d862d58b02a53c0e35c4b899a14fb9f8e8 [file] [log] [blame] [edit]
import puppeteer from 'puppeteer';
import fetch from 'node-fetch';
import assert from 'assert';
import {diff} from 'jest-diff';
import * as css from 'lightningcss';
let urls = [
'https://getbootstrap.com/docs/5.1/examples/headers/',
'https://getbootstrap.com/docs/5.1/examples/heroes/',
'https://getbootstrap.com/docs/5.1/examples/features/',
'https://getbootstrap.com/docs/5.1/examples/sidebars/',
'https://getbootstrap.com/docs/5.1/examples/footers/',
'https://getbootstrap.com/docs/5.1/examples/dropdowns/',
'https://getbootstrap.com/docs/5.1/examples/list-groups/',
'https://getbootstrap.com/docs/5.1/examples/modals/',
'http://csszengarden.com',
'http://csszengarden.com/221/',
'http://csszengarden.com/219/',
'http://csszengarden.com/218/',
'http://csszengarden.com/217/',
'http://csszengarden.com/216/',
'http://csszengarden.com/215/'
];
let success = true;
const browser = await puppeteer.launch();
const page = await browser.newPage();
for (let url of urls) {
await test(url);
}
async function test(url) {
console.log(`Testing ${url}...`);
await page.goto(url);
await page.waitForNetworkIdle();
// Snapshot the computed styles of all elements
let elements = await page.$$('body *');
let computed = [];
for (let element of elements) {
let style = await element.evaluate(node => {
let res = {};
let style = window.getComputedStyle(node);
for (let i = 0; i < style.length; i++) {
res[style.item(i)] = style.getPropertyValue(style.item(i));
}
return res;
});
for (let key in style) {
style[key] = normalize(key, style[key]);
}
computed.push(style);
}
// Find stylesheets, load, and minify.
let stylesheets = await page.evaluate(() => {
return [...document.styleSheets].map(styleSheet => styleSheet.href).filter(Boolean);
});
let texts = await Promise.all(stylesheets.map(async url => {
let res = await fetch(url);
return res.text();
}));
let minified = texts.map((code, i) => {
let minified = css.transform({
filename: 'test.css',
code: Buffer.from(code),
minify: true,
targets: {
chrome: 95 << 16
}
}).code.toString();
console.log(new URL(stylesheets[i]).pathname, '–', code.length + ' bytes', '=>', minified.length + ' bytes');
return minified;
});
await page.setCacheEnabled(false);
// Disable the original stylesheets and insert a <style> element containing the minified CSS for each.
await page.evaluate(minified => {
let i = 0;
for (let stylesheet of [...document.styleSheets]) {
if (stylesheet.href) {
stylesheet.disabled = true;
let style = document.createElement('style');
style.textContent = minified[i++].replace(/url\((.*?)\)/g, (orig, url) => {
if (/['"]?data:/.test(url)) {
return orig;
}
// Rewrite urls so they are relative to the original stylesheet.
return `url(${new URL(url, stylesheet.href)})`;
});
stylesheet.ownerNode.insertAdjacentElement('beforebegin', style);
}
}
}, minified);
await page.waitForNetworkIdle();
// Now get the computed style for each element again and compare.
let i = 0;
for (let element of elements) {
let style = await element.evaluate(node => {
let res = {};
let style = window.getComputedStyle(node);
for (let i = 0; i < style.length; i++) {
let name = style.item(i);
res[name] = style.getPropertyValue(name);
}
return res;
});
for (let key in style) {
style[key] = normalize(key, style[key]);
// Ignore prefixed properties that were removed during minification.
if (key.startsWith('-webkit-box-') && style[key] !== computed[i][key]) {
style[key] = computed[i][key];
}
}
try {
assert.deepEqual(style, computed[i]);
} catch (err) {
success = false;
console.log(diff(computed[i], style));
console.log(await element.evaluate(node => node.outerHTML))
console.log(minified[0]);
}
i++;
}
console.log('');
}
function normalize(key, value) {
if (key === 'background-position') {
value = value.replace(/(^|\s)0(%|px)/g, '$10');
}
if (key === 'background-image') {
// Center is implied.
value = value.replace('radial-gradient(at center center, ', 'radial-gradient(');
}
return value.split(' ').map(v => {
if (/^[\d\.]+px$/.test(v)) {
let val = parseFloat(v);
return Math.round(val) + 'px';
}
return v;
}).join(' ');
}
if (success) {
console.log('Pass!');
process.exit(0);
} else {
console.log('Fail!');
process.exit(1);
}