@ricsam/isolate-test-environment 0.1.11 → 0.1.12

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.
@@ -55,6 +55,26 @@ var testEnvironmentCode = `
55
55
  // Internal State
56
56
  // ============================================================
57
57
 
58
+ // Mock registry and call counter
59
+ let __mockCallOrder = 0;
60
+ const __mockRegistry = [];
61
+
62
+ // Assertion counting state
63
+ let __expectedAssertions = null;
64
+ let __assertionCount = 0;
65
+ let __hasAssertionsFlag = false;
66
+
67
+ function createMockState() {
68
+ return {
69
+ calls: [],
70
+ results: [],
71
+ contexts: [],
72
+ instances: [],
73
+ invocationCallOrder: [],
74
+ lastCall: undefined,
75
+ };
76
+ }
77
+
58
78
  function createSuite(name, skip = false, only = false) {
59
79
  return {
60
80
  name,
@@ -100,6 +120,34 @@ var testEnvironmentCode = `
100
120
  }
101
121
  }
102
122
 
123
+ // ============================================================
124
+ // Asymmetric Matcher Infrastructure
125
+ // ============================================================
126
+
127
+ const ASYMMETRIC_MATCHER = Symbol('asymmetricMatcher');
128
+
129
+ function isAsymmetricMatcher(obj) {
130
+ return obj && obj[ASYMMETRIC_MATCHER] === true;
131
+ }
132
+
133
+ // Deep equality with asymmetric matcher support
134
+ function asymmetricDeepEqual(a, b) {
135
+ if (isAsymmetricMatcher(b)) return b.asymmetricMatch(a);
136
+ if (isAsymmetricMatcher(a)) return a.asymmetricMatch(b);
137
+ if (a === b) return true;
138
+ if (typeof a !== typeof b) return false;
139
+ if (typeof a !== 'object' || a === null || b === null) return false;
140
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
141
+ const keysA = Object.keys(a);
142
+ const keysB = Object.keys(b);
143
+ if (keysA.length !== keysB.length) return false;
144
+ for (const key of keysA) {
145
+ if (!keysB.includes(key)) return false;
146
+ if (!asymmetricDeepEqual(a[key], b[key])) return false;
147
+ }
148
+ return true;
149
+ }
150
+
103
151
  // ============================================================
104
152
  // Deep Equality Helper
105
153
  // ============================================================
@@ -200,6 +248,7 @@ var testEnvironmentCode = `
200
248
  function expect(actual) {
201
249
  function createMatchers(negated = false) {
202
250
  const assert = (condition, message, matcherName, expected) => {
251
+ __assertionCount++;
203
252
  const pass = negated ? !condition : condition;
204
253
  if (!pass) {
205
254
  throw new TestError(message, matcherName, expected, actual);
@@ -220,7 +269,7 @@ var testEnvironmentCode = `
220
269
 
