jest-watch-typeahead 2.1.0 → 2.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,8 @@
1
+ throw new Error(`
2
+ jest-watch-typeahead includes two watch plugins: The filename plugin and the testname plugin.
3
+ Please configure Jest as follows:
4
+ "watchPlugins": [
5
+ "jest-watch-typeahead/filename",
6
+ "jest-watch-typeahead/testname"
7
+ ]
8
+ `);
@@ -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.0",
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 **/*.test.{js,ts},integration build/**/__tests__ build/test_utils",
30
- "prepublish": "yarn build",
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
  },