Is it possible to test a Node CLI tool that is able to read from standard input with

I'm looking for a way to test and verify the different behaviors of stdin.isTTY in my Node CLI tool implementation.

In my Node CLI tool, data can be passed either through the terminal or as command line arguments:

cli.js

#!/usr/bin/env node

const { stdin, exit } = require('process');

const parseCliArgs = (argv) => {
  // parse args...
  return argv;
};

const readFromStdin = async () => {
  stdin.setEncoding('utf-8');

  return new Promise(((resolve, reject) => {
    let data = '';

    stdin.on('readable', () => {
      let chunk;
      while ((chunk = stdin.read()) !== null) {
        data += chunk;
      }
    });

    stdin.on('end', () => {
      resolve(data);
    });

    stdin.on('error', err => reject(err));
  }));
};


const main = async (argv) => {
  let args;

  console.info('isTTY: ', stdin.isTTY);

  if (stdin.isTTY) {
    console.info('Parse arguments');
    args = parseCliArgs(argv);
  } else {
    console.info('Read from stdin');
    args = await readFromStdin();
  }
  console.info(args);
};


main(process.argv)
  .catch((err) => {
    console.error(err);
    exit(1);
  });

The tool functions correctly when used from the terminal by piping data to the CLI script:

$ echo "hello" | ./cli.js
isTTY:  undefined
Read from stdin
hello

Data is also successfully passed as command line arguments:

$ ./cli.js hello
isTTY:  true
Parse arguments
[
  '/usr/local/Cellar/node/13.2.0/bin/node',
  '[my local path]/cli.js',
  'hello'
]

I've written tests to ensure these behaviors are verified:

cli.spec.js

const { execSync } = require('child_process');

// pipe stdio and stdout from child process to node's stdio and stdout
const CHILD_PROCESS_OPTIONS = { stdio: 'inherit' };

describe('cli test', () => {
  it('pipe data to CLI tool', () => {
    const command = `echo "hello" | ${__dirname}/cli.js`;
    execSync(command, CHILD_PROCESS_OPTIONS);
  });

  it('pass data as CLI args', () => {
    const command = `${__dirname}/cli.js "hello"`;
    execSync(command, CHILD_PROCESS_OPTIONS);
  });
});

The pipe data to CLI tool test runs as expected, but the pass data as CLI args test hangs indefinitely. The output shows:

isTTY:  undefined
Read from stdin

This leads me to believe that stdin.isTTY is not being handled correctly, causing the test to hang because the promise from readFromStdin() remains unresolved.


  • How can I resolve the issue with the pass data as CLI args test?
  • Is there a way to mock process.stdin.isTTY in the child process?

Answer №1

If you're facing this issue, it's connected to how Node.js creates child processes. Specifically, it relates to the stdio option supported by the child_process API. There is an open issue in the Node.js core regarding this.

When a child process is spawned in Node.js, there are pipes/streams set up between it and the parent for handling stdio by default. This allows the parent to use child.stdin.write() to send data to the child's stdin and read from the child.stderr and child.stdout streams.

The stdin behavior can be confusing when using the sync child_process APIs because direct writing to the child is not allowed. While the docs suggest passing a Stream reference, I couldn't get it to work using tty.Read/WriteStream.

In my case, setting the options.stdio[0] to inherit solved the problem. Since my parent (test) process hadn't received any input via stdin and had the TTY flag set, the child inherited this and functioned as expected. I initially tried using ignore, but it didn't resolve the issue.

TLDR; You can find my complete test case here, along with the example below. To test a Node.js CLI that detects pipes through isTTY and also supports file arguments, consider the following:

try {
  const stdout = execSync('your-cli.js some-file.txt', {
    stdio: [
      // The child will inherit the process.stdin of
      // its parent process. This inherits isTTY,
      // allowing the child to also have isTTY set to true
      // and parse the "some-file.txt" argument instead of waiting for stdin input
      'inherit',
      // Capture the child's stdout. It will be returned from
      // the call and attached to an exception if one occurs,
      // e.g., error.stdout
      'pipe',
      // Capture the child's stderr. It will be attached
      // as a "stderr" property if an exception is raised
      'pipe'
    ]
  });
  console.log('Child Process stdout:', stdout)
} catch (e) {
  console.log('Error in child. Child stderr:', e.stderr) 
}

Answer №2

Upon further investigation, it appears that the tests were successful only under certain conditions. Specifically, when running the tests from the terminal (such as using npm test), everything seemed to be in order. However, the initial implementation failed to meet expectations. Despite my efforts to troubleshoot using the debugger in my IDE, I have yet to find a resolution. If anyone has an alternate solution that can ensure success across all environments, please share it as a separate response.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Getting around using Material-UI Icons

