UNPKG

creevey

Version:

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

227 lines 8.4 kB
"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