diff --git a/README.md b/README.md index cbc320a..077d34c 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,10 @@ npm i -g unit-test-recorder 5. Press `Q` or `Ctrl + C` key on the keyboard to safely stop the recording and your application and start generating the tests. 6. Run your favourite linter/prettier to clean up the generated test code. +### Typescript (beta) + +`unit-test-recorder` will look for `tsconfig.json` in `pwd`. You can specifiy a differnt file using `--typescript-config=my-tsconfig.json`. This will make UTR automatically generate tests in typescript. + ### Notes * **IMPORTANT**: Serialization of JSONs in the process of recording may cause slowdown. The APIs may take considerably longer to execute. (2-10x) @@ -33,17 +37,18 @@ npm i -g unit-test-recorder Except `entrypoint` all are optional. -| Flag name | Description | -| --------- | ------------ | -| entrypoint (positional) | Path to entrypoint of your application | -| --only | Run only on these files (relative path, comma separated, supports javascript RegExp) | -| --except | Dont run on these files (relative path, comma separated, supports javascript RegExp) | -| --whitelist | Path to `whitelist.json` | -| --max-tests | Maximum number of generated tests per function. Type -1 for infinity. Default 5. | -| --output-dir | The directory in which the tests would be written to. | -| --test-ext | Extension for test files (spec.js/test.ts) | -| --size-limit | Objects larger than this limit will be moved to a different file | -| --max-stack-depth | Properties of a JSON, at a depth higher than this, will not be recorded. Default 7. | +| Flag name | Description | Default | +| --------- | ------------ | ------------ | +| entrypoint (positional) | Path to entrypoint of your application | None (Required) | +| --only | Run only on these files (relative path, comma separated, supports javascript RegExp) | undefined | +| --except | Dont run on these files (relative path, comma separated, supports javascript RegExp) | undefined | +| --whitelist | Path to `whitelist.json` | `./whitelist.json` | +| --typescript-config | Path to typescript config | `./tsconfig.json` | +| --max-tests | Maximum number of generated tests per function. Type -1 for infinity. | 5 | +| --output-dir | The directory in which the tests would be written to. | `./` | +| --test-ext | Extension for test files (spec/test) | test | +| --size-limit | Objects larger than this limit will be moved to a different file | 500 | +| --max-stack-depth | Properties of a JSON, at a depth higher than this, will not be recorded. | 7 | ### Environment variables @@ -51,6 +56,21 @@ Except `entrypoint` all are optional. | --------- | ------------ | | UTR_EXPERIMENTAL_ALS | Set this flag to use `AsyncLocalStorage` instead of `cls-hooked`. (Experimental, requires nodejs `v13.10+`) | +## Features + +* Pure functions +* Dependency injections +* Mocks +* Typescript (beta) + +## Planned features + +* JSX +* class methods +* function methods +* Better typescript support +* Higher order functions + ## Mechanism Lets take this function as an example @@ -109,23 +129,6 @@ describe('fileName', () => { }) ``` -## Planned features - -* Higher order functions -* class methods and JSX -* function methods -* Better typescript support (partial available) - -## Typescript (work in progress) - -* Make sure you have `ts-node` installed. -* Run using `ts-unit-test-recorder` instead of `unit-test-recorder` - -If that doesnt work - -* Compile typescript into javascript -* Run `unit-test-recorder dist/index.js --output-dir=test --test-ext=test.ts` - ## Troubleshooting * UTR is still early in development and may be unstable. Users are recommended to use the `--only` flag to run UTR on a few files at a time. diff --git a/package-lock.json b/package-lock.json index 689a50d..735c05c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "unit-test-recorder", - "version": "0.1.1", + "version": "0.2.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -1986,8 +1986,7 @@ "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, "base": { "version": "0.11.2", @@ -2057,7 +2056,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2336,8 +2334,7 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "confusing-browser-globals": { "version": "1.0.9", @@ -3377,6 +3374,17 @@ "flatted": "^2.0.0", "rimraf": "2.6.3", "write": "1.0.3" + }, + "dependencies": { + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } } }, "flatted": { @@ -3420,8 +3428,7 @@ "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "fsevents": { "version": "2.1.3", @@ -3492,7 +3499,6 @@ "version": "7.1.6", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dev": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -3825,7 +3831,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, "requires": { "once": "^1.3.0", "wrappy": "1" @@ -3834,8 +3839,7 @@ "inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "inquirer": { "version": "6.5.2", @@ -5999,7 +6003,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, "requires": { "brace-expansion": "^1.1.7" } @@ -6275,7 +6278,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, "requires": { "wrappy": "1" } @@ -6380,8 +6382,7 @@ "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, "path-is-inside": { "version": "1.0.2", @@ -7026,10 +7027,9 @@ "dev": true }, "rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "dev": true, + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "requires": { "glob": "^7.1.3" } @@ -8034,6 +8034,11 @@ "is-typedarray": "^1.0.0" } }, + "typescript": { + "version": "3.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.5.tgz", + "integrity": "sha512-hSAifV3k+i6lEoCJ2k6R2Z/rp/H3+8sdmcn5NrS3/3kE7+RyZXm9aqvxWqjEXHAd8b0pShatpcdMTvEdvAJltQ==" + }, "unicode-canonical-property-names-ecmascript": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", @@ -8298,8 +8303,7 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "write": { "version": "1.0.3", diff --git a/package.json b/package.json index 5159e35..a431519 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,9 @@ { "name": "unit-test-recorder", "bin": { - "unit-test-recorder": "src/index.js", - "ts-unit-test-recorder": "src/index.ts" + "unit-test-recorder": "src/index.js" }, - "version": "0.1.1", + "version": "0.2.0", "description": "A cli tool records usage and generates unit tests", "main": "index.js", "scripts": { @@ -56,6 +55,8 @@ "lodash": "^4.17.15", "mkdirp": "^1.0.4", "prettier": "^1.19.1", + "rimraf": "^3.0.2", + "typescript": "^3.9.5", "uuid": "^8.0.0", "yargs": "^15.3.1" }, diff --git a/src/cli/generation.js b/src/cli/generation.js index fa48182..7e1edbf 100644 --- a/src/cli/generation.js +++ b/src/cli/generation.js @@ -12,8 +12,7 @@ const writeFileAsync = promisify(fs.writeFile); const writeTestAndExternalData = async ({ testObj, packagedArguments }) => { console.log('Writing test for: ', testObj.filePath); mkdirp.sync(path.dirname(testObj.filePath)); - const { testExt } = packagedArguments; - const testFileName = getTestFileNameForFile(testObj.filePath, testExt); + const testFileName = getTestFileNameForFile(testObj.filePath, packagedArguments); const testFilePromise = writeFileAsync(testFileName, testObj.fileString); const externalDataPromises = testObj.externalData.map((ed) => { console.log('Creating dir:', path.dirname(ed.filePath)); diff --git a/src/cli/index.js b/src/cli/index.js index 2e54320..ddeb5b8 100644 --- a/src/cli/index.js +++ b/src/cli/index.js @@ -10,6 +10,10 @@ const { argv } = require('yargs') .describe('whitelist', 'Specify the path to whitelist json') .alias('w', 'whitelist') + .default('typescript-config', './tsconfig.json') + .describe('typescript-config', 'Specify the path to tsconfig.json (ignore if not typescript)') + .alias('tc', 'typescript-config') + .default('max-tests', '5') .describe('max-tests', 'Maximum number of generated tests per function. Type -1 for infinity') .alias('t', 'max-tests') @@ -18,8 +22,8 @@ const { argv } = require('yargs') .describe('output-dir', 'The directory in which the tests would be written to.') .alias('o', 'output-dir') - .default('test-ext', 'test.js') - .describe('test-ext', 'Extension for test files (spec.js/test.ts)') + .default('test-ext', 'test') + .describe('test-ext', 'Extension for test files (spec/test)') .default('size-limit', 500) .describe('size-limit', 'Objects larger than this limit will be moved to a different file') @@ -38,12 +42,13 @@ const { argv } = require('yargs') .describe('record-stub-params', 'Record the arguments passed as parameters to stubs (Debugging only)') .boolean(['d']); // Debug +const { compileAndGetOutputDir } = require('./utils'); const { instrumentAllFiles } = require('./instrumentation'); const { generateAllTests } = require('./generation'); // Process and package arguments -const entryPoint = argv._[0]; +const entryPointArgument = argv._[0]; const maxTestsPerFunction = parseInt(argv.maxTests, 10) || -1; const debug = argv.d; const { @@ -52,9 +57,15 @@ const { sizeLimit, recordStubParams, maxStackDepth, + typescriptConfig, } = argv; + const exceptFiles = typeof argv.except === 'string' ? argv.except.split(',') : []; const onlyFiles = typeof argv.only === 'string' ? argv.only.split(',') : []; +const tsBuildDir = compileAndGetOutputDir(typescriptConfig); + +const entryPoint = path.join(tsBuildDir || './', entryPointArgument).replace(/\.ts$/, '.js'); + const packagedArguments = { entryPoint, maxTestsPerFunction, @@ -66,6 +77,8 @@ const packagedArguments = { onlyFiles, recordStubParams, maxStackDepth, + tsBuildDir, + isTypescript: !!tsBuildDir, }; // Set the environment variable flag so that recorder can pick it up diff --git a/src/cli/utils.js b/src/cli/utils.js new file mode 100644 index 0000000..4c500bd --- /dev/null +++ b/src/cli/utils.js @@ -0,0 +1,30 @@ +const cp = require('child_process'); +const fs = require('fs'); +const ts = require('typescript'); + +const rimraf = require('rimraf'); + +const compileAndGetOutputDir = (typescriptConfig) => { + if (fs.existsSync(typescriptConfig)) { + console.log(`Typescript config found at ${typescriptConfig}`); + const contents = fs.readFileSync(typescriptConfig).toString(); + const { config, error } = ts.parseConfigFileTextToJson(typescriptConfig, contents); + + if (error) throw new Error(error); + + const tsBuildDir = config.compilerOptions.outDir; + if (fs.existsSync(tsBuildDir)) { + console.log('Purging build directory'); + rimraf.sync(tsBuildDir); + } + console.log('Recompiling'); + cp.execSync('tsc'); + console.log('Compiled'); + return tsBuildDir; + } + return null; +}; + +module.exports = { + compileAndGetOutputDir, +}; diff --git a/src/generator/components/AssignmentOperation/AssignmentOperation.js b/src/generator/components/AssignmentOperation/AssignmentOperation.js index 44fb850..364e2ad 100644 --- a/src/generator/components/AssignmentOperation/AssignmentOperation.js +++ b/src/generator/components/AssignmentOperation/AssignmentOperation.js @@ -2,12 +2,10 @@ const { wrapSafely, shouldMoveToExternal, generateNameForExternal, - packageDataForExternal, } = require('../../utils'); -const { - AggregatorManager, -} = require('../../external-data-aggregator'); +const { PackagedExternalFile } = require('../PackagedExternalFile/PackagedExternalFile'); +const { AggregatorManager } = require('../../external-data-aggregator'); const AssignmentOperation = (props) => { const { @@ -20,16 +18,18 @@ const AssignmentOperation = (props) => { } = props; const { path } = meta; - const { sizeLimit } = packagedArguments; + const { sizeLimit, isTypescript } = packagedArguments; + const suffix = isTypescript ? ' as any' : ''; + if (!shouldMoveToExternal(maybeObject, sizeLimit)) { - const code = `let ${lIdentifier} = ${wrapSafely(maybeObject, paramType)}`; + const code = `let ${lIdentifier} = ${wrapSafely(maybeObject, paramType)}${suffix}`; return code; } const { identifier, filePath, importPath } = generateNameForExternal( meta, captureIndex, lIdentifier, ); - const fileString = packageDataForExternal(maybeObject); - const code = `let ${lIdentifier} = ${identifier};`; + const fileString = PackagedExternalFile({ obj: maybeObject, packagedArguments }); + const code = `let ${lIdentifier} = ${identifier}`; const externalData = [{ fileString, identifier, @@ -38,7 +38,7 @@ const AssignmentOperation = (props) => { }]; AggregatorManager.addExternalData(path, externalData); - return code; + return `${code}${suffix}`; }; module.exports = { AssignmentOperation }; diff --git a/src/generator/components/AssignmentOperation/AssignmentOperation.test.js b/src/generator/components/AssignmentOperation/AssignmentOperation.test.js index 4bb4fd1..50c93aa 100644 --- a/src/generator/components/AssignmentOperation/AssignmentOperation.test.js +++ b/src/generator/components/AssignmentOperation/AssignmentOperation.test.js @@ -17,61 +17,124 @@ const eda = require('../../external-data-aggregator'); const { AssignmentOperation } = require('./AssignmentOperation'); describe('AssignmentOperation', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - const meta = { - path: 'dir/file.js', - name: 'functionName', - relativePath: './', - }; - const packagedArguments = {}; - const maybeObject = 42; - const lIdentifier = 'lIdentifier'; - const captureIndex = 0; - const paramType = 'Number'; - it('should generate code when payload is small', () => { - jest.spyOn(utils, 'shouldMoveToExternal').mockReturnValueOnce(false); - const props = { - meta, - packagedArguments, - maybeObject, - lIdentifier, - captureIndex, - paramType, + describe('javascript', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + const meta = { + path: 'dir/file.js', + name: 'functionName', + relativePath: './', }; + const packagedArguments = {}; + const maybeObject = 42; + const lIdentifier = 'lIdentifier'; + const captureIndex = 0; + const paramType = 'Number'; + it('should generate code when payload is small', () => { + jest.spyOn(utils, 'shouldMoveToExternal').mockReturnValueOnce(false); + const props = { + meta, + packagedArguments, + maybeObject, + lIdentifier, + captureIndex, + paramType, + }; - const code = AssignmentOperation(props); - expect(code).toMatchInlineSnapshot('"let lIdentifier = 42"'); - expect(eda.AggregatorManager.addExternalData.mock.calls.length).toBe(0); + const code = AssignmentOperation(props); + expect(code).toMatchInlineSnapshot('"let lIdentifier = 42"'); + expect(eda.AggregatorManager.addExternalData.mock.calls.length).toBe(0); + }); + it('should generate code when payload is large', () => { + const props = { + meta, + packagedArguments, + maybeObject, + lIdentifier, + captureIndex, + paramType, + }; + jest.spyOn(utils, 'shouldMoveToExternal').mockReturnValueOnce(true); + const code = AssignmentOperation(props); + const path = eda.AggregatorManager.addExternalData.mock.calls[0][0]; + const externalData = eda.AggregatorManager.addExternalData.mock.calls[0][1]; + expect(code).toMatchInlineSnapshot( + '"let lIdentifier = functionName0lIdentifier"', + ); + expect(path).toEqual(meta.path); + expect(externalData).toMatchInlineSnapshot(` + Array [ + Object { + "filePath": "dir/file/functionName_0_lIdentifier.mock.js", + "fileString": "module.exports = 42; + ", + "identifier": "functionName0lIdentifier", + "importPath": "./file/functionName_0_lIdentifier.mock", + }, + ] + `); + }); }); - it('should generate code when payload is large', () => { - const props = { - meta, - packagedArguments, - maybeObject, - lIdentifier, - captureIndex, - paramType, + describe('typescript', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + const meta = { + path: 'dir/file.js', + name: 'functionName', + relativePath: './', }; - jest.spyOn(utils, 'shouldMoveToExternal').mockReturnValueOnce(true); - const code = AssignmentOperation(props); - const path = eda.AggregatorManager.addExternalData.mock.calls[0][0]; - const externalData = eda.AggregatorManager.addExternalData.mock.calls[0][1]; - expect(code).toMatchInlineSnapshot( - '"let lIdentifier = functionName0lIdentifier;"', - ); - expect(path).toEqual(meta.path); - expect(externalData).toMatchInlineSnapshot(` - Array [ - Object { - "filePath": "dir/file/functionName_0_lIdentifier.mock.js", - "fileString": "module.exports = 42; - ", - "identifier": "functionName0lIdentifier", - "importPath": "./file/functionName_0_lIdentifier.mock.js", - }, - ] - `); + const packagedArguments = { isTypescript: true }; + const maybeObject = 42; + const lIdentifier = 'lIdentifier'; + const captureIndex = 0; + const paramType = 'Number'; + it('should generate code when payload is small', () => { + jest.spyOn(utils, 'shouldMoveToExternal').mockReturnValueOnce(false); + const props = { + meta, + packagedArguments, + maybeObject, + lIdentifier, + captureIndex, + paramType, + }; + + const code = AssignmentOperation(props); + expect(code).toMatchInlineSnapshot( + '"let lIdentifier = 42 as any"', + ); + expect(eda.AggregatorManager.addExternalData.mock.calls.length).toBe(0); + }); + it('should generate code when payload is large', () => { + const props = { + meta, + packagedArguments, + maybeObject, + lIdentifier, + captureIndex, + paramType, + }; + jest.spyOn(utils, 'shouldMoveToExternal').mockReturnValueOnce(true); + const code = AssignmentOperation(props); + const path = eda.AggregatorManager.addExternalData.mock.calls[0][0]; + const externalData = eda.AggregatorManager.addExternalData.mock.calls[0][1]; + expect(code).toMatchInlineSnapshot( + '"let lIdentifier = functionName0lIdentifier as any"', + ); + expect(path).toEqual(meta.path); + expect(externalData).toMatchInlineSnapshot(` + Array [ + Object { + "filePath": "dir/file/functionName_0_lIdentifier.mock.js", + "fileString": "export default 42; + ", + "identifier": "functionName0lIdentifier", + "importPath": "./file/functionName_0_lIdentifier.mock", + }, + ] + `); + }); }); }); diff --git a/src/generator/components/DependencyInjectionStubBlock/DependencyInjectionStubBlock.test.js b/src/generator/components/DependencyInjectionStubBlock/DependencyInjectionStubBlock.test.js index 7cf07c4..98d7ff4 100644 --- a/src/generator/components/DependencyInjectionStubBlock/DependencyInjectionStubBlock.test.js +++ b/src/generator/components/DependencyInjectionStubBlock/DependencyInjectionStubBlock.test.js @@ -91,7 +91,7 @@ describe('DependencyInjectionStubBlock', () => { }; ", "identifier": "functionName0dbClientPoolQuery0", - "importPath": "./file/functionName_0_dbClientPoolQuery0.mock.js", + "importPath": "./file/functionName_0_dbClientPoolQuery0.mock", }, ], ], diff --git a/src/generator/components/FunctionStubBlock/FunctionStubBlock.test.js b/src/generator/components/FunctionStubBlock/FunctionStubBlock.test.js index 0e67859..33cf129 100644 --- a/src/generator/components/FunctionStubBlock/FunctionStubBlock.test.js +++ b/src/generator/components/FunctionStubBlock/FunctionStubBlock.test.js @@ -104,7 +104,7 @@ describe('FunctionStubBlock', () => { "fileString": "module.exports = 1; ", "identifier": "functionName0iid1Fn10", - "importPath": "./file/functionName_0_iid1Fn10.mock.js", + "importPath": "./file/functionName_0_iid1Fn10.mock", }, ], ], @@ -116,7 +116,7 @@ describe('FunctionStubBlock', () => { "fileString": "module.exports = 2; ", "identifier": "functionName0iid1Fn11", - "importPath": "./file/functionName_0_iid1Fn11.mock.js", + "importPath": "./file/functionName_0_iid1Fn11.mock", }, ], ], @@ -128,7 +128,7 @@ describe('FunctionStubBlock', () => { "fileString": "module.exports = 3; ", "identifier": "functionName0iid2Fn20", - "importPath": "./file/functionName_0_iid2Fn20.mock.js", + "importPath": "./file/functionName_0_iid2Fn20.mock", }, ], ], @@ -140,7 +140,7 @@ describe('FunctionStubBlock', () => { "fileString": "module.exports = 4; ", "identifier": "functionName0iid2Fn30", - "importPath": "./file/functionName_0_iid2Fn30.mock.js", + "importPath": "./file/functionName_0_iid2Fn30.mock", }, ], ], @@ -154,7 +154,7 @@ describe('FunctionStubBlock', () => { }; ", "identifier": "functionName0dbClientPoolQuery0", - "importPath": "./file/functionName_0_dbClientPoolQuery0.mock.js", + "importPath": "./file/functionName_0_dbClientPoolQuery0.mock", }, ], ], diff --git a/src/generator/components/ImportStatements/ImportStatements.js b/src/generator/components/ImportStatements/ImportStatements.js index e312506..da43cd4 100644 --- a/src/generator/components/ImportStatements/ImportStatements.js +++ b/src/generator/components/ImportStatements/ImportStatements.js @@ -5,21 +5,33 @@ const { } = require('../../external-data-aggregator'); const DefaultImportStatement = (props) => { - const { importPath, identifier } = props; + const { importPath, identifier, packagedArguments } = props; + const { isTypescript } = packagedArguments; + if (isTypescript) { + return `import * as ${identifier} from '${importPath}'`; + } return `const ${identifier} = require('${importPath}');`; }; const EcmaDefaultImportStatement = (props) => { - const { importPath, identifier } = props; + const { importPath, identifier, packagedArguments } = props; + const { isTypescript } = packagedArguments; + if (isTypescript) { + return `import ${identifier} from '${importPath}'`; + } return `const {default:${identifier}} = require('${importPath}');`; }; const DestructureImportStatement = (props) => { - const { importPath, identifier } = props; + const { importPath, identifier, packagedArguments } = props; + const { isTypescript } = packagedArguments; + if (isTypescript) { + return `import {${identifier}} from '${importPath}'`; + } return `const {${identifier}} = require('${importPath}');`; }; -const FunctionImportStatements = ({ exportedFunctions }) => { +const FunctionImportStatements = ({ exportedFunctions, packagedArguments }) => { const importedFunctions = Object.keys(exportedFunctions); // Functions in objects have names like obj.fun1, obj.fun2 const cleanImportedFunctions = _.uniqBy( @@ -31,27 +43,38 @@ const FunctionImportStatements = ({ exportedFunctions }) => { ); const importStatements = cleanImportedFunctions.map(({ identifier, meta }) => { const { isDefault, isEcmaDefault, importPath } = meta; - if (isEcmaDefault) return EcmaDefaultImportStatement({ identifier, importPath }); - if (isDefault) return DefaultImportStatement({ identifier, importPath }); - return DestructureImportStatement({ identifier, importPath }); + const props = { identifier, importPath, packagedArguments }; + if (isEcmaDefault) return EcmaDefaultImportStatement(props); + if (isDefault) return DefaultImportStatement(props); + return DestructureImportStatement(props); }); return importStatements.join('\n'); }; const ExternalDataImportStatements = (props) => { - const { path } = props; + const { path, packagedArguments } = props; + const { isTypescript } = packagedArguments; const externalData = AggregatorManager.getExternalData(path); const externalsWithoutMocks = externalData.filter(ed => !ed.isMock); - const statements = externalsWithoutMocks.map(DefaultImportStatement); + const ImportStatement = isTypescript ? EcmaDefaultImportStatement : DefaultImportStatement; + const statements = externalsWithoutMocks + .map(data => ({ ...data, packagedArguments })) + .map(ImportStatement); return statements.join('\n'); }; const ImportStatements = (props) => { - const { exportedFunctions, path } = props; + const { exportedFunctions, path, packagedArguments } = props; - const functionImportStatements = FunctionImportStatements({ exportedFunctions }); - const externalImportStatements = ExternalDataImportStatements({ path }); + const functionImportStatements = FunctionImportStatements({ + exportedFunctions, + packagedArguments, + }); + const externalImportStatements = ExternalDataImportStatements({ + path, + packagedArguments, + }); return `${functionImportStatements}\n\n${externalImportStatements}\n`; }; diff --git a/src/generator/components/ImportStatements/ImportStatements.test.js b/src/generator/components/ImportStatements/ImportStatements.test.js index 0ac6ff8..461ed87 100644 --- a/src/generator/components/ImportStatements/ImportStatements.test.js +++ b/src/generator/components/ImportStatements/ImportStatements.test.js @@ -42,7 +42,8 @@ describe('ImportStatements', () => { }, }; const path = 'dir1/file.js'; - const props = { exportedFunctions, path }; + const packagedArguments = {}; + const props = { exportedFunctions, path, packagedArguments }; AggregatorManager.getExternalData.mockReturnValueOnce([ { importPath: 'dir1/foo.mock.js', identifier: 'foo' }, @@ -66,5 +67,62 @@ describe('ImportStatements', () => { " `); }); + it('should generate code for typescript', () => { + const exportedFunctions = { + fun1: { + meta: { + isDefault: false, + isEcmaDefault: false, + importPath: './fun1', + }, + }, + fun2: { + meta: { + isDefault: true, + isEcmaDefault: false, + importPath: './fun2', + }, + }, + fun3: { + meta: { + isDefault: false, + isEcmaDefault: true, + importPath: './fun3', + }, + }, + 'obj.fun4': { + meta: { + isDefault: false, + isEcmaDefault: false, + importPath: './fun4', + }, + }, + }; + const path = 'dir1/file.js'; + const packagedArguments = { isTypescript: true }; + const props = { exportedFunctions, path, packagedArguments }; + + AggregatorManager.getExternalData.mockReturnValueOnce([ + { importPath: 'dir1/foo.mock.js', identifier: 'foo' }, + { importPath: 'dir1/bar.mock.js', identifier: 'bar' }, + { importPath: 'dir1/baz.mock.js', identifier: 'baz', isMock: true }, + ]); + + const code = ImportStatements(props); + const formattedCode = prettier.format(code, { + singleQuote: true, + parser: 'babel', + }); + expect(formattedCode).toMatchInlineSnapshot(` + "import { fun1 } from './fun1'; + import * as fun2 from './fun2'; + import fun3 from './fun3'; + import { obj } from './fun4'; + + import foo from 'dir1/foo.mock.js'; + import bar from 'dir1/bar.mock.js'; + " + `); + }); }); }); diff --git a/src/generator/components/JestMockImplementationStatement/JestMockImplementationStatement.js b/src/generator/components/JestMockImplementationStatement/JestMockImplementationStatement.js index 45aea0b..a8caca1 100644 --- a/src/generator/components/JestMockImplementationStatement/JestMockImplementationStatement.js +++ b/src/generator/components/JestMockImplementationStatement/JestMockImplementationStatement.js @@ -4,12 +4,10 @@ const { wrapSafely, shouldMoveToExternal, generateNameForExternal, - packageDataForExternal, } = require('../../utils'); -const { - AggregatorManager, -} = require('../../external-data-aggregator'); +const { PackagedExternalFile } = require('../PackagedExternalFile/PackagedExternalFile'); +const { AggregatorManager } = require('../../external-data-aggregator'); const JestMockImplementationStatement = ({ meta, @@ -32,7 +30,7 @@ const JestMockImplementationStatement = ({ const { identifier, filePath, importPath } = generateNameForExternal( meta, captureIndex, _.camelCase(`${lIdentifier}${innerCaptureIndex}`), ); - const fileString = packageDataForExternal(payload); + const fileString = PackagedExternalFile({ obj: payload, packagedArguments }); const code = inner(identifier); const externalData = [{ fileString, diff --git a/src/generator/components/JestMockImplementationStatement/JestMockImplementationStatement.test.js b/src/generator/components/JestMockImplementationStatement/JestMockImplementationStatement.test.js index 7342fc0..43a20e9 100644 --- a/src/generator/components/JestMockImplementationStatement/JestMockImplementationStatement.test.js +++ b/src/generator/components/JestMockImplementationStatement/JestMockImplementationStatement.test.js @@ -76,7 +76,7 @@ describe('JestMockImplementationStatement', () => { "fileString": "module.exports = 42; ", "identifier": "functionName0fsLIdentifier0", - "importPath": "./file/functionName_0_fsLIdentifier0.mock.js", + "importPath": "./file/functionName_0_fsLIdentifier0.mock", }, ] `); diff --git a/src/generator/components/MockFunctionStubBlock/MockFunctionStubBlock.test.js b/src/generator/components/MockFunctionStubBlock/MockFunctionStubBlock.test.js index 66bad5a..39f5d12 100644 --- a/src/generator/components/MockFunctionStubBlock/MockFunctionStubBlock.test.js +++ b/src/generator/components/MockFunctionStubBlock/MockFunctionStubBlock.test.js @@ -14,9 +14,7 @@ jest.mock('../../external-data-aggregator', () => ({ const utils = require('../../utils'); const eda = require('../../external-data-aggregator'); -const { - MockFunctionStubBlock, -} = require('./MockFunctionStubBlock'); +const { MockFunctionStubBlock } = require('./MockFunctionStubBlock'); describe('MockFunctionStubBlock', () => { const meta = { @@ -75,57 +73,57 @@ describe('MockFunctionStubBlock', () => { `); expect(path).toEqual(meta.path); expect(externalData).toMatchInlineSnapshot(` + Array [ Array [ + "dir/file.js", Array [ - "dir/file.js", - Array [ - Object { - "filePath": "dir/file/functionName_0_iid1Fn10.mock.js", - "fileString": "module.exports = 1; - ", - "identifier": "functionName0iid1Fn10", - "importPath": "./file/functionName_0_iid1Fn10.mock.js", - }, - ], + Object { + "filePath": "dir/file/functionName_0_iid1Fn10.mock.js", + "fileString": "module.exports = 1; + ", + "identifier": "functionName0iid1Fn10", + "importPath": "./file/functionName_0_iid1Fn10.mock", + }, ], + ], + Array [ + "dir/file.js", Array [ - "dir/file.js", - Array [ - Object { - "filePath": "dir/file/functionName_0_iid1Fn11.mock.js", - "fileString": "module.exports = 2; - ", - "identifier": "functionName0iid1Fn11", - "importPath": "./file/functionName_0_iid1Fn11.mock.js", - }, - ], + Object { + "filePath": "dir/file/functionName_0_iid1Fn11.mock.js", + "fileString": "module.exports = 2; + ", + "identifier": "functionName0iid1Fn11", + "importPath": "./file/functionName_0_iid1Fn11.mock", + }, ], + ], + Array [ + "dir/file.js", Array [ - "dir/file.js", - Array [ - Object { - "filePath": "dir/file/functionName_0_iid2Fn20.mock.js", - "fileString": "module.exports = 3; - ", - "identifier": "functionName0iid2Fn20", - "importPath": "./file/functionName_0_iid2Fn20.mock.js", - }, - ], + Object { + "filePath": "dir/file/functionName_0_iid2Fn20.mock.js", + "fileString": "module.exports = 3; + ", + "identifier": "functionName0iid2Fn20", + "importPath": "./file/functionName_0_iid2Fn20.mock", + }, ], + ], + Array [ + "dir/file.js", Array [ - "dir/file.js", - Array [ - Object { - "filePath": "dir/file/functionName_0_iid2Fn30.mock.js", - "fileString": "module.exports = 4; - ", - "identifier": "functionName0iid2Fn30", - "importPath": "./file/functionName_0_iid2Fn30.mock.js", - }, - ], + Object { + "filePath": "dir/file/functionName_0_iid2Fn30.mock.js", + "fileString": "module.exports = 4; + ", + "identifier": "functionName0iid2Fn30", + "importPath": "./file/functionName_0_iid2Fn30.mock", + }, ], - ] - `); + ], + ] + `); }); it('should return empty string if no mocks', () => { const props1 = { diff --git a/src/generator/components/MockImportBlock/MockImportBlock.js b/src/generator/components/MockImportBlock/MockImportBlock.js index 4db8301..e55753d 100644 --- a/src/generator/components/MockImportBlock/MockImportBlock.js +++ b/src/generator/components/MockImportBlock/MockImportBlock.js @@ -4,17 +4,53 @@ const { DefaultImportStatement } = require('../ImportStatements/ImportStatements const JestMockStatement = ({ importPath }) => `jest.mock('${importPath}');`; -const MockImportBlock = ({ meta }) => { +const SpecialImportStatement = ({ importPath, originalImportPath, packagedArguments }) => { + const identifier = _.camelCase(originalImportPath); + const { isTypescript } = packagedArguments; + if (!isTypescript) { + return DefaultImportStatement({ importPath, identifier, packagedArguments }); + } + const newIdentifier = `${identifier}Original`; + const importStatement = DefaultImportStatement({ + importPath, + identifier: newIdentifier, + packagedArguments, + }); + return importStatement; +}; + +const ReassignmentStatement = ({ originalImportPath, packagedArguments }) => { + const { isTypescript } = packagedArguments; + if (!isTypescript) return ''; + + const identifier = _.camelCase(originalImportPath); + const newIdentifier = `${identifier}Original`; + + return `const ${identifier} = ${newIdentifier} as any`; +}; + +const MockImportBlock = ({ meta, packagedArguments }) => { const mockStatements = meta.mocks .map(importPath => JestMockStatement({ importPath })); - const importStatements = meta.mocks - .map(importPath => DefaultImportStatement({ importPath, identifier: _.camelCase(importPath) })); + const importStatements = _.zip(meta.mocks, meta.originalMocks) + .map(([importPath, originalImportPath]) => SpecialImportStatement({ + importPath, + originalImportPath, + packagedArguments, + })); + + const reassignmentStatements = meta.originalMocks + .map(originalImportPath => ReassignmentStatement({ + originalImportPath, + packagedArguments, + })); const importStatementStr = importStatements.join('\n'); + const reassignmentStatementStr = reassignmentStatements.join('\n'); const mockStatementsStr = mockStatements.join('\n'); - return `${importStatementStr}\n\n${mockStatementsStr}`; + return `${importStatementStr}\n\n${reassignmentStatementStr}\n\n${mockStatementsStr}`; }; module.exports = { diff --git a/src/generator/components/MockImportBlock/MockImportBlock.test.js b/src/generator/components/MockImportBlock/MockImportBlock.test.js index 06fb3c0..654f3a2 100644 --- a/src/generator/components/MockImportBlock/MockImportBlock.test.js +++ b/src/generator/components/MockImportBlock/MockImportBlock.test.js @@ -11,31 +11,71 @@ describe('MockImportBlock', () => { }); }); describe('MockImportBlock', () => { - it('should generate code', () => { - const props = { - meta: { - mocks: ['m1', 'm2', 'm3', 'm4', 'm5'], - }, - }; - const code = MockImportBlock(props); - const formattedCode = prettier.format(code, { - singleQuote: true, - parser: 'babel', + describe('javascript', () => { + it('should generate code', () => { + const props = { + meta: { + mocks: ['m1', '../dir1/m2', 'm3', '../dir1/m4', 'm5'], + originalMocks: ['m1', './m2', 'm3', './m4', 'm5'], + }, + packagedArguments: {}, + }; + const code = MockImportBlock(props); + const formattedCode = prettier.format(code, { + singleQuote: true, + parser: 'babel', + }); + expect(formattedCode).toMatchInlineSnapshot(` + "const m1 = require('m1'); + const m2 = require('../dir1/m2'); + const m3 = require('m3'); + const m4 = require('../dir1/m4'); + const m5 = require('m5'); + + jest.mock('m1'); + jest.mock('../dir1/m2'); + jest.mock('m3'); + jest.mock('../dir1/m4'); + jest.mock('m5'); + " + `); }); - expect(formattedCode).toMatchInlineSnapshot(` - "const m1 = require('m1'); - const m2 = require('m2'); - const m3 = require('m3'); - const m4 = require('m4'); - const m5 = require('m5'); + }); + describe('typescript', () => { + it('should generate code', () => { + const props = { + meta: { + mocks: ['m1', '../dir1/m2', 'm3', '../dir1/m4', 'm5'], + originalMocks: ['m1', './m2', 'm3', './m4', 'm5'], + }, + packagedArguments: { isTypescript: true }, + }; + const code = MockImportBlock(props); + const formattedCode = prettier.format(code, { + singleQuote: true, + parser: 'typescript', + }); + expect(formattedCode).toMatchInlineSnapshot(` + "import * as m1Original from 'm1'; + import * as m2Original from '../dir1/m2'; + import * as m3Original from 'm3'; + import * as m4Original from '../dir1/m4'; + import * as m5Original from 'm5'; - jest.mock('m1'); - jest.mock('m2'); - jest.mock('m3'); - jest.mock('m4'); - jest.mock('m5'); - " - `); + const m1 = m1Original as any; + const m2 = m2Original as any; + const m3 = m3Original as any; + const m4 = m4Original as any; + const m5 = m5Original as any; + + jest.mock('m1'); + jest.mock('../dir1/m2'); + jest.mock('m3'); + jest.mock('../dir1/m4'); + jest.mock('m5'); + " + `); + }); }); }); }); diff --git a/src/generator/components/PackagedExternalFile/PackagedExternalFile.js b/src/generator/components/PackagedExternalFile/PackagedExternalFile.js new file mode 100644 index 0000000..6077f30 --- /dev/null +++ b/src/generator/components/PackagedExternalFile/PackagedExternalFile.js @@ -0,0 +1,22 @@ +const prettier = require('prettier'); + +const { wrapSafely } = require('../../utils'); + +const PackagedExternalFile = ({ obj, packagedArguments }) => { + const { isTypescript } = packagedArguments; + const dialect = isTypescript ? 'typescript' : 'javascript'; + const wrappedObj = wrapSafely(obj); + const code = { + typescript: `export default ${wrappedObj}`, + javascript: `module.exports = ${wrappedObj}`, + }[dialect]; + + return prettier.format(code, { + singleQuote: true, + parser: 'babel', + }); +}; + +module.exports = { + PackagedExternalFile, +}; diff --git a/src/generator/components/PackagedExternalFile/PackagedExternalFile.test.js b/src/generator/components/PackagedExternalFile/PackagedExternalFile.test.js new file mode 100644 index 0000000..c14ca88 --- /dev/null +++ b/src/generator/components/PackagedExternalFile/PackagedExternalFile.test.js @@ -0,0 +1,28 @@ +const { PackagedExternalFile } = require('./PackagedExternalFile'); + +describe('PackagedExternalFile', () => { + it('should generate code for javascript', () => { + const obj = { a: 42 }; + const packagedArguments = {}; + const props = { obj, packagedArguments }; + const code = PackagedExternalFile(props); + expect(code).toMatchInlineSnapshot(` + "module.exports = { + a: 42 + }; + " + `); + }); + it('should generate code for typescript', () => { + const obj = { a: 42 }; + const packagedArguments = { isTypescript: true }; + const props = { obj, packagedArguments }; + const code = PackagedExternalFile(props); + expect(code).toMatchInlineSnapshot(` + "export default { + a: 42 + }; + " + `); + }); +}); diff --git a/src/generator/components/TestFileBlock/TestFileBlock.js b/src/generator/components/TestFileBlock/TestFileBlock.js index 734a398..404d6a9 100644 --- a/src/generator/components/TestFileBlock/TestFileBlock.js +++ b/src/generator/components/TestFileBlock/TestFileBlock.js @@ -18,11 +18,12 @@ const TestFileBlock = (props) => { }); const importStatements = ImportStatements({ + packagedArguments, exportedFunctions, path: filePath, }); - const mockImportStatements = MockImportBlock(fileData); + const mockImportStatements = MockImportBlock({ ...fileData, packagedArguments }); const result = ` ${mockImportStatements} diff --git a/src/generator/components/TestFileBlock/TestFileBlock.test.js b/src/generator/components/TestFileBlock/TestFileBlock.test.js index 02a702b..15cb3cd 100644 --- a/src/generator/components/TestFileBlock/TestFileBlock.test.js +++ b/src/generator/components/TestFileBlock/TestFileBlock.test.js @@ -58,6 +58,7 @@ describe('TestFileBlock', () => { fileData: { meta: { mocks: ['../../someScript', 'fs'], + originalMocks: ['../../someScript', 'fs'], }, exportedFunctions: { [meta.name]: functionActivity, diff --git a/src/generator/index.js b/src/generator/index.js index 95fd4e7..4215ab5 100644 --- a/src/generator/index.js +++ b/src/generator/index.js @@ -1,6 +1,10 @@ // TODO: Use babel template const prettier = require('prettier'); -const { filePathToFileName, getOutputFilePath } = require('./utils'); +const { + filePathToFileName, + getOutputFilePath, + offsetMocks, +} = require('./utils'); const { TestFileBlock } = require('./components/TestFileBlock/TestFileBlock'); const { AggregatorManager } = require('./external-data-aggregator'); @@ -12,14 +16,21 @@ const extractTestsFromState = (state, packagedArguments) => Object .map((filePath) => { try { // Generate output file path and store it in the state meta - const { outputDir } = packagedArguments; - const { outputFilePath, importPath, relativePath } = getOutputFilePath(filePath, outputDir); + const { + outputFilePath, + importPath, + relativePath, + } = getOutputFilePath(filePath, packagedArguments); Object.keys(state[filePath].exportedFunctions).forEach((functionName) => { state[filePath].exportedFunctions[functionName].meta.importPath = importPath; state[filePath].exportedFunctions[functionName].meta.relativePath = relativePath; + state[filePath].exportedFunctions[functionName].meta + .tsBuildDir = packagedArguments.tsBuildDir; }); state[filePath].importPath = importPath; state[filePath].relativePath = relativePath; + state[filePath].tsBuildDir = packagedArguments.tsBuildDir; + offsetMocks(state, filePath, packagedArguments); // Generate file name from file path const fileName = filePathToFileName(filePath); @@ -38,7 +49,7 @@ const extractTestsFromState = (state, packagedArguments) => Object try { fileString = prettier.format(code, { singleQuote: true, - parser: 'babel', + parser: packagedArguments.isTypescript ? 'typescript' : 'babel', }); } catch (e) { console.error(e); diff --git a/src/generator/utils.js b/src/generator/utils.js index 5cb52da..9cbd213 100644 --- a/src/generator/utils.js +++ b/src/generator/utils.js @@ -1,14 +1,23 @@ const path = require('path'); -const prettier = require('prettier'); const _ = require('lodash'); const filePathToFileName = filePath => path.parse(filePath).name; -const getOutputFilePath = (rawFilePath, rawOutputDir) => { - // Handle Windows file paths - const filePath = rawFilePath.replace(/\\/g, '/'); +const getOutputFilePath = (rawFilePath, packagedArguments) => { + const { + outputDir: rawOutputDir, + tsBuildDir: rawTsBuildDir, + } = packagedArguments; + + // Windows to posix paths + let filePath = rawFilePath.replace(/\\/g, '/'); + const tsBuildDir = `${rawTsBuildDir || ''}`.replace(/\\/g, '/'); + const fileName = filePathToFileName(filePath); + // Remove typescript tsBuildDir if present + filePath = path.relative(tsBuildDir || './', filePath); + // rawOutputDir === null means use the same directory as inputDir if (!rawOutputDir) { return { @@ -17,6 +26,8 @@ const getOutputFilePath = (rawFilePath, rawOutputDir) => { relativePath: './', }; } + + // Windows to posix paths const outputDir = rawOutputDir.replace(/\\/g, '/'); const inputDir = path.posix.dirname(filePath); @@ -47,31 +58,73 @@ const wrapSafely = (param, paramType = typeof (param)) => { const shouldMoveToExternal = (obj, limit) => obj && JSON.stringify(obj).length > limit; const generateNameForExternal = (meta, captureIndex, identifierName) => { - const { path: sourceFilePath, name: functionName, relativePath } = meta; + const { + path: rawSourceFilePath, name: functionName, relativePath, tsBuildDir, + } = meta; + + // Remove typescript tsBuildDir if present + const sourceFilePath = path.relative(tsBuildDir || './', rawSourceFilePath); + const sourceFileDir = path.posix.dirname(path.posix.join('.', sourceFilePath)); const outputDir = path.posix.normalize(path.posix.join(sourceFileDir, relativePath)); const fileName = filePathToFileName(sourceFilePath); const cameledFnName = _.camelCase(functionName); - const externalName = `${cameledFnName}_${captureIndex}_${identifierName}.mock.js`; - const filePath = path.posix.join(outputDir, fileName, externalName); + const extension = tsBuildDir ? 'ts' : 'js'; + const externalName = `${cameledFnName}_${captureIndex}_${identifierName}.mock`; + const filePath = `${path.posix.join(outputDir, fileName, externalName)}.${extension}`; const importPath = `./${path.posix.join(fileName, externalName)}`; const identifier = `${cameledFnName}${captureIndex}${identifierName}`; return { identifier, filePath, importPath }; }; -const packageDataForExternal = obj => prettier.format( - `module.exports = ${wrapSafely(obj)}`, - { - singleQuote: true, - parser: 'babel', - }, -); +const offsetMock = (rawSourceFilePath, mockPath, packagedArguments) => { + // Early exit if from node_modules + if (!mockPath.startsWith('.')) return mockPath; + + const tsBuildDir = packagedArguments.tsBuildDir || './'; + const outputDir = packagedArguments.outputDir || './'; + + // ./dist/dir1/file1.js => ./dir1/file1.js + const sourcePath = path.relative(tsBuildDir, rawSourceFilePath); + // ./dir1/file1.js => ./dir1 + const sourceDir = path.dirname(sourcePath); + + // ./dir1/file1.js => ./test/dir1/file1.js + const offsetSourcePath = path.join(outputDir, sourcePath); + // ./test/dir1/file1.js => ./test/dir1 + const offsetSourceDir = path.dirname(offsetSourcePath); + + // ./dir1 + ../dir2/file2 => ./dir2/file2 + const pathToMockedModule = `./${path.join(sourceDir, mockPath)}`; + + // ./test/dir1 * ./dir2/file2 => ../../dir2/file2 + const relativePath = path.relative(offsetSourceDir, pathToMockedModule); + + // file2 => ./file2 + const formattedRelativePath = relativePath.startsWith('.') ? relativePath : `./${relativePath}`; + + return formattedRelativePath; +}; + +const offsetMocks = (state, filePath, packagedArguments) => { + if (state[filePath].meta.mocks) { + // Take a backup of the original mocks to be used by the import statements + state[filePath].meta.originalMocks = _.cloneDeep(state[filePath].meta.mocks); + + // Offset mocks if they exist + const sourceFilePath = state[filePath].meta.path; + const { mocks } = state[filePath].meta; + state[filePath].meta.mocks = mocks + .map(mock => offsetMock(sourceFilePath, mock, packagedArguments)); + } +}; module.exports = { filePathToFileName, wrapSafely, shouldMoveToExternal, generateNameForExternal, - packageDataForExternal, getOutputFilePath, + offsetMock, + offsetMocks, }; diff --git a/src/generator/utils.test.js b/src/generator/utils.test.js index fa0697f..ceaad02 100644 --- a/src/generator/utils.test.js +++ b/src/generator/utils.test.js @@ -1,4 +1,8 @@ -const { generateNameForExternal, getOutputFilePath } = require('./utils'); +const { + generateNameForExternal, + getOutputFilePath, + offsetMock, +} = require('./utils'); describe('generator_utils', () => { describe('generateNameForExternal', () => { @@ -13,7 +17,24 @@ describe('generator_utils', () => { expect(actual).toEqual({ identifier: 'getClickCounts1result', filePath: 'test_integration/flows/07_large_payload/07_large_payload/getClickCounts_1_result.mock.js', - importPath: './07_large_payload/getClickCounts_1_result.mock.js', + importPath: './07_large_payload/getClickCounts_1_result.mock', + }); + }); + it('should work for typescript', () => { + const sourceFilePath = 'dist/test_integration/flows/07_large_payload/07_large_payload.js'; + const functionName = 'getClickCounts'; + const captureIndex = 1; + const identifierName = 'result'; + const relativePath = './'; + const tsBuildDir = 'dist'; + const meta = { + path: sourceFilePath, name: functionName, relativePath, tsBuildDir, + }; + const actual = generateNameForExternal(meta, captureIndex, identifierName); + expect(actual).toEqual({ + identifier: 'getClickCounts1result', + filePath: 'test_integration/flows/07_large_payload/07_large_payload/getClickCounts_1_result.mock.ts', + importPath: './07_large_payload/getClickCounts_1_result.mock', }); }); it('should work for object exports', () => { @@ -27,7 +48,7 @@ describe('generator_utils', () => { expect(actual).toEqual({ identifier: 'objSubobjFunctionName1result', filePath: 'test_integration/flows/16_exported_objects/16_exported_objects/objSubobjFunctionName_1_result.mock.js', - importPath: './16_exported_objects/objSubobjFunctionName_1_result.mock.js', + importPath: './16_exported_objects/objSubobjFunctionName_1_result.mock', }); }); }); @@ -35,7 +56,7 @@ describe('generator_utils', () => { it('should work when outputDir is null', () => { const filePath = 'dir1/dir2/foo.js'; const outputDir = null; - const actual = getOutputFilePath(filePath, outputDir); + const actual = getOutputFilePath(filePath, { outputDir }); const expected = { outputFilePath: 'dir1/dir2/foo.js', importPath: './foo', @@ -46,7 +67,7 @@ describe('generator_utils', () => { it('should work when outputDir is pwd', () => { const filePath = 'dir1/dir2/foo.js'; const outputDir = './'; - const actual = getOutputFilePath(filePath, outputDir); + const actual = getOutputFilePath(filePath, { outputDir }); const expected = { outputFilePath: 'dir1/dir2/foo.js', importPath: './foo', @@ -57,7 +78,7 @@ describe('generator_utils', () => { it('should work when outputdir is different', () => { const filePath = './dir1/dir2/foo.js'; const outputDir = './dir3'; - const actual = getOutputFilePath(filePath, outputDir); + const actual = getOutputFilePath(filePath, { outputDir }); const expected = { outputFilePath: 'dir3/dir1/dir2/foo.js', importPath: '../../../dir1/dir2/foo', @@ -65,10 +86,11 @@ describe('generator_utils', () => { }; expect(actual).toEqual(expected); }); - it('should work on windows', () => { - const filePath = 'dir1\\dir2\\foo.js'; + it('should work on compiled typescript on windows', () => { + const filePath = 'dist\\dir1\\dir2\\foo.js'; const outputDir = '.\\dir3'; - const actual = getOutputFilePath(filePath, outputDir); + const tsBuildDir = '.\\dist'; + const actual = getOutputFilePath(filePath, { outputDir, tsBuildDir }); const expected = { outputFilePath: 'dir3/dir1/dir2/foo.js', importPath: '../../../dir1/dir2/foo', @@ -76,5 +98,85 @@ describe('generator_utils', () => { }; expect(actual).toEqual(expected); }); + it('should work on compiled typescript', () => { + const filePath = './dist/dir1/dir2/foo.js'; + const outputDir = './dir3'; + const tsBuildDir = './dist'; + const actual = getOutputFilePath(filePath, { outputDir, tsBuildDir }); + const expected = { + outputFilePath: 'dir3/dir1/dir2/foo.js', + importPath: '../../../dir1/dir2/foo', + relativePath: '../../dir3/dir1/dir2/', + }; + expect(actual).toEqual(expected); + }); + }); + describe('offsetMock', () => { + it('should do nothing if not a relative import', () => { + const sourceFilePath = './dir1/file1.js'; + const mockPath = 'fs'; + const packagedArguments = {}; + const expected = 'fs'; + + const actual = offsetMock(sourceFilePath, mockPath, packagedArguments); + expect(actual).toEqual(expected); + }); + it('should do nothing outputDir is not specified', () => { + const sourceFilePath = './dir1/file1.js'; + const mockPath = '../dir2/file2'; + const packagedArguments = { }; + // ./dir1/file1.js + const expected = '../dir2/file2'; + + const actual = offsetMock(sourceFilePath, mockPath, packagedArguments); + expect(actual).toEqual(expected); + }); + it('should have preceding ./', () => { + const sourceFilePath = './dir1/file1.js'; + const mockPath = './file2'; + const packagedArguments = { }; + // ./dir1/file1.js + const expected = './file2'; + + const actual = offsetMock(sourceFilePath, mockPath, packagedArguments); + expect(actual).toEqual(expected); + }); + it('should offset if outputDir is specified', () => { + const sourceFilePath = './dir1/file1.js'; + const mockPath = '../dir2/file2'; + const packagedArguments = { + outputDir: './test', + }; + // ./test/dir1/file1.js + const expected = '../../dir2/file2'; + + const actual = offsetMock(sourceFilePath, mockPath, packagedArguments); + expect(actual).toEqual(expected); + }); + it('should offset if outputDir and tsBuildDir is specified', () => { + const sourceFilePath = './dist/dir1/file1.js'; + const mockPath = '../dir2/file2'; + const packagedArguments = { + outputDir: './test', + tsBuildDir: './dist', + }; + // ./test/dir1/file1.ts + const expected = '../../dir2/file2'; + + const actual = offsetMock(sourceFilePath, mockPath, packagedArguments); + expect(actual).toEqual(expected); + }); + it('should offset if tsBuildDir is specified', () => { + const sourceFilePath = './dist/dir1/file1.js'; + const mockPath = '../dir2/file2'; + const packagedArguments = { + tsBuildDir: './dist', + }; + // ./test/dir1/file1.ts + const expected = '../dir2/file2'; + + const actual = offsetMock(sourceFilePath, mockPath, packagedArguments); + expect(actual).toEqual(expected); + }); }); }); diff --git a/src/util/misc.js b/src/util/misc.js index 0a9d09e..719db2a 100644 --- a/src/util/misc.js +++ b/src/util/misc.js @@ -1,6 +1,10 @@ const _ = require('lodash'); -const getTestFileNameForFile = (filePath, testExt) => filePath.replace('.js', `.${testExt}`); +const getTestFileNameForFile = (filePath, packagedArguments) => { + const { testExt, isTypescript } = packagedArguments; + const ext = isTypescript ? 'ts' : 'js'; + return filePath.replace(/\.[jt]s/, `.${testExt}.${ext}`); +}; // TODO: Make sure it doesnt clober any existing functions of this object const newFunctionNameGenerator = (functionName, fileName) => _.camelCase(`${fileName}.${functionName}`); diff --git a/src/util/misc.test.js b/src/util/misc.test.js index 1ff609f..77f777b 100644 --- a/src/util/misc.test.js +++ b/src/util/misc.test.js @@ -5,12 +5,23 @@ describe('misc', () => { it('should work for windows files', () => { const input = 'C:\\foo\\bar.js'; const expected = 'C:\\foo\\bar.test.js'; - expect(getTestFileNameForFile(input, 'test.js')).toEqual(expected); + const testExt = 'test'; + + expect(getTestFileNameForFile(input, { testExt })).toEqual(expected); }); it('should work for unix files', () => { const input = '/usr/Desktop/foo/bar.js'; - const expected = '/usr/Desktop/foo/bar.test.js'; - expect(getTestFileNameForFile(input, 'test.js')).toEqual(expected); + const expected = '/usr/Desktop/foo/bar.spec.js'; + const testExt = 'spec'; + + expect(getTestFileNameForFile(input, { testExt })).toEqual(expected); + }); + it('should work for typescript', () => { + const input = '/usr/Desktop/foo/bar.ts'; + const expected = '/usr/Desktop/foo/bar.spec.ts'; + const testExt = 'spec'; + + expect(getTestFileNameForFile(input, { testExt, isTypescript: true })).toEqual(expected); }); }); }); diff --git a/src/util/walker.js b/src/util/walker.js index b61d132..7ee7ba8 100644 --- a/src/util/walker.js +++ b/src/util/walker.js @@ -7,7 +7,7 @@ const checkIfDirectoryShouldBeIgnored = fullPath => !!fullPath // TODO: Ignore all directories listed in .gitignore const checkIfFileShouldBeIgnored = (fullPath) => { - const hasJsExtension = fullPath.trim().match(/\.[jt]sx?$/); + const hasJsExtension = fullPath.trim().match(/\.jsx?$/); const isTestFile = fullPath.trim().match(/(test.jsx?|spec.jsx?)/); return !(hasJsExtension && !isTestFile); diff --git a/test_integration/flows/06_mocks/06_mocks_generated.test.js b/test_integration/flows/06_mocks/06_mocks_generated.test.js index 7afbd4e..a6f4d99 100644 --- a/test_integration/flows/06_mocks/06_mocks_generated.test.js +++ b/test_integration/flows/06_mocks/06_mocks_generated.test.js @@ -10,12 +10,12 @@ const { expContinuationFn } = require('./06_mocks'); const { getTodo } = require('./06_mocks'); const { localMocksTest } = require('./06_mocks'); -const expContinuationFn0result = require('./06_mocks/expContinuationFn_0_result.mock.js'); -const expContinuationFn0fsReadFileSync0 = require('./06_mocks/expContinuationFn_0_fsReadFileSync0.mock.js'); -const getTodo0result = require('./06_mocks/getTodo_0_result.mock.js'); -const getTodo0auxilary1Foo40 = require('./06_mocks/getTodo_0_auxilary1Foo40.mock.js'); -const getTodo0fsReadFileSync0 = require('./06_mocks/getTodo_0_fsReadFileSync0.mock.js'); -const getTodo0fsReadFileSync1 = require('./06_mocks/getTodo_0_fsReadFileSync1.mock.js'); +const expContinuationFn0result = require('./06_mocks/expContinuationFn_0_result.mock'); +const expContinuationFn0fsReadFileSync0 = require('./06_mocks/expContinuationFn_0_fsReadFileSync0.mock'); +const getTodo0result = require('./06_mocks/getTodo_0_result.mock'); +const getTodo0auxilary1Foo40 = require('./06_mocks/getTodo_0_auxilary1Foo40.mock'); +const getTodo0fsReadFileSync0 = require('./06_mocks/getTodo_0_fsReadFileSync0.mock'); +const getTodo0fsReadFileSync1 = require('./06_mocks/getTodo_0_fsReadFileSync1.mock'); describe('06_mocks', () => { describe('expContinuationFn', () => { diff --git a/test_integration/flows/07_large_payload/07_large_payload_generated.test.js b/test_integration/flows/07_large_payload/07_large_payload_generated.test.js index 60e9d72..31d6d0c 100644 --- a/test_integration/flows/07_large_payload/07_large_payload_generated.test.js +++ b/test_integration/flows/07_large_payload/07_large_payload_generated.test.js @@ -1,9 +1,9 @@ const { getClickCounts } = require('./07_large_payload'); const { getClickCountsHelper } = require('./07_large_payload'); -const getClickCounts0result = require('./07_large_payload/getClickCounts_0_result.mock.js'); -const getClickCountsHelper0result = require('./07_large_payload/getClickCountsHelper_0_result.mock.js'); -const getClickCountsHelper0requestDataCb0 = require('./07_large_payload/getClickCountsHelper_0_requestDataCb0.mock.js'); +const getClickCounts0result = require('./07_large_payload/getClickCounts_0_result.mock'); +const getClickCountsHelper0result = require('./07_large_payload/getClickCountsHelper_0_result.mock'); +const getClickCountsHelper0requestDataCb0 = require('./07_large_payload/getClickCountsHelper_0_requestDataCb0.mock'); describe('07_large_payload', () => { describe('getClickCounts', () => { diff --git a/test_integration/flows/16_exported_objects/16_exported_objects_generated.test.js b/test_integration/flows/16_exported_objects/16_exported_objects_generated.test.js index 5571741..13746e6 100644 --- a/test_integration/flows/16_exported_objects/16_exported_objects_generated.test.js +++ b/test_integration/flows/16_exported_objects/16_exported_objects_generated.test.js @@ -2,7 +2,7 @@ const { largeObj } = require('./16_exported_objects'); const { obj1 } = require('./16_exported_objects'); const { obj2 } = require('./16_exported_objects'); -const largeObjLargeFun0result = require('./16_exported_objects/largeObjLargeFun_0_result.mock.js'); +const largeObjLargeFun0result = require('./16_exported_objects/largeObjLargeFun_0_result.mock'); describe('16_exported_objects', () => { describe('largeObj.largeFun', () => { diff --git a/test_integration/util/__snapshots__/walker.test.js.snap b/test_integration/util/__snapshots__/walker.test.js.snap index 82d9325..59ee4ef 100644 --- a/test_integration/util/__snapshots__/walker.test.js.snap +++ b/test_integration/util/__snapshots__/walker.test.js.snap @@ -4,7 +4,5 @@ exports[`walker walk should return list of all files in directory 1`] = ` Array [ "test_integration/util/walking_test/a.js", "test_integration/util/walking_test/directory/b.jsx", - "test_integration/util/walking_test/directory/e.tsx", - "test_integration/util/walking_test/f.ts", ] `;