jest-watch-typeahead 2.1.0 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/file_name_plugin/plugin.js +49 -0
- package/build/file_name_plugin/prompt.js +92 -0
- package/build/index.js +8 -0
- package/build/lib/pattern_mode_helpers.js +27 -0
- package/build/lib/scroll.js +26 -0
- package/build/lib/utils.js +126 -0
- package/build/test_name_plugin/plugin.js +49 -0
- package/build/test_name_plugin/prompt.js +86 -0
- package/build/types/Config.js +1 -0
- package/package.json +3 -3
@@ -0,0 +1,49 @@
|
|
1
|
+
import { Prompt } from 'jest-watcher';
|
2
|
+
import FileNamePatternPrompt from "./prompt.js";
|
3
|
+
export default class FileNamePlugin {
|
4
|
+
constructor({
|
5
|
+
stdin,
|
6
|
+
stdout,
|
7
|
+
config = {}
|
8
|
+
}) {
|
9
|
+
this._stdin = stdin;
|
10
|
+
this._stdout = stdout;
|
11
|
+
this._prompt = new Prompt();
|
12
|
+
this._projects = [];
|
13
|
+
this._usageInfo = {
|
14
|
+
key: config.key || 'p',
|
15
|
+
prompt: config.prompt || 'filter by a filename regex pattern'
|
16
|
+
};
|
17
|
+
}
|
18
|
+
|
19
|
+
apply(jestHooks) {
|
20
|
+
jestHooks.onFileChange(({
|
21
|
+
projects
|
22
|
+
}) => {
|
23
|
+
this._projects = projects;
|
24
|
+
});
|
25
|
+
}
|
26
|
+
|
27
|
+
onKey(key) {
|
28
|
+
this._prompt.put(key);
|
29
|
+
}
|
30
|
+
|
31
|
+
run(globalConfig, updateConfigAndRun) {
|
32
|
+
const p = new FileNamePatternPrompt(this._stdout, this._prompt);
|
33
|
+
p.updateSearchSources(this._projects);
|
34
|
+
return new Promise((res, rej) => {
|
35
|
+
p.run(testPathPattern => {
|
36
|
+
updateConfigAndRun({
|
37
|
+
mode: 'watch',
|
38
|
+
testPathPattern
|
39
|
+
});
|
40
|
+
res();
|
41
|
+
}, rej);
|
42
|
+
});
|
43
|
+
}
|
44
|
+
|
45
|
+
getUsageInfo() {
|
46
|
+
return this._usageInfo;
|
47
|
+
}
|
48
|
+
|
49
|
+
}
|
@@ -0,0 +1,92 @@
|
|
1
|
+
import chalk from 'chalk';
|
2
|
+
import ansiEscapes from 'ansi-escapes';
|
3
|
+
import stringLength from 'string-length';
|
4
|
+
import { PatternPrompt, printPatternCaret, printRestoredPatternCaret } from 'jest-watcher';
|
5
|
+
import { escapeStrForRegex } from 'jest-regex-util';
|
6
|
+
import { highlight, getTerminalWidth, trimAndFormatPath, removeTrimmingDots } from "../lib/utils.js";
|
7
|
+
import { formatTypeaheadSelection, printMore, printPatternMatches, printStartTyping, printTypeaheadItem } from "../lib/pattern_mode_helpers.js";
|
8
|
+
import scroll from "../lib/scroll.js";
|
9
|
+
export default class FileNamePatternPrompt extends PatternPrompt {
|
10
|
+
constructor(pipe, prompt) {
|
11
|
+
super(pipe, prompt);
|
12
|
+
this._entityName = 'filenames';
|
13
|
+
this._searchSources = [];
|
14
|
+
}
|
15
|
+
|
16
|
+
_onChange(pattern, options) {
|
17
|
+
super._onChange(pattern, options);
|
18
|
+
|
19
|
+
this._printTypeahead(pattern, options);
|
20
|
+
}
|
21
|
+
|
22
|
+
_printTypeahead(pattern, options) {
|
23
|
+
const matchedTests = this._getMatchedTests(pattern);
|
24
|
+
|
25
|
+
const total = matchedTests.length;
|
26
|
+
const pipe = this._pipe;
|
27
|
+
const prompt = this._prompt;
|
28
|
+
printPatternCaret(pattern, pipe);
|
29
|
+
pipe.write(ansiEscapes.cursorLeft);
|
30
|
+
|
31
|
+
if (pattern) {
|
32
|
+
printPatternMatches(total, 'file', pipe);
|
33
|
+
const prefix = ` ${chalk.dim('\u203A')} `;
|
34
|
+
const padding = stringLength(prefix) + 2;
|
35
|
+
const width = getTerminalWidth(pipe);
|
36
|
+
const {
|
37
|
+
start,
|
38
|
+
end,
|
39
|
+
index
|
40
|
+
} = scroll(total, options);
|
41
|
+
prompt.setPromptLength(total);
|
42
|
+
matchedTests.slice(start, end).map(({
|
43
|
+
path,
|
44
|
+
context
|
45
|
+
}) => {
|
46
|
+
const filePath = trimAndFormatPath(padding, context.config, path, width);
|
47
|
+
return highlight(path, filePath, pattern);
|
48
|
+
}).map((item, i) => formatTypeaheadSelection(item, i, index, prompt)).forEach(item => printTypeaheadItem(item, pipe));
|
49
|
+
|
50
|
+
if (total > end) {
|
51
|
+
printMore('file', pipe, total - end);
|
52
|
+
}
|
53
|
+
} else {
|
54
|
+
printStartTyping('filename', pipe);
|
55
|
+
}
|
56
|
+
|
57
|
+
printRestoredPatternCaret(pattern, this._currentUsageRows, pipe);
|
58
|
+
}
|
59
|
+
|
60
|
+
_getMatchedTests(pattern) {
|
61
|
+
let regex;
|
62
|
+
|
63
|
+
try {
|
64
|
+
regex = new RegExp(pattern, 'i');
|
65
|
+
} catch (e) {
|
66
|
+
return [];
|
67
|
+
}
|
68
|
+
|
69
|
+
return this._searchSources.reduce((tests, {
|
70
|
+
testPaths,
|
71
|
+
config
|
72
|
+
}) => {
|
73
|
+
return tests.concat(testPaths.filter(testPath => regex.test(testPath)).map(path => ({
|
74
|
+
path,
|
75
|
+
context: {
|
76
|
+
config
|
77
|
+
}
|
78
|
+
})));
|
79
|
+
}, []);
|
80
|
+
}
|
81
|
+
|
82
|
+
updateSearchSources(searchSources) {
|
83
|
+
this._searchSources = searchSources;
|
84
|
+
}
|
85
|
+
|
86
|
+
run(onSuccess, onCancel, options) {
|
87
|
+
super.run(value => {
|
88
|
+
onSuccess(removeTrimmingDots(value).split('/').map(escapeStrForRegex).join('/'));
|
89
|
+
}, onCancel, options);
|
90
|
+
}
|
91
|
+
|
92
|
+
}
|
package/build/index.js
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
import chalk from 'chalk';
|
2
|
+
import stripAnsi from 'strip-ansi';
|
3
|
+
|
4
|
+
const pluralize = (count, text) => count === 1 ? text : `${text}s`;
|
5
|
+
|
6
|
+
export const printPatternMatches = (count, entity, pipe, extraText = '') => {
|
7
|
+
const pluralized = pluralize(count, entity);
|
8
|
+
const result = count ? `\n\n Pattern matches ${count} ${pluralized}` : `\n\n Pattern matches no ${pluralized}`;
|
9
|
+
pipe.write(result + extraText);
|
10
|
+
};
|
11
|
+
export const printStartTyping = (entity, pipe) => {
|
12
|
+
pipe.write(`\n\n ${chalk.italic.yellow(`Start typing to filter by a ${entity} regex pattern.`)}`);
|
13
|
+
};
|
14
|
+
export const printMore = (entity, pipe, more) => {
|
15
|
+
pipe.write(`\n ${chalk.dim(`...and ${more} more ${pluralize(more, entity)}`)}`);
|
16
|
+
};
|
17
|
+
export const printTypeaheadItem = (item, pipe) => {
|
18
|
+
pipe.write(`\n ${chalk.dim('\u203A')} ${item}`);
|
19
|
+
};
|
20
|
+
export const formatTypeaheadSelection = (item, index, activeIndex, prompt) => {
|
21
|
+
if (index === activeIndex) {
|
22
|
+
prompt.setPromptSelection(stripAnsi(item));
|
23
|
+
return chalk.black.bgYellow(stripAnsi(item));
|
24
|
+
}
|
25
|
+
|
26
|
+
return item;
|
27
|
+
};
|
@@ -0,0 +1,26 @@
|
|
1
|
+
const scroll = (size, {
|
2
|
+
offset,
|
3
|
+
max
|
4
|
+
}) => {
|
5
|
+
let start = 0;
|
6
|
+
let index = Math.min(offset, size);
|
7
|
+
const halfScreen = max / 2;
|
8
|
+
|
9
|
+
if (index <= halfScreen) {
|
10
|
+
start = 0;
|
11
|
+
} else {
|
12
|
+
if (size >= max) {
|
13
|
+
start = Math.min(index - halfScreen - 1, size - max);
|
14
|
+
}
|
15
|
+
|
16
|
+
index = Math.min(index - start, size);
|
17
|
+
}
|
18
|
+
|
19
|
+
return {
|
20
|
+
end: Math.min(size, start + max),
|
21
|
+
index,
|
22
|
+
start
|
23
|
+
};
|
24
|
+
};
|
25
|
+
|
26
|
+
export default scroll;
|
@@ -0,0 +1,126 @@
|
|
1
|
+
import path from 'path';
|
2
|
+
import chalk from 'chalk';
|
3
|
+
import slash from 'slash';
|
4
|
+
import stripAnsi from 'strip-ansi';
|
5
|
+
const TRIMMING_DOTS = '...';
|
6
|
+
const ENTER = '⏎';
|
7
|
+
|
8
|
+
const relativePath = (config, testPath) => {
|
9
|
+
const relativeTestPath = path.relative(config.cwd || config.rootDir, testPath);
|
10
|
+
const dirname = path.dirname(relativeTestPath);
|
11
|
+
const basename = path.basename(relativeTestPath);
|
12
|
+
return {
|
13
|
+
basename,
|
14
|
+
dirname
|
15
|
+
};
|
16
|
+
};
|
17
|
+
|
18
|
+
const colorize = (str, start, end) => chalk.dim(str.slice(0, start)) + chalk.reset(str.slice(start, end)) + chalk.dim(str.slice(end));
|
19
|
+
|
20
|
+
export const trimAndFormatPath = (pad, config, testPath, columns) => {
|
21
|
+
const maxLength = columns - pad;
|
22
|
+
const relative = relativePath(config, testPath);
|
23
|
+
const {
|
24
|
+
basename
|
25
|
+
} = relative;
|
26
|
+
let {
|
27
|
+
dirname
|
28
|
+
} = relative; // length is ok
|
29
|
+
|
30
|
+
if ((dirname + path.sep + basename).length <= maxLength) {
|
31
|
+
return slash(chalk.dim(dirname + path.sep) + chalk.bold(basename));
|
32
|
+
} // we can fit trimmed dirname and full basename
|
33
|
+
|
34
|
+
|
35
|
+
const basenameLength = basename.length;
|
36
|
+
|
37
|
+
if (basenameLength + 4 < maxLength) {
|
38
|
+
const dirnameLength = maxLength - 4 - basenameLength;
|
39
|
+
dirname = `${TRIMMING_DOTS}${dirname.slice(dirname.length - dirnameLength, dirname.length)}`;
|
40
|
+
return slash(chalk.dim(dirname + path.sep) + chalk.bold(basename));
|
41
|
+
}
|
42
|
+
|
43
|
+
if (basenameLength + 4 === maxLength) {
|
44
|
+
return slash(chalk.dim(`${TRIMMING_DOTS}${path.sep}`) + chalk.bold(basename));
|
45
|
+
} // can't fit dirname, but can fit trimmed basename
|
46
|
+
|
47
|
+
|
48
|
+
return slash(chalk.bold(`${TRIMMING_DOTS}${basename.slice(-maxLength + 3)}`));
|
49
|
+
};
|
50
|
+
export const getTerminalWidth = (pipe = process.stdout) => pipe.columns;
|
51
|
+
export const highlight = (rawPath, filePath, pattern) => {
|
52
|
+
const relativePathHead = './';
|
53
|
+
let regexp;
|
54
|
+
|
55
|
+
try {
|
56
|
+
regexp = new RegExp(pattern, 'i');
|
57
|
+
} catch (e) {
|
58
|
+
return chalk.dim(filePath);
|
59
|
+
}
|
60
|
+
|
61
|
+
const strippedRawPath = stripAnsi(rawPath);
|
62
|
+
const strippedFilePath = stripAnsi(filePath);
|
63
|
+
const match = strippedRawPath.match(regexp);
|
64
|
+
|
65
|
+
if (!match || match.index == null) {
|
66
|
+
return chalk.dim(strippedFilePath);
|
67
|
+
}
|
68
|
+
|
69
|
+
const offset = strippedRawPath.length - strippedFilePath.length;
|
70
|
+
let trimLength;
|
71
|
+
|
72
|
+
if (strippedFilePath.startsWith(TRIMMING_DOTS)) {
|
73
|
+
trimLength = TRIMMING_DOTS.length;
|
74
|
+
} else if (strippedFilePath.startsWith(relativePathHead)) {
|
75
|
+
trimLength = relativePathHead.length;
|
76
|
+
} else {
|
77
|
+
trimLength = 0;
|
78
|
+
}
|
79
|
+
|
80
|
+
const start = match.index - offset;
|
81
|
+
const end = start + match[0].length;
|
82
|
+
return colorize(strippedFilePath, Math.max(start, 0), Math.max(end, trimLength));
|
83
|
+
};
|
84
|
+
export const formatTestNameByPattern = (testName, pattern, width) => {
|
85
|
+
const inlineTestName = testName.replace(/(\r\n|\n|\r)/gm, ENTER);
|
86
|
+
let regexp;
|
87
|
+
|
88
|
+
try {
|
89
|
+
regexp = new RegExp(pattern, 'i');
|
90
|
+
} catch (e) {
|
91
|
+
return chalk.dim(inlineTestName);
|
92
|
+
}
|
93
|
+
|
94
|
+
const match = inlineTestName.match(regexp);
|
95
|
+
|
96
|
+
if (!match || match.index == null) {
|
97
|
+
return chalk.dim(inlineTestName);
|
98
|
+
}
|
99
|
+
|
100
|
+
const startPatternIndex = Math.max(match.index, 0);
|
101
|
+
const endPatternIndex = startPatternIndex + match[0].length;
|
102
|
+
const testNameFitsInTerminal = inlineTestName.length <= width;
|
103
|
+
|
104
|
+
if (testNameFitsInTerminal) {
|
105
|
+
return colorize(inlineTestName, startPatternIndex, endPatternIndex);
|
106
|
+
}
|
107
|
+
|
108
|
+
const numberOfTruncatedChars = TRIMMING_DOTS.length + inlineTestName.length - width;
|
109
|
+
const end = Math.max(endPatternIndex - numberOfTruncatedChars, 0);
|
110
|
+
const truncatedTestName = inlineTestName.slice(numberOfTruncatedChars);
|
111
|
+
const shouldHighlightDots = startPatternIndex <= numberOfTruncatedChars;
|
112
|
+
|
113
|
+
if (shouldHighlightDots) {
|
114
|
+
return colorize(TRIMMING_DOTS + truncatedTestName, 0, end + TRIMMING_DOTS.length);
|
115
|
+
}
|
116
|
+
|
117
|
+
const start = startPatternIndex - numberOfTruncatedChars;
|
118
|
+
return colorize(TRIMMING_DOTS + truncatedTestName, start + TRIMMING_DOTS.length, end + TRIMMING_DOTS.length);
|
119
|
+
};
|
120
|
+
export const removeTrimmingDots = value => {
|
121
|
+
if (value.startsWith(TRIMMING_DOTS)) {
|
122
|
+
return value.slice(TRIMMING_DOTS.length);
|
123
|
+
}
|
124
|
+
|
125
|
+
return value;
|
126
|
+
};
|
@@ -0,0 +1,49 @@
|
|
1
|
+
import { Prompt } from 'jest-watcher';
|
2
|
+
import TestNamePatternPrompt from "./prompt.js";
|
3
|
+
export default class TestNamePlugin {
|
4
|
+
constructor({
|
5
|
+
stdin,
|
6
|
+
stdout,
|
7
|
+
config = {}
|
8
|
+
}) {
|
9
|
+
this._stdin = stdin;
|
10
|
+
this._stdout = stdout;
|
11
|
+
this._prompt = new Prompt();
|
12
|
+
this._testResults = [];
|
13
|
+
this._usageInfo = {
|
14
|
+
key: config.key || 't',
|
15
|
+
prompt: config.prompt || 'filter by a test name regex pattern'
|
16
|
+
};
|
17
|
+
}
|
18
|
+
|
19
|
+
apply(jestHooks) {
|
20
|
+
jestHooks.onTestRunComplete(({
|
21
|
+
testResults
|
22
|
+
}) => {
|
23
|
+
this._testResults = testResults;
|
24
|
+
});
|
25
|
+
}
|
26
|
+
|
27
|
+
onKey(key) {
|
28
|
+
this._prompt.put(key);
|
29
|
+
}
|
30
|
+
|
31
|
+
run(globalConfig, updateConfigAndRun) {
|
32
|
+
const p = new TestNamePatternPrompt(this._stdout, this._prompt);
|
33
|
+
p.updateCachedTestResults(this._testResults);
|
34
|
+
return new Promise((res, rej) => {
|
35
|
+
p.run(testNamePattern => {
|
36
|
+
updateConfigAndRun({
|
37
|
+
mode: 'watch',
|
38
|
+
testNamePattern
|
39
|
+
});
|
40
|
+
res();
|
41
|
+
}, rej);
|
42
|
+
});
|
43
|
+
}
|
44
|
+
|
45
|
+
getUsageInfo() {
|
46
|
+
return this._usageInfo;
|
47
|
+
}
|
48
|
+
|
49
|
+
}
|
@@ -0,0 +1,86 @@
|
|
1
|
+
import chalk from 'chalk';
|
2
|
+
import ansiEscapes from 'ansi-escapes';
|
3
|
+
import { PatternPrompt, printPatternCaret, printRestoredPatternCaret } from 'jest-watcher';
|
4
|
+
import { escapeStrForRegex } from 'jest-regex-util';
|
5
|
+
import scroll from "../lib/scroll.js";
|
6
|
+
import { formatTestNameByPattern, getTerminalWidth, removeTrimmingDots } from "../lib/utils.js";
|
7
|
+
import { formatTypeaheadSelection, printMore, printPatternMatches, printStartTyping, printTypeaheadItem } from "../lib/pattern_mode_helpers.js";
|
8
|
+
export default class TestNamePatternPrompt extends PatternPrompt {
|
9
|
+
constructor(pipe, prompt) {
|
10
|
+
super(pipe, prompt);
|
11
|
+
this._entityName = 'tests';
|
12
|
+
this._cachedTestResults = [];
|
13
|
+
this._offset = -1;
|
14
|
+
}
|
15
|
+
|
16
|
+
_onChange(pattern, options) {
|
17
|
+
super._onChange(pattern, options);
|
18
|
+
|
19
|
+
this._offset = options.offset;
|
20
|
+
|
21
|
+
this._printTypeahead(pattern, options);
|
22
|
+
}
|
23
|
+
|
24
|
+
_printTypeahead(pattern, options) {
|
25
|
+
const matchedTests = this._getMatchedTests(pattern);
|
26
|
+
|
27
|
+
const total = matchedTests.length;
|
28
|
+
const pipe = this._pipe;
|
29
|
+
const prompt = this._prompt;
|
30
|
+
printPatternCaret(pattern, pipe);
|
31
|
+
pipe.write(ansiEscapes.cursorLeft);
|
32
|
+
|
33
|
+
if (pattern) {
|
34
|
+
printPatternMatches(total, 'test', pipe, ` from ${chalk.yellow('cached')} test suites`);
|
35
|
+
const width = getTerminalWidth(pipe);
|
36
|
+
const {
|
37
|
+
start,
|
38
|
+
end,
|
39
|
+
index
|
40
|
+
} = scroll(total, options);
|
41
|
+
prompt.setPromptLength(total);
|
42
|
+
matchedTests.slice(start, end).map(name => formatTestNameByPattern(name, pattern, width - 4)).map((item, i) => formatTypeaheadSelection(item, i, index, prompt)).forEach(item => printTypeaheadItem(item, pipe));
|
43
|
+
|
44
|
+
if (total > end) {
|
45
|
+
printMore('test', pipe, total - end);
|
46
|
+
}
|
47
|
+
} else {
|
48
|
+
printStartTyping('test name', pipe);
|
49
|
+
}
|
50
|
+
|
51
|
+
printRestoredPatternCaret(pattern, this._currentUsageRows, pipe);
|
52
|
+
}
|
53
|
+
|
54
|
+
_getMatchedTests(pattern) {
|
55
|
+
let regex;
|
56
|
+
|
57
|
+
try {
|
58
|
+
regex = new RegExp(pattern, 'i');
|
59
|
+
} catch (e) {
|
60
|
+
return [];
|
61
|
+
}
|
62
|
+
|
63
|
+
return this._cachedTestResults.reduce((matchedTests, {
|
64
|
+
testResults
|
65
|
+
}) => {
|
66
|
+
return matchedTests.concat(testResults.filter(({
|
67
|
+
fullName
|
68
|
+
}) => regex.test(fullName)).map(({
|
69
|
+
fullName
|
70
|
+
}) => fullName));
|
71
|
+
}, []);
|
72
|
+
}
|
73
|
+
|
74
|
+
updateCachedTestResults(testResults = []) {
|
75
|
+
this._cachedTestResults = testResults;
|
76
|
+
}
|
77
|
+
|
78
|
+
run(onSuccess, onCancel, options) {
|
79
|
+
super.run(value => {
|
80
|
+
const preparedPattern = escapeStrForRegex(removeTrimmingDots(value));
|
81
|
+
const useExactMatch = this._offset !== -1;
|
82
|
+
onSuccess(useExactMatch ? `^${preparedPattern}$` : preparedPattern);
|
83
|
+
}, onCancel, options);
|
84
|
+
}
|
85
|
+
|
86
|
+
}
|
@@ -0,0 +1 @@
|
|
1
|
+
export {};
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "jest-watch-typeahead",
|
3
|
-
"version": "2.1.
|
3
|
+
"version": "2.1.1",
|
4
4
|
"main": "build/index.js",
|
5
5
|
"exports": {
|
6
6
|
".": "./build/index.js",
|
@@ -26,8 +26,8 @@
|
|
26
26
|
"test": "cross-env NODE_OPTIONS=\"--experimental-vm-modules\" jest",
|
27
27
|
"lint": "eslint .",
|
28
28
|
"prebuild": "rimraf build",
|
29
|
-
"build": "babel --extensions .js,.ts src -d build && rimraf
|
30
|
-
"
|
29
|
+
"build": "babel --extensions .js,.ts src -d build && rimraf 'build/**/*.test.{js,ts},integration' 'build/**/__tests__' build/test_utils",
|
30
|
+
"prepack": "yarn build",
|
31
31
|
"format": "prettier --write \"**/*.js\" \"**/*.md\" \"**/*.ts\"",
|
32
32
|
"typecheck": "yarn tsc -p ."
|
33
33
|
},
|