@controlium/utils 1.0.1 → 1.0.2-alpha.2

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.
@@ -10,9 +10,6 @@
10
10
  * - **expression** — what to compute (type-specific)
11
11
  * - **format** — how to format the result (type-specific, often optional)
12
12
  *
13
- * The delimiter `|` and the `[[` / `]]` endstops are configurable via {@link Detokeniser.delimiter} and
14
- * {@link Detokeniser.tokenStartEndChars}, but the defaults cover the vast majority of use cases.
15
- *
16
13
  * ---
17
14
  * ## Built-in token types
18
15
  *
@@ -117,54 +114,26 @@
117
114
  *
118
115
  * ---
119
116
  * ## Extending with callbacks
120
- * Custom token types are registered via {@link Detokeniser.addCallbackSync} (for sync processing) or
121
- * {@link Detokeniser.addCallbackAsync} (for async processing). Callbacks are tried in registration
122
- * order; return `undefined` to pass to the next callback. If all callbacks return `undefined` and no
123
- * built-in handler matched, an error is thrown.
117
+ * Custom token types are registered via {@link Detokeniser.addCallback}. Both sync and async handlers
118
+ * share the same registration method and callback list. Callbacks are tried in registration order;
119
+ * return `undefined` to pass to the next callback. If all callbacks return `undefined` and no built-in
120
+ * handler matched, an error is thrown.
121
+ *
122
+ * Async callbacks are silently skipped when {@link Detokeniser.do} (sync) is used — use
123
+ * {@link Detokeniser.doAsync} if your callback returns a Promise.
124
124
  *
125
- * @see {@link Detokeniser.addCallbackSync}
126
- * @see {@link Detokeniser.addCallbackAsync}
125
+ * @see {@link Detokeniser.addCallback}
127
126
  * @see {@link Detokeniser.do}
128
127
  * @see {@link Detokeniser.doAsync}
129
128
  */
