Socket
Socket
Sign inDemoInstall

@nerdwallet/jest-nock-fixtures

Package Overview
Dependencies
Maintainers
18
Versions
7
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@nerdwallet/jest-nock-fixtures - npm Package Compare versions

Comparing version 1.1.1 to 2.0.0

JestWatchPlugin.js

8

CHANGELOG.md

@@ -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 @@

2

config/setupTestFrameworkScriptFile.js
/* '@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 @@ };

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc