Skip to content

Commit ef301e7

Browse files
alexlamslJarred-Sumner
authored andcommitted
[jest] fix and improve hooks (#1689)
- wait for async hooks to complete before running tests - add support for `done(err)` callbacks in hooks fixes #1688
1 parent 63810c4 commit ef301e7

File tree

4 files changed

+189
-8
lines changed

4 files changed

+189
-8
lines changed

packages/bun-types/bun-test.d.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@ declare module "bun:test" {
2626
export { test as it };
2727

2828
export function expect(value: any): Expect;
29-
export function afterAll(fn: () => void): void;
30-
export function beforeAll(fn: () => void): void;
29+
export function afterAll(fn: (done: (err?: any) => void) => void | Promise<any>): void;
30+
export function beforeAll(fn: (done: (err?: any) => void) => void | Promise<any>): void;
3131

32-
export function afterEach(fn: () => void): void;
33-
export function beforeEach(fn: () => void): void;
32+
export function afterEach(fn: (done: (err?: any) => void) => void | Promise<any>): void;
33+
export function beforeEach(fn: (done: (err?: any) => void) => void | Promise<any>): void;
3434

3535
interface Expect {
3636
not: Expect;

packages/bun-types/globals.d.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1280,6 +1280,14 @@ declare function queueMicrotask(callback: (...args: any[]) => void): void;
12801280
* @param error Error or string
12811281
*/
12821282
declare function reportError(error: any): void;
1283+
/**
1284+
* Run a function immediately after main event loop is vacant
1285+
* @param handler function to call
1286+
*/
1287+
declare function setImmediate(
1288+
handler: TimerHandler,
1289+
...arguments: any[]
1290+
): number;
12831291
/**
12841292
* Run a function every `interval` milliseconds
12851293
* @param handler function to call
@@ -2277,7 +2285,7 @@ declare function alert(message?: string): void;
22772285
declare function confirm(message?: string): boolean;
22782286
declare function prompt(message?: string, _default?: string): string | null;
22792287

2280-
/*
2288+
/*
22812289
22822290
Web Crypto API
22832291

src/bun.js/test/jest.zig

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1398,6 +1398,7 @@ pub const DescribeScope = struct {
13981398
file_id: TestRunner.File.ID,
13991399
current_test_id: TestRunner.Test.ID = 0,
14001400
value: JSValue = .zero,
1401+
done: bool = false,
14011402

14021403
pub fn push(new: *DescribeScope) void {
14031404
if (comptime is_bindgen) return undefined;
@@ -1486,16 +1487,61 @@ pub const DescribeScope = struct {
14861487
},
14871488
);
14881489

1490+
pub fn onDone(
1491+
ctx: js.JSContextRef,
1492+
callframe: *JSC.CallFrame,
1493+
) callconv(.C) JSValue {
1494+
const function = callframe.callee();
1495+
const args = callframe.arguments(1);
1496+
defer ctx.bunVM().autoGarbageCollect();
1497+
1498+
if (JSC.getFunctionData(function)) |data| {
1499+
var scope = bun.cast(*DescribeScope, data);
1500+
JSC.setFunctionData(function, null);
1501+
if (args.len > 0) {
1502+
const err = args.ptr[0];
1503+
ctx.bunVM().runErrorHandlerWithDedupe(err, null);
1504+
}
1505+
scope.done = true;
1506+
}
1507+
1508+
return JSValue.jsUndefined();
1509+
}
1510+
14891511
pub fn execCallback(this: *DescribeScope, ctx: js.JSContextRef, comptime hook: LifecycleHook) JSValue {
14901512
const name = comptime @as(string, @tagName(hook));
14911513
var hooks: []JSC.JSValue = @field(this, name).items;
14921514
for (hooks) |cb, i| {
14931515
if (cb.isEmpty()) continue;
14941516

1495-
const err = cb.call(ctx, &.{});
1496-
if (err.isAnyError(ctx)) {
1497-
return err;
1517+
const pending_test = Jest.runner.?.pending_test;
1518+
// forbid `expect()` within hooks
1519+
Jest.runner.?.pending_test = null;
1520+
var result = if (cb.getLengthOfArray(ctx) > 0) brk: {
1521+
this.done = false;
1522+
const done_func = JSC.NewFunctionWithData(
1523+
ctx,
1524+
ZigString.static("done"),
1525+
0,
1526+
DescribeScope.onDone,
1527+
false,
1528+
this,
1529+
);
1530+
var result = cb.call(ctx, &.{done_func});
1531+
var vm = VirtualMachine.get();
1532+
while (!this.done) {
1533+
vm.eventLoop().autoTick();
1534+
if (this.done) break;
1535+
vm.eventLoop().tick();
1536+
}
1537+
break :brk result;
1538+
} else cb.call(ctx, &.{});
1539+
if (result.asPromise()) |promise| {
1540+
VirtualMachine.get().waitForPromise(promise);
1541+
result = promise.result(ctx.vm());
14981542
}
1543+
Jest.runner.?.pending_test = pending_test;
1544+
if (result.isAnyError(ctx)) return result;
14991545

15001546
if (comptime hook == .beforeAll or hook == .afterAll) {
15011547
hooks[i] = JSC.JSValue.zero;

test/bun.js/bun-test/jest-hooks.test.ts

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,4 +73,131 @@ describe("test jest hooks in bun-test", () => {
7373
expect(animal).toEqual("lion");
7474
});
7575
});
76+
77+
describe("test async hooks", async () => {
78+
let beforeAllCalled = 0;
79+
let beforeEachCalled = 0;
80+
let afterAllCalled = 0;
81+
let afterEachCalled = 0;
82+
83+
beforeAll(async () => {
84+
beforeAllCalled += await 1;
85+
});
86+
87+
beforeEach(async () => {
88+
beforeEachCalled += await 1;
89+
});
90+
91+
afterAll(async () => {
92+
afterAllCalled += await 1;
93+
});
94+
95+
afterEach(async () => {
96+
afterEachCalled += await 1;
97+
});
98+
99+
it("should run after beforeAll()", () => {
100+
expect(beforeAllCalled).toBe(1);
101+
expect(beforeEachCalled).toBe(1);
102+
expect(afterAllCalled).toBe(0);
103+
expect(afterEachCalled).toBe(0);
104+
});
105+
106+
it("should run after beforeEach()", () => {
107+
expect(beforeAllCalled).toBe(1);
108+
expect(beforeEachCalled).toBe(2);
109+
expect(afterAllCalled).toBe(0);
110+
expect(afterEachCalled).toBe(1);
111+
});
112+
});
113+
114+
describe("test done callback in hooks", () => {
115+
let beforeAllCalled = 0;
116+
let beforeEachCalled = 0;
117+
let afterAllCalled = 0;
118+
let afterEachCalled = 0;
119+
120+
beforeAll(done => {
121+
setImmediate(() => {
122+
beforeAllCalled++;
123+
done();
124+
});
125+
});
126+
127+
beforeEach(done => {
128+
setImmediate(() => {
129+
beforeEachCalled++;
130+
done();
131+
});
132+
});
133+
134+
afterAll(done => {
135+
setImmediate(() => {
136+
afterAllCalled++;
137+
done();
138+
});
139+
});
140+
141+
afterEach(done => {
142+
setImmediate(() => {
143+
afterEachCalled++;
144+
done();
145+
});
146+
});
147+
148+
it("should run after beforeAll()", () => {
149+
expect(beforeAllCalled).toBe(1);
150+
expect(beforeEachCalled).toBe(1);
151+
expect(afterAllCalled).toBe(0);
152+
expect(afterEachCalled).toBe(0);
153+
});
154+
155+
it("should run after beforeEach()", () => {
156+
expect(beforeAllCalled).toBe(1);
157+
expect(beforeEachCalled).toBe(2);
158+
expect(afterAllCalled).toBe(0);
159+
expect(afterEachCalled).toBe(1);
160+
});
161+
});
162+
163+
describe("test async hooks with done()", () => {
164+
let beforeAllCalled = 0;
165+
let beforeEachCalled = 0;
166+
let afterAllCalled = 0;
167+
let afterEachCalled = 0;
168+
169+
beforeAll(async done => {
170+
beforeAllCalled += await 1;
171+
setTimeout(done, 1);
172+
});
173+
174+
beforeEach(async done => {
175+
beforeEachCalled += await 1;
176+
setTimeout(done, 1);
177+
});
178+
179+
afterAll(async done => {
180+
afterAllCalled += await 1;
181+
setTimeout(done, 1);
182+
});
183+
184+
afterEach(async done => {
185+
afterEachCalled += await 1;
186+
setTimeout(done, 1);
187+
});
188+
189+
it("should run after beforeAll()", () => {
190+
expect(beforeAllCalled).toBe(1);
191+
expect(beforeEachCalled).toBe(1);
192+
expect(afterAllCalled).toBe(0);
193+
expect(afterEachCalled).toBe(0);
194+
});
195+
196+
it("should run after beforeEach()", () => {
197+
expect(beforeAllCalled).toBe(1);
198+
expect(beforeEachCalled).toBe(2);
199+
expect(afterAllCalled).toBe(0);
200+
expect(afterEachCalled).toBe(1);
201+
});
202+
});
76203
});

0 commit comments

Comments
 (0)