Skip to content

Commit

Permalink
Interactive Snapshot Update mode (#3831)
Browse files Browse the repository at this point in the history
* 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
genintho authored and cpojer committed Jan 11, 2018
1 parent 2d07714 commit 2dd69b9
Show file tree
Hide file tree
Showing 8 changed files with 447 additions and 1 deletion.
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 packages/jest-cli/src/__tests__/snapshot_interactive_mode.test.js
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();
});
});
});
2 changes: 2 additions & 0 deletions packages/jest-cli/src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,12 @@ export const KEYS = {
ENTER: '0d',
ESCAPE: '1b',
F: '66',
I: '69',
O: '6f',
P: '70',
Q: '71',
QUESTION_MARK: '3f',
S: '73',
T: '74',
U: '75',
W: '77',
Expand Down
133 changes: 133 additions & 0 deletions packages/jest-cli/src/snapshot_interactive_mode.js
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);
}
}

0 comments on commit 2dd69b9

Please sign in to comment.