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
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # frida-test Documentation
2
2
 
3
+ [![Test frida-test](https://github.com/bernhste/frida-test/actions/workflows/test.yml/badge.svg)](https://github.com/bernhste/frida-test/actions/workflows/test.yml)
4
+
3
5
  This is a small test framework which runs on the target. It is used to unit test Frida code running on actual devices. It was originally developed to test the Frida agent code used in [frooky](https://github.com/cpholguera/frooky).
4
6
 
5
7
  The following chapters explain how to write and run tests.
@@ -10,7 +12,7 @@ The following chapters explain how to write and run tests.
10
12
  npm install --save-dev frida-test
11
13
  ```
12
14
 
13
- After the installation, import the type `frida-test` into you project by adding the following configuration to the `tsconfig.json`:
15
+ After the installation, import the type `frida-test` into your project by adding the following configuration to the `tsconfig.json`:
14
16
 
15
17
  ```json
16
18
  {
@@ -42,28 +44,9 @@ describe('Classloader', () => {
42
44
 
43
45
  Tests can be nested to any depth and can be synchronous or asynchronous.
44
46
 
45
- ### Test discovery
46
-
47
- The framework collects every file matching `*.test.ts` in the directories passed on the command line, recursively.
48
-
49
- A good practice is to create test files next to the source code file as shown here:
50
-
51
- ```text
52
- myProject/
53
- ├── android/
54
- │ ├── classLoader.ts
55
- │ └── classLoader.test.ts
56
- ├── ios/
57
- │ ├── objcRuntime.ts
58
- │ └── objcRuntime.test.ts
59
- └── shared/
60
- ├── helper.ts
61
- └── helper.test.ts
62
- ```
63
-
64
47
  ## Matchers
65
48
 
66
- `expect(actualValue)` returns a `Matcher` which we can use to test for the expected value. Use the following functions to do that:
49
+ `expect(actualValue)` returns a `Matchers` object which we can use to test for the expected value. Use the following functions to do that:
67
50
 
68
51
  | Matcher | Description |
69
52
  | --- | --- |
@@ -76,15 +59,73 @@ myProject/
76
59
  | `.toBeUndefined()` | Value is `undefined` |
77
60
  | `.toBeGreaterThan(value)` | Numeric value is greater than `value` |
78
61
  | `.toBeLessThan(value)` | Numeric value is less than `value` |
79
- | `.toContain(value)` | Value (array, string, etc.) contains `value` |
80
- | `.toThrow(errorMatch)` | Exception thrown; optionally matches a string message or `Error` instance |
81
- | `.toReject(errorMatch)` | Returned promise rejects; optionally matches a string message or `Error` instance (must be `await`ed) |
82
- | `.toHaveBeenCalled()` | Spy target function was called at least once |
83
- | `.toHaveBeenCalledWith(...expected)` | Spy target function was called with expected arguments |
84
- | `.not.<matcher>` | Inverts the assertion result |
62
+ | `.toContain(value)` | Array, `Set`, or string contains `value` (array/`Set` items compared with `===`) |
63
+ | `.toContainEqual(value)` | Array or `Set` contains an item deeply equal to `value` |
64
+ | `.toThrow(errorMatch?)` | Function throws; `errorMatch` can be a substring, a `RegExp` matched against the message, an `Error` instance (message equality only), or an `Error` class (`instanceof` check) |
65
+ | `.toHaveBeenCalled()` | Mock/spy was called at least once |
66
+ | `.toHaveBeenCalledTimes(count)` | Mock/spy was called exactly `count` times |
67
+ | `.toHaveBeenCalledWith(...expected)` | Mock/spy was called (at any point) with the expected arguments |
68
+ | `.toHaveBeenLastCalledWith(...expected)` | Mock/spy's most recent call had the expected arguments |
69
+ | `.toHaveBeenNthCalledWith(n, ...expected)` | Mock/spy's `n`th call (1-indexed) had the expected arguments |
85
70
 
86
71
  > [!NOTE]
87
- > `frida-test` test itself. So for examples for all Matches and more, have a look a the `*.test.ts` located in the [test folder](./tests/)
72
+ > `frida-test` tests itself. So for examples of all matchers and more, have a look at the `*.test.ts` files located in the [test folder](./tests/).
73
+
74
+ ### Modifiers
75
+
76
+ | Modifier | Description |
77
+ | --- | --- |
78
+ | `.not` | Inverts the assertion result, e.g. `expect(2 + 2).not.toBe(5)` |
79
+ | `.resolves` | Unwraps a resolved promise so a matcher applies to its value; the assertion must be `await`ed. The received value must actually be a `Promise` - it fails otherwise |
80
+ | `.rejects` | Unwraps a rejected promise so a matcher applies to its reason; the assertion must be `await`ed. The received value must actually be a `Promise` - it fails otherwise |
81
+
82
+ ```typescript
83
+ await expect(fetchUser(1)).resolves.toEqual({ id: 1, name: 'Ada' });
84
+ await expect(fetchUser(-1)).rejects.toThrow('not found');
85
+
86
+ // modifiers compose:
87
+ await expect(fetchUser(1)).resolves.not.toBeNull();
88
+ ```
89
+
90
+ ## Mocking
91
+
92
+ `frida-test` mocks and spies follow the same API shape as Jest's mock functions.
93
+
94
+ - `fn(implementation?)`: creates a standalone mock function, optionally backed by `implementation`.
95
+ - `spyOn(object, methodName)`: replaces `object[methodName]` with a mock that calls through to the original method by default, and can be restored later.
96
+
97
+ Both forms return the same `Mock` type:
98
+
99
+ ```typescript
100
+ const mock = fridaTest.fn((a: number, b: number) => a + b);
101
+ mock(1, 2);
102
+
103
+ expect(mock).toHaveBeenCalledWith(1, 2);
104
+ mock.mock.calls; // [[1, 2]]
105
+ mock.mock.results; // [{ type: "return", value: 3 }]
106
+
107
+ mock.mockReturnValue(42); // set a default return value
108
+ mock.mockReturnValueOnce(99); // ...for just the next call
109
+ mock.mockResolvedValue(value); // wraps value in Promise.resolve()
110
+ mock.mockRejectedValue(error); // wraps error in Promise.reject()
111
+ mock.mockImplementation(impl); // replace the implementation
112
+ mock.mockImplementationOnce(impl); // ...for just the next call
113
+
114
+ mock.mockClear(); // reset calls/results, keep the implementation
115
+ mock.mockReset(); // reset calls/results and drop the implementation
116
+ mock.mockRestore(); // reset like mockReset(); for spyOn(), also restores the original method
117
+ ```
118
+
119
+ ```typescript
120
+ describe('Logger', () => {
121
+ it('should call the underlying console method', () => {
122
+ const spy = fridaTest.spyOn(console, 'log').mockImplementation(() => undefined);
123
+ logMessage('hello');
124
+ expect(spy).toHaveBeenCalledWith('hello');
125
+ spy.mockRestore();
126
+ });
127
+ });
128
+ ```
88
129
 
89
130
  ### Setup and Teardown
90
131
 
@@ -169,7 +210,7 @@ frida-test -U -N org.owasp.mastestapp.MASTestApp-iOS ./tests/ios ./tests/shared
169
210
  frida-test -H 192.168.1.10:27042 --token secret -p 4926 ./tests/shared
170
211
  ```
171
212
 
172
- ## Compile Agent
213
+ ## Compiling the Agent
173
214
 
174
215
  `frida-test` automatically bundles the test suites and compiles them together with the testing framework into a Frida agent.
175
216
 
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import("../dist/cli-frida-test-compiler.js").catch((err) => {
3
+ import("../dist/host/cli-frida-test-compiler.js").catch((err) => {
4
4
  console.error("Failed to load the CLI module. Make sure the project is built.");
5
5
  console.error(err);
6
6
  process.exit(1);
package/bin/frida-test.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import("../dist/cli-frida-test.js").catch((err) => {
3
+ import("../dist/host/cli-frida-test.js").catch((err) => {
4
4
  console.error("Failed to load the CLI module. Make sure the project is built.");
5
5
  console.error(err);
6
6
  process.exit(1);
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bundler.d.ts","sourceRoot":"","sources":["../../src/host/bundler.ts"],"names":[],"mappings":"AAyFA,wBAAsB,WAAW,CAAC,cAAc,EAAE,MAAM,EAAE,EAAE,IAAI,GAAE,OAAe,GAAG,OAAO,CAAC,MAAM,CAAC,CA2DlG"}
@@ -8,29 +8,28 @@ const IMPORT_MARKER = "/// IMPORT TESTS SUITES ///";
8
8
  const AGENT_ENTRYPOINT_FILENAME = "agentRuntime.ts";
9
9
  const AGENT_ENTRYPOINT_BASENAME = path.parse(AGENT_ENTRYPOINT_FILENAME).name;
10
10
  const AGENT_BUNDLE_FILENAME = `${AGENT_ENTRYPOINT_BASENAME}.bundle.js`;
11
- function getProjectRoot() {
12
- const startDir = process.cwd();
11
+ function findPackageRoot(startDir) {
13
12
  let dir = startDir;
14
13
  while (true) {
15
14
  if (existsSync(path.join(dir, "package.json")))
16
15
  return dir;
17
16
  const parent = path.dirname(dir);
18
17
  if (parent === dir)
19
- return startDir;
18
+ return undefined;
20
19
  dir = parent;
21
20
  }
22
21
  }
22
+ function getProjectRoot() {
23
+ const startDir = process.cwd();
24
+ return findPackageRoot(startDir) ?? startDir;
25
+ }
23
26
  function getPackageRoot() {
24
- let dir = path.dirname(fileURLToPath(import.meta.url));
25
- while (true) {
26
- if (existsSync(path.join(dir, "package.json")))
27
- return dir;
28
- const parent = path.dirname(dir);
29
- if (parent === dir) {
30
- throw new Error("Could not locate frida-test's own package.json.");
31
- }
32
- dir = parent;
27
+ const startDir = path.dirname(fileURLToPath(import.meta.url));
28
+ const root = findPackageRoot(startDir);
29
+ if (!root) {
30
+ throw new Error("Could not locate frida-test's own package.json.");
33
31
  }
32
+ return root;
34
33
  }
35
34
  function getAgentRuntimeSrcDir() {
36
35
  const dir = path.join(getPackageRoot(), "src", "agent-runtime");
@@ -99,7 +98,6 @@ export async function bundleAgent(testSuitePaths, keep = false) {
99
98
  return `import ${JSON.stringify(specifier)};`;
100
99
  })
101
100
  .join("\n");
102
- await rm(entrypointPath, { force: true });
103
101
  await writeFile(entrypointPath, agentSource.replace(IMPORT_MARKER, importStatements), "utf8");
104
102
  await prepareTypeCheckConfig(projectRoot, workDir);
105
103
  const outfilePath = path.join(workDir, AGENT_BUNDLE_FILENAME);
@@ -115,7 +113,7 @@ export async function bundleAgent(testSuitePaths, keep = false) {
115
113
  catch (err) {
116
114
  throw new Error(`frida-compile failed for entrypoint "${entrypointPath}" with suites [${testSuitePaths.join(", ")}] (see compiler output above for details)`, { cause: err });
117
115
  }
118
- logger.info(`Agent bundle sucessfully created and saved at ${outfilePath}.`);
116
+ logger.info(`Agent bundle successfully created and saved at ${outfilePath}.`);
119
117
  return await readFile(outfilePath, "utf8");
120
118
  }
121
119
  finally {
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-frida-test-compiler.d.ts","sourceRoot":"","sources":["../../src/host/cli-frida-test-compiler.ts"],"names":[],"mappings":""}
@@ -5,10 +5,10 @@ import { bundleAgent } from "./bundler.js";
5
5
  import { collectTestSuitePaths } from "./collector.js";
6
6
  import { logger } from "./logger.js";
7
7
  const usage = `
8
- Usage: frida-test-compiler [options] <src_path>...
8
+ Usage: frida-test-compile [options] <src_path>...
9
9
 
10
10
  Options:
11
- -o, --out <path> Path of the output file for JSON reporter (default: disabled)
11
+ -o, --out <path> Path of the output file for the compiled agent bundle (default: stdout)
12
12
  -h, --help Show this help message
13
13
  `;
14
14
  class CliError extends Error {
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-frida-test.d.ts","sourceRoot":"","sources":["../../src/host/cli-frida-test.ts"],"names":[],"mappings":""}
@@ -3,11 +3,12 @@ import { setTimeout as sleep } from "node:timers/promises";
3
3
  import { parseArgs } from "node:util";
4
4
  import { bundleAgent } from "./bundler.js";
5
5
  import { collectTestSuitePaths } from "./collector.js";
6
- import { resolveDevice, resolveTarget } from "./device.js";
6
+ import { resolveDevice } from "./device.js";
7
7
  import { logger } from "./logger.js";
8
8
  import { writeRunSummaryJson } from "./reporter/json.js";
9
9
  import { printSummary } from "./reporter/summary.js";
10
10
  import { TestRunner } from "./runner.js";
11
+ import { resolveTarget } from "./target.js";
11
12
  const usage = `
12
13
  Usage: frida-test [options] <src_path>...
13
14
 
@@ -28,7 +29,7 @@ const usage = `
28
29
  -o, --out <path> Path of the output file for JSON reporter (default: disabled)
29
30
  -t, --timeout <s> Abort the run after this many seconds (default: 600, 0 disables)
30
31
  -d, --delay <s> Start running the test suites after this many seconds (default: 0)
31
- -k, --keep Keep the generated agent in .frida-test/agent.js
32
+ -k, --keep Keep the generated agent bundle in .frida-test-cache/
32
33
  -v, --verbose Enable verbose logging
33
34
  -h, --help Show this help message
34
35
 
@@ -106,7 +107,7 @@ async function main() {
106
107
  else if (values.usb) {
107
108
  deviceSelector = "usb";
108
109
  }
109
- else if (values.host !== undefined || values.remote || hasRemoteParams) {
110
+ else if (values.host !== undefined || hasRemoteParams) {
110
111
  const keepaliveInterval = values["keepalive-interval"] !== undefined ? parseKeepaliveInterval(values["keepalive-interval"]) : undefined;
111
112
  deviceSelector = {
112
113
  host: values.host ?? "127.0.0.1:27042",
@@ -116,6 +117,9 @@ async function main() {
116
117
  ...(keepaliveInterval !== undefined && { keepaliveInterval }),
117
118
  };
118
119
  }
120
+ else if (values.remote) {
121
+ deviceSelector = "remote";
122
+ }
119
123
  else {
120
124
  deviceSelector = "local";
121
125
  }
@@ -179,7 +183,9 @@ async function main() {
179
183
  });
180
184
  let runSummary;
181
185
  try {
182
- runSummary = await Promise.race([runner.runTests(), timeoutPromise]);
186
+ const testsPromise = runner.runTests();
187
+ testsPromise.catch(() => { });
188
+ runSummary = await Promise.race([testsPromise, timeoutPromise]);
183
189
  }
184
190
  finally {
185
191
  if (timerId)
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collector.d.ts","sourceRoot":"","sources":["../../src/host/collector.ts"],"names":[],"mappings":"AA8CA,wBAAsB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAIjF"}
@@ -0,0 +1,40 @@
1
+ import { readdir, stat } from "node:fs/promises";
2
+ import { join, resolve } from "node:path";
3
+ const TEST_FILE_SUFFIX = ".test.ts";
4
+ const EXCLUDED_DIR_NAMES = new Set(["node_modules", ".git"]);
5
+ async function walkDirectory(dirPath) {
6
+ const entries = await readdir(dirPath, { withFileTypes: true });
7
+ const found = await Promise.all(entries.map((entry) => {
8
+ if (entry.isDirectory()) {
9
+ return EXCLUDED_DIR_NAMES.has(entry.name) ? [] : walkDirectory(join(dirPath, entry.name));
10
+ }
11
+ if (entry.isFile() && entry.name.endsWith(TEST_FILE_SUFFIX)) {
12
+ return [join(dirPath, entry.name)];
13
+ }
14
+ return [];
15
+ }));
16
+ return found.flat();
17
+ }
18
+ async function findTestFiles(absolutePath) {
19
+ let fileInfo;
20
+ try {
21
+ fileInfo = await stat(absolutePath);
22
+ }
23
+ catch (err) {
24
+ if (err.code === "ENOENT") {
25
+ throw new Error(`Path does not exist: ${absolutePath}`, { cause: err });
26
+ }
27
+ throw new Error(`Unable to access path "${absolutePath}": ${err.message}`, { cause: err });
28
+ }
29
+ if (fileInfo.isDirectory()) {
30
+ return walkDirectory(absolutePath);
31
+ }
32
+ if (absolutePath.endsWith(TEST_FILE_SUFFIX)) {
33
+ return [absolutePath];
34
+ }
35
+ throw new Error(`Not a *${TEST_FILE_SUFFIX} file: ${absolutePath}`);
36
+ }
37
+ export async function collectTestSuitePaths(srcPaths) {
38
+ const results = await Promise.all(srcPaths.map((p) => findTestFiles(resolve(p))));
39
+ return [...new Set(results.flat())].sort();
40
+ }
@@ -0,0 +1,12 @@
1
+ import type { Device } from "frida";
2
+ export type DeviceSelector = "usb" | "local" | "remote" | {
3
+ id: string;
4
+ } | {
5
+ host: string;
6
+ certificate?: string;
7
+ origin?: string;
8
+ token?: string;
9
+ keepaliveInterval?: number;
10
+ };
11
+ export declare function resolveDevice(selector?: DeviceSelector): Promise<Device>;
12
+ //# sourceMappingURL=device.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"device.d.ts","sourceRoot":"","sources":["../../src/host/device.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC;AAGpC,MAAM,MAAM,cAAc,GACtB,KAAK,GACL,OAAO,GACP,QAAQ,GACR;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,GACd;IACE,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEN,wBAAsB,aAAa,CAAC,QAAQ,GAAE,cAAwB,GAAG,OAAO,CAAC,MAAM,CAAC,CAcvF"}
@@ -0,0 +1,19 @@
1
+ import frida from "frida";
2
+ export async function resolveDevice(selector = "local") {
3
+ if (selector === "usb")
4
+ return frida.getUsbDevice();
5
+ if (selector === "local")
6
+ return frida.getLocalDevice();
7
+ if (selector === "remote")
8
+ return frida.getRemoteDevice();
9
+ if ("id" in selector)
10
+ return frida.getDevice(selector.id);
11
+ // Pass network parameters to addRemoteDevice
12
+ const manager = frida.getDeviceManager();
13
+ return manager.addRemoteDevice(selector.host, {
14
+ certificate: selector.certificate,
15
+ origin: selector.origin,
16
+ token: selector.token,
17
+ keepaliveInterval: selector.keepaliveInterval,
18
+ });
19
+ }
@@ -1,9 +1,7 @@
1
1
  export declare const logger: {
2
2
  setVerbose: (value: boolean) => void;
3
3
  info: (msg: string) => void;
4
- log: (msg: string) => void;
5
4
  warn: (msg: string) => void;
6
5
  error: (msg: string) => void;
7
- success: (msg: string) => void;
8
6
  };
9
7
  //# sourceMappingURL=logger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/host/logger.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,MAAM;IACjB,UAAU,UAAU,OAAO;IAG3B,IAAI,QAAQ,MAAM;IAGlB,IAAI,QAAQ,MAAM;IAClB,KAAK,QAAQ,MAAM;CACpB,CAAC"}
@@ -8,8 +8,6 @@ export const logger = {
8
8
  if (verbose)
9
9
  console.log(`${chalk.blue("[i]")} ${msg}`);
10
10
  },
11
- log: (msg) => console.log(`${chalk.blue("[i]")} ${msg}`),
12
- warn: (msg) => console.log(`${chalk.yellow("[!]")} ${msg}`),
11
+ warn: (msg) => console.error(`${chalk.yellow("[!]")} ${msg}`),
13
12
  error: (msg) => console.error(`${chalk.red("[!]")} ${msg}`),
14
- success: (msg) => console.log(`${chalk.green("[✓]")} ${msg}`),
15
13
  };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../../src/host/protocol.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,CAAC;AAEzD,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,UAAU,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,QAAQ,CAAC,EAAE,UAAU,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,UAAU,CAAC;IACnB,UAAU,CAAC,EAAE,UAAU,CAAC;CACzB;AAED,MAAM,MAAM,YAAY,GACpB;IAAE,IAAI,EAAE,aAAa,CAAA;CAAE,GACvB;IAAE,IAAI,EAAE,oBAAoB,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC5C;IAAE,IAAI,EAAE,qBAAqB,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,eAAe,CAAA;CAAE,GACtE;IAAE,IAAI,EAAE,cAAc,CAAA;CAAE,CAAC;AAS7B,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,YAAY,CAIpE;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,iBAAiB,EAAE,eAAe,EAAE,CAAC;CACtC"}
@@ -0,0 +1,12 @@
1
+ const AGENT_MESSAGE_TYPES = {
2
+ "agent-ready": true,
3
+ "test-suite-started": true,
4
+ "test-suite-finished": true,
5
+ "run-finished": true,
6
+ };
7
+ export function isAgentMessage(value) {
8
+ if (typeof value !== "object" || value === null)
9
+ return false;
10
+ const { type } = value;
11
+ return typeof type === "string" && Object.hasOwn(AGENT_MESSAGE_TYPES, type);
12
+ }
@@ -0,0 +1,4 @@
1
+ import { type TestStatus, type TestSuiteResult } from "../protocol.js";
2
+ export declare const STATUS_SYMBOLS: Record<TestStatus, string>;
3
+ export declare function printTestSuiteResult(suite: TestSuiteResult): void;
4
+ //# sourceMappingURL=console.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"console.d.ts","sourceRoot":"","sources":["../../../src/host/reporter/console.ts"],"names":[],"mappings":"AACA,OAAO,EAAmB,KAAK,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAExF,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,UAAU,EAAE,MAAM,CAIrD,CAAC;AAuBF,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,CASjE"}
@@ -1,6 +1,6 @@
1
1
  import { logger } from "../logger.js";
2
2
  import {} from "../protocol.js";
3
- const STATUS_SYMBOLS = {
3
+ export const STATUS_SYMBOLS = {
4
4
  passed: "✅",
5
5
  failed: "❌",
6
6
  skipped: "➖",
@@ -23,16 +23,12 @@ function printTestResult(node, depth = 0) {
23
23
  }
24
24
  }
25
25
  export function printTestSuiteResult(suite) {
26
- if (suite.status == "failed") {
27
- logger.warn(`Test suite "${suite.name}" failed:`);
28
- }
29
- else if (suite.status == "passed") {
30
- logger.success(`Test suite "${suite.name}" passed:`);
31
- }
26
+ const symbol = STATUS_SYMBOLS[suite.status] || "?";
27
+ console.log(`${symbol} Test suite "${suite.name}" ${suite.status}:`);
32
28
  if (suite.testResult) {
33
29
  printTestResult(suite.testResult, 1);
34
30
  }
35
- else {
36
- logger.warn(`No results for test suite "${suite.name}".`);
31
+ else if (suite.status !== "skipped") {
32
+ logger.warn(`No results for test suite "${suite.name}".`);
37
33
  }
38
34
  }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"json.d.ts","sourceRoot":"","sources":["../../../src/host/reporter/json.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAEjD,wBAAsB,mBAAmB,CAAC,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAG5F"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"summary.d.ts","sourceRoot":"","sources":["../../../src/host/reporter/summary.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAejD,wBAAgB,YAAY,CAAC,UAAU,EAAE,UAAU,GAAG,IAAI,CA+BzD"}
@@ -1,4 +1,5 @@
1
1
  import chalk from "chalk";
2
+ import { STATUS_SYMBOLS } from "./console.js";
2
3
  function formatDuration(ms) {
3
4
  return ms < 1000 ? `${ms}ms` : `${(ms / 1000).toFixed(2)}s`;
4
5
  }
@@ -14,16 +15,16 @@ function formatCount(passed, failed, total) {
14
15
  export function printSummary(runSummary) {
15
16
  const { total, passed, failed, durationMs, testSuitesResults } = runSummary;
16
17
  const suitePassed = testSuitesResults.filter((s) => s.status === "passed").length;
17
- const suiteFailed = testSuitesResults.length - suitePassed;
18
+ const suiteFailed = testSuitesResults.filter((s) => s.status === "failed").length;
18
19
  console.log();
19
20
  console.log("------------------------------------------------------------------------------");
20
21
  console.log();
21
22
  console.log(chalk.bold("Test Suites"));
22
23
  for (const suite of testSuitesResults) {
23
- const isPassed = suite.status === "passed";
24
- console.log(` ${isPassed ? chalk.green("✓") : chalk.red("✗")} ${suite.name}`);
25
- if (!isPassed && suite.testResult?.name) {
26
- for (const line of suite.testResult.name.split("\n")) {
24
+ const symbol = STATUS_SYMBOLS[suite.status] || "?";
25
+ console.log(` ${symbol} ${suite.name}`);
26
+ if (suite.status === "failed" && suite.testResult?.error?.message) {
27
+ for (const line of suite.testResult.error.message.split("\n")) {
27
28
  console.log(chalk.red(` ${line}`));
28
29
  }
29
30
  }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../src/host/runner.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAA4B,MAAM,OAAO,CAAC;AAE9D,OAAO,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAGjE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,qBAAa,UAAU;IAQnB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,WAAW;IAC5B,OAAO,CAAC,QAAQ,CAAC,OAAO;IAV1B,OAAO,CAAC,OAAO,CAAC,CAAU;IAC1B,OAAO,CAAC,MAAM,CAAC,CAAS;IACxB,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAyB;IACjD,OAAO,CAAC,UAAU,CAAC,CAAQ;IAE3B,YACmB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,OAAO,EAC/B;IAEJ,IAAI,YAAY,IAAI,SAAS,eAAe,EAAE,CAE7C;IAEK,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAyBhC;IAEK,QAAQ,IAAI,OAAO,CAAC,UAAU,CAAC,CASpC;IAEK,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAkB7B;IAED,OAAO,CAAC,SAAS;CA6BlB"}
@@ -32,12 +32,12 @@ export class TestRunner {
32
32
  this.fatalError ??= new Error("Script was destroyed unexpectedly");
33
33
  });
34
34
  await script.load();
35
- this.session = session;
36
- this.script = script;
37
- this.initialized = true;
38
35
  if (this.target.wasSpawned) {
39
36
  await this.device.resume(this.target.pid);
40
37
  }
38
+ this.session = session;
39
+ this.script = script;
40
+ this.initialized = true;
41
41
  }
42
42
  catch (error) {
43
43
  await session.detach().catch(() => { });
@@ -55,15 +55,25 @@ export class TestRunner {
55
55
  return summary;
56
56
  }
57
57
  async dispose() {
58
- try {
59
- await this.script?.unload();
58
+ if (this.script) {
59
+ try {
60
+ await this.script.unload();
61
+ }
62
+ catch (error) {
63
+ logger.warn(`Failed to unload script: ${error.message}`);
64
+ }
60
65
  }
61
- finally {
62
- await this.session?.detach();
63
- this.script = undefined;
64
- this.session = undefined;
65
- this.initialized = false;
66
+ if (this.session) {
67
+ try {
68
+ await this.session.detach();
69
+ }
70
+ catch (error) {
71
+ logger.warn(`Failed to detach session: ${error.message}`);
72
+ }
66
73
  }
74
+ this.script = undefined;
75
+ this.session = undefined;
76
+ this.initialized = false;
67
77
  }
68
78
  onMessage(message, _data) {
69
79
  if (message.type === "error") {
@@ -0,0 +1 @@
1
+ {"version":3,"file":"target.d.ts","sourceRoot":"","sources":["../../src/host/target.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAW,MAAM,OAAO,CAAC;AAE7C,MAAM,WAAW,MAAM;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,OAAO,CAAC;CACrB;AACD,MAAM,MAAM,SAAS,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,SAAS,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC;AAE7H,wBAAsB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,CAgEzF"}
@@ -26,12 +26,17 @@ export async function resolveTarget(device, targetDef) {
26
26
  throw new Error("Target process name must not be empty");
27
27
  const processes = await device.enumerateProcesses();
28
28
  const needle = targetName.toLowerCase();
29
- const tiers = [
30
- processes.filter((p) => p.name === targetName),
31
- processes.filter((p) => p.name.toLowerCase() === needle),
32
- processes.filter((p) => p.name.toLowerCase().includes(needle)),
29
+ const tierFilters = [
30
+ (p) => p.name === targetName,
31
+ (p) => p.name.toLowerCase() === needle,
32
+ (p) => p.name.toLowerCase().includes(needle),
33
33
  ];
34
- const matches = tiers.find((tier) => tier.length > 0) ?? [];
34
+ let matches = [];
35
+ for (const matchesTier of tierFilters) {
36
+ matches = processes.filter(matchesTier);
37
+ if (matches.length > 0)
38
+ break;
39
+ }
35
40
  if (matches.length === 0) {
36
41
  throw new Error(`No running process matching '${targetName}' was found.`);
37
42
  }
@@ -47,7 +52,7 @@ export async function resolveTarget(device, targetDef) {
47
52
  throw new Error("Target identifier must not be empty");
48
53
  const apps = await device.enumerateApplications();
49
54
  const matchedApp = apps.find((a) => a.identifier === identifier);
50
- if (!matchedApp || !matchedApp.pid || matchedApp.pid === 0) {
55
+ if (!matchedApp || !matchedApp.pid) {
51
56
  throw new Error(`Application '${identifier}' is not currently running.`);
52
57
  }
53
58
  return { pid: matchedApp.pid, wasSpawned: false };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frida-test",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "license": "GPL-3.0-only",
6
6
  "homepage": "https://github.com/bernhste/frida-test/blob/main/README.md",
@@ -22,11 +22,11 @@
22
22
  "frida-test": "bin/frida-test.js",
23
23
  "frida-test-compile": "bin/frida-test-compile.js"
24
24
  },
25
- "types": "./dist/agent-runtime/globals.d.ts",
25
+ "types": "./src/agent-runtime/globals.d.ts",
26
26
  "typesVersions": {
27
27
  "*": {
28
28
  "protocol.js": [
29
- "./dist/protocol.d.ts"
29
+ "./dist/host/protocol.d.ts"
30
30
  ]
31
31
  }
32
32
  },
@@ -36,8 +36,8 @@
36
36
  "default": "./src/agent-runtime/globals.js"
37
37
  },
38
38
  "./protocol.js": {
39
- "types": "./dist/protocol.d.ts",
40
- "default": "./dist/protocol.js"
39
+ "types": "./dist/host/protocol.d.ts",
40
+ "default": "./dist/host/protocol.js"
41
41
  }
42
42
  },
43
43
  "files": [
@@ -49,16 +49,22 @@
49
49
  "build": "tsc -b",
50
50
  "watch": "tsc -b --watch",
51
51
  "clean": "tsc -b --clean && rimraf dist",
52
- "test:android": "npm run build && npx frida-test -U -f 'com.google.android.dialer' ./tests/",
53
- "prepublishOnly": "npm run clean && npm run build && npm run test:android"
52
+ "test:unit": "jest",
53
+ "test:integration:android": "npm run build && npx frida-test -U -f 'com.google.android.dialer' ./tests/integration/",
54
+ "prepublishOnly": "npm run clean && npm run build && npm run test:unit && npm run test:integration:android"
54
55
  },
55
56
  "dependencies": {
56
57
  "chalk": "^6.0.0",
57
58
  "frida-compile": "^19.0.5"
58
59
  },
59
60
  "devDependencies": {
61
+ "@jest/globals": "^30.5.2",
62
+ "@swc/core": "^1.16.2",
63
+ "@swc/jest": "^0.2.39",
60
64
  "@types/frida-gum": "^19.10.0",
61
- "@types/node": "^26.4.0",
65
+ "@types/jest": "^30.0.0",
66
+ "@types/node": "^26.6.2",
67
+ "jest": "^30.5.2",
62
68
  "rimraf": "^6.1.3",
63
69
  "typescript": "^7.0.2"
64
70
  },