Is it possible to utilize a Material-UI Icon for navigation using React Router Dom? I attempted the following approach without success: <NavigateBeforeIcon path="/vehicles"></NavigateBeforeIcon> With buttons, I am able to use component={Link ...

Struggling to populate dropdown with values from array of objects

My issue is related to displaying mock data in a dropdown using the SUIR dropdown component. mock.onGet("/slotIds").reply(200, { data: { slotIds: [{ id: 1 }, { id: 2 }, { id: 3 }] } }); I'm fetching and updating state with the data: const ...

"Combining Cloud9 with sails.js and phpMyAdmin for optimal development experience

I'm facing an issue while trying to set up a basic sails.js application in the Cloud9 environment and connecting it to MySQL. Here are the steps I've followed: Created a Cloud9 project Installed sails: npm -g install sails Created the project: ...

Ensuring a DIV remains fixed in place for a specific number of scrolls

I'm looking to create a 'Page section' that remains in place while scrolling for a specific distance and then smoothly transitions to the next section. I've attempted to implement this within the child theme without success... Any sugge ...

The URL specified is currently not reachable through the @heroku-cli/plugin-buildpacks plugin

I keep encountering this error message consistently when trying to deploy my app to Heroku, no matter what I try. I have tried updating node_modules, reinstalling them, making sure cli is installed and running in VS Code. npm ERR! valid-url not accessible ...

Submitting a form within AJAX-powered tabs (using the Twitter Bootstrap framework)

I have encountered an issue while trying to submit a form that is located within tabs. The content of these tabs is generated through AJAX. My problem arises when I submit the form - the page refreshes and loads the "default" tab, causing the PHP function ...

Utilize JavaScript conditions to dynamically apply styles within your web application

I am facing a challenge with managing two separate <style> tags that each contain a large number of styles and media queries. The issue is that one set of styles is intended for desktop users, while the other is meant for mobile users. When both se ...

Enhancing a Pie Chart Dynamically with Ajax using Highcharts

I need assistance with updating the data for the pie chart when a list item is clicked. The issue arises when using dynamic values from $cid in data.php. For example, user_student.cid = 1 works correctly, but if I use user_student.cid = $cid, it doesn&apos ...

Is there a way to undo the click event in Javascript?

Is there a way to revert back to the initial position after clicking? I successfully changed the x icon to a + icon on click, but now I'm struggling to reverse that action. I've tried using MOUSEUP and DBLCLICK events, but they aren't ideal ...

Exploring arrays and objects in handlebars: A closer look at iteration

Database Schema Setup var ItemSchema = mongoose.Schema({ username: { type: String, index: true }, path: { type: String }, originalname: { type: String } }); var Item = module.exports = mongoose.model('Item',ItemSchema, 'itemi ...

Testing MochaJS on a NodeJS server deployed on Heroku

My ExpressJS / NodeJS API has a robust MochaJS test suite that covers object creation and removal from the database. All regression tests are passing in my development environment. However, I want to run these same tests on the staging environment, where ...

Converting a PHP variable to JSON in an AJAX call

At the moment, my approach involves using a jQuery Ajax asynchronous function to repeatedly request a PHP page until a large number of spreadsheet rows are processed. I am uncertain if the way I set the variables to be passed to the requested page is corre ...

What is the best way to incorporate background colors into menu items?

<div class="container"> <div class="row"> <div class="col-lg-3 col-md-3 col-sm-12 fl logo"> <a href="#"><img src="images/main-logo.png" alt="logo" /> </a> ...

Ways to enhance an image by zooming in when the user reaches a designated area on the webpage

I have implemented a feature where an image zooms in to letter L when the user scrolls on the page. However, I want this zoom effect to occur only when the user reaches a specific section of the site, rather than immediately when the image loads. In my exa ...

What is the best way to customize the spacing of grid lines in chartist.js?

I am struggling with chartist.js. I want to increase the spacing between y-axis gridlines by 40px. (Currently set at 36px) I have tried looking for examples, but haven't found any. .ct-grids line { stroke: #fff; opacity: .05; stroke-dasharray: ...

Combine the filter and orderBy components in AngularJS ng-options for a customized select dropdown menu

I am facing a challenge with my AngularJS dropdown that uses ng-options. I want to apply both the filter and orderBy components together in the ng-options section. The filter for the select is functioning correctly, but it seems like the OrderBy component ...

Error encountered in gulpfile in Visual Studio 2015 while trying to remove npm package

I am currently using the del package within my gulpfile for a clean task. Here are the versions of software that I am working with: - Visual Studio 2015 Community - Node.js v2.11.3 - gulp v3.9.0 - del v2.0.2 Below is a snippet from my gulp file: var gul ...

Ways to incorporate a dictionary into your website's content

I am in the process of developing a website for educational purposes while also honing my web programming skills. On this website, I have encountered some complicated terms that may be difficult for users to understand, so I want to implement a tooltip/mod ...

Refreshing the webpage with new data using jQuery after an AJAX request is

I'm experiencing a problem where the DOM is not updating until after the completion of the $.each loop. On my website, I have several div elements that are supposed to turn orange as they are being looped over. However, once the data is sent to the s ...

Is there a way for me to adjust my for loop so that it showcases my dynamic divs in a bootstrap col-md-6 grid layout?

Currently, the JSON data is appended to a wrapper, but the output shows 10 sections with 10 rows instead of having all divs nested inside one section tag and separated into 5 rows. I can see the dynamically created elements when inspecting the page, but th ...