diff --git a/README.md b/README.md index 5a6221a..d797e00 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # create-jest-runner +A highly opinionated way for creating Jest Runners + ## Install ```bash @@ -13,65 +15,71 @@ create-jest-runner takes care of handling the appropriate parallelization and cr You simply need two files: * Entry file: Used by Jest as an entrypoint to your runner. -* Run file: Contains the domain logic of your runner. +* Run file: Runs once per test file, and it encapsulates the logic of your runner ### 1) Create your entry file ```js // index.js -const createRunner = require('create-jest-runner'); -module.exports = createRunner('/path/to/run.js') +const { createJestRunner } = require('create-jest-runner'); +module.exports = createJestRunner(require.resolve('./run')); ``` ### 2) Create your run file - ```js -// run.js -module.exports = (options, workerCallback) => { - // ... -} +module.exports = (options) => { +}; ``` -## Run File API +### Run File API -This file should export a function that receives two parameters `(options, workerCallback)` +This file should export a function that receives one parameter with the options -### `options: { testPath, config, globalConfig }` +#### `options: { testPath, config, globalConfig }` - `testPath`: Path of the file that is going to be tests - `config`: Jest Project config used by this file - `globalConfig`: Jest global config -### `workerCallback: (error, testResult) => void` -_Use this callback function to report back the results (needs to be called exactly one time)._ - - `error`: Any Javascript error or a string. - - `testResult`: Needs to be an object of type https://github.com/facebook/jest/blob/master/types/TestResult.js#L131-L157 +You can return one of the following values: +- `testResult`: Needs to be an object of type https://github.com/facebook/jest/blob/master/types/TestResult.js#L131-L157 +- `Promise`: needs to be of above type. +- `Error`: good for reporting system error, not failed tests. + + + +## Example of a runner + +This runner "blade-runner" makes sure that these two emojis `⚔️ 🏃` are present in every file -### Reporting test results -#### Passing test suite ```js -// run.js -module.exports = (options, workerCallback) => { - if (/* something */) { - workerCallback(new Error('my message')); - } -} +// index.js +const { createJestRunner } = require('create-jest-runner'); +module.exports = createJestRunner(require.resolve('./run')); ``` -#### Failing test suite -### Reporting an error -You can report other errors by calling the `workerCallback` with the appropriate error. ```js // run.js -module.exports = (options, workerCallback) => { - if (/* something */) { - workerCallback(new Error('my message')); - } -} -``` +const fs = require('fs'); +const { pass, fail } = require('create-jest-runner'); +module.exports = ({ testPath }) => { + const start = +new Date(); + const contents = fs.readFileSync(testPath, 'utf8'); + const end = +new Date(); + if (contents.includes('⚔️🏃')) { + return pass({ start, end, test: { path: testPath } }); + } + const errorMessage = 'Company policies require ⚔️ 🏃 in every file'; + return fail({ + start, + end, + test: { path: testPath, errorMessage, title: 'Check for ⚔️ 🏃' }, + }); +}; +``` ## Add your runner to Jest config diff --git a/integrationTests/__fixtures__/failing/__src__/file1.js b/integrationTests/__fixtures__/failing/__src__/file1.js new file mode 100644 index 0000000..b7bd4c8 --- /dev/null +++ b/integrationTests/__fixtures__/failing/__src__/file1.js @@ -0,0 +1 @@ +console.log(); diff --git a/integrationTests/__fixtures__/failing/jest.config.js b/integrationTests/__fixtures__/failing/jest.config.js new file mode 100644 index 0000000..da169bf --- /dev/null +++ b/integrationTests/__fixtures__/failing/jest.config.js @@ -0,0 +1,4 @@ +module.exports = { + runner: require.resolve('../../runner'), + testMatch: ['**/__src__/**/*.js'], +}; diff --git a/integrationTests/__fixtures__/passing/__src__/file1.js b/integrationTests/__fixtures__/passing/__src__/file1.js new file mode 100644 index 0000000..df5d717 --- /dev/null +++ b/integrationTests/__fixtures__/passing/__src__/file1.js @@ -0,0 +1,2 @@ +// ⚔️🏃 +console.log(); diff --git a/integrationTests/__fixtures__/passing/jest.config.js b/integrationTests/__fixtures__/passing/jest.config.js new file mode 100644 index 0000000..da169bf --- /dev/null +++ b/integrationTests/__fixtures__/passing/jest.config.js @@ -0,0 +1,4 @@ +module.exports = { + runner: require.resolve('../../runner'), + testMatch: ['**/__src__/**/*.js'], +}; diff --git a/integrationTests/__snapshots__/failing.test.js.snap b/integrationTests/__snapshots__/failing.test.js.snap new file mode 100644 index 0000000..7ab8b33 --- /dev/null +++ b/integrationTests/__snapshots__/failing.test.js.snap @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Works when it has failing tests 1`] = ` +"FAIL integrationTests/__fixtures__/failing/__src__/file1.js +Company policies require ⚔️ 🏃 in every file + ✕ Check for ⚔️ 🏃 +Test Suites: 1 failed, 1 total +Tests: 1 failed, 1 total +Snapshots: 0 total +Time: +Ran all test suites. + +" +`; diff --git a/integrationTests/__snapshots__/passing.test.js.snap b/integrationTests/__snapshots__/passing.test.js.snap new file mode 100644 index 0000000..4364a8e --- /dev/null +++ b/integrationTests/__snapshots__/passing.test.js.snap @@ -0,0 +1,12 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Works when it has only passing tests 1`] = ` +"PASS integrationTests/__fixtures__/passing/__src__/file1.js + ✓ +Test Suites: 1 passed, 1 total +Tests: 1 passed, 1 total +Snapshots: 0 total +Time: +Ran all test suites. +" +`; diff --git a/integrationTests/failing.test.js b/integrationTests/failing.test.js new file mode 100644 index 0000000..865b34a --- /dev/null +++ b/integrationTests/failing.test.js @@ -0,0 +1,5 @@ +const runJest = require('./runJest'); + +it('Works when it has failing tests', () => { + return expect(runJest('failing')).resolves.toMatchSnapshot(); +}); diff --git a/integrationTests/passing.test.js b/integrationTests/passing.test.js new file mode 100644 index 0000000..5e85788 --- /dev/null +++ b/integrationTests/passing.test.js @@ -0,0 +1,5 @@ +const runJest = require('./runJest'); + +it('Works when it has only passing tests', () => { + return expect(runJest('passing')).resolves.toMatchSnapshot(); +}); diff --git a/integrationTests/runJest.js b/integrationTests/runJest.js new file mode 100644 index 0000000..46ead90 --- /dev/null +++ b/integrationTests/runJest.js @@ -0,0 +1,37 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +const execa = require('execa'); +const path = require('path'); + +const rootDir = path.join(__dirname, '..'); + +const normalize = output => + output + .replace(/\(?\d*\.?\d+m?s\)?/g, '') + .replace(/, estimated/g, '') + .replace(new RegExp(rootDir, 'g'), '/mocked-path-to-jest-runner-mocha') + .replace(new RegExp('.*at .*\\n', 'g'), 'mocked-stack-trace') + .replace(/.*at .*\\n/g, 'mocked-stack-trace') + .replace(/(mocked-stack-trace)+/, ' at mocked-stack-trace') + .replace(/\s+\n/g, '\n'); + +const runJest = (project, options = []) => { + // eslint-disable-next-line + jasmine.DEFAULT_TIMEOUT_INTERVAL = 15000; + return execa( + 'jest', + [ + '--useStderr', + '--no-watchman', + '--no-cache', + '--projects', + path.join(__dirname, '__fixtures__', project), + ].concat(options), + { + env: process.env, + }, + ) + .catch(t => t) + .then(({ stdout, stderr }) => `${normalize(stderr)}\n${normalize(stdout)}`); +}; + +module.exports = runJest; diff --git a/integrationTests/runner/index.js b/integrationTests/runner/index.js new file mode 100644 index 0000000..849674a --- /dev/null +++ b/integrationTests/runner/index.js @@ -0,0 +1,2 @@ +const { createJestRunner } = require('../../'); +module.exports = createJestRunner(require.resolve('./run')); diff --git a/integrationTests/runner/run.js b/integrationTests/runner/run.js new file mode 100644 index 0000000..8f76c3a --- /dev/null +++ b/integrationTests/runner/run.js @@ -0,0 +1,18 @@ +const fs = require('fs'); +const { pass, fail } = require('../../'); + +module.exports = ({ testPath }) => { + const start = +new Date(); + const contents = fs.readFileSync(testPath, 'utf8'); + const end = +new Date(); + + if (contents.includes('⚔️🏃')) { + return pass({ start, end, test: { path: testPath } }); + } + const errorMessage = 'Company policies require ⚔️ 🏃 in every file'; + return fail({ + start, + end, + test: { path: testPath, errorMessage, title: 'Check for ⚔️ 🏃' }, + }); +}; diff --git a/lib/createJestRunner.js b/lib/createJestRunner.js new file mode 100644 index 0000000..9beeea9 --- /dev/null +++ b/lib/createJestRunner.js @@ -0,0 +1,81 @@ +const Worker = require('jest-worker').default; + +class CancelRun extends Error { + constructor(message) { + super(message); + this.name = 'CancelRun'; + } +} + +const createRunner = runPath => { + class BaseTestRunner { + constructor(globalConfig) { + this._globalConfig = globalConfig; + } + + // eslint-disable-next-line + runTests(tests, watcher, onStart, onResult, onFailure, options) { + const worker = new Worker(runPath, { + exposedMethods: ['default'], + numWorkers: this._globalConfig.maxWorkers, + }); + + const runTestInWorker = test => { + if (watcher.isInterrupted()) { + throw new CancelRun(); + } + + return onStart(test).then(() => { + const baseOptions = { + config: test.context.config, + globalConfig: this._globalConfig, + testPath: test.path, + rawModuleMap: watcher.isWatchMode() + ? test.context.moduleMap.getRawModuleMap() + : null, + options, + }; + + return worker.default(baseOptions); + }); + }; + + const onError = (err, test) => { + return onFailure(test, err).then(() => { + if (err.type === 'ProcessTerminatedError') { + // eslint-disable-next-line no-console + console.error( + 'A worker process has quit unexpectedly! ' + + 'Most likely this is an initialization error.', + ); + process.exit(1); + } + }); + }; + + const onInterrupt = new Promise((_, reject) => { + watcher.on('change', state => { + if (state.interrupted) { + reject(new CancelRun()); + } + }); + }); + + const runAllTests = Promise.all( + tests.map(test => + runTestInWorker(test) + .then(testResult => onResult(test, testResult)) + .catch(error => onError(error, test)), + ), + ); + + const cleanup = () => worker.end(); + + return Promise.race([runAllTests, onInterrupt]).then(cleanup, cleanup); + } + } + + return BaseTestRunner; +}; + +module.exports = createRunner; diff --git a/lib/fail.js b/lib/fail.js new file mode 100644 index 0000000..8f0fd50 --- /dev/null +++ b/lib/fail.js @@ -0,0 +1,17 @@ +const toTestResult = require('./toTestResult'); + +const fail = ({ start, end, test, errorMessage }) => + toTestResult({ + errorMessage: errorMessage || test.errorMessage, + stats: { + failures: 1, + pending: 0, + passes: 0, + start, + end, + }, + tests: [Object.assign({ duration: end - start }, test)], + jestTestPath: test.path, + }); + +module.exports = fail; diff --git a/lib/index.js b/lib/index.js index 2fea2e5..9c84792 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,78 +1,9 @@ -const Worker = require('jest-worker'); - -class CancelRun extends Error { - constructor(message) { - super(message); - this.name = 'CancelRun'; - } -} - -const createRunner = runPath => { - class BaseTestRunner { - constructor(globalConfig) { - this._globalConfig = globalConfig; - } - - // eslint-disable-next-line - async runTests(tests, watcher, onStart, onResult, onFailure, options) { - const worker = new Worker(runPath, { - exposedMethods: ['default'], - numWorkers: this._globalConfig.maxWorkers, - }); - - const runTestInWorker = async test => { - if (watcher.isInterrupted()) { - throw new CancelRun(); - } - await onStart(test); - const baseOptions = { - config: test.context.config, - globalConfig: this._globalConfig, - testPath: test.path, - rawModuleMap: watcher.isWatchMode() - ? test.context.moduleMap.getRawModuleMap() - : null, - options, - }; - - return worker.default(baseOptions); - }; - - const onError = async (err, test) => { - await onFailure(test, err); - if (err.type === 'ProcessTerminatedError') { - // eslint-disable-next-line no-console - console.error( - 'A worker process has quit unexpectedly! ' + - 'Most likely this is an initialization error.', - ); - process.exit(1); - } - }; - - const onInterrupt = new Promise((_, reject) => { - watcher.on('change', state => { - if (state.interrupted) { - reject(new CancelRun()); - } - }); - }); - - const runAllTests = Promise.all( - tests.map(test => - runTestInWorker(test) - .then(testResult => onResult(test, testResult)) - .catch(error => onError(error, test)), - ), - ); - - const cleanup = () => worker.end(); - - return Promise.race([runAllTests, onInterrupt]).then(cleanup, cleanup); - } - } - - return BaseTestRunner; +const createJestRunner = require('./createJestRunner'); +const fail = require('./fail'); +const pass = require('./pass'); + +module.exports = { + createJestRunner, + fail, + pass, }; - -module.exports = createRunner; diff --git a/lib/pass.js b/lib/pass.js new file mode 100644 index 0000000..05e460f --- /dev/null +++ b/lib/pass.js @@ -0,0 +1,16 @@ +const toTestResult = require('./toTestResult'); + +const pass = ({ start, end, test }) => + toTestResult({ + stats: { + failures: 0, + pending: 0, + passes: 1, + start, + end, + }, + tests: [Object.assign({ duration: end - start }, test)], + jestTestPath: test.path, + }); + +module.exports = pass; diff --git a/lib/toTestResult.js b/lib/toTestResult.js new file mode 100644 index 0000000..2e5fe27 --- /dev/null +++ b/lib/toTestResult.js @@ -0,0 +1,44 @@ +const toTestResult = ({ + stats, + skipped, + errorMessage, + tests, + jestTestPath, +}) => { + return { + console: null, + failureMessage: errorMessage, + numFailingTests: stats.failures, + numPassingTests: stats.passes, + numPendingTests: stats.pending, + perfStats: { + end: +new Date(stats.end), + start: +new Date(stats.start), + }, + skipped, + snapshot: { + added: 0, + fileDeleted: false, + matched: 0, + unchecked: 0, + unmatched: 0, + updated: 0, + }, + sourceMaps: {}, + testExecError: null, + testFilePath: jestTestPath, + testResults: tests.map(test => { + return { + ancestorTitles: [], + duration: test.duration, + failureMessages: [test.errorMessage], + fullName: test.testPath, + numPassingAsserts: test.errorMessage ? 1 : 0, + status: test.errorMessage ? 'failed' : 'passed', + title: test.title || '', + }; + }), + }; +}; + +module.exports = toTestResult; diff --git a/package.json b/package.json index 807dd74..bd9defa 100644 --- a/package.json +++ b/package.json @@ -1,22 +1,21 @@ { "name": "create-jest-runner", - "version": "0.2.1", + "version": "0.0.1", "main": "build/index.js", "author": "Rogelio Guzman ", "description": "A simple way of creating a Jest runner", "license": "MIT", "repository": "https://github.com/rogeliog/create-jest-runner.git", "homepage": "https://github.com/rogeliog/create-jest-runner", - "files": [ - "build/" - ], + "files": ["build/"], "scripts": { "test": "jest", "lint": "eslint .", "watch": "babel lib -w --ignore **/*.test.js,integration -d build", "build": "babel lib --ignore **/*.test.js,integration -d build", "prepublish": "yarn build", - "format": "prettier --single-quote --trailing-comma all --write \"!(build)/**/*.js\"" + "format": + "prettier --single-quote --trailing-comma all --write \"!(build)/**/*.js\"" }, "dependencies": { "babel-plugin-istanbul": "4.1.4", @@ -43,6 +42,6 @@ "eslint-plugin-prettier": "2.2.0", "execa": "0.8.0", "jest": "^21.0.2", - "prettier": "1.5.3" + "prettier": "1.7.4" } } diff --git a/yarn.lock b/yarn.lock index 9bd23b5..a02f61b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2738,9 +2738,9 @@ preserve@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" -prettier@1.5.3: - version "1.5.3" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.5.3.tgz#59dadc683345ec6b88f88b94ed4ae7e1da394bfe" +prettier@1.7.4: + version "1.7.4" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.7.4.tgz#5e8624ae9363c80f95ec644584ecdf55d74f93fa" pretty-format@^21.0.2: version "21.0.2"