Skip to content

Commit

Permalink
Add arrows selection to typeahead (jestjs#3386)
Browse files Browse the repository at this point in the history
* Add typeahead arrow selection

* Handle max typeahead max offset

* Add tests for scroll list

* Add simple tests to typeheahead selection

* Clear typeahead selection on enter

* Fix prompt test

* Refactor pattern prompts

* Stress 'cached' in TestNamePatternPrompt

* Fix eslint

* Fix scrolling edgecase

* Live update number of remaining typeahead items
  • Loading branch information
rogeliog authored and cpojer committed May 4, 2017
1 parent 1c536f6 commit 829531f
Show file tree
Hide file tree
Showing 12 changed files with 582 additions and 294 deletions.
61 changes: 61 additions & 0 deletions packages/jest-cli/src/PatternPrompt.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* 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
*/

'use strict';

import type {ScrollOptions} from './lib/scrollList';

const chalk = require('chalk');
const ansiEscapes = require('ansi-escapes');
const Prompt = require('./lib/Prompt');

const usage = (entity: string) =>
`\n${chalk.bold('Pattern Mode Usage')}\n` +
` ${chalk.dim('\u203A Press')} Esc ${chalk.dim('to exit pattern mode.')}\n` +
` ${chalk.dim('\u203A Press')} Enter ` +
`${chalk.dim(`to apply pattern to all ${entity}.`)}\n` +
`\n`;

const usageRows = usage('').split('\n').length;

module.exports = class PatternPrompt {
_pipe: stream$Writable | tty$WriteStream;
_prompt: Prompt;
_entityName: string;
_currentUsageRows: number;

constructor(pipe: stream$Writable | tty$WriteStream, prompt: Prompt) {
this._pipe = pipe;
this._prompt = prompt;
this._currentUsageRows = usageRows;
}

run(onSuccess: Function, onCancel: Function, options?: {header: string}) {
this._pipe.write(ansiEscapes.cursorHide);
this._pipe.write(ansiEscapes.clearScreen);

if (options && options.header) {
this._pipe.write(options.header + '\n');
this._currentUsageRows = usageRows + options.header.split('\n').length;
} else {
this._currentUsageRows = usageRows;
}

this._pipe.write(usage(this._entityName));
this._pipe.write(ansiEscapes.cursorShow);

this._prompt.enter(this._onChange.bind(this), onSuccess, onCancel);
}

_onChange(pattern: string, options: ScrollOptions) {
this._pipe.write(ansiEscapes.eraseLine);
this._pipe.write(ansiEscapes.cursorLeft);
}
};
121 changes: 44 additions & 77 deletions packages/jest-cli/src/TestNamePatternPrompt.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,106 +11,73 @@
'use strict';

import type {TestResult} from 'types/TestResult';
import type {ScrollOptions} from './lib/scrollList';

const ansiEscapes = require('ansi-escapes');
const chalk = require('chalk');
const scroll = require('./lib/scrollList');
const {getTerminalWidth} = require('./lib/terminalUtils');
const stringLength = require('string-length');
const Prompt = require('./lib/Prompt');
const formatTestNameByPattern = require('./lib/formatTestNameByPattern');

const pluralizeTest = (total: number) => (total === 1 ? 'test' : 'tests');

const usage = () =>
`\n${chalk.bold('Pattern Mode Usage')}\n` +
` ${chalk.dim('\u203A Press')} Esc ${chalk.dim('to exit pattern mode.')}\n` +
` ${chalk.dim('\u203A Press')} Enter ` +
`${chalk.dim('to apply pattern to all tests.')}\n` +
`\n`;

const usageRows = usage().split('\n').length;

module.exports = class TestNamePatternPrompt {
const {
formatTypeaheadSelection,
printMore,
printPatternCaret,
printPatternMatches,
printRestoredPatternCaret,
printStartTyping,
printTypeaheadItem,
} = require('./lib/patternModeHelpers');
const PatternPrompt = require('./PatternPrompt');

module.exports = class TestNamePatternPrompt extends PatternPrompt {
_cachedTestResults: Array<TestResult>;
_pipe: stream$Writable | tty$WriteStream;
_prompt: Prompt;
_currentUsageRows: number;

constructor(pipe: stream$Writable | tty$WriteStream, prompt: Prompt) {
this._pipe = pipe;
this._prompt = prompt;
this._currentUsageRows = usageRows;
super(pipe, prompt);
this._entityName = 'tests';
this._cachedTestResults = [];
}

run(onSuccess: Function, onCancel: Function, options?: {header: string}) {
this._pipe.write(ansiEscapes.cursorHide);
this._pipe.write(ansiEscapes.clearScreen);
if (options && options.header) {
this._pipe.write(options.header + '\n');
this._currentUsageRows = usageRows + options.header.split('\n').length;
} else {
this._currentUsageRows = usageRows;
}
this._pipe.write(usage());
this._pipe.write(ansiEscapes.cursorShow);

this._prompt.enter(this._onChange.bind(this), onSuccess, onCancel);
_onChange(pattern: string, options: ScrollOptions) {
super._onChange(pattern, options);
this._printTypeahead(pattern, options);
}

_onChange(pattern: string) {
this._pipe.write(ansiEscapes.eraseLine);
this._pipe.write(ansiEscapes.cursorLeft);
this._printTypeahead(pattern, 10);
}

_printTypeahead(pattern: string, max: number) {
_printTypeahead(pattern: string, options: ScrollOptions) {
const {max} = options;
const matchedTests = this._getMatchedTests(pattern);

const total = matchedTests.length;
const results = matchedTests.slice(0, max);
const inputText = `${chalk.dim(' pattern \u203A')} ${pattern}`;
const pipe = this._pipe;
const prompt = this._prompt;

this._pipe.write(ansiEscapes.eraseDown);
this._pipe.write(inputText);
this._pipe.write(ansiEscapes.cursorSavePosition);
printPatternCaret(pattern, pipe);

if (pattern) {
if (total) {
this._pipe.write(
`\n\n Pattern matches ${total} ${pluralizeTest(total)}`,
);
} else {
this._pipe.write(`\n\n Pattern matches no tests`);
}

this._pipe.write(' from cached test suites.');
printPatternMatches(
total,
'test',
pipe,
` from ${require('chalk').yellow('cached')} test suites`,
);

const width = getTerminalWidth();
const {start, end, index} = scroll(total, options);

results.forEach(name => {
const testName = formatTestNameByPattern(name, pattern, width - 4);
prompt.setTypeaheadLength(total);

this._pipe.write(`\n ${chalk.dim('\u203A')} ${testName}`);
});
matchedTests
.slice(start, end)
.map(name => formatTestNameByPattern(name, pattern, width - 4))
.map((item, i) => formatTypeaheadSelection(item, i, index, prompt))
.forEach(item => printTypeaheadItem(item, pipe));

if (total > max) {
const more = total - max;
this._pipe.write(
// eslint-disable-next-line max-len
`\n ${chalk.dim(`\u203A and ${more} more ${pluralizeTest(more)}`)}`,
);
if (total > end) {
printMore('test', pipe, total - end);
}
} else {
this._pipe.write(
// eslint-disable-next-line max-len
`\n\n ${chalk.italic.yellow('Start typing to filter by a test name regex pattern.')}`,
);
printStartTyping('test name', pipe);
}

this._pipe.write(
ansiEscapes.cursorTo(stringLength(inputText), this._currentUsageRows - 1),
);
this._pipe.write(ansiEscapes.cursorRestorePosition);
printRestoredPatternCaret(pattern, this._currentUsageRows, pipe);
}

_getMatchedTests(pattern: string) {
Expand All @@ -135,7 +102,7 @@ module.exports = class TestNamePatternPrompt {
return matchedTests;
}

updateCachedTestResults(testResults: Array<TestResult>) {
this._cachedTestResults = testResults || [];
updateCachedTestResults(testResults: Array<TestResult> = []) {
this._cachedTestResults = testResults;
}
};
Loading

0 comments on commit 829531f

Please sign in to comment.