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.
Files changed (66) hide show
  1. package/README.md +70 -29
  2. package/bin/frida-test-compile.js +1 -1
  3. package/bin/frida-test.js +1 -1
  4. package/dist/host/bundler.d.ts.map +1 -0
  5. package/dist/{bundler.js → host/bundler.js} +12 -14
  6. package/dist/host/cli-frida-test-compiler.d.ts.map +1 -0
  7. package/dist/{cli-frida-test-compiler.js → host/cli-frida-test-compiler.js} +2 -2
  8. package/dist/host/cli-frida-test.d.ts.map +1 -0
  9. package/dist/{cli-frida-test.js → host/cli-frida-test.js} +10 -4
  10. package/dist/host/collector.d.ts.map +1 -0
  11. package/dist/host/collector.js +40 -0
  12. package/dist/host/device.d.ts +12 -0
  13. package/dist/host/device.d.ts.map +1 -0
  14. package/dist/host/device.js +19 -0
  15. package/dist/{logger.d.ts → host/logger.d.ts} +0 -2
  16. package/dist/host/logger.d.ts.map +1 -0
  17. package/dist/{logger.js → host/logger.js} +1 -3
  18. package/dist/host/protocol.d.ts.map +1 -0
  19. package/dist/host/protocol.js +12 -0
  20. package/dist/host/reporter/console.d.ts +4 -0
  21. package/dist/host/reporter/console.d.ts.map +1 -0
  22. package/dist/{reporter → host/reporter}/console.js +5 -9
  23. package/dist/host/reporter/json.d.ts.map +1 -0
  24. package/dist/host/reporter/summary.d.ts.map +1 -0
  25. package/dist/{reporter → host/reporter}/summary.js +6 -5
  26. package/dist/host/runner.d.ts.map +1 -0
  27. package/dist/{runner.js → host/runner.js} +20 -10
  28. package/dist/host/target.d.ts.map +1 -0
  29. package/dist/{target.js → host/target.js} +11 -6
  30. package/package.json +14 -8
  31. package/src/agent-runtime/expect.ts +5 -0
  32. package/src/agent-runtime/fridaTest.ts +9 -0
  33. package/src/agent-runtime/globals.d.ts +8 -4
  34. package/src/agent-runtime/globals.ts +10 -4
  35. package/src/agent-runtime/matchers.ts +102 -119
  36. package/src/agent-runtime/mock.ts +131 -0
  37. package/src/agent-runtime/modifiers.ts +102 -0
  38. package/src/agent-runtime/registry.ts +14 -3
  39. package/src/agent-runtime/spy.ts +30 -0
  40. package/dist/bundler.d.ts.map +0 -1
  41. package/dist/cli-frida-test-compiler.d.ts.map +0 -1
  42. package/dist/cli-frida-test.d.ts.map +0 -1
  43. package/dist/collector.d.ts.map +0 -1
  44. package/dist/collector.js +0 -27
  45. package/dist/device.d.ts +0 -28
  46. package/dist/device.d.ts.map +0 -1
  47. package/dist/device.js +0 -68
  48. package/dist/logger.d.ts.map +0 -1
  49. package/dist/protocol.d.ts.map +0 -1
  50. package/dist/protocol.js +0 -6
  51. package/dist/reporter/console.d.ts +0 -3
  52. package/dist/reporter/console.d.ts.map +0 -1
  53. package/dist/reporter/json.d.ts.map +0 -1
  54. package/dist/reporter/summary.d.ts.map +0 -1
  55. package/dist/runner.d.ts.map +0 -1
  56. package/dist/target.d.ts.map +0 -1
  57. /package/dist/{bundler.d.ts → host/bundler.d.ts} +0 -0
  58. /package/dist/{cli-frida-test-compiler.d.ts → host/cli-frida-test-compiler.d.ts} +0 -0
  59. /package/dist/{cli-frida-test.d.ts → host/cli-frida-test.d.ts} +0 -0
  60. /package/dist/{collector.d.ts → host/collector.d.ts} +0 -0
  61. /package/dist/{protocol.d.ts → host/protocol.d.ts} +0 -0
  62. /package/dist/{reporter → host/reporter}/json.d.ts +0 -0
  63. /package/dist/{reporter → host/reporter}/json.js +0 -0
  64. /package/dist/{reporter → host/reporter}/summary.d.ts +0 -0
  65. /package/dist/{runner.d.ts → host/runner.d.ts} +0 -0
  66. /package/dist/{target.d.ts → host/target.d.ts} +0 -0
