{"id":28684885,"url":"https://github.com/node-modules/clet","last_synced_at":"2025-06-14T03:07:31.278Z","repository":{"id":38371679,"uuid":"376516697","full_name":"node-modules/clet","owner":"node-modules","description":"Command Line E2E Testing","archived":false,"fork":false,"pushed_at":"2022-12-30T10:21:23.000Z","size":171,"stargazers_count":79,"open_issues_count":5,"forks_count":3,"subscribers_count":16,"default_branch":"master","last_synced_at":"2025-06-09T14:18:29.999Z","etag":null,"topics":["cli","command-line","commander","e2e-testing","nodejs","testing","testing-tools"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","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/node-modules.png","metadata":{"files":{"readme":"README.md","changelog":"History.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2021-06-13T10:55:37.000Z","updated_at":"2025-05-12T19:04:03.000Z","dependencies_parsed_at":"2023-01-31T11:15:25.440Z","dependency_job_id":null,"html_url":"https://github.com/node-modules/clet","commit_stats":null,"previous_names":[],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/node-modules/clet","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/node-modules%2Fclet","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/node-modules%2Fclet/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/node-modules%2Fclet/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/node-modules%2Fclet/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/node-modules","download_url":"https://codeload.github.com/node-modules/clet/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/node-modules%2Fclet/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":259752078,"owners_count":22905972,"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","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":["cli","command-line","commander","e2e-testing","nodejs","testing","testing-tools"],"created_at":"2025-06-14T03:07:28.504Z","updated_at":"2025-06-14T03:07:31.259Z","avatar_url":"https://github.com/node-modules.png","language":"JavaScript","readme":"![CLET - Command Line E2E Testing](https://socialify.git.ci/node-modules/clet/image?description=1\u0026descriptionEditable=_______________%20Command%20Line%20E2E%20Testing%20_______________%20%20%20%20%20%20%20%20%20%20%20%20%20%20%F0%9F%92%AA%20Powerful%20%2B%20%F0%9F%9A%80%20Simplified%20%2B%20%F0%9F%8E%A2%20Modern%20\u0026font=Source%20Code%20Pro\u0026language=1\u0026owner=1\u0026theme=Dark)\n\n# CLET - Command Line E2E Testing\n\n[![NPM Version](https://img.shields.io/npm/v/clet.svg?style=flat-square)](https://npmjs.org/package/clet)\n[![NPM Download](https://img.shields.io/npm/dm/clet.svg?style=flat-square)](https://npmjs.org/package/clet)\n[![NPM Quality](http://npm.packagequality.com/shield/clet.svg?style=flat-square)](http://packagequality.com/#?package=clet)\n[![GitHub Actions CI](https://github.com/node-modules/clet/actions/workflows/nodejs.yml/badge.svg)](https://github.com/node-modules/clet/actions/workflows/nodejs.yml)\n[![Coverage](https://img.shields.io/codecov/c/github/node-modules/clet.svg?style=flat-square)](https://codecov.io/gh/node-modules/clet)\n\n\n**CLET aims to make end-to-end testing for command-line apps as simple as possible.**\n\n- Powerful, stop writing util functions yourself.\n- Simplified, every API is chainable.\n- Modern, ESM first, but not leaving commonjs behind.\n\nInspired by [coffee](https://github.com/node-modules/coffee) and [nixt](https://github.com/vesln/nixt).\n\n\n## How it looks\n\n### Boilerplate \u0026\u0026 prompts\n\n```js\nimport { runner, KEYS } from 'clet';\n\nit('should works with boilerplate', async () =\u003e {\n  await runner()\n    .cwd(tmpDir, { init: true })\n    .spawn('npm init')\n    .stdin(/name:/, 'example') // wait for stdout, then respond\n    .stdin(/version:/, new Array(9).fill(KEYS.ENTER))\n    .stdout(/\"name\": \"example\"/) // validate stdout\n    .notStderr(/npm ERR/)\n    .file('package.json', { name: 'example', version: '1.0.0' }) // validate file\n});\n```\n\n### Command line apps\n\n```js\nimport { runner } from 'clet';\n\nit('should works with command-line apps', async () =\u003e {\n  const baseDir = path.resolve('test/fixtures/example');\n  await runner()\n    .cwd(baseDir)\n    .fork('bin/cli.js', [ '--name=test' ], { execArgv: [ '--no-deprecation' ] })\n    .stdout('this is example bin')\n    .stdout(`cwd=${baseDir}`)\n    .stdout(/argv=\\[\"--name=\\w+\"\\]/)\n    .stderr(/this is a warning/);\n});\n```\n\n### Build tools \u0026\u0026 Long-run servers\n\n```js\nimport { runner } from 'clet';\nimport request from 'supertest';\n\nit('should works with long-run apps', async () =\u003e {\n  await runner()\n    .cwd('test/fixtures/server')\n    .fork('bin/cli.js')\n    .wait('stdout', /server started/)\n    .expect(async () =\u003e {\n      // using supertest\n      return request('http://localhost:3000')\n        .get('/')\n        .query({ name: 'tz' })\n        .expect(200)\n        .expect('hi, tz');\n    })\n    .kill(); // long-run server will not auto exit, so kill it manually after test\n});\n```\n\n### Work with CommonJS\n\n```js\ndescribe('test/commonjs.test.cjs', () =\u003e {\n  it('should support spawn', async () =\u003e {\n    const { runner } = await import('clet');\n    await runner()\n      .spawn('npm -v')\n      .log('result.stdout')\n      .stdout(/\\d+\\.\\d+\\.\\d+/);\n  });\n});\n```\n\n## Installation\n\n```bash\n$ npm i --save clet\n```\n\n## Command\n\n### fork(cmd, args, opts)\n\nExecute a Node.js script as a child process.\n\n```js\nit('should fork', async () =\u003e {\n  await runner()\n    .cwd(fixtures)\n    .fork('example.js', [ '--name=test' ], { execArgv: [ '--no-deprecation' ] })\n    .stdout('this is example bin')\n    .stdout(/argv=\\[\"--name=\\w+\"\\]/)\n    .stdout(/execArgv=\\[\"--no-deprecation\"\\]/)\n    .stderr(/this is a warning/);\n});\n```\n\nOptions:\n\n- `timeout`: {Number} - will kill after timeout.\n- `execArgv`: {Array} - pass to child process's execArgv, default to `process.execArgv`.\n- `cwd`: {String} - working directory, prefer to use `.cwd()` instead of this.\n- `env`: {Object} - prefer to use `.env()` instead of this.\n- `extendEnv`: {Boolean} - whether extend `process.env`, default to true.\n- more detail: https://github.com/sindresorhus/execa#options\n\n### spawn(cmd, args, opts)\n\nExecute a shell script as a child process.\n\n```js\nit('should support spawn', async () =\u003e {\n  await runner()\n    .spawn('node -v')\n    .stdout(/v\\d+\\.\\d+\\.\\d+/);\n});\n```\n\n### cwd(dir, opts)\n\nChange the current working directory.\n\n\u003e Notice: it affects the relative path in `fork()`, `file()`, `mkdir()`, etc.\n\n```js\nit('support cwd()', async () =\u003e {\n  await runner()\n    .cwd(targetDir)\n    .fork(cliPath);\n});\n```\n\nSupport options:\n\n- `init`: delete and create the directory before tests.\n- `clean`: delete the directory after tests.\n\n\u003e Use `trash` instead of `fs.rm` to prevent misoperation.\n\n```js\nit('support cwd() with opts', async () =\u003e {\n  await runner()\n    .cwd(targetDir, { init: true, clean: true })\n    .fork(cliPath)\n    .notFile('should-delete.md')\n    .file('test.md', /# test/);\n});\n```\n\n### env(key, value)\n\nSet environment variables.\n\n\u003e Notice: if you don't want to extend the environment variables, set `opts.extendEnv` to false.\n\n```js\nit('support env', async () =\u003e {\n  await runner()\n    .env('DEBUG', 'CLI')\n    .fork('./example.js', [], { extendEnv: false });\n});\n```\n\n### timeout(ms)\n\nSet a timeout. Your application would receive `SIGTERM` and `SIGKILL` in sequent order.\n\n```js\nit('support timeout', async () =\u003e {\n  await runner()\n    .timeout(5000)\n    .fork('./example.js');\n});\n```\n\n### wait(type, expected)\n\nWait for your expectations to pass. It's useful for testing long-run apps such as build tools or http servers.\n\n- `type`: {String} - support `message` / `stdout` / `stderr` / `close`\n- `expected`: {String|RegExp|Object|Function}\n  - {String}: check whether the specified string is included\n  - {RegExp}: check whether it matches the specified regexp\n  - {Object}: check whether it partially includes the specified JSON\n  - {Function}: check whether it passes the specified function\n\n\u003e Notice: don't forgot to `wait('end')` or `kill()` later.\n\n```js\nit('should wait', async () =\u003e {\n  await runner()\n    .fork('./wait.js')\n    .wait('stdout', /server started/)\n    // .wait('message', { action: 'egg-ready' }) // ipc message\n    .file('logs/web.log')\n    .kill();\n});\n```\n\n### kill()\n\nKill the child process. It's useful for manually ending long-run apps after validation.\n\n\u003e Notice: when kill, exit code may be undefined if the command doesn't hook on signal event.\n\n```js\nit('should kill() manually after test server', async () =\u003e {\n  await runner()\n    .cwd(fixtures)\n    .fork('server.js')\n    .wait('stdout', /server started/)\n    .kill();\n});\n```\n\n### stdin(expected, respond)\n\nResponde to a prompt input.\n\n- `expected`: {String|RegExp} - test if `stdout` includes a string or matches regexp.\n- `respond`: {String|Array} - content to respond. CLET would write each with a delay if an array is set.\n\nYou could use `KEYS.UP` / `KEYS.DOWN` to respond to a prompt that has multiple choices.\n\n```js\nimport { runner, KEYS } from 'clet';\n\nit('should support stdin respond', async () =\u003e {\n  await runner()\n    .cwd(fixtures)\n    .fork('./prompt.js')\n    .stdin(/Name:/, 'tz')\n    .stdin(/Email:/, 'tz@eggjs.com')\n    .stdin(/Gender:/, [ KEYS.DOWN + KEYS.DOWN ])\n    .stdout(/Author: tz \u003ctz@eggjs.com\u003e/)\n    .stdout(/Gender: unknown/)\n    .code(0);\n});\n```\n\n\u003e Tips: type ENTER repeatedly if needed\n\n```js\nit('should works with boilerplate', async () =\u003e {\n  await runner()\n    .cwd(tmpDir, { init: true })\n    .spawn('npm init')\n    .stdin(/name:/, 'example')\n    .stdin(/version:/, new Array(9).fill(KEYS.ENTER)) // don't care about others, just enter\n    .stdout(/\"name\": \"example\"/)\n    .notStderr(/npm ERR/)\n    .file('package.json', { name: 'example', version: '1.0.0' })\n});\n```\n\n---\n\n## Validator\n\n### stdout(expected)\n\nValidate stdout, support `regexp` and `string.includes`.\n\n```js\nit('should support stdout()', async () =\u003e {\n  await runner()\n    .spawn('node -v')\n    .stdout(/v\\d+\\.\\d+\\.\\d+/) // regexp match\n    .stdout(process.version)  // string includes;\n});\n```\n\n### notStdout(unexpected)\n\nThe opposite of `stdout()`.\n\n### stderr(expected)\n\nValidate stdout, support `regexp` and `string.includes`.\n\n```js\nit('should support stderr()', async () =\u003e {\n  await runner()\n    .cwd(fixtures)\n    .fork('example.js')\n    .stderr(/a warning/)\n    .stderr('this is a warning');\n});\n```\n\n### notStderr(unexpected)\n\nThe opposite of `stderr()`.\n\n### code(n)\n\nValidate child process exit code.\n\nNo need to explicitly check if the process exits successfully, use `code(n)` only if you want to check other exit codes.\n\n\u003e Notice: when a process is killed, exit code may be undefined if you don't hook on signal events.\n\n```js\nit('should support code()', async () =\u003e {\n  await runner()\n    .spawn('node --unknown-argv')\n    .code(1);\n});\n```\n\n### file(filePath, expected)\n\nValidate the file.\n\n- `file(filePath)`: check whether the file exists\n- `file(filePath, 'some string')`: check whether the file content includes the specified string\n- `file(filePath, /some regexp/)`: checke whether the file content matches regexp\n- `file(filePath, {})`: check whether the file content partially includes the specified JSON\n\n```js\nit('should support file()', async () =\u003e {\n  await runner()\n    .cwd(tmpDir, { init: true })\n    .spawn('npm init -y')\n    .file('package.json')\n    .file('package.json', /\"name\":/)\n    .file('package.json', { name: 'example', config: { port: 8080 } });\n});\n```\n\n### notFile(filePath, unexpected)\n\nThe opposite of `file()`.\n\n\u003e Notice: `.notFile('not-exist.md', 'abc')` will throw because the file is not existing.\n\n### expect(fn)\n\nValidate with a custom function.\n\n```js\nit('should support expect()', async () =\u003e {\n  await runner()\n    .spawn('node -v')\n    .expect(ctx =\u003e {\n      const { assert, result } = ctx;\n      assert.match(result.stdout, /v\\d+\\.\\d+\\.\\d+/);\n    });\n});\n```\n\n---\n\n## Operation\n\n### log(format, ...keys)\n\nPrint log for debugging. `key` supports dot path such as `result.stdout`.\n\n```js\nit('should support log()', async () =\u003e {\n  await runner()\n    .spawn('node -v')\n    .log('result: %j', 'result')\n    .log('result.stdout')\n    .stdout(/v\\d+\\.\\d+\\.\\d+/);\n});\n```\n\n### tap(fn)\n\nTap a method to the chain sequence.\n\n```js\nit('should support tap()', async () =\u003e {\n  await runner()\n    .spawn('node -v')\n    .tap(async ({ result, assert}) =\u003e {\n      assert(result.stdout, /v\\d+\\.\\d+\\.\\d+/);\n    });\n});\n```\n\n### sleep(ms)\n\n```js\nit('should support sleep()', async () =\u003e {\n  await runner()\n    .fork(cliPath)\n    .sleep(2000)\n    .log('result.stdout');\n});\n```\n\n### shell(cmd, args, opts)\n\nRun a shell script. For example, run `npm install` after boilerplate init.\n\n```js\nit('should support shell', async () =\u003e {\n  await runner()\n    .cwd(tmpDir, { init: true })\n    .spawn('npm init -y')\n    .file('package.json', { name: 'shell', version: '1.0.0' })\n    .shell('npm version minor --no-git-tag-version', { reject: false })\n    .file('package.json', { version: '1.1.0' });\n});\n```\n\nThe output log could validate by `stdout()` and `stderr()` by default, if you don't want this, just pass `{ collectLog: false }`.\n\n\n### mkdir(path)\n\nAct like `mkdir -p`.\n\n```js\nit('should support mkdir', async () =\u003e {\n  await runner()\n    .cwd(tmpDir, { init: true })\n    .mkdir('a/b')\n    .file('a/b')\n    .spawn('npm -v');\n});\n```\n\n### rm(path)\n\nMove a file or a folder to trash (instead of permanently delete it). It doesn't throw if the file or the folder doesn't exist.\n\n```js\nit('should support rm', async () =\u003e {\n  await runner()\n    .cwd(tmpDir, { init: true })\n    .mkdir('a/b')\n    .rm('a/b')\n    .notFile('a/b')\n    .spawn('npm -v');\n});\n```\n\n### writeFile(filePath, content)\n\nWrite content to a file, support JSON and PlainText.\n\n```js\nit('should support writeFile', async () =\u003e {\n  await runner()\n    .cwd(tmpDir, { init: true })\n    .writeFile('test.json', { name: 'writeFile' })\n    .writeFile('test.md', 'this is a test')\n    .file('test.json', /\"name\": \"writeFile\"/)\n    .file('test.md', /this is a test/)\n    .spawn('npm -v');\n});\n```\n\n## Context\n\n```js\n/**\n * @typedef Context\n *\n * @property {Object} result - child process execute result\n * @property {String} result.stdout - child process stdout\n * @property {String} result.stderr - child process stderr\n * @property {Number} result.code - child process exit code\n *\n * @property {execa.ExecaChildProcess} proc - child process instance\n * @property {TestRunner} instance - runner instance\n * @property {String} cwd - child process current workspace directory\n *\n * @property {Object} assert - assert helper\n * @property {Object} utils -  utils helper\n * @property {Object} logger - built-in logger\n */\n```\n\n### assert\n\nExtend Node.js built-in `assert` with some powerful assertions.\n\n```js\n/**\n * assert `actual` matches `expected`\n *  - when `expected` is regexp, assert by `RegExp.test`\n *  - when `expected` is json, assert by `lodash.isMatch`\n *  - when `expected` is string, assert by `String.includes`\n *\n * @param {String|Object} actual - actual string\n * @param {String|RegExp|Object} expected - rule to validate\n */\nfunction matchRule(actual, expected) {}\n\n/**\n * assert `actual` does not match `expected`\n *  - when `expected` is regexp, assert by `RegExp.test`\n *  - when `expected` is json, assert by `lodash.isMatch`\n *  - when `expected` is string, assert by `String.includes`\n *\n * @param {String|Object} actual - actual string\n * @param {String|RegExp|Object} expected - rule to validate\n */\nfunction doesNotMatchRule(actual, expected) {}\n\n/**\n * validate file\n *\n *  - `matchFile('/path/to/file')`: check whether the file exists\n *  - `matchFile('/path/to/file', /\\w+/)`: check whether the file content matches regexp\n *  - `matchFile('/path/to/file', 'usage')`: check whether the file content includes the specified string\n *  - `matchFile('/path/to/file', { version: '1.0.0' })`: checke whether the file content partially includes the specified JSON\n *\n * @param {String} filePath - target path to validate, could be relative path\n * @param {String|RegExp|Object} [expected] - rule to validate\n * @throws {AssertionError}\n */\nasync function matchFile(filePath, expected) {}\n\n/**\n * validate file with opposite rule\n *\n *  - `doesNotMatchFile('/path/to/file')`: check whether the file exists\n *  - `doesNotMatchFile('/path/to/file', /\\w+/)`: check whether the file content does not match regex\n *  - `doesNotMatchFile('/path/to/file', 'usage')`: check whether the file content does not include the specified string\n *  - `doesNotMatchFile('/path/to/file', { version: '1.0.0' })`: checke whether the file content does not partially include the specified JSON\n *\n * @param {String} filePath - target path to validate, could be relative path\n * @param {String|RegExp|Object} [expected] - rule to validate\n * @throws {AssertionError}\n */\nasync function doesNotMatchFile(filePath, expected) {}\n```\n\n### debug(level)\n\nSet level of logger.\n\n```js\nimport { runner, LogLevel } from 'clet';\n\nit('should debug(level)', async () =\u003e {\n  await runner()\n    .debug(LogLevel.DEBUG)\n    // .debug('DEBUG')\n    .spawn('npm -v');\n});\n```\n\n---\n\n## Extendable\n\n### use(fn)\n\nMiddleware, always run before child process chains.\n\n```js\n// middleware.pre -\u003e before -\u003e fork -\u003e running -\u003e after -\u003e end -\u003e middleware.post -\u003e cleanup\n\nit('should support middleware', async () =\u003e {\n  await runner()\n    .use(async (ctx, next) =\u003e {\n      // pre\n      await utils.rm(dir);\n      await utils.mkdir(dir);\n\n      await next();\n\n      // post\n      await utils.rm(dir);\n    })\n    .spawn('npm -v');\n});\n```\n\n### register(Function|Object)\n\nRegister your custom APIs.\n\n```js\nit('should register(fn)', async () =\u003e {\n  await runner()\n    .register(({ ctx }) =\u003e {\n      ctx.cache = {};\n      cache = function(key, value) {\n        this.ctx.cache[key] = value;\n        return this;\n      };\n    })\n    .cache('a', 'b')\n    .tap(ctx =\u003e {\n      console.log(ctx.cache);\n    })\n    .spawn('node', [ '-v' ]);\n});\n```\n\n## Known Issues\n\n**Help Wanted**\n\n- when answer prompt with `inquirer` or `enquirer`, stdout will recieve duplicate output.\n- when print child error log with `.error()`, the log order maybe in disorder.\n\n## License\n\nMIT\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnode-modules%2Fclet","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnode-modules%2Fclet","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnode-modules%2Fclet/lists"}