1/**
2 * Provides a test implementation of the Effect `Console` service.
3 *
4 * When the test layer is provided, calls made through the Effect console APIs
5 * are captured in memory instead of being written to the host console. Tests can
6 * then assert on logged values deterministically. This module includes the
7 * console layer, helpers for reading captured `Console.log` and `Console.error`
8 * arguments, and access to the provided test console service.
9 *
10 * @since 4.0.0
11 */
12import * as Array from "../Array.ts"
13import * as Console from "../Console.ts"
14import * as Effect from "../Effect.ts"
15import * as Layer from "../Layer.ts"
16
17/**
18 * A `TestConsole` provides a testable implementation of the Console interface.
19 * It captures all console output for testing purposes while maintaining full
20 * compatibility with the standard Console API.
21 *
22 * **When to use**
23 *
24 * Use to provide a console implementation that records calls for assertions in
25 * tests.
26 *
27 * **Details**
28 *
29 * This interface extends the standard Console interface and adds methods to
30 * retrieve logged messages for verification in tests.
31 *
32 * **Example** (Capturing console output in tests)
33 *
34 * ```ts import.meta.vitest
35 * import { Console, Effect } from "effect"
36 * import { TestConsole } from "effect/testing"
37 *
38 * const program = Effect.gen(function*() {
39 * yield* Console.log("Hello, World!")
40 * yield* Console.error("An error occurred")
41 *
42 * const logs = yield* TestConsole.logLines
43 * const errors = yield* TestConsole.errorLines
44 *
45 * logs // => ["Hello, World!"]
46 * errors // => ["An error occurred"]
47 * }).pipe(Effect.provide(TestConsole.layer))
48 *
49 * await Effect.runPromise(program)
50 * ```
51 *
52 * @see {@link layer} for providing `TestConsole` to an effect
53 * @see {@link logLines} for reading captured `Console.log` calls
54 * @see {@link errorLines} for reading captured `Console.error` calls
55 *
56 * @category models
57 * @since 4.0.0
58 */
59export interface TestConsole extends Console.Console {
60 /**
61 * Returns an array of all items that have been logged by the program using
62 * `Console.log` thus far.
63 *
64 * **When to use**
65 *
66 * Use to inspect captured `Console.log` calls through a `TestConsole`
67 * instance.
68 */
69 readonly logLines: Effect.Effect<ReadonlyArray<unknown>>
70 /**
71 * Returns an array of all items that have been logged by the program using
72 * `Console.error` thus far.
73 *
74 * **When to use**
75 *
76 * Use to inspect captured `Console.error` calls through a `TestConsole`
77 * instance.
78 */
79 readonly errorLines: Effect.Effect<ReadonlyArray<unknown>>
80}
81
82/**
83 * The `TestConsole` namespace provides types and utilities for working with
84 * test console implementations.
85 *
86 * **When to use**
87 *
88 * Use when referring to types nested under the `TestConsole` namespace.
89 *
90 * @since 4.0.0
91 */
92export declare namespace TestConsole {
93 /**
94 * Represents a console method name that can be invoked on the TestConsole.
95 * This type includes all methods available on the Console interface.
96 *
97 * **When to use**
98 *
99 * Use to type the console method name recorded in a captured test console
100 * entry.
101 *
102 * **Example** (Typing captured console methods)
103 *
104 * ```ts import.meta.vitest
105 * import type { TestConsole } from "effect/testing"
106 *
107 * const method: TestConsole.TestConsole.Method = "log"
108 * ```
109 *
110 * @category models
111 * @since 4.0.0
112 */
113 export type Method = keyof Console.Console
114
115 /**
116 * Represents a single console method invocation captured by the TestConsole.
117 * Each entry contains the method name and the parameters passed to it.
118 *
119 * **When to use**
120 *
121 * Use to inspect or type one captured console invocation.
122 *
123 * **Example** (Typing captured console entries)
124 *
125 * ```ts import.meta.vitest
126 * import type { TestConsole } from "effect/testing"
127 *
128 * const entry: TestConsole.TestConsole.Entry = {
129 * method: "error",
130 * parameters: ["not found"]
131 * }
132 *
133 * entry // => { method: "error", parameters: ["not found"] }
134 * ```
135 *
136 * @category models
137 * @since 4.0.0
138 */
139 export interface Entry {
140 readonly method: Method
141 readonly parameters: ReadonlyArray<unknown>
142 }
143}
144
145/**
146 * Creates a new TestConsole instance that captures all console output.
147 * The returned TestConsole implements the Console interface and provides
148 * additional methods to retrieve logged messages.
149 *
150 * **When to use**
151 *
152 * Use to construct a test console service value directly.
153 *
154 * **Example** (Creating a test console)
155 *
156 * ```ts import.meta.vitest
157 * import { Console, Effect } from "effect"
158 * import { TestConsole } from "effect/testing"
159 *
160 * const program = Effect.gen(function*() {
161 * yield* Console.log("Debug message")
162 * yield* Console.error("Error occurred")
163 *
164 * const logs = yield* TestConsole.logLines
165 * const errors = yield* TestConsole.errorLines
166 *
167 * logs // => ["Debug message"]
168 * errors // => ["Error occurred"]
169 * }).pipe(Effect.provide(TestConsole.layer))
170 *
171 * await Effect.runPromise(program)
172 * ```
173 *
174 * @see {@link layer} for providing a `TestConsole` as a `Layer`
175 *
176 * @category constructors
177 * @since 4.0.0
178 */
179export const make = Effect.gen(function*() {
180 const entries: Array<TestConsole.Entry> = []
181
182 function createEntryUnsafe(method: TestConsole.Method) {
183 return (...parameters: ReadonlyArray<any>): void => {
184 entries.push({ method, parameters })
185 }
186 }
187
188 const logLines = Effect.sync(() => Array.flatMap(entries, (entry) => entry.method === "log" ? entry.parameters : []))
189
190 const errorLines = Effect.sync(() =>
191 Array.flatMap(entries, (entry) => entry.method === "error" ? entry.parameters : [])
192 )
193
194 return {
195 assert: createEntryUnsafe("assert"),
196 clear: createEntryUnsafe("clear"),
197 count: createEntryUnsafe("count"),
198 countReset: createEntryUnsafe("countReset"),
199 debug: createEntryUnsafe("debug"),
200 dir: createEntryUnsafe("dir"),
201 dirxml: createEntryUnsafe("dirxml"),
202 error: createEntryUnsafe("error"),
203 group: createEntryUnsafe("group"),
204 groupCollapsed: createEntryUnsafe("groupCollapsed"),
205 groupEnd: createEntryUnsafe("groupEnd"),
206 info: createEntryUnsafe("info"),
207 log: createEntryUnsafe("log"),
208 table: createEntryUnsafe("table"),
209 time: createEntryUnsafe("time"),
210 timeEnd: createEntryUnsafe("timeEnd"),
211 timeLog: createEntryUnsafe("timeLog"),
212 trace: createEntryUnsafe("trace"),
213 warn: createEntryUnsafe("warn"),
214 logLines,
215 errorLines
216 } as TestConsole
217})
218
219/**
220 * Retrieves the `TestConsole` service for this test and uses it to run the
221 * specified workflow.
222 *
223 * **When to use**
224 *
225 * Use to access the provided test console service inside an effect.
226 *
227 * **Example** (Accessing the test console service)
228 *
229 * ```ts import.meta.vitest
230 * import { Effect } from "effect"
231 * import { TestConsole } from "effect/testing"
232 *
233 * const program = TestConsole.testConsoleWith((testConsole) =>
234 * Effect.gen(function*() {
235 * testConsole.log("Test message")
236 * testConsole.error("Test error")
237 *
238 * const logs = yield* testConsole.logLines
239 * const errors = yield* testConsole.errorLines
240 *
241 * logs // => ["Test message"]
242 * errors // => ["Test error"]
243 * })
244 * ).pipe(Effect.provide(TestConsole.layer))
245 *
246 * await Effect.runPromise(program)
247 * ```
248 *
249 * @see {@link layer} for providing the test console service
250 * @see {@link logLines} for reading captured `Console.log` calls directly
251 * @see {@link errorLines} for reading captured `Console.error` calls directly
252 *
253 * @category testing
254 * @since 4.0.0
255 */
256export const testConsoleWith = <A, E, R>(f: (console: TestConsole) => Effect.Effect<A, E, R>): Effect.Effect<A, E, R> =>
257 Console.consoleWith((console) => f(console as TestConsole))
258
259/**
260 * Creates a `Layer` which constructs a `TestConsole`.
261 * This layer can be used to provide a TestConsole implementation
262 * for testing purposes.
263 *
264 * **When to use**
265 *
266 * Use to run an effect with console calls captured by `TestConsole`.
267 *
268 * **Example** (Providing a test console layer)
269 *
270 * ```ts import.meta.vitest
271 * import { Console, Effect } from "effect"
272 * import { TestConsole } from "effect/testing"
273 *
274 * const program = Effect.gen(function*() {
275 * yield* Console.log("This will be captured")
276 * yield* Console.error("This error will be captured")
277 *
278 * const logs = yield* TestConsole.logLines
279 * const errors = yield* TestConsole.errorLines
280 *
281 * logs // => ["This will be captured"]
282 * errors // => ["This error will be captured"]
283 * }).pipe(Effect.provide(TestConsole.layer))
284 *
285 * await Effect.runPromise(program)
286 * ```
287 *
288 * @see {@link make} for constructing the service value directly
289 * @see {@link testConsoleWith} for accessing the provided test console service
290 *
291 * @category layers
292 * @since 4.0.0
293 */
294export const layer: Layer.Layer<TestConsole> = Layer.effect(Console.Console)(make) as any
295
296/**
297 * Returns an array of all items that have been logged by the program using
298 * `Console.log` thus far.
299 *
300 * **When to use**
301 *
302 * Use to assert on captured `Console.log` output from a program provided with
303 * `TestConsole.layer`.
304 *
305 * **Example** (Reading captured log lines)
306 *
307 * ```ts import.meta.vitest
308 * import { Console, Effect } from "effect"
309 * import { TestConsole } from "effect/testing"
310 *
311 * const program = Effect.gen(function*() {
312 * yield* Console.log("First message")
313 * yield* Console.log("Second message", { key: "value" })
314 * yield* Console.log("Third message", 42, true)
315 *
316 * const logs = yield* TestConsole.logLines
317 *
318 * logs // => ["First message", "Second message", { key: "value" }, "Third message", 42, true]
319 * }).pipe(Effect.provide(TestConsole.layer))
320 *
321 * await Effect.runPromise(program)
322 * ```
323 *
324 * @see {@link errorLines} for reading captured `Console.error` output
325 * @see {@link layer} for capturing console calls during a test
326 *
327 * @category testing
328 * @since 4.0.0
329 */
330export const logLines: Effect.Effect<ReadonlyArray<unknown>, never, never> = testConsoleWith(
331 (console) => console.logLines
332)
333
334/**
335 * Returns an array of all items that have been logged by the program using
336 * `Console.error` thus far.
337 *
338 * **When to use**
339 *
340 * Use to assert on captured `Console.error` output from a program provided
341 * with `TestConsole.layer`.
342 *
343 * **Example** (Reading captured error lines)
344 *
345 * ```ts import.meta.vitest
346 * import { Console, Effect } from "effect"
347 * import { TestConsole } from "effect/testing"
348 *
349 * const program = Effect.gen(function*() {
350 * yield* Console.error("Error message")
351 * yield* Console.error("Another error", new Error("Something went wrong"))
352 *
353 * const errors = yield* TestConsole.errorLines
354 *
355 * const messages = [errors[0], errors[1], errors[2] instanceof Error ? errors[2].message : undefined]
356 * messages // => ["Error message", "Another error", "Something went wrong"]
357 * }).pipe(Effect.provide(TestConsole.layer))
358 *
359 * await Effect.runPromise(program)
360 * ```
361 *
362 * @see {@link logLines} for reading captured `Console.log` output
363 * @see {@link layer} for capturing console calls during a test
364 *
365 * @category testing
366 * @since 4.0.0
367 */
368export const errorLines: Effect.Effect<ReadonlyArray<unknown>, never, never> = testConsoleWith(
369 (console) => console.errorLines
370)