@ricsam/isolate-test-environment 0.1.11 → 0.1.13

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.
@@ -6,6 +6,26 @@ var testEnvironmentCode = `
6
6
  // Internal State
7
7
  // ============================================================
8
8
 
9
+ // Mock registry and call counter
10
+ let __mockCallOrder = 0;
11
+ const __mockRegistry = [];
12
+
13
+ // Assertion counting state
14
+ let __expectedAssertions = null;
15
+ let __assertionCount = 0;
16
+ let __hasAssertionsFlag = false;
17
+
18
+ function createMockState() {
19
+ return {
20
+ calls: [],
21
+ results: [],
22
+ contexts: [],
23
+ instances: [],
24
+ invocationCallOrder: [],
25
+ lastCall: undefined,
26
+ };
27
+ }
28
+
9
29
  function createSuite(name, skip = false, only = false) {
10
30
  return {
11
31
  name,
@@ -51,6 +71,34 @@ var testEnvironmentCode = `
51
71
  }
52
72
  }
53
73
 
74
+ // ============================================================
75
+ // Asymmetric Matcher Infrastructure
76
+ // ============================================================
77
+
78
+ const ASYMMETRIC_MATCHER = Symbol('asymmetricMatcher');
79
+
80
+ function isAsymmetricMatcher(obj) {
81
+ return obj && obj[ASYMMETRIC_MATCHER] === true;
82
+ }
83
+
84
+ // Deep equality with asymmetric matcher support
85
+ function asymmetricDeepEqual(a, b) {
86
+ if (isAsymmetricMatcher(b)) return b.asymmetricMatch(a);
87
+ if (isAsymmetricMatcher(a)) return a.asymmetricMatch(b);
88
+ if (a === b) return true;
89
+ if (typeof a !== typeof b) return false;
90
+ if (typeof a !== 'object' || a === null || b === null) return false;
91
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
92
+ const keysA = Object.keys(a);
93
+ const keysB = Object.keys(b);
94
+ if (keysA.length !== keysB.length) return false;
95
+ for (const key of keysA) {
96
+ if (!keysB.includes(key)) return false;
97
+ if (!asymmetricDeepEqual(a[key], b[key])) return false;
98
+ }
99
+ return true;
100
+ }
101
+
54
102
  // ============================================================
55
103
  // Deep Equality Helper
56
104
  // ============================================================
@@ -151,6 +199,7 @@ var testEnvironmentCode = `
151
199
  function expect(actual) {
152
200
  function createMatchers(negated = false) {
153
201
  const assert = (condition, message, matcherName, expected) => {
202
+ __assertionCount++;
154
203
  const pass = negated ? !condition : condition;
155
204
  if (!pass) {
156
205
  throw new TestError(message, matcherName, expected, actual);
@@ -171,7 +220,7 @@ var testEnvironmentCode = `
171
220
 
