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 .prettierignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
vendor
test/snapshots
# written by Jest and Vitest, and compared byte for byte
# written by Jest, Vitest and Bun 1.4.4, and compared byte for byte
test/js/bun/test/snapshot-tests/formats
test/bundler/transpiler/react-compiler-fixtures
src/**/*.generated.rs
Expand Down
6 changes: 5 additions & 1 deletion docs/test/snapshots.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ test("error snapshot", () => {
});
```

The snapshot holds the `message` of the thrown value. The value doesn't have to be an `Error`: for `throw { message: "Not found", status: 404 }`, the snapshot is `"Not found"`. A snapshot in [Vitest's format](#snapshots-from-jest-and-vitest) holds the thrown value itself.

## File Snapshots

`.toMatchFileSnapshot()` compares a value with a snapshot stored in its own file. The file can have the right extension, so you can open it like any other file. The path is relative to the test file.
Expand Down Expand Up @@ -296,7 +298,9 @@ Inline snapshots don't record which runner wrote them, so Bun accepts a match in

With `--update-snapshots`, Bun removes entries that no test uses anymore from a file in Jest's or Vitest's format. It keeps entries for tests that were skipped, filtered out or failed. A file in Bun's format keeps only the entries of tests that ran.

Earlier versions of Bun printed `[native code]` for an accessor instead of its value, included non-enumerable own properties, and printed DOM nodes as plain objects. Snapshots written that way still pass. Bun leaves them alone until `--update-snapshots` rewrites them.
Earlier versions of Bun printed `[native code]` for an accessor instead of its value, included non-enumerable own properties, and printed DOM nodes as plain objects. They also stored `undefined` in place of the message of a thrown object that no `Error` constructor created, such as a plain object or a `DOMException`. Snapshots written that way still pass. Bun leaves them alone until `--update-snapshots` rewrites them.

A stored `undefined` for such a thrown object passes for any message. Run `bun test --update-snapshots` to store the message.

## Best Practices

Expand Down
15 changes: 12 additions & 3 deletions src/runtime/test_runner/expect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -987,8 +987,10 @@ impl Expect {
};
match format {
SnapshotFormat::Vitest => Ok(thrown),
SnapshotFormat::Bun if !thrown.is_any_error() => Ok(JSValue::UNDEFINED),
SnapshotFormat::Bun => Ok(thrown.get_truthy(global_this, "message")?.unwrap_or(JSValue::UNDEFINED)),
SnapshotFormat::Bun if !thrown.is_object() => Ok(JSValue::UNDEFINED),
SnapshotFormat::Bun => {
Ok(thrown.fast_get_truthy(global_this, bun_jsc::BuiltinName::Message)?.unwrap_or(JSValue::UNDEFINED))
}
SnapshotFormat::Jest => Self::message_with_causes(global_this, thrown),
}
}
Expand Down Expand Up @@ -1268,9 +1270,16 @@ impl Expect {
property_matchers: Option<JSValue>,
saved: &[u8],
) -> JsResult<bool> {
// An older Bun stored `undefined` for a thrown object that `is_any_error()` is false for, whatever its `message`.
let undefined_to_older_bun =
matches!(received, Received::Thrown(thrown) if thrown.is_object() && !thrown.is_any_error());
let or_older_bun = or_older_bun || undefined_to_older_bun;
Comment thread
robobun marked this conversation as resolved.
Resolved
for format in or_older_bun.then_some(None).into_iter().chain(formats.iter().copied().map(Some)) {
let is_it = (|| {
let value = Self::value_to_snapshot(global_this, received, format.unwrap_or(SnapshotFormat::Bun))?;
let value = match format {
None if undefined_to_older_bun => JSValue::UNDEFINED,
Comment thread
robobun marked this conversation as resolved.
Resolved
_ => Self::value_to_snapshot(global_this, received, format.unwrap_or(SnapshotFormat::Bun))?,
};
let Some(format) = format else {
// Older Bun wrote the matchers into the object in place, which didn't stick for getters or frozen objects.
return Ok((property_matchers.is_some() && JestPrettyFormat::did_older_bun_print(global_this, with_matchers, saved)?)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Bun Snapshot v1, https://bun.sh/docs/test/snapshots

exports[`an Error 1`] = `"not found"`;

exports[`an instance of a class that extends Error 1`] = `"not found"`;

exports[`an Error with a cause 1`] = `"outer"`;

exports[`an Error with an empty message 1`] = `undefined`;

exports[`an Error whose message has a getter 1`] = `
{
"total": [native code],
}
`;

exports[`a plain object 1`] = `undefined`;

exports[`an instance of a class 1`] = `undefined`;

exports[`an object with a null prototype 1`] = `undefined`;

exports[`a DOMException 1`] = `undefined`;

exports[`an object that inherits Error.prototype 1`] = `undefined`;

exports[`an object with a name 1`] = `undefined`;

exports[`a rejected promise 1`] = `undefined`;

exports[`an object with the message 0 1`] = `undefined`;

exports[`an object with an empty message 1`] = `undefined`;

exports[`an object with no message 1`] = `undefined`;

exports[`a string 1`] = `undefined`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Bun 1.4.4 wrote the snapshots of this file, here and in __snapshots__/thrown.fixture.js.snap.
// To add a case, run it under that version. Never update them with a later Bun.
import { expect, test } from "bun:test";

const thrower = value => () => {
throw value;
};
const both = value => {
expect(thrower(value)).toThrowErrorMatchingSnapshot();
return expect(thrower(value));
};

test("an Error", () => { both(new Error("not found")).toThrowErrorMatchingInlineSnapshot(`"not found"`); });
test("an instance of a class that extends Error", () => { both(new (class HttpError extends Error {})("not found")).toThrowErrorMatchingInlineSnapshot(`"not found"`); });
test("an Error with a cause", () => { both(new Error("outer", { cause: new Error("inner") })).toThrowErrorMatchingInlineSnapshot(`"outer"`); });
test("an Error with an empty message", () => { both(new Error("")).toThrowErrorMatchingInlineSnapshot(`undefined`); });
test("an Error whose message has a getter", () => { both(Object.assign(new Error("x"), { message: { get total() { return 1; } } })).toThrowErrorMatchingInlineSnapshot(`
{
"total": [native code],
}
`); });
test("a plain object", () => { both({ message: "not found", code: 4 }).toThrowErrorMatchingInlineSnapshot(`undefined`); });
test("an instance of a class", () => { both(new (class Failure { constructor(m) { this.message = m; } })("not found")).toThrowErrorMatchingInlineSnapshot(`undefined`); });
test("an object with a null prototype", () => { both(Object.assign(Object.create(null), { message: "not found" })).toThrowErrorMatchingInlineSnapshot(`undefined`); });
test("a DOMException", () => { both(new DOMException("not found", "NotFoundError")).toThrowErrorMatchingInlineSnapshot(`undefined`); });
test("an object that inherits Error.prototype", () => { both(Object.assign(Object.create(Error.prototype), { message: "not found" })).toThrowErrorMatchingInlineSnapshot(`undefined`); });
test("an object with a name", () => { both({ name: "HttpError", message: "not found" }).toThrowErrorMatchingInlineSnapshot(`undefined`); });
test("a rejected promise", async () => { await expect(Promise.reject({ message: "not found" })).rejects.toThrowErrorMatchingSnapshot(); await expect(Promise.reject({ message: "not found" })).rejects.toThrowErrorMatchingInlineSnapshot(`undefined`); });
test("an object with the message 0", () => { both({ message: 0 }).toThrowErrorMatchingInlineSnapshot(`undefined`); });
test("an object with an empty message", () => { both({ message: "" }).toThrowErrorMatchingInlineSnapshot(`undefined`); });
test("an object with no message", () => { both({ code: 4 }).toThrowErrorMatchingInlineSnapshot(`undefined`); });
test("a string", () => { both("not found").toThrowErrorMatchingInlineSnapshot(`undefined`); });
Loading
Loading