diff --git a/CHANGELOG.md b/CHANGELOG.md index e8056d58bf32..8e2af27a9374 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ### Features +* `[jest-cli]` Added `--notifyMode` to specify when to be notified. + ([#5125](https://github.com/facebook/jest/pull/5125)) * `[diff-sequences]` New package compares items in two sequences to find a **longest common subsequence**. ([#5407](https://github.com/facebook/jest/pull/5407)) diff --git a/docs/Configuration.md b/docs/Configuration.md index 42cc5bc9f4b7..e38ac19a510e 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -425,6 +425,21 @@ Default: `false` Activates notifications for test results. +### `notifyMode` [string] + +Default: `always` + +Specifies notification mode. Requires `notify: true`. + +#### Modes + +* `always`: always send a notification. +* `failure`: send a notification when tests fail. +* `success`: send a notification when tests pass. +* `change`: send a notification when the status changed. +* `success-change`: send a notification when tests pass or once when it fails. +* `failure-success`: send a notification when tests fails or once when it passes. + ### `preset` [string] Default: `undefined` diff --git a/integration-tests/__tests__/__snapshots__/show_config.test.js.snap b/integration-tests/__tests__/__snapshots__/show_config.test.js.snap index d7b23f1cc99d..0d9f5b139263 100644 --- a/integration-tests/__tests__/__snapshots__/show_config.test.js.snap +++ b/integration-tests/__tests__/__snapshots__/show_config.test.js.snap @@ -80,6 +80,7 @@ exports[`--showConfig outputs config info and exits 1`] = ` \\"noStackTrace\\": false, \\"nonFlagArgs\\": [], \\"notify\\": false, + \\"notifyMode\\": \\"always\\", \\"passWithNoTests\\": false, \\"rootDir\\": \\"<>\\", \\"runTestsByPath\\": false, diff --git a/packages/jest-cli/src/__tests__/__snapshots__/notify_reporter.test.js.snap b/packages/jest-cli/src/__tests__/__snapshots__/notify_reporter.test.js.snap new file mode 100644 index 000000000000..30a010dcd5ef --- /dev/null +++ b/packages/jest-cli/src/__tests__/__snapshots__/notify_reporter.test.js.snap @@ -0,0 +1,118 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`test always 1`] = ` +Array [ + Object { + "message": "3 tests passed", + "title": "100% Passed", + }, + Object { + "message": "3 of 3 tests failed", + "title": "100% Failed", + }, + Object { + "message": "3 tests passed", + "title": "100% Passed", + }, + Object { + "message": "3 tests passed", + "title": "100% Passed", + }, + Object { + "message": "3 of 3 tests failed", + "title": "100% Failed", + }, + Object { + "message": "3 of 3 tests failed", + "title": "100% Failed", + }, +] +`; + +exports[`test change 1`] = ` +Array [ + Object { + "message": "3 tests passed", + "title": "100% Passed", + }, + Object { + "message": "3 of 3 tests failed", + "title": "100% Failed", + }, + Object { + "message": "3 tests passed", + "title": "100% Passed", + }, + Object { + "message": "3 of 3 tests failed", + "title": "100% Failed", + }, +] +`; + +exports[`test failure-change 1`] = ` +Array [ + Object { + "message": "3 tests passed", + "title": "100% Passed", + }, + Object { + "message": "3 of 3 tests failed", + "title": "100% Failed", + }, + Object { + "message": "3 tests passed", + "title": "100% Passed", + }, + Object { + "message": "3 of 3 tests failed", + "title": "100% Failed", + }, + Object { + "message": "3 of 3 tests failed", + "title": "100% Failed", + }, +] +`; + +exports[`test success 1`] = ` +Array [ + Object { + "message": "3 tests passed", + "title": "100% Passed", + }, + Object { + "message": "3 tests passed", + "title": "100% Passed", + }, + Object { + "message": "3 tests passed", + "title": "100% Passed", + }, +] +`; + +exports[`test success-change 1`] = ` +Array [ + Object { + "message": "3 tests passed", + "title": "100% Passed", + }, + Object { + "message": "3 of 3 tests failed", + "title": "100% Failed", + }, + Object { + "message": "3 tests passed", + "title": "100% Passed", + }, + Object { + "message": "3 tests passed", + "title": "100% Passed", + }, + Object { + "message": "3 of 3 tests failed", + "title": "100% Failed", + }, +] +`; diff --git a/packages/jest-cli/src/__tests__/notify_reporter.test.js b/packages/jest-cli/src/__tests__/notify_reporter.test.js new file mode 100644 index 000000000000..442815a197b4 --- /dev/null +++ b/packages/jest-cli/src/__tests__/notify_reporter.test.js @@ -0,0 +1,119 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +'use strict'; + +import TestScheduler from '../test_scheduler'; +import NotifyReporter from '../reporters/notify_reporter'; +import type {TestSchedulerContext} from '../test_scheduler'; +import type {AggregatedResult} from '../../../../types/TestResult'; + +jest.mock('../reporters/default_reporter'); +jest.mock('node-notifier', () => ({ + notify: jest.fn(), +})); + +const initialContext: TestSchedulerContext = { + firstRun: true, + previousSuccess: false, +}; + +const aggregatedResultsSuccess: AggregatedResult = { + numFailedTestSuites: 0, + numFailedTests: 0, + numPassedTestSuites: 1, + numPassedTests: 3, + numRuntimeErrorTestSuites: 0, + numTotalTestSuites: 1, + numTotalTests: 3, + success: true, +}; + +const aggregatedResultsFailure: AggregatedResult = { + numFailedTestSuites: 1, + numFailedTests: 3, + numPassedTestSuites: 0, + numPassedTests: 9, + numRuntimeErrorTestSuites: 0, + numTotalTestSuites: 1, + numTotalTests: 3, + success: false, +}; + +// Simulated sequence of events for NotifyReporter +const notifyEvents = [ + aggregatedResultsSuccess, + aggregatedResultsFailure, + aggregatedResultsSuccess, + aggregatedResultsSuccess, + aggregatedResultsFailure, + aggregatedResultsFailure, +]; + +test('.addReporter() .removeReporter()', () => { + const scheduler = new TestScheduler( + {}, + {}, + Object.assign({}, initialContext), + ); + const reporter = new NotifyReporter(); + scheduler.addReporter(reporter); + expect(scheduler._dispatcher._reporters).toContain(reporter); + scheduler.removeReporter(NotifyReporter); + expect(scheduler._dispatcher._reporters).not.toContain(reporter); +}); + +const testModes = (notifyMode: string, arl: Array) => { + const notify = require('node-notifier'); + + let previousContext = initialContext; + arl.forEach((ar, i) => { + const newContext = Object.assign(previousContext, { + firstRun: i === 0, + previousSuccess: previousContext.previousSuccess, + }); + const reporter = new NotifyReporter( + {notify: true, notifyMode}, + {}, + newContext, + ); + previousContext = newContext; + reporter.onRunComplete(new Set(), ar); + }); + + expect( + notify.notify.mock.calls.map(([{message, title}]) => ({ + message: message.replace('\u26D4\uFE0F ', '').replace('\u2705 ', ''), + title, + })), + ).toMatchSnapshot(); +}; + +test('test always', () => { + testModes('always', notifyEvents); +}); + +test('test success', () => { + testModes('success', notifyEvents); +}); + +test('test change', () => { + testModes('change', notifyEvents); +}); + +test('test success-change', () => { + testModes('success-change', notifyEvents); +}); + +test('test failure-change', () => { + testModes('failure-change', notifyEvents); +}); + +afterEach(() => { + jest.clearAllMocks(); +}); diff --git a/packages/jest-cli/src/cli/args.js b/packages/jest-cli/src/cli/args.js index 7f89eca93e26..a3eda097a6c3 100644 --- a/packages/jest-cli/src/cli/args.js +++ b/packages/jest-cli/src/cli/args.js @@ -363,6 +363,11 @@ export const options = { description: 'Activates notifications for test results.', type: 'boolean', }, + notifyMode: { + default: 'always', + description: 'Specifies when notifications will appear for test results.', + type: 'string', + }, onlyChanged: { alias: 'o', default: undefined, diff --git a/packages/jest-cli/src/reporters/notify_reporter.js b/packages/jest-cli/src/reporters/notify_reporter.js index 055668039202..6603c48c31a2 100644 --- a/packages/jest-cli/src/reporters/notify_reporter.js +++ b/packages/jest-cli/src/reporters/notify_reporter.js @@ -16,6 +16,7 @@ import path from 'path'; import util from 'util'; import notifier from 'node-notifier'; import BaseReporter from './base_reporter'; +import type {TestSchedulerContext} from '../test_scheduler'; const isDarwin = process.platform === 'darwin'; @@ -24,21 +25,33 @@ const icon = path.resolve(__dirname, '../assets/jest_logo.png'); export default class NotifyReporter extends BaseReporter { _startRun: (globalConfig: GlobalConfig) => *; _globalConfig: GlobalConfig; - + _context: TestSchedulerContext; constructor( globalConfig: GlobalConfig, startRun: (globalConfig: GlobalConfig) => *, + context: TestSchedulerContext, ) { super(); this._globalConfig = globalConfig; this._startRun = startRun; + this._context = context; } onRunComplete(contexts: Set, result: AggregatedResult): void { const success = result.numFailedTests === 0 && result.numRuntimeErrorTestSuites === 0; - if (success) { + const notifyMode = this._globalConfig.notifyMode; + const statusChanged = + this._context.previousSuccess !== success || this._context.firstRun; + if ( + success && + (notifyMode === 'always' || + notifyMode === 'success' || + notifyMode === 'success-change' || + (notifyMode === 'change' && statusChanged) || + (notifyMode === 'failure-change' && statusChanged)) + ) { const title = util.format('%d%% Passed', 100); const message = util.format( (isDarwin ? '\u2705 ' : '') + '%d tests passed', @@ -46,7 +59,14 @@ export default class NotifyReporter extends BaseReporter { ); notifier.notify({icon, message, title}); - } else { + } else if ( + !success && + (notifyMode === 'always' || + notifyMode === 'failure' || + notifyMode === 'failure-change' || + (notifyMode === 'change' && statusChanged) || + (notifyMode === 'success-change' && statusChanged)) + ) { const failed = result.numFailedTests / result.numTotalTests; const title = util.format( @@ -83,5 +103,7 @@ export default class NotifyReporter extends BaseReporter { }, ); } + this._context.previousSuccess = success; + this._context.firstRun = false; } } diff --git a/packages/jest-cli/src/run_jest.js b/packages/jest-cli/src/run_jest.js index 9ef2fc74ff7b..efc349464768 100644 --- a/packages/jest-cli/src/run_jest.js +++ b/packages/jest-cli/src/run_jest.js @@ -88,6 +88,11 @@ const processResults = (runResults, options) => { return options.onComplete && options.onComplete(runResults); }; +const testSchedulerContext = { + firstRun: true, + previousSuccess: true, +}; + export default (async function runJest({ contexts, globalConfig, @@ -199,9 +204,13 @@ export default (async function runJest({ // $FlowFixMe await require(globalConfig.globalSetup)(); } - const results = await new TestScheduler(globalConfig, { - startRun, - }).scheduleTests(allTests, testWatcher); + const results = await new TestScheduler( + globalConfig, + { + startRun, + }, + testSchedulerContext, + ).scheduleTests(allTests, testWatcher); sequencer.cacheResults(allTests, results); diff --git a/packages/jest-cli/src/test_scheduler.js b/packages/jest-cli/src/test_scheduler.js index 633a0ad2c31f..ecdcd06a281c 100644 --- a/packages/jest-cli/src/test_scheduler.js +++ b/packages/jest-cli/src/test_scheduler.js @@ -39,16 +39,25 @@ TestRunner; export type TestSchedulerOptions = {| startRun: (globalConfig: GlobalConfig) => *, |}; - +export type TestSchedulerContext = {| + firstRun: boolean, + previousSuccess: boolean, +|}; export default class TestScheduler { _dispatcher: ReporterDispatcher; _globalConfig: GlobalConfig; _options: TestSchedulerOptions; + _context: TestSchedulerContext; - constructor(globalConfig: GlobalConfig, options: TestSchedulerOptions) { + constructor( + globalConfig: GlobalConfig, + options: TestSchedulerOptions, + context: TestSchedulerContext, + ) { this._dispatcher = new ReporterDispatcher(); this._globalConfig = globalConfig; this._options = options; + this._context = context; this._setupReporters(); } @@ -257,7 +266,11 @@ export default class TestScheduler { if (notify) { this.addReporter( - new NotifyReporter(this._globalConfig, this._options.startRun), + new NotifyReporter( + this._globalConfig, + this._options.startRun, + this._context, + ), ); } diff --git a/packages/jest-config/src/defaults.js b/packages/jest-config/src/defaults.js index bdb8362dfe83..c839cc2af09a 100644 --- a/packages/jest-config/src/defaults.js +++ b/packages/jest-config/src/defaults.js @@ -53,6 +53,7 @@ export default ({ modulePathIgnorePatterns: [], noStackTrace: false, notify: false, + notifyMode: 'always', preset: null, resetMocks: false, resetModules: false, diff --git a/packages/jest-config/src/index.js b/packages/jest-config/src/index.js index ccfa0e2db4bf..57a647fdb0b7 100644 --- a/packages/jest-config/src/index.js +++ b/packages/jest-config/src/index.js @@ -118,6 +118,7 @@ const getConfigs = ( noStackTrace: options.noStackTrace, nonFlagArgs: options.nonFlagArgs, notify: options.notify, + notifyMode: options.notifyMode, onlyChanged: options.onlyChanged, onlyFailures: options.onlyFailures, outputFile: options.outputFile, diff --git a/packages/jest-config/src/normalize.js b/packages/jest-config/src/normalize.js index 91a88ed1dc3f..b5c04c0a8acf 100644 --- a/packages/jest-config/src/normalize.js +++ b/packages/jest-config/src/normalize.js @@ -482,6 +482,7 @@ export default function normalize(options: InitialOptions, argv: Argv) { case 'name': case 'noStackTrace': case 'notify': + case 'notifyMode': case 'onlyChanged': case 'outputFile': case 'passWithNoTests': diff --git a/packages/jest-config/src/valid_config.js b/packages/jest-config/src/valid_config.js index c2d4159c3b06..d866c8a9087f 100644 --- a/packages/jest-config/src/valid_config.js +++ b/packages/jest-config/src/valid_config.js @@ -61,6 +61,7 @@ export default ({ name: 'string', noStackTrace: false, notify: false, + notifyMode: 'always', onlyChanged: false, preset: 'react-native', projects: ['project-a', 'project-b/'], diff --git a/packages/jest-validate/src/__tests__/fixtures/jest_config.js b/packages/jest-validate/src/__tests__/fixtures/jest_config.js index 5657f5466b5e..edbea621e15e 100644 --- a/packages/jest-validate/src/__tests__/fixtures/jest_config.js +++ b/packages/jest-validate/src/__tests__/fixtures/jest_config.js @@ -39,6 +39,7 @@ const defaultConfig = { modulePathIgnorePatterns: [], noStackTrace: false, notify: false, + notifyMode: 'always', preset: null, resetMocks: false, resetModules: false, @@ -96,6 +97,7 @@ const validConfig = { name: 'string', noStackTrace: false, notify: false, + notifyMode: 'always', preset: 'react-native', resetMocks: false, resetModules: false, diff --git a/test_utils.js b/test_utils.js index 5f9645826635..e4f8476c4464 100644 --- a/test_utils.js +++ b/test_utils.js @@ -38,6 +38,7 @@ const DEFAULT_GLOBAL_CONFIG: GlobalConfig = { noStackTrace: false, nonFlagArgs: [], notify: false, + notifyMode: 'always', onlyChanged: false, onlyFailures: false, outputFile: null, diff --git a/types/Argv.js b/types/Argv.js index a46c5aa14bd2..0293067b8ef8 100644 --- a/types/Argv.js +++ b/types/Argv.js @@ -54,6 +54,7 @@ export type Argv = {| noSCM: boolean, noStackTrace: boolean, notify: boolean, + notifyMode: string, onlyChanged: boolean, outputFile: string, preset: ?string, diff --git a/types/Config.js b/types/Config.js index 48cbb287d172..d38bc2bbb609 100644 --- a/types/Config.js +++ b/types/Config.js @@ -45,6 +45,7 @@ export type DefaultOptions = {| modulePathIgnorePatterns: Array, noStackTrace: boolean, notify: boolean, + notifyMode: string, preset: ?string, resetMocks: boolean, resetModules: boolean, @@ -111,6 +112,7 @@ export type InitialOptions = { name?: string, noStackTrace?: boolean, notify?: boolean, + notifyMode?: string, onlyChanged?: boolean, outputFile?: Path, passWithNoTests?: boolean, @@ -187,6 +189,7 @@ export type GlobalConfig = {| nonFlagArgs: Array, noSCM: ?boolean, notify: boolean, + notifyMode: string, outputFile: ?Path, onlyChanged: boolean, onlyFailures: boolean,