@nerdwallet/jest-nock-fixtures
Advanced tools
Comparing version 1.1.1 to 2.0.0
@@ -0,1 +1,9 @@ | ||
## 2.0.0 (2021-03-31) benjroy | ||
- BREAKING: changed chape of fixture file to store recordings per test. | ||
- allows better cleanup and cleaner diffs | ||
- fixtures must be re-recorded after updating | ||
- FEAT: adds JestWatchPlugin | ||
- when jest is configured to use this watch plugin (see README), `mode` can be changed on the fly by pressing `'r'` when running `jest --watch ...` | ||
## 1.1.1 (2021-03-23) benjroy | ||
@@ -2,0 +10,0 @@ |
/* '@nerdwallet/jest-nock-fixtures' */ | ||
const createJestNockFixturesTestWrapper = require('../src/jest-nock-fixtures'); | ||
const createJestNockFixturesTestWrapper = require('../index'); | ||
@@ -4,0 +4,0 @@ createJestNockFixturesTestWrapper({ |
@@ -14,3 +14,2 @@ const BASE_CONFIG = { | ||
// Indicates whether each individual test should be reported during the run | ||
// verbose: true, | ||
verbose: false, | ||
@@ -20,2 +19,4 @@ }; | ||
module.exports = { | ||
watchPlugins: ['<rootDir>/JestWatchPlugin'], // in a repo using this tool, ['@nerdwallet/jest-nock-fixtures/JestWatchPlugin'] | ||
projects: [ | ||
@@ -22,0 +23,0 @@ { |
{ | ||
"version": "1.1.1", | ||
"version": "2.0.0", | ||
"name": "@nerdwallet/jest-nock-fixtures", | ||
@@ -23,5 +23,8 @@ "description": "jest-nock-fixtures", | ||
"dependencies": { | ||
"ansi-escapes": "^4.3.1", | ||
"chalk": "^4.1.0", | ||
"lodash": "^4.17.21", | ||
"mkdirp": "^0.5.5", | ||
"nock": "^10.0.6" | ||
"nock": "^13.0.11", | ||
"strip-ansi": "^6.0.0" | ||
}, | ||
@@ -34,3 +37,3 @@ "devDependencies": { | ||
"eslint-plugin-prettier": "^3.3.1", | ||
"jest": "^24.9.0", | ||
"jest": "^26.6.3", | ||
"node-fetch": "^2.6.1", | ||
@@ -37,0 +40,0 @@ "prettier": "1.17.0" |
@@ -52,5 +52,8 @@ # jest-nock-fixtures | ||
setupFilesAfterEnv: ['<rootDir>/setupAfterEvvJestNockFixtures.js'], | ||
// and ignore the folder where the fixtures are saved so they don't endlessly trigger re-runs in record mode | ||
// ignore the folder where the fixtures are saved | ||
// so they don't endlessly trigger re-runs in record mode | ||
watchPathIgnorePatterns: ['__nocks__'], | ||
// add the watch plugin to change modes while in --watch mode | ||
// press 'r' to cycle through jest modes between runs | ||
watchPlugins: ['@nerdwallet/jest-nock-fixtures/JestWatchPlugin'] | ||
} | ||
@@ -57,0 +60,0 @@ ``` |
const { dirname, basename, join } = require('path'); | ||
const { existsSync, writeFileSync, unlinkSync, rmdirSync } = require('fs'); | ||
const { sortBy } = require('lodash'); | ||
const { | ||
existsSync, | ||
readFileSync, | ||
writeFileSync, | ||
unlinkSync, | ||
rmdirSync, | ||
} = require('fs'); | ||
const { has, without } = require('lodash'); | ||
const mkdirp = require('mkdirp'); // eslint-disable-line import/no-extraneous-dependencies | ||
const nock = require('nock'); // eslint-disable-line import/no-extraneous-dependencies | ||
const chalk = require('chalk'); | ||
const { MODES } = require('./mode'); | ||
const MODES = { | ||
DRYRUN: 'dryrun', | ||
LOCKDOWN: 'lockdown', | ||
RECORD: 'record', | ||
WILD: 'wild', | ||
}; | ||
const { yellow, red, cyan, grey } = chalk; | ||
const SYMBOL_FOR_NOCK_FIXTURES = Symbol('nock-fixtures'); | ||
// https://github.com/nock/nock#events | ||
@@ -19,9 +24,5 @@ const NOCK_NO_MATCH_EVENT = 'no match'; | ||
const { | ||
beforeAll, | ||
afterAll, | ||
fixtureFolderName = '__nocks__', | ||
// by default this is passed the `fixtureFolderName` supplied above | ||
getFixtureFolderName = folderName => folderName, | ||
mode = MODES.DRYRUN, | ||
logNamePrefix = 'createNockFixturesTestWrapper', | ||
getTestPath = () => { | ||
@@ -32,2 +33,5 @@ throw new Error( | ||
}, | ||
jasmine, | ||
logNamePrefix = 'createNockFixturesTestWrapper', | ||
mode = MODES.DRYRUN, | ||
unmatchedErrorMessage = (unmatchedRequests, { fixtureFilepath }) => | ||
@@ -39,2 +43,12 @@ `unmatched requests not allowed (found ${ | ||
// A jasmine reporter is added to collect references to | ||
// *all* tests (even skipped) in order to allow cleanup | ||
// of fixture file when an individual test is deleted | ||
const allJasmineTestResults = []; | ||
// the jasmine test result object during each test run | ||
let currentResult; | ||
// holds recorded data from/for the fixture file on disk | ||
let fixture; | ||
const fixtureDir = () => | ||
@@ -45,6 +59,17 @@ join(dirname(getTestPath()), getFixtureFolderName(fixtureFolderName)); | ||
const isRecordingMode = () => mode === MODES.RECORD; | ||
const isLockdownMode = () => mode === MODES.LOCKDOWN; | ||
const isDryrunMode = () => mode === MODES.DRYRUN; | ||
const isWildMode = () => mode === MODES.WILD; | ||
// a map to store counter for duplicated test names | ||
const uniqueTestNameCounters = new Map(); | ||
// store the uniqueTestName on the jasmine result object | ||
const addUniqueTestNameToResult = result => { | ||
const testName = result.fullName; | ||
const ct = (uniqueTestNameCounters.get(testName) || 0) + 1; | ||
uniqueTestNameCounters.set(testName, ct); | ||
// eslint-disable-next-line no-param-reassign | ||
result[SYMBOL_FOR_NOCK_FIXTURES] = { | ||
uniqueTestName: `${testName} ${ct}`, | ||
}; | ||
}; | ||
// reads the appended uniqueTestName from jasmine result object | ||
const uniqueTestName = (result = currentResult) => | ||
result ? result[SYMBOL_FOR_NOCK_FIXTURES].uniqueTestName : null; | ||
@@ -57,91 +82,242 @@ // keeping track of unmatched requests when not recording | ||
beforeAll(() => { | ||
if (isRecordingMode()) { | ||
nock.recorder.rec({ | ||
dont_print: true, | ||
output_objects: true, | ||
}); | ||
} else { | ||
if (!isWildMode() && existsSync(fixtureFilepath())) { | ||
// load and define mocks from previously recorded fixtures | ||
const recordings = nock.loadDefs(fixtureFilepath()); | ||
nock.define(recordings); | ||
console.warn( // eslint-disable-line no-console,prettier/prettier | ||
`${logNamePrefix}: ${mode}: Defined (${ | ||
recordings.length | ||
}) request mocks for definitions found in ${fixtureFilepath()}` | ||
); | ||
// utility for creating user messages | ||
const message = str => | ||
[ | ||
`${[ | ||
cyan(`${logNamePrefix}`), | ||
yellow(`${mode}`), | ||
uniqueTestName() && grey(`${uniqueTestName()}`), | ||
] | ||
.filter(Boolean) | ||
.join(': ')}: `, | ||
str, | ||
].join(' '); | ||
// utility for logging user messages | ||
// eslint-disable-next-line no-console | ||
const print = str => console.log(message(str)); | ||
// ensure a valid mode is being used | ||
if (!Object.values(MODES).includes(mode)) { | ||
throw new Error( | ||
message( | ||
`unrecognized mode: ${JSON.stringify( | ||
mode | ||
)}. Mode must be one of the following: ${Object.values(MODES).join( | ||
', ' | ||
)}` | ||
) | ||
); | ||
} | ||
// "wild" mode allows all http requests, records none, plays back none | ||
if (mode === MODES.WILD) { | ||
print("Not intercepting any requests in 'wild' mode"); | ||
return; | ||
} | ||
// add reporter to jasmine environment to track tests as they are run | ||
jasmine.getEnv().addReporter({ | ||
specStarted: result => { | ||
addUniqueTestNameToResult(result); | ||
currentResult = result; | ||
allJasmineTestResults.push(result); | ||
}, | ||
specDone: () => { | ||
currentResult = null; | ||
}, | ||
}); | ||
// adds test lifecycle logic | ||
function attachLifecycleOperations(modeLifecycleOperations) { | ||
const { beforeEach, afterEach, beforeAll, afterAll } = jasmine.getEnv(); | ||
beforeAll(() => { | ||
// load pre-recorded fixture file if it exists | ||
try { | ||
fixture = JSON.parse(readFileSync(fixtureFilepath())); | ||
print(yellow(`loaded nock fixture file: ${fixtureFilepath()}`)); | ||
} catch (err) { | ||
fixture = {}; | ||
if (err.code !== 'ENOENT') { | ||
print( | ||
red( | ||
`Error parsing fixture file:\nFile:\n\t${fixtureFilepath()}\nError message:\n\t${ | ||
err.message | ||
}` | ||
) | ||
); | ||
} | ||
} | ||
}); | ||
beforeEach(() => { | ||
// Remove mocks between unit tests so they run in isolation | ||
// https://github.com/nock/nock/issues/2057#issuecomment-666494539 | ||
nock.cleanAll(); | ||
// Prevent memory leaks and ensures that | ||
// previous recorder session is cleared when in 'record' mode | ||
nock.restore(); | ||
if (!nock.isActive()) { | ||
nock.activate(); | ||
} | ||
// track requests that were not mocked | ||
unmatched = []; | ||
nock.emitter.removeListener(NOCK_NO_MATCH_EVENT, handleUnmatchedRequest); | ||
nock.emitter.on(NOCK_NO_MATCH_EVENT, handleUnmatchedRequest); | ||
if (isLockdownMode()) { | ||
nock.disableNetConnect(); | ||
} | ||
} | ||
}); | ||
modeLifecycleOperations.apply(); | ||
}); | ||
afterAll(() => { | ||
if (isRecordingMode()) { | ||
let recording = nock.recorder.play(); | ||
nock.recorder.clear(); | ||
nock.restore(); | ||
afterEach(() => { | ||
modeLifecycleOperations.finish(); | ||
}); | ||
if (recording.length > 0) { | ||
// ensure fixtures folder exists | ||
mkdirp.sync(fixtureDir()); | ||
// sort it | ||
recording = sortBy(recording, ['status', 'scope', 'method', 'path', 'body']); // eslint-disable-line prettier/prettier | ||
// write it | ||
writeFileSync(fixtureFilepath(), JSON.stringify(recording, null, 4)); | ||
// message what happened | ||
console.warn( // eslint-disable-line no-console,prettier/prettier | ||
`${logNamePrefix}: ${mode}: Recorded requests: ${recording.length}` | ||
afterAll(() => { | ||
// full cleanup | ||
nock.emitter.removeListener(NOCK_NO_MATCH_EVENT, handleUnmatchedRequest); | ||
nock.restore(); // Avoid memory-leaks: https://github.com/nock/nock/issues/2057#issuecomment-666494539 | ||
nock.cleanAll(); | ||
nock.enableNetConnect(); | ||
modeLifecycleOperations.cleanup(); | ||
}); | ||
} | ||
const modeLifecycles = { | ||
[MODES.DRYRUN]: { | ||
apply() { | ||
// explicitly enableNetConnect for dry-run | ||
nock.enableNetConnect(); | ||
// define mocks from previously recorded fixture | ||
const recordings = fixture[uniqueTestName()] || []; | ||
nock.define(recordings); | ||
print( | ||
yellow( | ||
`Defined (${ | ||
recordings.length | ||
}) request mocks for '${uniqueTestName()}'` | ||
) | ||
); | ||
} else if (existsSync(fixtureFilepath())) { | ||
// cleanup obsolete nock fixture file and dir if they exist | ||
console.warn( // eslint-disable-line no-console,prettier/prettier | ||
`${logNamePrefix}: ${mode}: Nothing recorded, cleaning up ${fixtureFilepath()}.` | ||
}, | ||
finish() { | ||
// report about unmatched requests | ||
if (unmatched.length) { | ||
print(yellow(`${unmatched.length} unmatched requests`)); | ||
} | ||
}, | ||
cleanup() {}, | ||
}, | ||
[MODES.LOCKDOWN]: { | ||
apply() { | ||
// http requests are NOT ALLOWED in 'lockdown' mode | ||
nock.disableNetConnect(); | ||
// define mocks from previously recorded fixture | ||
const recordings = fixture[uniqueTestName()] || []; | ||
nock.define(recordings); | ||
print( | ||
yellow( | ||
`Defined (${ | ||
recordings.length | ||
}) request mocks for '${uniqueTestName()}'` | ||
) | ||
); | ||
// remove the fixture file | ||
unlinkSync(fixtureFilepath()); | ||
// remove the directory if not empty | ||
try { | ||
rmdirSync(fixtureDir()); | ||
}, | ||
finish() { | ||
// error on unmatched requests | ||
if (unmatched.length) { | ||
throw new Error( | ||
message( | ||
`${unmatchedErrorMessage(unmatched, { | ||
fixtureFilepath: fixtureFilepath(), | ||
})}` | ||
) | ||
); | ||
} | ||
}, | ||
cleanup() {}, | ||
}, | ||
[MODES.RECORD]: { | ||
apply() { | ||
nock.recorder.rec({ | ||
dont_print: true, | ||
output_objects: true, | ||
}); | ||
}, | ||
finish() { | ||
const recordings = nock.recorder.play(); | ||
nock.recorder.clear(); | ||
if (recordings.length > 0) { | ||
fixture[uniqueTestName()] = recordings; | ||
// message what happened | ||
console.warn( // eslint-disable-line no-console,prettier/prettier | ||
`${logNamePrefix}: ${mode}: Cleaned up ${fixtureDir()} because no fixtures were left.` | ||
print(yellow(`Recorded ${recordings.length} request(s)`)); | ||
} else if (has(fixture, uniqueTestName())) { | ||
delete fixture[uniqueTestName()]; | ||
} | ||
}, | ||
cleanup() { | ||
// when tests are *deleted*, remove the associated fixture | ||
without( | ||
Object.keys(fixture), | ||
...allJasmineTestResults.map(result => uniqueTestName(result)) | ||
).forEach(name => { | ||
delete fixture[name]; | ||
print(yellow(`Removed obsolete fixture entry for ${name}`)); | ||
}); | ||
// Save it: write the recordings to disk | ||
if (Object.keys(fixture).length) { | ||
// ensure fixtures folder exists | ||
mkdirp.sync(fixtureDir()); | ||
// sort the fixture entries by the order they were defined in the test file | ||
const sortedFixture = allJasmineTestResults.reduce((memo, result) => { | ||
const name = uniqueTestName(result); | ||
// eslint-disable-next-line no-param-reassign | ||
memo[name] = fixture[name]; | ||
return memo; | ||
}, {}); | ||
// write the fixture file | ||
writeFileSync( | ||
fixtureFilepath(), | ||
JSON.stringify(sortedFixture, null, 2) | ||
); | ||
} catch (err) { | ||
if (err.code !== 'ENOTEMPTY') throw err; | ||
// message what happened | ||
print( | ||
yellow(`Wrote recordings to fixture file: ${fixtureFilepath()}`) | ||
); | ||
return; | ||
} | ||
} | ||
} | ||
const cachedUnmatched = unmatched; | ||
// Cleanup: remove previous fixture files previously written | ||
// when nothing was captured in the recordings | ||
if (existsSync(fixtureFilepath())) { | ||
// cleanup obsolete nock fixture file and dir if they exist | ||
print(yellow(`Nothing recorded, removing ${fixtureFilepath()}`)); | ||
// remove the fixture file | ||
unlinkSync(fixtureFilepath()); | ||
// remove the directory if not empty | ||
try { | ||
rmdirSync(fixtureDir()); | ||
// message what happened | ||
print( | ||
yellow( | ||
`Removed ${fixtureDir()} directory because no fixtures were left.` | ||
) | ||
); | ||
} catch (err) { | ||
if (err.code !== 'ENOTEMPTY') { | ||
throw err; | ||
} | ||
} | ||
} | ||
}, | ||
}, | ||
}; | ||
// full cleanup | ||
nock.emitter.removeListener(NOCK_NO_MATCH_EVENT, handleUnmatchedRequest); | ||
unmatched = []; | ||
nock.cleanAll(); | ||
nock.enableNetConnect(); | ||
// report about unmatched requests | ||
if (cachedUnmatched.length) { | ||
if (isLockdownMode()) { | ||
throw new Error( | ||
`${logNamePrefix}: ${mode}: ${unmatchedErrorMessage(cachedUnmatched, { | ||
fixtureFilepath: fixtureFilepath(), | ||
})}` | ||
); | ||
} else if (isDryrunMode()) { | ||
console.warn( // eslint-disable-line no-console,prettier/prettier | ||
`${logNamePrefix}: ${mode}: ${ | ||
cachedUnmatched.length | ||
} unmatched requests` | ||
); | ||
} | ||
} | ||
}); | ||
// pick the operations to run for the given mode | ||
// and wrap the testing environment | ||
attachLifecycleOperations(modeLifecycles[mode]); | ||
} | ||
@@ -148,0 +324,0 @@ |
const { dirname, basename, join } = require('path'); | ||
const createNockFixturesTestWrapper = require('./createNockFixturesTestWrapper'); | ||
const { getMode } = require('./mode'); | ||
// in CI: | ||
// - LOCKDOWN | ||
// - disallow all http calls that haven't been mocked (throws errors) | ||
// - will fail tests if any `unmatched` (read: unmocked) requests are initiated | ||
if (process.env.CI) { | ||
process.env.JEST_NOCK_FIXTURES_MODE = 'lockdown'; | ||
} | ||
// NOT in CI: | ||
// - use `npm run test:<mode>` to add matching JEST_NOCK_FIXTURES_MODE | ||
// - remember to run `npm run test:record` when http calls change | ||
// `JEST_NOCK_FIXTURES_MODE=dryrun` is default mode. | ||
// explicitly/redundantly set it here and add this comment | ||
// to help expose this to anyone reading this | ||
if (!process.env.JEST_NOCK_FIXTURES_MODE) { | ||
process.env.JEST_NOCK_FIXTURES_MODE = 'dryrun'; | ||
} | ||
function getJestGlobalState() { | ||
if (Symbol && typeof Symbol.for === 'function') { | ||
const globalStateKey = Symbol.for('$$jest-matchers-object'); | ||
if (globalStateKey) { | ||
return global[globalStateKey]; | ||
} | ||
throw new Error(`jest global state at global[${globalStateKey}] not found`); | ||
} | ||
throw new Error( | ||
'jest-nock-fixtures requires Symbol type in language environment' | ||
); | ||
} | ||
function getJestGlobalTestPath() { | ||
const jestGlobalState = getJestGlobalState(); | ||
const { state } = jestGlobalState; | ||
return state.testPath; | ||
return global.expect.getState().testPath; | ||
} | ||
function getJestNockFixtureFolderName(fixtureFolderName) { | ||
const jestGlobalState = getJestGlobalState(); | ||
const { state } = jestGlobalState; | ||
const snapshotFolderName = basename( | ||
dirname(state.snapshotState._snapshotPath) // eslint-disable-line no-underscore-dangle | ||
dirname(global.expect.getState().snapshotState._snapshotPath) // eslint-disable-line no-underscore-dangle | ||
); | ||
@@ -53,3 +18,2 @@ return join(snapshotFolderName, fixtureFolderName); | ||
const { | ||
mode = process.env.JEST_NOCK_FIXTURES_MODE, | ||
fixtureFolderName = '__nocks__', | ||
@@ -59,2 +23,3 @@ getFixtureFolderName = getJestNockFixtureFolderName, | ||
logNamePrefix = 'jest-nock-fixtures', | ||
mode = getMode(), | ||
unmatchedErrorMessage = (reqs, { fixtureFilepath }) => | ||
@@ -64,8 +29,5 @@ `unmatched requests not allowed (found ${ | ||
}). Looking for fixtures at ${fixtureFilepath}\n\nRun with env variable \`JEST_NOCK_FIXTURES_MODE=record\` to update fixtures.`, | ||
beforeAll = global.beforeAll, | ||
afterAll = global.afterAll, | ||
} = options; | ||
return createNockFixturesTestWrapper({ | ||
mode, | ||
fixtureFolderName, | ||
@@ -75,5 +37,5 @@ unmatchedErrorMessage, | ||
getTestPath, | ||
jasmine: global.jasmine, | ||
logNamePrefix, | ||
beforeAll, | ||
afterAll, | ||
mode, | ||
}); | ||
@@ -80,0 +42,0 @@ }; |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
35166
16
501
107
2
6
1
+ Addedansi-escapes@^4.3.1
+ Addedchalk@^4.1.0
+ Addedstrip-ansi@^6.0.0
+ Addedansi-escapes@4.3.2(transitive)
+ Addedansi-regex@5.0.1(transitive)
+ Addedansi-styles@4.3.0(transitive)
+ Addedchalk@4.1.2(transitive)
+ Addedcolor-convert@2.0.1(transitive)
+ Addedcolor-name@1.1.4(transitive)
+ Addedhas-flag@4.0.0(transitive)
+ Addednock@13.5.5(transitive)
+ Addedpropagate@2.0.1(transitive)
+ Addedstrip-ansi@6.0.1(transitive)
+ Addedsupports-color@7.2.0(transitive)
+ Addedtype-fest@0.21.3(transitive)
- Removedassertion-error@1.1.0(transitive)
- Removedcall-bind@1.0.7(transitive)
- Removedchai@4.5.0(transitive)
- Removedcheck-error@1.0.3(transitive)
- Removeddeep-eql@4.1.4(transitive)
- Removeddeep-equal@1.1.2(transitive)
- Removeddefine-data-property@1.1.4(transitive)
- Removeddefine-properties@1.2.1(transitive)
- Removedes-define-property@1.0.0(transitive)
- Removedes-errors@1.3.0(transitive)
- Removedfunction-bind@1.1.2(transitive)
- Removedfunctions-have-names@1.2.3(transitive)
- Removedget-func-name@2.0.2(transitive)
- Removedget-intrinsic@1.2.4(transitive)
- Removedgopd@1.0.1(transitive)
- Removedhas-property-descriptors@1.0.2(transitive)
- Removedhas-proto@1.0.3(transitive)
- Removedhas-symbols@1.0.3(transitive)
- Removedhas-tostringtag@1.0.2(transitive)
- Removedhasown@2.0.2(transitive)
- Removedis-arguments@1.1.1(transitive)
- Removedis-date-object@1.0.5(transitive)
- Removedis-regex@1.1.4(transitive)
- Removedloupe@2.3.7(transitive)
- Removednock@10.0.6(transitive)
- Removedobject-inspect@1.13.2(transitive)
- Removedobject-is@1.1.6(transitive)
- Removedobject-keys@1.1.1(transitive)
- Removedpathval@1.1.1(transitive)
- Removedpropagate@1.0.0(transitive)
- Removedqs@6.13.0(transitive)
- Removedregexp.prototype.flags@1.5.2(transitive)
- Removedsemver@5.7.2(transitive)
- Removedset-function-length@1.2.2(transitive)
- Removedset-function-name@2.0.2(transitive)
- Removedside-channel@1.0.6(transitive)
- Removedtype-detect@4.1.0(transitive)
Updatednock@^13.0.11