From 6a7f01f588e6890a31c3612f443c56e2000d194f Mon Sep 17 00:00:00 2001 From: Huafu Gandon Date: Tue, 21 Aug 2018 08:04:22 +0200 Subject: [PATCH] feat: directly writes to stdio so jest does not swallow --- src/__helpers__/mocks.ts | 55 ----------------- src/lib/__snapshots__/backports.spec.ts.snap | 22 +++---- src/lib/backports.spec.ts | 15 +++-- src/lib/debug.spec.ts | 60 ++++++++++++------- src/lib/debug.ts | 17 +++++- src/lib/transformers/hoisting.ts | 63 ++++++++++++++++++-- tslint.json | 4 +- 7 files changed, 131 insertions(+), 105 deletions(-) diff --git a/src/__helpers__/mocks.ts b/src/__helpers__/mocks.ts index c3998662d9..8c578653ce 100644 --- a/src/__helpers__/mocks.ts +++ b/src/__helpers__/mocks.ts @@ -9,58 +9,3 @@ export function spied( ): T extends (...args: any[]) => any ? jest.SpyInstance : jest.Mocked { return val as any } - -export function mockThese( - map: - | string[] - | { - [k: string]: () => any - }, -) { - const isArray = Array.isArray(map) - const items: string[] = isArray ? (map as string[]) : Object.keys(map) - items.forEach(item => { - const val = isArray ? () => item : (map as any)[item] - jest.doMock(item, val, { virtual: true }) - }) -} - -export function spyThese( - object: T, - implementations: { [key in K]: T[K] | any | undefined }, -): { [key in K]: jest.SpyInstance } & { - mockRestore: () => void - mockReset: () => void - mockClear: () => void -} { - const keys = Object.keys(implementations) as K[] - const res = keys.reduce( - (map, key) => { - const actual = object[key] as any - const spy = (map[key] = jest.spyOn(object, key as K)) - if (implementations[key]) { - const impl = implementations[key] as (...args: any[]) => any - if (impl.length && /\W\$super\W/.test(impl.toString())) { - spy.mockImplementation(function(this: T, ...args: any[]) { - return impl.call(this, () => actual.apply(this, args), ...args) - }) - } else { - spy.mockImplementation(impl) - } - } - return map - }, - {} as any, - ) - // utility to restore/reset/clear all - res.mockRestore = () => { - keys.forEach(key => res[key].mockRestore()) - } - res.mockReset = () => { - keys.forEach(key => res[key].mockReset()) - } - res.mockClear = () => { - keys.forEach(key => res[key].mockClear()) - } - return res -} diff --git a/src/lib/__snapshots__/backports.spec.ts.snap b/src/lib/__snapshots__/backports.spec.ts.snap index f9a9d10c82..bb0dfbd196 100644 --- a/src/lib/__snapshots__/backports.spec.ts.snap +++ b/src/lib/__snapshots__/backports.spec.ts.snap @@ -16,7 +16,7 @@ Object { } `; -exports[`backportJestConfig with "globals.__TRANSFORM_HTML__" set to false should wran the user 1`] = `"ts-jest: \\"[jest-config].globals.__TRANSFORM_HTML__\\" is deprecated, use \\"[jest-config].globals.ts-jest.stringifyContentPathRegex\\" instead."`; +exports[`backportJestConfig with "globals.__TRANSFORM_HTML__" set to false should wran the user 1`] = `"warn ts-jest: \\"[jest-config].globals.__TRANSFORM_HTML__\\" is deprecated, use \\"[jest-config].globals.ts-jest.stringifyContentPathRegex\\" instead."`; exports[`backportJestConfig with "globals.__TRANSFORM_HTML__" set to true should have changed the config correctly: before 1`] = ` Object { @@ -36,7 +36,7 @@ Object { } `; -exports[`backportJestConfig with "globals.__TRANSFORM_HTML__" set to true should wran the user 1`] = `"ts-jest: \\"[jest-config].globals.__TRANSFORM_HTML__\\" is deprecated, use \\"[jest-config].globals.ts-jest.stringifyContentPathRegex\\" instead."`; +exports[`backportJestConfig with "globals.__TRANSFORM_HTML__" set to true should wran the user 1`] = `"warn ts-jest: \\"[jest-config].globals.__TRANSFORM_HTML__\\" is deprecated, use \\"[jest-config].globals.ts-jest.stringifyContentPathRegex\\" instead."`; exports[`backportJestConfig with "globals.__TS_CONFIG__" set to { foo: 'bar' } should have changed the config correctly: before 1`] = ` Object { @@ -60,7 +60,7 @@ Object { } `; -exports[`backportJestConfig with "globals.__TS_CONFIG__" set to { foo: 'bar' } should wran the user 1`] = `"ts-jest: \\"[jest-config].globals.__TS_CONFIG__\\" is deprecated, use \\"[jest-config].globals.ts-jest.tsConfig\\" instead."`; +exports[`backportJestConfig with "globals.__TS_CONFIG__" set to { foo: 'bar' } should wran the user 1`] = `"warn ts-jest: \\"[jest-config].globals.__TS_CONFIG__\\" is deprecated, use \\"[jest-config].globals.ts-jest.tsConfig\\" instead."`; exports[`backportJestConfig with "globals.ts-jest.enableTsDiagnostics" set to '\\\\.spec\\\\.ts$' should have changed the config correctly: before 1`] = ` Object { @@ -84,7 +84,7 @@ Object { } `; -exports[`backportJestConfig with "globals.ts-jest.enableTsDiagnostics" set to '\\\\.spec\\\\.ts$' should wran the user 1`] = `"ts-jest: \\"[jest-config].globals.ts-jest.enableTsDiagnostics\\" is deprecated, use \\"[jest-config].globals.ts-jest.diagnostics\\" instead."`; +exports[`backportJestConfig with "globals.ts-jest.enableTsDiagnostics" set to '\\\\.spec\\\\.ts$' should wran the user 1`] = `"warn ts-jest: \\"[jest-config].globals.ts-jest.enableTsDiagnostics\\" is deprecated, use \\"[jest-config].globals.ts-jest.diagnostics\\" instead."`; exports[`backportJestConfig with "globals.ts-jest.enableTsDiagnostics" set to false should have changed the config correctly: before 1`] = ` Object { @@ -106,7 +106,7 @@ Object { } `; -exports[`backportJestConfig with "globals.ts-jest.enableTsDiagnostics" set to false should wran the user 1`] = `"ts-jest: \\"[jest-config].globals.ts-jest.enableTsDiagnostics\\" is deprecated, use \\"[jest-config].globals.ts-jest.diagnostics\\" instead."`; +exports[`backportJestConfig with "globals.ts-jest.enableTsDiagnostics" set to false should wran the user 1`] = `"warn ts-jest: \\"[jest-config].globals.ts-jest.enableTsDiagnostics\\" is deprecated, use \\"[jest-config].globals.ts-jest.diagnostics\\" instead."`; exports[`backportJestConfig with "globals.ts-jest.enableTsDiagnostics" set to true should have changed the config correctly: before 1`] = ` Object { @@ -128,7 +128,7 @@ Object { } `; -exports[`backportJestConfig with "globals.ts-jest.enableTsDiagnostics" set to true should wran the user 1`] = `"ts-jest: \\"[jest-config].globals.ts-jest.enableTsDiagnostics\\" is deprecated, use \\"[jest-config].globals.ts-jest.diagnostics\\" instead."`; +exports[`backportJestConfig with "globals.ts-jest.enableTsDiagnostics" set to true should wran the user 1`] = `"warn ts-jest: \\"[jest-config].globals.ts-jest.enableTsDiagnostics\\" is deprecated, use \\"[jest-config].globals.ts-jest.diagnostics\\" instead."`; exports[`backportJestConfig with "globals.ts-jest.skipBabel" set to false should have changed the config correctly: before 1`] = ` Object { @@ -150,7 +150,7 @@ Object { } `; -exports[`backportJestConfig with "globals.ts-jest.skipBabel" set to false should wran the user 1`] = `"ts-jest: \\"[jest-config].globals.ts-jest.skipBabel\\" is deprecated, use \\"[jest-config].globals.ts-jest.babelConfig\\" instead."`; +exports[`backportJestConfig with "globals.ts-jest.skipBabel" set to false should wran the user 1`] = `"warn ts-jest: \\"[jest-config].globals.ts-jest.skipBabel\\" is deprecated, use \\"[jest-config].globals.ts-jest.babelConfig\\" instead."`; exports[`backportJestConfig with "globals.ts-jest.skipBabel" set to true should have changed the config correctly: before 1`] = ` Object { @@ -170,7 +170,7 @@ Object { } `; -exports[`backportJestConfig with "globals.ts-jest.skipBabel" set to true should wran the user 1`] = `"ts-jest: \\"[jest-config].globals.ts-jest.skipBabel\\" is deprecated, use \\"[jest-config].globals.ts-jest.babelConfig\\" instead."`; +exports[`backportJestConfig with "globals.ts-jest.skipBabel" set to true should wran the user 1`] = `"warn ts-jest: \\"[jest-config].globals.ts-jest.skipBabel\\" is deprecated, use \\"[jest-config].globals.ts-jest.babelConfig\\" instead."`; exports[`backportJestConfig with "globals.ts-jest.tsConfigFile" set to 'tsconfig.build.json' should have changed the config correctly: before 1`] = ` Object { @@ -192,7 +192,7 @@ Object { } `; -exports[`backportJestConfig with "globals.ts-jest.tsConfigFile" set to 'tsconfig.build.json' should wran the user 1`] = `"ts-jest: \\"[jest-config].globals.ts-jest.tsConfigFile\\" is deprecated, use \\"[jest-config].globals.ts-jest.tsConfig\\" instead."`; +exports[`backportJestConfig with "globals.ts-jest.tsConfigFile" set to 'tsconfig.build.json' should wran the user 1`] = `"warn ts-jest: \\"[jest-config].globals.ts-jest.tsConfigFile\\" is deprecated, use \\"[jest-config].globals.ts-jest.tsConfig\\" instead."`; exports[`backportJestConfig with "globals.ts-jest.useBabelrc" set to false should have changed the config correctly: before 1`] = ` Object { @@ -215,7 +215,7 @@ Object { `; exports[`backportJestConfig with "globals.ts-jest.useBabelrc" set to false should wran the user 1`] = ` -"ts-jest: \\"[jest-config].globals.ts-jest.useBabelrc\\" is deprecated, use \\"[jest-config].globals.ts-jest.babelConfig\\" instead. +"warn ts-jest: \\"[jest-config].globals.ts-jest.useBabelrc\\" is deprecated, use \\"[jest-config].globals.ts-jest.babelConfig\\" instead. ↳ See \`babel-jest\` related issue: https://github.com/facebook/jest/issues/3845" `; @@ -240,6 +240,6 @@ Object { `; exports[`backportJestConfig with "globals.ts-jest.useBabelrc" set to true should wran the user 1`] = ` -"ts-jest: \\"[jest-config].globals.ts-jest.useBabelrc\\" is deprecated, use \\"[jest-config].globals.ts-jest.babelConfig\\" instead. +"warn ts-jest: \\"[jest-config].globals.ts-jest.useBabelrc\\" is deprecated, use \\"[jest-config].globals.ts-jest.babelConfig\\" instead. ↳ See \`babel-jest\` related issue: https://github.com/facebook/jest/issues/3845" `; diff --git a/src/lib/backports.spec.ts b/src/lib/backports.spec.ts index 96e7a44e7b..71ad5b2508 100644 --- a/src/lib/backports.spec.ts +++ b/src/lib/backports.spec.ts @@ -1,13 +1,12 @@ import { backportJestConfig } from './backports' -import { spyThese } from '../__helpers__/mocks' import set from 'lodash.set' import { inspect } from 'util' +import { __setup } from './debug' -const consoleSpies = spyThese(console, { - warn: () => undefined, -}) -afterEach(() => { - consoleSpies.mockReset() +const logger = jest.fn() +__setup({ logger }) +beforeEach(() => { + logger.mockClear() }) describe('backportJestConfig', () => { @@ -21,8 +20,8 @@ describe('backportJestConfig', () => { describe(`with "${oldPath}" set to ${inspect(val)}`, () => { it(`should wran the user`, () => { backportJestConfig(original) - expect(consoleSpies.warn).toHaveBeenCalledTimes(1) - expect(consoleSpies.warn.mock.calls[0].join(' ')).toMatchSnapshot() + expect(logger).toHaveBeenCalledTimes(1) + expect(logger.mock.calls[0].join(' ')).toMatchSnapshot() }) // should warn the user it(`should have changed the config correctly`, () => { expect(original).toMatchSnapshot('before') diff --git a/src/lib/debug.spec.ts b/src/lib/debug.spec.ts index fabc0bf6bc..fc49887aba 100644 --- a/src/lib/debug.spec.ts +++ b/src/lib/debug.spec.ts @@ -1,35 +1,53 @@ -import { spyThese } from '../__helpers__/mocks' -import { debug, wrapWithDebug, __setup } from './debug' +import { debug, wrapWithDebug, __setup, warn } from './debug' -const consoleSpies = spyThese(console, { - log: () => void 0, - warn: () => void 0, -}) +const stdoutSpy = jest.spyOn(process.stdout, 'write') +const stderrSpy = jest.spyOn(process.stderr, 'write') beforeEach(() => { delete process.env.TS_JEST_DEBUG - consoleSpies.mockClear() + stderrSpy.mockClear() + stdoutSpy.mockClear() +}) +afterAll(() => { + stderrSpy.mockRestore() + stdoutSpy.mockRestore() }) describe('debug', () => { - it('should log when TS_JEST_DEBUG is truthy', () => { + it('should log to stdout when TS_JEST_DEBUG is truthy', () => { process.env.TS_JEST_DEBUG = '1' __setup() debug('foo') - expect(consoleSpies.log).toHaveBeenCalledTimes(1) - expect(consoleSpies.log).toHaveBeenCalledWith('ts-jest:', 'foo') + expect(stdoutSpy).toHaveBeenCalledTimes(1) + expect(stdoutSpy).toHaveBeenCalledWith('ts-jest: foo\n') }) - it('should NOT log when TS_JEST_DEBUG is falsy', () => { + it('should NOT log to stdout when TS_JEST_DEBUG is falsy', () => { process.env.TS_JEST_DEBUG = '' __setup() debug('foo') - expect(consoleSpies.log).not.toHaveBeenCalled() + expect(stdoutSpy).not.toHaveBeenCalled() }) - it('should NOT log when TS_JEST_DEBUG is not set', () => { + it('should NOT log to stdout when TS_JEST_DEBUG is not set', () => { delete process.env.TS_JEST_DEBUG __setup() debug('foo') - expect(consoleSpies.log).not.toHaveBeenCalled() + expect(stdoutSpy).not.toHaveBeenCalled() + }) +}) +describe('warn', () => { + it('should log to stderr when TS_JEST_DEBUG is truthy', () => { + process.env.TS_JEST_DEBUG = '1' + __setup() + warn('foo') + expect(stderrSpy).toHaveBeenCalledTimes(1) + expect(stderrSpy).toHaveBeenCalledWith('ts-jest: foo\n') + }) + it('should log to stderr even when TS_JEST_DEBUG is falsy', () => { + delete process.env.TS_JEST_DEBUG + __setup() + warn('foo') + expect(stderrSpy).toHaveBeenCalledTimes(1) + expect(stderrSpy).toHaveBeenCalledWith('ts-jest: foo\n') }) }) @@ -37,23 +55,23 @@ describe('wrapWithDebug', () => { const subject = (val: string) => `hello ${val}` const wrapAndCall = (val: string) => wrapWithDebug('foo', subject)(val) - it('should log when TS_JEST_DEBUG is truthy', () => { + it('should log to stdout when TS_JEST_DEBUG is truthy', () => { process.env.TS_JEST_DEBUG = '1' __setup() expect(wrapAndCall('bar')).toBe('hello bar') - expect(consoleSpies.log).toHaveBeenCalledTimes(1) - expect(consoleSpies.log).toHaveBeenCalledWith('ts-jest:', 'foo') + expect(stdoutSpy).toHaveBeenCalledTimes(1) + expect(stdoutSpy).toHaveBeenCalledWith('ts-jest: foo\n') }) - it('should NOT log when TS_JEST_DEBUG is falsy', () => { + it('should NOT log to stdout when TS_JEST_DEBUG is falsy', () => { process.env.TS_JEST_DEBUG = '' __setup() expect(wrapAndCall('bar')).toBe('hello bar') - expect(consoleSpies.log).not.toHaveBeenCalled() + expect(stdoutSpy).not.toHaveBeenCalled() }) - it('should NOT log when TS_JEST_DEBUG is not set', () => { + it('should NOT log to stdout when TS_JEST_DEBUG is not set', () => { delete process.env.TS_JEST_DEBUG __setup() expect(wrapAndCall('bar')).toBe('hello bar') - expect(consoleSpies.log).not.toHaveBeenCalled() + expect(stdoutSpy).not.toHaveBeenCalled() }) }) diff --git a/src/lib/debug.ts b/src/lib/debug.ts index b163f85319..cad083acd6 100644 --- a/src/lib/debug.ts +++ b/src/lib/debug.ts @@ -1,3 +1,5 @@ +import { format } from 'util' + export let DEBUG_MODE!: boolean export let debug!: typeof console.log @@ -8,13 +10,22 @@ export let wrapWithDebug!: any>( func: T, ) => T -type LogKind = 'log' | 'warn' | 'debug' | 'info' +type LogKind = 'log' | 'warn' type Logger = (kind: LogKind, ...args: any[]) => void export let LOG_PREFIX = 'ts-jest:' -export const defaultLogger: Logger = (kind: LogKind, ...args: any[]) => { - console[kind](...args) +export const defaultLogger: Logger = ( + kind: LogKind, + msg: string = '', + ...args: any[] +) => { + // we use stderr/stdout dirrectly so that the log won't be swallowed by jest + if (kind === 'warn') { + process.stderr.write(format(msg, ...args) + '\n') + } else if (kind) { + process.stdout.write(format(msg, ...args) + '\n') + } } interface SetupOptions { diff --git a/src/lib/transformers/hoisting.ts b/src/lib/transformers/hoisting.ts index be7b9ec250..dbb810dc1e 100644 --- a/src/lib/transformers/hoisting.ts +++ b/src/lib/transformers/hoisting.ts @@ -1,5 +1,5 @@ // tslint:disable:curly -// take care of including ONLY TYPES here, for the rest use ts +// take care of including ONLY TYPES here, for the rest use `ts` import { Node, ExpressionStatement, @@ -12,10 +12,26 @@ import { } from 'typescript' import { ConfigSet } from '../config-set' +/** + * What methods of `jest` should we hoist + */ +const HOIST_METHODS = ['mock', 'unmock'] + +/** + * The factory of hoisting transformer factory + * @param cs Current jest configuration-set + */ export function factory(cs: ConfigSet) { + /** + * Our compiler (typescript, or a module with typescript-like interface) + */ const ts = cs.compilerModule - function isJestMockCallExpression(node: Node): node is ExpressionStatement { + /** + * Checks whether given node is a statement that we need to hoist + * @param node The node to test + */ + function shouldHoistNode(node: Node): node is ExpressionStatement { return ( ts.isExpressionStatement(node) && ts.isCallExpression(node.expression) && @@ -23,13 +39,27 @@ export function factory(cs: ConfigSet) { ts.isIdentifier(node.expression.expression.expression) && node.expression.expression.expression.text === 'jest' && ts.isIdentifier(node.expression.expression.name) && - node.expression.expression.name.text === 'mock' + HOIST_METHODS.includes(node.expression.expression.name.text) ) } + /** + * Create a source file visitor which will visit all nodes in a source file + * @param ctx The typescript transformation context + * @param sf The owning source file + */ function createVisitor(ctx: TransformationContext, sf: SourceFile) { + /** + * Current block level + */ let level = 0 + /** + * List of nodes which needs to be hoisted, indexed by their owning level + */ const hoisted: Statement[][] = [] + /** + * Called when we enter a block to increase the level + */ const enter = () => { level++ // reuse arrays @@ -37,7 +67,14 @@ export function factory(cs: ConfigSet) { hoisted[level].splice(0, hoisted[level].length) } } + /** + * Called when we leave a block to devrease the level + */ const exit = () => level-- + /** + * Adds a node to the list of nodes to be hoisted in the current level + * @param node The node to hoist + */ const hoist = (node: Statement) => { if (hoisted[level]) { hoisted[level].push(node) @@ -45,28 +82,44 @@ export function factory(cs: ConfigSet) { hoisted[level] = [node] } } - + /** + * Our main visitor, which will be called recursively for each node in the source file's AST + * @param node The node to be visited + */ const visitor: Visitor = node => { + // enter this level enter() + + // visit each child const resultNode = ts.visitEachChild(node, visitor, ctx) + + // check if we have something to hoist in this level if (hoisted[level] && hoisted[level].length) { + // re-order children so that hoisted ones appear first + // this is actually the main work of this transformer const block = resultNode as Block block.statements = ts.createNodeArray([ ...hoisted[level], ...block.statements, ]) } + + // exit the level exit() - if (isJestMockCallExpression(resultNode)) { + if (shouldHoistNode(resultNode)) { + // hoist into current level hoist(resultNode as Statement) return } + + // finsally returns the currently visited node return resultNode } return visitor } + // returns the transformer factory return (ctx: TransformationContext): Transformer => { return (sf: SourceFile) => ts.visitNode(sf, createVisitor(ctx, sf)) } diff --git a/tslint.json b/tslint.json index b0bdc0b34a..2a066cf55b 100644 --- a/tslint.json +++ b/tslint.json @@ -14,7 +14,7 @@ "functions": "always", "typeLiterals": "ignore" }, - "esSpecCompliant": false + "esSpecCompliant": true } ], "no-shadowed-variable": false, @@ -67,4 +67,4 @@ "**/node_modules/**/*" ] } -} \ No newline at end of file +}