creevey
Version:
Cross-browser screenshot testing tool for Storybook with fancy UI Runner
286 lines • 11.5 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.withCreevey = withCreevey;
exports.capture = capture;
const Events = __importStar(require("@storybook/core-events"));
const preview_api_1 = require("@storybook/preview-api");
const types_js_1 = require("../../types.js");
const index_js_1 = require("../../shared/index.js");
const helpers_js_1 = require("../shared/helpers.js");
const disableAnimationsStyles = `
*,
*:hover,
*::before,
*::after {
animation-delay: -0.0001ms !important;
animation-duration: 0s !important;
animation-play-state: paused !important;
cursor: none !important;
caret-color: transparent !important;
transition: 0s !important;
}
`;
function catchRenderError(channel) {
let rejectCallback;
const promise = new Promise((_resolve, reject) => (rejectCallback = reject));
function errorHandler({ title, description }) {
rejectCallback({
message: title,
stack: description,
});
}
function exceptionHandler(exception) {
rejectCallback(exception);
}
function removeHandlers() {
channel.off(Events.STORY_ERRORED, errorHandler);
channel.off(Events.STORY_THREW_EXCEPTION, errorHandler);
}
channel.once(Events.STORY_ERRORED, errorHandler);
channel.once(Events.STORY_THREW_EXCEPTION, exceptionHandler);
return Object.assign(promise, { cancel: removeHandlers });
}
function waitForStoryRendered(channel) {
let resolveCallback;
const promise = new Promise((resolve) => (resolveCallback = resolve));
function renderHandler() {
resolveCallback();
}
function removeHandlers() {
channel.off(Events.STORY_RENDERED, renderHandler);
}
channel.once(Events.STORY_RENDERED, renderHandler);
return Object.assign(promise, { cancel: removeHandlers });
}
function waitForFontsLoaded() {
// TODO Use document.fonts.ready instead
const areFontsLoading = Array.from(document.fonts).some((font) => font.status == 'loading');
if (areFontsLoading) {
return new Promise((resolve) => {
const fontsLoadedHandler = () => {
document.fonts.removeEventListener('loadingdone', fontsLoadedHandler);
resolve();
};
document.fonts.addEventListener('loadingdone', fontsLoadedHandler);
});
}
}
function waitForCaptureCall() {
return new Promise((resolve) => (captureResolver = resolve));
}
function initCreeveyState() {
const prevState = JSON.parse(window.localStorage.getItem('Creevey_Tests') ?? '{}');
if (prevState.creeveyHost)
window.__CREEVEY_SERVER_HOST__ = prevState.creeveyHost;
if (prevState.creeveyPort)
window.__CREEVEY_SERVER_PORT__ = prevState.creeveyPort;
if (prevState.setStoriesCounter)
setStoriesCounter = prevState.setStoriesCounter;
if (prevState.isTestBrowser)
isTestBrowser = prevState.isTestBrowser;
window.addEventListener('beforeunload', () => {
window.localStorage.setItem('Creevey_Tests', JSON.stringify({
creeveyHost: window.__CREEVEY_SERVER_HOST__,
creeveyPort: window.__CREEVEY_SERVER_PORT__,
setStoriesCounter,
isTestBrowser,
}));
});
}
let isTestBrowser = false;
let captureResolver;
let waitForCreevey;
let creeveyReady;
let setStoriesCounter = 0;
function withCreevey() {
const addonsChannel = () => window.__STORYBOOK_ADDONS_CHANNEL__;
let isAnimationDisabled = false;
initCreeveyState();
function disableAnimation() {
isAnimationDisabled = true;
const style = document.createElement('style');
const textNode = document.createTextNode(disableAnimationsStyles);
style.setAttribute('type', 'text/css');
style.appendChild(textNode);
document.head.appendChild(style);
}
async function getStories() {
const stories = (0, index_js_1.serializeRawStories)(await window.__STORYBOOK_PREVIEW__.extract());
const storiesByFiles = new Map();
Object.values(stories).forEach((story) => {
const fileName = story.parameters.fileName;
const storiesFromFile = storiesByFiles.get(fileName);
if (storiesFromFile)
storiesFromFile.push(story);
else
storiesByFiles.set(fileName, [story]);
});
void fetch(`http://${(0, helpers_js_1.getConnectionUrl)()}/stories`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ setStoriesCounter, stories: [...storiesByFiles.entries()] }),
});
return stories;
}
// TODO Use Events.STORY_RENDER_PHASE_CHANGED: `loading/rendering/completed` with storyId
// TODO Check other statuses and statuses with play function
async function selectStory(storyId, shouldWaitForReady, callback) {
const currentStory = window.__STORYBOOK_PREVIEW__.currentSelection?.storyId ?? '';
if (!isAnimationDisabled)
disableAnimation();
isTestBrowser = true;
const channel = addonsChannel();
const waitForReady = shouldWaitForReady
? new Promise((resolve) => (window.__CREEVEY_SET_READY_FOR_CAPTURE__ = resolve))
: Promise.resolve();
let isCaptureCalled = false;
const renderPromise = waitForStoryRendered(channel);
const errorPromise = catchRenderError(channel);
const capturePromise = waitForCaptureCall().then(() => (isCaptureCalled = true));
setTimeout(() => {
if (storyId == currentStory)
channel.emit(Events.FORCE_REMOUNT, { storyId });
else
channel.emit(Events.SET_CURRENT_STORY, { storyId });
}, 0);
try {
await Promise.race([
(async () => {
await Promise.race([renderPromise, capturePromise]);
await waitForFontsLoaded();
await waitForReady;
})(),
errorPromise,
]);
callback([null, isCaptureCalled]);
}
catch (reason) {
// NOTE Event `STORY_THREW_EXCEPTION` triggered only in react and vue frameworks and return Error instance
// NOTE Event `STORY_ERRORED` return error-like object without `name` field
const errorMessage = reason instanceof Error
? (reason.stack ?? reason.message)
: (0, types_js_1.isObject)(reason)
? `${reason.message}\n ${reason.stack}`
: reason;
callback([errorMessage]);
}
finally {
renderPromise.cancel();
errorPromise.cancel();
}
}
function updateGlobals(globals) {
addonsChannel().emit(Events.UPDATE_GLOBALS, { globals });
}
function insertIgnoreStyles(ignoreSelectors) {
const stylesElement = document.createElement('style');
stylesElement.setAttribute('type', 'text/css');
document.head.appendChild(stylesElement);
ignoreSelectors.forEach((selector) => {
stylesElement.innerHTML += `
${selector} {
background: #000 !important;
box-shadow: none !important;
text-shadow: none !important;
outline: 0 !important;
color: rgba(0,0,0,0) !important;
}
${selector} *, ${selector}::before, ${selector}::after {
visibility: hidden !important;
}
`;
});
return stylesElement;
}
function removeIgnoreStyles(ignoreStyles) {
ignoreStyles.parentNode?.removeChild(ignoreStyles);
}
function hasPlayCompletedYet(callback) {
creeveyReady();
let isCaptureCalled = false;
let isPlayCompleted = false;
const channel = addonsChannel();
void waitForStoryRendered(channel).then(() => {
if (isCaptureCalled)
return;
isPlayCompleted = true;
callback(true);
});
void waitForCaptureCall().then(() => {
if (isPlayCompleted)
return;
isCaptureCalled = true;
callback(false);
});
}
window.__CREEVEY_GET_STORIES__ = getStories;
window.__CREEVEY_SELECT_STORY__ = selectStory;
window.__CREEVEY_UPDATE_GLOBALS__ = updateGlobals;
window.__CREEVEY_INSERT_IGNORE_STYLES__ = insertIgnoreStyles;
window.__CREEVEY_REMOVE_IGNORE_STYLES__ = removeIgnoreStyles;
window.__CREEVEY_HAS_PLAY_COMPLETED_YET__ = hasPlayCompletedYet;
window.__CREEVEY_SET_READY_FOR_CAPTURE__ = types_js_1.noop;
return (0, preview_api_1.makeDecorator)({
name: 'withCreevey',
parameterName: 'creevey',
wrapper: (getStory, context) => {
// TODO Define proper types, like captureElement is a promise
const { captureElement } = (context.parameters.creevey = (context.parameters.creevey ??
{}));
Object.defineProperty(context.parameters.creevey, 'captureElement', {
get() {
switch (true) {
case captureElement === undefined:
return Promise.resolve(context.canvasElement);
case captureElement === null:
return Promise.resolve(document.documentElement);
case typeof captureElement == 'string':
return Promise.resolve(context.canvasElement.querySelector(captureElement));
case typeof captureElement == 'function':
// TODO Define type for it
return Promise.resolve(captureElement(context));
}
},
enumerable: true,
configurable: true,
});
return getStory(context);
},
});
}
async function capture(options) {
if (!isTestBrowser)
return;
captureResolver();
waitForCreevey = new Promise((resolve) => (creeveyReady = resolve));
void fetch(`http://${(0, helpers_js_1.getConnectionUrl)()}/capture`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ workerId: window.__CREEVEY_WORKER_ID__, options }),
});
await waitForCreevey;
}
//# sourceMappingURL=withCreevey.js.map