fail-on-console 1.2.0 → 1.3.1

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.1](https://github.com/benquarmby/fail-on-console/compare/v1.3.0...v1.3.1) (2026-08-01)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * use correct rule type for allowStream declaration ([23fe420](https://github.com/benquarmby/fail-on-console/commit/23fe42050a7a46959ea6f2b68204a41e424bcacc))
9
+
10
+ ## [1.3.0](https://github.com/benquarmby/fail-on-console/compare/v1.2.0...v1.3.0) (2026-08-01)
11
+
12
+
13
+ ### Features
14
+
15
+ * support process.stdout and process.stderr monitoring ([a79f82f](https://github.com/benquarmby/fail-on-console/commit/a79f82fa6ea778d44e8a59654f89bdd66464ab76))
16
+
3
17
  ## [1.2.0](https://github.com/benquarmby/fail-on-console/compare/v1.1.2...v1.2.0) (2026-07-25)
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
@@ -53,39 +54,42 @@ import {setupConsole} from "fail-on-console";
53
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
65
  // Fail on console.error, console.warn, and console.debug.
65
- 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"]
66
70
  });
67
71
  ```
68
72
 
69
73
  ## Suppressing Expected Logs
70
74
 
71
- 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.
72
76
 
73
- `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.
74
78
 
75
79
  ```javascript
76
- import {allowConsole} from "fail-on-console";
80
+ import {allowConsole, allowStream} from "fail-on-console";
77
81
 
78
82
  // Allow a substring.
79
83
  allowConsole("warn", "third-party library warning");
80
84
 
81
85
  // Allow a Regular Expression.
82
- allowConsole("error", /^Warning: Each child in a list/);
86
+ allowStream("stderr", /^Warning: Each child in a list/);
83
87
 
84
88
  // Allow with a custom predicate function
85
89
  allowConsole("log", (message) => message.startsWith("[analytics]"));
86
90
 
87
91
  // An array of mixed matchers
88
- allowConsole("error", ["known warning", /deprecated/, (msg) => msg.includes("third-party")]);
92
+ allowStream("stdout", ["known warning", /deprecated/, (msg) => msg.includes("third-party")]);
89
93
  ```
90
94
 
91
95
  ## API Reference
@@ -97,6 +101,7 @@ Initializes console spies that monitor active tests.
97
101
  - `options.beforeEach`: The framework's `beforeEach` hook.
98
102
  - `options.afterEach`: The framework's `afterEach` hook.
99
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).
100
105
 
101
106
  ### `allowConsole(method, rules)`
102
107
 
@@ -108,6 +113,16 @@ Registers a temporary or global allowlist rule for a monitored console method.
108
113
  - `RegExp`: Allowed if the regex tests true against the message.
109
114
  - `Function`: A predicate `(message: string) => boolean` returning `true` to allow the message.
110
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
+
111
126
  ## Limitations
112
127
 
113
128
  ### Concurrency
@@ -122,6 +137,10 @@ Monitoring `console.assert` is currently unsupported. It has a distinct signatur
122
137
 
123
138
  Mocha is unsupported due to API incompatibilities related to test context. There are no clean or reliable workarounds for integration.
124
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
+
125
144
  ## Credits & Prior Art
126
145
 
