Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/test/lifecycle.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ afterEach(() => {

## Async Lifecycle Hooks

All lifecycle hooks support async functions. Bun waits for the promise a hook or a test returns. It also waits for any other object with a `then()` method, such as the one React's `act()` returns.
All lifecycle hooks support async functions. Bun waits for the promise a hook or a test returns. It also waits for any other object with a `then()` method, such as the one React's `act()` returns. A hook or a test that takes a `done` parameter is the exception: Bun does not call `then()` on an object it returns.

```ts title="test.ts" icon="/icons/typescript.svg"
import { beforeAll, afterAll, test, expect } from "bun:test";
Expand Down
3 changes: 2 additions & 1 deletion src/runtime/test_runner/bun_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1389,7 +1389,8 @@ impl BunTest {
args_slice,
move || drop(on_stack),
)
.and_then(|returned| Self::promise_of_thenable(global_this, returned))
// A callback that takes `done` ends through it: `then()` of what it also returns is not called.
.and_then(|returned| if cfg_done_parameter { Ok(returned) } else { Self::promise_of_thenable(global_this, returned) })
{
Ok(v) => v,
Err(_) => {
Expand Down
44 changes: 44 additions & 0 deletions test/js/bun/test/vitest-flavor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2533,6 +2533,50 @@ describe.concurrent.each(["vitest", "bun:test", "@jest/globals"])("a thenable th
});
});

test.concurrent.each(["bun:test", "@jest/globals"])(
"then() of what a callback with a done parameter returns is not called: %s",
async module => {
const { log, results, errors, exitCode } = await runTests({
"a.test.js": `
import { test, describe, beforeEach } from "${module}";
// The shape of a supertest request: end() sends it, and then() calls end() too.
const request = name => ({
end(callback) {
if (this.sent) throw new Error(name + ": end() was called twice");
this.sent = true;
setImmediate(() => { console.log(name + ": sent"); callback(); });
return this;
},
then(resolve, reject) { console.log(name + ": then()"); this.end(error => (error ? reject(error) : resolve())); },
});
const thenable = (name, then) => ({ then(resolve, reject) { console.log(name + ": then()"); then?.(resolve, reject); } });
describe("a hook", () => {
beforeEach(done => request("beforeEach").end(done));
test("a request", done => request("test").end(done));
});
test.each([["each"]])("test.each with a done parameter: %s", (name, done) => request(name).end(done));
test("a thenable that rejects", done => { done(); return thenable("rejects", (resolve, reject) => reject(new Error("rejected"))); });
test("a thenable that never settles", done => { done(); return thenable("never settles"); }, 500);
test("done() is never called", done => request("never sent"), 10);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
test("done(error) is called later", done => { setImmediate(() => done(new Error("from done()"))); return request("never sent"); });
`,
});
expect({ log, results, errors, exitCode }).toEqual({
log: ["beforeEach: sent", "test: sent", "each: sent"],
results: [
"(pass) a hook > a request",
"(pass) test.each with a done parameter: each",
"(pass) a thenable that rejects",
"(pass) a thenable that never settles",
"(fail) done() is never called",
"(fail) done(error) is called later",
],
errors: ["error: from done()"],
exitCode: 1,
});
},
);

test.concurrent("a function that a thenable from beforeEach or beforeAll resolves to is a teardown", async () => {
const { log, exitCode } = await runTests({
"a.test.js": `
Expand Down
Loading