DEV Community

Cover image for CLI application with the Node.js Readline module (3/3)

CLI application with the Node.js Readline module (3/3)

Node.js version: 22.1.0.

Part one - the basics - is here: https://dev.to/camptocamp-geo/cli-application-with-the-nodejs-readline-module-48ic

Part two - going further - is here: https://dev.to/camptocamp-geo/cli-application-with-the-nodejs-readline-module-23-2ekg

Testing

A program is never complete without tests. In addition to unit tests, which are generally context-free, I tried to do a more global test. More or less an acceptance test for my CLI program. I was faced with two problems:

  • How to read my console.
  • How to simulate writing.

Read

To read what comes out of console.log, or its sub-layer stdout.write, it is by definition not possible to use readline. Otherwise, every output would be an input, which would be problematic. So we have to find a roundabout way. Here are two possibilities:

  1. Add a “debug” mode and associated methods to store the logs somewhere for access in tests. For example, in an object, or in a file, and in this case why not with a second readline instance.
  2. Overload “console.log” or “stdin.write” to redirect written values before continuing execution of the standard function. No need for a “debug” mode.

In my case, I used the second method. With vitest and its spyOn, it was easy and quite elegant, here's an extract:

import { test, describe, vi, beforeAll, expect } from "vitest";
import { stdin } from "node:process";
import { CliLearningCards } from "./cli-learning-cards.js";

const consoleSpy = vi.spyOn(console, "log");
const getLastLog = () => `${consoleSpy.mock.lastCall}`;
/** @returns n previous log */
const getReverseLog = (reverseIndex: number) => {
  const n = consoleSpy.mock.calls.length - reverseIndex;
  return `${consoleSpy.mock.calls[n]}`;
};
...

describe("Cli-learning-card", () => {
  beforeAll(() => {
    consoleSpy.mockClear();
  });
  ...
Enter fullscreen mode Exit fullscreen mode

Simulate writing

In theory, it's simple. Here's a quick summary:

  • stdin is unique per process.
  • In my case, the test process is the same one running my program.
  • I can give stdin as input to my Readline interface.
  • And if I write stdin.emit('keypress', null, {name: 'enter'}), my program receives my “enter” event.

However, in my test, if I create a readline interface, and write rl.write(“a sentence”), it never reaches my program. Worse still, if I add:

// Inside my program 
getRl(): readline.Interface {
  this.rl;
}
Enter fullscreen mode Exit fullscreen mode
// Inside my test
const mySoft = new MySoft();
mySoft.getRl().write(hello\n); // Does nothing :-(
Enter fullscreen mode Exit fullscreen mode

And if I use it to write my test, it doesn't work.
On the other hand, and even less elegantly, if I add a function to my program to write directly:

// Inside my program
write(line: string) {
  this.rl.write(`${line}\n`);
}
Enter fullscreen mode Exit fullscreen mode
// Inside my test
const mySoft = new MySoft();
mySoft.write(hello); // It works !!?
Enter fullscreen mode Exit fullscreen mode

In this case, it works. It's not clean, but I can simulate writing from my test. And I currently have no idea why it works in this case, but not in the previous one. Comments welcome!

Debugging

It's easy to debug your application with node --inspect and a Webkit browser. On the other hand, seeing the results can be complicated if your program regularly cleans the terminal. If you don't understand the results displayed, I advise you:

  • Temporarily comment out your terminal-cleaning method.
  • Or redirect your “console.log” to a file.

Multi stream

Events

As I was using several readline interfaces, I sometimes couldn't understand why my application wasn't reacting to my events. Always remember that each instance has its own listeners, including streams of course.

Close events and stdin/stdout

If several of your streams are linked to stdin, don't forget to handle the stream stop events in each stream. If only one is not managed, the stdin/stdout streams will be cut, and all your readline interfaces will stop. So, if your program is active as long as your interfaces are active, then your program will stop if you forget to handle a stop event for one of your readline interfaces.

CLI and the “I” problematics

In CLI , there's the I for interface. And making a beautiful, modular, responsive, clean interface can take a lot more time than coding the logic of your program. Here are some of the difficulties you'll have to face:

  • It's all text, and only text. But the good news is that any character always takes the same space.
  • It may take two characters in a string to form a visual character.
  • Color is also some characters in a string, but does not affect the length of the printed text.
  • Users can resize their terminal at any time (node.js can handle that with events and stdout.columns, stdout.lines).

For a simple program, it's possible to manage these cases in a reasonable time. If you want to go further, I advise you to turn to external libraries. For example (this list is obviously not intended to be exhaustive):

And many, many more. The aim here was to talk about Readline, which I think gives you a good basis for managing, understanding and using these CLI libraries, and creating your own program.

AWS GenAI LIVE image

How is generative AI increasing efficiency?

Join AWS GenAI LIVE! to find out how gen AI is reshaping productivity, streamlining processes, and driving innovation.

Learn more

Top comments (0)

👋 Kindness is contagious

DEV is better (more customized, reading settings like dark mode etc) when you're signed in!

Okay