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.
@@ -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
  },