| 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 | */ |
| 12 | import * as Array from "../Array.ts" |
| 13 | import * as Console from "../Console.ts" |
| 14 | import * as Effect from "../Effect.ts" |
| 15 | import * 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 | */ |
| 59 | export 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 | */ |
| 92 | export 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 | */ |
| 179 | export 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 | */ |
| 256 | export 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 | */ |
| 294 | export 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 | */ |
| 330 | export 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 | */ |
| 368 | export const errorLines: Effect.Effect<ReadonlyArray<unknown>, never, never> = testConsoleWith( |
| 369 | (console) => console.errorLines |
| 370 | ) |