diff --git a/package.json b/package.json index b98013b..3df6a13 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "happo.io", - "version": "5.6.1", + "version": "5.7.0-rc.1", "description": "Visual diffing for UI components", "main": "./build/index.js", "bin": { diff --git a/src/JSDOMDomProvider.js b/src/JSDOMDomProvider.js index d56bf4e..40051df 100644 --- a/src/JSDOMDomProvider.js +++ b/src/JSDOMDomProvider.js @@ -1,15 +1,28 @@ import { JSDOM, VirtualConsole } from 'jsdom'; +import Logger from './Logger'; + const { VERBOSE } = process.env; const MAX_ERROR_DETAIL_LENGTH = 200; -export default class JSDOMDomProvider { - constructor(jsdomOptions, { width, height, webpackBundle }) { +// Cache the JSDOM instance because re-loading the webpack bundle for every +// target can be very expensive. This assumes that jsdomOptions and +// webpackBundle do not change. +let dom; +function getCachedDOM(jsdomOptions, webpackBundle) { + if (!dom) { + const logger = new Logger(); + logger.start('Initializing JSDOM with the bundle...'); + const virtualConsole = new VirtualConsole(); virtualConsole.on('jsdomError', (e) => { const { stack, detail = '' } = e; - if (VERBOSE || typeof detail !== 'string' || detail.length < MAX_ERROR_DETAIL_LENGTH) { + if ( + VERBOSE || + typeof detail !== 'string' || + detail.length < MAX_ERROR_DETAIL_LENGTH + ) { console.error(stack, detail); } else { const newDetail = `${(detail || '').slice(0, MAX_ERROR_DETAIL_LENGTH)}... @@ -19,7 +32,7 @@ export default class JSDOMDomProvider { }); virtualConsole.sendTo(console, { omitJSDOMErrors: true }); - this.dom = new JSDOM( + dom = new JSDOM( ` @@ -37,14 +50,6 @@ export default class JSDOMDomProvider { url: 'http://localhost', virtualConsole, beforeParse(win) { - win.outerWidth = win.innerWidth = width; - win.outerHeight = win.innerHeight = height; - Object.defineProperties(win.screen, { - width: { value: width }, - availWidth: { value: width }, - height: { value: height }, - availHeight: { value: height }, - }); win.requestAnimationFrame = (callback) => setTimeout(callback, 0); win.cancelAnimationFrame = clearTimeout; }, @@ -52,15 +57,44 @@ export default class JSDOMDomProvider { jsdomOptions, ), ); + + logger.success(); + } + + return dom; +} + +// Useful for tests +export function clearCachedDOM() { + dom = undefined; +} + +export default class JSDOMDomProvider { + constructor(jsdomOptions, { webpackBundle }) { + this.dom = getCachedDOM(jsdomOptions, webpackBundle); } async init({ targetName }) { - await new Promise((resolve) => { - this.dom.window.onBundleReady = resolve; - }); + if (!this.dom.window.happoProcessor) { + await new Promise((resolve) => { + this.dom.window.onBundleReady = resolve; + }); + } return this.dom.window.happoProcessor.init({ targetName }); } + resize({ width, height }) { + this.dom.window.outerWidth = this.dom.window.innerWidth = width; + this.dom.window.outerHeight = this.dom.window.innerHeight = height; + Object.defineProperties(this.dom.window.screen, { + width: { value: width, configurable: true }, + availWidth: { value: width, configurable: true }, + height: { value: height, configurable: true }, + availHeight: { value: height, configurable: true }, + }); + return this.dom.window.happoProcessor.reset(); + } + next() { return this.dom.window.happoProcessor.next(); } @@ -74,6 +108,6 @@ export default class JSDOMDomProvider { } close() { - this.dom.window.close(); + // no-op } } diff --git a/src/browser/processor.js b/src/browser/processor.js index 71a5c31..f04f53a 100644 --- a/src/browser/processor.js +++ b/src/browser/processor.js @@ -81,6 +81,10 @@ export default class Processor { ); } + reset() { + this.cursor = -1; + } + addExamples(examples) { examples.forEach(({ fileName, component, variants }) => { Object.keys(variants).forEach((variant) => { diff --git a/src/processSnapsInBundle.js b/src/processSnapsInBundle.js index 8c3c9c4..aab858b 100644 --- a/src/processSnapsInBundle.js +++ b/src/processSnapsInBundle.js @@ -5,11 +5,10 @@ export default async function processSnapsInBundle( { viewport, DomProvider, targetName }, ) { const [width, height] = viewport.split('x').map((s) => parseInt(s, 10)); - const domProvider = new DomProvider({ - webpackBundle, - width, - height, - }); + + // TODO Remove width and height in next breaking change after puppeteer plugin + // has been updated. + const domProvider = new DomProvider({ webpackBundle, width, height }); const result = { snapPayloads: [], @@ -17,6 +16,12 @@ export default async function processSnapsInBundle( try { await domProvider.init({ targetName }); + // TODO remove resize guard in next breaking change and after puppeteer + // plugin has been updated. + if (typeof domProvider.resize !== 'undefined') { + await domProvider.resize({ width, height }); + } + // Disabling eslint here because we actually want to run things serially. /* eslint-disable no-await-in-loop */ while (await domProvider.next()) { diff --git a/test/integrations/error-test.js b/test/integrations/error-test.js index 3994a2e..ed431de 100644 --- a/test/integrations/error-test.js +++ b/test/integrations/error-test.js @@ -4,6 +4,7 @@ import MockTarget from './MockTarget'; import * as defaultConfig from '../../src/DEFAULTS'; import makeRequest from '../../src/makeRequest'; import runCommand from '../../src/commands/run'; +import { clearCachedDOM } from '../../src/JSDOMDomProvider'; jest.mock('../../src/makeRequest'); @@ -15,6 +16,7 @@ let config; let sha; beforeEach(() => { + clearCachedDOM(); console.warn = jest.fn(originalConsoleWarn); console.error = jest.fn(originalConsoleErr); console.error.mockReset(); diff --git a/test/integrations/react-test.js b/test/integrations/react-test.js index 7956594..230b911 100644 --- a/test/integrations/react-test.js +++ b/test/integrations/react-test.js @@ -8,6 +8,7 @@ import MockTarget from './MockTarget'; import * as defaultConfig from '../../src/DEFAULTS'; import makeRequest from '../../src/makeRequest'; import runCommand from '../../src/commands/run'; +import { clearCachedDOM } from '../../src/JSDOMDomProvider'; jest.mock('../../src/makeRequest'); @@ -16,6 +17,8 @@ let config; let sha; beforeEach(() => { + clearCachedDOM(); + makeRequest.mockImplementation(() => Promise.resolve({})); sha = 'foobar'; config = Object.assign({}, defaultConfig, {