130
129
  export declare class Detokeniser {
131
- private static _endTokenChar;
132
- private static _startTokenChar;
130
+ private static readonly _endTokenChar;
131
+ private static readonly _startTokenChar;
133
132
  private static EscapeChar;
134
- private static _delimiter;
135
- private static _asyncCallbacks;
136
- private static _syncCallbacks;
137
- /**
138
- * Gets the current single-character delimiter used to separate token parts.
139
- * Default: `|`
140
- */
141
- static get delimiter(): string;
142
- /**
143
- * Sets the single-character delimiter used to separate token parts.
144
- * The new value applies to all subsequent {@link Detokeniser.do} / {@link Detokeniser.doAsync} calls.
145
- * @param newDelimiter - Exactly one character
146
- * @throws If `newDelimiter` is not exactly one character
147
- * @example
148
- * Detokeniser.delimiter = ':';
149
- * Detokeniser.do('[[random:digits:6]]'); // → e.g. '482910'
150
- * Detokeniser.reset(); // restore default '|'
151
- */
152
- static set delimiter(newDelimiter: string);
153
- /**
154
- * Sets the start and end token sequences.
155
- * @param startEndChars - An even-length string whose first half is the start sequence and second half the end sequence.
156
- * @example Detokeniser.tokenStartEndChars = "[[]]"; // start = "[[", end = "]]"
157
- * @remarks Start and end sequences must differ. Minimum total length is 2 (one char each).
158
- */
159
- static set tokenStartEndChars(startEndChars: string);
160
- /**
161
- * Gets the current token start/end sequences concatenated (e.g. `"[[]]"`).
162
- */
163
- static get tokenStartEndChars(): string;
133
+ private static readonly _delimiter;
134
+ private static _callbacks;
164
135
  /**
165
136
  * Resets the Detokeniser to factory defaults:
166
- * - Token endstops restored to `[[` / `]]`
167
- * - Delimiter restored to `|`
168
137
  * - Escape char restored to `/`
169
138
  * - All registered sync and async callbacks cleared
170
139
  *
@@ -174,90 +143,48 @@ export declare class Detokeniser {
174
143
  */
175
144
  static reset(): void;
176
145
  /**
177
- * Registers a synchronous custom token handler.
146
+ * Registers a custom token handler. Both sync and async handlers are registered via this method
147
+ * and share the same callback list.
148
+ *
149
+ * Callbacks are tried in registration order. The first to return a non-`undefined` value wins.
150
+ * Return `undefined` to pass to the next callback. If all callbacks return `undefined` and no
151
+ * built-in handler matched, an error is thrown.
178
152
  *
179
- * When {@link Detokeniser.do} encounters a token not handled by the built-in set, it invokes each
180
- * registered sync callback in registration order. The first to return a non-`undefined` string wins.
181
- * Return `undefined` to pass to the next callback. If all callbacks return `undefined`, an error is thrown.
153
+ * Async callbacks (those returning a `Promise`) are silently skipped when {@link Detokeniser.do}
154
+ * is used register an async callback only if you intend to call {@link Detokeniser.doAsync}.
182
155
  *
183
- * @param callback - `(delimiter: string, token: string) => string | undefined`
184
- * - `delimiter` — current delimiter character (default `|`)
185
- * - `token` — full token body without `[[` / `]]`, e.g. `"mytype|arg1|arg2"`
156
+ * @param callback - `(token: string) => string | undefined | Promise<string | undefined>`
157
+ * - `token` — full token body without `[[` / `]]`, e.g. `"mytype|arg1|arg2"` (delimiter is always `|`)
186
158
  *
187
159
  * @example
188
- * // Handle [[env|VAR_NAME]] tokens
189
- * Detokeniser.addCallbackSync((delimiter, token) => {
190
- * const [type, name] = token.split(delimiter);
191
- * if (type.toLowerCase() !== 'env') return undefined;
160
+ * // Sync handler for [[env|VAR_NAME]] tokens
161
+ * Detokeniser.addCallback((token) => {
162
+ * const [type, name] = token.split('|');
163
+ * if (type !== 'env') return undefined;
192
164
  * return process.env[name] ?? '';
193
165
  * });
194
166
  * Detokeniser.do('Path: [[env|HOME]]'); // → 'Path: /home/user'
195
167
  *
196
168
  * @example
197
- * // Multiple callbacks each handles one type, passes on the rest
198
- * Detokeniser.addCallbackSync((delimiter, token) => {
199
- * const [type, value] = token.split(delimiter);
200
- * if (type === 'upper') return value.toUpperCase();
201
- * return undefined;
202
- * });
203
- * Detokeniser.addCallbackSync((delimiter, token) => {
204
- * const [type, value] = token.split(delimiter);
205
- * if (type === 'lower') return value.toLowerCase();
206
- * return undefined;
207
- * });
208
- * Detokeniser.do('[[upper|hello]] [[lower|WORLD]]'); // → 'HELLO world'
209
- *
210
- * @see {@link Detokeniser.resetSyncCallbacks} to remove all sync callbacks
211
- * @see {@link Detokeniser.addCallbackAsync} for async token handlers
212
- */
213
- static addCallbackSync(callback: Detokeniser.Callback_Sync): void;
214
- /**
215
- * Registers an asynchronous custom token handler.
216
- *
217
- * Works identically to {@link Detokeniser.addCallbackSync} but is invoked by {@link Detokeniser.doAsync}.
218
- * Async callbacks are tried in registration order; the first to return a non-`undefined` value wins.
219
- * Return `undefined` to pass to the next callback.
220
- *
221
- * @param asyncCallback - `(delimiter: string, token: string) => Promise<string | undefined>`
222
- * - `delimiter` — current delimiter character (default `|`)
223
- * - `token` — full token body without `[[` / `]]`, e.g. `"mytype|arg1|arg2"`
224
- *
225
- * @example
226
- * // Handle [[db|table|column|whereClause]] tokens
227
- * Detokeniser.addCallbackAsync(async (delimiter, token) => {
228
- * const [type, table, column, where] = token.split(delimiter);
229
- * if (type.toLowerCase() !== 'db') return undefined;
169
+ * // Async handler for [[db|table|column|where]] tokens
170
+ * Detokeniser.addCallback(async (token) => {
171
+ * const [type, table, column, where] = token.split('|');
172
+ * if (type !== 'db') return undefined;
230
173
  * const row = await db.query(`SELECT ${column} FROM ${table} WHERE ${where} LIMIT 1`);
231
174
  * return String(row[column]);
232
175
  * });
233
176
  * const result = await Detokeniser.doAsync('ID: [[db|users|id|active=1]]');
234
177
  *
235
- * @example
236
- * // Combine with nested tokens inner tokens resolve before the callback is called
237
- * Detokeniser.addCallbackAsync(async (delimiter, token) => {
238
- * const [type, key] = token.split(delimiter);
239
- * if (type !== 'cache') return undefined;
240
- * return await redis.get(key);
241
- * });
242
- * // [[random|digits|8]] resolves first, then [[cache|...]] receives the result
243
- * await Detokeniser.doAsync('Val: [[cache|prefix-[[random|digits|8]]]]');
244
- *
245
- * @see {@link Detokeniser.resetAsyncCallbacks} to remove all async callbacks
246
- * @see {@link Detokeniser.addCallbackSync} for synchronous token handlers
247
- */
248
- static addCallbackAsync(asyncCallback: Detokeniser.Callback_Async): void;
249
- /**
250
- * Removes all registered sync callbacks. Built-in token handlers are unaffected.
251
- * Use between tests or scenarios to ensure callback isolation.
252
- * @see {@link Detokeniser.reset} to also clear async callbacks and restore all defaults
178
+ * @see {@link Detokeniser.resetCallbacks} to remove all registered callbacks
179
+ * @see {@link Detokeniser.doAsync} for async token resolution
253
180
  */
254
- static resetSyncCallbacks(): void;
181
+ static addCallback(callback: Detokeniser.Callback): void;
255
182
  /**
256
- * Removes all registered async callbacks. Built-in token handlers are unaffected.
183
+ * Removes all registered callbacks. Built-in token handlers are unaffected.
257
184
  * Use between tests or scenarios to ensure callback isolation.
258
- * @see {@link Detokeniser.reset} to also clear sync callbacks and restore all defaults
185
+ * @see {@link Detokeniser.reset} to also restore all defaults
259
186
  */
260
- static resetAsyncCallbacks(): void;
187
+ static resetCallbacks(): void;
261
188
  /**
262
189
  * Synchronously resolves all tokens in the given string and returns the result.
263
190
  *
@@ -311,8 +238,8 @@ export declare class Detokeniser {
311
238
  *
312
239
  * @example
313
240
  * // Async callback for database-driven tokens
314
- * Detokeniser.addCallbackAsync(async (delim, token) => {
315
- * const [type, key] = token.split(delim);
241
+ * Detokeniser.addCallbackAsync(async (token) => {
242
+ * const [type, key] = token.split('|');
316
243
  * if (type !== 'db') return undefined;
317
244
  * return await fetchFromDatabase(key);
318
245
  * });
@@ -365,24 +292,16 @@ export declare class Detokeniser {
365
292
  private static doRandomFloat;
366
293
  }
367
294
  /**
368
- * Signature for an asynchronous custom token handler registered via {@link Detokeniser.addCallbackAsync}.
295
+ * Signature for a custom token handler registered via {@link Detokeniser.addCallback}.
369
296
  *
370
- * @param delimiter - Current delimiter character (default `|`)
371
- * @param token - Full token body without `[[` / `]]` endstops, e.g. `"mytype|arg1|arg2"`
372
- * @returns Promise resolving to the replacement string, or `undefined` to pass to the next handler
373
- */
374
- export interface Detokeniser_Callback_Async {
375
- (delimiter: string, token: string): Promise<string | undefined>;
376
- }
377
- /**
378
- * Signature for a synchronous custom token handler registered via {@link Detokeniser.addCallbackSync}.
297
+ * May be synchronous or asynchronous. Async callbacks (returning a `Promise`) are silently skipped
298
+ * by {@link Detokeniser.do} and only invoked by {@link Detokeniser.doAsync}.
379
299
  *
380
- * @param delimiter - Current delimiter character (default `|`)
381
- * @param token - Full token body without `[[` / `]]` endstops, e.g. `"mytype|arg1|arg2"`
382
- * @returns The replacement string, or `undefined` to pass to the next handler
300
+ * @param token - Full token body without `[[` / `]]` endstops, e.g. `"mytype|arg1|arg2"` (delimiter is always `|`)
301
+ * @returns The replacement string, a Promise resolving to it, or `undefined` to pass to the next handler
383
302
  */
384
- export interface Detokeniser_Callback_Sync {
385
- (delimiter: string, token: string): string | undefined;
303
+ export interface Detokeniser_Callback {
304
+ (token: string): string | undefined | Promise<string | undefined>;
386
305
  }
387
306
  /**
388
307
  * Options passed to {@link Detokeniser.do} and {@link Detokeniser.doAsync}.
@@ -396,7 +315,6 @@ export interface Detokeniser_DoOptions {
396
315
  contextParameters?: Record<string, unknown>;
397
316
  }
398
317
  export declare namespace Detokeniser {
399
- type Callback_Async = Detokeniser_Callback_Async;
400
- type Callback_Sync = Detokeniser_Callback_Sync;
318
+ type Callback = Detokeniser_Callback;
401
319
  type DoOptions = Detokeniser_DoOptions;
402
320
  }
@@ -16,3 +16,4 @@ export { JsonUtils } from "./jsonUtils/jsonUtils";
16
16
  export { StringUtils } from "./stringUtils/stringUtils";
17
17
  export { Utils, ExistingFileWriteActions } from "./utils/utils";
18
18
  export type { AssertTypeMap, ActionAndParams } from "./utils/utils";
19
+ export { Mock } from "./mock/mock";
@@ -130,7 +130,9 @@ export interface WriteLineOptions {
130
130
  * // "MyTest.someStep (myTest.ts:45:3)"
131
131
  * Logger.writeLine(Logger.Levels.TestInformation, message, { stackOffset: 1 });
132
132
  *
133
- * @remarks Not yet implemented reserved for a future release.
133
+ * Values less than 1 are treated as 0 (no offset). Values that would exceed
134
+ * the available stack depth are clipped to the outermost frame.
135
+ *
134
136
  */
135
137
  stackOffset?: number;
136
138
  }
@@ -0,0 +1,391 @@
1
+ /**
2
+ * Static HTTP request interception and mocking utility for test suites.
3
+ *
4
+ * `Mock` is designed to sit between a test framework's network interceptor
5
+ * (e.g. Playwright's `page.route`) and the application under test. Every
6
+ * outgoing request from the AUT is forwarded to {@link Mock.intercept}, which
7
+ * decides how to handle it based on the registered listeners.
8
+ *
9
+ * **Default-deny**: any request that does not match a listener is blocked and
10
+ * recorded as an `unmatched` transaction. The test suite has full control —
11
+ * nothing reaches the network unless explicitly permitted.
12
+ *
13
+ * **All transactions are stored** regardless of outcome, so test steps can
14
+ * later inspect real and mocked traffic via {@link Mock.getTransactions}.
15
+ *
16
+ * All methods are static — no instantiation is required. Call {@link Mock.reset}
17
+ * in `beforeEach` / `afterEach` hooks to start each test with a clean slate.
18
+ *
19
+ * ---
20
+ *
21
+ * ### Local mode (default)
22
+ *
23
+ * `Mock` runs in the same process as the test framework. When a `passthrough`
24
+ * listener matches, `Mock` fetches the real response itself and returns it
25
+ * directly. {@link Mock.intercept} always resolves to a {@link Mock.InterceptResult}
26
+ * with `action: 'fulfill'` or `action: 'block'`.
27
+ *
28
+ * ### Delegate mode
29
+ *
30
+ * Enable with `Mock.delegateMode = true` when `Mock` is hosted in a remote
31
+ * server (e.g. a mock proxy) and the caller — not `Mock` — is better placed
32
+ * to perform the real network fetch. In this mode, when a `passthrough`
33
+ * listener matches, {@link Mock.intercept} returns `action: 'passthrough'`
34
+ * plus a `correlationId`. The caller fetches the real response and reports it
35
+ * back via {@link Mock.completePassthrough}, allowing `Mock` to record the
36
+ * full transaction. Use {@link Mock.getPendingTransactions} to detect
37
+ * passthrough calls that were never completed.
38
+ *
39
+ * @example
40
+ * ```typescript
41
+ * // Local mode — Playwright adapter
42
+ * Mock.reset();
43
+ * Mock.addListener('get-users',
44
+ * ['$[?(@.url =~ /api\\/users/)]', '$[?(@.method == "GET")]'],
45
+ * { status: 200, body: [{ id: 1, name: 'Alice' }] }
46
+ * );
47
+ *
48
+ * await page.route('**\/*', async (route) => {
49
+ * const result = await Mock.intercept({
50
+ * url: route.request().url(),
51
+ * method: route.request().method(),
52
+ * headers: await route.request().allHeaders(),
53
+ * });
54
+ * if (result.action === 'fulfill') {
55
+ * await route.fulfill({ status: result.response.status });
56
+ * } else {
57
+ * await route.abort();
58
+ * }
59
+ * });
60
+ * ```
61
+ *
62
+ * @example
63
+ * ```typescript
64
+ * // Delegate mode — remote mock server
65
+ * Mock.delegateMode = true;
66
+ *
67
+ * // In the interceptor callback:
68
+ * const result = await Mock.intercept(request);
69
+ * if (result.action === 'passthrough') {
70
+ * const real = await myFetch(request);
71
+ * Mock.completePassthrough(result.correlationId, real);
72
+ * return real;
73
+ * }
74
+ * ```
75
+ */
76
+ export declare class Mock {
77
+ private static listeners;
78
+ private static transactions;
79
+ private static transactionCounter;
80
+ private static pendingTransactions;
81
+ private static _delegateMode;
82
+ /**
83
+ * When `true`, passthrough requests are delegated to the caller rather than
84
+ * fetched by `Mock` itself. {@link Mock.intercept} returns
85
+ * `action: 'passthrough'` with a `correlationId` for the caller to use when
86
+ * reporting the real response back via {@link Mock.completePassthrough}.
87
+ *
88
+ * Defaults to `false` (local mode — `Mock` fetches passthroughs itself).
89
+ *
90
+ * @example
91
+ * Mock.delegateMode = true;
92
+ */
93
+ static set delegateMode(value: boolean);
94
+ static get delegateMode(): boolean;
95
+ /**
96
+ * Registers a named listener that matches intercepted requests and defines
97
+ * how `Mock` should respond to them.
98
+ *
99
+ * Listeners are evaluated in registration order. The first listener whose
100
+ * every matcher returns a result wins. If a listener with the same `name`
101
+ * already exists it is replaced and a warning is logged.
102
+ *
103
+ * @param name - Unique name for this listener. Used as the key in the
104
+ * listener store and appears in transaction records and log output.
105
+ * Must be a non-empty string.
106
+ *
107
+ * @param matchers - One or more JSONPath expressions evaluated against the
108
+ * {@link Mock.Request} object. All expressions must return at least one
109
+ * result for the listener to match (AND logic). Each expression is
110
+ * validated for syntactic correctness at registration time — an invalid
111
+ * expression throws immediately rather than silently failing at intercept
112
+ * time. Must be a non-empty array of non-empty strings.
113
+ *
114
+ * @param action - What to do when this listener matches:
115
+ * - `'block'` — abort the request and return `action: 'block'` to the caller.
116
+ * - `'passthrough'` — forward the request to the real endpoint (or delegate
117
+ * to the caller in {@link Mock.delegateMode}) and return the real response.
118
+ * - {@link Mock.Response} — return the supplied response object directly
119
+ * without touching the network. `status` must be a valid HTTP status
120
+ * code (100–599).
121
+ *
122
+ * @param delayMs - Optional delay in milliseconds applied before the
123
+ * response is returned, whether mocked, real, or blocked. Useful for
124
+ * simulating slow networks. Must be a non-negative finite number.
125
+ * Defaults to `0` (no delay).
126
+ *
127
+ * @throws {Error} If any argument fails validation.
128
+ *
129
+ * @example
130
+ * // Block all requests to an analytics endpoint
131
+ * Mock.addListener('block-analytics', ['$[?(@.url =~ /analytics/)]'], 'block');
132
+ *
133
+ * @example
134
+ * // Return a mock response with a 2-second simulated delay
135
+ * Mock.addListener('slow-login',
136
+ * ['$[?(@.url =~ /api\\/login/)]'],
137
+ * { status: 200, headers: { 'content-type': 'application/json' }, body: { token: 'abc' } },
138
+ * 2000
139
+ * );
140
+ */
141
+ static addListener(name: string, matchers: string[], action: Mock.ListenerAction, delayMs?: number): void;
142
+ /**
143
+ * Removes a previously registered listener by name.
144
+ *
145
+ * If no listener with the given name exists, a warning is logged and the
146
+ * call is a no-op.
147
+ *
148
+ * @param name - Name of the listener to remove. Must be a non-empty string.
149
+ * @throws {Error} If `name` is not a non-empty string.
150
+ *
151
+ * @example
152
+ * Mock.removeListener('get-users');
153
+ */
154
+ static removeListener(name: string): void;
155
+ /**
156
+ * Removes all registered listeners.
157
+ *
158
+ * Transaction history and pending transactions are unaffected. Use
159
+ * {@link Mock.reset} to clear everything in a single call.
160
+ */
161
+ static clearListeners(): void;
162
+ /**
163
+ * Processes an intercepted HTTP request and returns a {@link Mock.InterceptResult}
164
+ * describing what the caller should do.
165
+ *
166
+ * Listeners are evaluated in registration order; the first full match wins.
167
+ * Every request — matched or not — is recorded in the transaction history.
168
+ * If the matching listener has a `delayMs`, the delay is applied before the
169
+ * result is returned.
170
+ *
171
+ * | Situation | `action` returned | Transaction type |
172
+ * |---|---|---|
173
+ * | Matched → mock response | `'fulfill'` | `'mocked'` |
174
+ * | Matched → passthrough, local mode | `'fulfill'` | `'passthrough'` |
175
+ * | Matched → passthrough, delegate mode | `'passthrough'` | pending until {@link Mock.completePassthrough} |
176
+ * | Matched → block | `'block'` | `'blocked'` |
177
+ * | No listener matched | `'block'` | `'unmatched'` |
178
+ *
179
+ * @param request - The intercepted request. Must be a non-null object with
180
+ * non-empty `url` and `method` string properties.
181
+ *
182
+ * @returns A promise resolving to a {@link Mock.InterceptResult}.
183
+ *
184
+ * @throws {Error} If `request` fails validation.
185
+ *
186
+ * @example
187
+ * const result = await Mock.intercept(request);
188
+ * if (result.action === 'fulfill') {
189
+ * respondWith(result.response);
190
+ * } else if (result.action === 'passthrough') {
191
+ * // delegate mode only
192
+ * const real = await fetch(request.url);
193
+ * Mock.completePassthrough(result.correlationId, real);
194
+ * respondWith(real);
195
+ * } else {
196
+ * abortRequest();
197
+ * }
198
+ */
199
+ static intercept(request: Mock.Request): Promise<Mock.InterceptResult>;
200
+ /**
201
+ * Completes a delegated passthrough by supplying the real response the caller
202
+ * fetched. Records the full transaction and removes the pending entry.
203
+ *
204
+ * Only relevant when {@link Mock.delegateMode} is `true`. The `correlationId`
205
+ * must match one previously returned by {@link Mock.intercept}.
206
+ *
207
+ * @param correlationId - The UUID returned in the `action: 'passthrough'`
208
+ * result from {@link Mock.intercept}. Must be a non-empty string that
209
+ * matches a pending transaction.
210
+ *
211
+ * @param response - The real response the caller received from the endpoint.
212
+ * Must be a valid {@link Mock.Response} with a status code of 100–599.
213
+ *
214
+ * @throws {Error} If `correlationId` is invalid, unknown, or already completed.
215
+ * @throws {Error} If `response` fails validation.
216
+ *
217
+ * @example
218
+ * const result = await Mock.intercept(request);
219
+ * if (result.action === 'passthrough') {
220
+ * const real = await myFetch(request);
221
+ * Mock.completePassthrough(result.correlationId, real);
222
+ * }
223
+ */
224
+ static completePassthrough(correlationId: string, response: Mock.Response): void;
225
+ /**
226
+ * Returns all completed transactions in chronological order.
227
+ *
228
+ * Includes mocked, passthrough (completed), blocked, and unmatched entries.
229
+ * Delegated passthroughs awaiting {@link Mock.completePassthrough} appear in
230
+ * {@link Mock.getPendingTransactions} instead.
231
+ *
232
+ * @returns A read-only array of {@link Mock.Transaction} objects.
233
+ *
234
+ * @example
235
+ * const blocked = Mock.getTransactions().filter(t => t.type === 'blocked');
236
+ */
237
+ static getTransactions(): readonly Mock.Transaction[];
238
+ /**
239
+ * Clears all completed transactions and resets the transaction counter.
240
+ *
241
+ * Pending passthrough transactions and registered listeners are unaffected.
242
+ * Use {@link Mock.reset} to clear everything in a single call.
243
+ */
244
+ static clearTransactions(): void;
245
+ /**
246
+ * Returns all delegated passthrough transactions that have not yet been
247
+ * completed via {@link Mock.completePassthrough}.
248
+ *
249
+ * Use this in test assertions to verify no passthrough calls were abandoned,
250
+ * or to diagnose remote fetch failures.
251
+ *
252
+ * Only populated when {@link Mock.delegateMode} is `true`.
253
+ *
254
+ * @returns A read-only array of {@link Mock.PendingTransaction} objects.
255
+ *
256
+ * @example
257
+ * // Assert no orphaned passthroughs after a test
258
+ * expect(Mock.getPendingTransactions()).toHaveLength(0);
259
+ */
260
+ static getPendingTransactions(): readonly Mock.PendingTransaction[];
261
+ /**
262
+ * Clears all listeners, completed transactions, pending transactions, and
263
+ * resets delegate mode to `false`.
264
+ *
265
+ * Call this in `beforeEach` or `afterEach` hooks to guarantee a clean slate
266
+ * between tests.
267
+ *
268
+ * @example
269
+ * beforeEach(() => {
270
+ * Mock.reset();
271
+ * Mock.addListener('static-assets', [...], 'passthrough');
272
+ * });
273
+ */
274
+ static reset(): void;
275
+ private static throwError;
276
+ private static findMatch;
277
+ private static fetchReal;
278
+ private static record;
279
+ }
280
+ export declare namespace Mock {
281
+ /**
282
+ * Represents an HTTP request as seen by the interceptor.
283
+ *
284
+ * The `url`, `method`, and `headers` fields are required. Additional
285
+ * properties (e.g. framework-specific metadata) may be included and are
286
+ * available to JSONPath matchers.
287
+ */
288
+ interface Request {
289
+ /** The fully-qualified request URL. */
290
+ url: string;
291
+ /** HTTP method in uppercase, e.g. `'GET'`, `'POST'`. */
292
+ method: string;
293
+ /** Request headers as a plain key/value object. */
294
+ headers: Record<string, string>;
295
+ /** Parsed request body, if present. */
296
+ body?: unknown;
297
+ /** Any additional properties provided by the framework adapter. */
298
+ [key: string]: unknown;
299
+ }
300
+ /**
301
+ * Represents an HTTP response returned to the AUT, whether real or mocked.
302
+ */
303
+ interface Response {
304
+ /** HTTP status code, e.g. `200`, `404`. Must be 100–599. */
305
+ status: number;
306
+ /** Response headers as a plain key/value object. */
307
+ headers?: Record<string, string>;
308
+ /** Response body. Objects are serialised to JSON by the framework adapter. */
309
+ body?: unknown;
310
+ }
311
+ /**
312
+ * Defines what a listener does when it matches a request.
313
+ *
314
+ * - `'block'` — abort the request; the AUT receives no response.
315
+ * - `'passthrough'` — forward to the real endpoint (or delegate to the caller
316
+ * in {@link Mock.delegateMode}) and return the actual response.
317
+ * - {@link Response} — return the supplied mock response without hitting the network.
318
+ */
319
+ type ListenerAction = 'block' | 'passthrough' | Response;
320
+ /**
321
+ * The result returned by {@link Mock.intercept} to the framework adapter.
322
+ *
323
+ * - `action: 'fulfill'` — return `response` to the AUT.
324
+ * - `action: 'block'` — abort the request; the AUT receives nothing.
325
+ * - `action: 'passthrough'` — only in {@link Mock.delegateMode}. Fetch the
326
+ * real response using `request` details and report it back via
327
+ * {@link Mock.completePassthrough} using the supplied `correlationId`.
328
+ */
329
+ type InterceptResult = {
330
+ action: 'fulfill';
331
+ response: Response;
332
+ } | {
333
+ action: 'block';
334
+ } | {
335
+ action: 'passthrough';
336
+ correlationId: string;
337
+ };
338
+ /**
339
+ * Describes the outcome of a single completed intercepted request.
340
+ *
341
+ * - `'mocked'` — a mock {@link Response} was returned.
342
+ * - `'passthrough'` — the request was forwarded and the real response returned.
343
+ * - `'blocked'` — the request was explicitly blocked by a listener.
344
+ * - `'unmatched'` — no listener matched; the request was blocked by default.
345
+ */
346
+ type TransactionType = 'mocked' | 'passthrough' | 'blocked' | 'unmatched';
347
+ /**
348
+ * A record of a single completed intercepted request and its outcome.
349
+ *
350
+ * Delegated passthroughs awaiting completion appear in
351
+ * {@link Mock.getPendingTransactions} as {@link PendingTransaction} until
352
+ * {@link Mock.completePassthrough} is called.
353
+ */
354
+ interface Transaction {
355
+ /** Auto-incremented identifier, e.g. `'txn-1'`. Resets on {@link Mock.reset}. */
356
+ id: string;
357
+ /** Wall-clock time at which the request was intercepted. */
358
+ timestamp: Date;
359
+ /** Name of the listener that matched, if any. Absent for `'unmatched'` transactions. */
360
+ listenerName?: string;
361
+ /** How the request was handled. */
362
+ type: TransactionType;
363
+ /** The intercepted request. */
364
+ request: Request;
365
+ /** The response returned to the AUT. Absent for `'blocked'` and `'unmatched'` transactions. */
366
+ response?: Response;
367
+ }
368
+ /**
369
+ * A delegated passthrough that has been issued by {@link Mock.intercept} but
370
+ * not yet completed via {@link Mock.completePassthrough}.
371
+ *
372
+ * Only created when {@link Mock.delegateMode} is `true`.
373
+ */
374
+ interface PendingTransaction {
375
+ /** The UUID issued by {@link Mock.intercept} to identify this passthrough. */
376
+ correlationId: string;
377
+ /** Wall-clock time at which the passthrough was issued. */
378
+ timestamp: Date;
379
+ /** Name of the listener that triggered the passthrough. */
380
+ listenerName: string;
381
+ /** The original intercepted request. */
382
+ request: Request;
383
+ }
384
+ /** @internal */
385
+ interface Listener {
386
+ name: string;
387
+ matchers: string[];
388
+ action: ListenerAction;
389
+ delayMs: number;
390
+ }
391
+ }