-
-
Notifications
You must be signed in to change notification settings - Fork 6.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Interactive Snapshot Update mode (#3831)
* Prototype Interactive Snapshot updates * SnapshotInteractive logic moved into a dedicate file * Unit tests for SnapshotInteractiveMode * Remove unfinish test * Ignore JetBrains IDE meta data folder * SnapshotInteractive callback use a bool to mark snapshot update * Fix the Snapshot Interactive tests by using bool * Remove unused function, but logic to update snapshot need to be abstracted * New Snapshot file * Fix code style of snapshot interactive test * Fixes after rebase * Patch with @thymikee review
- Loading branch information
Showing
8 changed files
with
447 additions
and
1 deletion.
There are no files selected for viewing
36 changes: 36 additions & 0 deletions
36
packages/jest-cli/src/__tests__/__snapshots__/snapshot_interactive_mode.test.js.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
// Jest Snapshot v1, https://goo.gl/fbAQLP | ||
|
||
exports[`SnapshotInteractiveMode updateWithResults last test success, trigger end of interactive mode 1`] = `"TEST RESULTS CONTENTS"`; | ||
|
||
exports[`SnapshotInteractiveMode updateWithResults overlay handle progress UI 1`] = ` | ||
"TEST RESULTS CONTENTS | ||
[MOCK - cursorUp] | ||
[MOCK - eraseDown] | ||
<bold>Interactive Snapshot Progress</> | ||
› <bold><red>2 suites failed</></>, <bold><green>1 suite passed</></> | ||
<bold>Watch Usage</> | ||
<dim> › Press </>u<dim> to update failing snapshots for this test.</> | ||
<dim> › Press </>s<dim> to skip the current snapshot.</> | ||
<dim> › Press </>q<dim> to quit Interactive Snapshot Update Mode.</> | ||
<dim> › Press </>Enter<dim> to trigger a test run.</> | ||
" | ||
`; | ||
exports[`SnapshotInteractiveMode updateWithResults with a test failure simply update UI 1`] = ` | ||
"TEST RESULTS CONTENTS | ||
[MOCK - cursorUp] | ||
[MOCK - eraseDown] | ||
<bold>Interactive Snapshot Progress</> | ||
› <bold><red>1 suite failed</></> | ||
<bold>Watch Usage</> | ||
<dim> › Press </>u<dim> to update failing snapshots for this test.</> | ||
<dim> › Press </>q<dim> to quit Interactive Snapshot Update Mode.</> | ||
<dim> › Press </>Enter<dim> to trigger a test run.</> | ||
" | ||
`; | ||
exports[`SnapshotInteractiveMode updateWithResults with a test success, call the next test 1`] = `"TEST RESULTS CONTENTS"`; |
142 changes: 142 additions & 0 deletions
142
packages/jest-cli/src/__tests__/snapshot_interactive_mode.test.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
/** | ||
* 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. | ||
*/ | ||
|
||
import chalk from 'chalk'; | ||
import {KEYS} from '../constants'; | ||
import SnapshotInteractiveMode from '../snapshot_interactive_mode'; | ||
|
||
jest.mock('../lib/terminal_utils', () => ({ | ||
getTerminalWidth: () => 80, | ||
rightPad: () => { | ||
''; | ||
}, | ||
})); | ||
|
||
jest.mock('ansi-escapes', () => ({ | ||
cursorRestorePosition: '[MOCK - cursorRestorePosition]', | ||
cursorSavePosition: '[MOCK - cursorSavePosition]', | ||
cursorScrollDown: '[MOCK - cursorScrollDown]', | ||
cursorTo: (x, y) => `[MOCK - cursorTo(${x}, ${y})]`, | ||
cursorUp: () => '[MOCK - cursorUp]', | ||
eraseDown: '[MOCK - eraseDown]', | ||
})); | ||
|
||
jest.doMock('chalk', () => | ||
Object.assign(new chalk.constructor({enabled: false}), { | ||
stripColor: str => str, | ||
}), | ||
); | ||
|
||
describe('SnapshotInteractiveMode', () => { | ||
let pipe; | ||
let instance; | ||
|
||
beforeEach(() => { | ||
pipe = {write: jest.fn()}; | ||
instance = new SnapshotInteractiveMode(pipe); | ||
}); | ||
|
||
test('is inactive at construction', () => { | ||
expect(instance.isActive()).toBeFalsy(); | ||
}); | ||
|
||
test('call to run process the first file', () => { | ||
const mockCallback = jest.fn(); | ||
instance.run(['first.js', 'second.js'], mockCallback); | ||
expect(instance.isActive()).toBeTruthy(); | ||
expect(mockCallback).toBeCalledWith('first.js', false); | ||
}); | ||
|
||
test('call to abort', () => { | ||
const mockCallback = jest.fn(); | ||
instance.run(['first.js', 'second.js'], mockCallback); | ||
expect(instance.isActive()).toBeTruthy(); | ||
instance.abort(); | ||
expect(instance.isActive()).toBeFalsy(); | ||
expect(mockCallback).toBeCalledWith('', 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); | ||
}); | ||
}); | ||
describe('updateWithResults', () => { | ||
test('with a test failure simply update UI', () => { | ||
const mockCallback = jest.fn(); | ||
instance.run(['first.js'], mockCallback); | ||
pipe.write('TEST RESULTS CONTENTS'); | ||
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'); | ||
instance.updateWithResults({snapshot: {failure: false}}); | ||
expect(pipe.write.mock.calls.join('\n')).toMatchSnapshot(); | ||
expect(mockCallback.mock.calls[1]).toEqual(['second.js', false]); | ||
}); | ||
|
||
test('overlay handle progress UI', () => { | ||
const mockCallback = jest.fn(); | ||
instance.run(['first.js', 'second.js', 'third.js'], mockCallback); | ||
pipe.write('TEST RESULTS CONTENTS'); | ||
instance.updateWithResults({snapshot: {failure: false}}); | ||
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'); | ||
instance.updateWithResults({snapshot: {failure: false}}); | ||
expect(pipe.write.mock.calls.join('\n')).toMatchSnapshot(); | ||
expect(instance.abort).toHaveBeenCalled(); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
/** | ||
* Copyright (c) 2014-present, Facebook, Inc. All rights reserved. | ||
* | ||
* This source code is licensed under the BSD-style license found in the | ||
* LICENSE file in the root directory of this source tree. An additional grant | ||
* of patent rights can be found in the PATENTS file in the same directory. | ||
* | ||
* @flow | ||
*/ | ||
|
||
import type {AggregatedResult} from 'types/TestResult'; | ||
|
||
const chalk = require('chalk'); | ||
const ansiEscapes = require('ansi-escapes'); | ||
const {pluralize} = require('./reporters/utils'); | ||
const {KEYS} = require('./constants'); | ||
|
||
export default class SnapshotInteractiveMode { | ||
_pipe: stream$Writable | tty$WriteStream; | ||
_isActive: boolean; | ||
_updateTestRunnerConfig: (path: string, shouldUpdateSnapshot: boolean) => *; | ||
_testFilePaths: Array<string>; | ||
_countPaths: number; | ||
|
||
constructor(pipe: stream$Writable | tty$WriteStream) { | ||
this._pipe = pipe; | ||
this._isActive = false; | ||
} | ||
|
||
isActive() { | ||
return this._isActive; | ||
} | ||
|
||
_drawUIOverlay() { | ||
this._pipe.write(ansiEscapes.cursorUp(6)); | ||
this._pipe.write(ansiEscapes.eraseDown); | ||
|
||
const numFailed = this._testFilePaths.length; | ||
const numPass = this._countPaths - this._testFilePaths.length; | ||
|
||
let stats = chalk.bold.red(pluralize('suite', numFailed) + ' failed'); | ||
if (numPass) { | ||
stats += ', ' + chalk.bold.green(pluralize('suite', numPass) + ' passed'); | ||
} | ||
const messages = [ | ||
'\n' + chalk.bold('Interactive Snapshot Progress'), | ||
' \u203A ' + stats, | ||
'\n' + chalk.bold('Watch Usage'), | ||
|
||
chalk.dim(' \u203A 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 snapshot.') | ||
: '', | ||
|
||
chalk.dim(' \u203A Press ') + | ||
'q' + | ||
chalk.dim(' to quit Interactive Snapshot Update Mode.'), | ||
|
||
chalk.dim(' \u203A Press ') + | ||
'Enter' + | ||
chalk.dim(' to trigger a test run.'), | ||
]; | ||
|
||
this._pipe.write(messages.filter(Boolean).join('\n') + '\n'); | ||
} | ||
|
||
put(key: string) { | ||
switch (key) { | ||
case KEYS.S: | ||
const testFilePath = this._testFilePaths.shift(); | ||
this._testFilePaths.push(testFilePath); | ||
this._run(false); | ||
break; | ||
case KEYS.U: | ||
this._run(true); | ||
break; | ||
case KEYS.Q: | ||
case KEYS.ESCAPE: | ||
this.abort(); | ||
break; | ||
case KEYS.ENTER: | ||
this._run(false); | ||
break; | ||
default: | ||
break; | ||
} | ||
} | ||
|
||
abort() { | ||
this._isActive = false; | ||
this._updateTestRunnerConfig('', false); | ||
} | ||
|
||
updateWithResults(results: AggregatedResult) { | ||
const hasSnapshotFailure = !!results.snapshot.failure; | ||
if (hasSnapshotFailure) { | ||
this._drawUIOverlay(); | ||
return; | ||
} | ||
|
||
this._testFilePaths.shift(); | ||
if (this._testFilePaths.length === 0) { | ||
this.abort(); | ||
return; | ||
} | ||
this._run(false); | ||
} | ||
|
||
_run(shouldUpdateSnapshot: boolean) { | ||
const testFilePath = this._testFilePaths[0]; | ||
this._updateTestRunnerConfig(testFilePath, shouldUpdateSnapshot); | ||
} | ||
|
||
run( | ||
failedSnapshotTestPaths: Array<string>, | ||
onConfigChange: (path: string, shouldUpdateSnapshot: boolean) => *, | ||
) { | ||
if (!failedSnapshotTestPaths.length) { | ||
return; | ||
} | ||
|
||
this._testFilePaths = [].concat(failedSnapshotTestPaths); | ||
this._countPaths = this._testFilePaths.length; | ||
this._updateTestRunnerConfig = onConfigChange; | ||
this._isActive = true; | ||
this._run(false); | ||
} | ||
} |
Oops, something went wrong.