frida-test 0.3.2 → 0.4.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/README.md +70 -29
- package/bin/frida-test-compile.js +1 -1
- package/bin/frida-test.js +1 -1
- package/dist/host/bundler.d.ts.map +1 -0
- package/dist/{bundler.js → host/bundler.js} +12 -14
- package/dist/host/cli-frida-test-compiler.d.ts.map +1 -0
- package/dist/{cli-frida-test-compiler.js → host/cli-frida-test-compiler.js} +2 -2
- package/dist/host/cli-frida-test.d.ts.map +1 -0
- package/dist/{cli-frida-test.js → host/cli-frida-test.js} +10 -4
- package/dist/host/collector.d.ts.map +1 -0
- package/dist/host/collector.js +40 -0
- package/dist/host/device.d.ts +12 -0
- package/dist/host/device.d.ts.map +1 -0
- package/dist/host/device.js +19 -0
- package/dist/{logger.d.ts → host/logger.d.ts} +0 -2
- package/dist/host/logger.d.ts.map +1 -0
- package/dist/{logger.js → host/logger.js} +1 -3
- package/dist/host/protocol.d.ts.map +1 -0
- package/dist/host/protocol.js +12 -0
- package/dist/host/reporter/console.d.ts +4 -0
- package/dist/host/reporter/console.d.ts.map +1 -0
- package/dist/{reporter → host/reporter}/console.js +5 -9
- package/dist/host/reporter/json.d.ts.map +1 -0
- package/dist/host/reporter/summary.d.ts.map +1 -0
- package/dist/{reporter → host/reporter}/summary.js +6 -5
- package/dist/host/runner.d.ts.map +1 -0
- package/dist/{runner.js → host/runner.js} +20 -10
- package/dist/host/target.d.ts.map +1 -0
- package/dist/{target.js → host/target.js} +11 -6
- package/package.json +14 -8
- package/src/agent-runtime/expect.ts +5 -0
- package/src/agent-runtime/fridaTest.ts +9 -0
- package/src/agent-runtime/globals.d.ts +8 -4
- package/src/agent-runtime/globals.ts +10 -4
- package/src/agent-runtime/matchers.ts +102 -119
- package/src/agent-runtime/mock.ts +131 -0
- package/src/agent-runtime/modifiers.ts +102 -0
- package/src/agent-runtime/registry.ts +14 -3
- package/src/agent-runtime/spy.ts +30 -0
- package/dist/bundler.d.ts.map +0 -1
- package/dist/cli-frida-test-compiler.d.ts.map +0 -1
- package/dist/cli-frida-test.d.ts.map +0 -1
- package/dist/collector.d.ts.map +0 -1
- package/dist/collector.js +0 -27
- package/dist/device.d.ts +0 -28
- package/dist/device.d.ts.map +0 -1
- package/dist/device.js +0 -68
- package/dist/logger.d.ts.map +0 -1
- package/dist/protocol.d.ts.map +0 -1
- package/dist/protocol.js +0 -6
- package/dist/reporter/console.d.ts +0 -3
- package/dist/reporter/console.d.ts.map +0 -1
- package/dist/reporter/json.d.ts.map +0 -1
- package/dist/reporter/summary.d.ts.map +0 -1
- package/dist/runner.d.ts.map +0 -1
- package/dist/target.d.ts.map +0 -1
- /package/dist/{bundler.d.ts → host/bundler.d.ts} +0 -0
- /package/dist/{cli-frida-test-compiler.d.ts → host/cli-frida-test-compiler.d.ts} +0 -0
- /package/dist/{cli-frida-test.d.ts → host/cli-frida-test.d.ts} +0 -0
- /package/dist/{collector.d.ts → host/collector.d.ts} +0 -0
- /package/dist/{protocol.d.ts → host/protocol.d.ts} +0 -0
- /package/dist/{reporter → host/reporter}/json.d.ts +0 -0
- /package/dist/{reporter → host/reporter}/json.js +0 -0
- /package/dist/{reporter → host/reporter}/summary.d.ts +0 -0
- /package/dist/{runner.d.ts → host/runner.d.ts} +0 -0
- /package/dist/{target.d.ts → host/target.d.ts} +0 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { fn, type AnyFn, type Mock } from "./mock.js";
|
|
2
|
+
import { spyOn } from "./spy.js";
|
|
3
|
+
|
|
4
|
+
export interface FridaTest {
|
|
5
|
+
fn(implementation?: AnyFn): Mock;
|
|
6
|
+
spyOn<T extends object, K extends keyof T>(target: T, key: K): Mock;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const fridaTest: FridaTest = { fn, spyOn };
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
type
|
|
2
|
-
type
|
|
1
|
+
type Matchers<T> = import("./modifiers.js").Matchers<T>;
|
|
2
|
+
type Mock = import("./mock.js").Mock;
|
|
3
|
+
type AnyFn = import("./mock.js").AnyFn;
|
|
4
|
+
type FridaTest = import("./fridaTest.js").FridaTest;
|
|
3
5
|
type TestFn = import("./registry.js").TestFn;
|
|
4
6
|
type HookFn = import("./registry.js").HookFn;
|
|
5
7
|
|
|
@@ -10,5 +12,7 @@ declare function beforeEach(fn: HookFn): void;
|
|
|
10
12
|
declare function afterEach(fn: HookFn): void;
|
|
11
13
|
declare function beforeAll(fn: HookFn): void;
|
|
12
14
|
declare function afterAll(fn: HookFn): void;
|
|
13
|
-
declare function expect<T>(actual: T):
|
|
14
|
-
declare function spyOn<T extends object, K extends keyof T>(target: T, key: K):
|
|
15
|
+
declare function expect<T>(actual: T): Matchers<T>;
|
|
16
|
+
declare function spyOn<T extends object, K extends keyof T>(target: T, key: K): Mock;
|
|
17
|
+
declare function fn(implementation?: AnyFn): Mock;
|
|
18
|
+
declare var fridaTest: FridaTest;
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import { expect
|
|
1
|
+
import { expect } from "./expect.js";
|
|
2
|
+
import { fridaTest, type FridaTest } from "./fridaTest.js";
|
|
3
|
+
import { fn, type AnyFn, type Mock } from "./mock.js";
|
|
4
|
+
import type { Matchers } from "./modifiers.js";
|
|
2
5
|
import type { HookFn, TestFn } from "./registry.js";
|
|
3
6
|
import { afterAll, afterEach, beforeAll, beforeEach, describe, it, test } from "./registry.js";
|
|
7
|
+
import { spyOn } from "./spy.js";
|
|
4
8
|
|
|
5
9
|
declare global {
|
|
6
10
|
function describe(name: string, fn: TestFn): void;
|
|
@@ -10,8 +14,10 @@ declare global {
|
|
|
10
14
|
function afterEach(fn: HookFn): void;
|
|
11
15
|
function beforeAll(fn: HookFn): void;
|
|
12
16
|
function afterAll(fn: HookFn): void;
|
|
13
|
-
function expect<T>(actual: T):
|
|
14
|
-
function spyOn<T extends object, K extends keyof T>(target: T, key: K):
|
|
17
|
+
function expect<T>(actual: T): Matchers<T>;
|
|
18
|
+
function spyOn<T extends object, K extends keyof T>(target: T, key: K): Mock;
|
|
19
|
+
function fn(implementation?: AnyFn): Mock;
|
|
20
|
+
var fridaTest: FridaTest;
|
|
15
21
|
}
|
|
16
22
|
|
|
17
|
-
Object.assign(globalThis, { describe, it, test, beforeEach, afterEach, beforeAll, afterAll, expect, spyOn });
|
|
23
|
+
Object.assign(globalThis, { describe, it, test, beforeEach, afterEach, beforeAll, afterAll, expect, spyOn, fn, fridaTest });
|
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
import { isMock, type Mock } from "./mock.js";
|
|
2
|
+
|
|
3
|
+
export type Containable<T> = T extends readonly (infer U)[] ? U : T extends ReadonlySet<infer U> ? U : T extends string ? string : never;
|
|
2
4
|
export type Numeric<T> = T extends number ? number : never;
|
|
3
5
|
|
|
4
|
-
export
|
|
6
|
+
export type ErrorClass = new (...args: any[]) => Error;
|
|
7
|
+
export type ErrorMatch = string | RegExp | Error | ErrorClass;
|
|
8
|
+
|
|
9
|
+
export interface Assertions<T> {
|
|
5
10
|
toBe(expected: T): void;
|
|
6
11
|
toEqual(expected: T): void;
|
|
7
12
|
toBeTruthy(): void;
|
|
@@ -12,19 +17,13 @@ export interface Matcher<T> {
|
|
|
12
17
|
toBeGreaterThan(expected: Numeric<T>): void;
|
|
13
18
|
toBeLessThan(expected: Numeric<T>): void;
|
|
14
19
|
toContain(expected: Containable<T>): void;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
toReject(errorMatch?: string | Error): Promise<void>;
|
|
20
|
+
toContainEqual(expected: Containable<T>): void;
|
|
21
|
+
toThrow(errorMatch?: ErrorMatch): void;
|
|
18
22
|
toHaveBeenCalled(): void;
|
|
23
|
+
toHaveBeenCalledTimes(expected: number): void;
|
|
19
24
|
toHaveBeenCalledWith(...expected: unknown[]): void;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
export interface Spy {
|
|
24
|
-
readonly calls: readonly unknown[][];
|
|
25
|
-
restore(): void;
|
|
26
|
-
mockReturnValue(value: unknown): Spy;
|
|
27
|
-
mockImplementation(fn: (...args: unknown[]) => unknown): Spy;
|
|
25
|
+
toHaveBeenLastCalledWith(...expected: unknown[]): void;
|
|
26
|
+
toHaveBeenNthCalledWith(n: number, ...expected: unknown[]): void;
|
|
28
27
|
}
|
|
29
28
|
|
|
30
29
|
const assert = (condition: boolean, message: string): void => {
|
|
@@ -55,10 +54,15 @@ const deepEqual = (a: unknown, b: unknown, seen: Array<[unknown, unknown]> = [])
|
|
|
55
54
|
}
|
|
56
55
|
|
|
57
56
|
if (a instanceof Set) {
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
// Match each element of `a` against a distinct, not-yet-consumed element of
|
|
58
|
+
// `b` (rather than `some`), so a value appearing twice in `a` can't both be
|
|
59
|
+
// satisfied by the same single matching element in `b`.
|
|
60
|
+
const remaining = [...(b as Set<unknown>)];
|
|
61
|
+
if (a.size !== remaining.length) return false;
|
|
60
62
|
for (const val of a) {
|
|
61
|
-
|
|
63
|
+
const index = remaining.findIndex((other) => deepEqual(val, other, nextSeen));
|
|
64
|
+
if (index === -1) return false;
|
|
65
|
+
remaining.splice(index, 1);
|
|
62
66
|
}
|
|
63
67
|
return true;
|
|
64
68
|
}
|
|
@@ -71,31 +75,40 @@ const deepEqual = (a: unknown, b: unknown, seen: Array<[unknown, unknown]> = [])
|
|
|
71
75
|
return keysA.every((k) => deepEqual(ra[k], rb[k], nextSeen));
|
|
72
76
|
};
|
|
73
77
|
|
|
74
|
-
const isSpy = (value: unknown): value is Spy => typeof value === "object" && value !== null && Array.isArray((value as Spy).calls);
|
|
75
|
-
|
|
76
78
|
function assertIsNumber(value: unknown, label: string): asserts value is number {
|
|
77
79
|
assert(typeof value === "number", `Expected ${label} to be a number`);
|
|
78
80
|
}
|
|
79
81
|
|
|
80
|
-
function
|
|
82
|
+
export function matchesThrown(caught: unknown, errorMatch: ErrorMatch): boolean {
|
|
83
|
+
if (typeof errorMatch === "string") {
|
|
84
|
+
const message = caught instanceof Error ? caught.message : String(caught);
|
|
85
|
+
return message.includes(errorMatch);
|
|
86
|
+
}
|
|
87
|
+
if (errorMatch instanceof RegExp) {
|
|
88
|
+
const message = caught instanceof Error ? caught.message : String(caught);
|
|
89
|
+
return errorMatch.test(message);
|
|
90
|
+
}
|
|
91
|
+
if (errorMatch instanceof Error) {
|
|
92
|
+
return caught instanceof Error && caught.message === errorMatch.message;
|
|
93
|
+
}
|
|
94
|
+
return caught instanceof errorMatch;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function describeCaught(caught: unknown): string {
|
|
98
|
+
return caught instanceof Error ? `${caught.constructor.name}: "${caught.message}"` : JSON.stringify(caught);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function describeErrorMatch(errorMatch: ErrorMatch): string {
|
|
102
|
+
if (typeof errorMatch === "string") return `a message including "${errorMatch}"`;
|
|
103
|
+
if (errorMatch instanceof RegExp) return `a message matching ${errorMatch}`;
|
|
104
|
+
if (errorMatch instanceof Error) return `${errorMatch.constructor.name}: "${errorMatch.message}"`;
|
|
105
|
+
return `an instance of ${errorMatch.name}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function createMatcher<T>(actual: T, negated = false): Assertions<T> {
|
|
81
109
|
const check = (condition: boolean, message: string): void => assert(negated ? !condition : condition, message);
|
|
82
110
|
const phrase = negated ? "not to" : "to";
|
|
83
111
|
|
|
84
|
-
const describeCaught = (caught: unknown): string =>
|
|
85
|
-
caught instanceof Error ? `${caught.constructor.name}: "${caught.message}"` : JSON.stringify(caught);
|
|
86
|
-
|
|
87
|
-
const checkThrown = (caught: unknown, errorMatch: string | Error): void => {
|
|
88
|
-
if (typeof errorMatch === "string") {
|
|
89
|
-
const message = caught instanceof Error ? caught.message : String(caught);
|
|
90
|
-
assert(message.includes(errorMatch), `Expected message to include "${errorMatch}" but got "${message}"`);
|
|
91
|
-
} else {
|
|
92
|
-
assert(
|
|
93
|
-
caught instanceof Error && caught instanceof errorMatch.constructor && caught.message === errorMatch.message,
|
|
94
|
-
`Expected ${errorMatch.constructor.name}: "${errorMatch.message}" but got ${describeCaught(caught)}`,
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
};
|
|
98
|
-
|
|
99
112
|
return {
|
|
100
113
|
toBe: (expected) => check(Object.is(actual, expected), `Expected ${String(actual)} ${phrase} be ${String(expected)}`),
|
|
101
114
|
|
|
@@ -121,12 +134,23 @@ function createMatcher<T>(actual: T, negated = false): Matcher<T> {
|
|
|
121
134
|
},
|
|
122
135
|
|
|
123
136
|
toContain: (expected) => {
|
|
124
|
-
assert(typeof actual === "string" || Array.isArray(actual), "Expected an array or string");
|
|
137
|
+
assert(typeof actual === "string" || Array.isArray(actual) || actual instanceof Set, "Expected an array, Set, or string");
|
|
125
138
|
const contains =
|
|
126
|
-
typeof actual === "string"
|
|
139
|
+
typeof actual === "string"
|
|
140
|
+
? actual.includes(expected as string)
|
|
141
|
+
: actual instanceof Set
|
|
142
|
+
? actual.has(expected)
|
|
143
|
+
: (actual as readonly unknown[]).includes(expected);
|
|
127
144
|
check(contains, `Expected ${JSON.stringify(actual)} ${phrase} contain ${JSON.stringify(expected)}`);
|
|
128
145
|
},
|
|
129
146
|
|
|
147
|
+
toContainEqual: (expected) => {
|
|
148
|
+
assert(Array.isArray(actual) || actual instanceof Set, "Expected an array or Set");
|
|
149
|
+
const items = actual instanceof Set ? [...actual] : (actual as readonly unknown[]);
|
|
150
|
+
const contains = items.some((item) => deepEqual(item, expected));
|
|
151
|
+
check(contains, `Expected ${JSON.stringify(actual)} ${phrase} contain an item equal to ${JSON.stringify(expected)}`);
|
|
152
|
+
},
|
|
153
|
+
|
|
130
154
|
toThrow: (errorMatch) => {
|
|
131
155
|
assert(typeof actual === "function", "Expected a function");
|
|
132
156
|
let caught: unknown;
|
|
@@ -140,104 +164,63 @@ function createMatcher<T>(actual: T, negated = false): Matcher<T> {
|
|
|
140
164
|
}
|
|
141
165
|
|
|
142
166
|
if (!threw && result != null && typeof (result as PromiseLike<unknown>).then === "function") {
|
|
143
|
-
|
|
167
|
+
// Avoid leaving the caller's promise unhandled while we redirect them to .rejects.
|
|
168
|
+
void Promise.resolve(result as PromiseLike<unknown>).catch(() => {});
|
|
169
|
+
throw new Error("toThrow() received a function returning a Promise; use await expect(fn()).rejects.toThrow(...) instead");
|
|
144
170
|
}
|
|
145
171
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
},
|
|
150
|
-
|
|
151
|
-
toResolve: async (expected) => {
|
|
152
|
-
assert(typeof actual === "function", "Expected a function returning a promise");
|
|
153
|
-
let caught: unknown;
|
|
154
|
-
let resolved = false;
|
|
155
|
-
try {
|
|
156
|
-
caught = await (actual as () => Promise<unknown>)();
|
|
157
|
-
resolved = true;
|
|
158
|
-
} catch (e) {
|
|
159
|
-
// promise rejected
|
|
172
|
+
if (errorMatch === undefined) {
|
|
173
|
+
check(threw, `Expected function ${phrase} throw`);
|
|
174
|
+
return;
|
|
160
175
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
176
|
+
const matches = threw && matchesThrown(caught, errorMatch);
|
|
177
|
+
check(
|
|
178
|
+
matches,
|
|
179
|
+
threw
|
|
180
|
+
? `Expected function ${phrase} throw ${describeErrorMatch(errorMatch)} but got ${describeCaught(caught)}`
|
|
181
|
+
: `Expected function ${phrase} throw`,
|
|
182
|
+
);
|
|
164
183
|
},
|
|
165
184
|
|
|
166
|
-
|
|
167
|
-
assert(
|
|
168
|
-
|
|
169
|
-
let rejected = false;
|
|
170
|
-
try {
|
|
171
|
-
await (actual as () => Promise<unknown>)();
|
|
172
|
-
} catch (e) {
|
|
173
|
-
rejected = true;
|
|
174
|
-
caught = e;
|
|
175
|
-
}
|
|
176
|
-
check(rejected, `Expected promise ${phrase} reject`);
|
|
177
|
-
if (negated || errorMatch === undefined) return;
|
|
178
|
-
checkThrown(caught, errorMatch);
|
|
185
|
+
toHaveBeenCalled: () => {
|
|
186
|
+
assert(isMock(actual), "Expected a mock function created with fn() or spyOn()");
|
|
187
|
+
check((actual as Mock).mock.calls.length > 0, `Expected mock ${phrase} have been called`);
|
|
179
188
|
},
|
|
180
189
|
|
|
181
|
-
|
|
182
|
-
assert(
|
|
183
|
-
|
|
190
|
+
toHaveBeenCalledTimes: (expected: number) => {
|
|
191
|
+
assert(isMock(actual), "Expected a mock function created with fn() or spyOn()");
|
|
192
|
+
const count = (actual as Mock).mock.calls.length;
|
|
193
|
+
check(count === expected, `Expected mock ${phrase} have been called ${expected} time(s) but it was called ${count} time(s)`);
|
|
184
194
|
},
|
|
185
195
|
|
|
186
196
|
toHaveBeenCalledWith: (...expected: unknown[]) => {
|
|
187
|
-
assert(
|
|
188
|
-
const calls = (actual as
|
|
197
|
+
assert(isMock(actual), "Expected a mock function created with fn() or spyOn()");
|
|
198
|
+
const calls = (actual as Mock).mock.calls;
|
|
189
199
|
const match = calls.some((args) => deepEqual(args, expected));
|
|
190
|
-
check(match, `Expected
|
|
200
|
+
check(match, `Expected mock ${phrase} have been called with ${JSON.stringify(expected)} but got ${JSON.stringify(calls)}`);
|
|
191
201
|
},
|
|
192
202
|
|
|
193
|
-
|
|
194
|
-
|
|
203
|
+
toHaveBeenLastCalledWith: (...expected: unknown[]) => {
|
|
204
|
+
assert(isMock(actual), "Expected a mock function created with fn() or spyOn()");
|
|
205
|
+
const calls = (actual as Mock).mock.calls;
|
|
206
|
+
const last = calls[calls.length - 1];
|
|
207
|
+
const match = calls.length > 0 && deepEqual(last, expected);
|
|
208
|
+
check(
|
|
209
|
+
match,
|
|
210
|
+
`Expected mock ${phrase} have last been called with ${JSON.stringify(expected)} but got ${calls.length > 0 ? JSON.stringify(last) : "no calls"}`,
|
|
211
|
+
);
|
|
195
212
|
},
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
213
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
let impl: (...args: unknown[]) => unknown = (original as (...a: unknown[]) => unknown).bind(target);
|
|
210
|
-
let returnValue: unknown;
|
|
211
|
-
let hasReturnValue = false;
|
|
212
|
-
let restored = false;
|
|
213
|
-
|
|
214
|
-
const spy: Spy = {
|
|
215
|
-
calls,
|
|
216
|
-
restore: () => {
|
|
217
|
-
if (restored) return;
|
|
218
|
-
restored = true;
|
|
219
|
-
if (descriptor) {
|
|
220
|
-
Object.defineProperty(target, key, descriptor);
|
|
221
|
-
} else {
|
|
222
|
-
Reflect.deleteProperty(target, key);
|
|
223
|
-
}
|
|
224
|
-
},
|
|
225
|
-
mockReturnValue(value: unknown) {
|
|
226
|
-
hasReturnValue = true;
|
|
227
|
-
returnValue = value;
|
|
228
|
-
return spy;
|
|
229
|
-
},
|
|
230
|
-
mockImplementation(fn: (...args: unknown[]) => unknown) {
|
|
231
|
-
impl = fn;
|
|
232
|
-
hasReturnValue = false;
|
|
233
|
-
return spy;
|
|
214
|
+
toHaveBeenNthCalledWith: (n: number, ...expected: unknown[]) => {
|
|
215
|
+
assert(isMock(actual), "Expected a mock function created with fn() or spyOn()");
|
|
216
|
+
assert(Number.isInteger(n) && n >= 1, "Expected the call index to be a positive integer");
|
|
217
|
+
const calls = (actual as Mock).mock.calls;
|
|
218
|
+
const call = calls[n - 1];
|
|
219
|
+
const match = call !== undefined && deepEqual(call, expected);
|
|
220
|
+
check(
|
|
221
|
+
match,
|
|
222
|
+
`Expected mock ${phrase} have been called on call ${n} with ${JSON.stringify(expected)} but got ${call !== undefined ? JSON.stringify(call) : "no such call"}`,
|
|
223
|
+
);
|
|
234
224
|
},
|
|
235
225
|
};
|
|
236
|
-
|
|
237
|
-
target[key] = ((...args: unknown[]) => {
|
|
238
|
-
calls.push(args);
|
|
239
|
-
return hasReturnValue ? returnValue : impl(...args);
|
|
240
|
-
}) as T[K];
|
|
241
|
-
|
|
242
|
-
return spy;
|
|
243
226
|
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// `any` (not `unknown`) so a concretely-typed function - e.g. `fn((a: number, b: number) => a + b)` -
|
|
2
|
+
// is assignable here; with `unknown` parameters, strict function-parameter variance would reject it.
|
|
3
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
4
|
+
export type AnyFn = (...args: any[]) => any;
|
|
5
|
+
|
|
6
|
+
export interface MockResult {
|
|
7
|
+
readonly type: "return" | "throw";
|
|
8
|
+
readonly value: unknown;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface MockContext {
|
|
12
|
+
readonly calls: readonly unknown[][];
|
|
13
|
+
readonly results: readonly MockResult[];
|
|
14
|
+
readonly instances: readonly unknown[];
|
|
15
|
+
readonly contexts: readonly unknown[];
|
|
16
|
+
readonly lastCall: readonly unknown[] | undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface Mock {
|
|
20
|
+
(...args: unknown[]): unknown;
|
|
21
|
+
readonly mock: MockContext;
|
|
22
|
+
mockClear(): Mock;
|
|
23
|
+
mockReset(): Mock;
|
|
24
|
+
mockRestore(): void;
|
|
25
|
+
mockImplementation(implementation: AnyFn): Mock;
|
|
26
|
+
mockImplementationOnce(implementation: AnyFn): Mock;
|
|
27
|
+
mockReturnValue(value: unknown): Mock;
|
|
28
|
+
mockReturnValueOnce(value: unknown): Mock;
|
|
29
|
+
mockResolvedValue(value: unknown): Mock;
|
|
30
|
+
mockResolvedValueOnce(value: unknown): Mock;
|
|
31
|
+
mockRejectedValue(value: unknown): Mock;
|
|
32
|
+
mockRejectedValueOnce(value: unknown): Mock;
|
|
33
|
+
mockReturnThis(): Mock;
|
|
34
|
+
mockName(name: string): Mock;
|
|
35
|
+
getMockName(): string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const isMock = (value: unknown): value is Mock => typeof value === "function" && Array.isArray((value as Partial<Mock>).mock?.calls);
|
|
39
|
+
|
|
40
|
+
export function createMockFunction(defaultImplementation: AnyFn | undefined, name: string): Mock {
|
|
41
|
+
const calls: unknown[][] = [];
|
|
42
|
+
const results: MockResult[] = [];
|
|
43
|
+
const instances: unknown[] = [];
|
|
44
|
+
const contexts: unknown[] = [];
|
|
45
|
+
const onceQueue: AnyFn[] = [];
|
|
46
|
+
let persistentImplementation: AnyFn | undefined = defaultImplementation;
|
|
47
|
+
let mockedName = name;
|
|
48
|
+
|
|
49
|
+
function invoke(this: unknown, ...args: unknown[]): unknown {
|
|
50
|
+
calls.push(args);
|
|
51
|
+
contexts.push(this);
|
|
52
|
+
if (new.target !== undefined) instances.push(this);
|
|
53
|
+
|
|
54
|
+
const impl = onceQueue.shift() ?? persistentImplementation ?? ((): undefined => undefined);
|
|
55
|
+
try {
|
|
56
|
+
const value = impl.apply(this, args);
|
|
57
|
+
results.push({ type: "return", value });
|
|
58
|
+
return value;
|
|
59
|
+
} catch (error) {
|
|
60
|
+
results.push({ type: "throw", value: error });
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const mock = invoke as unknown as Mock;
|
|
66
|
+
|
|
67
|
+
Object.defineProperty(mock, "mock", {
|
|
68
|
+
enumerable: true,
|
|
69
|
+
get(): MockContext {
|
|
70
|
+
return {
|
|
71
|
+
calls,
|
|
72
|
+
results,
|
|
73
|
+
instances,
|
|
74
|
+
contexts,
|
|
75
|
+
lastCall: calls.length > 0 ? calls[calls.length - 1] : undefined,
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
mock.mockClear = (): Mock => {
|
|
81
|
+
calls.length = 0;
|
|
82
|
+
results.length = 0;
|
|
83
|
+
instances.length = 0;
|
|
84
|
+
contexts.length = 0;
|
|
85
|
+
return mock;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
mock.mockReset = (): Mock => {
|
|
89
|
+
mock.mockClear();
|
|
90
|
+
onceQueue.length = 0;
|
|
91
|
+
persistentImplementation = undefined;
|
|
92
|
+
return mock;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
mock.mockRestore = (): void => {
|
|
96
|
+
mock.mockReset();
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
mock.mockImplementation = (implementation: AnyFn): Mock => {
|
|
100
|
+
persistentImplementation = implementation;
|
|
101
|
+
return mock;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
mock.mockImplementationOnce = (implementation: AnyFn): Mock => {
|
|
105
|
+
onceQueue.push(implementation);
|
|
106
|
+
return mock;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
mock.mockReturnValue = (value: unknown): Mock => mock.mockImplementation((): unknown => value);
|
|
110
|
+
mock.mockReturnValueOnce = (value: unknown): Mock => mock.mockImplementationOnce((): unknown => value);
|
|
111
|
+
mock.mockResolvedValue = (value: unknown): Mock => mock.mockImplementation((): Promise<unknown> => Promise.resolve(value));
|
|
112
|
+
mock.mockResolvedValueOnce = (value: unknown): Mock => mock.mockImplementationOnce((): Promise<unknown> => Promise.resolve(value));
|
|
113
|
+
mock.mockRejectedValue = (value: unknown): Mock => mock.mockImplementation((): Promise<unknown> => Promise.reject(value));
|
|
114
|
+
mock.mockRejectedValueOnce = (value: unknown): Mock => mock.mockImplementationOnce((): Promise<unknown> => Promise.reject(value));
|
|
115
|
+
mock.mockReturnThis = (): Mock =>
|
|
116
|
+
mock.mockImplementation(function (this: unknown): unknown {
|
|
117
|
+
return this;
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
mock.mockName = (newName: string): Mock => {
|
|
121
|
+
mockedName = newName;
|
|
122
|
+
return mock;
|
|
123
|
+
};
|
|
124
|
+
mock.getMockName = (): string => mockedName;
|
|
125
|
+
|
|
126
|
+
return mock;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function fn(implementation?: AnyFn): Mock {
|
|
130
|
+
return createMockFunction(implementation, "fn()");
|
|
131
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { createMatcher, describeCaught, matchesThrown, type Assertions, type ErrorMatch } from "./matchers.js";
|
|
2
|
+
|
|
3
|
+
export interface Matchers<T> extends Assertions<T> {
|
|
4
|
+
readonly not: Matchers<T>;
|
|
5
|
+
readonly resolves: AsyncMatcher<Awaited<T>>;
|
|
6
|
+
readonly rejects: AsyncMatcher<unknown>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
type Promisify<M> = {
|
|
10
|
+
[K in keyof M]: M[K] extends (...args: infer A) => void ? (...args: A) => Promise<void> : never;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export interface AsyncMatcher<T> extends Promisify<Assertions<T>> {
|
|
14
|
+
readonly not: AsyncMatcher<T>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type SyncMethodName = keyof Assertions<unknown>;
|
|
18
|
+
|
|
19
|
+
type Settled = { ok: true; value: unknown } | { ok: false; value: unknown };
|
|
20
|
+
|
|
21
|
+
async function settle(awaited: unknown): Promise<Settled> {
|
|
22
|
+
try {
|
|
23
|
+
return { ok: true, value: await awaited };
|
|
24
|
+
} catch (error) {
|
|
25
|
+
return { ok: false, value: error };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
|
|
30
|
+
return (typeof value === "object" || typeof value === "function") && value !== null && typeof (value as PromiseLike<unknown>).then === "function";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function createAsyncMatcher<T>(awaited: unknown, mode: "resolves" | "rejects", negated: boolean): AsyncMatcher<T> {
|
|
34
|
+
async function invoke(method: SyncMethodName, args: unknown[]): Promise<void> {
|
|
35
|
+
if (!isPromiseLike(awaited)) {
|
|
36
|
+
throw new Error(`Expected a Promise for .${mode} but received ${describeCaught(awaited)}`);
|
|
37
|
+
}
|
|
38
|
+
const result = await settle(awaited);
|
|
39
|
+
|
|
40
|
+
if (mode === "resolves") {
|
|
41
|
+
if (!result.ok) {
|
|
42
|
+
throw new Error(`Expected promise to resolve but it rejected with ${describeCaught(result.value)}`);
|
|
43
|
+
}
|
|
44
|
+
const sync = createMatcher(result.value, negated) as unknown as Record<SyncMethodName, (...a: unknown[]) => void>;
|
|
45
|
+
sync[method](...args);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// mode === "rejects"
|
|
50
|
+
if (result.ok) {
|
|
51
|
+
throw new Error(`Expected promise to reject but it resolved with ${JSON.stringify(result.value)}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (method === "toThrow") {
|
|
55
|
+
const [errorMatch] = args as [ErrorMatch | undefined];
|
|
56
|
+
const matches = errorMatch === undefined || matchesThrown(result.value, errorMatch);
|
|
57
|
+
const passed = negated ? !matches : matches;
|
|
58
|
+
if (!passed) {
|
|
59
|
+
const phrase = negated ? "not to" : "to";
|
|
60
|
+
throw new Error(
|
|
61
|
+
errorMatch === undefined
|
|
62
|
+
? `Expected promise ${phrase} reject`
|
|
63
|
+
: `Expected promise ${phrase} reject with a matching error but got ${describeCaught(result.value)}`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const sync = createMatcher(result.value, negated) as unknown as Record<SyncMethodName, (...a: unknown[]) => void>;
|
|
70
|
+
sync[method](...args);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return new Proxy({} as AsyncMatcher<T>, {
|
|
74
|
+
get(_target, prop: string | symbol): unknown {
|
|
75
|
+
if (prop === "not") return createAsyncMatcher(awaited, mode, !negated);
|
|
76
|
+
return (...args: unknown[]): Promise<void> => invoke(prop as SyncMethodName, args);
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function createResolvesMatcher<T>(awaited: unknown, negated = false): AsyncMatcher<T> {
|
|
82
|
+
return createAsyncMatcher<T>(awaited, "resolves", negated);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function createRejectsMatcher<T>(awaited: unknown, negated = false): AsyncMatcher<T> {
|
|
86
|
+
return createAsyncMatcher<T>(awaited, "rejects", negated);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function createExpectMatcher<T>(actual: T, negated = false): Matchers<T> {
|
|
90
|
+
return {
|
|
91
|
+
...createMatcher(actual, negated),
|
|
92
|
+
get not(): Matchers<T> {
|
|
93
|
+
return createExpectMatcher(actual, !negated);
|
|
94
|
+
},
|
|
95
|
+
get resolves(): AsyncMatcher<Awaited<T>> {
|
|
96
|
+
return createResolvesMatcher(actual, negated);
|
|
97
|
+
},
|
|
98
|
+
get rejects(): AsyncMatcher<unknown> {
|
|
99
|
+
return createRejectsMatcher(actual, negated);
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type AgentMessage,
|
|
3
|
+
type RunSummary,
|
|
4
|
+
type TestError,
|
|
5
|
+
type TestResult,
|
|
6
|
+
type TestStatus,
|
|
7
|
+
type TestSuiteResult,
|
|
8
|
+
} from "frida-test/protocol.js";
|
|
2
9
|
|
|
3
10
|
export type TestFn = () => void | Promise<void>;
|
|
4
11
|
export type HookFn = () => void | Promise<void>;
|
|
@@ -152,7 +159,10 @@ async function runTestSuiteNode(node: TestSuiteNode, verbose: boolean, parentHoo
|
|
|
152
159
|
durationMs: 0,
|
|
153
160
|
error: { message: `parent suite "${node.name}" failed before this test could run` },
|
|
154
161
|
}));
|
|
155
|
-
|
|
162
|
+
// Count each reported child once; fall back to counting the suite itself
|
|
163
|
+
// only when it has none, so an empty-but-broken suite isn't invisible.
|
|
164
|
+
const failedCount = Math.max(1, children.length);
|
|
165
|
+
const counts: Counts = { total: failedCount, passed: 0, failed: failedCount };
|
|
156
166
|
const result: TestResult = {
|
|
157
167
|
name: node.name,
|
|
158
168
|
status: "failed",
|
|
@@ -177,7 +187,8 @@ async function runTestSuiteNode(node: TestSuiteNode, verbose: boolean, parentHoo
|
|
|
177
187
|
error: { message: `parent suite "${node.name}" failed before this test could run` },
|
|
178
188
|
}));
|
|
179
189
|
await runTeardownHooks(hooks.afterAll, verbose);
|
|
180
|
-
const
|
|
190
|
+
const failedCount = Math.max(1, children.length);
|
|
191
|
+
const counts: Counts = { total: failedCount, passed: 0, failed: failedCount };
|
|
181
192
|
const result: TestResult = {
|
|
182
193
|
name: node.name,
|
|
183
194
|
status: "failed",
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { createMockFunction, type AnyFn, type Mock } from "./mock.js";
|
|
2
|
+
|
|
3
|
+
const assert = (condition: boolean, message: string): void => {
|
|
4
|
+
if (!condition) throw new Error(message);
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export function spyOn<T extends object, K extends keyof T>(target: T, key: K): Mock {
|
|
8
|
+
const original = target[key];
|
|
9
|
+
assert(typeof original === "function", `${String(key)} is not a function`);
|
|
10
|
+
|
|
11
|
+
// Capture the descriptor so restore can put things back exactly as they were, including
|
|
12
|
+
// whether the method was an own property or inherited from the prototype chain.
|
|
13
|
+
const descriptor = Object.getOwnPropertyDescriptor(target, key);
|
|
14
|
+
const mock = createMockFunction(original as unknown as AnyFn, String(key));
|
|
15
|
+
let restored = false;
|
|
16
|
+
|
|
17
|
+
mock.mockRestore = (): void => {
|
|
18
|
+
if (restored) return;
|
|
19
|
+
restored = true;
|
|
20
|
+
mock.mockReset();
|
|
21
|
+
if (descriptor) {
|
|
22
|
+
Object.defineProperty(target, key, descriptor);
|
|
23
|
+
} else {
|
|
24
|
+
Reflect.deleteProperty(target, key);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
target[key] = mock as unknown as T[K];
|
|
29
|
+
return mock;
|
|
30
|
+
}
|
package/dist/bundler.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"bundler.d.ts","sourceRoot":"","sources":["../src/bundler.ts"],"names":[],"mappings":"AAwFA,wBAAsB,WAAW,CAAC,cAAc,EAAE,MAAM,EAAE,EAAE,IAAI,GAAE,OAAe,GAAG,OAAO,CAAC,MAAM,CAAC,CA4DlG"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"cli-frida-test-compiler.d.ts","sourceRoot":"","sources":["../src/cli-frida-test-compiler.ts"],"names":[],"mappings":""}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"cli-frida-test.d.ts","sourceRoot":"","sources":["../src/cli-frida-test.ts"],"names":[],"mappings":""}
|
package/dist/collector.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"collector.d.ts","sourceRoot":"","sources":["../src/collector.ts"],"names":[],"mappings":"AA4BA,wBAAsB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAIjF"}
|