UNPKG

creevey

Version:

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

267 lines (216 loc) 8.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = worker; var _util = require("util"); var _fs = _interopRequireDefault(require("fs")); var _path = _interopRequireDefault(require("path")); var _chai = _interopRequireDefault(require("chai")); var _chalk = _interopRequireDefault(require("chalk")); var _mocha = _interopRequireDefault(require("mocha")); var _seleniumWebdriver = require("selenium-webdriver"); var _types = require("../../types"); var _messages = require("../messages"); var _chaiImage = _interopRequireDefault(require("./chai-image")); var _selenium = require("../selenium"); var _reporter = require("./reporter"); var _helpers = require("./helpers"); var _logger = require("../logger"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } const statAsync = (0, _util.promisify)(_fs.default.stat); const readdirAsync = (0, _util.promisify)(_fs.default.readdir); const readFileAsync = (0, _util.promisify)(_fs.default.readFile); const writeFileAsync = (0, _util.promisify)(_fs.default.writeFile); const mkdirAsync = (0, _util.promisify)(_fs.default.mkdir); async function getStat(filePath) { try { return await statAsync(filePath); } catch (error) { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access if (error.code === 'ENOENT') { return null; } throw error; } } async function getLastImageNumber(imageDir, imageName) { const actualImagesRegexp = new RegExp(`${imageName}-actual-(\\d+)\\.png`); try { var _await$readdirAsync$m; return (_await$readdirAsync$m = (await readdirAsync(imageDir)).map(filename => filename.replace(actualImagesRegexp, '$1')).map(Number).filter(x => !isNaN(x)).sort((a, b) => b - a)[0]) !== null && _await$readdirAsync$m !== void 0 ? _await$readdirAsync$m : 0; } catch (_error) { return 0; } } // FIXME browser options hotfix async function worker(config, options) { var _await$browser$getSes; let retries = 0; let images = {}; let error = undefined; const testScope = []; function runHandler(failures) { if (failures > 0 && (error || Object.values(images).some(image => (image === null || image === void 0 ? void 0 : image.error) != null))) { var _error2; const isTimeout = hasTimeout(error) || Object.values(images).some(image => hasTimeout(image === null || image === void 0 ? void 0 : image.error)); const payload = { status: 'failed', images, error }; isTimeout ? (0, _messages.emitWorkerMessage)({ type: 'error', payload: { error: (_error2 = error) !== null && _error2 !== void 0 ? _error2 : 'Unknown error' } }) : (0, _messages.emitTestMessage)({ type: 'end', payload }); } else { (0, _messages.emitTestMessage)({ type: 'end', payload: { status: 'success', images } }); } } async function saveImages(imageDir, images) { await mkdirAsync(imageDir, { recursive: true }); for (const { name, data } of images) { await writeFileAsync(_path.default.join(imageDir, name), data); } } async function getExpected(assertImageName) { var _images$imageName; // context => [kind, story, test, browser] // rootSuite -> kindSuite -> storyTest -> [browsers.png] // rootSuite -> kindSuite -> storySuite -> test -> [browsers.png] const testPath = [...testScope]; const imageName = assertImageName !== null && assertImageName !== void 0 ? assertImageName : testPath.pop(); const imagesMeta = []; const reportImageDir = _path.default.join(config.reportDir, ...testPath); const imageNumber = (await getLastImageNumber(reportImageDir, imageName)) + 1; const actualImageName = `${imageName}-actual-${imageNumber}.png`; const image = images[imageName] = (_images$imageName = images[imageName]) !== null && _images$imageName !== void 0 ? _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.default.join(config.screenDir, ...testPath); const expectImageStat = await getStat(_path.default.join(expectImageDir, `${imageName}.png`)); if (!expectImageStat) return { expected: null, onCompare }; const expected = await readFileAsync(_path.default.join(expectImageDir, `${imageName}.png`)); return { expected, onCompare }; } const mochaOptions = { timeout: 30000, reporter: process.env.TEAMCITY_VERSION ? _reporter.TeamcityReporter : options.reporter || _reporter.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.default(mochaOptions); // @ts-expect-error: @types/mocha has out-dated types // eslint-disable-next-line @typescript-eslint/no-unsafe-call mocha.cleanReferencesAfterRun(false); _chai.default.use((0, _chaiImage.default)(getExpected, config.diffOptions)); await (0, _helpers.addTestsFromStories)(mocha.suite, config, { browser: options.browser, watch: options.ui, debug: options.debug }); const browserConfig = config.browsers[options.browser]; const browser = await (0, _selenium.getBrowser)(config, browserConfig); const sessionId = (_await$browser$getSes = await (browser === null || browser === void 0 ? void 0 : browser.getSession())) === null || _await$browser$getSes === void 0 ? void 0 : _await$browser$getSes.getId(); if (browser == null) return; const interval = setInterval(() => void browser.getCurrentUrl().then(url => { if (options.debug) _logger.logger.debug(`${options.browser}:${_chalk.default.gray(sessionId)}`, 'current url', _chalk.default.magenta(url)); }), 10 * 1000); (0, _messages.subscribeOn)('shutdown', () => clearInterval(interval)); mocha.suite.beforeAll(function () { this.config = config; this.browser = browser; this.until = _seleniumWebdriver.until; this.keys = _seleniumWebdriver.Key; this.expect = _chai.default.expect; this.browserName = options.browser; this.testScope = testScope; }); mocha.suite.beforeEach(_selenium.switchStory); (0, _messages.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}$`)); 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.isImageError)(reason)) { var _reason$stack; error = (_reason$stack = reason.stack) !== null && _reason$stack !== void 0 ? _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]; }); } }); }); _logger.logger.info(`${options.browser}:${_chalk.default.gray(sessionId)} is ready`); (0, _messages.emitWorkerMessage)({ type: 'ready' }); } function hasTimeout(str) { return str != null && str.toLowerCase().includes('timeout'); }