diff --git a/CHANGELOG.md b/CHANGELOG.md index 016da2e36835..be3d9a539a64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,7 +33,9 @@ ([#5605](https://github.com/facebook/jest/pull/5605)) * `[jest-cli]` Add `isSerial` property that runners can expose to specify that they can not run in parallel - [#5706](https://github.com/facebook/jest/pull/5706) + ([#5706](https://github.com/facebook/jest/pull/5706)) +* `[jest-cli]` Interactive Snapshot Mode improvements + ([#5864](https://github.com/facebook/jest/pull/5864)) ### Fixes diff --git a/docs/SnapshotTesting.md b/docs/SnapshotTesting.md index 1f01c626cc90..ca285458766b 100644 --- a/docs/SnapshotTesting.md +++ b/docs/SnapshotTesting.md @@ -128,13 +128,18 @@ Failed snapshots can also be updated interactively in watch mode: ![](/jest/img/content/interactiveSnapshot.png) Once you enter Interactive Snapshot Mode, Jest will step you through the failed -snapshots one test suite at a time and give you the opportunity to review the -failed output. +snapshots one test at a time and give you the opportunity to review the failed +output. From here you can choose to update that snapshot or skip to the next: ![](/jest/img/content/interactiveSnapshotUpdate.gif) +Once you're finished, Jest will give you a summary before returning back to +watch mode: + +![](/jest/img/content/interactiveSnapshotDone.png) + ### Tests Should Be Deterministic Your tests should be deterministic. That is, running the same tests multiple diff --git a/packages/jest-cli/src/__tests__/__snapshots__/snapshot_interactive_mode.test.js.snap b/packages/jest-cli/src/__tests__/__snapshots__/snapshot_interactive_mode.test.js.snap index fd47bf1ef31f..e842ea64375f 100644 --- a/packages/jest-cli/src/__tests__/__snapshots__/snapshot_interactive_mode.test.js.snap +++ b/packages/jest-cli/src/__tests__/__snapshots__/snapshot_interactive_mode.test.js.snap @@ -1,36 +1,308 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`SnapshotInteractiveMode updateWithResults last test success, trigger end of interactive mode 1`] = `"TEST RESULTS CONTENTS"`; +exports[`SnapshotInteractiveMode skip 1 test, then quit 1`] = ` +"[MOCK - cursorUp] +[MOCK - eraseDown] + +Interactive Snapshot Progress + › 1 snapshot remaining + +Watch Usage + › Press u to update failing snapshots for this test. + › Press s to skip the current test. + › Press q to quit Interactive Snapshot Mode. + › Press Enter to trigger a test run. +" +`; + +exports[`SnapshotInteractiveMode skip 1 test, then quit 2`] = ` +"[MOCK - eraseDown] + +Interactive Snapshot Result + › 1 snapshot reviewed, 1 snapshot skipped + +Watch Usage + › Press r to restart Interactive Snapshot Mode. + › Press q to quit Interactive Snapshot Mode. +" +`; + +exports[`SnapshotInteractiveMode skip 1 test, then restart 1`] = ` +"[MOCK - cursorUp] +[MOCK - eraseDown] + +Interactive Snapshot Progress + › 1 snapshot remaining + +Watch Usage + › Press u to update failing snapshots for this test. + › Press s to skip the current test. + › Press q to quit Interactive Snapshot Mode. + › Press Enter to trigger a test run. +" +`; + +exports[`SnapshotInteractiveMode skip 1 test, then restart 2`] = ` +"[MOCK - eraseDown] + +Interactive Snapshot Result + › 1 snapshot reviewed, 1 snapshot skipped + +Watch Usage + › Press r to restart Interactive Snapshot Mode. + › Press q to quit Interactive Snapshot Mode. +" +`; + +exports[`SnapshotInteractiveMode skip 1 test, then restart 3`] = ` +"[MOCK - cursorUp] +[MOCK - eraseDown] + +Interactive Snapshot Progress + › 1 snapshot remaining + +Watch Usage + › Press u to update failing snapshots for this test. + › Press s to skip the current test. + › Press q to quit Interactive Snapshot Mode. + › Press Enter to trigger a test run. +" +`; + +exports[`SnapshotInteractiveMode skip 1 test, update 1 test, then finish and restart 1`] = ` +"[MOCK - cursorUp] +[MOCK - eraseDown] + +Interactive Snapshot Progress + › 2 snapshots remaining + +Watch Usage + › Press u to update failing snapshots for this test. + › Press s to skip the current test. + › Press q to quit Interactive Snapshot Mode. + › Press Enter to trigger a test run. +" +`; + +exports[`SnapshotInteractiveMode skip 1 test, update 1 test, then finish and restart 2`] = ` +"[MOCK - cursorUp] +[MOCK - eraseDown] + +Interactive Snapshot Progress + › 1 snapshot remaining, 1 snapshot skipped + +Watch Usage + › Press u to update failing snapshots for this test. + › Press s to skip the current test. + › Press q to quit Interactive Snapshot Mode. + › Press Enter to trigger a test run. +" +`; + +exports[`SnapshotInteractiveMode skip 1 test, update 1 test, then finish and restart 3`] = ` +"[MOCK - eraseDown] + +Interactive Snapshot Result + › 2 snapshots reviewed, 1 snapshot updated, 1 snapshot skipped + +Watch Usage + › Press r to restart Interactive Snapshot Mode. + › Press q to quit Interactive Snapshot Mode. +" +`; + +exports[`SnapshotInteractiveMode skip 1 test, update 1 test, then finish and restart 4`] = ` +"[MOCK - cursorUp] +[MOCK - eraseDown] + +Interactive Snapshot Progress + › 1 snapshot remaining + +Watch Usage + › Press u to update failing snapshots for this test. + › Press s to skip the current test. + › Press q to quit Interactive Snapshot Mode. + › Press Enter to trigger a test run. +" +`; + +exports[`SnapshotInteractiveMode skip 2 tests, then finish and restart 1`] = ` +"[MOCK - cursorUp] +[MOCK - eraseDown] + +Interactive Snapshot Progress + › 2 snapshots remaining + +Watch Usage + › Press u to update failing snapshots for this test. + › Press s to skip the current test. + › Press q to quit Interactive Snapshot Mode. + › Press Enter to trigger a test run. +" +`; + +exports[`SnapshotInteractiveMode skip 2 tests, then finish and restart 2`] = ` +"[MOCK - cursorUp] +[MOCK - eraseDown] + +Interactive Snapshot Progress + › 1 snapshot remaining, 1 snapshot skipped + +Watch Usage + › Press u to update failing snapshots for this test. + › Press s to skip the current test. + › Press q to quit Interactive Snapshot Mode. + › Press Enter to trigger a test run. +" +`; + +exports[`SnapshotInteractiveMode skip 2 tests, then finish and restart 3`] = ` +"[MOCK - eraseDown] + +Interactive Snapshot Result + › 2 snapshots reviewed, 2 snapshots skipped -exports[`SnapshotInteractiveMode updateWithResults overlay handle progress UI 1`] = ` -"TEST RESULTS CONTENTS -[MOCK - cursorUp] +Watch Usage + › Press r to restart Interactive Snapshot Mode. + › Press q to quit Interactive Snapshot Mode. +" +`; + +exports[`SnapshotInteractiveMode skip 2 tests, then finish and restart 4`] = ` +"[MOCK - cursorUp] [MOCK - eraseDown] Interactive Snapshot Progress - › 2 suites failed, 1 suite passed + › 2 snapshots remaining Watch Usage › Press u to update failing snapshots for this test. - › Press s to skip the current test suite. - › Press q to quit Interactive Snapshot Update Mode. + › Press s to skip the current test. + › Press q to quit Interactive Snapshot Mode. › Press Enter to trigger a test run. " `; -exports[`SnapshotInteractiveMode updateWithResults with a test failure simply update UI 1`] = ` -"TEST RESULTS CONTENTS -[MOCK - cursorUp] +exports[`SnapshotInteractiveMode update 1 test, skip 1 test, then finish and restart 1`] = ` +"[MOCK - cursorUp] [MOCK - eraseDown] Interactive Snapshot Progress - › 1 suite failed + › 2 snapshots remaining Watch Usage › Press u to update failing snapshots for this test. - › Press q to quit Interactive Snapshot Update Mode. + › Press s to skip the current test. + › Press q to quit Interactive Snapshot Mode. › Press Enter to trigger a test run. " `; -exports[`SnapshotInteractiveMode updateWithResults with a test success, call the next test 1`] = `"TEST RESULTS CONTENTS"`; +exports[`SnapshotInteractiveMode update 1 test, skip 1 test, then finish and restart 2`] = ` +"[MOCK - cursorUp] +[MOCK - eraseDown] + +Interactive Snapshot Progress + › 1 snapshot remaining, 1 snapshot updated + +Watch Usage + › Press u to update failing snapshots for this test. + › Press s to skip the current test. + › Press q to quit Interactive Snapshot Mode. + › Press Enter to trigger a test run. +" +`; + +exports[`SnapshotInteractiveMode update 1 test, skip 1 test, then finish and restart 3`] = ` +"[MOCK - eraseDown] + +Interactive Snapshot Result + › 2 snapshots reviewed, 1 snapshot updated, 1 snapshot skipped + +Watch Usage + › Press r to restart Interactive Snapshot Mode. + › Press q to quit Interactive Snapshot Mode. +" +`; + +exports[`SnapshotInteractiveMode update 1 test, skip 1 test, then finish and restart 4`] = ` +"[MOCK - cursorUp] +[MOCK - eraseDown] + +Interactive Snapshot Progress + › 1 snapshot remaining + +Watch Usage + › Press u to update failing snapshots for this test. + › Press s to skip the current test. + › Press q to quit Interactive Snapshot Mode. + › Press Enter to trigger a test run. +" +`; + +exports[`SnapshotInteractiveMode update 1 test, then finish and return 1`] = ` +"[MOCK - cursorUp] +[MOCK - eraseDown] + +Interactive Snapshot Progress + › 1 snapshot remaining + +Watch Usage + › Press u to update failing snapshots for this test. + › Press s to skip the current test. + › Press q to quit Interactive Snapshot Mode. + › Press Enter to trigger a test run. +" +`; + +exports[`SnapshotInteractiveMode update 1 test, then finish and return 2`] = ` +"[MOCK - eraseDown] + +Interactive Snapshot Result + › 1 snapshot reviewed, 1 snapshot updated + +Watch Usage + › Press Enter to return to watch mode. +" +`; + +exports[`SnapshotInteractiveMode update 2 tests, then finish and return 1`] = ` +"[MOCK - cursorUp] +[MOCK - eraseDown] + +Interactive Snapshot Progress + › 2 snapshots remaining + +Watch Usage + › Press u to update failing snapshots for this test. + › Press s to skip the current test. + › Press q to quit Interactive Snapshot Mode. + › Press Enter to trigger a test run. +" +`; + +exports[`SnapshotInteractiveMode update 2 tests, then finish and return 2`] = ` +"[MOCK - cursorUp] +[MOCK - eraseDown] + +Interactive Snapshot Progress + › 1 snapshot remaining, 1 snapshot updated + +Watch Usage + › Press u to update failing snapshots for this test. + › Press s to skip the current test. + › Press q to quit Interactive Snapshot Mode. + › Press Enter to trigger a test run. +" +`; + +exports[`SnapshotInteractiveMode update 2 tests, then finish and return 3`] = ` +"[MOCK - eraseDown] + +Interactive Snapshot Result + › 2 snapshots reviewed, 2 snapshots updated + +Watch Usage + › Press Enter to return to watch mode. +" +`; diff --git a/packages/jest-cli/src/__tests__/snapshot_interactive_mode.test.js b/packages/jest-cli/src/__tests__/snapshot_interactive_mode.test.js index 638bb8c3e7a8..26fd83792a9f 100644 --- a/packages/jest-cli/src/__tests__/snapshot_interactive_mode.test.js +++ b/packages/jest-cli/src/__tests__/snapshot_interactive_mode.test.js @@ -17,6 +17,7 @@ jest.mock('../lib/terminal_utils', () => ({ })); jest.mock('ansi-escapes', () => ({ + clearScreen: '[MOCK - eraseDown]', cursorRestorePosition: '[MOCK - cursorRestorePosition]', cursorSavePosition: '[MOCK - cursorSavePosition]', cursorScrollDown: '[MOCK - cursorScrollDown]', @@ -34,10 +35,13 @@ jest.doMock('chalk', () => describe('SnapshotInteractiveMode', () => { let pipe; let instance; - + let mockCallback; beforeEach(() => { pipe = {write: jest.fn()}; instance = new SnapshotInteractiveMode(pipe); + mockCallback = jest.fn(() => { + instance.updateWithResults({snapshot: {failure: true}}); + }); }); test('is inactive at construction', () => { @@ -45,98 +49,293 @@ describe('SnapshotInteractiveMode', () => { }); test('call to run process the first file', () => { - const mockCallback = jest.fn(); - instance.run(['first.js', 'second.js'], mockCallback); + const assertions = [ + {path: 'first.js', title: 'test one'}, + {path: 'second.js', title: 'test two'}, + ]; + instance.run(assertions, mockCallback); expect(instance.isActive()).toBeTruthy(); - expect(mockCallback).toBeCalledWith('first.js', false); + expect(mockCallback).toBeCalledWith(assertions[0], false); }); test('call to abort', () => { - const mockCallback = jest.fn(); - instance.run(['first.js', 'second.js'], mockCallback); + const assertions = [ + {path: 'first.js', title: 'test one'}, + {path: 'second.js', title: 'test two'}, + ]; + instance.run(assertions, mockCallback); expect(instance.isActive()).toBeTruthy(); instance.abort(); expect(instance.isActive()).toBeFalsy(); - expect(mockCallback).toBeCalledWith('', false); + expect(instance.getSkippedNum()).toBe(0); + expect(mockCallback).toBeCalledWith(null, false); }); - describe('key press handler', () => { - test('call to skip trigger a processing of next file', () => { - const mockCallback = jest.fn(); - instance.run(['first.js', 'second.js'], mockCallback); - expect(mockCallback.mock.calls[0]).toEqual(['first.js', false]); - instance.put(KEYS.S); - expect(mockCallback.mock.calls[1]).toEqual(['second.js', false]); - instance.put(KEYS.S); - expect(mockCallback.mock.calls[2]).toEqual(['first.js', false]); - }); - - test('call to skip works with 1 file', () => { - const mockCallback = jest.fn(); - instance.run(['first.js'], mockCallback); - expect(mockCallback.mock.calls[0]).toEqual(['first.js', false]); - instance.put(KEYS.S); - expect(mockCallback.mock.calls[1]).toEqual(['first.js', false]); - }); - - test('press U trigger a snapshot update call', () => { - const mockCallback = jest.fn(); - instance.run(['first.js'], mockCallback); - expect(mockCallback.mock.calls[0]).toEqual(['first.js', false]); - instance.put(KEYS.U); - expect(mockCallback.mock.calls[1]).toEqual(['first.js', true]); - }); - - test('press Q or ESC triggers an abort', () => { - instance.abort = jest.fn(); - instance.put(KEYS.Q); - instance.put(KEYS.ESCAPE); - expect(instance.abort).toHaveBeenCalledTimes(2); - }); - - test('press ENTER trigger a run', () => { - const mockCallback = jest.fn(); - instance.run(['first.js'], mockCallback); - instance.put(KEYS.ENTER); - expect(mockCallback).toHaveBeenCalledTimes(2); - expect(mockCallback).toHaveBeenCalledWith('first.js', false); - }); + + test('call to reset', () => { + const assertions = [ + {path: 'first.js', title: 'test one'}, + {path: 'second.js', title: 'test two'}, + ]; + instance.run(assertions, mockCallback); + expect(instance.isActive()).toBeTruthy(); + instance.restart(); + expect(instance.isActive()).toBeTruthy(); + expect(instance.getSkippedNum()).toBe(0); + expect(mockCallback).toBeCalledWith(assertions[0], false); + }); + + test('press Q or ESC triggers an abort', () => { + instance.abort = jest.fn(); + instance.put(KEYS.Q); + instance.put(KEYS.ESCAPE); + expect(instance.abort).toHaveBeenCalledTimes(2); }); - describe('updateWithResults', () => { - test('with a test failure simply update UI', () => { - const mockCallback = jest.fn(); - instance.run(['first.js'], mockCallback); - pipe.write('TEST RESULTS CONTENTS'); + + test('press ENTER trigger a run', () => { + const assertions = [{path: 'first.js', title: 'test one'}]; + instance.run(assertions, mockCallback); + instance.put(KEYS.ENTER); + expect(mockCallback).toHaveBeenCalledTimes(2); + expect(mockCallback).toHaveBeenCalledWith(assertions[0], false); + }); + + test('skip 1 test, then restart', () => { + const assertions = [{path: 'first.js', title: 'test one'}]; + + instance.run(assertions, mockCallback); + expect(mockCallback).nthCalledWith(1, assertions[0], false); + expect(pipe.write.mock.calls.join('\n')).toMatchSnapshot(); + pipe.write.mockClear(); + + instance.put(KEYS.S); + expect(mockCallback).toHaveBeenCalledTimes(1); + expect(pipe.write.mock.calls.join('\n')).toMatchSnapshot(); + pipe.write.mockClear(); + + instance.put(KEYS.R); + expect(instance.getSkippedNum()).toBe(0); + expect(mockCallback).nthCalledWith(2, assertions[0], false); + expect(mockCallback).toHaveBeenCalledTimes(2); + expect(pipe.write.mock.calls.join('\n')).toMatchSnapshot(); + }); + + test('skip 1 test, then quit', () => { + const assertions = [{path: 'first.js', title: 'test one'}]; + + instance.run(assertions, mockCallback); + expect(mockCallback).nthCalledWith(1, assertions[0], false); + expect(pipe.write.mock.calls.join('\n')).toMatchSnapshot(); + pipe.write.mockClear(); + + instance.put(KEYS.S); + expect(mockCallback).toHaveBeenCalledTimes(1); + expect(pipe.write.mock.calls.join('\n')).toMatchSnapshot(); + pipe.write.mockClear(); + + instance.put(KEYS.Q); + expect(instance.getSkippedNum()).toBe(0); + expect(mockCallback).nthCalledWith(2, null, false); + expect(mockCallback).toHaveBeenCalledTimes(2); + }); + + test('update 1 test, then finish and return', () => { + const mockCallback = jest.fn(); + mockCallback.mockImplementationOnce(() => { + instance.updateWithResults({snapshot: {failure: true}}); + }); + mockCallback.mockImplementationOnce(() => { + instance.updateWithResults({snapshot: {failure: false}}); + }); + mockCallback.mockImplementationOnce(() => { instance.updateWithResults({snapshot: {failure: true}}); - expect(pipe.write.mock.calls.join('\n')).toMatchSnapshot(); - expect(mockCallback).toHaveBeenCalledTimes(1); }); - test('with a test success, call the next test', () => { - const mockCallback = jest.fn(); - instance.run(['first.js', 'second.js'], mockCallback); - pipe.write('TEST RESULTS CONTENTS'); + const assertions = [{path: 'first.js', title: 'test one'}]; + + instance.run(assertions, mockCallback); + expect(mockCallback).nthCalledWith(1, assertions[0], false); + expect(pipe.write.mock.calls.join('\n')).toMatchSnapshot(); + pipe.write.mockClear(); + + instance.put(KEYS.U); + expect(mockCallback).nthCalledWith(2, assertions[0], true); + expect(mockCallback).toHaveBeenCalledTimes(2); + expect(pipe.write.mock.calls.join('\n')).toMatchSnapshot(); + + instance.put(KEYS.ENTER); + expect(instance.isActive()).toBe(false); + expect(mockCallback).nthCalledWith(3, null, false); + }); + + test('skip 2 tests, then finish and restart', () => { + const assertions = [ + {path: 'first.js', title: 'test one'}, + {path: 'first.js', title: 'test two'}, + ]; + instance.run(assertions, mockCallback); + expect(mockCallback).nthCalledWith(1, assertions[0], false); + expect(pipe.write.mock.calls.join('\n')).toMatchSnapshot(); + pipe.write.mockClear(); + + instance.put(KEYS.S); + expect(mockCallback).nthCalledWith(2, assertions[1], false); + expect(mockCallback).toHaveBeenCalledTimes(2); + expect(pipe.write.mock.calls.join('\n')).toMatchSnapshot(); + pipe.write.mockClear(); + + instance.put(KEYS.S); + expect(mockCallback).toHaveBeenCalledTimes(2); + expect(pipe.write.mock.calls.join('\n')).toMatchSnapshot(); + pipe.write.mockClear(); + + instance.put(KEYS.R); + expect(instance.getSkippedNum()).toBe(0); + expect(mockCallback).nthCalledWith(3, assertions[0], false); + expect(mockCallback).toHaveBeenCalledTimes(3); + expect(pipe.write.mock.calls.join('\n')).toMatchSnapshot(); + }); + + test('update 2 tests, then finish and return', () => { + const mockCallback = jest.fn(); + mockCallback.mockImplementationOnce(() => { + instance.updateWithResults({snapshot: {failure: true}}); + }); + mockCallback.mockImplementationOnce(() => { + instance.updateWithResults({snapshot: {failure: false}}); + }); + mockCallback.mockImplementationOnce(() => { + instance.updateWithResults({snapshot: {failure: true}}); + }); + mockCallback.mockImplementationOnce(() => { instance.updateWithResults({snapshot: {failure: false}}); - expect(pipe.write.mock.calls.join('\n')).toMatchSnapshot(); - expect(mockCallback.mock.calls[1]).toEqual(['second.js', false]); }); + mockCallback.mockImplementationOnce(() => { + instance.updateWithResults({snapshot: {failure: true}}); + }); + + const assertions = [ + {path: 'first.js', title: 'test one'}, + {path: 'first.js', title: 'test two'}, + ]; + + instance.run(assertions, mockCallback); + expect(mockCallback).nthCalledWith(1, assertions[0], false); + expect(mockCallback).toHaveBeenCalledTimes(1); + expect(pipe.write.mock.calls.join('\n')).toMatchSnapshot(); + pipe.write.mockClear(); + + instance.put(KEYS.U); + expect(mockCallback).nthCalledWith(2, assertions[0], true); + expect(mockCallback).nthCalledWith(3, assertions[1], false); + expect(mockCallback).toHaveBeenCalledTimes(3); + expect(pipe.write.mock.calls.join('\n')).toMatchSnapshot(); + pipe.write.mockClear(); + + instance.put(KEYS.U); + expect(mockCallback).nthCalledWith(4, assertions[1], true); + expect(mockCallback).toHaveBeenCalledTimes(4); + expect(pipe.write.mock.calls.join('\n')).toMatchSnapshot(); + pipe.write.mockClear(); - test('overlay handle progress UI', () => { - const mockCallback = jest.fn(); - instance.run(['first.js', 'second.js', 'third.js'], mockCallback); - pipe.write('TEST RESULTS CONTENTS'); + instance.put(KEYS.ENTER); + expect(instance.isActive()).toBe(false); + expect(mockCallback).nthCalledWith(5, null, false); + expect(mockCallback).toHaveBeenCalledTimes(5); + }); + + test('update 1 test, skip 1 test, then finish and restart', () => { + const mockCallback = jest.fn(); + mockCallback.mockImplementationOnce(() => { + instance.updateWithResults({snapshot: {failure: true}}); + }); + mockCallback.mockImplementationOnce(() => { instance.updateWithResults({snapshot: {failure: false}}); + }); + mockCallback.mockImplementationOnce(() => { + instance.updateWithResults({snapshot: {failure: true}}); + }); + mockCallback.mockImplementationOnce(() => { + instance.updateWithResults({snapshot: {failure: true}}); + }); + mockCallback.mockImplementationOnce(() => { instance.updateWithResults({snapshot: {failure: true}}); - expect(pipe.write.mock.calls.join('\n')).toMatchSnapshot(); }); - test('last test success, trigger end of interactive mode', () => { - const mockCallback = jest.fn(); - instance.abort = jest.fn(); - instance.run(['first.js'], mockCallback); - pipe.write('TEST RESULTS CONTENTS'); + const assertions = [ + {path: 'first.js', title: 'test one'}, + {path: 'first.js', title: 'test two'}, + ]; + + instance.run(assertions, mockCallback); + expect(mockCallback).nthCalledWith(1, assertions[0], false); + expect(mockCallback).toHaveBeenCalledTimes(1); + expect(pipe.write.mock.calls.join('\n')).toMatchSnapshot(); + pipe.write.mockClear(); + + instance.put(KEYS.U); + expect(mockCallback).nthCalledWith(2, assertions[0], true); + expect(mockCallback).nthCalledWith(3, assertions[1], false); + expect(mockCallback).toHaveBeenCalledTimes(3); + expect(pipe.write.mock.calls.join('\n')).toMatchSnapshot(); + pipe.write.mockClear(); + + instance.put(KEYS.S); + expect(mockCallback).toHaveBeenCalledTimes(3); + expect(pipe.write.mock.calls.join('\n')).toMatchSnapshot(); + pipe.write.mockClear(); + + instance.put(KEYS.R); + expect(instance.getSkippedNum()).toBe(0); + expect(mockCallback).nthCalledWith(4, assertions[1], false); + expect(mockCallback).toHaveBeenCalledTimes(4); + expect(pipe.write.mock.calls.join('\n')).toMatchSnapshot(); + }); + + test('skip 1 test, update 1 test, then finish and restart', () => { + const mockCallback = jest.fn(); + mockCallback.mockImplementationOnce(() => { + instance.updateWithResults({snapshot: {failure: true}}); + }); + mockCallback.mockImplementationOnce(() => { + instance.updateWithResults({snapshot: {failure: true}}); + }); + mockCallback.mockImplementationOnce(() => { instance.updateWithResults({snapshot: {failure: false}}); - expect(pipe.write.mock.calls.join('\n')).toMatchSnapshot(); - expect(instance.abort).toHaveBeenCalled(); }); + mockCallback.mockImplementationOnce(() => { + instance.updateWithResults({snapshot: {failure: true}}); + }); + mockCallback.mockImplementationOnce(() => { + instance.updateWithResults({snapshot: {failure: true}}); + }); + + const assertions = [ + {path: 'first.js', title: 'test one'}, + {path: 'first.js', title: 'test two'}, + ]; + + instance.run(assertions, mockCallback); + expect(mockCallback).nthCalledWith(1, assertions[0], false); + expect(mockCallback).toHaveBeenCalledTimes(1); + expect(pipe.write.mock.calls.join('\n')).toMatchSnapshot(); + pipe.write.mockClear(); + + instance.put(KEYS.S); + expect(mockCallback).nthCalledWith(2, assertions[1], false); + expect(mockCallback).toHaveBeenCalledTimes(2); + expect(pipe.write.mock.calls.join('\n')).toMatchSnapshot(); + pipe.write.mockClear(); + + instance.put(KEYS.U); + expect(mockCallback).nthCalledWith(3, assertions[1], true); + expect(mockCallback).toHaveBeenCalledTimes(3); + expect(pipe.write.mock.calls.join('\n')).toMatchSnapshot(); + pipe.write.mockClear(); + + instance.put(KEYS.R); + expect(instance.getSkippedNum()).toBe(0); + expect(mockCallback).nthCalledWith(4, assertions[0], false); + expect(mockCallback).toHaveBeenCalledTimes(4); + expect(pipe.write.mock.calls.join('\n')).toMatchSnapshot(); }); }); diff --git a/packages/jest-cli/src/__tests__/watch.test.js b/packages/jest-cli/src/__tests__/watch.test.js index 1fcc111dfd7d..51ff4c97774a 100644 --- a/packages/jest-cli/src/__tests__/watch.test.js +++ b/packages/jest-cli/src/__tests__/watch.test.js @@ -245,8 +245,26 @@ describe('Watch mode flows', () => { jest.unmock('jest-util'); const util = require('jest-util'); util.isInteractive = true; - util.getFailedSnapshotTests = jest.fn(() => ['test.js']); - results = {snapshot: {failure: true}}; + results = { + numFailedTests: 1, + snapshot: { + failure: true, + }, + testPath: 'test.js', + testResults: [ + { + snapshot: { + unmatched: true, + }, + testResults: [ + { + status: 'failed', + title: 'test a', + }, + ], + }, + ], + }; const ci_watch = require('../watch').default; ci_watch( diff --git a/packages/jest-cli/src/constants.js b/packages/jest-cli/src/constants.js index b29941b351ef..0804d3922780 100644 --- a/packages/jest-cli/src/constants.js +++ b/packages/jest-cli/src/constants.js @@ -10,7 +10,7 @@ const isWindows = process.platform === 'win32'; export const CLEAR = isWindows ? '\x1B[2J\x1B[0f' : '\x1B[2J\x1B[3J\x1B[H'; - +export const ARROW = ' \u203A '; export const KEYS = { A: '61', ARROW_DOWN: '1b5b42', @@ -29,6 +29,7 @@ export const KEYS = { P: '70', Q: '71', QUESTION_MARK: '3f', + R: '72', S: '73', T: '74', U: '75', diff --git a/packages/jest-cli/src/plugins/update_snapshots_interactive.js b/packages/jest-cli/src/plugins/update_snapshots_interactive.js index 32be3ee4a114..97f995d9852d 100644 --- a/packages/jest-cli/src/plugins/update_snapshots_interactive.js +++ b/packages/jest-cli/src/plugins/update_snapshots_interactive.js @@ -8,26 +8,52 @@ */ import type {JestHookSubscriber} from '../jest_hooks'; import type {GlobalConfig} from 'types/Config'; +import type {AggregatedResult, AssertionLocation} from 'types/TestResult'; import BaseWatchPlugin from '../base_watch_plugin'; -import {getFailedSnapshotTests} from 'jest-util'; import SnapshotInteractiveMode from '../snapshot_interactive_mode'; class UpdateSnapshotInteractivePlugin extends BaseWatchPlugin { _snapshotInteractiveMode: SnapshotInteractiveMode; - _failedSnapshotTestPaths: Array<*>; + _failedSnapshotTestAssertions: Array; constructor(options: { stdin: stream$Readable | tty$ReadStream, stdout: stream$Writable | tty$WriteStream, }) { super(options); - this._failedSnapshotTestPaths = []; + this._failedSnapshotTestAssertions = []; this._snapshotInteractiveMode = new SnapshotInteractiveMode(this._stdout); } + getFailedSnapshotTestAssertions( + testResults: AggregatedResult, + ): Array { + const failedTestPaths = []; + if (testResults.numFailedTests === 0 || !testResults.testResults) { + return failedTestPaths; + } + + testResults.testResults.forEach(testResult => { + if (testResult.snapshot && testResult.snapshot.unmatched) { + testResult.testResults.forEach(result => { + if (result.status === 'failed') { + failedTestPaths.push({ + path: testResult.testFilePath, + title: result.title, + }); + } + }); + } + }); + + return failedTestPaths; + } + apply(hooks: JestHookSubscriber) { hooks.testRunComplete(results => { - this._failedSnapshotTestPaths = getFailedSnapshotTests(results); + this._failedSnapshotTestAssertions = this.getFailedSnapshotTestAssertions( + results, + ); if (this._snapshotInteractiveMode.isActive()) { this._snapshotInteractiveMode.updateWithResults(results); } @@ -41,15 +67,16 @@ class UpdateSnapshotInteractivePlugin extends BaseWatchPlugin { } run(globalConfig: GlobalConfig, updateConfigAndRun: Function): Promise { - if (this._failedSnapshotTestPaths.length) { + if (this._failedSnapshotTestAssertions.length) { return new Promise(res => { this._snapshotInteractiveMode.run( - this._failedSnapshotTestPaths, - (path: string, shouldUpdateSnapshot: boolean) => { + this._failedSnapshotTestAssertions, + (assertion: ?AssertionLocation, shouldUpdateSnapshot: boolean) => { updateConfigAndRun({ mode: 'watch', - testNamePattern: '', - testPathPattern: path, + testNamePattern: assertion ? `^${assertion.title}$` : '', + testPathPattern: assertion ? assertion.path : '', + updateSnapshot: shouldUpdateSnapshot ? 'all' : 'none', }); if (!this._snapshotInteractiveMode.isActive()) { @@ -65,8 +92,8 @@ class UpdateSnapshotInteractivePlugin extends BaseWatchPlugin { getUsageInfo(globalConfig: GlobalConfig) { if ( - this._failedSnapshotTestPaths && - this._failedSnapshotTestPaths.length > 0 + this._failedSnapshotTestAssertions && + this._failedSnapshotTestAssertions.length > 0 ) { return { key: 'i'.codePointAt(0), diff --git a/packages/jest-cli/src/snapshot_interactive_mode.js b/packages/jest-cli/src/snapshot_interactive_mode.js index fdd6f62121fc..a78336aa6d37 100644 --- a/packages/jest-cli/src/snapshot_interactive_mode.js +++ b/packages/jest-cli/src/snapshot_interactive_mode.js @@ -8,60 +8,78 @@ * @flow */ -import type {AggregatedResult} from 'types/TestResult'; +import type {AggregatedResult, AssertionLocation} from 'types/TestResult'; const chalk = require('chalk'); const ansiEscapes = require('ansi-escapes'); const {pluralize} = require('./reporters/utils'); -const {KEYS} = require('./constants'); +const {KEYS, ARROW} = require('./constants'); export default class SnapshotInteractiveMode { _pipe: stream$Writable | tty$WriteStream; _isActive: boolean; - _updateTestRunnerConfig: (path: string, shouldUpdateSnapshot: boolean) => *; - _testFilePaths: Array; + _updateTestRunnerConfig: ( + assertion: ?AssertionLocation, + shouldUpdateSnapshot: boolean, + ) => *; + _testAssertions: Array; _countPaths: number; + _skippedNum: number; constructor(pipe: stream$Writable | tty$WriteStream) { this._pipe = pipe; this._isActive = false; + this._skippedNum = 0; } isActive() { return this._isActive; } - _drawUIOverlay() { + getSkippedNum() { + return this._skippedNum; + } + + _clearTestSummary() { this._pipe.write(ansiEscapes.cursorUp(6)); this._pipe.write(ansiEscapes.eraseDown); + } - const numFailed = this._testFilePaths.length; - const numPass = this._countPaths - this._testFilePaths.length; + _drawUIProgress() { + this._clearTestSummary(); + const numPass = this._countPaths - this._testAssertions.length; + const numRemaining = this._countPaths - numPass - this._skippedNum; - let stats = chalk.bold.red(pluralize('suite', numFailed) + ' failed'); + let stats = chalk.bold.dim( + pluralize('snapshot', numRemaining) + ' remaining', + ); if (numPass) { - stats += ', ' + chalk.bold.green(pluralize('suite', numPass) + ' passed'); + stats += + ', ' + chalk.bold.green(pluralize('snapshot', numPass) + ' updated'); + } + if (this._skippedNum) { + stats += + ', ' + + chalk.bold.yellow(pluralize('snapshot', this._skippedNum) + ' skipped'); } const messages = [ '\n' + chalk.bold('Interactive Snapshot Progress'), - ' \u203A ' + stats, + ARROW + stats, '\n' + chalk.bold('Watch Usage'), - chalk.dim(' \u203A Press ') + + chalk.dim(ARROW + 'Press ') + 'u' + chalk.dim(' to update failing snapshots for this test.'), - this._testFilePaths.length > 1 - ? chalk.dim(' \u203A Press ') + - 's' + - chalk.dim(' to skip the current test suite.') - : '', + chalk.dim(ARROW + 'Press ') + + 's' + + chalk.dim(' to skip the current test.'), - chalk.dim(' \u203A Press ') + + chalk.dim(ARROW + 'Press ') + 'q' + - chalk.dim(' to quit Interactive Snapshot Update Mode.'), + chalk.dim(' to quit Interactive Snapshot Mode.'), - chalk.dim(' \u203A Press ') + + chalk.dim(ARROW + 'Press ') + 'Enter' + chalk.dim(' to trigger a test run.'), ]; @@ -69,12 +87,89 @@ export default class SnapshotInteractiveMode { this._pipe.write(messages.filter(Boolean).join('\n') + '\n'); } + _drawUIDoneWithSkipped() { + this._pipe.write(ansiEscapes.clearScreen); + const numPass = this._countPaths - this._testAssertions.length; + + let stats = chalk.bold.dim( + pluralize('snapshot', this._countPaths) + ' reviewed', + ); + if (numPass) { + stats += + ', ' + chalk.bold.green(pluralize('snapshot', numPass) + ' updated'); + } + if (this._skippedNum) { + stats += + ', ' + + chalk.bold.yellow(pluralize('snapshot', this._skippedNum) + ' skipped'); + } + const messages = [ + '\n' + chalk.bold('Interactive Snapshot Result'), + ARROW + stats, + '\n' + chalk.bold('Watch Usage'), + + chalk.dim(ARROW + 'Press ') + + 'r' + + chalk.dim(' to restart Interactive Snapshot Mode.'), + + chalk.dim(ARROW + 'Press ') + + 'q' + + chalk.dim(' to quit Interactive Snapshot Mode.'), + ]; + + this._pipe.write(messages.filter(Boolean).join('\n') + '\n'); + } + + _drawUIDone() { + this._pipe.write(ansiEscapes.clearScreen); + const numPass = this._countPaths - this._testAssertions.length; + + let stats = chalk.bold.dim( + pluralize('snapshot', this._countPaths) + ' reviewed', + ); + if (numPass) { + stats += + ', ' + chalk.bold.green(pluralize('snapshot', numPass) + ' updated'); + } + const messages = [ + '\n' + chalk.bold('Interactive Snapshot Result'), + ARROW + stats, + '\n' + chalk.bold('Watch Usage'), + + chalk.dim(ARROW + 'Press ') + + 'Enter' + + chalk.dim(' to return to watch mode.'), + ]; + + this._pipe.write(messages.filter(Boolean).join('\n') + '\n'); + } + + _drawUIOverlay() { + if (this._testAssertions.length === 0) { + return this._drawUIDone(); + } + + if (this._testAssertions.length - this._skippedNum === 0) { + return this._drawUIDoneWithSkipped(); + } + + return this._drawUIProgress(); + } + put(key: string) { switch (key) { case KEYS.S: - const testFilePath = this._testFilePaths.shift(); - this._testFilePaths.push(testFilePath); - this._run(false); + if (this._skippedNum === this._testAssertions.length) break; + this._skippedNum += 1; + + // move skipped test to the end + this._testAssertions.push(this._testAssertions.shift()); + if (this._testAssertions.length - this._skippedNum > 0) { + this._run(false); + } else { + this._drawUIDoneWithSkipped(); + } + break; case KEYS.U: this._run(true); @@ -83,8 +178,15 @@ export default class SnapshotInteractiveMode { case KEYS.ESCAPE: this.abort(); break; + case KEYS.R: + this.restart(); + break; case KEYS.ENTER: - this._run(false); + if (this._testAssertions.length === 0) { + this.abort(); + } else { + this._run(false); + } break; default: break; @@ -93,7 +195,14 @@ export default class SnapshotInteractiveMode { abort() { this._isActive = false; - this._updateTestRunnerConfig('', false); + this._skippedNum = 0; + this._updateTestRunnerConfig(null, false); + } + + restart() { + this._skippedNum = 0; + this._countPaths = this._testAssertions.length; + this._run(false); } updateWithResults(results: AggregatedResult) { @@ -103,29 +212,34 @@ export default class SnapshotInteractiveMode { return; } - this._testFilePaths.shift(); - if (this._testFilePaths.length === 0) { - this.abort(); + this._testAssertions.shift(); + if (this._testAssertions.length - this._skippedNum === 0) { + this._drawUIOverlay(); return; } + + // Go to the next test this._run(false); } _run(shouldUpdateSnapshot: boolean) { - const testFilePath = this._testFilePaths[0]; - this._updateTestRunnerConfig(testFilePath, shouldUpdateSnapshot); + const testAssertion = this._testAssertions[0]; + this._updateTestRunnerConfig(testAssertion, shouldUpdateSnapshot); } run( - failedSnapshotTestPaths: Array, - onConfigChange: (path: string, shouldUpdateSnapshot: boolean) => *, + failedSnapshotTestAssertions: Array, + onConfigChange: ( + assertion: ?AssertionLocation, + shouldUpdateSnapshot: boolean, + ) => *, ) { - if (!failedSnapshotTestPaths.length) { + if (!failedSnapshotTestAssertions.length) { return; } - this._testFilePaths = [].concat(failedSnapshotTestPaths); - this._countPaths = this._testFilePaths.length; + this._testAssertions = [].concat(failedSnapshotTestAssertions); + this._countPaths = this._testAssertions.length; this._updateTestRunnerConfig = onConfigChange; this._isActive = true; this._run(false); diff --git a/types/TestResult.js b/types/TestResult.js index 4442398290cd..7a293f5862f2 100644 --- a/types/TestResult.js +++ b/types/TestResult.js @@ -78,6 +78,11 @@ export type FailedAssertion = {| stack?: string, |}; +export type AssertionLocation = {| + path: string, + title: string, +|}; + export type Status = 'passed' | 'failed' | 'skipped' | 'pending'; export type Bytes = number; diff --git a/website/static/img/content/interactiveSnapshot.png b/website/static/img/content/interactiveSnapshot.png index 0d42f67c4c8c..f43fd5f8a9af 100644 Binary files a/website/static/img/content/interactiveSnapshot.png and b/website/static/img/content/interactiveSnapshot.png differ diff --git a/website/static/img/content/interactiveSnapshotDone.png b/website/static/img/content/interactiveSnapshotDone.png new file mode 100644 index 000000000000..3b7fd653ef42 Binary files /dev/null and b/website/static/img/content/interactiveSnapshotDone.png differ diff --git a/website/static/img/content/interactiveSnapshotUpdate.gif b/website/static/img/content/interactiveSnapshotUpdate.gif index 83928f705ca6..5b41cf1ece4c 100644 Binary files a/website/static/img/content/interactiveSnapshotUpdate.gif and b/website/static/img/content/interactiveSnapshotUpdate.gif differ