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/README.md +81 -28
- package/bin/hermes-test.js +12 -0
- package/dist/harness.bundle.js +2000 -272
- package/globals.d.ts +19 -0
- package/index.d.ts +77 -7
- package/package.json +13 -8
- package/src/expect.ts +286 -19
- package/src/fetch.ts +22 -10
- package/src/harness.ts +187 -17
- package/src/hooks.ts +54 -34
- package/src/index.ts +3 -5
- package/src/mock.ts +4 -4
- package/src/render.ts +296 -0
- package/src/shims/rtk-query-core.js +39 -0
- package/src/spy.ts +6 -0
- package/src/store.ts +1 -0
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.
|
|
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.
|
|
54
|
-
"@hermes-test/darwin-x64": "0.
|
|
55
|
-
"@hermes-test/linux-x64": "0.
|
|
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
|
-
|
|
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(
|
|
57
|
-
: `Expected ${formatValue(expected)}
|
|
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(
|
|
66
|
-
: `Expected
|
|
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
|
-
? `
|
|
75
|
-
: `
|
|
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
|
-
? `
|
|
84
|
-
: `
|
|
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
|
-
? `
|
|
93
|
-
: `
|
|
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
|
|
103
|
-
: `Expected
|
|
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
|
-
? `
|
|
112
|
-
: `Expected
|
|
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
|
-
? `
|
|
121
|
-
: `
|
|
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
|
-
? `
|
|
130
|
-
: `
|
|
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
|
-
//
|
|
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 = `[
|
|
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
|
|
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 (
|
|
177
|
-
//
|
|
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
|
-
|
|
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())
|