creevey
Version:
Cross-browser screenshot testing tool for Storybook with fancy UI Runner
219 lines • 9.28 kB
JavaScript
;
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