├── .eslintignore
├── .eslintrc.js
├── .github
└── workflows
│ └── main.yml
├── .gitignore
├── .husky
└── pre-commit
├── .npmignore
├── .prettierignore
├── .prettierrc
├── README.md
├── cli-examples
├── __tests__
│ ├── graphic-print.test.js
│ ├── multiple-prompts.test.js
│ ├── prompts.test.js
│ ├── select.test.js
│ └── simple-print.test.js
├── graphic-print.js
├── multiple-prompts.js
├── prompts.js
├── select.js
├── simple-print.js
└── test-utils
│ └── icons.js
├── lib
├── __tests__
│ └── cli-ansi-parser.test.js
├── cli-ansi-parser.js
├── cli-testing-tool.js
└── index.js
├── package-lock.json
└── package.json
/.eslintignore:
--------------------------------------------------------------------------------
1 | dist/
2 | old-code/
3 | dev/
4 | node_modules
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | es6: true,
4 | node: true,
5 | jest: true
6 | },
7 | extends: ['google', 'prettier'],
8 | plugins: ['prettier'],
9 | globals: {
10 | Atomics: 'readonly',
11 | SharedArrayBuffer: 'readonly'
12 | },
13 | parserOptions: {
14 | ecmaVersion: 2018
15 | },
16 | rules: {
17 | 'prettier/prettier': ['error'],
18 | 'comma-dangle': 0,
19 | 'require-jsdoc': 0,
20 | 'arrow-parens': 0,
21 | 'guard-for-in': 0,
22 | 'valid-jsdoc': 0,
23 | 'no-undef': 'error',
24 | 'operator-linebreak': [
25 | 'error',
26 | 'after',
27 | {
28 | overrides: { '?': 'ignore', ':': 'ignore', '+': 'ignore' }
29 | }
30 | ],
31 | indent: [
32 | 'error',
33 | 2,
34 | {
35 | CallExpression: { arguments: 'first' },
36 | ignoredNodes: [
37 | 'CallExpression > CallExpression',
38 | 'CallExpression > MemberExpression'
39 | ],
40 | SwitchCase: 1
41 | }
42 | ],
43 | 'max-len': ['error', { code: 80, ignoreComments: true }]
44 | }
45 | };
46 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | lint:
7 | runs-on: ${{ matrix.os }}
8 |
9 | strategy:
10 | matrix:
11 | node-version: [12.x]
12 | os: ['macos-latest']
13 |
14 | steps:
15 | - uses: actions/checkout@v2
16 | - name: Use Node.js ${{ matrix.node-version }}
17 | uses: actions/setup-node@v1
18 | with:
19 | node-version: ${{ matrix.node-version }}
20 | - run: npm ci
21 | - run: npm run eslint
22 |
23 | test:
24 | runs-on: ${{ matrix.os }}
25 |
26 | strategy:
27 | matrix:
28 | node-version: [12.x, 15.x]
29 | os: ['windows-latest', 'ubuntu-latest']
30 |
31 | steps:
32 | - uses: actions/checkout@v2
33 | - name: Use Node.js ${{ matrix.node-version }}
34 | uses: actions/setup-node@v1
35 | with:
36 | node-version: ${{ matrix.node-version }}
37 | - run: npm ci
38 | - run: npm test
39 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .vscode
3 | playground.js
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npm run prettier
5 | npm run eslint:fix
6 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | cli-examples
2 | node_modules
3 | tests
4 | .prettierrc
5 | .prettierignore
6 | .husky
7 | .github
8 | .eslintrc.js
9 | .eslintignore
10 | .vscode
11 | playground.js
12 | lib/__tests__/
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | dist/
2 | old-code/
3 | dev/
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": true,
3 | "singleQuote": true,
4 | "printWidth": 80,
5 | "trailingComma": "none"
6 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CLI Testing Tool
2 |
3 | A testing library that allows you to test input and outputs of your CLI command.
4 |
5 | *Note: This is WIP but it should be ready enough for most common CLI use-cases I can think of*
6 |
7 | ## Installation
8 |
9 | With NPM:
10 | ```sh
11 | npm install --save-dev cli-testing-tool
12 | ```
13 |
14 | With Yarn:
15 | ```sh
16 | yarn add --dev cli-testing-tool
17 | ```
18 |
19 | ## Examples
20 |
21 | Check out [Interactive Examples on Stackblitz](https://stackblitz.com/edit/node-kfod5b?file=examples%2Fprompts%2Fprompts.test.js)
22 |
23 | ### Testing Colored Terminal Text
24 |
25 | Check out [this example of StackBlitz](https://stackblitz.com/edit/node-kfod5b?file=examples%2Fgraphic-hello-world%2Fgraphic-hello-world.test.js)
26 |
27 | ```js
28 | // colored-greeting.test.js
29 | const { createCommandInterface } = require('cli-testing-tool');
30 |
31 | test('should print colored greetings', async () => {
32 | const commandInterface = createCommandInterface('node ./graphic-print.js', {
33 | cwd: __dirname, // considering, the test file is in the same directory as the cli file
34 | });
35 | await commandInterface.type('Saurabh\n');
36 | const terminal = await commandInterface.getOutput();
37 |
38 | // ANSI Escape codes are tokenized into readable text token in tokenizedOutput
39 | // Helpful when libraries like inquirer or prompts add ansi-escape codes.
40 | expect(terminal.tokenizedOutput).toBe(
41 | "What's your name?Hi, [BOLD_START][RED_START]Saurabh[COLOR_END][BOLD_END]!"
42 | );
43 |
44 | // ANSI Escape codes are not tokenized.
45 | expect(terminal.rawOutput).toBe(
46 | `What's your name?Hi, \x1B[1m\x1B[31mSaurabh\x1B[39m\x1B[22m!`
47 | );
48 |
49 | // ANSI Escape codes are removed
50 | expect(terminal.stringOutput).toBe(`What's your name?Hi, Saurabh!`);
51 | });
52 |
53 | ```
54 |
55 |
56 | Code of the CLI that we're testing in above snippet
57 |
58 | ```js
59 | // colored-greeting.js
60 | const readline = require('readline').createInterface({
61 | input: process.stdin,
62 | output: process.stdout
63 | });
64 |
65 | const bold = (str) => `\x1b[1m${str}\x1b[22m`;
66 | const red = (str) => `\x1b[31m${str}\x1b[39m`;
67 |
68 | readline.question(`What's your name?`, (name) => {
69 | console.log(`Hi, ${bold(red('Saurabh'))}!`);
70 | readline.close();
71 | });
72 |
73 | ```
74 |
75 |
76 |
77 |
78 |
79 | ## Options
80 |
81 | You can pass options as 2nd param to `createCommandInterface`.
82 |
83 | The default options are:
84 | ```js
85 | const defaultOptions = {
86 | typeDelay: 100, // number. delay between each `.type()` call
87 | logData: false, // boolean. if true, logs the command data on terminal
88 | logError: true, // boolean. if false, won't add command errors on terminal
89 | cwd: process.cwd(), // string. working directory from where your simulated command is executed
90 | env: undefined // object | undefined. environment variables object if there are any
91 | };
92 | ```
93 |
94 |
95 | ## Terminal Text Parsing Support Checklist
96 | Refer to [Full List of Ansi Escape Codes](https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797) that need to be handled.
97 | - [x] Normal text without ansi escape codes
98 | - [x] Colored text
99 | - [x] Cursor movement (Basic Support. Not tested)
100 | - [x] Erase Line/Screen Clear (Basic Support. Not tested)
101 | - [ ] Screen Modes (No Support)
102 | - [ ] Private Modes (No Support)
103 | - [ ] Multiple Arguments (No Support. Difficult to support this)
104 |
105 |
106 |
107 | ----
108 |
109 | Big Shoutout to
110 | - [@fnky](https://github.com/fnky) for the [list of all ansi escape codes](https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797).
111 |
112 | - [@netzkolchose](https://github.com/netzkolchose) for [node-ansiterminal](https://github.com/netzkolchose/node-ansiterminal) library.
113 |
--------------------------------------------------------------------------------
/cli-examples/__tests__/graphic-print.test.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const { createCommandInterface } = require('../../lib/index');
3 |
4 | test('should print greetings', async () => {
5 | const commandInterface = createCommandInterface('node ./graphic-print.js', {
6 | cwd: path.join(__dirname, '..')
7 | });
8 | await commandInterface.type('Saurabh\n');
9 | const terminal = await commandInterface.getOutput();
10 |
11 | // ANSI Escape codes are tokenized into readable text token in tokenizedOutput
12 | // Helpful when libraries like inquirer or prompts add ansi-escape codes.
13 | expect(terminal.tokenizedOutput).toBe(
14 | "What's your name?Hi, [BOLD_START][RED_START]Saurabh[COLOR_END][BOLD_END]!"
15 | );
16 |
17 | // ANSI Escape codes are not tokenized.
18 | expect(terminal.rawOutput).toBe(
19 | `What's your name?Hi, \x1B[1m\x1B[31mSaurabh\x1B[39m\x1B[22m!`
20 | );
21 |
22 | // ANSI Escape codes are removed
23 | expect(terminal.stringOutput).toBe(`What's your name?Hi, Saurabh!`);
24 | });
25 |
--------------------------------------------------------------------------------
/cli-examples/__tests__/multiple-prompts.test.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const { createCommandInterface } = require('../../lib');
3 | const { CHECK_MARK, THREE_DOTS } = require('../test-utils/icons');
4 |
5 | test('should pass', async () => {
6 | const commandInterface = createCommandInterface(
7 | 'node ./multiple-prompts.js',
8 | {
9 | cwd: path.join(__dirname, '..')
10 | }
11 | );
12 | await commandInterface.type('19\n');
13 | await commandInterface.type('saurabh\n');
14 | const terminal = await commandInterface.getOutput();
15 | expect(terminal.stringOutput).toBe(
16 | [
17 | `${CHECK_MARK} How old are you? ${THREE_DOTS} 19`,
18 | '{ value: 19 }',
19 | `${CHECK_MARK} What is your name? ${THREE_DOTS} saurabh`,
20 | "{ value: 'saurabh' }"
21 | ].join('\n')
22 | );
23 | });
24 |
--------------------------------------------------------------------------------
/cli-examples/__tests__/prompts.test.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const { createCommandInterface } = require('../../lib');
3 | const {
4 | FORWARD_ARROW,
5 | CHECK_MARK,
6 | THREE_DOTS
7 | } = require('../test-utils/icons');
8 |
9 | test('should block 12 year old', async () => {
10 | const commandInterface = createCommandInterface('node ./prompts.js', {
11 | cwd: path.join(__dirname, '..')
12 | });
13 | await commandInterface.type('12\n');
14 | const terminal = await commandInterface.getOutput();
15 | expect(terminal.stringOutput.replace(/ /g, '')).toBe(
16 | // eslint-disable-next-line max-len
17 | `? How old are you? ${FORWARD_ARROW} 12\n${FORWARD_ARROW} Nightclub is 18+ only`
18 | );
19 | });
20 |
21 | test('should allow 20 year old', async () => {
22 | const commandInterface = createCommandInterface('node ./prompts.js', {
23 | cwd: path.join(__dirname, '..')
24 | });
25 | await commandInterface.type('20\n');
26 | const terminal = await commandInterface.getOutput();
27 | expect(terminal.stringOutput).toBe(
28 | `${CHECK_MARK} How old are you? ${THREE_DOTS} 20\n{ value: 20 }`
29 | );
30 | });
31 |
--------------------------------------------------------------------------------
/cli-examples/__tests__/select.test.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const { createCommandInterface } = require('../../lib');
3 | const {
4 | FORWARD_ARROW,
5 | THREE_DOTS,
6 | SELECT_ICON
7 | } = require('../test-utils/icons');
8 |
9 | test('select', async () => {
10 | const commandInterface = createCommandInterface('node ./select.js', {
11 | cwd: path.join(__dirname, '..'),
12 | typeDelay: 500
13 | });
14 | const terminalBeforeDownArrow = await commandInterface.getOutput();
15 | expect(terminalBeforeDownArrow.stringOutput).toMatch(
16 | // eslint-disable-next-line max-len
17 | `? test:${THREE_DOTS} \n? test:${FORWARD_ARROW} \n${SELECT_ICON}test 1\ntest 2`
18 | );
19 | expect(terminalBeforeDownArrow.stringOutput).toMatch('');
20 | // move to next item
21 | await commandInterface.keys.arrowDown();
22 | const terminalAfterDownArrow = await commandInterface.getOutput();
23 | expect(terminalAfterDownArrow.stringOutput).toMatch(
24 | // eslint-disable-next-line max-len
25 | `? test:${THREE_DOTS} \n? test:${FORWARD_ARROW} \n${SELECT_ICON}test 1\n? test:${FORWARD_ARROW} \ntest 1\n${SELECT_ICON}test 2`
26 | );
27 | expect(1).toBe(1);
28 | });
29 |
--------------------------------------------------------------------------------
/cli-examples/__tests__/simple-print.test.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const { createCommandInterface } = require('../../lib');
3 |
4 | test('should pass', async () => {
5 | const commandInterface = createCommandInterface('node ./simple-print.js', {
6 | cwd: path.join(__dirname, '..')
7 | });
8 | await commandInterface.type('Saurabh\n');
9 | await commandInterface.type('22\n');
10 | const terminal = await commandInterface.getOutput();
11 |
12 | expect(terminal.stringOutput).toBe(
13 | "What's your name?Hi, Saurabh!\nWhat's your age?So you are 22!"
14 | );
15 | });
16 |
--------------------------------------------------------------------------------
/cli-examples/graphic-print.js:
--------------------------------------------------------------------------------
1 | const readline = require('readline').createInterface({
2 | input: process.stdin,
3 | output: process.stdout
4 | });
5 |
6 | const bold = (str) => `\x1b[1m${str}\x1b[22m`;
7 | const red = (str) => `\x1b[31m${str}\x1b[39m`;
8 |
9 | readline.question(`What's your name?`, (name) => {
10 | console.log(`Hi, ${bold(red('Saurabh'))}!`);
11 | readline.close();
12 | });
13 |
--------------------------------------------------------------------------------
/cli-examples/multiple-prompts.js:
--------------------------------------------------------------------------------
1 | const prompts = require('prompts');
2 |
3 | (async () => {
4 | const response1 = await prompts({
5 | type: 'number',
6 | name: 'value',
7 | message: 'How old are you?',
8 | validate: (value) => (value < 18 ? `Nightclub is 18+ only` : true)
9 | });
10 |
11 | console.log(response1); // => { value: 24 }
12 |
13 | const response2 = await prompts({
14 | type: 'text',
15 | name: 'value',
16 | message: 'What is your name?'
17 | });
18 |
19 | console.log(response2);
20 | })();
21 |
--------------------------------------------------------------------------------
/cli-examples/prompts.js:
--------------------------------------------------------------------------------
1 | const prompts = require('prompts');
2 |
3 | (async () => {
4 | const response = await prompts({
5 | type: 'number',
6 | name: 'value',
7 | message: 'How old are you?',
8 | validate: (value) => (value < 18 ? `Nightclub is 18+ only` : true)
9 | });
10 |
11 | console.log(response); // => { value: 24 }
12 | })();
13 |
--------------------------------------------------------------------------------
/cli-examples/select.js:
--------------------------------------------------------------------------------
1 | const prompts = require('prompts');
2 |
3 | const questions = [
4 | {
5 | type: 'autocomplete',
6 | message: `test: `,
7 | name: 'selectedProject',
8 | choices: [
9 | {
10 | title: 'test 1',
11 | value: 'test1'
12 | },
13 | {
14 | title: 'test 2',
15 | value: 'test2'
16 | }
17 | ],
18 | limit: 4
19 | }
20 | ];
21 |
22 | (async () => {
23 | const { selectedProject } = await prompts(questions);
24 | console.log(selectedProject);
25 | })();
26 |
--------------------------------------------------------------------------------
/cli-examples/simple-print.js:
--------------------------------------------------------------------------------
1 | const readline = require('readline').createInterface({
2 | input: process.stdin,
3 | output: process.stdout
4 | });
5 |
6 | readline.question(`What's your name?`, (name) => {
7 | console.log(`Hi, ${name}!`);
8 | readline.question(`What's your age?`, (age) => {
9 | console.log(`So you are ${age}!`);
10 | readline.close();
11 | });
12 | });
13 |
--------------------------------------------------------------------------------
/cli-examples/test-utils/icons.js:
--------------------------------------------------------------------------------
1 | // Windows is just weird.
2 | const win = process.platform === 'win32';
3 |
4 | module.exports = {
5 | FORWARD_ARROW: win ? '»' : '›',
6 | CHECK_MARK: win ? '√' : '✔',
7 | THREE_DOTS: win ? '...' : '…',
8 | SELECT_ICON: win ? '>' : '❯'
9 | };
10 |
--------------------------------------------------------------------------------
/lib/__tests__/cli-ansi-parser.test.js:
--------------------------------------------------------------------------------
1 | const { parseOutput } = require('../cli-ansi-parser');
2 |
3 | describe('parseOutput', () => {
4 | test('should tokenize cursor move escape codes', () => {
5 | const cursorMove = '\x1b[10A\x1b[12B\x1b[34C\x1b[56D';
6 | const { tokenizedOutput: cursorMoveOutput } = parseOutput(cursorMove);
7 | expect(cursorMoveOutput).toBe(
8 | '[CURSOR_UP_10][CURSOR_DOWN_12][CURSOR_RIGHT_34][CURSOR_LEFT_56]'
9 | );
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/lib/cli-ansi-parser.js:
--------------------------------------------------------------------------------
1 | const AnsiTerminal = require('node-ansiterminal').AnsiTerminal;
2 | const AnsiParser = require('node-ansiparser');
3 |
4 | // Big shoutout to https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797 for super cool reference to all ansi codes
5 | const ESC = '(?:\\x1[bB])|(?:\\u001b)\\[';
6 | const ESC_NO_BRACKET = ESC.replace('\\[', '');
7 | const STRING_ESC = '\x1b[';
8 | const ANY_NUMBER = '(\\d+)';
9 |
10 | const ANSI_CHARACTER_MAPS = {
11 | // Erase
12 | CLEAR_SCREEN: `${ESC}J`,
13 | CLEAR_CURSOR_TO_END_SCREEN: `${ESC}0J`,
14 | CLEAR_CURSOR_TO_START_SCREEN: `${ESC}1J`,
15 | CLEAR_ENTIRE_SCREEN: `${ESC}2J`,
16 | CLEAR_LINE: `${ESC}K`,
17 | CLEAR_CURSOR_TO_END_LINE: `${ESC}0K`,
18 | CLEAR_CURSOR_TO_START_LINE: `${ESC}1K`,
19 | CLEAR_ENTIRE_LINE: `${ESC}2K`,
20 |
21 | // Cursor
22 | CURSOR_TO_HOME: `${ESC}${ANY_NUMBER};${ANY_NUMBER}H`,
23 | CURSOR_TO_END: `${ESC}${ANY_NUMBER};${ANY_NUMBER}f`,
24 | CURSOR_UP: `${ESC}${ANY_NUMBER}A`,
25 | CURSOR_DOWN: `${ESC}${ANY_NUMBER}B`,
26 | CURSOR_RIGHT: `${ESC}${ANY_NUMBER}C`,
27 | CURSOR_LEFT: `${ESC}${ANY_NUMBER}D`,
28 | CURSOR_UP_FROM_BEGINNING: `${ESC}${ANY_NUMBER}E`,
29 | CURSOR_DOWN_FROM_BEGINNING: `${ESC}${ANY_NUMBER}F`,
30 | CURSOR_TO_COL: `${ESC}${ANY_NUMBER}G`,
31 | CURSOR_STORE: `${ESC_NO_BRACKET}7`,
32 | CURSOR_RESTORE: `${ESC_NO_BRACKET}8`,
33 | CURSOR_VISIBLE: `${ESC}\\?25h`,
34 | CURSOR_INVISIBLE: `${ESC}\\?25l`,
35 | CURSOR_SAVE: `${ESC}s`,
36 | CURSOR_RESET: `${ESC}u`,
37 |
38 | // Colors
39 | BLACK_START: `${ESC}30m`,
40 | RED_START: `${ESC}31m`,
41 | GREEN_START: `${ESC}32m`,
42 | YELLOW_START: `${ESC}33m`,
43 | BLUE_START: `${ESC}34m`,
44 | MAGENTA_START: `${ESC}35m`,
45 | CYAN_START: `${ESC}36m`,
46 | WHITE_START: `${ESC}37m`,
47 | COLOR_END: `${ESC}39m`,
48 | GREY_START: `${ESC}90m`,
49 | BLACK_BG_START: `${ESC}40m`,
50 | RED_BG_START: `${ESC}41m`,
51 | GREEN_BG_START: `${ESC}42m`,
52 | YELLOW_BG_START: `${ESC}43m`,
53 | BLUE_BG_START: `${ESC}44m`,
54 | MAGENTA_BG_START: `${ESC}45m`,
55 | CYAN_BG_START: `${ESC}46m`,
56 | WHITE_BG_START: `${ESC}47m`,
57 | BG_END: `${ESC}49m`,
58 |
59 | // Graphics
60 | BOLD_START: `${ESC}1m`,
61 | BOLD_END: `${ESC}22m`,
62 | FAINT_START: `${ESC}2m`,
63 | FAINT_END: `${ESC}22m`,
64 | ITALIC_START: `${ESC}3m`,
65 | ITALIC_END: `${ESC}23m`,
66 | UNDERLINE_START: `${ESC}4m`,
67 | UNDERLINE_END: `${ESC}24m`,
68 | BLINKING_START: `${ESC}5m`,
69 | BLINKING_END: `${ESC}25m`,
70 | INVERSE_START: `${ESC}7m`,
71 | INVERSE_END: `${ESC}27m`,
72 | HIDDEN_START: `${ESC}8m`,
73 | HIDDEN_END: `${ESC}28m`,
74 | STRIKE_START: `${ESC}9m`,
75 | STRIKE_END: `${ESC}29m`
76 |
77 | // @TODO: Screen Modes
78 | };
79 |
80 | const parseOutput = (output) => {
81 | const tokenizeOutput = () => {
82 | let out = output;
83 | for (const [
84 | ESCAPE_CHARACTER_NAME,
85 | ESCAPE_CHARACTER_REGEX
86 | ] of Object.entries(ANSI_CHARACTER_MAPS)) {
87 | out = out.replace(
88 | new RegExp(`${ESCAPE_CHARACTER_REGEX}`, 'g'),
89 | (...args) => {
90 | const hasCapturedElement = args.length > 3;
91 | const firstCapture = args[1];
92 | return `[${ESCAPE_CHARACTER_NAME}${
93 | hasCapturedElement ? `_${firstCapture}` : ''
94 | }]`;
95 | }
96 | );
97 | }
98 |
99 | return out;
100 | };
101 |
102 | const finalString = () => {
103 | const terminal = new AnsiTerminal(80, 25, 500);
104 | const terminalParser = new AnsiParser(terminal);
105 | terminalParser.parse(output);
106 | const trimmedOutput = terminal
107 | .toString()
108 | .trim()
109 | .replace(/[ \t]{2,}/g, '');
110 | return trimmedOutput;
111 | };
112 |
113 | return {
114 | rawOutput: output,
115 | tokenizedOutput: tokenizeOutput(),
116 | stringOutput: finalString()
117 | };
118 | };
119 |
120 | module.exports = { parseOutput, STRING_ESC, ANSI_CHARACTER_MAPS };
121 |
--------------------------------------------------------------------------------
/lib/cli-testing-tool.js:
--------------------------------------------------------------------------------
1 | const { spawn } = require('child_process');
2 | const { parseOutput, STRING_ESC } = require('./cli-ansi-parser');
3 |
4 | const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
5 |
6 | // types
7 | /**
8 | * @typedef {({
9 | * stringOutput: string,
10 | * tokenizedOutput: string,
11 | * rawOutput: string
12 | * })} ParsedOutput
13 | *
14 | * @typedef {({
15 | * typeDelay: number,
16 | * logData: boolean,
17 | * logError: boolean,
18 | * cwd: string,
19 | * env: any
20 | * })} Options
21 | */
22 |
23 | /** @type {Options} defaultOptions */
24 | const defaultOptions = {
25 | typeDelay: 100,
26 | logData: false,
27 | logError: true,
28 | cwd: process.cwd(),
29 | env: undefined
30 | };
31 |
32 | /**
33 | *
34 | * @param {string} commandString
35 | * @param {Options} userOptions
36 | */
37 | const createCommandInterface = (commandString, userOptions = {}) => {
38 | const options = { ...defaultOptions, ...userOptions };
39 |
40 | const commandArgs = commandString.split(' ');
41 |
42 | let outputs = '';
43 | let isFinishTypingCalled = false;
44 |
45 | const initCommandListeners = () => {
46 | outputs = '';
47 | isFinishTypingCalled = false;
48 | const commandInterface = spawn(commandArgs[0], commandArgs.slice(1), {
49 | detached: true,
50 | stdio: 'pipe',
51 | cwd: options.cwd,
52 | env: options.env
53 | });
54 |
55 | commandInterface.stdout.on('data', (data) => {
56 | if (options.logData) {
57 | console.log(data.toString());
58 | }
59 | outputs += data.toString();
60 | });
61 |
62 | commandInterface.stderr.on('data', (error) => {
63 | if (options.logData) {
64 | console.error(error.toString());
65 | }
66 | outputs += error.toString();
67 | });
68 |
69 | commandInterface.on('error', (error) => {
70 | throw error;
71 | });
72 |
73 | return commandInterface;
74 | };
75 |
76 | let command = initCommandListeners();
77 |
78 | const type = async (text) => {
79 | if (isFinishTypingCalled) {
80 | command = initCommandListeners();
81 | }
82 |
83 | await wait(options.typeDelay);
84 |
85 | return new Promise((resolve) => {
86 | command.stdin.write(`${text}`, () => {
87 | resolve();
88 | });
89 | });
90 | };
91 |
92 | const keys = {
93 | enter: () => type('\n'),
94 | arrowDown: () => type(`${STRING_ESC}1B`),
95 | arrowUp: () => type(`${STRING_ESC}1A`)
96 | };
97 |
98 | /**
99 | * @returns {Promise}
100 | */
101 | const getOutput = () => {
102 | if (!isFinishTypingCalled) {
103 | finishTyping();
104 | }
105 | return new Promise((resolve) => {
106 | command.stdout.on('end', () => {
107 | return resolve(parseOutput(outputs.trim()));
108 | });
109 | });
110 | };
111 |
112 | const finishTyping = () => {
113 | isFinishTypingCalled = true;
114 | command.stdin.end();
115 | };
116 |
117 | return {
118 | type,
119 | finishTyping,
120 | getOutput,
121 | command,
122 | keys
123 | };
124 | };
125 |
126 | module.exports = {
127 | createCommandInterface,
128 | parseOutput
129 | };
130 |
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./cli-testing-tool');
2 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "cli-testing-tool",
3 | "version": "0.3.0",
4 | "description": "",
5 | "main": "lib/index.js",
6 | "directories": {
7 | "lib": "lib"
8 | },
9 | "scripts": {
10 | "test": "jest",
11 | "eslint": "eslint cli-examples lib",
12 | "eslint:fix": "npm run eslint -- --fix",
13 | "prettier": "prettier --write cli-examples lib",
14 | "prepare": "husky install",
15 | "prepublishOnly": "npm run eslint && npm test"
16 | },
17 | "keywords": [],
18 | "author": "",
19 | "license": "MIT",
20 | "devDependencies": {
21 | "eslint": "^7.0.0",
22 | "eslint-config-google": "^0.14.0",
23 | "eslint-config-prettier": "^6.11.0",
24 | "eslint-plugin-prettier": "^3.1.3",
25 | "husky": "^7.0.2",
26 | "jest": "^27.2.0",
27 | "prettier": "^2.0.5",
28 | "prompts": "^2.4.1"
29 | },
30 | "dependencies": {
31 | "node-ansiparser": "^2.2.0",
32 | "node-ansiterminal": "^0.2.1-beta"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------