UNPKG

jest-quarantine

Version:

> A quarantine method and reporter for [jest](https://jestjs.io/) to help quite flappy tests.

143 lines (125 loc) 4.92 kB
const fs = require("fs"); const path = require("path"); const cwd = process.cwd(); const { isMatch, differenceInCalendarDays } = require("date-fns"); // Check to see if quarantine has expired between the current run and the date provided const hasQuarantineExpired = (runDate, expirationDate) => { const diff = differenceInCalendarDays(runDate, new Date(expirationDate)); return diff >= 0; }; // TODOs: // - make an async/done version /** * Wrap tests in a try/catch - marking them as "todo" when failed and logging them for reporting/later use either way. * * @param {string} name The name of your test * @param {(string|function)} expirationOrTest The function for your test or expiration date (yyyy-MM-dd) if using that. If no date is provided the quarantine lasts forever. * @param {function} fn The function for your test if providing an expiration date */ function quarantine(name, expirationOrTest, fn = null) { // Check if "expirationDate" is a function and fn is missing. // If so it is likely that no date was provided it should be treated as the function. if (typeof expirationOrTest === "function" && fn === null) { fn = expirationOrTest; expirationOrTest = null; } // If there is an expiration date then make sure it is valid if (expirationOrTest !== null && !isMatch(expirationOrTest, "yyyy-MM-dd")) { throw new Error( `quarantine: expirationDate must be a date string like "yyyy-MM-dd", instead it was passed:${expirationOrTest}` ); } // Get the run date and name of the current test const now = new Date(); const currentSpec = expect.getState(); let passes; try { // Check to see if the test function passes fn(); passes = true; } catch (e) { // If it doesn't then replace it with a todo so it's still tracked without needing to write a custom reporter passes = false; if ( expirationOrTest !== null && hasQuarantineExpired(now, expirationOrTest) ) { it(name, fn); } else { it.todo(`${name}`); } } // If we are keeping track of the results push these new ones to the array if (global.quarantineResults) { const testPath = path.relative(cwd, currentSpec.testPath); // ? Note: we don't have access to the suite or test full name global.quarantineResults.push({ name, passes, testPath, date: now.toISOString(), }); } } // Save quarantined test output const saveResults = () => { const { quarantineResults } = global; // Grab details about the test file and build the log path const currentSpec = expect.getState(); const { name, dir } = path.parse(currentSpec?.testPath); const testDir = dir.replace(cwd, ""); const filePath = path.join(cwd, "quarantined-tests", testDir, `${name}.log`); // If there are results save them - replacing the old logs if (quarantineResults && quarantineResults.length > 0) { if (!fs.existsSync("./quarantined-tests/")) fs.mkdirSync("./quarantined-tests/"); // Make sure the directory(ies) exist we need to have in order to save the log and then save it. ensureDirectoryExistence(filePath); fs.writeFileSync(filePath, JSON.stringify(quarantineResults, null, 4)); global.quarantineResults = []; } else { // If don't have new results but do already have a log file then remove it because it's stale if (fs.existsSync(filePath)) fs.unlinkSync(filePath); } }; // Recursively build directories based on a provided path. function ensureDirectoryExistence(filePath) { var dirname = path.dirname(filePath); if (fs.existsSync(dirname)) { return true; } ensureDirectoryExistence(dirname); fs.mkdirSync(dirname); } // Add the quarantine method and tracking array to the appropriate places in the global space const setupQuarantine = () => { global.quarantine = quarantine; test.quarantine = quarantine; it.quarantine = quarantine; global.quarantineResults = []; // Add the call to saveResults here global.afterAll(async () => { saveResults(); }); // ? Note: this would be a good place to patch the jasmine reporter to get suite details // but that process has a few issues: // - jasmine2 is no longer the default runner for jest, jest-circus is // - jest-circus has no methods yet for expose the same suite details // - even when patching jasmine2 the order of execution means that every reporter runs after // the work is done and so can't inject suite details into the global scope }; // Clean up any added global methods as well as the tracking array // In normal usage this should not be needed but makes life easier when writing tests const tearDownQuarantine = () => { delete global.quarantine; delete test.quarantine; delete it.quarantine; delete global.quarantineResults; }; module.exports = { quarantine, saveResults, setupQuarantine, tearDownQuarantine, ensureDirectoryExistence, };