172
221
  toEqual(expected) {
173
222
  assert(
174
- deepEqual(actual, expected),
223
+ asymmetricDeepEqual(actual, expected),
175
224
  negated
176
225
  ? \`Expected \${formatValue(actual)} not to equal \${formatValue(expected)}\`
177
226
  : \`Expected \${formatValue(actual)} to equal \${formatValue(expected)}\`,
@@ -346,7 +395,7 @@ var testEnvironmentCode = `
346
395
  toHaveProperty(path, value) {
347
396
  const prop = getNestedProperty(actual, path);
348
397
  const hasProperty = prop.exists;
349
- const valueMatches = arguments.length < 2 || deepEqual(prop.value, value);
398
+ const valueMatches = arguments.length < 2 || asymmetricDeepEqual(prop.value, value);
350
399
 
351
400
  assert(
352
401
  hasProperty && valueMatches,
@@ -401,6 +450,163 @@ var testEnvironmentCode = `
401
450
  expected
402
451
  );
403
452
  },
453
+
454
+ toBeCloseTo(expected, numDigits = 2) {
455
+ const precision = Math.pow(10, -numDigits) / 2;
456
+ const pass = Math.abs(actual - expected) < precision;
457
+ assert(
458
+ pass,
459
+ negated
460
+ ? \`Expected \${formatValue(actual)} not to be close to \${formatValue(expected)} (precision: \${numDigits} digits)\`
461
+ : \`Expected \${formatValue(actual)} to be close to \${formatValue(expected)} (precision: \${numDigits} digits)\`,
462
+ 'toBeCloseTo',
463
+ expected
464
+ );
465
+ },
466
+
467
+ toBeNaN() {
468
+ assert(
469
+ Number.isNaN(actual),
470
+ negated
471
+ ? \`Expected \${formatValue(actual)} not to be NaN\`
472
+ : \`Expected \${formatValue(actual)} to be NaN\`,
473
+ 'toBeNaN',
474
+ NaN
475
+ );
476
+ },
477
+
478
+ toMatchObject(expected) {
479
+ function matchesObject(obj, pattern) {
480
+ if (isAsymmetricMatcher(pattern)) return pattern.asymmetricMatch(obj);
481
+ if (typeof pattern !== 'object' || pattern === null) return obj === pattern;
482
+ if (typeof obj !== 'object' || obj === null) return false;
483
+ for (const key of Object.keys(pattern)) {
484
+ if (!(key in obj)) return false;
485
+ if (!matchesObject(obj[key], pattern[key])) return false;
486
+ }
487
+ return true;
488
+ }
489
+ assert(
490
+ matchesObject(actual, expected),
491
+ negated
492
+ ? \`Expected \${formatValue(actual)} not to match object \${formatValue(expected)}\`
493
+ : \`Expected \${formatValue(actual)} to match object \${formatValue(expected)}\`,
494
+ 'toMatchObject',
495
+ expected
496
+ );
497
+ },
498
+
499
+ toContainEqual(item) {
500
+ const contains = Array.isArray(actual) && actual.some(el => asymmetricDeepEqual(el, item));
501
+ assert(
502
+ contains,
503
+ negated
504
+ ? \`Expected array not to contain equal \${formatValue(item)}\`
505
+ : \`Expected array to contain equal \${formatValue(item)}\`,
506
+ 'toContainEqual',
507
+ item
508
+ );
509
+ },
510
+
511
+ toBeTypeOf(expectedType) {
512
+ const actualType = typeof actual;
513
+ assert(
514
+ actualType === expectedType,
515
+ negated
516
+ ? \`Expected typeof \${formatValue(actual)} not to be "\${expectedType}"\`
517
+ : \`Expected typeof \${formatValue(actual)} to be "\${expectedType}", got "\${actualType}"\`,
518
+ 'toBeTypeOf',
519
+ expectedType
520
+ );
521
+ },
522
+
523
+ // Mock matchers
524
+ toHaveBeenCalled() {
525
+ if (!actual.__isMockFunction) throw new Error('toHaveBeenCalled requires a mock function');
526
+ assert(actual.mock.calls.length > 0,
527
+ negated ? \`Expected mock not to have been called\` : \`Expected mock to have been called\`,
528
+ 'toHaveBeenCalled', 'called');
529
+ },
530
+
531
+ toHaveBeenCalledTimes(n) {
532
+ if (!actual.__isMockFunction) throw new Error('toHaveBeenCalledTimes requires a mock function');
533
+ assert(actual.mock.calls.length === n,
534
+ negated ? \`Expected mock not to have been called \${n} times\`
535
+ : \`Expected mock to be called \${n} times, got \${actual.mock.calls.length}\`,
536
+ 'toHaveBeenCalledTimes', n);
537
+ },
538
+
539
+ toHaveBeenCalledWith(...expectedArgs) {
540
+ if (!actual.__isMockFunction) throw new Error('toHaveBeenCalledWith requires a mock function');
541
+ const match = actual.mock.calls.some(args => asymmetricDeepEqual(args, expectedArgs));
542
+ assert(match,
543
+ negated ? \`Expected mock not to have been called with \${formatValue(expectedArgs)}\`
544
+ : \`Expected mock to have been called with \${formatValue(expectedArgs)}\`,
545
+ 'toHaveBeenCalledWith', expectedArgs);
546
+ },
547
+
548
+ toHaveBeenLastCalledWith(...expectedArgs) {
549
+ if (!actual.__isMockFunction) throw new Error('toHaveBeenLastCalledWith requires a mock function');
550
+ assert(actual.mock.lastCall && asymmetricDeepEqual(actual.mock.lastCall, expectedArgs),
551
+ negated ? \`Expected last call not to be \${formatValue(expectedArgs)}\`
552
+ : \`Expected last call to be \${formatValue(expectedArgs)}, got \${formatValue(actual.mock.lastCall)}\`,
553
+ 'toHaveBeenLastCalledWith', expectedArgs);
554
+ },
555
+
556
+ toHaveBeenNthCalledWith(n, ...expectedArgs) {
557
+ if (!actual.__isMockFunction) throw new Error('toHaveBeenNthCalledWith requires a mock function');
558
+ const nthCall = actual.mock.calls[n - 1];
559
+ assert(nthCall && asymmetricDeepEqual(nthCall, expectedArgs),
560
+ negated ? \`Expected call \${n} not to be \${formatValue(expectedArgs)}\`
561
+ : \`Expected call \${n} to be \${formatValue(expectedArgs)}, got \${formatValue(nthCall)}\`,
562
+ 'toHaveBeenNthCalledWith', expectedArgs);
563
+ },
564
+
565
+ toHaveReturned() {
566
+ if (!actual.__isMockFunction) throw new Error('toHaveReturned requires a mock function');
567
+ const hasReturned = actual.mock.results.some(r => r.type === 'return');
568
+ assert(hasReturned,
569
+ negated ? \`Expected mock not to have returned\` : \`Expected mock to have returned\`,
570
+ 'toHaveReturned', 'returned');
571
+ },
572
+
573
+ toHaveReturnedWith(value) {
574
+ if (!actual.__isMockFunction) throw new Error('toHaveReturnedWith requires a mock function');
575
+ const match = actual.mock.results.some(r => r.type === 'return' && asymmetricDeepEqual(r.value, value));
576
+ assert(match,
577
+ negated ? \`Expected mock not to have returned \${formatValue(value)}\`
578
+ : \`Expected mock to have returned \${formatValue(value)}\`,
579
+ 'toHaveReturnedWith', value);
580
+ },
581
+
582
+ toHaveLastReturnedWith(value) {
583
+ if (!actual.__isMockFunction) throw new Error('toHaveLastReturnedWith requires a mock function');
584
+ const returns = actual.mock.results.filter(r => r.type === 'return');
585
+ const last = returns[returns.length - 1];
586
+ assert(last && asymmetricDeepEqual(last.value, value),
587
+ negated ? \`Expected last return not to be \${formatValue(value)}\`
588
+ : \`Expected last return to be \${formatValue(value)}, got \${formatValue(last?.value)}\`,
589
+ 'toHaveLastReturnedWith', value);
590
+ },
591
+
592
+ toHaveReturnedTimes(n) {
593
+ if (!actual.__isMockFunction) throw new Error('toHaveReturnedTimes requires a mock function');
594
+ const returnCount = actual.mock.results.filter(r => r.type === 'return').length;
595
+ assert(returnCount === n,
596
+ negated ? \`Expected mock not to have returned \${n} times\`
597
+ : \`Expected mock to have returned \${n} times, got \${returnCount}\`,
598
+ 'toHaveReturnedTimes', n);
599
+ },
600
+
601
+ toHaveNthReturnedWith(n, value) {
602
+ if (!actual.__isMockFunction) throw new Error('toHaveNthReturnedWith requires a mock function');
603
+ const returns = actual.mock.results.filter(r => r.type === 'return');
604
+ const nthReturn = returns[n - 1];
605
+ assert(nthReturn && asymmetricDeepEqual(nthReturn.value, value),
606
+ negated ? \`Expected return \${n} not to be \${formatValue(value)}\`
607
+ : \`Expected return \${n} to be \${formatValue(value)}, got \${formatValue(nthReturn?.value)}\`,
608
+ 'toHaveNthReturnedWith', value);
609
+ },
404
610
  };
405
611
 
406
612
  return matchers;
@@ -409,9 +615,129 @@ var testEnvironmentCode = `
409
615
  const matchers = createMatchers(false);
410
616
  matchers.not = createMatchers(true);
411
617
 
618
+ // Promise matchers using Proxy
619
+ matchers.resolves = new Proxy({}, {
620
+ get(_, matcherName) {
621
+ if (matcherName === 'not') {
622
+ return new Proxy({}, {
623
+ get(_, negatedMatcherName) {
624
+ return async (...args) => {
625
+ const result = await actual;
626
+ return expect(result).not[negatedMatcherName](...args);
627
+ };
628
+ }
629
+ });
630
+ }
631
+ return async (...args) => {
632
+ const result = await actual;
633
+ return expect(result)[matcherName](...args);
634
+ };
635
+ }
636
+ });
637
+
638
+ matchers.rejects = new Proxy({}, {
639
+ get(_, matcherName) {
640
+ if (matcherName === 'not') {
641
+ return new Proxy({}, {
642
+ get(_, negatedMatcherName) {
643
+ return async (...args) => {
644
+ let error;
645
+ try {
646
+ await actual;
647
+ throw new TestError('Expected promise to reject', 'rejects', 'rejection', undefined);
648
+ } catch (e) {
649
+ if (e instanceof TestError && e.matcherName === 'rejects') throw e;
650
+ error = e;
651
+ }
652
+ return expect(error).not[negatedMatcherName](...args);
653
+ };
654
+ }
655
+ });
656
+ }
657
+ return async (...args) => {
658
+ let error;
659
+ try {
660
+ await actual;
661
+ throw new TestError('Expected promise to reject', 'rejects', 'rejection', undefined);
662
+ } catch (e) {
663
+ if (e instanceof TestError && e.matcherName === 'rejects') throw e;
664
+ error = e;
665
+ }
666
+ return expect(error)[matcherName](...args);
667
+ };
668
+ }
669
+ });
670
+
412
671
  return matchers;
413
672
  }
414
673
 
674
+ // Asymmetric matcher implementations
675
+ expect.anything = () => ({
676
+ [ASYMMETRIC_MATCHER]: true,
677
+ asymmetricMatch: (other) => other !== null && other !== undefined,
678
+ toString: () => 'anything()',
679
+ });
680
+
681
+ expect.any = (constructor) => ({
682
+ [ASYMMETRIC_MATCHER]: true,
683
+ asymmetricMatch: (other) => {
684
+ if (constructor === String) return typeof other === 'string' || other instanceof String;
685
+ if (constructor === Number) return typeof other === 'number' || other instanceof Number;
686
+ if (constructor === Boolean) return typeof other === 'boolean' || other instanceof Boolean;
687
+ if (constructor === Function) return typeof other === 'function';
688
+ if (constructor === Object) return typeof other === 'object' && other !== null;
689
+ if (constructor === Array) return Array.isArray(other);
690
+ return other instanceof constructor;
691
+ },
692
+ toString: () => \`any(\${constructor.name || constructor})\`,
693
+ });
694
+
695
+ expect.stringContaining = (str) => ({
696
+ [ASYMMETRIC_MATCHER]: true,
697
+ asymmetricMatch: (other) => typeof other === 'string' && other.includes(str),
698
+ toString: () => \`stringContaining("\${str}")\`,
699
+ });
700
+
701
+ expect.stringMatching = (pattern) => ({
702
+ [ASYMMETRIC_MATCHER]: true,
703
+ asymmetricMatch: (other) => {
704
+ if (typeof other !== 'string') return false;
705
+ return typeof pattern === 'string' ? other.includes(pattern) : pattern.test(other);
706
+ },
707
+ toString: () => \`stringMatching(\${pattern})\`,
708
+ });
709
+
710
+ expect.arrayContaining = (expected) => ({
711
+ [ASYMMETRIC_MATCHER]: true,
712
+ asymmetricMatch: (other) => {
713
+ if (!Array.isArray(other)) return false;
714
+ return expected.every(exp => other.some(item => asymmetricDeepEqual(item, exp)));
715
+ },
716
+ toString: () => \`arrayContaining(\${formatValue(expected)})\`,
717
+ });
718
+
719
+ expect.objectContaining = (expected) => ({
720
+ [ASYMMETRIC_MATCHER]: true,
721
+ asymmetricMatch: (other) => {
722
+ if (typeof other !== 'object' || other === null) return false;
723
+ for (const key of Object.keys(expected)) {
724
+ if (!(key in other)) return false;
725
+ if (!asymmetricDeepEqual(other[key], expected[key])) return false;
726
+ }
727
+ return true;
728
+ },
729
+ toString: () => \`objectContaining(\${formatValue(expected)})\`,
730
+ });
731
+
732
+ // Assertion counting
733
+ expect.assertions = (n) => {
734
+ __expectedAssertions = n;
735
+ };
736
+
737
+ expect.hasAssertions = () => {
738
+ __hasAssertionsFlag = true;
739
+ };
740
+
415
741
  // ============================================================
416
742
  // Test Registration Functions
417
743
  // ============================================================
@@ -520,6 +846,113 @@ var testEnvironmentCode = `
520
846
  currentSuite.afterAll.push(fn);
521
847
  }
522
848
 
849
+ // ============================================================
850
+ // Mock Implementation
851
+ // ============================================================
852
+
853
+ const mock = {
854
+ fn(implementation) {
855
+ const mockState = createMockState();
856
+ let defaultImpl = implementation;
857
+ const onceImpls = [];
858
+ const onceReturns = [];
859
+ let returnVal, resolvedVal, rejectedVal;
860
+ let returnSet = false, resolvedSet = false, rejectedSet = false;
861
+
862
+ function mockFn(...args) {
863
+ mockState.calls.push(args);
864
+ mockState.contexts.push(this);
865
+ mockState.lastCall = args;
866
+ mockState.invocationCallOrder.push(++__mockCallOrder);
867
+
868
+ let result;
869
+ try {
870
+ if (onceImpls.length > 0) {
871
+ result = onceImpls.shift().apply(this, args);
872
+ } else if (onceReturns.length > 0) {
873
+ result = onceReturns.shift();
874
+ } else if (returnSet) {
875
+ result = returnVal;
876
+ } else if (resolvedSet) {
877
+ result = Promise.resolve(resolvedVal);
878
+ } else if (rejectedSet) {
879
+ result = Promise.reject(rejectedVal);
880
+ } else if (defaultImpl) {
881
+ result = defaultImpl.apply(this, args);
882
+ }
883
+ mockState.results.push({ type: 'return', value: result });
884
+ return result;
885
+ } catch (e) {
886
+ mockState.results.push({ type: 'throw', value: e });
887
+ throw e;
888
+ }
889
+ }
890
+
891
+ mockFn.__isMockFunction = true;
892
+ mockFn.mock = mockState;
893
+
894
+ // Configuration methods
895
+ mockFn.mockReturnValue = (v) => { returnVal = v; returnSet = true; return mockFn; };
896
+ mockFn.mockReturnValueOnce = (v) => { onceReturns.push(v); return mockFn; };
897
+ mockFn.mockResolvedValue = (v) => { resolvedVal = v; resolvedSet = true; return mockFn; };
898
+ mockFn.mockRejectedValue = (v) => { rejectedVal = v; rejectedSet = true; return mockFn; };
899
+ mockFn.mockImplementation = (fn) => { defaultImpl = fn; return mockFn; };
900
+ mockFn.mockImplementationOnce = (fn) => { onceImpls.push(fn); return mockFn; };
901
+
902
+ // Clearing methods
903
+ mockFn.mockClear = () => {
904
+ mockState.calls = []; mockState.results = []; mockState.contexts = [];
905
+ mockState.instances = []; mockState.invocationCallOrder = []; mockState.lastCall = undefined;
906
+ return mockFn;
907
+ };
908
+ mockFn.mockReset = () => {
909
+ mockFn.mockClear();
910
+ defaultImpl = undefined; returnVal = resolvedVal = rejectedVal = undefined;
911
+ returnSet = resolvedSet = rejectedSet = false;
912
+ onceImpls.length = 0; onceReturns.length = 0;
913
+ return mockFn;
914
+ };
915
+ mockFn.mockRestore = () => mockFn.mockReset();
916
+
917
+ __mockRegistry.push(mockFn);
918
+ return mockFn;
919
+ },
920
+
921
+ spyOn(object, methodName) {
922
+ const original = object[methodName];
923
+ if (typeof original !== 'function') {
924
+ throw new Error(\`Cannot spy on \${methodName}: not a function\`);
925
+ }
926
+ const spy = mock.fn(original);
927
+ spy.__originalMethod = original;
928
+ spy.__spyTarget = object;
929
+ spy.__spyMethodName = methodName;
930
+ spy.__isSpyFunction = true;
931
+ spy.mockRestore = () => {
932
+ object[methodName] = original;
933
+ const idx = __mockRegistry.indexOf(spy);
934
+ if (idx !== -1) __mockRegistry.splice(idx, 1);
935
+ return spy;
936
+ };
937
+ object[methodName] = spy;
938
+ return spy;
939
+ },
940
+
941
+ clearAllMocks() {
942
+ for (const fn of __mockRegistry) fn.mockClear();
943
+ },
944
+
945
+ resetAllMocks() {
946
+ for (const fn of __mockRegistry) fn.mockReset();
947
+ },
948
+
949
+ restoreAllMocks() {
950
+ for (let i = __mockRegistry.length - 1; i >= 0; i--) {
951
+ if (__mockRegistry[i].__isSpyFunction) __mockRegistry[i].mockRestore();
952
+ }
953
+ },
954
+ };
955
+
523
956
  // ============================================================
524
957
  // Test Runner Helpers
525
958
  // ============================================================
@@ -686,6 +1119,11 @@ var testEnvironmentCode = `
686
1119
  }
687
1120
 
688
1121
  const testStart = Date.now();
1122
+ // Reset assertion counting state before each test
1123
+ __expectedAssertions = null;
1124
+ __assertionCount = 0;
1125
+ __hasAssertionsFlag = false;
1126
+
689
1127
  try {
690
1128
  // Run all beforeEach hooks (parent first, then current)
691
1129
  for (const hook of [...parentHooks.beforeEach, ...suite.beforeEach]) {
@@ -700,6 +1138,17 @@ var testEnvironmentCode = `
700
1138
  await hook();
701
1139
  }
702
1140
 
1141
+ // Verify assertion counts after test passes
1142
+ if (__hasAssertionsFlag && __assertionCount === 0) {
1143
+ throw new TestError('Expected at least one assertion', 'hasAssertions', '>0', 0);
1144
+ }
1145
+ if (__expectedAssertions !== null && __assertionCount !== __expectedAssertions) {
1146
+ throw new TestError(
1147
+ \`Expected \${__expectedAssertions} assertions, got \${__assertionCount}\`,
1148
+ 'assertions', __expectedAssertions, __assertionCount
1149
+ );
1150
+ }
1151
+
703
1152
  const testResult = {
704
1153
  name: t.name,
705
1154
  suitePath: suitePath,
@@ -826,6 +1275,10 @@ var testEnvironmentCode = `
826
1275
  currentSuite = rootSuite;
827
1276
  suiteStack.length = 0;
828
1277
  suiteStack.push(rootSuite);
1278
+ // Clear mocks
1279
+ __mockCallOrder = 0;
1280
+ mock.restoreAllMocks();
1281
+ __mockRegistry.length = 0;
829
1282
  }
830
1283
 
831
1284
  function __setEventCallback(callback) {
@@ -844,6 +1297,8 @@ var testEnvironmentCode = `
844
1297
  globalThis.afterEach = afterEach;
845
1298
  globalThis.beforeAll = beforeAll;
846
1299
  globalThis.afterAll = afterAll;
1300
+ globalThis.mock = mock;
1301
+ globalThis.jest = mock; // Jest compatibility alias
847
1302
  globalThis.__runAllTests = __runAllTests;
848
1303
  globalThis.__resetTestEnvironment = __resetTestEnvironment;
849
1304
  globalThis.__hasTests = __hasTests;
@@ -893,4 +1348,4 @@ export {
893
1348
  getTestCount
894
1349
  };
895
1350
 
896
- //# debugId=E073089EDACCEE1764756E2164756E21
1351
+ //# debugId=2616F937BBAFD06664756E2164756E21