UNPKG

creevey

Version:

Cross-browser screenshot testing tool for Storybook with fancy UI Runner

286 lines 11.5 kB
"use strict"; 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