@oclif/test 4.0.1-beta.1 → 4.0.1-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,230 +6,33 @@ test helpers for oclif CLIs
6
6
  [![Downloads/week](https://img.shields.io/npm/dw/@oclif/test.svg)](https://npmjs.org/package/@oclif/test)
7
7
  [![License](https://img.shields.io/npm/l/@oclif/test.svg)](https://github.com/oclif/test/blob/main/package.json)
8
8
 
9
+ ## Migration
10
+
11
+ See the [V4 Migration Guide](./MIGRATION.md) if you are migrating from v3 or older.
12
+
9
13
  ## Usage
10
14
 
11
- `@oclif/test` is an extension of [fancy-test](https://github.com/oclif/fancy-test). Please see the [fancy-test documentation](https://github.com/oclif/fancy-test#fancy-test) for all the features that are available.
12
-
13
- The following are the features that `@oclif/test` adds to `fancy-test`.
14
-
15
- ### `.loadConfig()`
16
-
17
- `.loadConfig()` creates and returns a new [`Config`](https://github.com/oclif/core/blob/main/src/config/config.ts) instance. This instance will be available on the `ctx` variable that's provided in the callback.
18
-
19
- ```typescript
20
- import {join} from 'node:path'
21
- import {expect, test} from '@oclif/test'
22
-
23
- const root = join(__dirname, 'fixtures/test-cli')
24
- test
25
- .loadConfig({root})
26
- .stdout()
27
- .command(['foo:bar'])
28
- .it('should run the command from the given directory', (ctx) => {
29
- expect(ctx.stdout).to.equal('hello world!\n')
30
- expect(ctx.config.root).to.equal(root)
31
- const {name} = ctx.returned as {name: string}
32
- expect(name).to.equal('world')
33
- })
34
- ```
35
-
36
- If you would like to run the same test without using `@oclif/test`:
37
-
38
- ```typescript
39
- import {Config, ux} from '@oclif/core'
40
- import {expect} from 'chai'
41
- import {join} from 'node:path'
42
- import {SinonSandbox, SinonStub, createSandbox} from 'sinon'
43
-
44
- const root = join(__dirname, 'fixtures/test-cli')
45
- describe('non-fancy test', () => {
46
- let sandbox: SinonSandbox
47
- let config: Config
48
- let stdoutStub: SinonStub
49
-
50
- beforeEach(async () => {
51
- sandbox = createSandbox()
52
- stdoutStub = sandbox.stub(ux.write, 'stdout')
53
- config = await Config.load({root})
54
- })
55
-
56
- afterEach(async () => {
57
- sandbox.restore()
58
- })
59
-
60
- it('should run command from the given directory', async () => {
61
- const {name} = await config.runCommand<{name: string}>('foo:bar')
62
- expect(stdoutStub.calledWith('hello world!\n')).to.be.true
63
- expect(config.root).to.equal(root)
64
- expect(name).to.equal('world')
65
- })
66
- })
67
- ```
68
-
69
- ### `.command()`
70
-
71
- `.command()` let's you run a command from your CLI.
72
-
73
- ```typescript
74
- import {expect, test} from '@oclif/test'
75
-
76
- describe('hello world', () => {
77
- test
78
- .stdout()
79
- .command(['hello:world'])
80
- .it('runs hello world cmd', (ctx) => {
81
- expect(ctx.stdout).to.contain('hello world!')
82
- })
83
- })
84
- ```
85
-
86
- For a [single command cli](https://oclif.io/docs/single_command_cli) you would provide `'.'` as the command. For instance:
87
-
88
- ```typescript
89
- import {expect, test} from '@oclif/test'
90
-
91
- describe('hello world', () => {
92
- test
93
- .stdout()
94
- .command(['.'])
95
- .it('runs hello world cmd', (ctx) => {
96
- expect(ctx.stdout).to.contain('hello world!')
97
- })
98
- })
99
- ```
100
-
101
- If you would like to run the same test without using `@oclif/test`:
102
-
103
- ```typescript
104
- import {Config, ux} from '@oclif/core'
105
- import {expect} from 'chai'
106
- import {SinonSandbox, SinonStub, createSandbox} from 'sinon'
107
-
108
- describe('non-fancy test', () => {
109
- let sandbox: SinonSandbox
110
- let config: Config
111
- let stdoutStub: SinonStub
112
-
113
- beforeEach(async () => {
114
- sandbox = createSandbox()
115
- stdoutStub = sandbox.stub(ux.write, 'stdout')
116
- config = await Config.load({root: process.cwd()})
117
- })
118
-
119
- afterEach(async () => {
120
- sandbox.restore()
121
- })
122
-
123
- it('should run command', async () => {
124
- // use '.' for a single command CLI
125
- const {name} = await config.runCommand<{name: string}>('hello:world')
126
- expect(stdoutStub.calledWith('hello world!\n')).to.be.true
127
- expect(name).to.equal('world')
128
- })
129
- })
130
- ```
131
-
132
- ### `.exit()`
133
-
134
- `.exit()` let's you test that a command exited with a certain exit code.
135
-
136
- ```typescript
137
- import {join} from 'node:path'
138
- import {expect, test} from '@oclif/test'
139
-
140
- describe('exit', () => {
141
- test
142
- .loadConfig()
143
- .stdout()
144
- .command(['hello:world', '--code=101'])
145
- .exit(101)
146
- .do((output) => expect(output.stdout).to.equal('exiting with code 101\n'))
147
- .it('should exit with code 101')
148
- })
149
- ```
150
-
151
- If you would like to run the same test without using `@oclif/test`:
152
-
153
- ```typescript
154
- import {Config, Errors, ux} from '@oclif/core'
155
- import {expect} from 'chai'
156
- import {SinonSandbox, createSandbox} from 'sinon'
157
-
158
- describe('non-fancy test', () => {
159
- let sandbox: SinonSandbox
160
- let config: Config
161
-
162
- beforeEach(async () => {
163
- sandbox = createSandbox()
164
- sandbox.stub(ux.write, 'stdout')
165
- config = await Config.load({root: process.cwd()})
166
- })
167
-
168
- afterEach(async () => {
169
- sandbox.restore()
170
- })
171
-
172
- it('should run command from the given directory', async () => {
173
- try {
174
- await config.runCommand('.')
175
- throw new Error('Expected CLIError to be thrown')
176
- } catch (error) {
177
- if (error instanceof Errors.CLIError) {
178
- expect(error.oclif.exit).to.equal(101)
179
- } else {
180
- throw error
181
- }
182
- }
183
- })
184
- })
185
- ```
186
-
187
- ### `.hook()`
188
-
189
- `.hook()` let's you test a hook in your CLI.
190
-
191
- ```typescript
192
- import {join} from 'node:path'
193
-
194
- import {expect, test} from '@oclif/test'
195
-
196
- const root = join(__dirname, 'fixtures/test-cli')
197
-
198
- describe('hooks', () => {
199
- test
200
- .loadConfig({root})
201
- .stdout()
202
- .hook('foo', {argv: ['arg']}, {root})
203
- .do((output) => expect(output.stdout).to.equal('foo hook args: arg\n'))
204
- .it('should run hook')
205
- })
206
- ```
207
-
208
- If you would like to run the same test without using `@oclif/test`:
209
-
210
- ```typescript
211
- import {Config, ux} from '@oclif/core'
212
- import {expect} from 'chai'
213
- import {SinonSandbox, SinonStub, createSandbox} from 'sinon'
214
-
215
- describe('non-fancy test', () => {
216
- let sandbox: SinonSandbox
217
- let config: Config
218
- let stdoutStub: SinonStub
219
-
220
- beforeEach(async () => {
221
- sandbox = createSandbox()
222
- stdoutStub = sandbox.stub(ux.write, 'stdout')
223
- config = await Config.load({root: process.cwd()})
224
- })
225
-
226
- afterEach(async () => {
227
- sandbox.restore()
228
- })
229
-
230
- it('should run hook', async () => {
231
- const {name} = await config.runHook('foo', {argv: ['arg']})
232
- expect(stdoutStub.calledWith('foo hook args: arg\n')).to.be.true
233
- })
234
- })
235
- ```
15
+ `@oclif/test` provides a handful of utilities that make it easy to test your [oclif](https://oclif.io) CLI.
16
+
17
+ ### `captureOutput`
18
+
19
+ `captureOutput` allows you to get the stdout, stderr, return value, and error of the callback you provide it. This makes it possible to assert that certain strings were printed to stdout and stderr or that the callback failed with the expected error or succeeded with the expected result.
20
+
21
+ **Options**
22
+
23
+ - `print` - Print everything that goes to stdout and stderr.
24
+ - `stripAnsi` - Strip ansi codes from everything that goes to stdout and stderr. Defaults to true.
25
+
26
+ See the [tests](./test/capture-output.test.ts) for example usage.
27
+
28
+ ### `runCommand`
29
+
30
+ `runCommand` allows you to get the stdout, stderr, return value, and error of a command in your CLI.
31
+
32
+ See the [tests](./test/run-command.test.ts) for example usage.
33
+
34
+ ### `runHook`
35
+
36
+ `runHook` allows you to get the stdout, stderr, return value, and error of a hook in your CLI.
37
+
38
+ See the [tests](./test/run-hook.test.ts) for example usage.
package/lib/index.d.ts CHANGED
@@ -3,22 +3,52 @@ type CaptureOptions = {
3
3
  print?: boolean;
4
4
  stripAnsi?: boolean;
5
5
  };
6
- export declare function captureOutput<T>(fn: () => Promise<unknown>, opts?: CaptureOptions): Promise<{
6
+ type CaptureResult<T> = {
7
7
  error?: Error & Partial<Errors.CLIError>;
8
8
  result?: T;
9
9
  stderr: string;
10
10
  stdout: string;
11
- }>;
12
- export declare function runCommand<T>(args: string | string[], loadOpts?: Interfaces.LoadOptions, captureOpts?: CaptureOptions): Promise<{
13
- error?: Error & Partial<Errors.CLIError>;
14
- result?: T;
15
- stderr: string;
16
- stdout: string;
17
- }>;
18
- export declare function runHook<T>(hook: string, options: Record<string, unknown>, loadOpts?: Interfaces.LoadOptions, recordOpts?: CaptureOptions): Promise<{
19
- error?: Error & Partial<Errors.CLIError>;
20
- result?: T;
21
- stderr: string;
22
- stdout: string;
23
- }>;
11
+ };
12
+ /**
13
+ * Capture the stderr and stdout output of a function
14
+ * @param fn async function to run
15
+ * @param opts options
16
+ * - print: Whether to print the output to the console
17
+ * - stripAnsi: Whether to strip ANSI codes from the output
18
+ * @returns {Promise<CaptureResult<T>>} Captured output
19
+ * - error: Error object if the function throws an error
20
+ * - result: Result of the function if it returns a value and succeeds
21
+ * - stderr: Captured stderr output
22
+ * - stdout: Captured stdout output
23
+ */
24
+ export declare function captureOutput<T>(fn: () => Promise<unknown>, opts?: CaptureOptions): Promise<CaptureResult<T>>;
25
+ /**
26
+ * Capture the stderr and stdout output of a command in your CLI
27
+ * @param args Command arguments, e.g. `['my:command', '--flag']` or `'my:command --flag'`
28
+ * @param loadOpts options for loading oclif `Config`
29
+ * @param captureOpts options for capturing the output
30
+ * - print: Whether to print the output to the console
31
+ * - stripAnsi: Whether to strip ANSI codes from the output
32
+ * @returns {Promise<CaptureResult<T>>} Captured output
33
+ * - error: Error object if the command throws an error
34
+ * - result: Result of the command if it returns a value and succeeds
35
+ * - stderr: Captured stderr output
36
+ * - stdout: Captured stdout output
37
+ */
38
+ export declare function runCommand<T>(args: string | string[], loadOpts?: Interfaces.LoadOptions, captureOpts?: CaptureOptions): Promise<CaptureResult<T>>;
39
+ /**
40
+ * Capture the stderr and stdout output of a hook in your CLI
41
+ * @param hook Hook name
42
+ * @param options options to pass to the hook
43
+ * @param loadOpts options for loading oclif `Config`
44
+ * @param captureOpts options for capturing the output
45
+ * - print: Whether to print the output to the console
46
+ * - stripAnsi: Whether to strip ANSI codes from the output
47
+ * @returns {Promise<CaptureResult<T>>} Captured output
48
+ * - error: Error object if the hook throws an error
49
+ * - result: Result of the hook if it returns a value and succeeds
50
+ * - stderr: Captured stderr output
51
+ * - stdout: Captured stdout output
52
+ */
53
+ export declare function runHook<T>(hook: string, options: Record<string, unknown>, loadOpts?: Interfaces.LoadOptions, captureOpts?: CaptureOptions): Promise<CaptureResult<T>>;
24
54
  export {};
package/lib/index.js CHANGED
@@ -1,57 +1,36 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
2
25
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
26
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
27
  };
5
28
  Object.defineProperty(exports, "__esModule", { value: true });
6
29
  exports.runHook = exports.runCommand = exports.captureOutput = void 0;
7
30
  const core_1 = require("@oclif/core");
8
- const ansis_1 = __importDefault(require("ansis"));
9
31
  const debug_1 = __importDefault(require("debug"));
10
32
  const node_path_1 = require("node:path");
11
33
  const debug = (0, debug_1.default)('oclif-test');
12
- const RECORD_OPTIONS = {
13
- print: false,
14
- stripAnsi: true,
15
- };
16
- const originals = {
17
- NODE_ENV: process.env.NODE_ENV,
18
- stderr: process.stderr.write,
19
- stdout: process.stdout.write,
20
- };
21
- const output = {
22
- stderr: [],
23
- stdout: [],
24
- };
25
- function mockedStdout(str, encoding, cb) {
26
- output.stdout.push(str);
27
- if (!RECORD_OPTIONS.print)
28
- return true;
29
- if (typeof encoding === 'string') {
30
- return originals.stdout.bind(process.stdout)(str, encoding, cb);
31
- }
32
- return originals.stdout.bind(process.stdout)(str, cb);
33
- }
34
- function mockedStderr(str, encoding, cb) {
35
- output.stderr.push(str);
36
- if (!RECORD_OPTIONS.print)
37
- return true;
38
- if (typeof encoding === 'string') {
39
- return originals.stdout.bind(process.stderr)(str, encoding, cb);
40
- }
41
- return originals.stdout.bind(process.stderr)(str, cb);
42
- }
43
- const restore = () => {
44
- process.stderr.write = originals.stderr;
45
- process.stdout.write = originals.stdout;
46
- process.env.NODE_ENV = originals.NODE_ENV;
47
- };
48
- const reset = () => {
49
- output.stderr = [];
50
- output.stdout = [];
51
- };
52
- const toString = (str) => RECORD_OPTIONS.stripAnsi ? ansis_1.default.strip(str.toString()) : str.toString();
53
- const getStderr = () => output.stderr.map((b) => toString(b)).join('');
54
- const getStdout = () => output.stdout.map((b) => toString(b)).join('');
55
34
  function traverseFilePathUntil(filename, predicate) {
56
35
  let current = filename;
57
36
  while (!predicate(current)) {
@@ -70,11 +49,48 @@ function findRoot() {
70
49
  function makeLoadOptions(loadOpts) {
71
50
  return loadOpts ?? { root: findRoot() };
72
51
  }
52
+ /**
53
+ * Capture the stderr and stdout output of a function
54
+ * @param fn async function to run
55
+ * @param opts options
56
+ * - print: Whether to print the output to the console
57
+ * - stripAnsi: Whether to strip ANSI codes from the output
58
+ * @returns {Promise<CaptureResult<T>>} Captured output
59
+ * - error: Error object if the function throws an error
60
+ * - result: Result of the function if it returns a value and succeeds
61
+ * - stderr: Captured stderr output
62
+ * - stdout: Captured stdout output
63
+ */
73
64
  async function captureOutput(fn, opts) {
74
- RECORD_OPTIONS.print = opts?.print ?? false;
75
- RECORD_OPTIONS.stripAnsi = opts?.stripAnsi ?? true;
76
- process.stderr.write = mockedStderr;
77
- process.stdout.write = mockedStdout;
65
+ const print = opts?.print ?? false;
66
+ const originals = {
67
+ NODE_ENV: process.env.NODE_ENV,
68
+ stderr: process.stderr.write,
69
+ stdout: process.stdout.write,
70
+ };
71
+ const output = {
72
+ stderr: [],
73
+ stdout: [],
74
+ };
75
+ const { default: stripAnsi } = (opts?.stripAnsi ?? true) ? await Promise.resolve().then(() => __importStar(require('strip-ansi'))) : { default: (str) => str };
76
+ const toString = (str) => stripAnsi(str.toString());
77
+ const getStderr = () => output.stderr.map((b) => toString(b)).join('');
78
+ const getStdout = () => output.stdout.map((b) => toString(b)).join('');
79
+ const mock = (std) => (str, encoding, cb) => {
80
+ output[std].push(str);
81
+ if (print) {
82
+ if (encoding !== null && typeof encoding === 'function') {
83
+ cb = encoding;
84
+ encoding = undefined;
85
+ }
86
+ originals[std].apply(process[std], [str, encoding, cb]);
87
+ }
88
+ else if (typeof cb === 'function')
89
+ cb();
90
+ return true;
91
+ };
92
+ process.stdout.write = mock('stdout');
93
+ process.stderr.write = mock('stderr');
78
94
  process.env.NODE_ENV = 'test';
79
95
  try {
80
96
  const result = await fn();
@@ -86,18 +102,32 @@ async function captureOutput(fn, opts) {
86
102
  }
87
103
  catch (error) {
88
104
  return {
89
- ...(error instanceof core_1.Errors.CLIError && { error }),
90
- ...(error instanceof Error && { error }),
105
+ ...(error instanceof core_1.Errors.CLIError && { error: { ...error, message: toString(error.message) } }),
106
+ ...(error instanceof Error && { error: { ...error, message: toString(error.message) } }),
91
107
  stderr: getStderr(),
92
108
  stdout: getStdout(),
93
109
  };
94
110
  }
95
111
  finally {
96
- restore();
97
- reset();
112
+ process.stderr.write = originals.stderr;
113
+ process.stdout.write = originals.stdout;
114
+ process.env.NODE_ENV = originals.NODE_ENV;
98
115
  }
99
116
  }
100
117
  exports.captureOutput = captureOutput;
118
+ /**
119
+ * Capture the stderr and stdout output of a command in your CLI
120
+ * @param args Command arguments, e.g. `['my:command', '--flag']` or `'my:command --flag'`
121
+ * @param loadOpts options for loading oclif `Config`
122
+ * @param captureOpts options for capturing the output
123
+ * - print: Whether to print the output to the console
124
+ * - stripAnsi: Whether to strip ANSI codes from the output
125
+ * @returns {Promise<CaptureResult<T>>} Captured output
126
+ * - error: Error object if the command throws an error
127
+ * - result: Result of the command if it returns a value and succeeds
128
+ * - stderr: Captured stderr output
129
+ * - stdout: Captured stdout output
130
+ */
101
131
  async function runCommand(args, loadOpts, captureOpts) {
102
132
  const loadOptions = makeLoadOptions(loadOpts);
103
133
  const argsArray = (Array.isArray(args) ? args : [args]).join(' ').split(' ');
@@ -108,12 +138,26 @@ async function runCommand(args, loadOpts, captureOpts) {
108
138
  return captureOutput(async () => (0, core_1.run)(finalArgs, loadOptions), captureOpts);
109
139
  }
110
140
  exports.runCommand = runCommand;
111
- async function runHook(hook, options, loadOpts, recordOpts) {
141
+ /**
142
+ * Capture the stderr and stdout output of a hook in your CLI
143
+ * @param hook Hook name
144
+ * @param options options to pass to the hook
145
+ * @param loadOpts options for loading oclif `Config`
146
+ * @param captureOpts options for capturing the output
147
+ * - print: Whether to print the output to the console
148
+ * - stripAnsi: Whether to strip ANSI codes from the output
149
+ * @returns {Promise<CaptureResult<T>>} Captured output
150
+ * - error: Error object if the hook throws an error
151
+ * - result: Result of the hook if it returns a value and succeeds
152
+ * - stderr: Captured stderr output
153
+ * - stdout: Captured stdout output
154
+ */
155
+ async function runHook(hook, options, loadOpts, captureOpts) {
112
156
  const loadOptions = makeLoadOptions(loadOpts);
113
157
  debug('loadOpts: %O', loadOptions);
114
158
  return captureOutput(async () => {
115
159
  const config = await core_1.Config.load(loadOptions);
116
160
  return config.runHook(hook, options);
117
- }, recordOpts);
161
+ }, captureOpts);
118
162
  }
119
163
  exports.runHook = runHook;
package/package.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@oclif/test",
3
3
  "description": "test helpers for oclif components",
4
- "version": "4.0.1-beta.1",
4
+ "version": "4.0.1-beta.3",
5
5
  "author": "Salesforce",
6
6
  "bugs": "https://github.com/oclif/test/issues",
7
7
  "dependencies": {
8
- "ansis": "^3.2.0",
9
- "debug": "^4.3.4"
8
+ "debug": "^4.3.4",
9
+ "strip-ansi": "^7.1.0"
10
10
  },
11
11
  "peerDependencies": {
12
- "@oclif/core": "^4.0.0-beta.6"
12
+ "@oclif/core": ">= 3.0.0"
13
13
  },
14
14
  "devDependencies": {
15
15
  "@commitlint/config-conventional": "^18.6.3",