frida-test 0.2.1 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -0
- package/dist/bundler.d.ts.map +1 -1
- package/dist/bundler.js +18 -4
- package/package.json +5 -5
- package/src/agent-runtime/globals.d.ts +5 -0
- package/src/agent-runtime/globals.ts +7 -3
- package/src/agent-runtime/registry.ts +160 -23
package/README.md
CHANGED
|
@@ -86,6 +86,41 @@ myProject/
|
|
|
86
86
|
> [!NOTE]
|
|
87
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/)
|
|
88
88
|
|
|
89
|
+
### Setup and Teardown
|
|
90
|
+
|
|
91
|
+
Use `beforeEach()` / `afterEach()` and `beforeAll()` / `afterAll()` to run code before or after tests. They can be declared at the top level of a file or inside a `describe()` block:
|
|
92
|
+
|
|
93
|
+
- `beforeEach()` / `afterEach()`: Run before/after every `it()` in the same and nested `describe()` blocks.
|
|
94
|
+
- `beforeAll()` / `afterAll()`: Run once before/after all tests in the same `describe()` block (or, at the top level, once before/after the whole file).
|
|
95
|
+
|
|
96
|
+
Hooks declared in an outer `describe()` also apply to tests in nested `describe()` blocks. `beforeEach` hooks run outer-to-inner; `afterEach` hooks run inner-to-outer.
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
describe('ClassLoader', () => {
|
|
100
|
+
let loader: ClassLoader;
|
|
101
|
+
|
|
102
|
+
beforeAll(() => {
|
|
103
|
+
loader = new ClassLoader();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
beforeEach(() => {
|
|
107
|
+
loader.reset();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
afterEach(() => {
|
|
111
|
+
loader.clearCache();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
afterAll(() => {
|
|
115
|
+
loader.destroy();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should load a known class', () => {
|
|
119
|
+
expect(loader.load('com.example.Foo')).toBeDefined();
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
89
124
|
## Running Tests
|
|
90
125
|
|
|
91
126
|
`frida-test` takes one or more directories, collects every `*.test.ts` file below them, compiles them together with the framework agent, and runs the resulting agent on the target.
|
package/dist/bundler.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bundler.d.ts","sourceRoot":"","sources":["../src/bundler.ts"],"names":[],"mappings":"
|
|
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"}
|
package/dist/bundler.js
CHANGED
|
@@ -63,6 +63,20 @@ async function deleteWorkDir(workDir) {
|
|
|
63
63
|
logger.warn(`Failed to remove temporary work dir "${workDir}": ${err.message}`);
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
|
+
async function prepareTypeCheckConfig(projectRoot, workDir) {
|
|
67
|
+
const realTsconfigPath = path.join(projectRoot, "tsconfig.json");
|
|
68
|
+
if (!existsSync(realTsconfigPath))
|
|
69
|
+
return;
|
|
70
|
+
const scratchConfig = {
|
|
71
|
+
extends: realTsconfigPath.replace(/\\/g, "/"),
|
|
72
|
+
compilerOptions: {
|
|
73
|
+
rootDir: projectRoot.replace(/\\/g, "/"),
|
|
74
|
+
},
|
|
75
|
+
include: [],
|
|
76
|
+
exclude: [],
|
|
77
|
+
};
|
|
78
|
+
await writeFile(path.join(workDir, "tsconfig.json"), JSON.stringify(scratchConfig, null, 2), "utf8");
|
|
79
|
+
}
|
|
66
80
|
export async function bundleAgent(testSuitePaths, keep = false) {
|
|
67
81
|
if (testSuitePaths.length === 0) {
|
|
68
82
|
throw new Error("bundleAgent requires at least one test suite path.");
|
|
@@ -87,19 +101,19 @@ export async function bundleAgent(testSuitePaths, keep = false) {
|
|
|
87
101
|
.join("\n");
|
|
88
102
|
await rm(entrypointPath, { force: true });
|
|
89
103
|
await writeFile(entrypointPath, agentSource.replace(IMPORT_MARKER, importStatements), "utf8");
|
|
104
|
+
await prepareTypeCheckConfig(projectRoot, workDir);
|
|
90
105
|
const outfilePath = path.join(workDir, AGENT_BUNDLE_FILENAME);
|
|
91
106
|
const fridaCompile = getFridaCompileBin(projectRoot);
|
|
92
107
|
const args = fridaCompile.useLocal ? [entrypointPath, "-o", outfilePath] : ["frida-compile", entrypointPath, "-o", outfilePath];
|
|
93
108
|
try {
|
|
94
109
|
execFileSync(fridaCompile.path, args, {
|
|
95
|
-
cwd:
|
|
110
|
+
cwd: workDir,
|
|
96
111
|
shell: process.platform === "win32",
|
|
97
|
-
|
|
112
|
+
stdio: "inherit",
|
|
98
113
|
});
|
|
99
114
|
}
|
|
100
115
|
catch (err) {
|
|
101
|
-
|
|
102
|
-
throw new Error(`frida-compile failed for entrypoint "${entrypointPath}" with suites [${testSuitePaths.join(", ")}]: ${stderr || err.message}`, { cause: err });
|
|
116
|
+
throw new Error(`frida-compile failed for entrypoint "${entrypointPath}" with suites [${testSuitePaths.join(", ")}] (see compiler output above for details)`, { cause: err });
|
|
103
117
|
}
|
|
104
118
|
logger.info(`Agent bundle sucessfully created and saved at ${outfilePath}.`);
|
|
105
119
|
return await readFile(outfilePath, "utf8");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "frida-test",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "GPL-3.0-only",
|
|
6
6
|
"homepage": "https://github.com/bernhste/frida-test/blob/main/README.md",
|
|
@@ -49,12 +49,9 @@
|
|
|
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 && frida-test -U -f 'com.google.android.dialer' ./tests/",
|
|
52
|
+
"test:android": "npm run build && npx frida-test -U -f 'com.google.android.dialer' ./tests/",
|
|
53
53
|
"prepublishOnly": "npm run clean && npm run build && npm run test:android"
|
|
54
54
|
},
|
|
55
|
-
"allowScripts": {
|
|
56
|
-
"frida": true
|
|
57
|
-
},
|
|
58
55
|
"dependencies": {
|
|
59
56
|
"chalk": "^6.0.0",
|
|
60
57
|
"frida-compile": "^19.0.5"
|
|
@@ -64,5 +61,8 @@
|
|
|
64
61
|
"@types/node": "^26.4.0",
|
|
65
62
|
"rimraf": "^6.1.3",
|
|
66
63
|
"typescript": "^7.0.2"
|
|
64
|
+
},
|
|
65
|
+
"allowScripts": {
|
|
66
|
+
"frida@17.18.0": true
|
|
67
67
|
}
|
|
68
68
|
}
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
type Matcher<T> = import("./matchers.js").Matcher<T>;
|
|
2
2
|
type Spy = import("./matchers.js").Spy;
|
|
3
3
|
type TestFn = import("./registry.js").TestFn;
|
|
4
|
+
type HookFn = import("./registry.js").HookFn;
|
|
4
5
|
|
|
5
6
|
declare function describe(name: string, fn: TestFn): void;
|
|
6
7
|
declare function it(name: string, fn: TestFn): void;
|
|
7
8
|
declare function test(name: string, fn: TestFn): void;
|
|
9
|
+
declare function beforeEach(fn: HookFn): void;
|
|
10
|
+
declare function afterEach(fn: HookFn): void;
|
|
11
|
+
declare function beforeAll(fn: HookFn): void;
|
|
12
|
+
declare function afterAll(fn: HookFn): void;
|
|
8
13
|
declare function expect<T>(actual: T): Matcher<T>;
|
|
9
14
|
declare function spyOn<T extends object, K extends keyof T>(target: T, key: K): Spy;
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { expect, spyOn, type Matcher, type Spy } from "./matchers.js";
|
|
2
|
-
import type { TestFn } from "./registry.js";
|
|
3
|
-
import { describe, it, test } from "./registry.js";
|
|
2
|
+
import type { HookFn, TestFn } from "./registry.js";
|
|
3
|
+
import { afterAll, afterEach, beforeAll, beforeEach, describe, it, test } from "./registry.js";
|
|
4
4
|
|
|
5
5
|
declare global {
|
|
6
6
|
function describe(name: string, fn: TestFn): void;
|
|
7
7
|
function it(name: string, fn: TestFn): void;
|
|
8
8
|
function test(name: string, fn: TestFn): void;
|
|
9
|
+
function beforeEach(fn: HookFn): void;
|
|
10
|
+
function afterEach(fn: HookFn): void;
|
|
11
|
+
function beforeAll(fn: HookFn): void;
|
|
12
|
+
function afterAll(fn: HookFn): void;
|
|
9
13
|
function expect<T>(actual: T): Matcher<T>;
|
|
10
14
|
function spyOn<T extends object, K extends keyof T>(target: T, key: K): Spy;
|
|
11
15
|
}
|
|
12
16
|
|
|
13
|
-
Object.assign(globalThis, { describe, it, test, expect, spyOn });
|
|
17
|
+
Object.assign(globalThis, { describe, it, test, beforeEach, afterEach, beforeAll, afterAll, expect, spyOn });
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type AgentMessage, type RunSummary, type TestError, type TestResult, type TestStatus, type TestSuiteResult } from "frida-test/protocol.js";
|
|
2
2
|
|
|
3
3
|
export type TestFn = () => void | Promise<void>;
|
|
4
|
+
export type HookFn = () => void | Promise<void>;
|
|
4
5
|
|
|
5
6
|
type NodeKind = "describe" | "it";
|
|
6
7
|
|
|
@@ -9,13 +10,40 @@ interface TestSuiteNode {
|
|
|
9
10
|
name: string;
|
|
10
11
|
fn: TestFn;
|
|
11
12
|
children?: TestSuiteNode[];
|
|
13
|
+
hooks?: HooksBag;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface HooksBag {
|
|
17
|
+
beforeEach: HookFn[];
|
|
18
|
+
afterEach: HookFn[];
|
|
19
|
+
beforeAll: HookFn[];
|
|
20
|
+
afterAll: HookFn[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface EachHooks {
|
|
24
|
+
beforeEach: HookFn[];
|
|
25
|
+
afterEach: HookFn[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function createHooksBag(): HooksBag {
|
|
29
|
+
return { beforeEach: [], afterEach: [], beforeAll: [], afterAll: [] };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface Frame {
|
|
33
|
+
children: TestSuiteNode[];
|
|
34
|
+
hooks: HooksBag;
|
|
12
35
|
}
|
|
13
36
|
|
|
14
37
|
export const registry: TestSuiteNode[] = [];
|
|
15
|
-
const
|
|
38
|
+
const rootHooks: HooksBag = createHooksBag();
|
|
39
|
+
const stack: Frame[] = [{ children: registry, hooks: rootHooks }];
|
|
40
|
+
|
|
41
|
+
function currentFrame(): Frame {
|
|
42
|
+
return stack[stack.length - 1];
|
|
43
|
+
}
|
|
16
44
|
|
|
17
45
|
function registerNode(kind: NodeKind, name: string, fn: TestFn): void {
|
|
18
|
-
(
|
|
46
|
+
currentFrame().children.push({ kind, name, fn });
|
|
19
47
|
}
|
|
20
48
|
|
|
21
49
|
export const describe = (name: string, fn: TestFn): void => registerNode("describe", name, fn);
|
|
@@ -23,6 +51,22 @@ export const describe = (name: string, fn: TestFn): void => registerNode("descri
|
|
|
23
51
|
export const it = (name: string, fn: TestFn): void => registerNode("it", name, fn);
|
|
24
52
|
export const test = it; // alias
|
|
25
53
|
|
|
54
|
+
export const beforeEach = (fn: HookFn): void => {
|
|
55
|
+
currentFrame().hooks.beforeEach.push(fn);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const afterEach = (fn: HookFn): void => {
|
|
59
|
+
currentFrame().hooks.afterEach.push(fn);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const beforeAll = (fn: HookFn): void => {
|
|
63
|
+
currentFrame().hooks.beforeAll.push(fn);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const afterAll = (fn: HookFn): void => {
|
|
67
|
+
currentFrame().hooks.afterAll.push(fn);
|
|
68
|
+
};
|
|
69
|
+
|
|
26
70
|
function serializeError(err: unknown, verbose: boolean): TestError {
|
|
27
71
|
if (err instanceof Error) {
|
|
28
72
|
return { message: err.message, stack: verbose ? err.stack : undefined };
|
|
@@ -30,6 +74,31 @@ function serializeError(err: unknown, verbose: boolean): TestError {
|
|
|
30
74
|
return { message: String(err) };
|
|
31
75
|
}
|
|
32
76
|
|
|
77
|
+
// Setup hooks (beforeEach/beforeAll): stop at the first failure since later hooks may depend on earlier ones.
|
|
78
|
+
async function runSetupHooks(hooks: HookFn[], verbose: boolean): Promise<TestError | undefined> {
|
|
79
|
+
for (const hook of hooks) {
|
|
80
|
+
try {
|
|
81
|
+
await hook();
|
|
82
|
+
} catch (err) {
|
|
83
|
+
return serializeError(err, verbose);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Teardown hooks (afterEach/afterAll): run every hook regardless of earlier failures, to give cleanup a chance to run.
|
|
90
|
+
async function runTeardownHooks(hooks: HookFn[], verbose: boolean): Promise<TestError | undefined> {
|
|
91
|
+
let firstError: TestError | undefined;
|
|
92
|
+
for (const hook of hooks) {
|
|
93
|
+
try {
|
|
94
|
+
await hook();
|
|
95
|
+
} catch (err) {
|
|
96
|
+
firstError ??= serializeError(err, verbose);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return firstError;
|
|
100
|
+
}
|
|
101
|
+
|
|
33
102
|
interface Counts {
|
|
34
103
|
total: number;
|
|
35
104
|
passed: number;
|
|
@@ -53,27 +122,28 @@ function withExpandLock<T>(fn: () => Promise<T>): Promise<T> {
|
|
|
53
122
|
return run;
|
|
54
123
|
}
|
|
55
124
|
|
|
56
|
-
async function expand(node: TestSuiteNode, verbose: boolean): Promise<{ children: TestSuiteNode[]; error?: TestError }> {
|
|
125
|
+
async function expand(node: TestSuiteNode, verbose: boolean): Promise<{ children: TestSuiteNode[]; hooks: HooksBag; error?: TestError }> {
|
|
57
126
|
return withExpandLock(async () => {
|
|
58
|
-
const children:
|
|
59
|
-
stack.push(
|
|
127
|
+
const frame: Frame = { children: [], hooks: createHooksBag() };
|
|
128
|
+
stack.push(frame);
|
|
60
129
|
try {
|
|
61
130
|
await node.fn();
|
|
62
|
-
return { children };
|
|
131
|
+
return { children: frame.children, hooks: frame.hooks };
|
|
63
132
|
} catch (err) {
|
|
64
|
-
return { children, error: serializeError(err, verbose) };
|
|
133
|
+
return { children: frame.children, hooks: frame.hooks, error: serializeError(err, verbose) };
|
|
65
134
|
} finally {
|
|
66
135
|
stack.pop();
|
|
67
136
|
}
|
|
68
137
|
});
|
|
69
138
|
}
|
|
70
139
|
|
|
71
|
-
async function runTestSuiteNode(node: TestSuiteNode, verbose: boolean): Promise<{ result: TestResult; counts: Counts }> {
|
|
140
|
+
async function runTestSuiteNode(node: TestSuiteNode, verbose: boolean, parentHooks: EachHooks): Promise<{ result: TestResult; counts: Counts }> {
|
|
72
141
|
const start = Date.now();
|
|
73
142
|
|
|
74
143
|
if (node.kind === "describe") {
|
|
75
|
-
const { children: nodes, error } = await expand(node, verbose);
|
|
144
|
+
const { children: nodes, hooks, error } = await expand(node, verbose);
|
|
76
145
|
node.children = nodes;
|
|
146
|
+
node.hooks = hooks;
|
|
77
147
|
|
|
78
148
|
if (error) {
|
|
79
149
|
const children: TestResult[] = nodes.map((child) => ({
|
|
@@ -93,42 +163,99 @@ async function runTestSuiteNode(node: TestSuiteNode, verbose: boolean): Promise<
|
|
|
93
163
|
return { result, counts };
|
|
94
164
|
}
|
|
95
165
|
|
|
166
|
+
const combinedHooks: EachHooks = {
|
|
167
|
+
beforeEach: [...parentHooks.beforeEach, ...hooks.beforeEach],
|
|
168
|
+
afterEach: [...hooks.afterEach, ...parentHooks.afterEach],
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const beforeAllError = await runSetupHooks(hooks.beforeAll, verbose);
|
|
172
|
+
if (beforeAllError) {
|
|
173
|
+
const children: TestResult[] = nodes.map((child) => ({
|
|
174
|
+
name: child.name,
|
|
175
|
+
status: "failed",
|
|
176
|
+
durationMs: 0,
|
|
177
|
+
error: { message: `parent suite "${node.name}" failed before this test could run` },
|
|
178
|
+
}));
|
|
179
|
+
await runTeardownHooks(hooks.afterAll, verbose);
|
|
180
|
+
const counts: Counts = { total: 1 + children.length, passed: 0, failed: 1 + children.length };
|
|
181
|
+
const result: TestResult = {
|
|
182
|
+
name: node.name,
|
|
183
|
+
status: "failed",
|
|
184
|
+
durationMs: Date.now() - start,
|
|
185
|
+
error: beforeAllError,
|
|
186
|
+
children,
|
|
187
|
+
};
|
|
188
|
+
return { result, counts };
|
|
189
|
+
}
|
|
190
|
+
|
|
96
191
|
const children: TestResult[] = [];
|
|
97
192
|
let counts = ZERO_COUNTS;
|
|
98
193
|
for (const child of node.children) {
|
|
99
|
-
const childRun = await runTestSuiteNode(child, verbose);
|
|
194
|
+
const childRun = await runTestSuiteNode(child, verbose, combinedHooks);
|
|
100
195
|
children.push(childRun.result);
|
|
101
196
|
counts = addCounts(counts, childRun.counts);
|
|
102
197
|
}
|
|
103
198
|
|
|
199
|
+
const afterAllError = await runTeardownHooks(hooks.afterAll, verbose);
|
|
200
|
+
if (afterAllError) {
|
|
201
|
+
children.push({ name: "afterAll hook", status: "failed", durationMs: 0, error: afterAllError });
|
|
202
|
+
counts = addCounts(counts, { total: 1, passed: 0, failed: 1 });
|
|
203
|
+
}
|
|
204
|
+
|
|
104
205
|
const status: TestStatus = counts.failed > 0 ? "failed" : "passed";
|
|
105
206
|
const result: TestResult = { name: node.name, status, durationMs: Date.now() - start, children };
|
|
106
207
|
return { result, counts };
|
|
107
208
|
}
|
|
108
209
|
|
|
109
210
|
// Leaf test.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
211
|
+
const beforeEachError = await runSetupHooks(parentHooks.beforeEach, verbose);
|
|
212
|
+
let result: TestResult;
|
|
213
|
+
let counts: Counts;
|
|
214
|
+
if (beforeEachError) {
|
|
215
|
+
result = { name: node.name, status: "failed", durationMs: 0, error: beforeEachError };
|
|
216
|
+
counts = { total: 1, passed: 0, failed: 1 };
|
|
217
|
+
} else {
|
|
218
|
+
try {
|
|
219
|
+
await node.fn();
|
|
220
|
+
result = { name: node.name, status: "passed", durationMs: 0 };
|
|
221
|
+
counts = { total: 1, passed: 1, failed: 0 };
|
|
222
|
+
} catch (err) {
|
|
223
|
+
result = { name: node.name, status: "failed", durationMs: 0, error: serializeError(err, verbose) };
|
|
224
|
+
counts = { total: 1, passed: 0, failed: 1 };
|
|
225
|
+
}
|
|
122
226
|
}
|
|
227
|
+
|
|
228
|
+
const afterEachError = await runTeardownHooks(parentHooks.afterEach, verbose);
|
|
229
|
+
if (afterEachError && result.status === "passed") {
|
|
230
|
+
result = { ...result, status: "failed", error: afterEachError };
|
|
231
|
+
counts = { total: 1, passed: 0, failed: 1 };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return { result: { ...result, durationMs: Date.now() - start }, counts };
|
|
123
235
|
}
|
|
124
236
|
|
|
125
237
|
export async function runTests(nodes: TestSuiteNode[], emit: (message: AgentMessage) => void, verbose: boolean = false): Promise<RunSummary> {
|
|
126
238
|
const start = Date.now();
|
|
127
239
|
|
|
240
|
+
const beforeAllError = await runSetupHooks(rootHooks.beforeAll, verbose);
|
|
241
|
+
if (beforeAllError) {
|
|
242
|
+
const testSuitesResults: TestSuiteResult[] = nodes.map((node) => {
|
|
243
|
+
emit({ type: "test-suite-started", name: node.name });
|
|
244
|
+
const testResult: TestResult = { name: node.name, status: "failed", durationMs: 0, error: beforeAllError };
|
|
245
|
+
const suiteResult: TestSuiteResult = { name: node.name, testResult, status: "failed" };
|
|
246
|
+
emit({ type: "test-suite-finished", name: node.name, result: suiteResult });
|
|
247
|
+
return suiteResult;
|
|
248
|
+
});
|
|
249
|
+
await runTeardownHooks(rootHooks.afterAll, verbose);
|
|
250
|
+
return { total: nodes.length, passed: 0, failed: nodes.length, durationMs: Date.now() - start, testSuitesResults };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const parentHooks: EachHooks = { beforeEach: rootHooks.beforeEach, afterEach: rootHooks.afterEach };
|
|
254
|
+
|
|
128
255
|
const settled = await Promise.allSettled(
|
|
129
256
|
nodes.map(async (node) => {
|
|
130
257
|
emit({ type: "test-suite-started", name: node.name });
|
|
131
|
-
const { result: testResult, counts } = await runTestSuiteNode(node, verbose);
|
|
258
|
+
const { result: testResult, counts } = await runTestSuiteNode(node, verbose, parentHooks);
|
|
132
259
|
const suiteResult: TestSuiteResult = { name: node.name, testResult, status: testResult.status };
|
|
133
260
|
emit({ type: "test-suite-finished", name: node.name, result: suiteResult });
|
|
134
261
|
return { suiteResult, counts };
|
|
@@ -155,5 +282,15 @@ export async function runTests(nodes: TestSuiteNode[], emit: (message: AgentMess
|
|
|
155
282
|
counts = addCounts(counts, { total: 1, passed: 0, failed: 1 });
|
|
156
283
|
});
|
|
157
284
|
|
|
285
|
+
const afterAllError = await runTeardownHooks(rootHooks.afterAll, verbose);
|
|
286
|
+
if (afterAllError) {
|
|
287
|
+
testSuitesResults.push({
|
|
288
|
+
name: "afterAll hook",
|
|
289
|
+
status: "failed",
|
|
290
|
+
testResult: { name: "afterAll hook", status: "failed", durationMs: 0, error: afterAllError },
|
|
291
|
+
});
|
|
292
|
+
counts = addCounts(counts, { total: 1, passed: 0, failed: 1 });
|
|
293
|
+
}
|
|
294
|
+
|
|
158
295
|
return { ...counts, durationMs: Date.now() - start, testSuitesResults };
|
|
159
296
|
}
|