{"id":26221717,"url":"https://github.com/push-based/node-cli-testing","last_synced_at":"2025-08-30T22:11:29.853Z","repository":{"id":65187966,"uuid":"586080102","full_name":"push-based/node-cli-testing","owner":"push-based","description":"📦 A lib to test node processes and CLI's","archived":false,"fork":false,"pushed_at":"2023-09-21T12:13:25.000Z","size":361,"stargazers_count":3,"open_issues_count":5,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-08-30T22:03:32.124Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/push-based.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":["push-based","BioPhoton"]}},"created_at":"2023-01-06T22:16:02.000Z","updated_at":"2023-12-24T13:10:28.000Z","dependencies_parsed_at":"2025-03-12T16:34:30.740Z","dependency_job_id":"579bf75a-beb8-410a-ae07-0a6bfc51d849","html_url":"https://github.com/push-based/node-cli-testing","commit_stats":null,"previous_names":[],"tags_count":7,"template":false,"template_full_name":null,"purl":"pkg:github/push-based/node-cli-testing","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/push-based%2Fnode-cli-testing","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/push-based%2Fnode-cli-testing/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/push-based%2Fnode-cli-testing/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/push-based%2Fnode-cli-testing/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/push-based","download_url":"https://codeload.github.com/push-based/node-cli-testing/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/push-based%2Fnode-cli-testing/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":272912489,"owners_count":25014077,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-08-30T02:00:09.474Z","response_time":77,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-03-12T16:29:19.936Z","updated_at":"2025-08-30T22:11:29.802Z","avatar_url":"https://github.com/push-based.png","language":"TypeScript","funding_links":["https://github.com/sponsors/push-based","https://github.com/sponsors/BioPhoton"],"categories":[],"sub_categories":[],"readme":"# node-cli-testing\n\n### A e2e-testing library for node CLI and processes including sandbox generation on the fly\n \n[![npm](https://img.shields.io/npm/v/%40push-based%2Fnode-cli-testing.svg)](https://www.npmjs.com/package/%40push-based%2Fnode-cli-testing)\n\n\n## Motivation\n\nTesting node code is easy as long as you can stick to unit tests (testing input and output of a single function).\nWhen it comes to more complex scenarios it get's pretty hard.\nNode processes, file outputs, integration tests, and console output is a hassle and involves many moving gears that are hard to setup an keep stable.\n\nThis is what this library tackles. A smooth out of the box experience for testing node CLIs and processes with unit, integration and e2e tests.\n\n\n## Features\n\n- 🚥 Testing node process output (`stdout`, `stderr`, `exitCode`)\n- 🧠 Handle `.rc.jons` files\n- ⌨️ Simulate keyboard interaction\n- 💬 Test console output\n- 🥸 Initializing a sandbox environment for each test\n- ⚙️ Automatically creating files needed for the test \n- 🧹 Cleanup after tests\n- 🦮 Helpers to check the generated files and folders of a node process\n\n## Install\n\nYou can install the node-cli-testing over `npm` or `yarn` as following:\n\n```text\nnpm install --save @push-based/node-cli-testing\n# or\nyarn add @push-based/node-cli-testing\n```\n\n## Setup\n\nThe `node-cli-testing` lib can be imported as following:\n\n```ts\nimport { CliProject } from '@push-based/node-cli-testing/cli-project';\n\nlet projectSandbox: CliProject;\n\nconst cfg: ProjectConfig = {\n  root: './',\n  bin: 'cli.js'\n};\n\ndescribe('The CLI configuration in default mode', () =\u003e {\n  beforeEach(async () =\u003e {\n    projectSandbox = await CliProjectFactory.create(cfg);\n  });\n  afterEach(async () =\u003e {\n    projectSandbox = await CliProjectFactory.teardown(cfg);\n  });\n\n  it('should work', async () =\u003e {\n    const { exitCode, stdout, stderr } = await projectSandbox.exec();\n  });\n\n});\n```\n\n## Basic Usage of the `CliProject` wrapper\n\n### Setup a basic project and tests\n\nThe `CliProject` class makes it easy to execute a node file and handle it's in and outputs as well as process arguments.\nLet's set up a simple test for a CLI:\n\n\n```ts\nimport { CliProject, CliProjectFactory } from '@push-based/node-cli-testing/cli-project';\n\nlet projectSandbox: CliProject;\n\nconst cfg: ProjectConfig = {\n  root: './', // the directory in which the test should take place\n  bin: 'cli.js' // the bin file the gets executed as node process\n};\n\ndescribe('The CLI', () =\u003e {\n  beforeEach(async () =\u003e {\n    projectSandbox = await CliProjectFactory.create(cfg);\n  });\n  afterEach(async () =\u003e {\n    projectSandbox = await CliProjectFactory.teardown(cfg);\n  });\n\n  it('should work', async () =\u003e {\n    const { exitCode, stdout, stderr } = await projectSandbox.exec();\n    expect(stdin).toContain('some console output');\n    expect(stderr).toBe('');\n    expect(exitCode).toBe(0);\n  });\n\n});\n```\n\n### Use setup helper\n\nAs is it kind of repetitive to set up `beforeEach` and `afterEach` this library provides a helper. \nThis comes in handy when there are many different setups in one describe block. \n\nThe helper consumes a configuration for the project and handles setup and teardown internally.\n\n```ts\nimport { ProjectConfig, withProject } from '@push-based/node-cli-testing/cli-project';\n\nconst cfg: ProjectConfig = {\n  root: './',\n  bin: 'cli.js' \n};\nconst cfg2: ProjectConfig = {\n  root: './other/',\n  bin: 'cli.js' \n};\n\ndescribe('The CLI', () =\u003e {\n\n  it('should work in version 1', withProject(cfg, async (prj) =\u003e {\n    const { exitCode } = await prj.exec();\n    expect(exitCode).toBe(0);\n   })\n  );\n  \n  it('should work in version 2', withProject(cfg2, async (prj) =\u003e {\n    const { exitCode } = await prj.exec();\n    expect(exitCode).toBe(0);\n   })\n  );\n\n});\n```\n\n### Use process arguments\n\nNode processes can retrieve arguments over `process.argv` to be configurable.\nIn the next snippet we pass arguments to a node process:\n\n\n```typescript\nimport { CliProject } from '@push-based/node-cli-testing/cli-project';\n\n// Set up here.\n// Details see the above example for setup a basic CLI project and test it\n\ndescribe('The CLI', () =\u003e {\n  beforeEach(/* same as in above */);\n  afterEach(/* same as in above */);\n\n  it('should work with params', async () =\u003e {\n    // We can pass proces params as simple object ans it transporms it to standard process param style\n    const { exitCode, stdout, stderr } = await projectSandbox.exec({verbose: true, count: 42, names: ['Srashti', 'Eliran', 'Mike']});\n    \n    expect(stdin).toContain('verbose mode is active');\n    expect(stdin).toContain('count is 42');\n    expect(stdin).toContain('names are Srashti, Eliran, Mike');\n    \n  });\n\n});\n```\n\nThe example above takes an object to define the process args like this:\n```\n{ verbose: false, count: 42, names: ['Srashti', 'Eliran', 'Mike'] }\n```\n\nInternally it converts them to the following string and passes it to the defined process:\n```\n--no-verbose --count=42 --name=Srashti --name=Eliran --name=Mike \n```\n\n### Test keyboard interaction\n\nOften processes running in the console prompt to users and ask for some input.\nThe lib exports some helper constants to interact simulate keyboard interaction.  \n\n```typescript\nimport { CliProject, DOWN, SPACE, ENTER, DECLINE_BOOLEAN } from '@push-based/node-cli-testing/cli-project';\n\ndescribe('The CLI', () =\u003e {\n  beforeEach(/* ... */);\n  afterEach(/* ... */);\n\n  it('should work with params', async () =\u003e {\n    const { stdout } = await projectSandbox.exec({prompt: true}, [DOWN, SPACE, ENTER, DECLINE_BOOLEAN]);\n    \n    expect(stdin).toContain('You selected the first option and hit enter');\n    expect(stdin).toContain('You declined the option to generate a new file');\n  });\n\n});\n```\n\n### Test file interaction\n\n```typescript\nimport { CliProject, CliProjectFactory } from '@push-based/node-cli-testing/cli-project';\n\nlet projectSandbox: CliProject;\n\nconst cfg: ProjectConfig = {\n  root: './',\n  bin: 'cli.js',\n  create: {\n    'fileA.txt': 'Content of file A'\n  },\n  delete: ['fileA.txt', 'fileB.txt']\n};\n\ndescribe('The CLI in a folder structure', () =\u003e {\n  beforeEach(async () =\u003e {\n    projectSandbox = await CliProjectFactory.create(cfg);\n    // files from cfg.create are created here\n    await projectSandbox.setup();\n  });\n  afterEach(async () =\u003e {\n    // files from cfg.delete are deleted here    \n    await projectSandbox.teardown();\n  });\n\n  it('should copy a file', async () =\u003e {\n    const { exitCode, stdout, stderr } = await projectSandbox.exec({source: 'fileA.txt', dest: 'fileB.txt'});\n    expect(stdout).toContain('file copied');\n    \n    const fileAContent = fs.readFileSync('fileA.txt', 'utf8').toString();\n    const fileBContent = fs.readFileSync('fileB.txt', 'utf8').toString();\n    \n    expect(fileAContent).toBe(fileBContent);\n  });\n\n});\n```\n\nThe `CliProject` will create all defined files from cfg.create when `CliProject#setup` is called.\nThe `CliProject` will delete all defined filesNames from cfg.delete when `CliProject#teardown` is called.\n\n### Working with a `.rc.json` file\n\nOften CLIs can be configured over a `.rc.json` file containing some default configuration.\nLet's create a test for this scenario:\n\n```typescript\nimport { CliProject, CliProjectFactory } from '@push-based/node-cli-testing/cli-project';\n\ntype MyRcJson = {\n  count: number\n};\n\nlet projectSandbox: CliProject\u003cMyRcJson\u003e;\n\nconst cfg: ProjectConfig\u003cMyRcJson\u003e = {\n  root: './',\n  bin: 'cli.js',\n  rcFile: {\n    '.rc.json': {\n        count: 42\n    }\n  }\n};\n\ndescribe('The CLI configured over the .rc file', () =\u003e {\n  beforeEach(async () =\u003e {\n    projectSandbox = await CliProjectFactory.create(cfg);\n    await projectSandbox.setup();\n  });\n  afterEach(async () =\u003e { \n    await projectSandbox.teardown();\n  });\n\n  it('should have count of 42', async () =\u003e {\n    const { stdout } = await projectSandbox.exec();\n    expect(stdout).toContain('count is 42');\n  });\n\n});\n```\n\nThe `CliProject` will create the `.rc.json` file when `CliProject#setup` is called.\nThe `CliProject` will delete the `.rc.json` file when `CliProject#setup` is called.\n\nYou can also create multiple rc files at once:\n\n```typescript\nimport { CliProject, CliProjectFactory } from '@push-based/node-cli-testing/cli-project';\n\nconst cfg: ProjectConfig = {\n  root: './',\n  bin: 'cli.js',\n  rcFile: {\n    '.rc.json': {\n        count: 42\n    }\n  }\n};\n\n```\n\n## Advanced Usage\n\n### Creating custom CLI wrapper\n\nLet's extend the `CliProject` class to add custom logic to handle a command:\n\n```typescript\nimport { CliProject, CliProjectFactory, TestResult } from '@push-based/node-cli-testing/cli-project';\n\n\ntype MyRcJson = {\n  count: number\n};\n\nexport class MyCliProject extends CliProject\u003cMyRcJson\u003e {\n  \n  constructor() {\n    super();\n  }\n\n  $myCommand(processParams?: Partial\u003c{count: number}\u003e, userInput?: string[]): Promise\u003cTestResult\u003e {\n    const prcParams: ProcessParams = { _: 'my-command', ...processParams } as ProcessParams;\n    return this.exec(prcParams, userInput);\n  }\n\n}\n```\n\nNext, for better DX let's create a custom factory: \n\n```\nexport class MyCliProjectFactory {\n  static async create(cfg: ProjectConfig\u003cMyRcJson\u003e): Promise\u003cMyCliProject\u003cMyRcJson\u003e\u003e {\n    const prj = new MyCliProject();\n    await prj._setup(cfg);\n    return prj;\n  }\n}\n```\n\nNow we can use it in our tests like this:\n\n```typescript\nlet projectSandbox: MyCliProject;\n\nconst cfg: ProjectConfig\u003cMyRcJson\u003e = {\n  root: './',\n  bin: 'cli.js'\n};\n\ndescribe('The CLI configuration in default mode', () =\u003e {\n  beforeEach(async () =\u003e {\n    projectSandbox = await MyCliProjectFactory.create(cfg);\n  });\n\n  it('should work', async () =\u003e {\n    const { stdout } = await projectSandbox.$myCommand({count: 42});\n    expect(stdout).toContain('count is 42');\n  });\n\n});\n```\n\nThe above test would be equivalent to:\n`node cli.js my-command --count=42`\n\n---\n\nmade with ❤ by [push-based.io](https://www.push-based.io)\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpush-based%2Fnode-cli-testing","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpush-based%2Fnode-cli-testing","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpush-based%2Fnode-cli-testing/lists"}