UNPKG

dom-to-svg

Version:
245 lines 11.7 kB
import { writeFile } from 'fs/promises'; import * as path from 'path'; import { fileURLToPath, pathToFileURL } from 'url'; import * as util from 'util'; import { Polly } from '@pollyjs/core'; import FSPersister from '@pollyjs/persister-fs'; import { assert } from 'chai'; import delay from 'delay'; import ParcelBundler from 'parcel-bundler'; import pixelmatch from 'pixelmatch'; import { PNG } from 'pngjs'; import puppeteer from 'puppeteer'; import css from 'tagged-template-noop'; import formatXML from 'xml-formatter'; import { PuppeteerAdapter } from './PuppeteerAdapter.js'; import { createDeferred, readFileOrUndefined } from './util.js'; // Reduce log verbosity util.inspect.defaultOptions.depth = 0; util.inspect.defaultOptions.maxStringLength = 80; Polly.register(PuppeteerAdapter); const defaultViewport = { width: 1200, height: 800, }; const mode = (process.env.POLLY_MODE || 'replay'); console.log('Using Polly mode', mode); const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..'); describe('documentToSVG()', () => { let browser; let server; before('Launch devserver', async () => { const bundler = new ParcelBundler(path.resolve(root, 'lib/test/injected-script.js'), { hmr: false, sourceMaps: false, minify: false, autoInstall: false, }); server = await bundler.serve(8080); }); before('Launch browser', async () => { browser = await puppeteer.launch({ headless: true, defaultViewport, devtools: true, args: [ '--window-size=1920,1080', '--lang=en-US', '--disable-web-security', '--font-render-hinting=none', '--enable-font-antialiasing', ], timeout: 0, // slowMo: 100, }); }); after('Close browser', () => browser === null || browser === void 0 ? void 0 : browser.close()); after('Close devserver', done => server === null || server === void 0 ? void 0 : server.close(done)); const snapshotDirectory = path.resolve(root, 'src/test/snapshots'); const sites = [ new URL('https://sourcegraph.com/search'), new URL('https://sourcegraph.com/extensions'), new URL('https://www.google.com?hl=en'), new URL('https://news.ycombinator.com'), new URL('https://github.com/felixfbecker/dom-to-svg/blob/fee7e1e7b63c888bc1c5205126b05c63073ebdd3/.vscode/settings.json'), ]; for (const url of sites) { const encodedName = encodeURIComponent(url.href); const svgFilePath = path.resolve(snapshotDirectory, encodedName + '.svg'); describe(url.href, () => { let polly; let page; before('Open tab and setup Polly', async () => { page = await browser.newPage(); await page.setRequestInterception(true); await page.setBypassCSP(true); // Prevent Google cookie consent prompt if (url.hostname.endsWith('google.com')) { await page.setCookie({ name: 'CONSENT', value: 'YES+DE.de+V14+BX', domain: '.google.com' }); } await page.setExtraHTTPHeaders({ 'Accept-Language': 'en-US', DNT: '1', }); page.on('console', message => { console.log('🖥 ' + (message.type() !== 'log' ? message.type().toUpperCase() : ''), message.text()); }); const requestResourceTypes = [ 'xhr', 'fetch', 'document', 'script', 'stylesheet', 'image', 'font', 'other', ]; polly = new Polly(url.href, { mode, recordIfMissing: false, recordFailedRequests: true, flushRequestsOnStop: false, logging: false, adapters: [PuppeteerAdapter], adapterOptions: { puppeteer: { page, requestResourceTypes, }, }, // Very lenient, but pages often have very complex URL parameters and this usually works fine. matchRequestsBy: { method: true, body: false, url: { username: false, password: false, hostname: true, pathname: true, query: url.hostname !== 'www.google.com', hash: false, }, order: false, headers: false, }, persister: FSPersister, persisterOptions: { fs: { recordingsDir: path.resolve(root, 'src/test/recordings'), }, }, }); polly.server.get('http://localhost:8080/*').passthrough(); polly.server.get('data:*').passthrough(); polly.server.any('https://sentry.io/*rest').intercept((request, response) => { response.sendStatus(204); }); polly.server.any('https://www.googletagmanager.com/*').intercept((request, response) => { response.sendStatus(204); }); polly.server.any('https://api.github.com/_private/*rest').intercept((request, response) => { response.sendStatus(204); }); polly.server.any('https://collector.githubapp.com/*rest').intercept((request, response) => { response.sendStatus(204); }); polly.server.any('https://www.google.com/gen_204').intercept((request, response) => { response.sendStatus(204); }); }); before('Go to page', async () => { await page.goto(url.href, { waitUntil: url.host === 'github.com' ? 'domcontentloaded' : 'networkidle2', timeout: 60000, }); await page.waitForTimeout(2000); await page.mouse.click(0, 0); // Override system font to Arial to make screenshots deterministic cross-platform await page.addStyleTag({ content: css ` @font-face { font-family: system-ui; font-style: normal; font-weight: 300; src: local('Arial'); } @font-face { font-family: -apple-system; font-style: normal; font-weight: 300; src: local('Arial'); } @font-face { font-family: BlinkMacSystemFont; font-style: normal; font-weight: 300; src: local('Arial'); } `, }); // await new Promise<never>(() => {}) }); after('Stop Polly', () => polly === null || polly === void 0 ? void 0 : polly.stop()); after('Close page', () => page === null || page === void 0 ? void 0 : page.close()); let svgPage; before('Produce SVG', async () => { const svgDeferred = createDeferred(); await page.exposeFunction('resolveSVG', svgDeferred.resolve); await page.exposeFunction('rejectSVG', svgDeferred.reject); const injectedScriptUrl = 'http://localhost:8080/injected-script.js'; await page.addScriptTag({ url: injectedScriptUrl }); const generatedSVGMarkup = await Promise.race([ svgDeferred.promise.catch(({ message, ...error }) => Promise.reject(Object.assign(new Error(message), error))), delay(120000).then(() => Promise.reject(new Error('Timeout generating SVG'))), ]); console.log('Formatting SVG'); const generatedSVGMarkupFormatted = formatXML(generatedSVGMarkup); await writeFile(svgFilePath, generatedSVGMarkupFormatted); svgPage = await browser.newPage(); await svgPage.goto(pathToFileURL(svgFilePath).href); // await new Promise<never>(() => {}) }); after('Close SVG page', () => svgPage === null || svgPage === void 0 ? void 0 : svgPage.close()); it('produces SVG that is visually the same', async () => { console.log('Bringing page to front'); await page.bringToFront(); console.log('Snapshotting the original page'); const expectedScreenshot = await page.screenshot({ encoding: 'binary', type: 'png', fullPage: false }); await writeFile(path.resolve(snapshotDirectory, `${encodedName}.expected.png`), expectedScreenshot); console.log('Snapshotting the SVG'); const actualScreenshot = await svgPage.screenshot({ encoding: 'binary', type: 'png', fullPage: false }); await writeFile(path.resolve(snapshotDirectory, `${encodedName}.actual.png`), actualScreenshot); console.log('Snapshotted, comparing PNGs'); const expectedPNG = PNG.sync.read(expectedScreenshot); const actualPNG = PNG.sync.read(actualScreenshot); const { width, height } = expectedPNG; const diffPNG = new PNG({ width, height }); const differentPixels = pixelmatch(expectedPNG.data, actualPNG.data, diffPNG.data, width, height, { threshold: 0.3, }); const differenceRatio = differentPixels / (width * height); const diffPngBuffer = PNG.sync.write(diffPNG); await writeFile(path.resolve(snapshotDirectory, `${encodedName}.diff.png`), diffPngBuffer); if (process.env.TERM_PROGRAM === 'iTerm.app') { const nameBase64 = Buffer.from(encodedName + '.diff.png').toString('base64'); const diffBase64 = diffPngBuffer.toString('base64'); console.log(`\u001B]1337;File=name=${nameBase64};inline=1;width=1080px:${diffBase64}\u0007`); } const differencePercentage = differenceRatio * 100; console.log('Difference', differencePercentage.toFixed(2) + '%'); assert.isBelow(differencePercentage, 0.5); // % }); it('produces SVG with the expected accessibility tree', async function () { const snapshotPath = path.resolve(snapshotDirectory, encodedName + '.a11y.json'); const expectedAccessibilityTree = await readFileOrUndefined(snapshotPath); const actualAccessibilityTree = await svgPage.accessibility.snapshot(); await writeFile(snapshotPath, JSON.stringify(actualAccessibilityTree, null, 2)); if (!expectedAccessibilityTree) { this.skip(); } assert.deepStrictEqual(actualAccessibilityTree, JSON.parse(expectedAccessibilityTree), 'Expected accessibility tree to be the same as snapshot'); }); }); } }); //# sourceMappingURL=test.js.map