import puppeteer from 'puppeteer'; import fetch from 'node-fetch'; import assert from 'assert'; import {diff} from 'jest-diff'; import css from './native.js'; 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 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); }