creevey
Version:
Cross-browser screenshot testing tool for Storybook with fancy UI Runner
267 lines (216 loc) • 8.4 kB
JavaScript
;
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');
}