Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
9 changes: 1 addition & 8 deletions src/runtime/test_runner/expect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1182,14 +1182,7 @@ impl Expect {
}
}

if value.jest_snapshot_pretty_format(pretty_value, global_this).is_err() {
let mut formatter = ConsoleObject::Formatter::new(global_this);
return Err(global_this.throw(format_args!(
"Failed to pretty format value: {}",
value.to_fmt(&mut formatter),
)));
}
Ok(())
value.jest_snapshot_pretty_format(pretty_value, global_this)
}

pub(crate) fn snapshot(
Expand Down
9 changes: 4 additions & 5 deletions src/runtime/test_runner/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -273,11 +273,10 @@ pub mod expect {
out,
fmt_options,
)?;
// `FormatOptions.flush` is false, so the formatter does not flush
// internally; a buffered `out` would otherwise drop trailing
// snapshot bytes. Propagate the writer error as a thrown JS error
// so the caller's `.is_err()` branch
// (expect.rs `to_match_snapshot_value_kind`) fires.
// The formatter ignores the result of its own flush. Throw the
// writer error here so that, like the formatter's failures, every
// `Err` from this function has a JS exception pending and callers
// can simply `?` it.
Comment thread
robobun marked this conversation as resolved.
Outdated
out.flush().map_err(|e| global.throw_error(e, "snapshot writer flush failed"))?;
Ok(())
}
Expand Down
104 changes: 104 additions & 0 deletions test/js/bun/test/snapshot-tests/bun-snapshots.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,108 @@ describe("toMatchSnapshot errors", () => {
expect({ a: 4 }).toMatchSnapshot({ a: expect.any("not a constructor") });
}).toThrow();
});

describe("when formatting the received value throws", () => {
// The snapshot formatter reads `$$typeof` off every object (React element
// detection), `size` off Maps and Sets, and JSON-stringifies Dates, so
// user code on any of those runs while the value is being formatted.
const received: [string, () => unknown][] = [
[
"$$typeof getter on the received value",
() => ({
get $$typeof(): unknown {
throw new Error("boom");
},
}),
],
[
// Keep this a single property: until #37331 lands, the property walk
// only surfaces an exception thrown while formatting the last key.
"$$typeof getter on a nested value",
() => ({
a: {
get $$typeof(): unknown {
throw new Error("boom");
},
},
}),
],
[
"size getter on a Map",
() =>
Object.defineProperty(new Map(), "size", {
get() {
throw new Error("boom");
},
}),
],
[
"size getter on a Set",
() =>
Object.defineProperty(new Set(), "size", {
get() {
throw new Error("boom");
},
}),
],
[
"toJSON on a Date",
() =>
Object.assign(new Date(0), {
toJSON() {
throw new Error("boom");
},
}),
],
];

it.each(received)("toMatchSnapshot throws the error from the %s", (_, makeValue) => {
expect(() => expect(makeValue()).toMatchSnapshot()).toThrow("boom");
});

it.each(received)("toMatchInlineSnapshot throws the error from the %s", (_, makeValue) => {
// Passing the inline snapshot means a build that does not throw fails on
// the mismatch instead of writing into this file.
expect(() => expect(makeValue()).toMatchInlineSnapshot(`"never recorded"`)).toThrow("boom");
});

it("throws the exception itself rather than a wrapper", () => {
const error = new Error("boom");
const value = {
get $$typeof(): unknown {
throw error;
},
};

let thrown: unknown;
try {
expect(value).toMatchSnapshot();
} catch (e) {
thrown = e;
}
expect(thrown).toBe(error);

thrown = undefined;
try {
expect(value).toMatchInlineSnapshot(`"never recorded"`);
} catch (e) {
thrown = e;
}
expect(thrown).toBe(error);
});

it("still throws the formatting error after the property matchers matched", () => {
// Fresh object per call: matched property matchers are written into the received object (#3521).
const makeValue = () => ({
n: 1,
get $$typeof(): unknown {
throw new Error("boom");
},
});
expect(() => expect(makeValue()).toMatchSnapshot({ n: expect.any(Number) })).toThrow("boom");
expect(() => expect(makeValue()).toMatchInlineSnapshot({ n: expect.any(Number) }, `"never recorded"`)).toThrow(
"boom",
);
});
});
});