221
270
  toEqual(expected) {
222
271
  assert(
223
- deepEqual(actual, expected),
272
+ asymmetricDeepEqual(actual, expected),
224
273
  negated
225
274
  ? \`Expected \${formatValue(actual)} not to equal \${formatValue(expected)}\`
226
275
  : \`Expected \${formatValue(actual)} to equal \${formatValue(expected)}\`,
@@ -395,7 +444,7 @@ var testEnvironmentCode = `
395
444
  toHaveProperty(path, value) {
396
445
  const prop = getNestedProperty(actual, path);
397
446
  const hasProperty = prop.exists;
398
- const valueMatches = arguments.length < 2 || deepEqual(prop.value, value);
447
+ const valueMatches = arguments.length < 2 || asymmetricDeepEqual(prop.value, value);
399
448
 
400
449
  assert(
401
450
  hasProperty && valueMatches,
@@ -450,6 +499,163 @@ var testEnvironmentCode = `
450
499
  expected
451
500
  );
452
501
  },
502
+
503
+ toBeCloseTo(expected, numDigits = 2) {
504
+ const precision = Math.pow(10, -numDigits) / 2;
505
+ const pass = Math.abs(actual - expected) < precision;
506
+ assert(
507
+ pass,
508
+ negated
509
+ ? \`Expected \${formatValue(actual)} not to be close to \${formatValue(expected)} (precision: \${numDigits} digits)\`
510
+ : \`Expected \${formatValue(actual)} to be close to \${formatValue(expected)} (precision: \${numDigits} digits)\`,
511
+ 'toBeCloseTo',
512
+ expected
513
+ );
514
+ },
515
+
516
+ toBeNaN() {
517
+ assert(
518
+ Number.isNaN(actual),
519
+ negated
520
+ ? \`Expected \${formatValue(actual)} not to be NaN\`
521
+ : \`Expected \${formatValue(actual)} to be NaN\`,
522
+ 'toBeNaN',
523
+ NaN
524
+ );
525
+ },
526
+
527
+ toMatchObject(expected) {
528
+ function matchesObject(obj, pattern) {
529
+ if (isAsymmetricMatcher(pattern)) return pattern.asymmetricMatch(obj);
530
+ if (typeof pattern !== 'object' || pattern === null) return obj === pattern;
531
+ if (typeof obj !== 'object' || obj === null) return false;
532
+ for (const key of Object.keys(pattern)) {
533
+ if (!(key in obj)) return false;
534
+ if (!matchesObject(obj[key], pattern[key])) return false;
535
+ }
536
+ return true;
537
+ }
538
+ assert(
539
+ matchesObject(actual, expected),
540
+ negated
541
+ ? \`Expected \${formatValue(actual)} not to match object \${formatValue(expected)}\`
542
+ : \`Expected \${formatValue(actual)} to match object \${formatValue(expected)}\`,
543
+ 'toMatchObject',
544
+ expected
545
+ );
546
+ },
547
+
548
+ toContainEqual(item) {
549
+ const contains = Array.isArray(actual) && actual.some(el => asymmetricDeepEqual(el, item));
550
+ assert(
551
+ contains,
552
+ negated
553
+ ? \`Expected array not to contain equal \${formatValue(item)}\`
554
+ : \`Expected array to contain equal \${formatValue(item)}\`,
555
+ 'toContainEqual',
556
+ item
557
+ );
558
+ },
559
+
560
+ toBeTypeOf(expectedType) {
561
+ const actualType = typeof actual;
562
+ assert(
563
+ actualType === expectedType,
564
+ negated
565
+ ? \`Expected typeof \${formatValue(actual)} not to be "\${expectedType}"\`
566
+ : \`Expected typeof \${formatValue(actual)} to be "\${expectedType}", got "\${actualType}"\`,
567
+ 'toBeTypeOf',
568
+ expectedType
569
+ );
570
+ },
571
+
572
+ // Mock matchers
573
+ toHaveBeenCalled() {
574
+ if (!actual.__isMockFunction) throw new Error('toHaveBeenCalled requires a mock function');
575
+ assert(actual.mock.calls.length > 0,
576
+ negated ? \`Expected mock not to have been called\` : \`Expected mock to have been called\`,
577
+ 'toHaveBeenCalled', 'called');
578
+ },
579
+
580
+ toHaveBeenCalledTimes(n) {
581
+ if (!actual.__isMockFunction) throw new Error('toHaveBeenCalledTimes requires a mock function');
582
+ assert(actual.mock.calls.length === n,
583
+ negated ? \`Expected mock not to have been called \${n} times\`
584
+ : \`Expected mock to be called \${n} times, got \${actual.mock.calls.length}\`,
585
+ 'toHaveBeenCalledTimes', n);
586
+ },
587
+
588
+ toHaveBeenCalledWith(...expectedArgs) {
589
+ if (!actual.__isMockFunction) throw new Error('toHaveBeenCalledWith requires a mock function');
590
+ const match = actual.mock.calls.some(args => asymmetricDeepEqual(args, expectedArgs));
591
+ assert(match,
592
+ negated ? \`Expected mock not to have been called with \${formatValue(expectedArgs)}\`
593
+ : \`Expected mock to have been called with \${formatValue(expectedArgs)}\`,
594
+ 'toHaveBeenCalledWith', expectedArgs);
595
+ },
596
+
597
+ toHaveBeenLastCalledWith(...expectedArgs) {
598
+ if (!actual.__isMockFunction) throw new Error('toHaveBeenLastCalledWith requires a mock function');
599
+ assert(actual.mock.lastCall && asymmetricDeepEqual(actual.mock.lastCall, expectedArgs),
600
+ negated ? \`Expected last call not to be \${formatValue(expectedArgs)}\`
601
+ : \`Expected last call to be \${formatValue(expectedArgs)}, got \${formatValue(actual.mock.lastCall)}\`,
602
+ 'toHaveBeenLastCalledWith', expectedArgs);
603
+ },
604
+
605
+ toHaveBeenNthCalledWith(n, ...expectedArgs) {
606
+ if (!actual.__isMockFunction) throw new Error('toHaveBeenNthCalledWith requires a mock function');
607
+ const nthCall = actual.mock.calls[n - 1];
608
+ assert(nthCall && asymmetricDeepEqual(nthCall, expectedArgs),
609
+ negated ? \`Expected call \${n} not to be \${formatValue(expectedArgs)}\`
610
+ : \`Expected call \${n} to be \${formatValue(expectedArgs)}, got \${formatValue(nthCall)}\`,
611
+ 'toHaveBeenNthCalledWith', expectedArgs);
612
+ },
613
+
614
+ toHaveReturned() {
615
+ if (!actual.__isMockFunction) throw new Error('toHaveReturned requires a mock function');
616
+ const hasReturned = actual.mock.results.some(r => r.type === 'return');
617
+ assert(hasReturned,
618
+ negated ? \`Expected mock not to have returned\` : \`Expected mock to have returned\`,
619
+ 'toHaveReturned', 'returned');
620
+ },
621
+
622
+ toHaveReturnedWith(value) {
623
+ if (!actual.__isMockFunction) throw new Error('toHaveReturnedWith requires a mock function');
624
+ const match = actual.mock.results.some(r => r.type === 'return' && asymmetricDeepEqual(r.value, value));
625
+ assert(match,
626
+ negated ? \`Expected mock not to have returned \${formatValue(value)}\`
627
+ : \`Expected mock to have returned \${formatValue(value)}\`,
628
+ 'toHaveReturnedWith', value);
629
+ },
630
+
631
+ toHaveLastReturnedWith(value) {
632
+ if (!actual.__isMockFunction) throw new Error('toHaveLastReturnedWith requires a mock function');
633
+ const returns = actual.mock.results.filter(r => r.type === 'return');
634
+ const last = returns[returns.length - 1];
635
+ assert(last && asymmetricDeepEqual(last.value, value),
636
+ negated ? \`Expected last return not to be \${formatValue(value)}\`
637
+ : \`Expected last return to be \${formatValue(value)}, got \${formatValue(last?.value)}\`,
638
+ 'toHaveLastReturnedWith', value);
639
+ },
640
+
641
+ toHaveReturnedTimes(n) {
642
+ if (!actual.__isMockFunction) throw new Error('toHaveReturnedTimes requires a mock function');
643
+ const returnCount = actual.mock.results.filter(r => r.type === 'return').length;
644
+ assert(returnCount === n,
645
+ negated ? \`Expected mock not to have returned \${n} times\`
646
+ : \`Expected mock to have returned \${n} times, got \${returnCount}\`,
647
+ 'toHaveReturnedTimes', n);
648
+ },
649
+
650
+ toHaveNthReturnedWith(n, value) {
651
+ if (!actual.__isMockFunction) throw new Error('toHaveNthReturnedWith requires a mock function');
652
+ const returns = actual.mock.results.filter(r => r.type === 'return');
653
+ const nthReturn = returns[n - 1];
654
+ assert(nthReturn && asymmetricDeepEqual(nthReturn.value, value),
655
+ negated ? \`Expected return \${n} not to be \${formatValue(value)}\`
656
+ : \`Expected return \${n} to be \${formatValue(value)}, got \${formatValue(nthReturn?.value)}\`,
657
+ 'toHaveNthReturnedWith', value);
658
+ },
453
659
  };
454
660
 
455
661
  return matchers;
@@ -458,9 +664,129 @@ var testEnvironmentCode = `
458
664
  const matchers = createMatchers(false);
459
665
  matchers.not = createMatchers(true);
460
666
 
667
+ // Promise matchers using Proxy
668
+ matchers.resolves = new Proxy({}, {
669
+ get(_, matcherName) {
670
+ if (matcherName === 'not') {
671
+ return new Proxy({}, {
672
+ get(_, negatedMatcherName) {
673
+ return async (...args) => {
674
+ const result = await actual;
675
+ return expect(result).not[negatedMatcherName](...args);
676
+ };
677
+ }
678
+ });
679
+ }
680
+ return async (...args) => {
681
+ const result = await actual;
682
+ return expect(result)[matcherName](...args);
683
+ };
684
+ }
685
+ });
686
+
687
+ matchers.rejects = new Proxy({}, {
688
+ get(_, matcherName) {
689
+ if (matcherName === 'not') {
690
+ return new Proxy({}, {
691
+ get(_, negatedMatcherName) {
692
+ return async (...args) => {
693
+ let error;
694
+ try {
695
+ await actual;
696
+ throw new TestError('Expected promise to reject', 'rejects', 'rejection', undefined);
697
+ } catch (e) {
698
+ if (e instanceof TestError && e.matcherName === 'rejects') throw e;
699
+ error = e;
700
+ }
701
+ return expect(error).not[negatedMatcherName](...args);
702
+ };
703
+ }
704
+ });
705
+ }
706
+ return async (...args) => {
707
+ let error;
708
+ try {
709
+ await actual;
710
+ throw new TestError('Expected promise to reject', 'rejects', 'rejection', undefined);
711
+ } catch (e) {
712
+ if (e instanceof TestError && e.matcherName === 'rejects') throw e;
713
+ error = e;
714
+ }
715
+ return expect(error)[matcherName](...args);
716
+ };
717
+ }
718
+ });
719
+
461
720
  return matchers;
462
721
  }
463
722
 
723
+ // Asymmetric matcher implementations
724
+ expect.anything = () => ({
725
+ [ASYMMETRIC_MATCHER]: true,
726
+ asymmetricMatch: (other) => other !== null && other !== undefined,
727
+ toString: () => 'anything()',
728
+ });
729
+
730
+ expect.any = (constructor) => ({
731
+ [ASYMMETRIC_MATCHER]: true,
732
+ asymmetricMatch: (other) => {
733
+ if (constructor === String) return typeof other === 'string' || other instanceof String;
734
+ if (constructor === Number) return typeof other === 'number' || other instanceof Number;
735
+ if (constructor === Boolean) return typeof other === 'boolean' || other instanceof Boolean;
736
+ if (constructor === Function) return typeof other === 'function';
737
+ if (constructor === Object) return typeof other === 'object' && other !== null;
738
+ if (constructor === Array) return Array.isArray(other);
739
+ return other instanceof constructor;
740
+ },
741
+ toString: () => \`any(\${constructor.name || constructor})\`,
742
+ });
743
+
744
+ expect.stringContaining = (str) => ({
745
+ [ASYMMETRIC_MATCHER]: true,
746
+ asymmetricMatch: (other) => typeof other === 'string' && other.includes(str),
747
+ toString: () => \`stringContaining("\${str}")\`,
748
+ });
749
+
750
+ expect.stringMatching = (pattern) => ({
751
+ [ASYMMETRIC_MATCHER]: true,
752
+ asymmetricMatch: (other) => {
753
+ if (typeof other !== 'string') return false;
754
+ return typeof pattern === 'string' ? other.includes(pattern) : pattern.test(other);
755
+ },
756
+ toString: () => \`stringMatching(\${pattern})\`,
757
+ });
758
+
759
+ expect.arrayContaining = (expected) => ({
760
+ [ASYMMETRIC_MATCHER]: true,
761
+ asymmetricMatch: (other) => {
762
+ if (!Array.isArray(other)) return false;
763
+ return expected.every(exp => other.some(item => asymmetricDeepEqual(item, exp)));
764
+ },
765
+ toString: () => \`arrayContaining(\${formatValue(expected)})\`,
766
+ });
767
+
768
+ expect.objectContaining = (expected) => ({
769
+ [ASYMMETRIC_MATCHER]: true,
770
+ asymmetricMatch: (other) => {
771
+ if (typeof other !== 'object' || other === null) return false;
772
+ for (const key of Object.keys(expected)) {
773
+ if (!(key in other)) return false;
774
+ if (!asymmetricDeepEqual(other[key], expected[key])) return false;
775
+ }
776
+ return true;
777
+ },
778
+ toString: () => \`objectContaining(\${formatValue(expected)})\`,
779
+ });
780
+
781
+ // Assertion counting
782
+ expect.assertions = (n) => {
783
+ __expectedAssertions = n;
784
+ };
785
+
786
+ expect.hasAssertions = () => {
787
+ __hasAssertionsFlag = true;
788
+ };
789
+
464
790
  // ============================================================
465
791
  // Test Registration Functions
466
792
  // ============================================================
@@ -569,6 +895,113 @@ var testEnvironmentCode = `
569
895
  currentSuite.afterAll.push(fn);
570
896
  }
571
897
 
898
+ // ============================================================
899
+ // Mock Implementation
900
+ // ============================================================
901
+
902
+ const mock = {
903
+ fn(implementation) {
904
+ const mockState = createMockState();
905
+ let defaultImpl = implementation;
906
+ const onceImpls = [];
907
+ const onceReturns = [];
908
+ let returnVal, resolvedVal, rejectedVal;
909
+ let returnSet = false, resolvedSet = false, rejectedSet = false;
910
+
911
+ function mockFn(...args) {
912
+ mockState.calls.push(args);
913
+ mockState.contexts.push(this);
914
+ mockState.lastCall = args;
915
+ mockState.invocationCallOrder.push(++__mockCallOrder);
916
+
917
+ let result;
918
+ try {
919
+ if (onceImpls.length > 0) {
920
+ result = onceImpls.shift().apply(this, args);
921
+ } else if (onceReturns.length > 0) {
922
+ result = onceReturns.shift();
923
+ } else if (returnSet) {
924
+ result = returnVal;
925
+ } else if (resolvedSet) {
926
+ result = Promise.resolve(resolvedVal);
927
+ } else if (rejectedSet) {
928
+ result = Promise.reject(rejectedVal);
929
+ } else if (defaultImpl) {
930
+ result = defaultImpl.apply(this, args);
931
+ }
932
+ mockState.results.push({ type: 'return', value: result });
933
+ return result;
934
+ } catch (e) {
935
+ mockState.results.push({ type: 'throw', value: e });
936
+ throw e;
937
+ }
938
+ }
939
+
940
+ mockFn.__isMockFunction = true;
941
+ mockFn.mock = mockState;
942
+
943
+ // Configuration methods
944
+ mockFn.mockReturnValue = (v) => { returnVal = v; returnSet = true; return mockFn; };
945
+ mockFn.mockReturnValueOnce = (v) => { onceReturns.push(v); return mockFn; };
946
+ mockFn.mockResolvedValue = (v) => { resolvedVal = v; resolvedSet = true; return mockFn; };
947
+ mockFn.mockRejectedValue = (v) => { rejectedVal = v; rejectedSet = true; return mockFn; };
948
+ mockFn.mockImplementation = (fn) => { defaultImpl = fn; return mockFn; };
949
+ mockFn.mockImplementationOnce = (fn) => { onceImpls.push(fn); return mockFn; };
950
+
951
+ // Clearing methods
952
+ mockFn.mockClear = () => {
953
+ mockState.calls = []; mockState.results = []; mockState.contexts = [];
954
+ mockState.instances = []; mockState.invocationCallOrder = []; mockState.lastCall = undefined;
955
+ return mockFn;
956
+ };
957
+ mockFn.mockReset = () => {
958
+ mockFn.mockClear();
959
+ defaultImpl = undefined; returnVal = resolvedVal = rejectedVal = undefined;
960
+ returnSet = resolvedSet = rejectedSet = false;
961
+ onceImpls.length = 0; onceReturns.length = 0;
962
+ return mockFn;
963
+ };
964
+ mockFn.mockRestore = () => mockFn.mockReset();
965
+
966
+ __mockRegistry.push(mockFn);
967
+ return mockFn;
968
+ },
969
+
970
+ spyOn(object, methodName) {
971
+ const original = object[methodName];
972
+ if (typeof original !== 'function') {
973
+ throw new Error(\`Cannot spy on \${methodName}: not a function\`);
974
+ }
975
+ const spy = mock.fn(original);
976
+ spy.__originalMethod = original;
977
+ spy.__spyTarget = object;
978
+ spy.__spyMethodName = methodName;
979
+ spy.__isSpyFunction = true;
980
+ spy.mockRestore = () => {
981
+ object[methodName] = original;
982
+ const idx = __mockRegistry.indexOf(spy);
983
+ if (idx !== -1) __mockRegistry.splice(idx, 1);
984
+ return spy;
985
+ };
986
+ object[methodName] = spy;
987
+ return spy;
988
+ },
989
+
990
+ clearAllMocks() {
991
+ for (const fn of __mockRegistry) fn.mockClear();
992
+ },
993
+
994
+ resetAllMocks() {
995
+ for (const fn of __mockRegistry) fn.mockReset();
996
+ },
997
+
998
+ restoreAllMocks() {
999
+ for (let i = __mockRegistry.length - 1; i >= 0; i--) {
1000
+ if (__mockRegistry[i].__isSpyFunction) __mockRegistry[i].mockRestore();
1001
+ }
1002
+ },
1003
+ };
1004
+
572
1005
  // ============================================================
573
1006
  // Test Runner Helpers
574
1007
  // ============================================================
@@ -735,6 +1168,11 @@ var testEnvironmentCode = `
735
1168
  }
736
1169
 
737
1170
  const testStart = Date.now();
1171
+ // Reset assertion counting state before each test
1172
+ __expectedAssertions = null;
1173
+ __assertionCount = 0;
1174
+ __hasAssertionsFlag = false;
1175
+
738
1176
  try {
739
1177
  // Run all beforeEach hooks (parent first, then current)
740
1178
  for (const hook of [...parentHooks.beforeEach, ...suite.beforeEach]) {
@@ -749,6 +1187,17 @@ var testEnvironmentCode = `
749
1187
  await hook();
750
1188
  }
751
1189
 
1190
+ // Verify assertion counts after test passes
1191
+ if (__hasAssertionsFlag && __assertionCount === 0) {
1192
+ throw new TestError('Expected at least one assertion', 'hasAssertions', '>0', 0);
1193
+ }
1194
+ if (__expectedAssertions !== null && __assertionCount !== __expectedAssertions) {
1195
+ throw new TestError(
1196
+ \`Expected \${__expectedAssertions} assertions, got \${__assertionCount}\`,
1197
+ 'assertions', __expectedAssertions, __assertionCount
1198
+ );
1199
+ }
1200
+
752
1201
  const testResult = {
753
1202
  name: t.name,
754
1203
  suitePath: suitePath,
@@ -875,6 +1324,10 @@ var testEnvironmentCode = `
875
1324
  currentSuite = rootSuite;
876
1325
  suiteStack.length = 0;
877
1326
  suiteStack.push(rootSuite);
1327
+ // Clear mocks
1328
+ __mockCallOrder = 0;
1329
+ mock.restoreAllMocks();
1330
+ __mockRegistry.length = 0;
878
1331
  }
879
1332
 
880
1333
  function __setEventCallback(callback) {
@@ -893,6 +1346,8 @@ var testEnvironmentCode = `
893
1346
  globalThis.afterEach = afterEach;
894
1347
  globalThis.beforeAll = beforeAll;
895
1348
  globalThis.afterAll = afterAll;
1349
+ globalThis.mock = mock;
1350
+ globalThis.jest = mock; // Jest compatibility alias
896
1351
  globalThis.__runAllTests = __runAllTests;
897
1352
  globalThis.__resetTestEnvironment = __resetTestEnvironment;
898
1353
  globalThis.__hasTests = __hasTests;
@@ -936,4 +1391,4 @@ function getTestCount(context) {
936
1391
  return context.evalSync("__getTestCount()");
937
1392
  }
938
1393
 
939
- //# debugId=62DC5588AE7E091364756E2164756E21
1394
+ //# debugId=4F21D3A88A45D56164756E2164756E21