127
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;
@@ -23,6 +24,7 @@ export interface TestApi {
23
24
 
24
25
  export interface SetupOptions extends TestApi {
25
26
  methods?: ConsoleMethod[];
27
+ streams?: ProcessStream[];
26
28
  }
27
29
 
28
30
  export interface AllowPredicate {
@@ -46,6 +48,7 @@ export function setup(options: SetupOptions): void;
46
48
  * @param {Function} options.beforeEach The beforeEach hook from the test framework.
47
49
  * @param {Function} options.afterEach The afterEach hook from the test framework.
48
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.
49
52
  * @example
50
53
  * // Vitest
51
54
  * import {beforeEach, afterEach} from "vitest";
@@ -65,14 +68,13 @@ export function setupConsole(options: SetupOptions): void;
65
68
  * Allows specific console calls to pass. Console exceptions can be configured
66
69
  * globally, within a describe block or inside a single test.
67
70
  * @param {string} method The console method to allow: "error", "warn", "info",
68
- * "log", "debug", or "assert".
71
+ * "log" or "debug".
69
72
  * @param {string|RegExp|Function|Array<string|RegExp|Function>} rules One or
70
- * more matchers. A message is allowed if any matcher matches it. A string
71
- * matches when the message contains it. A RegExp matches when it tests true
72
- * against the message. A function receives the message and returns true to
73
- * 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.
74
76
  * @example
75
- * // Single string - allow any warn containing this substring
77
+ * // String - allow any warn containing this substring
76
78
  * allowConsole("warn", "third-party library warning");
77
79
  * @example
78
80
  * // RegExp - allow errors matching a pattern
@@ -81,7 +83,26 @@ export function setupConsole(options: SetupOptions): void;
81
83
  * // Predicate - allow logs from a specific source
82
84
  * allowConsole("log", (message) => message.startsWith("[analytics]"));
83
85
  * @example
84
- * // Mixed array - allow multiple matchers at once
86
+ * // Mixed array - allow multiple rules at once
85
87
  * allowConsole("error", ["known warning", /deprecated/, (m) => m.includes("third-party")]);
86
88
  */
87
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: AllowRule | AllowRule[]): void;
package/index.js CHANGED
@@ -1,7 +1,10 @@
1
1
  const supportedMethods = ["error", "warn", "info", "log", "debug"];
2
+ const supportedStreams = ["stderr", "stdout"];
2
3
  // Everything but console.debug is monitored by default. Debug logging in tests
3
4
  // is usually intentional.
4
5
  const defaultMethods = supportedMethods.slice(0, -1);
6
+ // No streams are monitored by default.
7
+ const defaultStreams = [];
5
8
  // %s string, %d/%i integer, %o object, %f float
6
9
  const printfPattern = /%[sdiof]/g;
7
10
 
@@ -9,32 +12,40 @@ const printfPattern = /%[sdiof]/g;
9
12
  // Test frameworks run tests serially by default.
10
13
  let testApi;
11
14
  let isInsideTest = false;
12
- const allowed = new Map();
15
+ const allowedMethods = new Map();
16
+ const allowedStreams = new Map();
13
17
 
14
18
  function quoteString(value) {
15
19
  return `"${value}"`;
16
20
  }
17
21
 
18
- function assertSupportedMethods(methods) {
19
- if (!Array.isArray(methods)) {
20
- 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}.`);
21
25
  }
22
26
 
23
- const unsupported = methods.filter((method) => !supportedMethods.includes(method));
27
+ const unsupported = values.filter((value) => !supportedValues.includes(value));
24
28
 
25
29
  if (!unsupported.length) {
26
30
  return;
27
31
  }
28
32
 
29
- const method = unsupported.length === 1 ? "method" : "methods";
30
33
  const invalidList = unsupported.map(quoteString).join(", ");
31
- const validList = supportedMethods.map(quoteString).join(", ");
34
+ const validList = supportedValues.map(quoteString).join(", ");
32
35
 
33
36
  throw new Error(
34
- `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}.`
35
38
  );
36
39
  }
37
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
+
38
49
  /**
39
50
  * Basic implementation of node:util/format for console message formatting.
40
51
  * Covers only the most common uses. Does not handle all specifiers (such as
@@ -77,18 +88,36 @@ function isAllowed(message, rule) {
77
88
  return rule.test(message);
78
89
  }
79
90
 
80
- function setupConsole({beforeEach, afterEach, 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}) {
81
108
  if (testApi) {
82
109
  throw new Error("fail-on-console: Call setupConsole() only once.");
83
110
  }
84
111
 
85
112
  assertSupportedMethods(methods);
113
+ assertSupportedStreams(streams);
86
114
 
87
115
  testApi = {beforeEach, afterEach};
88
116
 
89
117
  beforeEach(function () {
90
118
  isInsideTest = true;
91
- allowed.clear();
119
+ allowedMethods.clear();
120
+ allowedStreams.clear();
92
121
  });
93
122
 
94
123
  afterEach(function () {
@@ -104,7 +133,7 @@ function setupConsole({beforeEach, afterEach, methods = defaultMethods}) {
104
133
 
105
134
  console[method] = function consoleOverride(...args) {
106
135
  const message = format(...args);
107
- const rules = allowed.get(method);
136
+ const rules = allowedMethods.get(method);
108
137
 
109
138
  if (rules?.some((rule) => isAllowed(message, rule))) {
110
139
  return;
@@ -131,6 +160,56 @@ function setupConsole({beforeEach, afterEach, methods = defaultMethods}) {
131
160
  );
132
161
  });
133
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
+ });
134
213
  }
135
214
 
136
215
  function allowConsole(method, rules) {
@@ -143,8 +222,29 @@ function allowConsole(method, rules) {
143
222
  const normalized = Array.isArray(rules) ? rules : [rules];
144
223
 
145
224
  function addRules() {
146
- const existing = allowed.get(method) ?? [];
147
- 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]);
148
248
  }
149
249
 
150
250
  if (isInsideTest) {
@@ -157,3 +257,4 @@ function allowConsole(method, rules) {
157
257
  exports.setup = setupConsole;
158
258
  exports.setupConsole = setupConsole;
159
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.2.0",
3
+ "version": "1.3.1",
4
4
  "description": "Fail Vitest or Jest tests when unexpected console logs, warnings or errors occur.",
5
5
  "keywords": [
6
6
  "jest",
@@ -23,9 +23,10 @@
23
23
  "@vitest/coverage-v8": "4.1.10",
24
24
  "commitlint": "^21.2.1",
25
25
  "jest": "^30.4.2",
26
- "npm-run-all2": "^9.0.2",
26
+ "npm-run-all2": "^9.0.3",
27
27
  "prettier": "^3.9.6",
28
28
  "prettier-plugin-packagejson": "^3.0.2",
29
+ "typescript": "^7.0.2",
29
30
  "vitest": "^4.1.10"
30
31
  },
31
32
  "scripts": {
@@ -34,6 +35,7 @@
34
35
  "format:check": "prettier --check .",
35
36
  "test:jest": "jest",
36
37
  "test:vitest": "vitest run --coverage",
37
- "verify": "run-s format:check test:vitest test:jest"
38
+ "type:check": "tsc",
39
+ "verify": "run-s commit:check format:check type:check test:vitest test:jest"
38
40
  }
39
41
  }