hermes-test 0.2.4 → 1.0.1

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/globals.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ declare global {
2
+ interface HtMockFetch {
3
+ (...handlers: import('hermes-test').FetchHandler[]): void
4
+ overwrite(...handlers: import('hermes-test').FetchHandler[]): void
5
+ reset(): void
6
+ clear(): void
7
+ }
8
+
9
+ interface HtMock {
10
+ (modulePath: string, factory: () => Record<string, unknown>): void
11
+ fetch: HtMockFetch
12
+ }
13
+
14
+ /** Shallow render: auto-mock child components so the parent renders without deep dependencies. */
15
+ function htShallow(componentPath: string): void
16
+
17
+ const ht: { mock: HtMock; shallow: typeof htShallow }
18
+ }
19
+ export {}
package/index.d.ts CHANGED
@@ -71,6 +71,19 @@ export interface Assertion<T = unknown> {
71
71
  toHaveBeenCalledWith(...args: any[]): void;
72
72
  toHaveBeenLastCalledWith(...args: any[]): void;
73
73
 
74
+ // Element matchers (for render() results)
75
+ toBeRendered(): void;
76
+ toHaveTextContent(expected: string | RegExp): void;
77
+ toContainElement(child: unknown): void;
78
+ toBeEmpty(): void;
79
+ toHaveDisplayValue(expected: string | RegExp): void;
80
+ toHaveProp(name: string, value?: unknown): void;
81
+ toHaveStyle(expected: Record<string, unknown>): void;
82
+ toBeEnabled(): void;
83
+ toBeDisabled(): void;
84
+ toBeVisible(): void;
85
+ toMatchSnapshot(): void;
86
+
74
87
  not: Assertion<T>;
75
88
 
76
89
  resolves: {
@@ -116,6 +129,7 @@ export interface TestFunction {
116
129
 
117
130
  export const test: TestFunction;
118
131
  export function group(name: string, fn: () => void): void;
132
+ export function describe(name: string, fn: () => void): void;
119
133
  export function beforeEach(fn: () => void | Promise<void>): void;
120
134
  export function afterEach(fn: () => void | Promise<void>): void;
121
135
  export function beforeAll(fn: () => void | Promise<void>): void;
@@ -152,15 +166,58 @@ export function waitFor<T>(
152
166
  export function flushAsync<T>(promise: Promise<T>): T;
153
167
  export function flushAsync<T>(value: T): T;
154
168
 
169
+ // --- Component rendering ---
170
+
171
+ export interface HTNode {
172
+ type: string;
173
+ props: Record<string, any>;
174
+ children: HTNode[];
175
+ text?: string;
176
+ }
177
+
178
+ export interface RenderResult {
179
+ container: HTNode;
180
+ getByText(text: string | RegExp): HTNode;
181
+ getAllByText(text: string | RegExp): HTNode[];
182
+ queryByText(text: string | RegExp): HTNode | null;
183
+ queryAllByText(text: string | RegExp): HTNode[];
184
+ getByTestId(testID: string | RegExp): HTNode;
185
+ getAllByTestId(testID: string | RegExp): HTNode[];
186
+ queryByTestId(testID: string | RegExp): HTNode | null;
187
+ queryAllByTestId(testID: string | RegExp): HTNode[];
188
+ getByProps(props: Record<string, any>): HTNode;
189
+ getAllByProps(props: Record<string, any>): HTNode[];
190
+ queryByProps(props: Record<string, any>): HTNode | null;
191
+ queryAllByProps(props: Record<string, any>): HTNode[];
192
+ getByType(type: string): HTNode;
193
+ getAllByType(type: string): HTNode[];
194
+ queryByType(type: string): HTNode | null;
195
+ queryAllByType(type: string): HTNode[];
196
+ toJSON(): any;
197
+ toTree(): string;
198
+ rerender(element: React.ReactElement): void;
199
+ unmount(): void;
200
+ }
201
+
202
+ export function render(element: React.ReactElement, options?: { shallow?: boolean }): RenderResult;
203
+
204
+ export interface FireEventObject {
205
+ (node: HTNode, eventName: string, ...args: any[]): void;
206
+ press(node: HTNode, event?: any): void;
207
+ changeText(node: HTNode, text: string): void;
208
+ scroll(node: HTNode, event: any): void;
209
+ }
210
+
211
+ export const fireEvent: FireEventObject;
212
+
155
213
  // --- Mocking ---
156
214
 
157
- export function mockModule(modulePath: string, factory: () => Record<string, unknown>): void;
158
215
  export function useMock<T extends Record<string, unknown>>(
159
216
  moduleExports: T,
160
217
  implementation: Partial<{ [K in keyof T]: T[K] extends (...args: any[]) => any ? Spy<T[K]> | T[K] : T[K] }>,
161
218
  ): void;
162
219
 
163
- // --- Fetch mocking ---
220
+ // --- Fetch mocking types ---
164
221
 
165
222
  export interface MockRequest {
166
223
  method: string;
@@ -183,11 +240,6 @@ export type FetchHandler = {
183
240
  once?: boolean;
184
241
  };
185
242
 
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
243
  export const http: {
192
244
  get(url: string | RegExp, handler: (req: MockRequest) => MockResponse): FetchHandler;
193
245
  post(url: string | RegExp, handler: (req: MockRequest) => MockResponse): FetchHandler;
@@ -229,3 +281,21 @@ export function advanceTimersToNextTimer(): void;
229
281
 
230
282
  // React types used if @types/react is installed
231
283
  import type React from 'react';
284
+
285
+ // --- ht global (available without import, like jest) ---
286
+
287
+ declare global {
288
+ interface HtMockFetch {
289
+ (...handlers: FetchHandler[]): void;
290
+ overwrite(...handlers: FetchHandler[]): void;
291
+ reset(): void;
292
+ clear(): void;
293
+ }
294
+
295
+ interface HtMock {
296
+ (modulePath: string, factory: () => Record<string, unknown>): void;
297
+ fetch: HtMockFetch;
298
+ }
299
+
300
+ const ht: { mock: HtMock };
301
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hermes-test",
3
- "version": "0.2.4",
3
+ "version": "1.0.1",
4
4
  "description": "26-64x faster than Jest. A test runner built for React Native and Expo. One esbuild pass, one process, zero Babel.",
5
5
  "main": "src/index.ts",
6
6
  "types": "index.d.ts",
@@ -8,12 +8,16 @@
8
8
  "*": {
9
9
  "store": [
10
10
  "store.d.ts"
11
+ ],
12
+ "globals": [
13
+ "globals.d.ts"
11
14
  ]
12
15
  }
13
16
  },
14
17
  "exports": {
15
18
  ".": "./src/index.ts",
16
- "./store": "./src/store.ts"
19
+ "./store": "./src/store.ts",
20
+ "./globals": "./globals.d.ts"
17
21
  },
18
22
  "bin": {
19
23
  "hermes-test": "bin/hermes-test.js"
@@ -40,25 +44,26 @@
40
44
  "devDependencies": {
41
45
  "typescript": "^5.4.0",
42
46
  "esbuild": "^0.25.0",
43
- "react": "^19.2.0",
44
- "react-reconciler": "^0.33.0"
47
+ "react": "^19.2.0"
45
48
  },
46
49
  "dependencies": {
47
- "@react-native/js-polyfills": "^0.85.3"
50
+ "@react-native/js-polyfills": "^0.85.3",
51
+ "react-reconciler": "^0.33.0"
48
52
  },
49
53
  "peerDependencies": {
50
54
  "react": ">=18"
51
55
  },
52
56
  "optionalDependencies": {
53
- "@hermes-test/darwin-arm64": "0.2.4",
54
- "@hermes-test/darwin-x64": "0.2.4",
55
- "@hermes-test/linux-x64": "0.2.4"
57
+ "@hermes-test/darwin-arm64": "1.0.1",
58
+ "@hermes-test/darwin-x64": "1.0.1",
59
+ "@hermes-test/linux-x64": "1.0.1"
56
60
  },
57
61
  "files": [
58
62
  "src/",
59
63
  "bin/",
60
64
  "dist/",
61
65
  "index.d.ts",
66
+ "globals.d.ts",
62
67
  "store.d.ts",
63
68
  "shims/"
64
69
  ]
package/src/expect.ts CHANGED
@@ -1,5 +1,86 @@
1
1
  import type { Spy } from './spy';
2
2
 
3
+ // --- Snapshot support ---
4
+
5
+ const _readFile = (globalThis as any).__HT_readFile || (() => null);
6
+ const _writeFile = (globalThis as any).__HT_writeFile || (() => false);
7
+
8
+ // Snapshot state — set by the harness before each test runs
9
+ let _snapshotFile = ''; // path to .snap file
10
+ let _snapshotTestName = ''; // current test name (used as snapshot key)
11
+ let _snapshotCounter = 0; // counter for multiple snapshots in one test
12
+ let _updateSnapshots = false;
13
+
14
+ // Cache of loaded snapshot files: path → { key → serialized value }
15
+ const _snapshotCache: Record<string, Record<string, string>> = {};
16
+
17
+ function _setSnapshotContext(file: string, testName: string, update: boolean) {
18
+ _snapshotFile = file;
19
+ _snapshotTestName = testName;
20
+ _snapshotCounter = 0;
21
+ _updateSnapshots = update;
22
+ }
23
+
24
+ function _serializeSnapshot(value: any): string {
25
+ return JSON.stringify(value, (_key, val) => {
26
+ if (typeof val === 'function') return '[Function]';
27
+ return val;
28
+ }, 2);
29
+ }
30
+
31
+ function _loadSnapshots(path: string): Record<string, string> {
32
+ if (_snapshotCache[path]) return _snapshotCache[path];
33
+ const content = _readFile(path);
34
+ if (content) {
35
+ try {
36
+ _snapshotCache[path] = JSON.parse(content);
37
+ } catch {
38
+ _snapshotCache[path] = {};
39
+ }
40
+ } else {
41
+ _snapshotCache[path] = {};
42
+ }
43
+ return _snapshotCache[path];
44
+ }
45
+
46
+ function _saveSnapshots(path: string, data: Record<string, string>) {
47
+ _snapshotCache[path] = data;
48
+ _writeFile(path, JSON.stringify(data, null, 2) + '\n');
49
+ }
50
+
51
+ let _totalSnapshotCount = 0;
52
+ export function getSnapshotCount(): number { return _totalSnapshotCount; }
53
+
54
+ function _matchSnapshot(actual: any): void {
55
+ _snapshotCounter++;
56
+ _totalSnapshotCount++;
57
+ const key = _snapshotTestName + (_snapshotCounter > 1 ? ` ${_snapshotCounter}` : '');
58
+ const serialized = _serializeSnapshot(actual);
59
+
60
+ if (!_snapshotFile) {
61
+ throw new Error('toMatchSnapshot: no snapshot file configured. Is __currentTestFile set?');
62
+ }
63
+
64
+ const snapshots = _loadSnapshots(_snapshotFile);
65
+
66
+ if (_updateSnapshots || !(key in snapshots)) {
67
+ // Write new or updated snapshot
68
+ snapshots[key] = serialized;
69
+ _saveSnapshots(_snapshotFile, snapshots);
70
+ return;
71
+ }
72
+
73
+ // Compare
74
+ const expected = snapshots[key];
75
+ if (serialized !== expected) {
76
+ throw new Error(
77
+ `Snapshot mismatch for "${key}":\n` +
78
+ `Expected:\n${expected}\n\nReceived:\n${serialized}\n\n` +
79
+ `Run with --update-snapshots to update.`
80
+ );
81
+ }
82
+ }
83
+
3
84
  function deepEqual(a: any, b: any): boolean {
4
85
  // Support asymmetric matchers (expect.anything(), expect.any(), expect.objectContaining())
5
86
  if (b != null && typeof b === 'object' && b.__htMatcher && typeof b.matches === 'function') return b.matches(a);
@@ -40,11 +121,23 @@ function formatValue(v: any): string {
40
121
  }
41
122
  }
42
123
 
124
+ // Extract text content from an HTNode tree (for element matchers)
125
+ function _getTextContent(node: any): string {
126
+ if (!node || typeof node !== 'object') return '';
127
+ if (node.type === '__TEXT__') return node.text || '';
128
+ if (!node.children) return '';
129
+ return node.children.map(_getTextContent).join('');
130
+ }
131
+
43
132
  function createAssertion(actual: any, negated: boolean): any {
44
133
  function assert(condition: boolean, message: string) {
45
134
  const pass = negated ? !condition : condition;
46
135
  if (!pass) {
47
- throw new Error(message);
136
+ let hint = '';
137
+ if (actual === undefined && !negated) {
138
+ hint = '\n Hint: Received is undefined. This usually means the module needs to be mocked with ht.mock().';
139
+ }
140
+ throw new Error(message + hint);
48
141
  }
49
142
  }
50
143
 
@@ -53,8 +146,8 @@ function createAssertion(actual: any, negated: boolean): any {
53
146
  assert(
54
147
  actual === expected,
55
148
  negated
56
- ? `Expected ${formatValue(actual)} not to be ${formatValue(expected)}`
57
- : `Expected ${formatValue(expected)}, got ${formatValue(actual)}`
149
+ ? `expect(received).not.toBe(expected)\n\n Expected: not ${formatValue(expected)}\n Received: ${formatValue(actual)}`
150
+ : `expect(received).toBe(expected)\n\n Expected: ${formatValue(expected)}\n Received: ${formatValue(actual)}`
58
151
  );
59
152
  },
60
153
 
@@ -62,8 +155,29 @@ function createAssertion(actual: any, negated: boolean): any {
62
155
  assert(
63
156
  deepEqual(actual, expected),
64
157
  negated
65
- ? `Expected ${formatValue(actual)} not to deeply equal ${formatValue(expected)}`
66
- : `Expected deep equal to ${formatValue(expected)}, got ${formatValue(actual)}`
158
+ ? `expect(received).not.toEqual(expected)\n\n Expected: not ${formatValue(expected)}\n Received: ${formatValue(actual)}`
159
+ : `expect(received).toEqual(expected)\n\n Expected: ${formatValue(expected)}\n Received: ${formatValue(actual)}`
160
+ );
161
+ },
162
+
163
+ toMatchObject(expected: any) {
164
+ function matchesObject(a: any, b: any): boolean {
165
+ if (b != null && typeof b === 'object' && (b as any).__htMatcher && typeof (b as any).matches === 'function') return (b as any).matches(a);
166
+ if (typeof b !== 'object' || b === null) return a === b;
167
+ if (Array.isArray(b)) {
168
+ if (!Array.isArray(a) || a.length < b.length) return false;
169
+ return b.every((v: any, i: number) => matchesObject(a[i], v));
170
+ }
171
+ for (const key of Object.keys(b)) {
172
+ if (!(key in a) || !matchesObject(a[key], b[key])) return false;
173
+ }
174
+ return true;
175
+ }
176
+ assert(
177
+ matchesObject(actual, expected),
178
+ negated
179
+ ? `expect(received).not.toMatchObject(expected)\n\n Expected: not ${formatValue(expected)}\n Received: ${formatValue(actual)}`
180
+ : `expect(received).toMatchObject(expected)\n\n Expected: ${formatValue(expected)}\n Received: ${formatValue(actual)}`
67
181
  );
68
182
  },
69
183
 
@@ -71,8 +185,8 @@ function createAssertion(actual: any, negated: boolean): any {
71
185
  assert(
72
186
  actual !== undefined,
73
187
  negated
74
- ? `Expected value to be undefined, got ${formatValue(actual)}`
75
- : `Expected value to be defined, got undefined`
188
+ ? `expect(received).not.toBeDefined()\n\n Received: ${formatValue(actual)}`
189
+ : `expect(received).toBeDefined()\n\n Received: undefined`
76
190
  );
77
191
  },
78
192
 
@@ -80,8 +194,8 @@ function createAssertion(actual: any, negated: boolean): any {
80
194
  assert(
81
195
  actual === undefined,
82
196
  negated
83
- ? `Expected value not to be undefined`
84
- : `Expected undefined, got ${formatValue(actual)}`
197
+ ? `expect(received).not.toBeUndefined()\n\n Received: ${formatValue(actual)}`
198
+ : `expect(received).toBeUndefined()\n\n Received: ${formatValue(actual)}`
85
199
  );
86
200
  },
87
201
 
@@ -89,8 +203,8 @@ function createAssertion(actual: any, negated: boolean): any {
89
203
  assert(
90
204
  actual === null,
91
205
  negated
92
- ? `Expected value not to be null`
93
- : `Expected null, got ${formatValue(actual)}`
206
+ ? `expect(received).not.toBeNull()\n\n Received: ${formatValue(actual)}`
207
+ : `expect(received).toBeNull()\n\n Received: ${formatValue(actual)}`
94
208
  );
95
209
  },
96
210
 
@@ -99,8 +213,8 @@ function createAssertion(actual: any, negated: boolean): any {
99
213
  assert(
100
214
  len === expected,
101
215
  negated
102
- ? `Expected length not to be ${expected}, but it was`
103
- : `Expected length ${expected}, got ${len}`
216
+ ? `expect(received).not.toHaveLength(expected)\n\n Expected: not ${expected}\n Received length: ${len}`
217
+ : `expect(received).toHaveLength(expected)\n\n Expected: ${expected}\n Received length: ${len}`
104
218
  );
105
219
  },
106
220
 
@@ -108,8 +222,8 @@ function createAssertion(actual: any, negated: boolean): any {
108
222
  assert(
109
223
  actual instanceof expected,
110
224
  negated
111
- ? `Expected ${formatValue(actual)} not to be instance of ${expected?.name ?? expected}`
112
- : `Expected instance of ${expected?.name ?? expected}, got ${formatValue(actual)}`
225
+ ? `expect(received).not.toBeInstanceOf(expected)\n\n Expected: not ${expected?.name ?? expected}`
226
+ : `expect(received).toBeInstanceOf(expected)\n\n Expected: ${expected?.name ?? expected}\n Received: ${formatValue(actual)}`
113
227
  );
114
228
  },
115
229
 
@@ -117,8 +231,8 @@ function createAssertion(actual: any, negated: boolean): any {
117
231
  assert(
118
232
  !!actual,
119
233
  negated
120
- ? `Expected ${formatValue(actual)} to be falsy`
121
- : `Expected truthy value, got ${formatValue(actual)}`
234
+ ? `expect(received).not.toBeTruthy()\n\n Received: ${formatValue(actual)}`
235
+ : `expect(received).toBeTruthy()\n\n Received: ${formatValue(actual)}`
122
236
  );
123
237
  },
124
238
 
@@ -126,8 +240,8 @@ function createAssertion(actual: any, negated: boolean): any {
126
240
  assert(
127
241
  !actual,
128
242
  negated
129
- ? `Expected ${formatValue(actual)} to be truthy`
130
- : `Expected falsy value, got ${formatValue(actual)}`
243
+ ? `expect(received).not.toBeFalsy()\n\n Received: ${formatValue(actual)}`
244
+ : `expect(received).toBeFalsy()\n\n Received: ${formatValue(actual)}`
131
245
  );
132
246
  },
133
247
 
@@ -291,6 +405,157 @@ function createAssertion(actual: any, negated: boolean): any {
291
405
  toHaveBeenCalledTimes(n: number) { return this.wasCalledTimes(n); },
292
406
  toHaveBeenCalledWith(...args: any[]) { return this.wasCalledWith(...args); },
293
407
  toHaveBeenLastCalledWith(...args: any[]) { return this.wasLastCalledWith(...args); },
408
+
409
+ // --- Element matchers (for render() HTNode results) ---
410
+
411
+ toBeRendered() {
412
+ const el = actual;
413
+ const isNode = el && typeof el === 'object' && 'type' in el && 'children' in el;
414
+ assert(
415
+ isNode && el.type !== '__ROOT__',
416
+ negated
417
+ ? `Expected element not to be rendered`
418
+ : `Expected element to be rendered, got ${formatValue(el)}`
419
+ );
420
+ },
421
+
422
+ toHaveTextContent(expected: string | RegExp) {
423
+ const text = _getTextContent(actual);
424
+ const matches = typeof expected === 'string'
425
+ ? text === expected || text.includes(expected)
426
+ : expected.test(text);
427
+ assert(
428
+ matches,
429
+ negated
430
+ ? `Expected element not to have text content "${expected}", but it does`
431
+ : `Expected text content "${expected}", got "${text}"`
432
+ );
433
+ },
434
+
435
+ toContainElement(child: any) {
436
+ function _contains(node: any, target: any): boolean {
437
+ if (node === target) return true;
438
+ if (!node?.children) return false;
439
+ return node.children.some((c: any) => _contains(c, target));
440
+ }
441
+ assert(
442
+ _contains(actual, child),
443
+ negated
444
+ ? `Expected element not to contain the given child`
445
+ : `Expected element to contain the given child`
446
+ );
447
+ },
448
+
449
+ toBeEmpty() {
450
+ const empty = !actual?.children || actual.children.length === 0;
451
+ assert(
452
+ empty,
453
+ negated
454
+ ? `Expected element not to be empty, but it has no children`
455
+ : `Expected element to be empty, but it has ${actual?.children?.length} children`
456
+ );
457
+ },
458
+
459
+ toHaveDisplayValue(expected: string | RegExp) {
460
+ const value = actual?.props?.value ?? '';
461
+ const matches = typeof expected === 'string' ? value === expected : expected.test(value);
462
+ assert(
463
+ matches,
464
+ negated
465
+ ? `Expected display value not to be "${expected}"`
466
+ : `Expected display value "${expected}", got "${value}"`
467
+ );
468
+ },
469
+
470
+ toHaveProp(name: string, value?: any) {
471
+ const hasProp = actual?.props && name in actual.props;
472
+ if (value === undefined) {
473
+ assert(
474
+ hasProp,
475
+ negated
476
+ ? `Expected element not to have prop "${name}"`
477
+ : `Expected element to have prop "${name}"`
478
+ );
479
+ } else {
480
+ const propVal = actual?.props?.[name];
481
+ assert(
482
+ hasProp && deepEqual(propVal, value),
483
+ negated
484
+ ? `Expected prop "${name}" not to be ${formatValue(value)}`
485
+ : `Expected prop "${name}" to be ${formatValue(value)}, got ${formatValue(propVal)}`
486
+ );
487
+ }
488
+ },
489
+
490
+ toHaveStyle(expected: Record<string, any>) {
491
+ const style = actual?.props?.style || {};
492
+ // RN styles can be arrays — flatten
493
+ const flat: Record<string, any> = {};
494
+ const styles = Array.isArray(style) ? style : [style];
495
+ for (const s of styles) {
496
+ if (s && typeof s === 'object') Object.assign(flat, s);
497
+ }
498
+ const allMatch = Object.keys(expected).every((k) => deepEqual(flat[k], expected[k]));
499
+ const mismatches = Object.keys(expected)
500
+ .filter((k) => !deepEqual(flat[k], expected[k]))
501
+ .map((k) => `${k}: expected ${formatValue(expected[k])}, got ${formatValue(flat[k])}`);
502
+ assert(
503
+ allMatch,
504
+ negated
505
+ ? `Expected element not to have styles ${formatValue(expected)}`
506
+ : `Style mismatch: ${mismatches.join('; ')}`
507
+ );
508
+ },
509
+
510
+ toBeEnabled() {
511
+ const disabled = actual?.props?.disabled === true
512
+ || actual?.props?.editable === false
513
+ || actual?.props?.accessibilityState?.disabled === true
514
+ || actual?.props?.['aria-disabled'] === true;
515
+ assert(
516
+ !disabled,
517
+ negated
518
+ ? `Expected element to be disabled, but it is enabled`
519
+ : `Expected element to be enabled, but it is disabled`
520
+ );
521
+ },
522
+
523
+ toBeDisabled() {
524
+ const disabled = actual?.props?.disabled === true
525
+ || actual?.props?.editable === false
526
+ || actual?.props?.accessibilityState?.disabled === true
527
+ || actual?.props?.['aria-disabled'] === true;
528
+ assert(
529
+ disabled,
530
+ negated
531
+ ? `Expected element not to be disabled, but it is`
532
+ : `Expected element to be disabled, but it is enabled`
533
+ );
534
+ },
535
+
536
+ toBeVisible() {
537
+ const style = actual?.props?.style || {};
538
+ const styles = Array.isArray(style) ? style : [style];
539
+ const flat: Record<string, any> = {};
540
+ for (const s of styles) {
541
+ if (s && typeof s === 'object') Object.assign(flat, s);
542
+ }
543
+ const hidden = flat.display === 'none' || flat.opacity === 0
544
+ || actual?.props?.accessibilityElementsHidden === true
545
+ || actual?.props?.importantForAccessibility === 'no-hide-descendants';
546
+ assert(
547
+ !hidden,
548
+ negated
549
+ ? `Expected element not to be visible`
550
+ : `Expected element to be visible, but it is hidden`
551
+ );
552
+ },
553
+
554
+ // --- Snapshot matcher ---
555
+
556
+ toMatchSnapshot() {
557
+ _matchSnapshot(actual);
558
+ },
294
559
  };
295
560
 
296
561
  if (!negated) {
@@ -352,3 +617,5 @@ expect.stringMatching = (pattern: RegExp | string) => makeMatcher((v) => {
352
617
  const re = typeof pattern === 'string' ? new RegExp(pattern) : pattern;
353
618
  return typeof v === 'string' && re.test(v);
354
619
  });
620
+
621
+ export { _setSnapshotContext };
package/src/fetch.ts CHANGED
@@ -1,4 +1,4 @@
1
- // mockFetch — lightweight fetch mock for Hermes
1
+ // mock.fetch — lightweight fetch mock for Hermes
2
2
  // Replaces globalThis.fetch with a handler-based mock (like MSW but pure JS)
3
3
 
4
4
  type Method = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';
@@ -91,7 +91,7 @@ function fakeFetch(input: any, init?: any): any {
91
91
 
92
92
  if (!handler) {
93
93
  // Unhandled request — return a rejected-style response
94
- const msg = `[mockFetch] Unhandled ${method} ${url}`;
94
+ const msg = `[mock.fetch] Unhandled ${method} ${url}`;
95
95
  // Return a promise that resolves to a 500 response
96
96
  return Promise.resolve({
97
97
  ok: false,
@@ -134,8 +134,24 @@ function createHandler(method: Method, url: string | RegExp, response: MockRespo
134
134
  return { method, url, handler, once };
135
135
  }
136
136
 
137
- // Register base handlers (persist across tests)
137
+ // Register handlers automatically replaces any existing handler with same method+url
138
138
  export function mockFetch(...newHandlers: MockHandler[]): void {
139
+ for (const nh of newHandlers) {
140
+ // Remove existing handler with same method+url (auto-overwrite)
141
+ for (let i = handlers.length - 1; i >= 0; i--) {
142
+ const h = handlers[i];
143
+ if (h.method === nh.method && String(h.url) === String(nh.url)) {
144
+ handlers.splice(i, 1);
145
+ }
146
+ }
147
+ // Also remove from overrides
148
+ for (let i = overrideHandlers.length - 1; i >= 0; i--) {
149
+ const h = overrideHandlers[i];
150
+ if (h.method === nh.method && String(h.url) === String(nh.url)) {
151
+ overrideHandlers.splice(i, 1);
152
+ }
153
+ }
154
+ }
139
155
  handlers.push(...newHandlers);
140
156
  // Install fetch on globalThis
141
157
  (globalThis as any).fetch = fakeFetch;
@@ -173,14 +189,10 @@ export const HttpResponse = {
173
189
  },
174
190
  };
175
191
 
176
- // Per-test overrides (like MSW server.use())
177
- // Also installs fakeFetch on globalThis if not already done (handlers-only path).
192
+ // Per-test overrides — now just delegates to mockFetch (auto-overwrite handles dedup)
193
+ // Kept for backwards compatibility with existing tests using mock.fetch.overwrite()
178
194
  export function mockFetchUse(...newHandlers: MockHandler[]): void {
179
- overrideHandlers.push(...newHandlers);
180
- // Ensure fakeFetch is installed — mockFetchUse may be called without a prior mockFetch call
181
- if ((globalThis as any).fetch !== fakeFetch) {
182
- (globalThis as any).fetch = fakeFetch;
183
- }
195
+ mockFetch(...newHandlers);
184
196
  }
185
197
 
186
198
  // Reset per-test overrides (like MSW server.resetHandlers())