UNPKG

creevey

Version:

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

219 lines 9.28 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.start = start; const path_1 = __importDefault(require("path")); const chai_1 = __importDefault(require("chai")); const chalk_1 = __importDefault(require("chalk")); const assert_1 = __importDefault(require("assert")); const promises_1 = require("fs/promises"); const mocha_1 = __importDefault(require("mocha")); const selenium_webdriver_1 = require("selenium-webdriver"); const types_js_1 = require("../../types.js"); const messages_js_1 = require("../messages.js"); const chai_image_js_1 = __importDefault(require("./chai-image.js")); const index_js_1 = require("../selenium/index.js"); const reporter_js_1 = require("./reporter.js"); const helpers_js_1 = require("./helpers.js"); const logger_js_1 = require("../logger.js"); async function getStat(filePath) { try { return await (0, promises_1.stat)(filePath); } catch (error) { if (typeof error == 'object' && error && error.code === 'ENOENT') { return null; } throw error; } } async function getLastImageNumber(imageDir, imageName) { const actualImagesRegexp = new RegExp(`${imageName}-actual-(\\d+)\\.png`); try { return ((await (0, promises_1.readdir)(imageDir)) .map((filename) => filename.replace(actualImagesRegexp, '$1')) .map(Number) .filter((x) => !isNaN(x)) .sort((a, b) => b - a)[0] ?? 0); } catch (_error) { return 0; } } // FIXME browser options hotfix async function start(config, options) { let retries = 0; let images = {}; let error = undefined; const screenshots = []; const testScope = []; function runHandler(failures) { if (failures > 0 && (error || Object.values(images).some((image) => image?.error != null))) { const isTimeout = hasTimeout(error) || Object.values(images).some((image) => hasTimeout(image?.error)); const payload = { status: 'failed', images, error, }; if (isTimeout) (0, messages_js_1.emitWorkerMessage)({ type: 'error', payload: { error: error ?? 'Unknown error' } }); else (0, messages_js_1.emitTestMessage)({ type: 'end', payload }); } else { (0, messages_js_1.emitTestMessage)({ type: 'end', payload: { status: 'success', images } }); } } async function saveImages(imageDir, images) { await (0, promises_1.mkdir)(imageDir, { recursive: true }); for (const { name, data } of images) { await (0, promises_1.writeFile)(path_1.default.join(imageDir, name), data); } } async function getExpected(assertImageName) { // context => [title, name, test, browser] // rootSuite -> kindSuite -> storyTest -> [browsers.png] // rootSuite -> kindSuite -> storySuite -> test -> [browsers.png] const testPath = [...testScope]; const imageName = assertImageName ?? testPath.pop(); (0, assert_1.default)(typeof imageName === 'string', `Can't get image name from empty test scope`); const imagesMeta = []; const reportImageDir = path_1.default.join(config.reportDir, ...testPath); const imageNumber = (await getLastImageNumber(reportImageDir, imageName)) + 1; const actualImageName = `${imageName}-actual-${imageNumber}.png`; const image = (images[imageName] = images[imageName] ?? { actual: actualImageName }); const onCompare = async (actual, expect, diff) => { imagesMeta.push({ name: image.actual, data: actual }); if (diff && expect) { image.expect = `${imageName}-expect-${imageNumber}.png`; image.diff = `${imageName}-diff-${imageNumber}.png`; imagesMeta.push({ name: image.expect, data: expect }); imagesMeta.push({ name: image.diff, data: diff }); } if (options.saveReport) { await saveImages(reportImageDir, imagesMeta); } }; const expectImageDir = path_1.default.join(config.screenDir, ...testPath); const expectImageStat = await getStat(path_1.default.join(expectImageDir, `${imageName}.png`)); if (!expectImageStat) return { expected: null, onCompare }; const expected = await (0, promises_1.readFile)(path_1.default.join(expectImageDir, `${imageName}.png`)); return { expected, onCompare }; } const mochaOptions = { timeout: 30000, reporter: process.env.TEAMCITY_VERSION ? reporter_js_1.TeamcityReporter : (options.reporter ?? reporter_js_1.CreeveyReporter), reporterOptions: { reportDir: config.reportDir, topLevelSuite: options.browser, get willRetry() { return retries < config.maxRetries; }, get images() { return images; }, get sessionId() { return sessionId; }, }, }; const mocha = new mocha_1.default(mochaOptions); mocha.cleanReferencesAfterRun(false); chai_1.default.use((0, chai_image_js_1.default)(getExpected, config.diffOptions)); const browser = await (async () => { try { return await (0, index_js_1.getBrowser)(config, options); } catch (error) { const errorMessage = error instanceof Error ? error.message : (error ?? 'Unknown error'); (0, logger_js_1.logger)().error('Failed to initiate webdriver:', errorMessage); (0, messages_js_1.emitWorkerMessage)({ type: 'error', payload: { error: errorMessage }, }); return null; } })(); if (browser == null) return; await (0, helpers_js_1.addTestsFromStories)(mocha.suite, config, { browser: options.browser, watch: options.ui, debug: options.debug, port: options.port, }); const sessionId = (await browser.getSession()).getId(); const interval = setInterval(() => // NOTE Simple way to keep session alive void browser.getCurrentUrl().then((url) => { (0, logger_js_1.logger)().debug('current url', chalk_1.default.magenta(url)); }), 10 * 1000); (0, messages_js_1.subscribeOn)('shutdown', () => { clearInterval(interval); }); mocha.suite.beforeAll(function () { this.config = config; this.browser = browser; this.until = selenium_webdriver_1.until; this.keys = selenium_webdriver_1.Key; this.expect = chai_1.default.expect; this.browserName = options.browser; this.testScope = testScope; this.screenshots = screenshots; }); mocha.suite.beforeEach(index_js_1.switchStory); if (options.trace) { mocha.suite.afterEach(async function () { const output = []; const types = await browser.manage().logs().getAvailableLogTypes(); for (const type of types) { const logs = await browser.manage().logs().get(type); output.push(logs.map((log) => JSON.stringify(log.toJSON(), null, 2)).join('\n')); } (0, logger_js_1.logger)().debug('----------', this.currentTest?.titlePath().join('/'), '----------\n', output.join('\n'), '\n----------------------------------------------------------------------------------------------------'); }); } (0, messages_js_1.subscribeOn)('test', (message) => { if (message.type != 'start') return; const test = message.payload; const testPath = test.path.join(' ').replace(/[|\\{}()[\]^$+*?.-]/g, '\\$&'); images = {}; error = undefined; retries = test.retries; mocha.grep(new RegExp(`^${testPath}$`)); mocha.unloadFiles(); const runner = mocha.run(runHandler); // TODO How handle browser corruption? runner.on('fail', (_test, reason) => { if (!(reason instanceof Error)) { error = reason; } else if (!(0, types_js_1.isImageError)(reason)) { error = reason.stack ?? reason.message; } else if (typeof reason.images == 'string') { const image = images[testScope.slice(-1)[0]]; if (image) image.error = reason.images; } else { const imageErrors = reason.images; Object.keys(imageErrors).forEach((imageName) => { const image = images[imageName]; if (image) image.error = imageErrors[imageName]; }); } }); }); (0, logger_js_1.logger)().info('Worker is ready'); (0, messages_js_1.emitWorkerMessage)({ type: 'ready' }); } function hasTimeout(str) { return str?.toLowerCase().includes('timeout') ?? false; } //# sourceMappingURL=worker.js.map