creevey
Version:
Cross-browser screenshot testing tool for Storybook with fancy UI Runner
227 lines • 8.4 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const path_1 = __importDefault(require("path"));
const promises_1 = require("fs/promises");
const events_1 = require("events");
const types_js_1 = require("../../types.js");
const pool_js_1 = __importDefault(require("./pool.js"));
const queue_js_1 = require("./queue.js");
class Runner extends events_1.EventEmitter {
failFast;
screenDir;
reportDir;
browsers;
scheduler;
pools = {};
tests = {};
get isRunning() {
return Object.values(this.pools).some((pool) => pool.isRunning);
}
constructor(config) {
super();
this.failFast = config.failFast;
this.screenDir = config.screenDir;
this.reportDir = config.reportDir;
this.scheduler = new queue_js_1.WorkerQueue(config.useWorkerQueue);
this.browsers = Object.keys(config.browsers);
this.browsers
.map((browser) => (this.pools[browser] = new pool_js_1.default(this.scheduler, config, browser)))
.map((pool) => pool.on('test', this.handlePoolMessage));
}
handlePoolMessage = (message) => {
const { id, status, result } = message;
const test = this.tests[id];
if (!test)
return;
const { browser, testName, storyPath, storyId } = test;
// TODO Handle 'retrying' status
test.status = status == 'retrying' ? 'failed' : status;
if (!result) {
// NOTE: Running status
this.sendUpdate({ tests: { [id]: { id, browser, testName, storyPath, status: test.status, storyId } } });
return;
}
if (!test.results) {
test.results = [];
}
test.results.push(result);
if (status == 'failed') {
test.approved = null;
}
this.sendUpdate({
tests: {
[id]: {
id,
browser,
testName,
storyPath,
status: test.status,
approved: test.approved,
results: [result],
storyId,
},
},
});
if (this.failFast && status == 'failed')
this.stop();
};
handlePoolStop = () => {
if (!this.isRunning) {
this.sendUpdate({ isRunning: false });
this.emit('stop');
}
};
async init() {
await Promise.all(Object.values(this.pools).map((pool) => pool.init()));
}
updateTests(testsDiff) {
const tests = {};
const removedTests = [];
Object.entries(testsDiff).forEach(([id, newTest]) => {
const oldTest = this.tests[id];
if (newTest) {
if (oldTest) {
this.tests[id] = {
...newTest,
retries: oldTest.retries,
results: oldTest.results,
approved: oldTest.approved,
};
}
else
this.tests[id] = newTest;
const { story: _, fn: __, ...restTest } = newTest;
tests[id] = { ...restTest, status: 'unknown' };
}
else if (oldTest) {
const { id, browser, testName, storyPath, storyId } = oldTest;
removedTests.push({ id, browser, testName, storyPath, storyId });
// TODO Use Map instead
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete this.tests[id];
}
});
this.sendUpdate({ tests, removedTests });
}
start(ids) {
if (this.isRunning)
return;
const testsToStart = ids
.map((id) => this.tests[id])
.filter(types_js_1.isDefined)
.filter((test) => !test.skip);
if (testsToStart.length == 0)
return;
this.sendUpdate({
isRunning: true,
tests: testsToStart.reduce((update, { id, storyId, browser, testName, storyPath }) => ({
...update,
[id]: { id, browser, testName, storyPath, status: 'pending', storyId },
}), {}),
});
const testsByBrowser = testsToStart.reduce((tests, test) => {
const { id, browser, testName, storyPath } = test;
const restPath = [...storyPath, testName].filter(types_js_1.isDefined);
test.status = 'pending';
return {
...tests,
[browser]: [...(tests[browser] ?? []), { id, path: restPath }],
};
}, {});
this.browsers.forEach((browser) => {
const pool = this.pools[browser];
const tests = testsByBrowser[browser];
if (tests && tests.length > 0 && pool.start(tests)) {
pool.once('stop', this.handlePoolStop);
}
});
}
stop() {
if (!this.isRunning)
return;
this.browsers.forEach((browser) => {
this.pools[browser].stop();
});
}
get status() {
const tests = {};
Object.values(this.tests)
.filter(types_js_1.isDefined)
.forEach(({ story: _, fn: __, ...test }) => (tests[test.id] = test));
return {
isRunning: this.isRunning,
tests,
browsers: this.browsers,
};
}
async copyImage(test, image, actual) {
const { browser, testName, storyPath } = test;
const restPath = [...storyPath, testName].filter(types_js_1.isDefined);
const testPath = path_1.default.join(...restPath, image == browser ? '' : browser);
const srcImagePath = path_1.default.join(this.reportDir, testPath, actual);
const dstImagePath = path_1.default.join(this.screenDir, testPath, `${image}.png`);
await (0, promises_1.mkdir)(path_1.default.join(this.screenDir, testPath), { recursive: true });
await (0, promises_1.copyFile)(srcImagePath, dstImagePath);
}
async approveAll() {
const updatedTests = {};
for (const test of Object.values(this.tests)) {
if (!test?.results)
continue;
const retry = test.results.length - 1;
const { images, status } = test.results.at(retry) ?? {};
if (!images || status != 'failed')
continue;
for (const [name, image] of Object.entries(images)) {
if (!image)
continue;
await this.copyImage(test, name, image.actual);
if (!test.approved) {
test.approved = {};
}
test.approved[name] = retry;
test.status = 'approved';
updatedTests[test.id] = {
id: test.id,
browser: test.browser,
storyPath: test.storyPath,
storyId: test.storyId,
status: test.status,
approved: { [name]: retry },
};
}
}
this.sendUpdate({ tests: updatedTests });
}
async approve({ id, retry, image }) {
const test = this.tests[id];
if (!test?.results)
return;
const result = test.results[retry];
if (!result.images)
return;
const images = result.images[image];
if (!images)
return;
if (!test.approved) {
test.approved = {};
}
const { browser, testName, storyPath, storyId } = test;
await this.copyImage(test, image, images.actual);
test.approved[image] = retry;
if (Object.keys(result.images).every((name) => typeof test.approved?.[name] == 'number')) {
test.status = 'approved';
}
this.sendUpdate({
tests: { [id]: { id, browser, testName, storyPath, status: test.status, approved: { [image]: retry }, storyId } },
});
}
sendUpdate(data) {
this.emit('update', data);
}
}
exports.default = Runner;
//# sourceMappingURL=runner.js.map