storybook-chromatic
Version:
Visual Testing for Storybook
197 lines (168 loc) • 5.68 kB
JavaScript
/* eslint-disable no-console, class-methods-use-this, no-param-reassign */
import { JSDOM, VirtualConsole } from 'jsdom';
// Add canvas mock based on this comment: https://github.com/jsdom/jsdom/issues/1782#issuecomment-337656878
function mockCanvas(window) {
window.HTMLCanvasElement.prototype.getContext = () => ({
fillRect: () => ({}),
clearRect: () => ({}),
getImageData: (x, y, w, h) => ({ data: new Array(w * h * 4) }),
putImageData: () => ({}),
createImageData: () => [],
setTransform: () => ({}),
drawImage: () => ({}),
save: () => ({}),
fillText: () => ({}),
restore: () => ({}),
beginPath: () => ({}),
moveTo: () => ({}),
lineTo: () => ({}),
closePath: () => ({}),
stroke: () => ({}),
translate: () => ({}),
scale: () => ({}),
rotate: () => ({}),
arc: () => ({}),
fill: () => ({}),
measureText: () => ({ width: 0 }),
transform: () => ({}),
rect: () => ({}),
clip: () => ({}),
});
window.HTMLCanvasElement.prototype.toDataURL = () => '';
}
function addShimsToJSDOM(dom) {
Object.defineProperty(dom.window, 'matchMedia', {
value: () => ({
matches: true,
addListener: () => {},
removeListener: () => {},
}),
writable: true,
});
class LocalStorageMock {
constructor() {
this.store = {};
}
getItem(key) {
return this.store[key];
}
removeItem(key) {
delete this.store[key];
}
setItem(key, value) {
this.store[key] = value.toString();
}
clear() {
this.store = {};
}
}
Object.defineProperty(dom.window, 'localStorage', {
value: new LocalStorageMock(),
writable: true,
});
class WorkerMock {
addEventListener() {}
removeEventLister() {}
postMessage() {}
terminate() {}
}
Object.defineProperty(dom.window, 'Worker', {
value: WorkerMock,
writable: true,
});
Object.defineProperty(dom.window, 'crypto', {
value: {
getRandomValues: () => 0,
},
writable: true,
});
Object.defineProperty(dom.window.navigator, 'mimeTypes', {
value: () => [],
writable: true,
});
Object.defineProperty(dom.window.URL, 'createObjectURL', { value: () => {} });
Object.defineProperty(dom.window.URL, 'revokeObjectURL', { value: () => {} });
// We have to do this in this screwy way because Angular does some monkey patching
// expects an non-es2015 class here.
function MutationObserverMock() {}
MutationObserverMock.prototype = {
observe() {
return [];
},
takeRecords() {
return [];
},
disconnect() {},
};
Object.defineProperty(dom.window, 'MutationObserver', {
value: MutationObserverMock,
writable: true,
});
mockCanvas(dom.window);
}
export default async function getRuntimeSpecs(url, { verbose = false } = {}) {
const logs = [];
const virtualConsole = new VirtualConsole();
Object.keys(console).forEach(logType => {
virtualConsole.on(logType, log => logs.push({ logType, log }));
});
virtualConsole.on('jsdomError', log => logs.push({ logType: 'error', log }));
if (verbose) {
virtualConsole.sendTo(console);
}
const dom = await JSDOM.fromURL(url, {
userAgent: 'Chromatic',
// We need to execute the scripts on the page
runScripts: 'dangerously',
// We need to load scripts that are loaded via script tags
resources: 'usable',
// Send console.logs -> /dev/null (so to speak)
virtualConsole,
// Add a requestAnimationFrame polyfill, react@16 warns about it
pretendToBeVisual: true,
});
// NOTE: this line runs immediately after the HTML for the page has been loaded
// it's not possible that any external script tags have been executed.
// It is possible that they have a <script> tag that need these shims, but
// I highly doubt it. If we run into this we can always use JSDOM's old API
// to inject our own scripts at 'create' time
addShimsToJSDOM(dom);
return new Promise((resolve, reject) =>
dom.window.document.addEventListener('DOMContentLoaded', () => {
try {
const separator = '=========================';
if (!dom.window.__chromaticRuntimeSpecs__) {
console.error(
`Didn't find Chromatic addon in your storybook.
Did you add it with \`import 'storybook-chromatic'\` in your \`.storybook/config.js\`?
Read more: http://docs.chromaticqa.com`
);
if (!verbose && logs.length) {
console.error(`Your app's output:\n${separator}\n`);
logs.forEach(({ logType, log }) => console[logType](log));
console.error(`\n${separator}\n`);
}
throw new Error(`Didn't find 'window.__chromaticRuntimeSpecs__' at ${url}.`);
}
// If their app logged something to console.error, it's probably, but
// not definitely an issue. See https://github.com/hichroma/chromatic/issues/757
if (logs.find(log => log.logType === 'error')) {
console.error(`\nYour app logged the following to the error console:\n${separator}`);
logs
.filter(log => log.logType === 'error')
.forEach(({ logType, log }) => console[logType](log));
console.error(
`\n${separator}\nThis may lead to some stories not working right or getting detected by Chromatic` +
`\nWe suggest you fix the errors, but we will continue anyway..\n`
);
}
const specs = dom.window.__chromaticRuntimeSpecs__();
dom.window.close();
resolve(specs);
} catch (err) {
dom.window.close();
reject(err);
}
})
);
}