fail-on-console 1.1.2 → 1.3.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.3.0](https://github.com/benquarmby/fail-on-console/compare/v1.2.0...v1.3.0) (2026-08-01)
4
+
5
+
6
+ ### Features
7
+
8
+ * support process.stdout and process.stderr monitoring ([a79f82f](https://github.com/benquarmby/fail-on-console/commit/a79f82fa6ea778d44e8a59654f89bdd66464ab76))
9
+
10
+ ## [1.2.0](https://github.com/benquarmby/fail-on-console/compare/v1.1.2...v1.2.0) (2026-07-25)
11
+
12
+
13
+ ### Features
14
+
15
+ * do not require expect API in setupConsole ([04c0bad](https://github.com/benquarmby/fail-on-console/commit/04c0bad55f39773e6fb326f6364a5c64d46b3e0f))
16
+
3
17
  ## [1.1.2](https://github.com/benquarmby/fail-on-console/compare/v1.1.1...v1.1.2) (2026-07-22)
4
18
 
5
19
 
package/README.md CHANGED
@@ -5,13 +5,14 @@ Fail Vitest or Jest tests when unexpected console logs, warnings or errors occur
5
5
  [![npm version](https://img.shields.io/npm/v/fail-on-console.svg)](https://www.npmjs.com/package/fail-on-console)
6
6
  [![license](https://img.shields.io/npm/l/fail-on-console.svg)](https://github.com/benquarmby/fail-on-console/blob/main/LICENSE)
7
7
 
8
- The `fail-on-console` utility fails test suites whenever unexpected `console` logs, warnings, or errors are triggered, keeping test results clear and easy to read.
8
+ The `fail-on-console` utility fails test suites whenever unexpected `console` output or `process` writes are triggered, keeping test results clear and easy to read.
9
9
 
10
10
  ## Features
11
11
 
12
12
  - **⚡ Vitest and Jest Native**: Seamless integration with Vitest (including Browser Mode) and Jest using standard lifecycle hooks.
13
+ - **📡 Raw Stream Monitoring:** Fails on unexpected writes to `process.stdout` or `process.stderr` on top of standard console calls. Output from libraries that write directly to the stream doesn't slip through.
13
14
  - **đŸĒļ Zero Dependencies**: Pure, lightweight JavaScript with a tiny footprint.
14
- - **đŸŽ¯ Configurable Targets**: Choose exactly which console methods to monitor (`log`, `warn`, `error`, `info`, `debug`).
15
+ - **đŸŽ¯ Configurable Targets**: Choose exactly which console methods and / or process streams to monitor.
15
16
  - **📋 Flexible Allowlist**: Easily suppress expected console noise globally, per suite, or per test using strings, regular expressions, or custom predicates.
16
17
 
17
18
  ## Installation
@@ -35,10 +36,10 @@ Initialize `setupConsole` inside a configured [`setupFiles`](https://vitest.dev/
35
36
 
36
37
  ```js
37
38
  // vitest.setup.js
38
- import {beforeEach, afterEach, expect} from "vitest";
39
+ import {beforeEach, afterEach} from "vitest";
39
40
  import {setupConsole} from "fail-on-console";
40
41
 
41
- setupConsole({beforeEach, afterEach, expect});
42
+ setupConsole({beforeEach, afterEach});
42
43
  ```
43
44
 
44
45
  ### With Jest
@@ -47,46 +48,48 @@ Initialize `setupConsole` inside a configured [`setupFilesAfterEnv`](https://jes
47
48
 
48
49
  ```js
49
50
  // jest.setup.js
50
- import {beforeEach, afterEach, expect} from "@jest/globals";
51
+ import {beforeEach, afterEach} from "@jest/globals";
51
52
  import {setupConsole} from "fail-on-console";
52
53
 
53
- setupConsole({beforeEach, afterEach, expect});
54
+ setupConsole({beforeEach, afterEach});
54
55
  ```
55
56
 
56
- ### Customizing Monitored Methods
57
+ ### Customizing Monitored Methods and Streams
57
58
 
58
- By default, `debug` is not monitored but `error`, `warn`, `info`, and `log` are. This can be customized by passing a `methods` array:
59
+ By default, `console.debug` is not monitored but `error`, `warn`, `info`, and `log` are. This can be customized by passing a `methods` array. Similarly, `process.stdout` and `process.stderr` are not monitored by default, but can be configured with a `streams` array:
59
60
 
60
61
  ```ts
61
62
  setupConsole({
62
63
  beforeEach,
63
64
  afterEach,
64
- expect,
65
65
  // Fail on console.error, console.warn, and console.debug.
66
- methods: ["error", "warn", "debug"]
66
+ methods: ["error", "warn", "debug"],
67
+ // Fail when libraries like Bunyan write straight to process.stdout,
68
+ // bypassing console entirely.
69
+ streams: ["stdout", "stderr"]
67
70
  });
68
71
  ```
69
72
 
70
73
  ## Suppressing Expected Logs
71
74
 
72
- If a specific test or third-party dependency intentionally logs to the console, `allowConsole` can be used to allow the test to pass.
75
+ If a specific test or third-party dependency intentionally logs to the console, `allowConsole` and `allowStream` can be used to allow the test to pass.
73
76
 
74
- `allowConsole` can be invoked globally, inside a `describe` block, or inside a specific `test`/`it` block.
77
+ These functions can be invoked globally, inside a `describe` block, or inside a specific `test`/`it` block.
75
78
 
76
79
  ```javascript
77
- import {allowConsole} from "fail-on-console";
80
+ import {allowConsole, allowStream} from "fail-on-console";
78
81
 
79
82
  // Allow a substring.
80
83
  allowConsole("warn", "third-party library warning");
81
84
 
82
85
  // Allow a Regular Expression.
83
- allowConsole("error", /^Warning: Each child in a list/);
86
+ allowStream("stderr", /^Warning: Each child in a list/);
84
87
 
85
88
  // Allow with a custom predicate function
86
89
  allowConsole("log", (message) => message.startsWith("[analytics]"));
87
90
 
88
91
  // An array of mixed matchers
89
- allowConsole("error", ["known warning", /deprecated/, (msg) => msg.includes("third-party")]);
92
+ allowStream("stdout", ["known warning", /deprecated/, (msg) => msg.includes("third-party")]);
90
93
  ```
91
94
 
92
95
  ## API Reference
@@ -97,8 +100,8 @@ Initializes console spies that monitor active tests.
97
100
 
98
101
  - `options.beforeEach`: The framework's `beforeEach` hook.
99
102
  - `options.afterEach`: The framework's `afterEach` hook.
100
- - `options.expect`: The framework's `expect` object (must expose `getState()`).
101
103
  - `options.methods`: _(Optional)_ Array of `console` methods to track. Defaults to `["error", "warn", "info", "log"]`.
104
+ - `options.streams`: _(Optional)_ Array of `process` streams to track. Defaults to `[]` (no streams monitored).
102
105
 
103
106
  ### `allowConsole(method, rules)`
104
107
 
@@ -110,6 +113,16 @@ Registers a temporary or global allowlist rule for a monitored console method.
110
113
  - `RegExp`: Allowed if the regex tests true against the message.
111
114
  - `Function`: A predicate `(message: string) => boolean` returning `true` to allow the message.
112
115
 
116
+ ### `allowStream(stream, rules)`
117
+
118
+ Registers a temporary or global allowlist rule for a monitored process stream.
119
+
120
+ - `stream`: `"stdout" | "stderr"`
121
+ - `rules`: A single rule or an array of rules. A rule can be:
122
+ - `string`: Allowed if the written message contains this substring.
123
+ - `RegExp`: Allowed if the regex tests true against the message.
124
+ - `Function`: A predicate `(message: string) => boolean` returning `true` to allow the message.
125
+
113
126
  ## Limitations
114
127
 
115
128
  ### Concurrency
@@ -124,6 +137,10 @@ Monitoring `console.assert` is currently unsupported. It has a distinct signatur
124
137
 
125
138
  Mocha is unsupported due to API incompatibilities related to test context. There are no clean or reliable workarounds for integration.
126
139
 
140
+ ### Browser Mode
141
+
142
+ `process.stdout` and `process.stderr` don't exist in a real browser environment, so the `streams` option has no effect under Vitest Browser Mode. `setupConsole` detects this and silently skips stream monitoring rather than throwing. `methods` monitoring is unaffected and works normally.
143
+
127
144
  ## Credits & Prior Art
128
145
 
129
146
  This package is inspired by and builds on the excellent foundation laid by:
package/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export type ConsoleMethod = "error" | "warn" | "info" | "log" | "debug";
2
+ export type ProcessStream = "stderr" | "stdout";
2
3
 
3
4
  export interface ExpectStateLike {
4
5
  currentTestName?: string;
@@ -15,11 +16,15 @@ export interface LifecycleHookLike {
15
16
  export interface TestApi {
16
17
  beforeEach: LifecycleHookLike;
17
18
  afterEach: LifecycleHookLike;
18
- expect: ExpectLike;
19
+ /**
20
+ * @deprecated No longer needed. This option can be omitted.
21
+ */
22
+ expect?: ExpectLike;
19
23
  }
20
24
 
21
25
  export interface SetupOptions extends TestApi {
22
26
  methods?: ConsoleMethod[];
27
+ streams?: ProcessStream[];
23
28
  }
24
29
 
25
30
  export interface AllowPredicate {
@@ -37,25 +42,25 @@ export function setup(options: SetupOptions): void;
37
42
  /**
38
43
  * Installs console spies that fail the current test if any monitored console
39
44
  * method is called. Call once at the top of the test setup file, passing the
40
- * lifecycle hooks and expect function from the test framework. Compatible with
41
- * any Jest-compatible API (Jest, Vitest, etc.).
45
+ * lifecycle hooks from the test framework. Compatible with any Jest-like API
46
+ * (Vitest, Jest, etc.).
42
47
  * @param {Object} options
43
48
  * @param {Function} options.beforeEach The beforeEach hook from the test framework.
44
49
  * @param {Function} options.afterEach The afterEach hook from the test framework.
45
- * @param {Object} options.expect The expect object from the test framework. Must expose getState().
46
50
  * @param {string[]} [options.methods=["error","warn","info","log"]] Console methods to monitor.
51
+ * @param {string[]} [options.streams=[]] Process streams to monitor. None by default.
47
52
  * @example
48
- * // Jest
49
- * import {beforeEach, afterEach, expect} from "@jest/globals";
53
+ * // Vitest
54
+ * import {beforeEach, afterEach} from "vitest";
50
55
  * import {setup} from "fail-on-console";
51
56
  *
52
- * setup({beforeEach, afterEach, expect});
57
+ * setup({beforeEach, afterEach});
53
58
  * @example
54
- * // Vitest
55
- * import {beforeEach, afterEach, expect} from "vitest";
59
+ * // Jest
60
+ * import {beforeEach, afterEach} from "@jest/globals";
56
61
  * import {setup} from "fail-on-console";
57
62
  *
58
- * setup({beforeEach, afterEach, expect});
63
+ * setup({beforeEach, afterEach});
59
64
  */
60
65
  export function setupConsole(options: SetupOptions): void;
61
66
 
@@ -63,14 +68,13 @@ export function setupConsole(options: SetupOptions): void;
63
68
  * Allows specific console calls to pass. Console exceptions can be configured
64
69
  * globally, within a describe block or inside a single test.
65
70
  * @param {string} method The console method to allow: "error", "warn", "info",
66
- * "log", "debug", or "assert".
71
+ * "log" or "debug".
67
72
  * @param {string|RegExp|Function|Array<string|RegExp|Function>} rules One or
68
- * more matchers. A message is allowed if any matcher matches it. A string
69
- * matches when the message contains it. A RegExp matches when it tests true
70
- * against the message. A function receives the message and returns true to
71
- * allow it.
73
+ * more rules. A message is allowed if any rule finds a match. A string matches
74
+ * when the message contains it. A RegExp matches when it tests true against
75
+ * the message. A function receives the message and returns true to allow it.
72
76
  * @example
73
- * // Single string - allow any warn containing this substring
77
+ * // String - allow any warn containing this substring
74
78
  * allowConsole("warn", "third-party library warning");
75
79
  * @example
76
80
  * // RegExp - allow errors matching a pattern
@@ -79,7 +83,26 @@ export function setupConsole(options: SetupOptions): void;
79
83
  * // Predicate - allow logs from a specific source
80
84
  * allowConsole("log", (message) => message.startsWith("[analytics]"));
81
85
  * @example
82
- * // Mixed array - allow multiple matchers at once
86
+ * // Mixed array - allow multiple rules at once
83
87
  * allowConsole("error", ["known warning", /deprecated/, (m) => m.includes("third-party")]);
84
88
  */
85
89
  export function allowConsole(method: ConsoleMethod, rules: AllowRule | AllowRule[]): void;
90
+
91
+ /**
92
+ * Allows specific writes to `process.stdout` or `process.stderr` to pass.
93
+ * Stream exceptions can be configured globally, within a describe block or
94
+ * inside a single test. No-op in environments where `globalThis.process` does
95
+ * not exist.
96
+ * @param {string} stream The target process stream to allow: "stdout" or "stderr".
97
+ * @param {string|RegExp|Function|Array<string|RegExp|Function>} rules One or
98
+ * more rules. A message is allowed if any rule finds a match. A string matches
99
+ * when the message contains it. A RegExp matches when it tests true against
100
+ * the message. A function receives the message and returns true to allow it.
101
+ * @example
102
+ * // String - allow standard output containing this substring
103
+ * allowStream("stdout", "unavoidable log message");]);
104
+ * @example
105
+ * // Mixed array - allow standard errors using multiple rules at once
106
+ * allowStream("stderr", ["known warning", /deprecated/, (m) => m.includes("third-party")]);
107
+ */
108
+ export function allowStream(stream: ProcessStream, rules: Rule | Rule[]): void;
package/index.js CHANGED
@@ -1,36 +1,51 @@
1
- let testApi;
2
1
  const supportedMethods = ["error", "warn", "info", "log", "debug"];
2
+ const supportedStreams = ["stderr", "stdout"];
3
3
  // Everything but console.debug is monitored by default. Debug logging in tests
4
4
  // is usually intentional.
5
5
  const defaultMethods = supportedMethods.slice(0, -1);
6
- const allowed = new Map();
6
+ // No streams are monitored by default.
7
+ const defaultStreams = [];
7
8
  // %s string, %d/%i integer, %o object, %f float
8
9
  const printfPattern = /%[sdiof]/g;
9
10
 
11
+ // Module scoped variables to manage test state. Not safe for concurrent tests.
12
+ // Test frameworks run tests serially by default.
13
+ let testApi;
14
+ let isInsideTest = false;
15
+ const allowedMethods = new Map();
16
+ const allowedStreams = new Map();
17
+
10
18
  function quoteString(value) {
11
19
  return `"${value}"`;
12
20
  }
13
21
 
14
- function assertSupportedMethods(methods) {
15
- if (!Array.isArray(methods)) {
16
- throw new Error("fail-on-console: Expected an array of console methods.");
22
+ function assertSupportedValues(pluralName, supportedValues, values) {
23
+ if (!Array.isArray(values)) {
24
+ throw new Error(`fail-on-console: Expected an array of ${pluralName}.`);
17
25
  }
18
26
 
19
- const unsupported = methods.filter((method) => !supportedMethods.includes(method));
27
+ const unsupported = values.filter((value) => !supportedValues.includes(value));
20
28
 
21
29
  if (!unsupported.length) {
22
30
  return;
23
31
  }
24
32
 
25
- const method = unsupported.length === 1 ? "method" : "methods";
26
33
  const invalidList = unsupported.map(quoteString).join(", ");
27
- const validList = supportedMethods.map(quoteString).join(", ");
34
+ const validList = supportedValues.map(quoteString).join(", ");
28
35
 
29
36
  throw new Error(
30
- `fail-on-console: Unsupported console ${method} provided: ${invalidList}. Supported methods are: ${validList}.`
37
+ `fail-on-console: One or more unsupported ${pluralName} provided: ${invalidList}. Supported ${pluralName} are: ${validList}.`
31
38
  );
32
39
  }
33
40
 
41
+ function assertSupportedMethods(methods) {
42
+ return assertSupportedValues("console methods", supportedMethods, methods);
43
+ }
44
+
45
+ function assertSupportedStreams(streams) {
46
+ return assertSupportedValues("process streams", supportedStreams, streams);
47
+ }
48
+
34
49
  /**
35
50
  * Basic implementation of node:util/format for console message formatting.
36
51
  * Covers only the most common uses. Does not handle all specifiers (such as
@@ -73,16 +88,41 @@ function isAllowed(message, rule) {
73
88
  return rule.test(message);
74
89
  }
75
90
 
76
- function setupConsole({beforeEach, afterEach, expect, methods = defaultMethods}) {
91
+ function chunkToString(chunk, encoding = "utf8") {
92
+ if (typeof chunk === "string") {
93
+ return chunk;
94
+ }
95
+
96
+ if (ArrayBuffer.isView(chunk)) {
97
+ if (Buffer.isBuffer(chunk)) {
98
+ return chunk.toString(encoding);
99
+ }
100
+
101
+ return new TextDecoder(encoding).decode(chunk);
102
+ }
103
+
104
+ return Buffer.from(chunk).toString(encoding);
105
+ }
106
+
107
+ function setupConsole({beforeEach, afterEach, methods = defaultMethods, streams = defaultStreams}) {
77
108
  if (testApi) {
78
109
  throw new Error("fail-on-console: Call setupConsole() only once.");
79
110
  }
80
111
 
81
112
  assertSupportedMethods(methods);
113
+ assertSupportedStreams(streams);
82
114
 
83
- testApi = {beforeEach, afterEach, expect};
115
+ testApi = {beforeEach, afterEach};
84
116
 
85
- beforeEach(() => allowed.clear());
117
+ beforeEach(function () {
118
+ isInsideTest = true;
119
+ allowedMethods.clear();
120
+ allowedStreams.clear();
121
+ });
122
+
123
+ afterEach(function () {
124
+ isInsideTest = false;
125
+ });
86
126
 
87
127
  methods.forEach(function (method) {
88
128
  const original = console[method];
@@ -93,7 +133,7 @@ function setupConsole({beforeEach, afterEach, expect, methods = defaultMethods})
93
133
 
94
134
  console[method] = function consoleOverride(...args) {
95
135
  const message = format(...args);
96
- const rules = allowed.get(method);
136
+ const rules = allowedMethods.get(method);
97
137
 
98
138
  if (rules?.some((rule) => isAllowed(message, rule))) {
99
139
  return;
@@ -120,6 +160,56 @@ function setupConsole({beforeEach, afterEach, expect, methods = defaultMethods})
120
160
  );
121
161
  });
122
162
  });
163
+
164
+ streams.forEach(function (streamName) {
165
+ const stream = globalThis.process?.[streamName];
166
+
167
+ if (!stream) {
168
+ return;
169
+ }
170
+
171
+ const originalWrite = stream.write;
172
+ const calls = [];
173
+
174
+ beforeEach(function () {
175
+ calls.length = 0;
176
+
177
+ stream.write = function streamOverride(chunk, encoding, cb) {
178
+ if (typeof encoding === "function") {
179
+ cb = encoding;
180
+ encoding = undefined;
181
+ }
182
+
183
+ const message = chunkToString(chunk, encoding);
184
+ const rules = allowedStreams.get(streamName);
185
+
186
+ if (!rules?.some((rule) => isAllowed(message, rule))) {
187
+ const call = {message, stack: ""};
188
+ Error.captureStackTrace?.(call, streamOverride);
189
+
190
+ calls.push(call);
191
+ }
192
+
193
+ cb?.();
194
+
195
+ return true;
196
+ };
197
+ });
198
+
199
+ afterEach(function () {
200
+ stream.write = originalWrite;
201
+
202
+ if (!calls.length) {
203
+ return;
204
+ }
205
+
206
+ const detail = calls.map(({message, stack}) => `${message}\n${stack}`).join("\n\n");
207
+
208
+ throw new Error(
209
+ `Expected test not to write to process.${streamName}.\n\n${detail}\n\nIf expected, use allowStream("${streamName}", ...) to add an exception.`
210
+ );
211
+ });
212
+ });
123
213
  }
124
214
 
125
215
  function allowConsole(method, rules) {
@@ -130,11 +220,31 @@ function allowConsole(method, rules) {
130
220
  assertSupportedMethods([method]);
131
221
 
132
222
  const normalized = Array.isArray(rules) ? rules : [rules];
133
- const isInsideTest = !!testApi.expect.getState().currentTestName;
134
223
 
135
224
  function addRules() {
136
- const existing = allowed.get(method) ?? [];
137
- allowed.set(method, [...existing, ...normalized]);
225
+ const existing = allowedMethods.get(method) ?? [];
226
+ allowedMethods.set(method, [...existing, ...normalized]);
227
+ }
228
+
229
+ if (isInsideTest) {
230
+ addRules();
231
+ } else {
232
+ testApi.beforeEach(addRules);
233
+ }
234
+ }
235
+
236
+ function allowStream(stream, rules) {
237
+ if (!testApi) {
238
+ throw new Error("fail-on-console: Call setupConsole() before using allowStream().");
239
+ }
240
+
241
+ assertSupportedStreams([stream]);
242
+
243
+ const normalized = Array.isArray(rules) ? rules : [rules];
244
+
245
+ function addRules() {
246
+ const existing = allowedStreams.get(stream) ?? [];
247
+ allowedStreams.set(stream, [...existing, ...normalized]);
138
248
  }
139
249
 
140
250
  if (isInsideTest) {
@@ -147,3 +257,4 @@ function allowConsole(method, rules) {
147
257
  exports.setup = setupConsole;
148
258
  exports.setupConsole = setupConsole;
149
259
  exports.allowConsole = allowConsole;
260
+ exports.allowStream = allowStream;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fail-on-console",
3
- "version": "1.1.2",
3
+ "version": "1.3.0",
4
4
  "description": "Fail Vitest or Jest tests when unexpected console logs, warnings or errors occur.",
5
5
  "keywords": [
6
6
  "jest",
@@ -20,19 +20,20 @@
20
20
  ],
21
21
  "devDependencies": {
22
22
  "@commitlint/config-conventional": "^21.2.0",
23
- "@vitest/coverage-v8": "4.1.9",
24
- "commitlint": "^21.2.0",
23
+ "@vitest/coverage-v8": "4.1.10",
24
+ "commitlint": "^21.2.1",
25
25
  "jest": "^30.4.2",
26
- "prettier": "^3.8.4",
26
+ "npm-run-all2": "^9.0.3",
27
+ "prettier": "^3.9.6",
27
28
  "prettier-plugin-packagejson": "^3.0.2",
28
- "vitest": "^4.1.9"
29
+ "vitest": "^4.1.10"
29
30
  },
30
31
  "scripts": {
31
32
  "commit:check": "commitlint --from-last-tag",
32
33
  "format": "prettier --write .",
33
34
  "format:check": "prettier --check .",
34
- "test": "vitest run",
35
- "test:compat": "jest",
36
- "test:cover": "vitest run --coverage"
35
+ "test:jest": "jest",
36
+ "test:vitest": "vitest run --coverage",
37
+ "verify": "run-s commit:check format:check test:vitest test:jest"
37
38
  }
38
39
  }