hermes-test 0.2.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/index.d.ts ADDED
@@ -0,0 +1,231 @@
1
+ // Type declarations for hermes-test
2
+
3
+ // --- Spy ---
4
+
5
+ export type Spy<F extends (...args: any[]) => any = (...args: any[]) => any> =
6
+ F & {
7
+ readonly calls: ReadonlyArray<Parameters<F>>;
8
+ readonly callCount: number;
9
+ readonly returnValues: ReadonlyArray<ReturnType<F>>;
10
+ reset(): void;
11
+ setImpl(impl: F): Spy<F>;
12
+ returns(value: ReturnType<F>): Spy<F>;
13
+ mockImplementation(fn: F): Spy<F>;
14
+ mockImplementationOnce(fn: F): Spy<F>;
15
+ mockReturnValue(value: ReturnType<F>): Spy<F>;
16
+ mockReturnValueOnce(value: ReturnType<F>): Spy<F>;
17
+ mockResolvedValue<V>(value: V): Spy<(...args: Parameters<F>) => Promise<V>>;
18
+ mockResolvedValueOnce<V>(value: V): Spy<F>;
19
+ mockRejectedValue(value: unknown): Spy<F>;
20
+ mockRejectedValueOnce(value: unknown): Spy<F>;
21
+ mockClear(): void;
22
+ mockReset(): void;
23
+ mockRestore(): void;
24
+ readonly _isSpy: true;
25
+ };
26
+
27
+ export function spy(): Spy<(...args: any[]) => undefined>;
28
+ export function spy<F extends (...args: any[]) => any>(impl: F): Spy<F>;
29
+ export function spyOn<T extends Record<string, any>, K extends keyof T & string>(
30
+ obj: T,
31
+ method: K,
32
+ ): T[K] extends (...args: any[]) => any ? Spy<T[K]> : Spy;
33
+ export function clearAllMocks(): void;
34
+
35
+ // --- Expect ---
36
+
37
+ export interface AsymmetricMatcher {
38
+ readonly __htMatcher: true;
39
+ matches(value: unknown): boolean;
40
+ }
41
+
42
+ export interface Assertion<T = unknown> {
43
+ toBe(expected: T): void;
44
+ toEqual(expected: T): void;
45
+ toContain(item: T extends ReadonlyArray<infer U> ? U : T extends string ? string : unknown): void;
46
+ toContainEqual(item: T extends ReadonlyArray<infer U> ? U : unknown): void;
47
+ toMatch(pattern: string | RegExp): void;
48
+ toThrow(message?: string | RegExp): void;
49
+ toBeNull(): void;
50
+ toBeUndefined(): void;
51
+ toBeDefined(): void;
52
+ toBeTruthy(): void;
53
+ toBeFalsy(): void;
54
+ toBeGreaterThan(n: number): void;
55
+ toBeLessThan(n: number): void;
56
+ toBeInstanceOf(cls: new (...args: any[]) => any): void;
57
+ toHaveLength(n: number): void;
58
+ toBeCloseTo(expected: number, precision?: number): void;
59
+
60
+ // Spy-specific
61
+ wasCalled(): void;
62
+ wasCalledOnce(): void;
63
+ wasCalledTimes(n: number): void;
64
+ wasCalledWith(...args: any[]): void;
65
+ wasLastCalledWith(...args: any[]): void;
66
+ wasNeverCalled(): void;
67
+
68
+ // Jest-compatible aliases
69
+ toHaveBeenCalled(): void;
70
+ toHaveBeenCalledTimes(n: number): void;
71
+ toHaveBeenCalledWith(...args: any[]): void;
72
+ toHaveBeenLastCalledWith(...args: any[]): void;
73
+
74
+ not: Assertion<T>;
75
+
76
+ resolves: {
77
+ toBe(expected: unknown): Promise<void>;
78
+ toEqual(expected: unknown): Promise<void>;
79
+ toBeDefined(): Promise<void>;
80
+ toBeUndefined(): Promise<void>;
81
+ toBeNull(): Promise<void>;
82
+ toBeTruthy(): Promise<void>;
83
+ toBeFalsy(): Promise<void>;
84
+ };
85
+
86
+ rejects: {
87
+ toThrow(message?: string | RegExp): Promise<void>;
88
+ };
89
+ }
90
+
91
+ export interface ExpectFunction {
92
+ <T>(actual: T): Assertion<T>;
93
+ anything(): AsymmetricMatcher;
94
+ any(constructor: new (...args: any[]) => any): AsymmetricMatcher;
95
+ objectContaining<T extends Record<string, unknown>>(subset: T): AsymmetricMatcher;
96
+ arrayContaining<T>(arr: T[]): AsymmetricMatcher;
97
+ stringContaining(substr: string): AsymmetricMatcher;
98
+ stringMatching(pattern: string | RegExp): AsymmetricMatcher;
99
+ }
100
+
101
+ export const expect: ExpectFunction;
102
+
103
+ // --- Test structure ---
104
+
105
+ export interface TestOptions {
106
+ timeout?: number;
107
+ skip?: boolean;
108
+ only?: boolean;
109
+ }
110
+
111
+ export interface TestFunction {
112
+ (name: string, fn: () => void | Promise<void>, options?: TestOptions): void;
113
+ skip(name: string, fn: () => void | Promise<void>): void;
114
+ only(name: string, fn: () => void | Promise<void>): void;
115
+ }
116
+
117
+ export const test: TestFunction;
118
+ export function group(name: string, fn: () => void): void;
119
+ export function beforeEach(fn: () => void | Promise<void>): void;
120
+ export function afterEach(fn: () => void | Promise<void>): void;
121
+ export function beforeAll(fn: () => void | Promise<void>): void;
122
+ export function afterAll(fn: () => void | Promise<void>): void;
123
+
124
+ // --- Hooks ---
125
+
126
+ export interface HookResult<T> {
127
+ readonly result: { readonly current: T };
128
+ readonly current: T;
129
+ readonly history: ReadonlyArray<T>;
130
+ readonly renderCount: number;
131
+ rerender(props?: unknown): void;
132
+ unmount(): void;
133
+ }
134
+
135
+ export interface RenderHookOptions<P = unknown> {
136
+ initialProps?: P;
137
+ wrapper?: React.ComponentType<{ children: React.ReactNode }>;
138
+ }
139
+
140
+ export function renderHook<T, P = unknown>(
141
+ hookFn: (props?: P) => T,
142
+ options?: RenderHookOptions<P>,
143
+ ): HookResult<T>;
144
+
145
+ export function act(fn: () => void | Promise<void>): void;
146
+
147
+ export function waitFor<T>(
148
+ predicate: () => T | false | null | undefined,
149
+ options?: { timeout?: number; interval?: number },
150
+ ): T;
151
+
152
+ export function flushAsync<T>(promise: Promise<T>): T;
153
+ export function flushAsync<T>(value: T): T;
154
+
155
+ // --- Mocking ---
156
+
157
+ export function mockModule(modulePath: string, factory: () => Record<string, unknown>): void;
158
+ export function useMock<T extends Record<string, unknown>>(
159
+ moduleExports: T,
160
+ implementation: Partial<{ [K in keyof T]: T[K] extends (...args: any[]) => any ? Spy<T[K]> | T[K] : T[K] }>,
161
+ ): void;
162
+
163
+ // --- Fetch mocking ---
164
+
165
+ export interface MockRequest {
166
+ method: string;
167
+ url: string;
168
+ headers: Record<string, string>;
169
+ body: unknown;
170
+ }
171
+
172
+ export interface MockResponse {
173
+ body?: unknown;
174
+ status?: number;
175
+ statusText?: string;
176
+ headers?: Record<string, string>;
177
+ }
178
+
179
+ export type FetchHandler = {
180
+ method: string;
181
+ url: string | RegExp;
182
+ handler: (req: MockRequest) => MockResponse;
183
+ once?: boolean;
184
+ };
185
+
186
+ export function mockFetch(...handlers: FetchHandler[]): void;
187
+ export function mockFetchUse(...handlers: FetchHandler[]): void;
188
+ export function mockFetchReset(): void;
189
+ export function mockFetchClear(): void;
190
+
191
+ export const http: {
192
+ get(url: string | RegExp, handler: (req: MockRequest) => MockResponse): FetchHandler;
193
+ post(url: string | RegExp, handler: (req: MockRequest) => MockResponse): FetchHandler;
194
+ put(url: string | RegExp, handler: (req: MockRequest) => MockResponse): FetchHandler;
195
+ patch(url: string | RegExp, handler: (req: MockRequest) => MockResponse): FetchHandler;
196
+ delete(url: string | RegExp, handler: (req: MockRequest) => MockResponse): FetchHandler;
197
+ };
198
+
199
+ export const HttpResponse: {
200
+ json<T>(body: T, init?: { status?: number; headers?: Record<string, string> }): MockResponse;
201
+ text(body: string, init?: { status?: number }): MockResponse;
202
+ error(): MockResponse;
203
+ };
204
+
205
+ // --- Redux store ---
206
+
207
+ export interface StoreContext {
208
+ readonly store: any;
209
+ dispatch(action: any): any;
210
+ getState(): any;
211
+ setState(state: Record<string, any>): void;
212
+ patchState(partial: Record<string, any>): void;
213
+ renderHookWithReduxStore<T>(hookFn: (props?: any) => T, options?: { initialProps?: any }): HookResult<T>;
214
+ }
215
+
216
+ export function withStore(initialState?: Record<string, any>): StoreContext;
217
+ export function withAppReducer(reducer: (state: any, action: any) => any, preloadedState?: Record<string, any>): StoreContext;
218
+
219
+ // --- Timers ---
220
+
221
+ export function useFakeTimers(initialTime?: number): void;
222
+ export function useRealTimers(): void;
223
+ export function advanceTimersByTime(ms: number): void;
224
+ export function runAllTimers(): void;
225
+ export function getTimerCount(): number;
226
+ export function advanceTimersToNextTimer(): void;
227
+
228
+ // --- React import (for type inference) ---
229
+
230
+ // React types used if @types/react is installed
231
+ import type React from 'react';
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "hermes-test",
3
+ "version": "0.2.0",
4
+ "description": "147x faster than Jest. Test runner for React Native and Expo that runs in Hermes.",
5
+ "main": "src/index.ts",
6
+ "types": "index.d.ts",
7
+ "typesVersions": {
8
+ "*": {
9
+ "store": [
10
+ "store.d.ts"
11
+ ]
12
+ }
13
+ },
14
+ "exports": {
15
+ ".": "./src/index.ts",
16
+ "./store": "./src/store.ts"
17
+ },
18
+ "bin": {
19
+ "hermes-test": "bin/hermes-test.js"
20
+ },
21
+ "scripts": {
22
+ "build": "tsc",
23
+ "bundle": "node bundle.mjs"
24
+ },
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "https://github.com/marcuzgabriel/hermes-test"
28
+ },
29
+ "keywords": [
30
+ "react-native",
31
+ "expo",
32
+ "hermes",
33
+ "test-runner",
34
+ "testing",
35
+ "hooks",
36
+ "redux",
37
+ "rtk-query"
38
+ ],
39
+ "license": "MIT",
40
+ "devDependencies": {
41
+ "typescript": "^5.4.0",
42
+ "esbuild": "^0.25.0",
43
+ "react": "^19.2.0",
44
+ "react-reconciler": "^0.33.0"
45
+ },
46
+ "dependencies": {
47
+ "@react-native/js-polyfills": "^0.85.3"
48
+ },
49
+ "peerDependencies": {
50
+ "react": ">=18"
51
+ },
52
+ "optionalDependencies": {
53
+ "@hermes-test/darwin-arm64": "0.2.0",
54
+ "@hermes-test/darwin-x64": "0.2.0",
55
+ "@hermes-test/linux-x64": "0.2.0"
56
+ },
57
+ "files": [
58
+ "src/",
59
+ "bin/",
60
+ "dist/",
61
+ "index.d.ts",
62
+ "store.d.ts",
63
+ "shims/"
64
+ ]
65
+ }
package/src/expect.ts ADDED
@@ -0,0 +1,354 @@
1
+ import type { Spy } from './spy';
2
+
3
+ function deepEqual(a: any, b: any): boolean {
4
+ // Support asymmetric matchers (expect.anything(), expect.any(), expect.objectContaining())
5
+ if (b != null && typeof b === 'object' && b.__htMatcher && typeof b.matches === 'function') return b.matches(a);
6
+ if (a != null && typeof a === 'object' && a.__htMatcher && typeof a.matches === 'function') return a.matches(b);
7
+
8
+ if (a === b) return true;
9
+ if (a == null || b == null) return a === b;
10
+ if (typeof a !== typeof b) return false;
11
+
12
+ if (Array.isArray(a)) {
13
+ if (!Array.isArray(b) || a.length !== b.length) return false;
14
+ return a.every((v, i) => deepEqual(v, b[i]));
15
+ }
16
+
17
+ if (a instanceof Date && b instanceof Date) {
18
+ return a.getTime() === b.getTime();
19
+ }
20
+
21
+ if (typeof a === 'object') {
22
+ const keysA = Object.keys(a).filter((k) => a[k] !== undefined);
23
+ const keysB = Object.keys(b).filter((k) => b[k] !== undefined);
24
+ if (keysA.length !== keysB.length) return false;
25
+ return keysA.every((k) => deepEqual(a[k], b[k]));
26
+ }
27
+
28
+ return false;
29
+ }
30
+
31
+ function formatValue(v: any): string {
32
+ if (v === undefined) return 'undefined';
33
+ if (v === null) return 'null';
34
+ if (typeof v === 'string') return JSON.stringify(v);
35
+ if (typeof v === 'function') return '[Function]';
36
+ try {
37
+ return JSON.stringify(v);
38
+ } catch {
39
+ return String(v);
40
+ }
41
+ }
42
+
43
+ function createAssertion(actual: any, negated: boolean): any {
44
+ function assert(condition: boolean, message: string) {
45
+ const pass = negated ? !condition : condition;
46
+ if (!pass) {
47
+ throw new Error(message);
48
+ }
49
+ }
50
+
51
+ const assertion: any = {
52
+ toBe(expected: any) {
53
+ assert(
54
+ actual === expected,
55
+ negated
56
+ ? `Expected ${formatValue(actual)} not to be ${formatValue(expected)}`
57
+ : `Expected ${formatValue(expected)}, got ${formatValue(actual)}`
58
+ );
59
+ },
60
+
61
+ toEqual(expected: any) {
62
+ assert(
63
+ deepEqual(actual, expected),
64
+ negated
65
+ ? `Expected ${formatValue(actual)} not to deeply equal ${formatValue(expected)}`
66
+ : `Expected deep equal to ${formatValue(expected)}, got ${formatValue(actual)}`
67
+ );
68
+ },
69
+
70
+ toBeDefined() {
71
+ assert(
72
+ actual !== undefined,
73
+ negated
74
+ ? `Expected value to be undefined, got ${formatValue(actual)}`
75
+ : `Expected value to be defined, got undefined`
76
+ );
77
+ },
78
+
79
+ toBeUndefined() {
80
+ assert(
81
+ actual === undefined,
82
+ negated
83
+ ? `Expected value not to be undefined`
84
+ : `Expected undefined, got ${formatValue(actual)}`
85
+ );
86
+ },
87
+
88
+ toBeNull() {
89
+ assert(
90
+ actual === null,
91
+ negated
92
+ ? `Expected value not to be null`
93
+ : `Expected null, got ${formatValue(actual)}`
94
+ );
95
+ },
96
+
97
+ toHaveLength(expected: number) {
98
+ const len = actual?.length;
99
+ assert(
100
+ len === expected,
101
+ negated
102
+ ? `Expected length not to be ${expected}, but it was`
103
+ : `Expected length ${expected}, got ${len}`
104
+ );
105
+ },
106
+
107
+ toBeInstanceOf(expected: any) {
108
+ assert(
109
+ actual instanceof expected,
110
+ negated
111
+ ? `Expected ${formatValue(actual)} not to be instance of ${expected?.name ?? expected}`
112
+ : `Expected instance of ${expected?.name ?? expected}, got ${formatValue(actual)}`
113
+ );
114
+ },
115
+
116
+ toBeTruthy() {
117
+ assert(
118
+ !!actual,
119
+ negated
120
+ ? `Expected ${formatValue(actual)} to be falsy`
121
+ : `Expected truthy value, got ${formatValue(actual)}`
122
+ );
123
+ },
124
+
125
+ toBeFalsy() {
126
+ assert(
127
+ !actual,
128
+ negated
129
+ ? `Expected ${formatValue(actual)} to be truthy`
130
+ : `Expected falsy value, got ${formatValue(actual)}`
131
+ );
132
+ },
133
+
134
+ toBeGreaterThan(n: number) {
135
+ assert(
136
+ actual > n,
137
+ negated
138
+ ? `Expected ${actual} not to be greater than ${n}`
139
+ : `Expected ${actual} to be greater than ${n}`
140
+ );
141
+ },
142
+
143
+ toBeLessThan(n: number) {
144
+ assert(
145
+ actual < n,
146
+ negated
147
+ ? `Expected ${actual} not to be less than ${n}`
148
+ : `Expected ${actual} to be less than ${n}`
149
+ );
150
+ },
151
+
152
+ toContain(item: any) {
153
+ const contains = Array.isArray(actual)
154
+ ? actual.some((v: any) => deepEqual(v, item))
155
+ : typeof actual === 'string'
156
+ ? actual.includes(item)
157
+ : false;
158
+ assert(
159
+ contains,
160
+ negated
161
+ ? `Expected ${formatValue(actual)} not to contain ${formatValue(item)}`
162
+ : `Expected ${formatValue(actual)} to contain ${formatValue(item)}`
163
+ );
164
+ },
165
+
166
+ toContainEqual(item: any) {
167
+ const contains = Array.isArray(actual) && actual.some((v: any) => deepEqual(v, item));
168
+ assert(
169
+ contains,
170
+ negated
171
+ ? `Expected array not to contain equal ${formatValue(item)}`
172
+ : `Expected array to contain equal ${formatValue(item)}, got ${formatValue(actual)}`
173
+ );
174
+ },
175
+
176
+ toBeCloseTo(expected: number, precision: number = 2) {
177
+ const pass = Math.abs(actual - expected) < Math.pow(10, -precision) / 2;
178
+ assert(
179
+ pass,
180
+ negated
181
+ ? `Expected ${actual} not to be close to ${expected}`
182
+ : `Expected ${actual} to be close to ${expected} (precision ${precision})`
183
+ );
184
+ },
185
+
186
+ toMatch(pattern: RegExp | string) {
187
+ const regex = typeof pattern === 'string' ? new RegExp(pattern) : pattern;
188
+ assert(
189
+ regex.test(String(actual)),
190
+ negated
191
+ ? `Expected ${formatValue(actual)} not to match ${pattern}`
192
+ : `Expected ${formatValue(actual)} to match ${pattern}`
193
+ );
194
+ },
195
+
196
+ toThrow(message?: string | RegExp) {
197
+ let threw = false;
198
+ let error: any;
199
+ try {
200
+ actual();
201
+ } catch (e) {
202
+ threw = true;
203
+ error = e;
204
+ }
205
+
206
+ if (message === undefined) {
207
+ assert(
208
+ threw,
209
+ negated
210
+ ? `Expected function not to throw, but it threw ${formatValue(error)}`
211
+ : `Expected function to throw, but it did not`
212
+ );
213
+ } else {
214
+ const errMsg = error?.message ?? String(error ?? '');
215
+ const matches =
216
+ typeof message === 'string'
217
+ ? errMsg.includes(message)
218
+ : message.test(errMsg);
219
+ assert(
220
+ threw && matches,
221
+ negated
222
+ ? `Expected function not to throw matching ${message}`
223
+ : threw
224
+ ? `Expected thrown error to match ${message}, got "${errMsg}"`
225
+ : `Expected function to throw, but it did not`
226
+ );
227
+ }
228
+ },
229
+
230
+ // Spy assertions
231
+ wasCalled() {
232
+ assert(
233
+ (actual as Spy).callCount > 0,
234
+ negated
235
+ ? `Expected spy not to have been called, but it was called ${(actual as Spy).callCount} times`
236
+ : `Expected spy to have been called, but it was never called`
237
+ );
238
+ },
239
+
240
+ wasCalledOnce() {
241
+ assert(
242
+ (actual as Spy).callCount === 1,
243
+ negated
244
+ ? `Expected spy not to have been called once, but it was`
245
+ : `Expected spy to have been called once, but it was called ${(actual as Spy).callCount} times`
246
+ );
247
+ },
248
+
249
+ wasCalledTimes(n: number) {
250
+ assert(
251
+ (actual as Spy).callCount === n,
252
+ negated
253
+ ? `Expected spy not to have been called ${n} times`
254
+ : `Expected spy to have been called ${n} times, but it was called ${(actual as Spy).callCount} times`
255
+ );
256
+ },
257
+
258
+ wasCalledWith(...args: any[]) {
259
+ const s = actual as Spy;
260
+ const match = s.calls.some((call: any[]) => deepEqual(call, args));
261
+ assert(
262
+ match,
263
+ negated
264
+ ? `Expected spy not to have been called with ${formatValue(args)}`
265
+ : `Expected spy to have been called with ${formatValue(args)}, calls: ${formatValue(s.calls)}`
266
+ );
267
+ },
268
+
269
+ wasLastCalledWith(...args: any[]) {
270
+ const s = actual as Spy;
271
+ const lastCall = s.calls[s.calls.length - 1];
272
+ assert(
273
+ deepEqual(lastCall, args),
274
+ negated
275
+ ? `Expected last call not to be ${formatValue(args)}`
276
+ : `Expected last call to be ${formatValue(args)}, got ${formatValue(lastCall)}`
277
+ );
278
+ },
279
+
280
+ wasNeverCalled() {
281
+ assert(
282
+ (actual as Spy).callCount === 0,
283
+ negated
284
+ ? `Expected spy to have been called, but it was never called`
285
+ : `Expected spy to never have been called, but it was called ${(actual as Spy).callCount} times`
286
+ );
287
+ },
288
+
289
+ // Jest-compatible aliases
290
+ toHaveBeenCalled() { return this.wasCalled(); },
291
+ toHaveBeenCalledTimes(n: number) { return this.wasCalledTimes(n); },
292
+ toHaveBeenCalledWith(...args: any[]) { return this.wasCalledWith(...args); },
293
+ toHaveBeenLastCalledWith(...args: any[]) { return this.wasLastCalledWith(...args); },
294
+ };
295
+
296
+ if (!negated) {
297
+ assertion.not = createAssertion(actual, true);
298
+ }
299
+
300
+ return assertion;
301
+ }
302
+
303
+ // --- Asymmetric matchers ---
304
+ // Asymmetric matchers — plain objects with __htMatcher flag
305
+ function makeMatcher(matchFn: (v: any) => boolean) {
306
+ return { __htMatcher: true, matches: matchFn };
307
+ }
308
+
309
+ export function expect(actual: any): any {
310
+ const base = createAssertion(actual, false);
311
+
312
+ // resolves / rejects for promise assertions
313
+ base.resolves = {
314
+ toBeUndefined: async () => { const r = await actual; if (r !== undefined) throw new Error(`Expected undefined, got ${formatValue(r)}`); },
315
+ toBe: async (expected: any) => { const r = await actual; if (r !== expected) throw new Error(`Expected ${formatValue(expected)}, got ${formatValue(r)}`); },
316
+ toEqual: async (expected: any) => { const r = await actual; if (!deepEqual(r, expected)) throw new Error(`Expected deep equal to ${formatValue(expected)}, got ${formatValue(r)}`); },
317
+ toBeDefined: async () => { const r = await actual; if (r === undefined) throw new Error(`Expected value to be defined`); },
318
+ toBeTruthy: async () => { const r = await actual; if (!r) throw new Error(`Expected truthy, got ${formatValue(r)}`); },
319
+ toBeFalsy: async () => { const r = await actual; if (r) throw new Error(`Expected falsy, got ${formatValue(r)}`); },
320
+ toBeNull: async () => { const r = await actual; if (r !== null) throw new Error(`Expected null, got ${formatValue(r)}`); },
321
+ };
322
+
323
+ base.rejects = {
324
+ toThrow: async (msg?: string | RegExp) => {
325
+ try { await actual; throw new Error('Expected promise to reject'); }
326
+ catch (e: any) { if (msg) { const m = e?.message ?? String(e); const ok = typeof msg === 'string' ? m.includes(msg) : msg.test(m); if (!ok) throw new Error(`Expected rejection matching ${msg}, got "${m}"`); } }
327
+ },
328
+ };
329
+
330
+ return base;
331
+ }
332
+
333
+ // Static matchers on expect
334
+ expect.anything = () => makeMatcher((v) => v !== null && v !== undefined);
335
+ expect.any = (ctor: any) => makeMatcher((v) => {
336
+ if (ctor === String) return typeof v === 'string';
337
+ if (ctor === Number) return typeof v === 'number';
338
+ if (ctor === Boolean) return typeof v === 'boolean';
339
+ if (ctor === Function) return typeof v === 'function';
340
+ return v instanceof ctor;
341
+ });
342
+ expect.objectContaining = (subset: Record<string, any>) => makeMatcher((v) => {
343
+ if (typeof v !== 'object' || v === null) return false;
344
+ return Object.keys(subset).every((k) => deepEqual(v[k], subset[k]));
345
+ });
346
+ expect.arrayContaining = (expected: any[]) => makeMatcher((v) => {
347
+ if (!Array.isArray(v)) return false;
348
+ return expected.every((e) => v.some((item: any) => deepEqual(item, e)));
349
+ });
350
+ expect.stringContaining = (substr: string) => makeMatcher((v) => typeof v === 'string' && v.includes(substr));
351
+ expect.stringMatching = (pattern: RegExp | string) => makeMatcher((v) => {
352
+ const re = typeof pattern === 'string' ? new RegExp(pattern) : pattern;
353
+ return typeof v === 'string' && re.test(v);
354
+ });