@@ -0,0 +1,5 @@
1
+ import { createExpectMatcher, type Matchers } from "./modifiers.js";
2
+
3
+ export function expect<T>(actual: T): Matchers<T> {
4
+ return createExpectMatcher(actual);
5
+ }
@@ -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 Matcher<T> = import("./matchers.js").Matcher<T>;
2
- type Spy = import("./matchers.js").Spy;
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): Matcher<T>;
14
- declare function spyOn<T extends object, K extends keyof T>(target: T, key: K): Spy;
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, spyOn, type Matcher, type Spy } from "./matchers.js";
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): Matcher<T>;
14
- function spyOn<T extends object, K extends keyof T>(target: T, key: K): Spy;
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
- export type Containable<T> = T extends readonly (infer U)[] ? U : T extends string ? string : never;
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 interface Matcher<T> {
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
- toThrow(errorMatch?: string | Error): void;
16
- toResolve(valueMatch?: unknown): Promise<void>;
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
- readonly not: Matcher<T>;
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
- const bs = [...(b as Set<unknown>)];
59
- if (a.size !== bs.length) return false;
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
- if (!bs.some((other) => deepEqual(val, other, nextSeen))) return false;
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 createMatcher<T>(actual: T, negated = false): Matcher<T> {
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" ? actual.includes(expected as string) : (actual as readonly unknown[]).some((item) => deepEqual(item, expected));
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
- throw new Error("toThrow() received a function returning a Promise; use await expect(fn).toReject(...) instead");
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
- check(threw, `Expected function ${phrase} throw`);
147
- if (negated || errorMatch === undefined) return;
148
- checkThrown(caught, errorMatch);
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
- check(resolved, `Expected promise ${phrase} resolve`);
162
- if (negated || expected === undefined) return;
163
- assert(deepEqual(caught, expected), `Expected ${JSON.stringify(caught)} to equal ${JSON.stringify(expected)}`);
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
- toReject: async (errorMatch) => {
167
- assert(typeof actual === "function", "Expected a function returning a promise");
168
- let caught: unknown;
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
- toHaveBeenCalled: () => {
182
- assert(isSpy(actual), "Expected a spy created with spyOn()");
183
- check((actual as Spy).calls.length > 0, `Expected spy ${phrase} have been called`);
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(isSpy(actual), "Expected a spy created with spyOn()");
188
- const calls = (actual as Spy).calls;
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 spy ${phrase} have been called with ${JSON.stringify(expected)} but got ${JSON.stringify(calls)}`);
200
+ check(match, `Expected mock ${phrase} have been called with ${JSON.stringify(expected)} but got ${JSON.stringify(calls)}`);
191
201
  },
192
202
 
193
- get not(): Matcher<T> {
194
- return createMatcher(actual, !negated);
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
- export function expect<T>(actual: T): Matcher<T> {
200
- return createMatcher(actual);
201
- }
202
-
203
- export function spyOn<T extends object, K extends keyof T>(target: T, key: K): Spy {
204
- const original = target[key];
205
- assert(typeof original === "function", `${String(key)} is not a function`);
206
-
207
- const descriptor = Object.getOwnPropertyDescriptor(target, key); // fix: capture descriptor for faithful restore
208
- const calls: unknown[][] = [];
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 { type AgentMessage, type RunSummary, type TestError, type TestResult, type TestStatus, type TestSuiteResult } from "frida-test/protocol.js";
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
- const counts: Counts = { total: 1 + children.length, passed: 0, failed: 1 + children.length };
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 counts: Counts = { total: 1 + children.length, passed: 0, failed: 1 + children.length };
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
+ }
@@ -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":""}
@@ -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"}