mirror of
https://github.com/oven-sh/bun
synced 2026-02-16 22:01:47 +00:00
Compare commits
1 Commits
claude/add
...
claude/imp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27163b9acd |
@@ -541,6 +541,51 @@ pub const JSValue = enum(i64) {
|
||||
}
|
||||
|
||||
pub fn jestSnapshotPrettyFormat(this: JSValue, out: *MutableString, globalObject: *JSGlobalObject) !void {
|
||||
// Check for custom snapshot serializers
|
||||
if (Jest.runner) |runner| {
|
||||
for (runner.snapshots.serializers.items) |serializer| {
|
||||
// Call the test function to check if this serializer applies
|
||||
const test_fn = (try serializer.getIfPropertyExists(globalObject, "test")) orelse continue;
|
||||
|
||||
var test_result = test_fn.callWithGlobalThis(globalObject, &[_]JSValue{this}) catch continue;
|
||||
|
||||
// If test returns truthy, use this serializer
|
||||
if (test_result.toBoolean()) {
|
||||
// Try print first (modern Jest API)
|
||||
if (try serializer.getIfPropertyExists(globalObject, "print")) |print_fn| {
|
||||
// Call print(val) with just the value
|
||||
// Note: Jest's print() can optionally receive a second argument (printer function)
|
||||
// but many serializers work fine with just the value
|
||||
const result = print_fn.callWithGlobalThis(globalObject, &[_]JSValue{this}) catch {
|
||||
// If print fails, fall through to default formatting
|
||||
break;
|
||||
};
|
||||
|
||||
if (result.isString()) {
|
||||
const result_str = try result.toBunString(globalObject);
|
||||
defer result_str.deref();
|
||||
try out.append(result_str.toUTF8(bun.default_allocator).slice());
|
||||
return;
|
||||
}
|
||||
} else if (try serializer.getIfPropertyExists(globalObject, "serialize")) |serialize_fn| {
|
||||
// Try serialize (legacy Jest API) - call it with just the value
|
||||
const result = serialize_fn.callWithGlobalThis(globalObject, &[_]JSValue{this}) catch {
|
||||
// If serialize fails, fall through to default formatting
|
||||
break;
|
||||
};
|
||||
|
||||
if (result.isString()) {
|
||||
const result_str = try result.toBunString(globalObject);
|
||||
defer result_str.deref();
|
||||
try out.append(result_str.toUTF8(bun.default_allocator).slice());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to default formatting
|
||||
var buffered_writer = MutableString.BufferedWriter{ .context = out };
|
||||
const writer = buffered_writer.writer();
|
||||
const Writer = @TypeOf(writer);
|
||||
@@ -2413,6 +2458,7 @@ const string = []const u8;
|
||||
const FFI = @import("./FFI.zig");
|
||||
const std = @import("std");
|
||||
const JestPrettyFormat = @import("../test/pretty_format.zig").JestPrettyFormat;
|
||||
const Jest = @import("../test/jest.zig").Jest;
|
||||
|
||||
const bun = @import("bun");
|
||||
const Environment = bun.Environment;
|
||||
|
||||
@@ -1157,7 +1157,46 @@ pub const Expect = struct {
|
||||
return thisValue;
|
||||
}
|
||||
|
||||
pub const addSnapshotSerializer = notImplementedStaticFn;
|
||||
/// Implements `expect.addSnapshotSerializer(serializer)`
|
||||
/// https://jestjs.io/docs/expect#expectaddsnapshotserializerserializer
|
||||
pub fn addSnapshotSerializer(globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue {
|
||||
const args = callFrame.arguments_old(1).slice();
|
||||
|
||||
if (args.len == 0 or !args[0].isObject()) {
|
||||
return globalThis.throwPretty("<d>expect.<r>addSnapshotSerializer<d>(<r>serializer<d>)<r>\n\nExpected an object with 'test' and 'serialize' or 'print' properties\n", .{});
|
||||
}
|
||||
|
||||
const serializer = args[0];
|
||||
|
||||
// Validate that serializer has a 'test' function
|
||||
const test_fn = try serializer.getIfPropertyExists(globalThis, "test") orelse {
|
||||
return globalThis.throwPretty("<d>expect.<r>addSnapshotSerializer<d>(<r>serializer<d>)<r>\n\nExpected serializer to have a 'test' function\n", .{});
|
||||
};
|
||||
|
||||
if (!test_fn.jsType().isFunction()) {
|
||||
return globalThis.throwPretty("<d>expect.<r>addSnapshotSerializer<d>(<r>serializer<d>)<r>\n\nExpected serializer.test to be a function\n", .{});
|
||||
}
|
||||
|
||||
// Validate that serializer has either 'serialize' or 'print' function
|
||||
const serialize_fn = try serializer.getIfPropertyExists(globalThis, "serialize");
|
||||
const print_fn = try serializer.getIfPropertyExists(globalThis, "print");
|
||||
|
||||
if ((serialize_fn == null and print_fn == null) or
|
||||
(serialize_fn != null and !serialize_fn.?.jsType().isFunction()) or
|
||||
(print_fn != null and !print_fn.?.jsType().isFunction()))
|
||||
{
|
||||
return globalThis.throwPretty("<d>expect.<r>addSnapshotSerializer<d>(<r>serializer<d>)<r>\n\nExpected serializer to have either 'serialize' or 'print' function\n", .{});
|
||||
}
|
||||
|
||||
// Add serializer to the list (prepend so latest serializers are checked first, matching Jest behavior)
|
||||
if (Jest.runner) |runner| {
|
||||
try runner.snapshots.serializers.insert(0, serializer);
|
||||
}
|
||||
|
||||
globalThis.bunVM().autoGarbageCollect();
|
||||
|
||||
return .js_undefined;
|
||||
}
|
||||
|
||||
pub fn hasAssertions(globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue {
|
||||
_ = callFrame;
|
||||
|
||||
@@ -16,6 +16,7 @@ pub const Snapshots = struct {
|
||||
_current_file: ?File = null,
|
||||
snapshot_dir_path: ?string = null,
|
||||
inline_snapshots_to_write: *std.AutoArrayHashMap(TestRunner.File.ID, std.ArrayList(InlineSnapshotToWrite)),
|
||||
serializers: std.ArrayList(JSValue),
|
||||
|
||||
pub const InlineSnapshotToWrite = struct {
|
||||
line: c_ulong,
|
||||
@@ -565,3 +566,4 @@ const strings = bun.strings;
|
||||
|
||||
const jsc = bun.jsc;
|
||||
const VirtualMachine = jsc.VirtualMachine;
|
||||
const JSValue = jsc.JSValue;
|
||||
|
||||
@@ -1349,6 +1349,7 @@ pub const TestCommand = struct {
|
||||
.values = &snapshot_values,
|
||||
.counts = &snapshot_counts,
|
||||
.inline_snapshots_to_write = &inline_snapshots_to_write,
|
||||
.serializers = std.ArrayList(jsc.JSValue).init(ctx.allocator),
|
||||
},
|
||||
.bun_test_root = .init(ctx.allocator),
|
||||
},
|
||||
|
||||
369
test/js/bun/test/snapshot-serializer.test.ts
Normal file
369
test/js/bun/test/snapshot-serializer.test.ts
Normal file
@@ -0,0 +1,369 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { bunEnv, bunExe, tempDir } from "harness";
|
||||
|
||||
describe("expect.addSnapshotSerializer", () => {
|
||||
test("should serialize custom objects with print function", async () => {
|
||||
using dir = tempDir("snapshot-serializer-print", {
|
||||
"test.test.js": `
|
||||
import { test, expect } from "bun:test";
|
||||
|
||||
class Point {
|
||||
constructor(x, y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
|
||||
expect.addSnapshotSerializer({
|
||||
test(val) {
|
||||
return val instanceof Point;
|
||||
},
|
||||
print(val) {
|
||||
return \`Point(\${val.x}, \${val.y})\`;
|
||||
}
|
||||
});
|
||||
|
||||
test("snapshot with custom serializer", () => {
|
||||
const point = new Point(10, 20);
|
||||
expect(point).toMatchSnapshot();
|
||||
});
|
||||
`,
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "test", "test.test.js", "--update-snapshots"],
|
||||
env: bunEnv,
|
||||
cwd: String(dir),
|
||||
stderr: "pipe",
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
expect(stderr).not.toContain("panic");
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
// Check that snapshot file was created with custom serialization
|
||||
const snapshotContent = await Bun.file(`${dir}/__snapshots__/test.test.js.snap`).text();
|
||||
expect(snapshotContent).toContain("Point(10, 20)");
|
||||
});
|
||||
|
||||
test("should serialize custom objects with serialize function", async () => {
|
||||
using dir = tempDir("snapshot-serializer-serialize", {
|
||||
"test.test.js": `
|
||||
import { test, expect } from "bun:test";
|
||||
|
||||
class Vector {
|
||||
constructor(x, y, z) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
}
|
||||
}
|
||||
|
||||
expect.addSnapshotSerializer({
|
||||
test(val) {
|
||||
return val instanceof Vector;
|
||||
},
|
||||
serialize(val) {
|
||||
return \`<Vector x=\${val.x} y=\${val.y} z=\${val.z}>\`;
|
||||
}
|
||||
});
|
||||
|
||||
test("snapshot with serialize function", () => {
|
||||
const vec = new Vector(1, 2, 3);
|
||||
expect(vec).toMatchSnapshot();
|
||||
});
|
||||
`,
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "test", "test.test.js", "--update-snapshots"],
|
||||
env: bunEnv,
|
||||
cwd: String(dir),
|
||||
stderr: "pipe",
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
expect(stderr).not.toContain("panic");
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
const snapshotContent = await Bun.file(`${dir}/__snapshots__/test.test.js.snap`).text();
|
||||
expect(snapshotContent).toContain("<Vector x=1 y=2 z=3>");
|
||||
});
|
||||
|
||||
test("should use most recently added serializer first", async () => {
|
||||
using dir = tempDir("snapshot-serializer-order", {
|
||||
"test.test.js": `
|
||||
import { test, expect } from "bun:test";
|
||||
|
||||
class MyClass {
|
||||
constructor(value) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
expect.addSnapshotSerializer({
|
||||
test(val) {
|
||||
return val instanceof MyClass;
|
||||
},
|
||||
print(val) {
|
||||
return \`FirstSerializer(\${val.value})\`;
|
||||
}
|
||||
});
|
||||
|
||||
expect.addSnapshotSerializer({
|
||||
test(val) {
|
||||
return val instanceof MyClass;
|
||||
},
|
||||
print(val) {
|
||||
return \`SecondSerializer(\${val.value})\`;
|
||||
}
|
||||
});
|
||||
|
||||
test("uses most recent serializer", () => {
|
||||
const obj = new MyClass("test");
|
||||
expect(obj).toMatchSnapshot();
|
||||
});
|
||||
`,
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "test", "test.test.js", "--update-snapshots"],
|
||||
env: bunEnv,
|
||||
cwd: String(dir),
|
||||
stderr: "pipe",
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
expect(stderr).not.toContain("panic");
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
const snapshotContent = await Bun.file(`${dir}/__snapshots__/test.test.js.snap`).text();
|
||||
expect(snapshotContent).toContain("SecondSerializer(test)");
|
||||
expect(snapshotContent).not.toContain("FirstSerializer");
|
||||
});
|
||||
|
||||
test("should fall back to default formatting if test returns false", async () => {
|
||||
using dir = tempDir("snapshot-serializer-fallback", {
|
||||
"test.test.js": `
|
||||
import { test, expect } from "bun:test";
|
||||
|
||||
expect.addSnapshotSerializer({
|
||||
test(val) {
|
||||
return false; // Never matches
|
||||
},
|
||||
print(val) {
|
||||
return "SHOULD_NOT_APPEAR";
|
||||
}
|
||||
});
|
||||
|
||||
test("uses default formatter", () => {
|
||||
expect({ x: 1, y: 2 }).toMatchSnapshot();
|
||||
});
|
||||
`,
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "test", "test.test.js", "--update-snapshots"],
|
||||
env: bunEnv,
|
||||
cwd: String(dir),
|
||||
stderr: "pipe",
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
expect(stderr).not.toContain("panic");
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
const snapshotContent = await Bun.file(`${dir}/__snapshots__/test.test.js.snap`).text();
|
||||
expect(snapshotContent).not.toContain("SHOULD_NOT_APPEAR");
|
||||
expect(snapshotContent).toContain("x");
|
||||
expect(snapshotContent).toContain("y");
|
||||
});
|
||||
|
||||
test("should throw error if serializer is not an object", async () => {
|
||||
using dir = tempDir("snapshot-serializer-invalid-object", {
|
||||
"test.test.js": `
|
||||
import { test, expect } from "bun:test";
|
||||
|
||||
try {
|
||||
expect.addSnapshotSerializer("not an object");
|
||||
console.log("FAIL: Should have thrown");
|
||||
} catch (e) {
|
||||
console.log("PASS: Threw error");
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "test.test.js"],
|
||||
env: bunEnv,
|
||||
cwd: String(dir),
|
||||
stderr: "pipe",
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
expect(stdout).toContain("PASS: Threw error");
|
||||
});
|
||||
|
||||
test("should throw error if serializer missing test function", async () => {
|
||||
using dir = tempDir("snapshot-serializer-no-test", {
|
||||
"test.test.js": `
|
||||
import { test, expect } from "bun:test";
|
||||
|
||||
try {
|
||||
expect.addSnapshotSerializer({
|
||||
print(val) {
|
||||
return String(val);
|
||||
}
|
||||
});
|
||||
console.log("FAIL: Should have thrown");
|
||||
} catch (e) {
|
||||
console.log("PASS: Threw error");
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "test.test.js"],
|
||||
env: bunEnv,
|
||||
cwd: String(dir),
|
||||
stderr: "pipe",
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
expect(stdout).toContain("PASS: Threw error");
|
||||
});
|
||||
|
||||
test("should throw error if serializer missing print/serialize function", async () => {
|
||||
using dir = tempDir("snapshot-serializer-no-print", {
|
||||
"test.test.js": `
|
||||
import { test, expect } from "bun:test";
|
||||
|
||||
try {
|
||||
expect.addSnapshotSerializer({
|
||||
test(val) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
console.log("FAIL: Should have thrown");
|
||||
} catch (e) {
|
||||
console.log("PASS: Threw error");
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "test.test.js"],
|
||||
env: bunEnv,
|
||||
cwd: String(dir),
|
||||
stderr: "pipe",
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
expect(stdout).toContain("PASS: Threw error");
|
||||
});
|
||||
|
||||
test("should work with inline snapshots", async () => {
|
||||
using dir = tempDir("snapshot-serializer-inline", {
|
||||
"test.test.js": `
|
||||
import { test, expect } from "bun:test";
|
||||
|
||||
class Color {
|
||||
constructor(r, g, b) {
|
||||
this.r = r;
|
||||
this.g = g;
|
||||
this.b = b;
|
||||
}
|
||||
}
|
||||
|
||||
expect.addSnapshotSerializer({
|
||||
test(val) {
|
||||
return val instanceof Color;
|
||||
},
|
||||
print(val) {
|
||||
return \`rgb(\${val.r}, \${val.g}, \${val.b})\`;
|
||||
}
|
||||
});
|
||||
|
||||
test("inline snapshot with serializer", () => {
|
||||
const color = new Color(255, 128, 0);
|
||||
expect(color).toMatchInlineSnapshot();
|
||||
});
|
||||
`,
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "test", "test.test.js", "--update-snapshots"],
|
||||
env: bunEnv,
|
||||
cwd: String(dir),
|
||||
stderr: "pipe",
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
expect(stderr).not.toContain("panic");
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
// Check that the test file was updated with the inline snapshot
|
||||
const testContent = await Bun.file(`${dir}/test.test.js`).text();
|
||||
expect(testContent).toContain("rgb(255, 128, 0)");
|
||||
});
|
||||
|
||||
test("should serialize top-level custom object", async () => {
|
||||
using dir = tempDir("snapshot-serializer-toplevel", {
|
||||
"test.test.js": `
|
||||
import { test, expect } from "bun:test";
|
||||
|
||||
class Container {
|
||||
constructor(items) {
|
||||
this.items = items;
|
||||
}
|
||||
}
|
||||
|
||||
expect.addSnapshotSerializer({
|
||||
test(val) {
|
||||
return val instanceof Container;
|
||||
},
|
||||
print(val) {
|
||||
return \`Container[\${val.items.length} items]\`;
|
||||
}
|
||||
});
|
||||
|
||||
test("top-level custom serializer", () => {
|
||||
const container = new Container([1, 2, 3]);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
`,
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "test", "test.test.js", "--update-snapshots"],
|
||||
env: bunEnv,
|
||||
cwd: String(dir),
|
||||
stderr: "pipe",
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
expect(stderr).not.toContain("panic");
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
const snapshotContent = await Bun.file(`${dir}/__snapshots__/test.test.js.snap`).text();
|
||||
expect(snapshotContent).toContain("Container[3 items]");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user