@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.
@@ -0,0 +1,505 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { JSONPath } from 'jsonpath-plus';
3
+ import { Utils } from '../utils/utils';
4
+ import { Log, LogLevels } from '..';
5
+ /**
6
+ * Static HTTP request interception and mocking utility for test suites.
7
+ *
8
+ * `Mock` is designed to sit between a test framework's network interceptor
9
+ * (e.g. Playwright's `page.route`) and the application under test. Every
10
+ * outgoing request from the AUT is forwarded to {@link Mock.intercept}, which
11
+ * decides how to handle it based on the registered listeners.
12
+ *
13
+ * **Default-deny**: any request that does not match a listener is blocked and
14
+ * recorded as an `unmatched` transaction. The test suite has full control —
15
+ * nothing reaches the network unless explicitly permitted.
16
+ *
17
+ * **All transactions are stored** regardless of outcome, so test steps can
18
+ * later inspect real and mocked traffic via {@link Mock.getTransactions}.
19
+ *
20
+ * All methods are static — no instantiation is required. Call {@link Mock.reset}
21
+ * in `beforeEach` / `afterEach` hooks to start each test with a clean slate.
22
+ *
23
+ * ---
24
+ *
25
+ * ### Local mode (default)
26
+ *
27
+ * `Mock` runs in the same process as the test framework. When a `passthrough`
28
+ * listener matches, `Mock` fetches the real response itself and returns it
29
+ * directly. {@link Mock.intercept} always resolves to a {@link Mock.InterceptResult}
30
+ * with `action: 'fulfill'` or `action: 'block'`.
31
+ *
32
+ * ### Delegate mode
33
+ *
34
+ * Enable with `Mock.delegateMode = true` when `Mock` is hosted in a remote
35
+ * server (e.g. a mock proxy) and the caller — not `Mock` — is better placed
36
+ * to perform the real network fetch. In this mode, when a `passthrough`
37
+ * listener matches, {@link Mock.intercept} returns `action: 'passthrough'`
38
+ * plus a `correlationId`. The caller fetches the real response and reports it
39
+ * back via {@link Mock.completePassthrough}, allowing `Mock` to record the
40
+ * full transaction. Use {@link Mock.getPendingTransactions} to detect
41
+ * passthrough calls that were never completed.
42
+ *
43
+ * @example
44
+ * ```typescript
45
+ * // Local mode — Playwright adapter
46
+ * Mock.reset();
47
+ * Mock.addListener('get-users',
48
+ * ['$[?(@.url =~ /api\\/users/)]', '$[?(@.method == "GET")]'],
49
+ * { status: 200, body: [{ id: 1, name: 'Alice' }] }
50
+ * );
51
+ *
52
+ * await page.route('**\/*', async (route) => {
53
+ * const result = await Mock.intercept({
54
+ * url: route.request().url(),
55
+ * method: route.request().method(),
56
+ * headers: await route.request().allHeaders(),
57
+ * });
58
+ * if (result.action === 'fulfill') {
59
+ * await route.fulfill({ status: result.response.status });
60
+ * } else {
61
+ * await route.abort();
62
+ * }
63
+ * });
64
+ * ```
65
+ *
66
+ * @example
67
+ * ```typescript
68
+ * // Delegate mode — remote mock server
69
+ * Mock.delegateMode = true;
70
+ *
71
+ * // In the interceptor callback:
72
+ * const result = await Mock.intercept(request);
73
+ * if (result.action === 'passthrough') {
74
+ * const real = await myFetch(request);
75
+ * Mock.completePassthrough(result.correlationId, real);
76
+ * return real;
77
+ * }
78
+ * ```
79
+ */
80
+ export class Mock {
81
+ // ── Configuration ──────────────────────────────────────────────────────────
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) {
94
+ Utils.assertType(value, 'boolean', 'delegateMode', 'value');
95
+ Log.writeLine(LogLevels.FrameworkInformation, `Mock: delegate mode ${value ? 'enabled' : 'disabled'}`);
96
+ Mock._delegateMode = value;
97
+ }
98
+ static get delegateMode() {
99
+ return Mock._delegateMode;
100
+ }
101
+ // ── Listener management ────────────────────────────────────────────────────
102
+ /**
103
+ * Registers a named listener that matches intercepted requests and defines
104
+ * how `Mock` should respond to them.
105
+ *
106
+ * Listeners are evaluated in registration order. The first listener whose
107
+ * every matcher returns a result wins. If a listener with the same `name`
108
+ * already exists it is replaced and a warning is logged.
109
+ *
110
+ * @param name - Unique name for this listener. Used as the key in the
111
+ * listener store and appears in transaction records and log output.
112
+ * Must be a non-empty string.
113
+ *
114
+ * @param matchers - One or more JSONPath expressions evaluated against the
115
+ * {@link Mock.Request} object. All expressions must return at least one
116
+ * result for the listener to match (AND logic). Each expression is
117
+ * validated for syntactic correctness at registration time — an invalid
118
+ * expression throws immediately rather than silently failing at intercept
119
+ * time. Must be a non-empty array of non-empty strings.
120
+ *
121
+ * @param action - What to do when this listener matches:
122
+ * - `'block'` — abort the request and return `action: 'block'` to the caller.
123
+ * - `'passthrough'` — forward the request to the real endpoint (or delegate
124
+ * to the caller in {@link Mock.delegateMode}) and return the real response.
125
+ * - {@link Mock.Response} — return the supplied response object directly
126
+ * without touching the network. `status` must be a valid HTTP status
127
+ * code (100–599).
128
+ *
129
+ * @param delayMs - Optional delay in milliseconds applied before the
130
+ * response is returned, whether mocked, real, or blocked. Useful for
131
+ * simulating slow networks. Must be a non-negative finite number.
132
+ * Defaults to `0` (no delay).
133
+ *
134
+ * @throws {Error} If any argument fails validation.
135
+ *
136
+ * @example
137
+ * // Block all requests to an analytics endpoint
138
+ * Mock.addListener('block-analytics', ['$[?(@.url =~ /analytics/)]'], 'block');
139
+ *
140
+ * @example
141
+ * // Return a mock response with a 2-second simulated delay
142
+ * Mock.addListener('slow-login',
143
+ * ['$[?(@.url =~ /api\\/login/)]'],
144
+ * { status: 200, headers: { 'content-type': 'application/json' }, body: { token: 'abc' } },
145
+ * 2000
146
+ * );
147
+ */
148
+ static addListener(name, matchers, action, delayMs = 0) {
149
+ Utils.assertType(name, 'string', 'addListener', 'name');
150
+ if (name.trim().length === 0)
151
+ Mock.throwError('addListener', '[name] must not be empty');
152
+ if (!Array.isArray(matchers)) {
153
+ Mock.throwError('addListener', `[matchers] must be a string array, got [${typeof matchers}]`);
154
+ }
155
+ if (matchers.length === 0) {
156
+ Mock.throwError('addListener', '[matchers] array must not be empty');
157
+ }
158
+ matchers.forEach((matcher, index) => {
159
+ if (typeof matcher !== 'string') {
160
+ Mock.throwError('addListener', `[matchers[${index}]] must be a string, got [${typeof matcher}]`);
161
+ }
162
+ if (matcher.trim().length === 0) {
163
+ Mock.throwError('addListener', `[matchers[${index}]] must not be empty`);
164
+ }
165
+ try {
166
+ JSONPath({ path: matcher, json: {}, wrap: true });
167
+ }
168
+ catch (e) {
169
+ Mock.throwError('addListener', `[matchers[${index}]] is not valid JSONPath syntax: "${matcher}". ${e.message}`);
170
+ }
171
+ });
172
+ if (action !== 'block' && action !== 'passthrough') {
173
+ if (action === null || typeof action !== 'object' || Array.isArray(action)) {
174
+ Mock.throwError('addListener', `[action] must be 'block', 'passthrough', or a Mock.Response object, got [${action === null ? 'null' : typeof action}]`);
175
+ }
176
+ const response = action;
177
+ if (typeof response.status !== 'number' || !Number.isInteger(response.status) || response.status < 100 || response.status > 599) {
178
+ Mock.throwError('addListener', `[action.status] must be a valid HTTP status code (100-599), got [${response.status}]`);
179
+ }
180
+ if (response.headers !== undefined && (typeof response.headers !== 'object' || response.headers === null || Array.isArray(response.headers))) {
181
+ Mock.throwError('addListener', `[action.headers] must be a plain object if provided`);
182
+ }
183
+ }
184
+ Utils.assertType(delayMs, 'number', 'addListener', 'delayMs');
185
+ if (!Number.isFinite(delayMs) || delayMs < 0) {
186
+ Mock.throwError('addListener', `[delayMs] must be a non-negative finite number, got [${delayMs}]`);
187
+ }
188
+ if (Mock.listeners.has(name)) {
189
+ Log.writeLine(LogLevels.Warning, `Mock.addListener: replacing existing listener <${name}>`);
190
+ }
191
+ const isBlockOrPassthrough = action === 'block' || action === 'passthrough';
192
+ const actionText = (() => {
193
+ if (action === 'block')
194
+ return 'Block';
195
+ if (action === 'passthrough')
196
+ return 'Passthrough';
197
+ return JSON.stringify(action, null, 2);
198
+ })();
199
+ Log.writeLine(LogLevels.FrameworkInformation, `Add mock listener <${name}${delayMs > 0 ? ` (Delay by ${delayMs}mS)` : ''}>:\nMatcher: ${matchers.join('\nMatcher: ')}\nAction:${isBlockOrPassthrough ? ` ${actionText}` : `\n${actionText}`}`, { suppressMultilinePreamble: true });
200
+ Mock.listeners.set(name, { name, matchers, action, delayMs });
201
+ }
202
+ /**
203
+ * Removes a previously registered listener by name.
204
+ *
205
+ * If no listener with the given name exists, a warning is logged and the
206
+ * call is a no-op.
207
+ *
208
+ * @param name - Name of the listener to remove. Must be a non-empty string.
209
+ * @throws {Error} If `name` is not a non-empty string.
210
+ *
211
+ * @example
212
+ * Mock.removeListener('get-users');
213
+ */
214
+ static removeListener(name) {
215
+ Utils.assertType(name, 'string', 'removeListener', 'name');
216
+ if (name.trim().length === 0)
217
+ Mock.throwError('removeListener', '[name] must not be empty');
218
+ if (!Mock.listeners.has(name)) {
219
+ Log.writeLine(LogLevels.Warning, `Mock.removeListener: no listener named <${name}> exists — nothing removed`);
220
+ return;
221
+ }
222
+ Log.writeLine(LogLevels.FrameworkInformation, `Mock: Remove listener <${name}>`);
223
+ Mock.listeners.delete(name);
224
+ }
225
+ /**
226
+ * Removes all registered listeners.
227
+ *
228
+ * Transaction history and pending transactions are unaffected. Use
229
+ * {@link Mock.reset} to clear everything in a single call.
230
+ */
231
+ static clearListeners() {
232
+ Log.writeLine(LogLevels.FrameworkInformation, `Mock: Clear all listeners (${Mock.listeners.size} removed)`);
233
+ Mock.listeners.clear();
234
+ }
235
+ // ── Intercept ──────────────────────────────────────────────────────────────
236
+ /**
237
+ * Processes an intercepted HTTP request and returns a {@link Mock.InterceptResult}
238
+ * describing what the caller should do.
239
+ *
240
+ * Listeners are evaluated in registration order; the first full match wins.
241
+ * Every request — matched or not — is recorded in the transaction history.
242
+ * If the matching listener has a `delayMs`, the delay is applied before the
243
+ * result is returned.
244
+ *
245
+ * | Situation | `action` returned | Transaction type |
246
+ * |---|---|---|
247
+ * | Matched → mock response | `'fulfill'` | `'mocked'` |
248
+ * | Matched → passthrough, local mode | `'fulfill'` | `'passthrough'` |
249
+ * | Matched → passthrough, delegate mode | `'passthrough'` | pending until {@link Mock.completePassthrough} |
250
+ * | Matched → block | `'block'` | `'blocked'` |
251
+ * | No listener matched | `'block'` | `'unmatched'` |
252
+ *
253
+ * @param request - The intercepted request. Must be a non-null object with
254
+ * non-empty `url` and `method` string properties.
255
+ *
256
+ * @returns A promise resolving to a {@link Mock.InterceptResult}.
257
+ *
258
+ * @throws {Error} If `request` fails validation.
259
+ *
260
+ * @example
261
+ * const result = await Mock.intercept(request);
262
+ * if (result.action === 'fulfill') {
263
+ * respondWith(result.response);
264
+ * } else if (result.action === 'passthrough') {
265
+ * // delegate mode only
266
+ * const real = await fetch(request.url);
267
+ * Mock.completePassthrough(result.correlationId, real);
268
+ * respondWith(real);
269
+ * } else {
270
+ * abortRequest();
271
+ * }
272
+ */
273
+ static async intercept(request) {
274
+ if (request === null || request === undefined || typeof request !== 'object' || Array.isArray(request)) {
275
+ Mock.throwError('intercept', `[request] must be a non-null object, got [${request === null ? 'null' : typeof request}]`);
276
+ }
277
+ if (typeof request.url !== 'string' || request.url.trim().length === 0) {
278
+ Mock.throwError('intercept', `[request.url] must be a non-empty string, got [${typeof request.url}]`);
279
+ }
280
+ if (typeof request.method !== 'string' || request.method.trim().length === 0) {
281
+ Mock.throwError('intercept', `[request.method] must be a non-empty string, got [${typeof request.method}]`);
282
+ }
283
+ if (request.headers !== undefined && request.headers !== null && (typeof request.headers !== 'object' || Array.isArray(request.headers))) {
284
+ Mock.throwError('intercept', `[request.headers] must be a plain object if provided`);
285
+ }
286
+ Log.writeLine(LogLevels.FrameworkDebug, `Mock.intercept: ${request.method} ${request.url}`);
287
+ const listener = Mock.findMatch(request);
288
+ if (!listener) {
289
+ Log.writeLine(LogLevels.FrameworkDebug, `Mock.intercept: no listener matched — blocking`);
290
+ Mock.record({ type: 'unmatched', request });
291
+ return { action: 'block' };
292
+ }
293
+ Log.writeLine(LogLevels.FrameworkDebug, `Mock.intercept: matched listener <${listener.name}>`);
294
+ if (listener.delayMs > 0) {
295
+ Log.writeLine(LogLevels.FrameworkDebug, `Mock.intercept: delaying ${listener.delayMs}ms`);
296
+ await Utils.sleep(listener.delayMs);
297
+ }
298
+ if (listener.action === 'block') {
299
+ Log.writeLine(LogLevels.FrameworkDebug, `Mock.intercept: blocking request`);
300
+ Mock.record({ type: 'blocked', listenerName: listener.name, request });
301
+ return { action: 'block' };
302
+ }
303
+ if (listener.action === 'passthrough') {
304
+ if (Mock._delegateMode) {
305
+ const correlationId = randomUUID();
306
+ Log.writeLine(LogLevels.FrameworkDebug, `Mock.intercept: delegating passthrough (correlationId: ${correlationId})`);
307
+ Mock.pendingTransactions.set(correlationId, {
308
+ correlationId,
309
+ timestamp: new Date(),
310
+ listenerName: listener.name,
311
+ request,
312
+ });
313
+ return { action: 'passthrough', correlationId };
314
+ }
315
+ Log.writeLine(LogLevels.FrameworkDebug, `Mock.intercept: passing through to real endpoint`);
316
+ const response = await Mock.fetchReal(request);
317
+ Mock.record({ type: 'passthrough', listenerName: listener.name, request, response });
318
+ return { action: 'fulfill', response };
319
+ }
320
+ const response = listener.action;
321
+ Log.writeLine(LogLevels.FrameworkDebug, `Mock.intercept: returning mocked response (status ${response.status})`);
322
+ Mock.record({ type: 'mocked', listenerName: listener.name, request, response });
323
+ return { action: 'fulfill', response };
324
+ }
325
+ /**
326
+ * Completes a delegated passthrough by supplying the real response the caller
327
+ * fetched. Records the full transaction and removes the pending entry.
328
+ *
329
+ * Only relevant when {@link Mock.delegateMode} is `true`. The `correlationId`
330
+ * must match one previously returned by {@link Mock.intercept}.
331
+ *
332
+ * @param correlationId - The UUID returned in the `action: 'passthrough'`
333
+ * result from {@link Mock.intercept}. Must be a non-empty string that
334
+ * matches a pending transaction.
335
+ *
336
+ * @param response - The real response the caller received from the endpoint.
337
+ * Must be a valid {@link Mock.Response} with a status code of 100–599.
338
+ *
339
+ * @throws {Error} If `correlationId` is invalid, unknown, or already completed.
340
+ * @throws {Error} If `response` fails validation.
341
+ *
342
+ * @example
343
+ * const result = await Mock.intercept(request);
344
+ * if (result.action === 'passthrough') {
345
+ * const real = await myFetch(request);
346
+ * Mock.completePassthrough(result.correlationId, real);
347
+ * }
348
+ */
349
+ static completePassthrough(correlationId, response) {
350
+ Utils.assertType(correlationId, 'string', 'completePassthrough', 'correlationId');
351
+ if (correlationId.trim().length === 0) {
352
+ Mock.throwError('completePassthrough', '[correlationId] must not be empty');
353
+ }
354
+ if (!Mock.pendingTransactions.has(correlationId)) {
355
+ Mock.throwError('completePassthrough', `[correlationId] "${correlationId}" does not match any pending passthrough — it may be unknown, already completed, or cleared by reset()`);
356
+ }
357
+ if (response === null || response === undefined || typeof response !== 'object' || Array.isArray(response)) {
358
+ Mock.throwError('completePassthrough', `[response] must be a non-null object, got [${response === null ? 'null' : typeof response}]`);
359
+ }
360
+ if (typeof response.status !== 'number' || !Number.isInteger(response.status) || response.status < 100 || response.status > 599) {
361
+ Mock.throwError('completePassthrough', `[response.status] must be a valid HTTP status code (100-599), got [${response.status}]`);
362
+ }
363
+ if (response.headers !== undefined && (typeof response.headers !== 'object' || response.headers === null || Array.isArray(response.headers))) {
364
+ Mock.throwError('completePassthrough', `[response.headers] must be a plain object if provided`);
365
+ }
366
+ const pending = Mock.pendingTransactions.get(correlationId);
367
+ Log.writeLine(LogLevels.FrameworkDebug, `Mock.completePassthrough: completing passthrough for correlationId ${correlationId} (status ${response.status})`);
368
+ Mock.pendingTransactions.delete(correlationId);
369
+ Mock.record({ type: 'passthrough', listenerName: pending.listenerName, request: pending.request, response });
370
+ }
371
+ // ── Transaction history ────────────────────────────────────────────────────
372
+ /**
373
+ * Returns all completed transactions in chronological order.
374
+ *
375
+ * Includes mocked, passthrough (completed), blocked, and unmatched entries.
376
+ * Delegated passthroughs awaiting {@link Mock.completePassthrough} appear in
377
+ * {@link Mock.getPendingTransactions} instead.
378
+ *
379
+ * @returns A read-only array of {@link Mock.Transaction} objects.
380
+ *
381
+ * @example
382
+ * const blocked = Mock.getTransactions().filter(t => t.type === 'blocked');
383
+ */
384
+ static getTransactions() {
385
+ return Mock.transactions;
386
+ }
387
+ /**
388
+ * Clears all completed transactions and resets the transaction counter.
389
+ *
390
+ * Pending passthrough transactions and registered listeners are unaffected.
391
+ * Use {@link Mock.reset} to clear everything in a single call.
392
+ */
393
+ static clearTransactions() {
394
+ Log.writeLine(LogLevels.FrameworkInformation, `Mock: Clear transaction history (${Mock.transactions.length} removed)`);
395
+ Mock.transactions = [];
396
+ Mock.transactionCounter = 0;
397
+ }
398
+ /**
399
+ * Returns all delegated passthrough transactions that have not yet been
400
+ * completed via {@link Mock.completePassthrough}.
401
+ *
402
+ * Use this in test assertions to verify no passthrough calls were abandoned,
403
+ * or to diagnose remote fetch failures.
404
+ *
405
+ * Only populated when {@link Mock.delegateMode} is `true`.
406
+ *
407
+ * @returns A read-only array of {@link Mock.PendingTransaction} objects.
408
+ *
409
+ * @example
410
+ * // Assert no orphaned passthroughs after a test
411
+ * expect(Mock.getPendingTransactions()).toHaveLength(0);
412
+ */
413
+ static getPendingTransactions() {
414
+ return [...Mock.pendingTransactions.values()];
415
+ }
416
+ // ── Reset ──────────────────────────────────────────────────────────────────
417
+ /**
418
+ * Clears all listeners, completed transactions, pending transactions, and
419
+ * resets delegate mode to `false`.
420
+ *
421
+ * Call this in `beforeEach` or `afterEach` hooks to guarantee a clean slate
422
+ * between tests.
423
+ *
424
+ * @example
425
+ * beforeEach(() => {
426
+ * Mock.reset();
427
+ * Mock.addListener('static-assets', [...], 'passthrough');
428
+ * });
429
+ */
430
+ static reset() {
431
+ Log.writeLine(LogLevels.FrameworkInformation, `Mock: Reset (${Mock.listeners.size} listeners, ${Mock.transactions.length} transactions, ${Mock.pendingTransactions.size} pending cleared)`);
432
+ Mock.listeners.clear();
433
+ Mock.transactions = [];
434
+ Mock.transactionCounter = 0;
435
+ Mock.pendingTransactions.clear();
436
+ Mock._delegateMode = false;
437
+ }
438
+ // ── Private helpers ────────────────────────────────────────────────────────
439
+ static throwError(funcName, message) {
440
+ const errorText = `Mock.${funcName}: ${message}`;
441
+ Log.writeLine(LogLevels.Error, errorText, { stackOffset: 1 });
442
+ throw new Error(errorText);
443
+ }
444
+ static findMatch(request) {
445
+ for (const listener of Mock.listeners.values()) {
446
+ try {
447
+ const allMatch = listener.matchers.every((path) => {
448
+ try {
449
+ const results = JSONPath({ path, json: request, wrap: true });
450
+ return Array.isArray(results) && results.length > 0;
451
+ }
452
+ catch (e) {
453
+ Log.writeLine(LogLevels.Warning, `Mock: JSONPath evaluation error in listener <${listener.name}> for path "${path}": ${e.message} — treating as non-match`);
454
+ return false;
455
+ }
456
+ });
457
+ if (allMatch)
458
+ return listener;
459
+ }
460
+ catch (e) {
461
+ Log.writeLine(LogLevels.Warning, `Mock: Unexpected error evaluating listener <${listener.name}>: ${e.message} — skipping`);
462
+ }
463
+ }
464
+ return undefined;
465
+ }
466
+ static async fetchReal(request) {
467
+ Log.writeLine(LogLevels.FrameworkDebug, `Mock.fetchReal: ${request.method} ${request.url}`);
468
+ try {
469
+ const response = await fetch(request.url, {
470
+ method: request.method,
471
+ headers: request.headers,
472
+ body: request.body != null ? JSON.stringify(request.body) : undefined,
473
+ });
474
+ const body = await response.text().then((text) => {
475
+ try {
476
+ return JSON.parse(text);
477
+ }
478
+ catch {
479
+ return text;
480
+ }
481
+ });
482
+ const headers = {};
483
+ response.headers.forEach((value, key) => { headers[key] = value; });
484
+ Log.writeLine(LogLevels.FrameworkDebug, `Mock.fetchReal: received ${response.status} from ${request.url}`);
485
+ return { status: response.status, headers, body };
486
+ }
487
+ catch (e) {
488
+ const message = e.message;
489
+ Log.writeLine(LogLevels.Error, `Mock.fetchReal: network error fetching ${request.url}: ${message}`);
490
+ return { status: 502, headers: {}, body: `Mock passthrough network error: ${message}` };
491
+ }
492
+ }
493
+ static record(entry) {
494
+ Mock.transactions.push({
495
+ id: `txn-${++Mock.transactionCounter}`,
496
+ timestamp: new Date(),
497
+ ...entry,
498
+ });
499
+ }
500
+ }
501
+ Mock.listeners = new Map();
502
+ Mock.transactions = [];
503
+ Mock.transactionCounter = 0;
504
+ Mock.pendingTransactions = new Map();
505
+ Mock._delegateMode = false;