diff --git a/packages/jest-cli/src/SearchSource.js b/packages/jest-cli/src/SearchSource.js index d90516540c04..7b29edf9067e 100644 --- a/packages/jest-cli/src/SearchSource.js +++ b/packages/jest-cli/src/SearchSource.js @@ -25,13 +25,6 @@ const { replacePathSepForRegex, } = require('jest-regex-util'); -type SearchSourceConfig = { - roots: Array, - testMatch: Array, - testRegex: string, - testPathIgnorePatterns: Array, -}; - type SearchResult = {| noSCM?: boolean, paths: Array, @@ -83,7 +76,6 @@ const regexToMatcher = (testRegex: string) => { class SearchSource { _context: Context; - _config: SearchSourceConfig; _options: ResolveModuleConfig; _rootPattern: RegExp; _testIgnorePattern: ?RegExp; @@ -94,13 +86,9 @@ class SearchSource { testPathIgnorePatterns: (path: Path) => boolean, }; - constructor( - context: Context, - config: SearchSourceConfig, - options?: ResolveModuleConfig, - ) { + constructor(context: Context, options?: ResolveModuleConfig) { + const {config} = context; this._context = context; - this._config = config; this._options = options || { skipNodeResolution: false, }; @@ -199,7 +187,9 @@ class SearchSource { } findChangedTests(options: Options): Promise { - return Promise.all(this._config.roots.map(determineSCM)).then(repos => { + return Promise.all( + this._context.config.roots.map(determineSCM), + ).then(repos => { if (!repos.every(([gitRepo, hgRepo]) => gitRepo || hgRepo)) { return { noSCM: true, diff --git a/packages/jest-cli/src/TestPathPatternPrompt.js b/packages/jest-cli/src/TestPathPatternPrompt.js index 86531ba9d805..9117cc6dd4f8 100644 --- a/packages/jest-cli/src/TestPathPatternPrompt.js +++ b/packages/jest-cli/src/TestPathPatternPrompt.js @@ -11,7 +11,8 @@ 'use strict'; import type {Context} from 'types/Context'; -import type {Config, Path} from 'types/Config'; +import type {Test} from 'types/TestRunner'; +import type SearchSource from './SearchSource'; const ansiEscapes = require('ansi-escapes'); const chalk = require('chalk'); @@ -19,9 +20,13 @@ const {getTerminalWidth} = require('./lib/terminalUtils'); const highlight = require('./lib/highlight'); const stringLength = require('string-length'); const {trimAndFormatPath} = require('./reporters/utils'); -const SearchSource = require('./SearchSource'); const Prompt = require('./lib/Prompt'); +type SearchSources = Array<{| + context: Context, + searchSource: SearchSource, +|}>; + const pluralizeFile = (total: number) => total === 1 ? 'file' : 'files'; const usage = () => @@ -34,17 +39,11 @@ const usage = () => const usageRows = usage().split('\n').length; module.exports = class TestPathPatternPrompt { - _config: Config; _pipe: stream$Writable | tty$WriteStream; _prompt: Prompt; - _searchSource: SearchSource; - - constructor( - config: Config, - pipe: stream$Writable | tty$WriteStream, - prompt: Prompt, - ) { - this._config = config; + _searchSources: SearchSources; + + constructor(pipe: stream$Writable | tty$WriteStream, prompt: Prompt) { this._pipe = pipe; this._prompt = prompt; } @@ -65,16 +64,24 @@ module.exports = class TestPathPatternPrompt { regex = new RegExp(pattern, 'i'); } catch (e) {} - const paths = regex - ? this._searchSource.findMatchingTests(pattern).paths - : []; + let paths = []; + if (regex) { + this._searchSources.forEach(({searchSource, context}) => { + paths = paths.concat( + searchSource.findMatchingTests(pattern).paths.map(path => ({ + context, + path, + })), + ); + }); + } this._pipe.write(ansiEscapes.eraseLine); this._pipe.write(ansiEscapes.cursorLeft); this._printTypeahead(pattern, paths, 10); } - _printTypeahead(pattern: string, allResults: Array, max: number) { + _printTypeahead(pattern: string, allResults: Array, max: number) { const total = allResults.length; const results = allResults.slice(0, max); const inputText = `${chalk.dim(' pattern \u203A')} ${pattern}`; @@ -97,14 +104,14 @@ module.exports = class TestPathPatternPrompt { const padding = stringLength(prefix) + 2; results - .map(rawPath => { + .map(({path, context}) => { const filePath = trimAndFormatPath( padding, - this._config, - rawPath, + context.config, + path, width, ); - return highlight(rawPath, filePath, pattern, this._config.rootDir); + return highlight(path, filePath, pattern, context.config.rootDir); }) .forEach(filePath => this._pipe.write(`\n ${chalk.dim('\u203A')} ${filePath}`)); @@ -129,7 +136,7 @@ module.exports = class TestPathPatternPrompt { this._pipe.write(ansiEscapes.cursorRestorePosition); } - updateSearchSource(context: Context) { - this._searchSource = new SearchSource(context, this._config); + updateSearchSources(searchSources: SearchSources) { + this._searchSources = searchSources; } }; diff --git a/packages/jest-cli/src/__tests__/SearchSource-test.js b/packages/jest-cli/src/__tests__/SearchSource-test.js index 5b8a49f10f20..5db27c34914e 100644 --- a/packages/jest-cli/src/__tests__/SearchSource-test.js +++ b/packages/jest-cli/src/__tests__/SearchSource-test.js @@ -45,15 +45,14 @@ describe('SearchSource', () => { rootDir: '.', roots: [], }).config; - return Runtime.createContext(config, {maxWorkers}).then(hasteMap => { - searchSource = new SearchSource(hasteMap, config); + return Runtime.createContext(config, {maxWorkers}).then(context => { + searchSource = new SearchSource(context); }); }); // micromatch doesn't support '..' through the globstar ('**') to avoid // infinite recursion. - - it('supports ../ paths and unix separators via textRegex', () => { + it('supports ../ paths and unix separators via testRegex', () => { if (process.platform !== 'win32') { config = normalizeConfig({ name, @@ -64,8 +63,8 @@ describe('SearchSource', () => { }).config; return Runtime.createContext(config, { maxWorkers, - }).then(hasteMap => { - searchSource = new SearchSource(hasteMap, config); + }).then(context => { + searchSource = new SearchSource(context); const path = '/path/to/__tests__/foo/bar/baz/../../../test.js'; expect(searchSource.isTestFilePath(path)).toEqual(true); @@ -95,8 +94,8 @@ describe('SearchSource', () => { findMatchingTests = config => Runtime.createContext(config, { maxWorkers, - }).then(hasteMap => - new SearchSource(hasteMap, config).findMatchingTests()); + }).then(context => + new SearchSource(context).findMatchingTests()); }); it('finds tests matching a pattern via testRegex', () => { @@ -309,8 +308,8 @@ describe('SearchSource', () => { name: 'SearchSource-findRelatedTests-tests', rootDir, }); - Runtime.createContext(config, {maxWorkers}).then(hasteMap => { - searchSource = new SearchSource(hasteMap, config); + Runtime.createContext(config, {maxWorkers}).then(context => { + searchSource = new SearchSource(context); done(); }); }); @@ -342,8 +341,8 @@ describe('SearchSource', () => { rootDir, testMatch, }); - Runtime.createContext(config, {maxWorkers}).then(hasteMap => { - searchSource = new SearchSource(hasteMap, config); + Runtime.createContext(config, {maxWorkers}).then(context => { + searchSource = new SearchSource(context); done(); }); }); diff --git a/packages/jest-cli/src/__tests__/watch-filename-pattern-mode-test.js b/packages/jest-cli/src/__tests__/watch-filename-pattern-mode-test.js index db6c9c8f9c7e..ed287a71f245 100644 --- a/packages/jest-cli/src/__tests__/watch-filename-pattern-mode-test.js +++ b/packages/jest-cli/src/__tests__/watch-filename-pattern-mode-test.js @@ -80,25 +80,23 @@ afterEach(runJestMock.mockReset); describe('Watch mode flows', () => { let pipe; - let hasteMap; + let hasteMapInstances; let argv; - let context; - let config; + let contexts; let stdin; beforeEach(() => { terminalWidth = 80; pipe = {write: jest.fn()}; - hasteMap = {on: () => {}}; + hasteMapInstances = [{on: () => {}}]; argv = {}; - context = {}; - config = {}; + contexts = [{config: {}}]; stdin = new MockStdin(); }); it('Pressing "P" enters pattern mode', () => { - config = {rootDir: ''}; - watch(config, pipe, argv, hasteMap, context, stdin); + contexts[0].config = {rootDir: ''}; + watch(contexts, argv, pipe, hasteMapInstances, stdin); // Write a enter pattern mode stdin.emit(KEYS.P); @@ -134,8 +132,8 @@ describe('Watch mode flows', () => { }); it('Results in pattern mode get truncated appropriately', () => { - config = {rootDir: ''}; - watch(config, pipe, argv, hasteMap, context, stdin); + contexts[0].config = {rootDir: ''}; + watch(contexts, argv, pipe, hasteMapInstances, stdin); stdin.emit(KEYS.P); diff --git a/packages/jest-cli/src/__tests__/watch-test-name-pattern-mode-test.js b/packages/jest-cli/src/__tests__/watch-test-name-pattern-mode-test.js index de9c8f7931c0..82a9d1d9d0dc 100644 --- a/packages/jest-cli/src/__tests__/watch-test-name-pattern-mode-test.js +++ b/packages/jest-cli/src/__tests__/watch-test-name-pattern-mode-test.js @@ -104,25 +104,23 @@ afterEach(runJestMock.mockReset); describe('Watch mode flows', () => { let pipe; - let hasteMap; + let hasteMapInstances; let argv; - let context; - let config; + let contexts; let stdin; beforeEach(() => { terminalWidth = 80; pipe = {write: jest.fn()}; - hasteMap = {on: () => {}}; + hasteMapInstances = [{on: () => {}}]; argv = {}; - context = {}; - config = {}; + contexts = [{config: {}}]; stdin = new MockStdin(); }); it('Pressing "T" enters pattern mode', () => { - config = {rootDir: ''}; - watch(config, pipe, argv, hasteMap, context, stdin); + contexts[0].config = {rootDir: ''}; + watch(contexts, argv, pipe, hasteMapInstances, stdin); // Write a enter pattern mode stdin.emit(KEYS.T); @@ -158,8 +156,8 @@ describe('Watch mode flows', () => { }); it('Results in pattern mode get truncated appropriately', () => { - config = {rootDir: ''}; - watch(config, pipe, argv, hasteMap, context, stdin); + contexts[0].config = {rootDir: ''}; + watch(contexts, argv, pipe, hasteMapInstances, stdin); stdin.emit(KEYS.T); diff --git a/packages/jest-cli/src/__tests__/watch-test.js b/packages/jest-cli/src/__tests__/watch-test.js index 35f2d7086b9e..16b1e83deed6 100644 --- a/packages/jest-cli/src/__tests__/watch-test.js +++ b/packages/jest-cli/src/__tests__/watch-test.js @@ -37,29 +37,28 @@ afterEach(runJestMock.mockReset); describe('Watch mode flows', () => { let pipe; - let hasteMap; + let hasteMapInstances; let argv; - let context; - let config; + let contexts; let stdin; beforeEach(() => { + const config = {roots: [], testPathIgnorePatterns: [], testRegex: ''}; pipe = {write: jest.fn()}; - hasteMap = {on: () => {}}; + hasteMapInstances = [{on: () => {}}]; argv = {}; - context = {}; - config = {roots: [], testPathIgnorePatterns: [], testRegex: ''}; + contexts = [{config}]; stdin = new MockStdin(); }); it('Correctly passing test path pattern', () => { argv.testPathPattern = 'test-*'; - config.testPathPattern = 'test-*'; + contexts[0].config.testPathPattern = 'test-*'; - watch(config, pipe, argv, hasteMap, context, stdin); + watch(contexts, argv, pipe, hasteMapInstances, stdin); expect(runJestMock).toBeCalledWith( - [context], + contexts, argv, pipe, new TestWatcher({isWatchMode: true}), @@ -70,12 +69,12 @@ describe('Watch mode flows', () => { it('Correctly passing test name pattern', () => { argv.testNamePattern = 'test-*'; - config.testNamePattern = 'test-*'; + contexts[0].config.testNamePattern = 'test-*'; - watch(config, pipe, argv, hasteMap, context, stdin); + watch(contexts, argv, pipe, hasteMapInstances, stdin); expect(runJestMock).toBeCalledWith( - [context], + contexts, argv, pipe, new TestWatcher({isWatchMode: true}), @@ -85,9 +84,9 @@ describe('Watch mode flows', () => { }); it('Runs Jest once by default and shows usage', () => { - watch(config, pipe, argv, hasteMap, context, stdin); + watch(contexts, argv, pipe, hasteMapInstances, stdin); expect(runJestMock).toBeCalledWith( - [context], + contexts, argv, pipe, new TestWatcher({isWatchMode: true}), @@ -98,7 +97,7 @@ describe('Watch mode flows', () => { }); it('Pressing "o" runs test in "only changed files" mode', () => { - watch(config, pipe, argv, hasteMap, context, stdin); + watch(contexts, argv, pipe, hasteMapInstances, stdin); runJestMock.mockReset(); stdin.emit(KEYS.O); @@ -112,7 +111,7 @@ describe('Watch mode flows', () => { }); it('Pressing "a" runs test in "watch all" mode', () => { - watch(config, pipe, argv, hasteMap, context, stdin); + watch(contexts, argv, pipe, hasteMapInstances, stdin); runJestMock.mockReset(); stdin.emit(KEYS.A); @@ -126,14 +125,14 @@ describe('Watch mode flows', () => { }); it('Pressing "ENTER" reruns the tests', () => { - watch(config, pipe, argv, hasteMap, context, stdin); + watch(contexts, argv, pipe, hasteMapInstances, stdin); expect(runJestMock).toHaveBeenCalledTimes(1); stdin.emit(KEYS.ENTER); expect(runJestMock).toHaveBeenCalledTimes(2); }); it('Pressing "u" reruns the tests in "update snapshot" mode', () => { - watch(config, pipe, argv, hasteMap, context, stdin); + watch(contexts, argv, pipe, hasteMapInstances, stdin); runJestMock.mockReset(); stdin.emit(KEYS.U); diff --git a/packages/jest-cli/src/cli/runCLI.js b/packages/jest-cli/src/cli/runCLI.js index d7698dd2e884..e07c8d19116e 100644 --- a/packages/jest-cli/src/cli/runCLI.js +++ b/packages/jest-cli/src/cli/runCLI.js @@ -53,44 +53,33 @@ module.exports = async ( logDebugMessages(configs[0].config, pipe); } - if (argv.watch || argv.watchAll) { - const {config, hasDeprecationWarnings} = configs[0]; - createDirectory(config.cacheDirectory); - const hasteMapInstance = Runtime.createHasteMap(config, { - console: new Console(pipe, pipe), - maxWorkers: getMaxWorkers(argv), - resetCache: !config.cache, - watch: config.watch, - }); + const hasteMapInstances = Array(configs.length); + const contexts = await Promise.all( + configs.map(async ({config}, index) => { + createDirectory(config.cacheDirectory); + const hasteMapInstance = Runtime.createHasteMap(config, { + console: new Console(pipe, pipe), + maxWorkers: getMaxWorkers(argv), + resetCache: !config.cache, + watch: config.watch, + }); + hasteMapInstances[index] = hasteMapInstance; + return createContext(config, await hasteMapInstance.build()); + }), + ); - const hasteMap = await hasteMapInstance.build(); - const context = createContext(config, hasteMap); - if (hasDeprecationWarnings) { + if (argv.watch || argv.watchAll) { + if (configs.some(({hasDeprecationWarnings}) => hasDeprecationWarnings)) { try { await handleDeprecationWarnings(pipe, process.stdin); - return watch(config, pipe, argv, hasteMapInstance, context); + return watch(contexts, argv, pipe, hasteMapInstances); } catch (e) { process.exit(0); } } - return watch(config, pipe, argv, hasteMapInstance, context); + return watch(contexts, argv, pipe, hasteMapInstances); } else { - const contexts = await Promise.all( - configs.map(async ({config}) => { - createDirectory(config.cacheDirectory); - return createContext( - config, - await Runtime.createHasteMap(config, { - console: new Console(pipe, pipe), - maxWorkers: getMaxWorkers(argv), - resetCache: !config.cache, - watch: config.watch, - }).build(), - ); - }), - ); - const startRun = () => { preRunMessage.print(pipe); runJest( diff --git a/packages/jest-cli/src/runJest.js b/packages/jest-cli/src/runJest.js index c4624cc8a7e9..9fe9bf232227 100644 --- a/packages/jest-cli/src/runJest.js +++ b/packages/jest-cli/src/runJest.js @@ -113,7 +113,7 @@ const getNoTestsFoundMessage = (testRunData, pattern) => { }; const getTestPaths = async (context, pattern, argv, pipe) => { - const source = new SearchSource(context, context.config); + const source = new SearchSource(context); let data = await source.getTestPaths(pattern); if (!data.paths.length) { const localConsole = new Console(pipe, pipe); diff --git a/packages/jest-cli/src/watch.js b/packages/jest-cli/src/watch.js index 7e1cc751e562..15af16f46f5c 100644 --- a/packages/jest-cli/src/watch.js +++ b/packages/jest-cli/src/watch.js @@ -10,7 +10,6 @@ 'use strict'; import type {Context} from 'types/Context'; -import type {Config} from 'types/Config'; const ansiEscapes = require('ansi-escapes'); const chalk = require('chalk'); @@ -20,18 +19,20 @@ const isValidPath = require('./lib/isValidPath'); const preRunMessage = require('./preRunMessage'); const runJest = require('./runJest'); const setState = require('./lib/setState'); +const SearchSource = require('./SearchSource'); const TestWatcher = require('./TestWatcher'); const Prompt = require('./lib/Prompt'); const TestPathPatternPrompt = require('./TestPathPatternPrompt'); const TestNamePatternPrompt = require('./TestNamePatternPrompt'); const {KEYS, CLEAR} = require('./constants'); +let hasExitListener = false; + const watch = ( - config: Config, - pipe: stream$Writable | tty$WriteStream, + contexts: Array, argv: Object, - hasteMap: HasteMap, - context: Context, + pipe: stream$Writable | tty$WriteStream, + hasteMapInstances: Array, stdin?: stream$Readable | tty$ReadStream = process.stdin, ) => { setState(argv, argv.watch ? 'watch' : 'watchAll', { @@ -41,35 +42,55 @@ const watch = ( }); const prompt = new Prompt(); - const testPathPatternPrompt = new TestPathPatternPrompt(config, pipe, prompt); + const testPathPatternPrompt = new TestPathPatternPrompt(pipe, prompt); const testNamePatternPrompt = new TestNamePatternPrompt(pipe, prompt); + let searchSources = contexts.map(context => ({ + context, + searchSource: new SearchSource(context), + })); let hasSnapshotFailure = false; let isRunning = false; let testWatcher; let shouldDisplayWatchUsage = true; let isWatchUsageDisplayed = false; - testPathPatternPrompt.updateSearchSource(context); + testPathPatternPrompt.updateSearchSources(searchSources); - hasteMap.on('change', ({eventsQueue, hasteFS, moduleMap}) => { - const validPaths = eventsQueue.filter(({filePath}) => { - return isValidPath(config, filePath); - }); + hasteMapInstances.forEach((hasteMapInstance, index) => { + hasteMapInstance.on('change', ({eventsQueue, hasteFS, moduleMap}) => { + const validPaths = eventsQueue.filter(({filePath}) => { + return isValidPath(contexts[index].config, filePath); + }); - if (validPaths.length) { - context = createContext(config, {hasteFS, moduleMap}); - prompt.abort(); - testPathPatternPrompt.updateSearchSource(context); - startRun(); - } + if (validPaths.length) { + const context = (contexts[index] = createContext( + contexts[index].config, + { + hasteFS, + moduleMap, + }, + )); + prompt.abort(); + searchSources = searchSources.slice(); + searchSources[index] = { + context, + searchSource: new SearchSource(context), + }; + testPathPatternPrompt.updateSearchSources(searchSources); + startRun(); + } + }); }); - process.on('exit', () => { - if (prompt.isEntering()) { - pipe.write(ansiEscapes.cursorDown()); - pipe.write(ansiEscapes.eraseDown); - } - }); + if (!hasExitListener) { + hasExitListener = true; + process.on('exit', () => { + if (prompt.isEntering()) { + pipe.write(ansiEscapes.cursorDown()); + pipe.write(ansiEscapes.eraseDown); + } + }); + } const startRun = (overrideConfig: Object = {}) => { if (isRunning) { @@ -80,19 +101,21 @@ const watch = ( pipe.write(CLEAR); preRunMessage.print(pipe); isRunning = true; - // $FlowFixMe - context.config = Object.freeze( + contexts.forEach(context => { // $FlowFixMe - Object.assign( - { - testNamePattern: argv.testNamePattern, - testPathPattern: argv.testPathPattern, - }, - config, - overrideConfig, - ), - ); - return runJest([context], argv, pipe, testWatcher, startRun, results => { + context.config = Object.freeze( + // $FlowFixMe + Object.assign( + { + testNamePattern: argv.testNamePattern, + testPathPattern: argv.testPathPattern, + }, + context.config, + overrideConfig, + ), + ); + }); + return runJest(contexts, argv, pipe, testWatcher, startRun, results => { isRunning = false; hasSnapshotFailure = !!results.snapshot.failure; // Create a new testWatcher instance so that re-runs won't be blocked. @@ -110,7 +133,7 @@ const watch = ( } testNamePatternPrompt.updateCachedTestResults(results.testResults); - }).then(() => {}, error => console.error(chalk.red(error.stack))); + }).catch(error => console.error(chalk.red(error.stack))); }; const onKeypress = (key: string) => { diff --git a/types/TestRunner.js b/types/TestRunner.js index 04fbd9554dc3..469bf1fc8005 100644 --- a/types/TestRunner.js +++ b/types/TestRunner.js @@ -12,10 +12,10 @@ import type {Context} from './Context'; import type {Path} from './Config'; -export type Test = { +export type Test = {| context: Context, path: Path, duration?: number, -}; +|}; export type Tests = Array;