aeon-test 0.1.0

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/README.md ADDED
@@ -0,0 +1,47 @@
1
+ # aeon-test
2
+
3
+ Marble testing utilities and helpers for [Aeon](https://github.com/joshburgess/aeon).
4
+
5
+ This package provides:
6
+
7
+ - **`marble`** — RxJS-style marble syntax for declaratively constructing test events
8
+ - **`collect`** — synchronously collect all emitted values from an event into an array
9
+ - **`testEvent`** — helpers for building events that fire at specific virtual times
10
+ - **`assert`** — assertion helpers for comparing event sequences
11
+
12
+ For the underlying virtual scheduler, see [`aeon-scheduler`](https://www.npmjs.com/package/aeon-scheduler) (`VirtualScheduler`).
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ pnpm add -D aeon-test aeon-scheduler
18
+ ```
19
+
20
+ ## Quick Start
21
+
22
+ ```typescript
23
+ import { describe, it, expect } from "vitest";
24
+ import { map } from "aeon-core";
25
+ import { VirtualScheduler } from "aeon-scheduler";
26
+ import { collect } from "aeon-test";
27
+
28
+ describe("map", () => {
29
+ it("doubles each value", () => {
30
+ const scheduler = new VirtualScheduler();
31
+ const result = collect(
32
+ map((x: number) => x * 2, fromArray([1, 2, 3])),
33
+ scheduler,
34
+ );
35
+ expect(result).toEqual([2, 4, 6]);
36
+ });
37
+ });
38
+ ```
39
+
40
+ ## Documentation
41
+
42
+ - [Main README](https://github.com/joshburgess/aeon#readme)
43
+ - [Getting Started](https://github.com/joshburgess/aeon/blob/main/docs/getting-started.md)
44
+
45
+ ## License
46
+
47
+ MIT
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Assertion helpers for marble-based stream testing.
3
+ */
4
+ import type { CollectedEntry } from "./collect.js";
5
+ import type { MarbleEntry } from "./marble.js";
6
+ /**
7
+ * Compare collected entries against expected marble entries.
8
+ *
9
+ * Returns `{ pass: true }` if they match, or `{ pass: false, message }`
10
+ * with a human-readable diff if they don't.
11
+ *
12
+ * This is framework-agnostic — use it with any assertion library:
13
+ * ```typescript
14
+ * const check = assertEvents(result.entries, expected);
15
+ * if (!check.pass) throw new Error(check.message);
16
+ * ```
17
+ */
18
+ export declare const assertEvents: <A, E>(actual: readonly CollectedEntry<A, E>[], expected: readonly MarbleEntry<A, E>[]) => {
19
+ pass: true;
20
+ } | {
21
+ pass: false;
22
+ message: string;
23
+ };
24
+ //# sourceMappingURL=assert.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assert.d.ts","sourceRoot":"","sources":["../src/assert.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,YAAY,GAAI,CAAC,EAAE,CAAC,EAC/B,QAAQ,SAAS,cAAc,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EACvC,UAAU,SAAS,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,KACrC;IAAE,IAAI,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAuDjD,CAAC"}
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Test collection helpers.
3
+ *
4
+ * Helpers for running a pulse Event and collecting its output
5
+ * as a structured list of timed entries for assertion.
6
+ */
7
+ import type { Disposable, Event as PulseEvent, Scheduler, Time } from "aeon-types";
8
+ /** A collected event entry (value, error, or end). */
9
+ export type CollectedEntry<A, E = never> = {
10
+ readonly type: "event";
11
+ readonly time: Time;
12
+ readonly value: A;
13
+ } | {
14
+ readonly type: "error";
15
+ readonly time: Time;
16
+ readonly error: E;
17
+ } | {
18
+ readonly type: "end";
19
+ readonly time: Time;
20
+ };
21
+ /** Result of collecting events from a stream. */
22
+ export interface CollectResult<A, E = never> {
23
+ /** All collected entries in order. */
24
+ readonly entries: CollectedEntry<A, E>[];
25
+ /** Just the values (convenience accessor). */
26
+ readonly values: A[];
27
+ /** Whether the stream ended. */
28
+ readonly ended: boolean;
29
+ /** Whether the stream errored. */
30
+ readonly errored: boolean;
31
+ /** The error value, if any. */
32
+ readonly error: E | undefined;
33
+ /** Disposable to stop collection. */
34
+ readonly disposable: Disposable;
35
+ }
36
+ /**
37
+ * Subscribe to a pulse Event and collect all emissions.
38
+ *
39
+ * Returns a CollectResult whose `entries`, `values`, `ended`, etc.
40
+ * update live as the scheduler advances. Use with VirtualScheduler:
41
+ *
42
+ * ```typescript
43
+ * const scheduler = new VirtualScheduler();
44
+ * const result = collectEvents(myEvent, scheduler);
45
+ * scheduler.advanceTo(toTime(100));
46
+ * expect(result.values).toEqual([1, 2, 3]);
47
+ * expect(result.ended).toBe(true);
48
+ * result.disposable.dispose();
49
+ * ```
50
+ */
51
+ export declare const collectEvents: <A, E = never>(event: PulseEvent<A, E>, scheduler: Scheduler) => CollectResult<A, E>;
52
+ /**
53
+ * Collect all values from a synchronous Event.
54
+ *
55
+ * For events backed by synchronous sources (fromArray, now, empty),
56
+ * all values are available immediately without advancing time.
57
+ */
58
+ export declare const collectSync: <A>(event: PulseEvent<A, never>, scheduler: Scheduler) => A[];
59
+ //# sourceMappingURL=collect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collect.d.ts","sourceRoot":"","sources":["../src/collect.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,KAAK,IAAI,UAAU,EAAE,SAAS,EAAgB,IAAI,EAAE,MAAM,YAAY,CAAC;AAEjG,sDAAsD;AACtD,MAAM,MAAM,cAAc,CAAC,CAAC,EAAE,CAAC,GAAG,KAAK,IACnC;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAA;CAAE,GAClE;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAA;CAAE,GAClE;IAAE,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAA;CAAE,CAAC;AAElD,iDAAiD;AACjD,MAAM,WAAW,aAAa,CAAC,CAAC,EAAE,CAAC,GAAG,KAAK;IACzC,sCAAsC;IACtC,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;IACzC,8CAA8C;IAC9C,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC;IACrB,gCAAgC;IAChC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,kCAAkC;IAClC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,+BAA+B;IAC/B,QAAQ,CAAC,KAAK,EAAE,CAAC,GAAG,SAAS,CAAC;IAC9B,qCAAqC;IACrC,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAC;CACjC;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,aAAa,GAAI,CAAC,EAAE,CAAC,GAAG,KAAK,EACxC,OAAO,UAAU,CAAC,CAAC,EAAE,CAAC,CAAC,EACvB,WAAW,SAAS,KACnB,aAAa,CAAC,CAAC,EAAE,CAAC,CAiCpB,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GAAI,CAAC,EAAE,OAAO,UAAU,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,WAAW,SAAS,KAAG,CAAC,EAInF,CAAC"}
package/dist/index.cjs ADDED
@@ -0,0 +1,521 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Branded types for temporal values.
5
+ *
6
+ * Branding prevents accidental mixing of Time, Duration, and Offset
7
+ * at the type level while remaining plain numbers at runtime.
8
+ */ // --- Constructors ---
9
+ /** Wrap a raw millisecond value as a Time. */ const toTime$1 = (ms)=>ms;
10
+ /** Advance a Time by a Duration. */ const timeAdd = (t, d)=>t + d;
11
+ // --- Constants ---
12
+ /** Time zero — the epoch. */ const TIME_ZERO = 0;
13
+ /** Manually advanceable clock for deterministic testing. */ let VirtualClock = class VirtualClock {
14
+ constructor(initialTime = toTime$1(0)){
15
+ this.time = initialTime;
16
+ }
17
+ now() {
18
+ return this.time;
19
+ }
20
+ setTime(time) {
21
+ this.time = time;
22
+ }
23
+ advance(ms) {
24
+ this.time = toTime$1(this.time + ms);
25
+ }
26
+ };
27
+ /**
28
+ * Binary min-heap for the scheduler's timer queue.
29
+ *
30
+ * Keyed on a numeric priority (scheduled time). Uses a pre-allocated
31
+ * backing array to minimize per-node allocation.
32
+ */ let BinaryHeap = class BinaryHeap {
33
+ constructor(initialCapacity = 64){
34
+ this.items = [];
35
+ // Pre-allocate hint (V8 will grow as needed)
36
+ if (initialCapacity > 0) {
37
+ this.items.length = 0;
38
+ }
39
+ }
40
+ get size() {
41
+ return this.items.length;
42
+ }
43
+ peek() {
44
+ return this.items[0];
45
+ }
46
+ insert(value, priority) {
47
+ const entry = {
48
+ value,
49
+ priority,
50
+ index: this.items.length
51
+ };
52
+ this.items.push(entry);
53
+ this.siftUp(entry.index);
54
+ return entry;
55
+ }
56
+ extractMin() {
57
+ if (this.items.length === 0) return undefined;
58
+ const min = this.items[0];
59
+ this.removeAt(0);
60
+ return min;
61
+ }
62
+ remove(entry) {
63
+ if (entry.index < 0 || entry.index >= this.items.length) return false;
64
+ if (this.items[entry.index] !== entry) return false;
65
+ this.removeAt(entry.index);
66
+ return true;
67
+ }
68
+ removeAt(index) {
69
+ const last = this.items.length - 1;
70
+ if (index === last) {
71
+ this.items.pop();
72
+ return;
73
+ }
74
+ const moved = this.items[last];
75
+ this.items[index] = moved;
76
+ moved.index = index;
77
+ this.items.pop();
78
+ // Restore heap property
79
+ const parent = index - 1 >>> 1;
80
+ if (index > 0 && moved.priority < this.items[parent].priority) {
81
+ this.siftUp(index);
82
+ } else {
83
+ this.siftDown(index);
84
+ }
85
+ }
86
+ siftUp(index) {
87
+ const item = this.items[index];
88
+ while(index > 0){
89
+ const parentIndex = index - 1 >>> 1;
90
+ const parent = this.items[parentIndex];
91
+ if (item.priority >= parent.priority) break;
92
+ this.items[index] = parent;
93
+ parent.index = index;
94
+ index = parentIndex;
95
+ }
96
+ this.items[index] = item;
97
+ item.index = index;
98
+ }
99
+ siftDown(index) {
100
+ const item = this.items[index];
101
+ const halfLength = this.items.length >>> 1;
102
+ while(index < halfLength){
103
+ let childIndex = 2 * index + 1;
104
+ let child = this.items[childIndex];
105
+ const rightIndex = childIndex + 1;
106
+ if (rightIndex < this.items.length && this.items[rightIndex].priority < child.priority) {
107
+ childIndex = rightIndex;
108
+ child = this.items[rightIndex];
109
+ }
110
+ if (item.priority <= child.priority) break;
111
+ this.items[index] = child;
112
+ child.index = index;
113
+ index = childIndex;
114
+ }
115
+ this.items[index] = item;
116
+ item.index = index;
117
+ }
118
+ };
119
+ let VirtualScheduler = class VirtualScheduler {
120
+ constructor(initialTime = TIME_ZERO){
121
+ this.clock = new VirtualClock(initialTime);
122
+ this.heap = new BinaryHeap();
123
+ }
124
+ currentTime() {
125
+ return this.clock.now();
126
+ }
127
+ scheduleTask(delay, task) {
128
+ const time = timeAdd(this.clock.now(), delay);
129
+ const pending = {
130
+ task,
131
+ time,
132
+ cancelled: false,
133
+ heapEntry: undefined
134
+ };
135
+ pending.heapEntry = this.heap.insert(pending, time);
136
+ return {
137
+ task,
138
+ time,
139
+ dispose () {
140
+ if (!pending.cancelled) {
141
+ pending.cancelled = true;
142
+ if (pending.heapEntry) ;
143
+ }
144
+ }
145
+ };
146
+ }
147
+ relative(offset) {
148
+ return new VirtualRelativeScheduler(offset, this);
149
+ }
150
+ cancelTask(st) {
151
+ st.dispose();
152
+ }
153
+ /** Advance time by a duration, executing all tasks that fall within range. */ advance(duration) {
154
+ const target = timeAdd(this.clock.now(), duration);
155
+ this.advanceTo(target);
156
+ }
157
+ /** Advance to an exact time, executing all tasks up to and including that time. */ advanceTo(time) {
158
+ while(this.heap.size > 0){
159
+ const next = this.heap.peek();
160
+ if (next.priority > time) break;
161
+ this.heap.extractMin();
162
+ const pending = next.value;
163
+ if (!pending.cancelled) {
164
+ this.clock.setTime(pending.time);
165
+ try {
166
+ pending.task.run(pending.time);
167
+ } catch (err) {
168
+ pending.task.error(pending.time, err);
169
+ }
170
+ }
171
+ }
172
+ this.clock.setTime(time);
173
+ }
174
+ /** Execute all pending tasks regardless of time. */ flush() {
175
+ while(this.heap.size > 0){
176
+ const next = this.heap.extractMin();
177
+ const pending = next.value;
178
+ if (!pending.cancelled) {
179
+ this.clock.setTime(pending.time);
180
+ try {
181
+ pending.task.run(pending.time);
182
+ } catch (err) {
183
+ pending.task.error(pending.time, err);
184
+ }
185
+ }
186
+ }
187
+ }
188
+ /** Number of pending (non-cancelled) tasks. */ get pendingCount() {
189
+ return this.heap.size;
190
+ }
191
+ };
192
+ let VirtualRelativeScheduler = class VirtualRelativeScheduler {
193
+ constructor(offset, parent){
194
+ this.offset = offset;
195
+ this.parent = parent;
196
+ }
197
+ currentTime() {
198
+ return toTime$1(this.parent.currentTime() + this.offset);
199
+ }
200
+ scheduleTask(delay, task) {
201
+ return this.parent.scheduleTask(delay, task);
202
+ }
203
+ relative(offset) {
204
+ return new VirtualRelativeScheduler(toTime$1(this.offset + offset), this.parent);
205
+ }
206
+ cancelTask(st) {
207
+ this.parent.cancelTask(st);
208
+ }
209
+ };
210
+
211
+ /**
212
+ * Branded types for temporal values.
213
+ *
214
+ * Branding prevents accidental mixing of Time, Duration, and Offset
215
+ * at the type level while remaining plain numbers at runtime.
216
+ */ // --- Constructors ---
217
+ /** Wrap a raw millisecond value as a Time. */ const toTime = (ms)=>ms;
218
+ /** Wrap a raw millisecond value as a Duration. */ const toDuration = (ms)=>ms;
219
+
220
+ /**
221
+ * Parse a marble string into a sequence of timed entries.
222
+ *
223
+ * @param marble - The marble notation string
224
+ * @param values - Map from single-character keys to values
225
+ * @param error - The error value for `#`
226
+ * @param timeUnit - Milliseconds per time unit (default: 1)
227
+ */ const parseMarble = (marble, values, error, timeUnit = 1)=>{
228
+ const entries = [];
229
+ let time = 0;
230
+ let inGroup = false;
231
+ for(let i = 0; i < marble.length; i++){
232
+ const ch = marble[i];
233
+ switch(ch){
234
+ case "-":
235
+ if (!inGroup) time += timeUnit;
236
+ break;
237
+ case "(":
238
+ inGroup = true;
239
+ break;
240
+ case ")":
241
+ inGroup = false;
242
+ if (!inGroup) time += timeUnit;
243
+ break;
244
+ case "|":
245
+ entries.push({
246
+ type: "end",
247
+ time: toTime(time)
248
+ });
249
+ if (!inGroup) time += timeUnit;
250
+ break;
251
+ case "#":
252
+ entries.push({
253
+ type: "error",
254
+ time: toTime(time),
255
+ error: error
256
+ });
257
+ if (!inGroup) time += timeUnit;
258
+ break;
259
+ case " ":
260
+ break;
261
+ default:
262
+ {
263
+ const value = values[ch];
264
+ if (value === undefined) {
265
+ throw new Error(`Marble character '${ch}' not found in values map`);
266
+ }
267
+ entries.push({
268
+ type: "event",
269
+ time: toTime(time),
270
+ value
271
+ });
272
+ if (!inGroup) time += timeUnit;
273
+ break;
274
+ }
275
+ }
276
+ }
277
+ return entries;
278
+ };
279
+ /**
280
+ * Compute the total duration of a marble string (in time units).
281
+ */ const marbleDuration = (marble, timeUnit = 1)=>{
282
+ let time = 0;
283
+ let inGroup = false;
284
+ for(let i = 0; i < marble.length; i++){
285
+ const ch = marble[i];
286
+ if (ch === "(") {
287
+ inGroup = true;
288
+ continue;
289
+ }
290
+ if (ch === ")") {
291
+ inGroup = false;
292
+ time += timeUnit;
293
+ continue;
294
+ }
295
+ if (ch === " ") continue;
296
+ if (!inGroup) time += timeUnit;
297
+ }
298
+ return time;
299
+ };
300
+
301
+ let MarbleSource = class MarbleSource {
302
+ constructor(entries){
303
+ this.entries = entries;
304
+ }
305
+ run(sink, scheduler) {
306
+ const disposables = [];
307
+ const currentTime = scheduler.currentTime();
308
+ for (const entry of this.entries){
309
+ const delay = toDuration(entry.time - currentTime);
310
+ switch(entry.type){
311
+ case "event":
312
+ {
313
+ const value = entry.value;
314
+ disposables.push(scheduler.scheduleTask(delay, {
315
+ run (t) {
316
+ sink.event(t, value);
317
+ },
318
+ error (t, err) {
319
+ sink.error(t, err);
320
+ },
321
+ dispose () {}
322
+ }));
323
+ break;
324
+ }
325
+ case "error":
326
+ {
327
+ const error = entry.error;
328
+ disposables.push(scheduler.scheduleTask(delay, {
329
+ run (t) {
330
+ sink.error(t, error);
331
+ },
332
+ error () {},
333
+ dispose () {}
334
+ }));
335
+ break;
336
+ }
337
+ case "end":
338
+ {
339
+ disposables.push(scheduler.scheduleTask(delay, {
340
+ run (t) {
341
+ sink.end(t);
342
+ },
343
+ error () {},
344
+ dispose () {}
345
+ }));
346
+ break;
347
+ }
348
+ }
349
+ }
350
+ return {
351
+ dispose () {
352
+ for (const d of disposables)d.dispose();
353
+ }
354
+ };
355
+ }
356
+ };
357
+ /**
358
+ * Create a pulse Event from a marble string.
359
+ *
360
+ * Events are scheduled on the provided scheduler. Use with VirtualScheduler
361
+ * and advance/flush to control time.
362
+ *
363
+ * @param marble - Marble notation string
364
+ * @param values - Map from single-character keys to values
365
+ * @param error - Error value for `#` in the marble string
366
+ * @param timeUnit - Milliseconds per time unit (default: 1)
367
+ */ const testEvent = (marble, values, error, timeUnit)=>{
368
+ const entries = parseMarble(marble, values, error, timeUnit);
369
+ return new MarbleSource(entries);
370
+ };
371
+
372
+ /**
373
+ * Test collection helpers.
374
+ *
375
+ * Helpers for running a pulse Event and collecting its output
376
+ * as a structured list of timed entries for assertion.
377
+ */ /**
378
+ * Subscribe to a pulse Event and collect all emissions.
379
+ *
380
+ * Returns a CollectResult whose `entries`, `values`, `ended`, etc.
381
+ * update live as the scheduler advances. Use with VirtualScheduler:
382
+ *
383
+ * ```typescript
384
+ * const scheduler = new VirtualScheduler();
385
+ * const result = collectEvents(myEvent, scheduler);
386
+ * scheduler.advanceTo(toTime(100));
387
+ * expect(result.values).toEqual([1, 2, 3]);
388
+ * expect(result.ended).toBe(true);
389
+ * result.disposable.dispose();
390
+ * ```
391
+ */ const collectEvents = (event, scheduler)=>{
392
+ const entries = [];
393
+ const values = [];
394
+ const result = {
395
+ entries,
396
+ values,
397
+ ended: false,
398
+ errored: false,
399
+ error: undefined,
400
+ disposable: {
401
+ dispose () {}
402
+ }
403
+ };
404
+ const sink = {
405
+ event (time, value) {
406
+ entries.push({
407
+ type: "event",
408
+ time,
409
+ value
410
+ });
411
+ values.push(value);
412
+ },
413
+ error (time, err) {
414
+ entries.push({
415
+ type: "error",
416
+ time,
417
+ error: err
418
+ });
419
+ result.errored = true;
420
+ result.error = err;
421
+ },
422
+ end (time) {
423
+ entries.push({
424
+ type: "end",
425
+ time
426
+ });
427
+ result.ended = true;
428
+ }
429
+ };
430
+ const source = event;
431
+ const disposable = source.run(sink, scheduler);
432
+ result.disposable = disposable;
433
+ return result;
434
+ };
435
+ /**
436
+ * Collect all values from a synchronous Event.
437
+ *
438
+ * For events backed by synchronous sources (fromArray, now, empty),
439
+ * all values are available immediately without advancing time.
440
+ */ const collectSync = (event, scheduler)=>{
441
+ const result = collectEvents(event, scheduler);
442
+ result.disposable.dispose();
443
+ return result.values;
444
+ };
445
+
446
+ /**
447
+ * Assertion helpers for marble-based stream testing.
448
+ */ /**
449
+ * Compare collected entries against expected marble entries.
450
+ *
451
+ * Returns `{ pass: true }` if they match, or `{ pass: false, message }`
452
+ * with a human-readable diff if they don't.
453
+ *
454
+ * This is framework-agnostic — use it with any assertion library:
455
+ * ```typescript
456
+ * const check = assertEvents(result.entries, expected);
457
+ * if (!check.pass) throw new Error(check.message);
458
+ * ```
459
+ */ const assertEvents = (actual, expected)=>{
460
+ if (actual.length !== expected.length) {
461
+ return {
462
+ pass: false,
463
+ message: `Expected ${expected.length} entries, got ${actual.length}.\n` + ` actual: ${formatEntries(actual)}\n` + ` expected: ${formatEntries(expected)}`
464
+ };
465
+ }
466
+ for(let i = 0; i < actual.length; i++){
467
+ const a = actual[i];
468
+ const e = expected[i];
469
+ if (a.type !== e.type) {
470
+ return {
471
+ pass: false,
472
+ message: `Entry ${i}: expected type '${e.type}', got '${a.type}'.\n` + ` actual: ${formatEntry(a)}\n` + ` expected: ${formatEntry(e)}`
473
+ };
474
+ }
475
+ if (a.time !== e.time) {
476
+ return {
477
+ pass: false,
478
+ message: `Entry ${i}: expected time ${e.time}, got ${a.time}.\n` + ` actual: ${formatEntry(a)}\n` + ` expected: ${formatEntry(e)}`
479
+ };
480
+ }
481
+ if (a.type === "event" && e.type === "event") {
482
+ if (!Object.is(a.value, e.value)) {
483
+ return {
484
+ pass: false,
485
+ message: `Entry ${i}: expected value ${JSON.stringify(e.value)}, got ${JSON.stringify(a.value)}.`
486
+ };
487
+ }
488
+ }
489
+ if (a.type === "error" && e.type === "error") {
490
+ if (!Object.is(a.error, e.error)) {
491
+ return {
492
+ pass: false,
493
+ message: `Entry ${i}: expected error ${JSON.stringify(e.error)}, got ${JSON.stringify(a.error)}.`
494
+ };
495
+ }
496
+ }
497
+ }
498
+ return {
499
+ pass: true
500
+ };
501
+ };
502
+ const formatEntry = (e)=>{
503
+ switch(e.type){
504
+ case "event":
505
+ return `event(${e.time}, ${JSON.stringify(e.value)})`;
506
+ case "error":
507
+ return `error(${e.time}, ${JSON.stringify(e.error)})`;
508
+ case "end":
509
+ return `end(${e.time})`;
510
+ }
511
+ };
512
+ const formatEntries = (entries)=>`[${entries.map(formatEntry).join(", ")}]`;
513
+
514
+ exports.VirtualScheduler = VirtualScheduler;
515
+ exports.assertEvents = assertEvents;
516
+ exports.collectEvents = collectEvents;
517
+ exports.collectSync = collectSync;
518
+ exports.marbleDuration = marbleDuration;
519
+ exports.parseMarble = parseMarble;
520
+ exports.testEvent = testEvent;
521
+ //# sourceMappingURL=index.cjs.map