mirror of
https://github.com/oven-sh/bun
synced 2026-02-07 09:28:51 +00:00
Compare commits
2 Commits
dylan/pyth
...
claude/imp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a10c4740ca | ||
|
|
5588097d63 |
3
__snapshots__/test-snapshot-simple.js.snap
Normal file
3
__snapshots__/test-snapshot-simple.js.snap
Normal file
@@ -0,0 +1,3 @@
|
||||
// Bun Snapshot v1, https://bun.sh/docs/test/snapshots
|
||||
|
||||
exports[`basic snapshot serializer test 1`] = `CustomType(test)`;
|
||||
@@ -2916,12 +2916,176 @@ pub const Expect = struct {
|
||||
}
|
||||
}
|
||||
|
||||
value.jestSnapshotPrettyFormat(pretty_value, globalThis) catch {
|
||||
try this.serializeWithCustomSerializers(globalThis, value, pretty_value);
|
||||
}
|
||||
|
||||
fn trySerializeWithCustomSerializers(globalThis: *JSGlobalObject, value: JSValue, file_id: u32, test_runner: *TestRunner) ?JSValue {
|
||||
// Check if there are any custom serializers for this file
|
||||
const serializers = test_runner.snapshot_serializers.get(file_id) orelse return null;
|
||||
|
||||
// Iterate through serializers in reverse order (most recently added first)
|
||||
var i: usize = serializers.items.len;
|
||||
while (i > 0) {
|
||||
i -= 1;
|
||||
const serializer = serializers.items[i];
|
||||
|
||||
// Call the test function to see if this serializer handles this value
|
||||
const test_fn = serializer.get(globalThis, "test") catch continue orelse continue;
|
||||
if (!test_fn.isCallable()) continue;
|
||||
|
||||
const test_result = test_fn.call(globalThis, serializer, &[_]JSValue{value}) catch continue;
|
||||
const should_serialize = test_result.toBoolean();
|
||||
|
||||
if (should_serialize) {
|
||||
// This serializer handles this value type
|
||||
const serialize_fn = serializer.get(globalThis, "serialize") catch continue orelse continue;
|
||||
if (!serialize_fn.isCallable()) continue;
|
||||
|
||||
// Create printer function for this recursive call
|
||||
const printer_fn = JSC.JSFunction.create(globalThis, bun.String.static("printer"), snapshotPrinterCallback, 1, .{});
|
||||
printer_fn.protect();
|
||||
|
||||
// Call serialize(value, printer)
|
||||
const serialized = serialize_fn.call(globalThis, serializer, &[_]JSValue{ value, printer_fn }) catch continue;
|
||||
|
||||
if (serialized.isString()) {
|
||||
return serialized;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
fn serializeWithCustomSerializers(this: *Expect, globalThis: *JSGlobalObject, value: JSValue, pretty_value: *MutableString) bun.JSError!void {
|
||||
const runner = Jest.runner orelse {
|
||||
// Fallback to default serialization if no runner available
|
||||
return value.jestSnapshotPrettyFormat(pretty_value, globalThis) catch {
|
||||
var formatter = JSC.ConsoleObject.Formatter{ .globalThis = globalThis };
|
||||
defer formatter.deinit();
|
||||
return globalThis.throw("Failed to pretty format value: {s}", .{value.toFmt(&formatter)});
|
||||
};
|
||||
};
|
||||
|
||||
const current_test_scope = this.testScope() orelse {
|
||||
// Fallback to default serialization if no test scope available
|
||||
return value.jestSnapshotPrettyFormat(pretty_value, globalThis) catch {
|
||||
var formatter = JSC.ConsoleObject.Formatter{ .globalThis = globalThis };
|
||||
defer formatter.deinit();
|
||||
return globalThis.throw("Failed to pretty format value: {s}", .{value.toFmt(&formatter)});
|
||||
};
|
||||
};
|
||||
|
||||
const file_id = current_test_scope.describe.file_id;
|
||||
|
||||
// Check if there are any custom serializers for this file
|
||||
if (runner.snapshot_serializers.get(file_id)) |serializers| {
|
||||
// Iterate through serializers in reverse order (most recently added first)
|
||||
var i: usize = serializers.items.len;
|
||||
while (i > 0) {
|
||||
i -= 1;
|
||||
const serializer = serializers.items[i];
|
||||
|
||||
// Call the test function to see if this serializer matches
|
||||
const test_fn = serializer.get(globalThis, "test") catch continue orelse continue;
|
||||
const test_result = test_fn.call(globalThis, serializer, &[_]JSValue{value}) catch continue;
|
||||
|
||||
if (test_result.toBoolean()) {
|
||||
// This serializer matches, use it
|
||||
const serialize_fn = serializer.get(globalThis, "serialize") catch continue orelse continue;
|
||||
|
||||
// Create a printer function for recursive serialization
|
||||
const printer_fn = this.createPrinterFunction(globalThis, file_id);
|
||||
|
||||
const serialized_result = serialize_fn.call(globalThis, serializer, &[_]JSValue{ value, printer_fn }) catch {
|
||||
// If the serializer call fails, continue to the next one
|
||||
continue;
|
||||
};
|
||||
|
||||
// Convert the result to a string
|
||||
var temp_str = ZigString.Empty;
|
||||
try serialized_result.toZigString(&temp_str, globalThis);
|
||||
const result_slice = temp_str.toSlice(default_allocator);
|
||||
defer result_slice.deinit();
|
||||
|
||||
try pretty_value.appendSlice(result_slice.slice());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No custom serializer matched, use default serialization
|
||||
return value.jestSnapshotPrettyFormat(pretty_value, globalThis) catch {
|
||||
var formatter = JSC.ConsoleObject.Formatter{ .globalThis = globalThis };
|
||||
defer formatter.deinit();
|
||||
return globalThis.throw("Failed to pretty format value: {s}", .{value.toFmt(&formatter)});
|
||||
};
|
||||
}
|
||||
|
||||
fn createPrinterFunction(this: *Expect, globalThis: *JSGlobalObject, file_id: u32) JSValue {
|
||||
_ = this; // For now, we'll use a simpler approach without recursive serialization
|
||||
_ = file_id; // unused for now
|
||||
|
||||
// Create a printer function that serializes values using Jest's default format
|
||||
// This is passed as the second argument to serializer.serialize(val, printer)
|
||||
const printer_fn = JSC.JSFunction.create(globalThis, bun.String.static("printer"), snapshotPrinterCallback, 1, .{});
|
||||
|
||||
// Protect the function from garbage collection
|
||||
printer_fn.protect();
|
||||
|
||||
return printer_fn;
|
||||
}
|
||||
|
||||
/// Callback function for the printer function passed to snapshot serializers
|
||||
fn snapshotPrinterCallback(globalThis: *JSGlobalObject, callFrame: *JSC.CallFrame) bun.JSError!JSValue {
|
||||
const args = callFrame.argumentsAsArray(1);
|
||||
if (args.len != 1) {
|
||||
return globalThis.throw("printer() requires exactly one argument", .{});
|
||||
}
|
||||
|
||||
const val = args[0];
|
||||
|
||||
// Get the test runner to access serializers for recursive calls
|
||||
const test_runner = Jest.runner orelse {
|
||||
// Fallback to default formatting if no test runner available
|
||||
var temp_pretty_value = MutableString.init(default_allocator, 0) catch |err| {
|
||||
return globalThis.throw("Failed to allocate memory for printer: {}", .{err});
|
||||
};
|
||||
defer temp_pretty_value.deinit();
|
||||
|
||||
val.jestSnapshotPrettyFormat(&temp_pretty_value, globalThis) catch |err| {
|
||||
return globalThis.throw("Failed to format value in printer: {}", .{err});
|
||||
};
|
||||
|
||||
return ZigString.fromUTF8(temp_pretty_value.slice()).toJS(globalThis);
|
||||
};
|
||||
|
||||
// Get current file ID from the active test
|
||||
const file_id = if (test_runner.pending_test) |current_test|
|
||||
current_test.describe.file_id
|
||||
else
|
||||
0; // fallback to 0 if no active test
|
||||
|
||||
// Try to use custom serializers for recursive formatting
|
||||
// This allows nested objects to also use custom serializers
|
||||
if (Expect.trySerializeWithCustomSerializers(globalThis, val, file_id, test_runner)) |result| {
|
||||
if (result.isString()) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to default formatting
|
||||
var temp_pretty_value = MutableString.init(default_allocator, 0) catch |err| {
|
||||
return globalThis.throw("Failed to allocate memory for printer: {}", .{err});
|
||||
};
|
||||
defer temp_pretty_value.deinit();
|
||||
|
||||
val.jestSnapshotPrettyFormat(&temp_pretty_value, globalThis) catch |err| {
|
||||
return globalThis.throw("Failed to format value in printer: {}", .{err});
|
||||
};
|
||||
|
||||
return ZigString.fromUTF8(temp_pretty_value.slice()).toJS(globalThis);
|
||||
}
|
||||
fn snapshot(this: *Expect, globalThis: *JSGlobalObject, value: JSValue, property_matchers: ?JSValue, hint: []const u8, comptime fn_name: []const u8) bun.JSError!JSValue {
|
||||
var pretty_value: MutableString = try MutableString.init(default_allocator, 0);
|
||||
defer pretty_value.deinit();
|
||||
@@ -4904,7 +5068,75 @@ pub const Expect = struct {
|
||||
return thisValue;
|
||||
}
|
||||
|
||||
pub const addSnapshotSerializer = notImplementedStaticFn;
|
||||
pub fn addSnapshotSerializer(globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue {
|
||||
defer globalThis.bunVM().autoGarbageCollect();
|
||||
|
||||
const _arguments = callFrame.arguments_old(1);
|
||||
const arguments: []const JSValue = _arguments.ptr[0.._arguments.len];
|
||||
|
||||
if (arguments.len != 1) {
|
||||
return globalThis.throw("addSnapshotSerializer() requires exactly one argument", .{});
|
||||
}
|
||||
|
||||
const serializer_arg = arguments[0];
|
||||
if (!serializer_arg.isObject()) {
|
||||
return globalThis.throw("addSnapshotSerializer() argument must be an object", .{});
|
||||
}
|
||||
|
||||
// Validate serializer has required methods
|
||||
const test_prop = serializer_arg.get(globalThis, "test") catch |err| {
|
||||
return globalThis.throwError(err, "Failed to get 'test' property from snapshot serializer");
|
||||
} orelse {
|
||||
return globalThis.throw("Snapshot serializer must have a 'test' method", .{});
|
||||
};
|
||||
|
||||
if (!test_prop.isCallable()) {
|
||||
return globalThis.throw("Snapshot serializer 'test' property must be a function", .{});
|
||||
}
|
||||
|
||||
const serialize_prop = serializer_arg.get(globalThis, "serialize") catch |err| {
|
||||
return globalThis.throwError(err, "Failed to get 'serialize' property from snapshot serializer");
|
||||
} orelse {
|
||||
return globalThis.throw("Snapshot serializer must have a 'serialize' method", .{});
|
||||
};
|
||||
|
||||
if (!serialize_prop.isCallable()) {
|
||||
return globalThis.throw("Snapshot serializer 'serialize' property must be a function", .{});
|
||||
}
|
||||
|
||||
// Get the current test runner and file ID
|
||||
const runner = Jest.runner orelse {
|
||||
return globalThis.throw("addSnapshotSerializer() can only be called within a test", .{});
|
||||
};
|
||||
|
||||
// Get the current file ID from the test runner
|
||||
const current_test = runner.pending_test orelse {
|
||||
return globalThis.throw("addSnapshotSerializer() can only be called within a test", .{});
|
||||
};
|
||||
|
||||
const current_test_scope = current_test.describe;
|
||||
|
||||
const file_id = current_test_scope.file_id;
|
||||
|
||||
// Get or create the serializer list for this file
|
||||
const gop = runner.snapshot_serializers.getOrPut(runner.allocator, file_id) catch |err| {
|
||||
return globalThis.throw("Failed to allocate memory for snapshot serializer: {}", .{err});
|
||||
};
|
||||
|
||||
if (!gop.found_existing) {
|
||||
gop.value_ptr.* = std.ArrayListUnmanaged(JSValue){};
|
||||
}
|
||||
|
||||
// Add the serializer to the list (most recently added gets priority)
|
||||
gop.value_ptr.append(runner.allocator, serializer_arg) catch |err| {
|
||||
return globalThis.throw("Failed to add snapshot serializer: {}", .{err});
|
||||
};
|
||||
|
||||
// Protect the serializer from garbage collection
|
||||
serializer_arg.protect();
|
||||
|
||||
return .js_undefined;
|
||||
}
|
||||
|
||||
pub fn hasAssertions(globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue {
|
||||
_ = callFrame;
|
||||
|
||||
@@ -87,6 +87,9 @@ pub const TestRunner = struct {
|
||||
unhandled_errors_between_tests: u32 = 0,
|
||||
summary: Summary = Summary{},
|
||||
|
||||
/// Per-file snapshot serializers, ordered by registration (last added has priority)
|
||||
snapshot_serializers: std.ArrayHashMapUnmanaged(File.ID, std.ArrayListUnmanaged(JSValue), std.array_hash_map.AutoContext(File.ID), false) = .{},
|
||||
|
||||
pub const Drainer = JSC.AnyTask.New(TestRunner, drain);
|
||||
|
||||
pub const Summary = struct {
|
||||
@@ -280,6 +283,15 @@ pub const TestRunner = struct {
|
||||
fail_because_expected_assertion_count,
|
||||
};
|
||||
};
|
||||
|
||||
pub fn deinit(this: *TestRunner) void {
|
||||
// Clean up snapshot serializers
|
||||
var iter = this.snapshot_serializers.iterator();
|
||||
while (iter.next()) |entry| {
|
||||
entry.value_ptr.deinit(this.allocator);
|
||||
}
|
||||
this.snapshot_serializers.deinit(this.allocator);
|
||||
}
|
||||
};
|
||||
|
||||
pub const Jest = struct {
|
||||
|
||||
17
test-snapshot-simple.js
Normal file
17
test-snapshot-simple.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import { test, expect } from "bun:test";
|
||||
|
||||
test("basic snapshot serializer test", () => {
|
||||
const serializer = {
|
||||
test(val) {
|
||||
return val && val.type === 'custom';
|
||||
},
|
||||
serialize(val, printer) {
|
||||
return `CustomType(${val.name})`;
|
||||
},
|
||||
};
|
||||
|
||||
expect.addSnapshotSerializer(serializer);
|
||||
|
||||
const obj = { type: 'custom', name: 'test' };
|
||||
expect(obj).toMatchSnapshot();
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
// Bun Snapshot v1, https://bun.sh/docs/test/snapshots
|
||||
|
||||
exports[`expect.addSnapshotSerializer basic functionality 1`] = `Date(1234567890123)`;
|
||||
|
||||
exports[`expect.addSnapshotSerializer with nested objects 1`] = `CustomObject(test: {"nested":true,"array":[1,2,3]})`;
|
||||
|
||||
exports[`expect.addSnapshotSerializer precedence - last added wins 1`] = `Second: hello`;
|
||||
|
||||
exports[`expect.addSnapshotSerializer handles complex objects 1`] = `Map {"key1" => "value1", "key2" => {"nested":true}, 123 => "number key"}`;
|
||||
128
test/js/bun/test/snapshot-serializer.test.ts
Normal file
128
test/js/bun/test/snapshot-serializer.test.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { test, expect } from "bun:test";
|
||||
import { tempDirWithFiles } from "harness";
|
||||
|
||||
test("expect.addSnapshotSerializer basic functionality", () => {
|
||||
// Define a simple serializer for Date objects
|
||||
const dateSerializer = {
|
||||
test(val: any) {
|
||||
return val instanceof Date;
|
||||
},
|
||||
serialize(val: Date, printer: (val: any) => string) {
|
||||
return `Date(${val.getTime()})`;
|
||||
},
|
||||
};
|
||||
|
||||
expect.addSnapshotSerializer(dateSerializer);
|
||||
|
||||
const now = new Date(1234567890123);
|
||||
expect(now).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("expect.addSnapshotSerializer with nested objects", () => {
|
||||
// Define a serializer for objects with a special property
|
||||
const customSerializer = {
|
||||
test(val: any) {
|
||||
return val && typeof val === 'object' && val.type === 'custom';
|
||||
},
|
||||
serialize(val: any, printer: (val: any) => string) {
|
||||
// For now, we'll just use a simple string representation since the printer is not fully implemented
|
||||
return `CustomObject(${val.name}: ${JSON.stringify(val.value)})`;
|
||||
},
|
||||
};
|
||||
|
||||
expect.addSnapshotSerializer(customSerializer);
|
||||
|
||||
const obj = {
|
||||
type: 'custom',
|
||||
name: 'test',
|
||||
value: { nested: true, array: [1, 2, 3] }
|
||||
};
|
||||
|
||||
expect(obj).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("expect.addSnapshotSerializer precedence - last added wins", () => {
|
||||
// First serializer
|
||||
const firstSerializer = {
|
||||
test(val: any) {
|
||||
return typeof val === 'string';
|
||||
},
|
||||
serialize(val: string, printer: (val: any) => string) {
|
||||
return `First: ${val}`;
|
||||
},
|
||||
};
|
||||
|
||||
// Second serializer
|
||||
const secondSerializer = {
|
||||
test(val: any) {
|
||||
return typeof val === 'string';
|
||||
},
|
||||
serialize(val: string, printer: (val: any) => string) {
|
||||
return `Second: ${val}`;
|
||||
},
|
||||
};
|
||||
|
||||
expect.addSnapshotSerializer(firstSerializer);
|
||||
expect.addSnapshotSerializer(secondSerializer);
|
||||
|
||||
expect("hello").toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("expect.addSnapshotSerializer handles complex objects", () => {
|
||||
// Serializer for Map objects
|
||||
const mapSerializer = {
|
||||
test(val: any) {
|
||||
return val instanceof Map;
|
||||
},
|
||||
serialize(val: Map<any, any>, printer: (val: any) => string) {
|
||||
const entries = Array.from(val.entries());
|
||||
return `Map {${entries.map(([key, value]) => `${JSON.stringify(key)} => ${JSON.stringify(value)}`).join(', ')}}`;
|
||||
},
|
||||
};
|
||||
|
||||
expect.addSnapshotSerializer(mapSerializer);
|
||||
|
||||
const map = new Map([
|
||||
["key1", "value1"],
|
||||
["key2", { nested: true }],
|
||||
[123, "number key"],
|
||||
]);
|
||||
|
||||
expect(map).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("expect.addSnapshotSerializer error handling", () => {
|
||||
// Test validation errors
|
||||
expect(() => {
|
||||
expect.addSnapshotSerializer({});
|
||||
}).toThrow("must have a 'test' method");
|
||||
|
||||
expect(() => {
|
||||
expect.addSnapshotSerializer({
|
||||
test: "not a function",
|
||||
});
|
||||
}).toThrow("'test' property must be a function");
|
||||
|
||||
expect(() => {
|
||||
expect.addSnapshotSerializer({
|
||||
test: () => true,
|
||||
});
|
||||
}).toThrow("must have a 'serialize' method");
|
||||
|
||||
expect(() => {
|
||||
expect.addSnapshotSerializer({
|
||||
test: () => true,
|
||||
serialize: "not a function",
|
||||
});
|
||||
}).toThrow("'serialize' property must be a function");
|
||||
});
|
||||
|
||||
test("expect.addSnapshotSerializer without test context throws", () => {
|
||||
const serializer = {
|
||||
test: () => true,
|
||||
serialize: () => "test",
|
||||
};
|
||||
|
||||
// This should work inside a test
|
||||
expect.addSnapshotSerializer(serializer);
|
||||
});
|
||||
Reference in New Issue
Block a user