Skip to content

Commit 2dd69b9

Browse files
geninthocpojer
authored andcommitted
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
1 parent 2d07714 commit 2dd69b9

File tree

8 files changed

+447
-1
lines changed

8 files changed

+447
-1
lines changed
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`SnapshotInteractiveMode updateWithResults last test success, trigger end of interactive mode 1`] = `"TEST RESULTS CONTENTS"`;
4+
5+
exports[`SnapshotInteractiveMode updateWithResults overlay handle progress UI 1`] = `
6+
"TEST RESULTS CONTENTS
7+
[MOCK - cursorUp]
8+
[MOCK - eraseDown]
9+
10+
<bold>Interactive Snapshot Progress</>
11+
› <bold><red>2 suites failed</></>, <bold><green>1 suite passed</></>
12+
13+
<bold>Watch Usage</>
14+
<dim> › Press </>u<dim> to update failing snapshots for this test.</>
15+
<dim> › Press </>s<dim> to skip the current snapshot.</>
16+
<dim> › Press </>q<dim> to quit Interactive Snapshot Update Mode.</>
17+
<dim> › Press </>Enter<dim> to trigger a test run.</>
18+
"
19+
`;
20+
21+
exports[`SnapshotInteractiveMode updateWithResults with a test failure simply update UI 1`] = `
22+
"TEST RESULTS CONTENTS
23+
[MOCK - cursorUp]
24+
[MOCK - eraseDown]
25+
26+
<bold>Interactive Snapshot Progress</>
27+
› <bold><red>1 suite failed</></>
28+
29+
<bold>Watch Usage</>
30+
<dim> › Press </>u<dim> to update failing snapshots for this test.</>
31+
<dim> › Press </>q<dim> to quit Interactive Snapshot Update Mode.</>
32+
<dim> › Press </>Enter<dim> to trigger a test run.</>
33+
"
34+
`;
35+
36+
exports[`SnapshotInteractiveMode updateWithResults with a test success, call the next test 1`] = `"TEST RESULTS CONTENTS"`;
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/**
2+
* Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import chalk from 'chalk';
9+
import {KEYS} from '../constants';
10+
import SnapshotInteractiveMode from '../snapshot_interactive_mode';
11+
12+
jest.mock('../lib/terminal_utils', () => ({
13+
getTerminalWidth: () => 80,
14+
rightPad: () => {
15+
'';
16+
},
17+
}));
18+
19+
jest.mock('ansi-escapes', () => ({
20+
cursorRestorePosition: '[MOCK - cursorRestorePosition]',
21+
cursorSavePosition: '[MOCK - cursorSavePosition]',
22+
cursorScrollDown: '[MOCK - cursorScrollDown]',
23+
cursorTo: (x, y) => `[MOCK - cursorTo(${x}, ${y})]`,
24+
cursorUp: () => '[MOCK - cursorUp]',
25+
eraseDown: '[MOCK - eraseDown]',
26+
}));
27+
28+
jest.doMock('chalk', () =>
29+
Object.assign(new chalk.constructor({enabled: false}), {
30+
stripColor: str => str,
31+
}),
32+
);
33+
34+
describe('SnapshotInteractiveMode', () => {
35+
let pipe;
36+
let instance;
37+
38+
beforeEach(() => {
39+
pipe = {write: jest.fn()};
40+
instance = new SnapshotInteractiveMode(pipe);
41+
});
42+
43+
test('is inactive at construction', () => {
44+
expect(instance.isActive()).toBeFalsy();
45+
});
46+
47+
test('call to run process the first file', () => {
48+
const mockCallback = jest.fn();
49+
instance.run(['first.js', 'second.js'], mockCallback);
50+
expect(instance.isActive()).toBeTruthy();
51+
expect(mockCallback).toBeCalledWith('first.js', false);
52+
});
53+
54+
test('call to abort', () => {
55+
const mockCallback = jest.fn();
56+
instance.run(['first.js', 'second.js'], mockCallback);
57+
expect(instance.isActive()).toBeTruthy();
58+
instance.abort();
59+
expect(instance.isActive()).toBeFalsy();
60+
expect(mockCallback).toBeCalledWith('', false);
61+
});
62+
describe('key press handler', () => {
63+
test('call to skip trigger a processing of next file', () => {
64+
const mockCallback = jest.fn();
65+
instance.run(['first.js', 'second.js'], mockCallback);
66+
expect(mockCallback.mock.calls[0]).toEqual(['first.js', false]);
67+
instance.put(KEYS.S);
68+
expect(mockCallback.mock.calls[1]).toEqual(['second.js', false]);
69+
instance.put(KEYS.S);
70+
expect(mockCallback.mock.calls[2]).toEqual(['first.js', false]);
71+
});
72+
73+
test('call to skip works with 1 file', () => {
74+
const mockCallback = jest.fn();
75+
instance.run(['first.js'], mockCallback);
76+
expect(mockCallback.mock.calls[0]).toEqual(['first.js', false]);
77+
instance.put(KEYS.S);
78+
expect(mockCallback.mock.calls[1]).toEqual(['first.js', false]);
79+
});
80+
81+
test('press U trigger a snapshot update call', () => {
82+
const mockCallback = jest.fn();
83+
instance.run(['first.js'], mockCallback);
84+
expect(mockCallback.mock.calls[0]).toEqual(['first.js', false]);
85+
instance.put(KEYS.U);
86+
expect(mockCallback.mock.calls[1]).toEqual(['first.js', true]);
87+
});
88+
89+
test('press Q or ESC triggers an abort', () => {
90+
instance.abort = jest.fn();
91+
instance.put(KEYS.Q);
92+
instance.put(KEYS.ESCAPE);
93+
expect(instance.abort).toHaveBeenCalledTimes(2);
94+
});
95+
96+
test('press ENTER trigger a run', () => {
97+
const mockCallback = jest.fn();
98+
instance.run(['first.js'], mockCallback);
99+
instance.put(KEYS.ENTER);
100+
expect(mockCallback).toHaveBeenCalledTimes(2);
101+
expect(mockCallback).toHaveBeenCalledWith('first.js', false);
102+
});
103+
});
104+
describe('updateWithResults', () => {
105+
test('with a test failure simply update UI', () => {
106+
const mockCallback = jest.fn();
107+
instance.run(['first.js'], mockCallback);
108+
pipe.write('TEST RESULTS CONTENTS');
109+
instance.updateWithResults({snapshot: {failure: true}});
110+
expect(pipe.write.mock.calls.join('\n')).toMatchSnapshot();
111+
expect(mockCallback).toHaveBeenCalledTimes(1);
112+
});
113+
114+
test('with a test success, call the next test', () => {
115+
const mockCallback = jest.fn();
116+
instance.run(['first.js', 'second.js'], mockCallback);
117+
pipe.write('TEST RESULTS CONTENTS');
118+
instance.updateWithResults({snapshot: {failure: false}});
119+
expect(pipe.write.mock.calls.join('\n')).toMatchSnapshot();
120+
expect(mockCallback.mock.calls[1]).toEqual(['second.js', false]);
121+
});
122+
123+
test('overlay handle progress UI', () => {
124+
const mockCallback = jest.fn();
125+
instance.run(['first.js', 'second.js', 'third.js'], mockCallback);
126+
pipe.write('TEST RESULTS CONTENTS');
127+
instance.updateWithResults({snapshot: {failure: false}});
128+
instance.updateWithResults({snapshot: {failure: true}});
129+
expect(pipe.write.mock.calls.join('\n')).toMatchSnapshot();
130+
});
131+
132+
test('last test success, trigger end of interactive mode', () => {
133+
const mockCallback = jest.fn();
134+
instance.abort = jest.fn();
135+
instance.run(['first.js'], mockCallback);
136+
pipe.write('TEST RESULTS CONTENTS');
137+
instance.updateWithResults({snapshot: {failure: false}});
138+
expect(pipe.write.mock.calls.join('\n')).toMatchSnapshot();
139+
expect(instance.abort).toHaveBeenCalled();
140+
});
141+
});
142+
});

packages/jest-cli/src/constants.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,12 @@ export const KEYS = {
2424
ENTER: '0d',
2525
ESCAPE: '1b',
2626
F: '66',
27+
I: '69',
2728
O: '6f',
2829
P: '70',
2930
Q: '71',
3031
QUESTION_MARK: '3f',
32+
S: '73',
3133
T: '74',
3234
U: '75',
3335
W: '77',
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/**
2+
* Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
3+
*
4+
* This source code is licensed under the BSD-style license found in the
5+
* LICENSE file in the root directory of this source tree. An additional grant
6+
* of patent rights can be found in the PATENTS file in the same directory.
7+
*
8+
* @flow
9+
*/
10+
11+
import type {AggregatedResult} from 'types/TestResult';
12+
13+
const chalk = require('chalk');
14+
const ansiEscapes = require('ansi-escapes');
15+
const {pluralize} = require('./reporters/utils');
16+
const {KEYS} = require('./constants');
17+
18+
export default class SnapshotInteractiveMode {
19+
_pipe: stream$Writable | tty$WriteStream;
20+
_isActive: boolean;
21+
_updateTestRunnerConfig: (path: string, shouldUpdateSnapshot: boolean) => *;
22+
_testFilePaths: Array<string>;
23+
_countPaths: number;
24+
25+
constructor(pipe: stream$Writable | tty$WriteStream) {
26+
this._pipe = pipe;
27+
this._isActive = false;
28+
}
29+
30+
isActive() {
31+
return this._isActive;
32+
}
33+
34+
_drawUIOverlay() {
35+
this._pipe.write(ansiEscapes.cursorUp(6));
36+
this._pipe.write(ansiEscapes.eraseDown);
37+
38+
const numFailed = this._testFilePaths.length;
39+
const numPass = this._countPaths - this._testFilePaths.length;
40+
41+
let stats = chalk.bold.red(pluralize('suite', numFailed) + ' failed');
42+
if (numPass) {
43+
stats += ', ' + chalk.bold.green(pluralize('suite', numPass) + ' passed');
44+
}
45+
const messages = [
46+
'\n' + chalk.bold('Interactive Snapshot Progress'),
47+
' \u203A ' + stats,
48+
'\n' + chalk.bold('Watch Usage'),
49+
50+
chalk.dim(' \u203A Press ') +
51+
'u' +
52+
chalk.dim(' to update failing snapshots for this test.'),
53+
54+
this._testFilePaths.length > 1
55+
? chalk.dim(' \u203A Press ') +
56+
's' +
57+
chalk.dim(' to skip the current snapshot.')
58+
: '',
59+
60+
chalk.dim(' \u203A Press ') +
61+
'q' +
62+
chalk.dim(' to quit Interactive Snapshot Update Mode.'),
63+
64+
chalk.dim(' \u203A Press ') +
65+
'Enter' +
66+
chalk.dim(' to trigger a test run.'),
67+
];
68+
69+
this._pipe.write(messages.filter(Boolean).join('\n') + '\n');
70+
}
71+
72+
put(key: string) {
73+
switch (key) {
74+
case KEYS.S:
75+
const testFilePath = this._testFilePaths.shift();
76+
this._testFilePaths.push(testFilePath);
77+
this._run(false);
78+
break;
79+
case KEYS.U:
80+
this._run(true);
81+
break;
82+
case KEYS.Q:
83+
case KEYS.ESCAPE:
84+
this.abort();
85+
break;
86+
case KEYS.ENTER:
87+
this._run(false);
88+
break;
89+
default:
90+
break;
91+
}
92+
}
93+
94+
abort() {
95+
this._isActive = false;
96+
this._updateTestRunnerConfig('', false);
97+
}
98+
99+
updateWithResults(results: AggregatedResult) {
100+
const hasSnapshotFailure = !!results.snapshot.failure;
101+
if (hasSnapshotFailure) {
102+
this._drawUIOverlay();
103+
return;
104+
}
105+
106+
this._testFilePaths.shift();
107+
if (this._testFilePaths.length === 0) {
108+
this.abort();
109+
return;
110+
}
111+
this._run(false);
112+
}
113+
114+
_run(shouldUpdateSnapshot: boolean) {
115+
const testFilePath = this._testFilePaths[0];
116+
this._updateTestRunnerConfig(testFilePath, shouldUpdateSnapshot);
117+
}
118+
119+
run(
120+
failedSnapshotTestPaths: Array<string>,
121+
onConfigChange: (path: string, shouldUpdateSnapshot: boolean) => *,
122+
) {
123+
if (!failedSnapshotTestPaths.length) {
124+
return;
125+
}
126+
127+
this._testFilePaths = [].concat(failedSnapshotTestPaths);
128+
this._countPaths = this._testFilePaths.length;
129+
this._updateTestRunnerConfig = onConfigChange;
130+
this._isActive = true;
131+
this._run(false);
132+
}
133+
}

0 commit comments

Comments
 (0)