implement toThrowErrorMatchingSnapshot, toThrowErrorMatchingInlineSnapshot (#15607)

This commit is contained in:
pfg
2024-12-05 19:07:18 -08:00
committed by GitHub
parent eacf89e5bf
commit 1476e4c958
11 changed files with 398 additions and 149 deletions

View File

@@ -536,12 +536,12 @@ Bun implements the following matchers. Full Jest compatibility is on the roadmap
---
-
-
- [`.toThrowErrorMatchingSnapshot()`](https://jestjs.io/docs/expect#tothrowerrormatchingsnapshothint)
---
-
-
- [`.toThrowErrorMatchingInlineSnapshot()`](https://jestjs.io/docs/expect#tothrowerrormatchinginlinesnapshotinlinesnapshot)
{% /table %}

View File

@@ -1299,15 +1299,17 @@ declare module "bun:test" {
* Asserts that a value matches the most recent inline snapshot.
*
* @example
* expect("Hello").toMatchInlineSnapshot();
* expect("Hello").toMatchInlineSnapshot(`"Hello"`);
* @param value The latest snapshot value.
*
* @param value The latest automatically-updated snapshot value.
*/
toMatchInlineSnapshot(value?: string): void;
/**
* Asserts that a value matches the most recent inline snapshot.
*
* @example
* expect("Hello").toMatchInlineSnapshot(`"Hello"`);
* expect({ c: new Date() }).toMatchInlineSnapshot({ c: expect.any(Date) });
* expect({ c: new Date() }).toMatchInlineSnapshot({ c: expect.any(Date) }, `
* {
* "v": Any<Date>,
@@ -1315,9 +1317,35 @@ declare module "bun:test" {
* `);
*
* @param propertyMatchers Object containing properties to match against the value.
* @param hint Hint used to identify the snapshot in the snapshot file.
* @param value The latest automatically-updated snapshot value.
*/
toMatchInlineSnapshot(propertyMatchers?: object, value?: string): void;
/**
* Asserts that a function throws an error matching the most recent snapshot.
*
* @example
* function fail() {
* throw new Error("Oops!");
* }
* expect(fail).toThrowErrorMatchingSnapshot();
* expect(fail).toThrowErrorMatchingSnapshot("This one should say Oops!");
*
* @param value The latest automatically-updated snapshot value.
*/
toThrowErrorMatchingSnapshot(hint?: string): void;
/**
* Asserts that a function throws an error matching the most recent snapshot.
*
* @example
* function fail() {
* throw new Error("Oops!");
* }
* expect(fail).toThrowErrorMatchingInlineSnapshot();
* expect(fail).toThrowErrorMatchingInlineSnapshot(`"Oops!"`);
*
* @param value The latest automatically-updated snapshot value.
*/
toThrowErrorMatchingInlineSnapshot(value?: string): void;
/**
* Asserts that an object matches a subset of properties.
*

View File

@@ -74,7 +74,7 @@ JSC::JSValue generateModule(JSC::JSGlobalObject* globalObject, JSC::VM& vm, cons
return result;
}
#if BUN_DYNAMIC_JS_LOAD_PATH
#ifdef BUN_DYNAMIC_JS_LOAD_PATH
JSValue initializeInternalModuleFromDisk(
JSGlobalObject* globalObject,
VM& vm,

View File

@@ -6032,11 +6032,10 @@ CPP_DECL bool Bun__CallFrame__isFromBunMain(JSC::CallFrame* callFrame, JSC::VM*
return source.string() == "builtin://bun/main"_s;
}
CPP_DECL void Bun__CallFrame__getCallerSrcLoc(JSC::CallFrame* callFrame, JSC::JSGlobalObject* globalObject, unsigned int* outSourceID, unsigned int* outLine, unsigned int* outColumn)
CPP_DECL void Bun__CallFrame__getCallerSrcLoc(JSC::CallFrame* callFrame, JSC::JSGlobalObject* globalObject, BunString* outSourceURL, unsigned int* outLine, unsigned int* outColumn)
{
JSC::VM& vm = globalObject->vm();
JSC::LineColumn lineColumn;
JSC::SourceID sourceID = 0;
String sourceURL;
ZigStackFrame remappedFrame = {};
@@ -6046,6 +6045,7 @@ CPP_DECL void Bun__CallFrame__getCallerSrcLoc(JSC::CallFrame* callFrame, JSC::JS
return WTF::IterationStatus::Continue;
if (visitor->hasLineAndColumnInfo()) {
lineColumn = visitor->computeLineAndColumn();
String sourceURLForFrame = visitor->sourceURL();
@@ -6071,8 +6071,6 @@ CPP_DECL void Bun__CallFrame__getCallerSrcLoc(JSC::CallFrame* callFrame, JSC::JS
sourceURLForFrame = origin.string();
}
}
sourceID = provider->asID();
}
}
@@ -6099,7 +6097,7 @@ CPP_DECL void Bun__CallFrame__getCallerSrcLoc(JSC::CallFrame* callFrame, JSC::JS
lineColumn.column = OrdinalNumber::fromZeroBasedInt(remappedFrame.position.column_zero_based).oneBasedInt();
}
*outSourceID = sourceID;
*outSourceURL = Bun::toStringRef(sourceURL);
*outLine = lineColumn.line;
*outColumn = lineColumn.column;
}

View File

@@ -6666,19 +6666,19 @@ pub const CallFrame = opaque {
return value;
}
extern fn Bun__CallFrame__getCallerSrcLoc(*const CallFrame, *JSGlobalObject, *c_uint, *c_uint, *c_uint) void;
extern fn Bun__CallFrame__getCallerSrcLoc(*const CallFrame, *JSGlobalObject, *bun.String, *c_uint, *c_uint) void;
pub const CallerSrcLoc = struct {
source_file_id: c_uint,
str: bun.String,
line: c_uint,
column: c_uint,
};
pub fn getCallerSrcLoc(call_frame: *const CallFrame, globalThis: *JSGlobalObject) CallerSrcLoc {
var source_id: c_uint = undefined;
var str: bun.String = undefined;
var line: c_uint = undefined;
var column: c_uint = undefined;
Bun__CallFrame__getCallerSrcLoc(call_frame, globalThis, &source_id, &line, &column);
Bun__CallFrame__getCallerSrcLoc(call_frame, globalThis, &str, &line, &column);
return .{
.source_file_id = source_id,
.str = str,
.line = line,
.column = column,
};

View File

@@ -2152,7 +2152,6 @@ pub const Expect = struct {
pub fn toThrow(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue {
defer this.postMatch(globalThis);
const vm = globalThis.bunVM();
const thisValue = callFrame.this();
const arguments = callFrame.argumentsAsArray(1);
@@ -2178,63 +2177,9 @@ pub const Expect = struct {
};
expected_value.ensureStillAlive();
const value: JSValue = try this.getValue(globalThis, thisValue, "toThrow", "<green>expected<r>");
const not = this.flags.not;
var return_value_from_function: JSValue = .zero;
const result_: ?JSValue = brk: {
if (!value.jsType().isFunction()) {
if (this.flags.promise != .none) {
break :brk value;
}
return globalThis.throw("Expected value must be a function", .{});
}
var return_value: JSValue = .zero;
// Drain existing unhandled rejections
vm.global.handleRejectedPromises();
var scope = vm.unhandledRejectionScope();
const prev_unhandled_pending_rejection_to_capture = vm.unhandled_pending_rejection_to_capture;
vm.unhandled_pending_rejection_to_capture = &return_value;
vm.onUnhandledRejection = &VirtualMachine.onQuietUnhandledRejectionHandlerCaptureValue;
return_value_from_function = value.call(globalThis, .undefined, &.{}) catch |err| globalThis.takeException(err);
vm.unhandled_pending_rejection_to_capture = prev_unhandled_pending_rejection_to_capture;
vm.global.handleRejectedPromises();
if (return_value == .zero) {
return_value = return_value_from_function;
}
if (return_value.asAnyPromise()) |promise| {
vm.waitForPromise(promise);
scope.apply(vm);
switch (promise.unwrap(globalThis.vm(), .mark_handled)) {
.fulfilled => {
break :brk null;
},
.rejected => |rejected| {
// since we know for sure it rejected, we should always return the error
break :brk rejected.toError() orelse rejected;
},
.pending => unreachable,
}
}
if (return_value != return_value_from_function) {
if (return_value_from_function.asAnyPromise()) |existing| {
existing.setHandled(globalThis.vm());
}
}
scope.apply(vm);
break :brk return_value.toError() orelse return_value_from_function.toError();
};
const result_, const return_value_from_function = try this.getValueAsToThrow(globalThis, try this.getValue(globalThis, thisValue, "toThrow", "<green>expected<r>"));
const did_throw = result_ != null;
@@ -2506,10 +2451,152 @@ pub const Expect = struct {
expected_value.getClassName(globalThis, &expected_class);
return this.throw(globalThis, signature, expected_fmt, .{ expected_class, result.toFmt(&formatter) });
}
pub fn toMatchInlineSnapshot(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue {
// in jest, a failing inline snapshot does not block the rest from running
// not sure why - empty snapshots will autofill and with the `-u` flag none will fail
fn getValueAsToThrow(this: *Expect, globalThis: *JSGlobalObject, value: JSValue) bun.JSError!struct { ?JSValue, JSValue } {
const vm = globalThis.bunVM();
var return_value_from_function: JSValue = .zero;
if (!value.jsType().isFunction()) {
if (this.flags.promise != .none) {
return .{ value, return_value_from_function };
}
return globalThis.throw("Expected value must be a function", .{});
}
var return_value: JSValue = .zero;
// Drain existing unhandled rejections
vm.global.handleRejectedPromises();
var scope = vm.unhandledRejectionScope();
const prev_unhandled_pending_rejection_to_capture = vm.unhandled_pending_rejection_to_capture;
vm.unhandled_pending_rejection_to_capture = &return_value;
vm.onUnhandledRejection = &VirtualMachine.onQuietUnhandledRejectionHandlerCaptureValue;
return_value_from_function = value.call(globalThis, .undefined, &.{}) catch |err| globalThis.takeException(err);
vm.unhandled_pending_rejection_to_capture = prev_unhandled_pending_rejection_to_capture;
vm.global.handleRejectedPromises();
if (return_value == .zero) {
return_value = return_value_from_function;
}
if (return_value.asAnyPromise()) |promise| {
vm.waitForPromise(promise);
scope.apply(vm);
switch (promise.unwrap(globalThis.vm(), .mark_handled)) {
.fulfilled => {
return .{ null, return_value_from_function };
},
.rejected => |rejected| {
// since we know for sure it rejected, we should always return the error
return .{ rejected.toError() orelse rejected, return_value_from_function };
},
.pending => unreachable,
}
}
if (return_value != return_value_from_function) {
if (return_value_from_function.asAnyPromise()) |existing| {
existing.setHandled(globalThis.vm());
}
}
scope.apply(vm);
return .{ return_value.toError() orelse return_value_from_function.toError(), return_value_from_function };
}
pub fn toThrowErrorMatchingSnapshot(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue {
defer this.postMatch(globalThis);
const thisValue = callFrame.this();
const _arguments = callFrame.arguments_old(2);
const arguments: []const JSValue = _arguments.ptr[0.._arguments.len];
incrementExpectCallCounter();
const not = this.flags.not;
if (not) {
const signature = comptime getSignature("toThrowErrorMatchingSnapshot", "", true);
return this.throw(globalThis, signature, "\n\n<b>Matcher error<r>: Snapshot matchers cannot be used with <b>not<r>\n", .{});
}
if (this.testScope() == null) {
const signature = comptime getSignature("toThrowErrorMatchingSnapshot", "", true);
return this.throw(globalThis, signature, "\n\n<b>Matcher error<r>: Snapshot matchers cannot be used outside of a test\n", .{});
}
var hint_string: ZigString = ZigString.Empty;
switch (arguments.len) {
0 => {},
1 => {
if (arguments[0].isString()) {
arguments[0].toZigString(&hint_string, globalThis);
} else {
return this.throw(globalThis, "", "\n\nMatcher error: Expected first argument to be a string\n", .{});
}
},
else => return this.throw(globalThis, "", "\n\nMatcher error: Expected zero or one arguments\n", .{}),
}
var hint = hint_string.toSlice(default_allocator);
defer hint.deinit();
const value: JSValue = try this.fnToErrStringOrUndefined(globalThis, try this.getValue(globalThis, thisValue, "toThrowErrorMatchingSnapshot", "<green>properties<r><d>, <r>hint"));
return this.snapshot(globalThis, value, null, hint.slice(), "toThrowErrorMatchingSnapshot");
}
fn fnToErrStringOrUndefined(this: *Expect, globalThis: *JSGlobalObject, value: JSValue) !JSValue {
const err_value, _ = try this.getValueAsToThrow(globalThis, value);
var err_value_res = err_value orelse JSValue.undefined;
if (err_value_res.isAnyError()) {
const message = try err_value_res.getTruthyComptime(globalThis, "message") orelse JSValue.undefined;
err_value_res = message;
} else {
err_value_res = JSValue.undefined;
}
return err_value_res;
}
pub fn toThrowErrorMatchingInlineSnapshot(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue {
defer this.postMatch(globalThis);
const thisValue = callFrame.this();
const _arguments = callFrame.arguments_old(2);
const arguments: []const JSValue = _arguments.ptr[0.._arguments.len];
incrementExpectCallCounter();
const not = this.flags.not;
if (not) {
const signature = comptime getSignature("toThrowErrorMatchingInlineSnapshot", "", true);
return this.throw(globalThis, signature, "\n\n<b>Matcher error<r>: Snapshot matchers cannot be used with <b>not<r>\n", .{});
}
var has_expected = false;
var expected_string: ZigString = ZigString.Empty;
switch (arguments.len) {
0 => {},
1 => {
if (arguments[0].isString()) {
has_expected = true;
arguments[0].toZigString(&expected_string, globalThis);
} else {
return this.throw(globalThis, "", "\n\nMatcher error: Expected first argument to be a string\n", .{});
}
},
else => return this.throw(globalThis, "", "\n\nMatcher error: Expected zero or one arguments\n", .{}),
}
var expected = expected_string.toSlice(default_allocator);
defer expected.deinit();
const expected_slice: ?[]const u8 = if (has_expected) expected.slice() else null;
const value: JSValue = try this.fnToErrStringOrUndefined(globalThis, try this.getValue(globalThis, thisValue, "toThrowErrorMatchingInlineSnapshot", "<green>properties<r><d>, <r>hint"));
return this.inlineSnapshot(globalThis, callFrame, value, null, expected_slice, "toThrowErrorMatchingInlineSnapshot");
}
pub fn toMatchInlineSnapshot(this: *Expect, globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue {
defer this.postMatch(globalThis);
const thisValue = callFrame.this();
const _arguments = callFrame.arguments_old(2);
@@ -2556,48 +2643,43 @@ pub const Expect = struct {
var expected = expected_string.toSlice(default_allocator);
defer expected.deinit();
const value: JSValue = try this.getValue(globalThis, thisValue, "toMatchInlineSnapshot", "<green>properties<r><d>, <r>hint");
const expected_slice: ?[]const u8 = if (has_expected) expected.slice() else null;
if (!value.isObject() and property_matchers != null) {
const signature = comptime getSignature("toMatchInlineSnapshot", "<green>properties<r><d>, <r>hint", false);
return this.throw(globalThis, signature, "\n\n<b>Matcher error: <red>received<r> values must be an object when the matcher has <green>properties<r>\n", .{});
}
if (property_matchers) |_prop_matchers| {
const prop_matchers = _prop_matchers;
if (!value.jestDeepMatch(prop_matchers, globalThis, true)) {
// TODO: print diff with properties from propertyMatchers
const signature = comptime getSignature("toMatchInlineSnapshot", "<green>propertyMatchers<r>", false);
const fmt = signature ++ "\n\nExpected <green>propertyMatchers<r> to match properties from received object" ++
"\n\nReceived: {any}\n";
var formatter = JSC.ConsoleObject.Formatter{ .globalThis = globalThis };
return globalThis.throwPretty(fmt, .{value.toFmt(&formatter)});
}
}
const result: ?[]const u8 = if (has_expected) expected.byteSlice() else null;
const value = try this.getValue(globalThis, thisValue, "toMatchInlineSnapshot", "<green>properties<r><d>, <r>hint");
return this.inlineSnapshot(globalThis, callFrame, value, property_matchers, expected_slice, "toMatchInlineSnapshot");
}
fn inlineSnapshot(
this: *Expect,
globalThis: *JSGlobalObject,
callFrame: *CallFrame,
value: JSValue,
property_matchers: ?JSValue,
result: ?[]const u8,
comptime fn_name: []const u8,
) bun.JSError!JSValue {
// jest counts inline snapshots towards the snapshot counter for some reason
_ = Jest.runner.?.snapshots.addCount(this, "") catch |e| switch (e) {
error.OutOfMemory => return error.OutOfMemory,
error.NoTest => {},
};
const update = Jest.runner.?.snapshots.update_snapshots;
var needs_write = false;
var pretty_value: MutableString = MutableString.init(default_allocator, 0) catch unreachable;
value.jestSnapshotPrettyFormat(&pretty_value, globalThis) catch {
var formatter = JSC.ConsoleObject.Formatter{ .globalThis = globalThis };
return globalThis.throw("Failed to pretty format value: {s}", .{value.toFmt(&formatter)});
};
var pretty_value: MutableString = try MutableString.init(default_allocator, 0);
defer pretty_value.deinit();
try this.matchAndFmtSnapshot(globalThis, value, property_matchers, &pretty_value, fn_name);
if (result) |saved_value| {
if (strings.eqlLong(pretty_value.slice(), saved_value, true)) {
Jest.runner.?.snapshots.passed += 1;
return .undefined;
} else if (update) {
Jest.runner.?.snapshots.passed += 1;
needs_write = true;
} else {
Jest.runner.?.snapshots.failed += 1;
const signature = comptime getSignature("toMatchInlineSnapshot", "<green>expected<r>", false);
const signature = comptime getSignature(fn_name, "<green>expected<r>", false);
const fmt = signature ++ "\n\n{any}\n";
const diff_format = DiffFormatter{
.received_string = pretty_value.slice(),
@@ -2613,24 +2695,40 @@ pub const Expect = struct {
if (needs_write) {
if (this.testScope() == null) {
const signature = comptime getSignature("toMatchSnapshot", "", true);
const signature = comptime getSignature(fn_name, "", true);
return this.throw(globalThis, signature, "\n\n<b>Matcher error<r>: Snapshot matchers cannot be used outside of a test\n", .{});
}
// 1. find the src loc of the snapshot
const srcloc = callFrame.getCallerSrcLoc(globalThis);
defer srcloc.str.deref();
const describe = this.testScope().?.describe;
const fget = Jest.runner.?.files.get(describe.file_id);
if (srcloc.source_file_id != this.testScope().?.describe.file_id) {
const signature = comptime getSignature("toMatchSnapshot", "", true);
return this.throw(globalThis, signature, "\n\n<b>Matcher error<r>: Inline snapshot matchers must be called from the same file as the test\n", .{});
if (!srcloc.str.eqlUTF8(fget.source.path.text)) {
const signature = comptime getSignature(fn_name, "", true);
return this.throw(globalThis, signature,
\\
\\
\\<b>Matcher error<r>: Inline snapshot matchers must be called from the test file:
\\ Expected to be called from file: <green>"{}"<r>
\\ {s} called from file: <red>"{}"<r>
\\
, .{
std.zig.fmtEscapes(fget.source.path.text),
fn_name,
std.zig.fmtEscapes(srcloc.str.toUTF8(Jest.runner.?.snapshots.allocator).slice()),
});
}
// 2. save to write later
try Jest.runner.?.snapshots.addInlineSnapshotToWrite(srcloc.source_file_id, .{
try Jest.runner.?.snapshots.addInlineSnapshotToWrite(describe.file_id, .{
.line = srcloc.line,
.col = srcloc.column,
.value = pretty_value.toOwnedSlice(),
.has_matchers = property_matchers != null,
.is_added = result == null,
.kind = fn_name,
});
}
@@ -2689,17 +2787,20 @@ pub const Expect = struct {
const value: JSValue = try this.getValue(globalThis, thisValue, "toMatchSnapshot", "<green>properties<r><d>, <r>hint");
if (!value.isObject() and property_matchers != null) {
const signature = comptime getSignature("toMatchSnapshot", "<green>properties<r><d>, <r>hint", false);
return this.throw(globalThis, signature, "\n\n<b>Matcher error: <red>received<r> values must be an object when the matcher has <green>properties<r>\n", .{});
}
return this.snapshot(globalThis, value, property_matchers, hint.slice(), "toMatchSnapshot");
}
fn matchAndFmtSnapshot(this: *Expect, globalThis: *JSGlobalObject, value: JSValue, property_matchers: ?JSValue, pretty_value: *MutableString, comptime fn_name: []const u8) bun.JSError!void {
if (property_matchers) |_prop_matchers| {
if (!value.isObject()) {
const signature = comptime getSignature(fn_name, "<green>properties<r><d>, <r>hint", false);
return this.throw(globalThis, signature, "\n\n<b>Matcher error: <red>received<r> values must be an object when the matcher has <green>properties<r>\n", .{});
}
const prop_matchers = _prop_matchers;
if (!value.jestDeepMatch(prop_matchers, globalThis, true)) {
// TODO: print diff with properties from propertyMatchers
const signature = comptime getSignature("toMatchSnapshot", "<green>propertyMatchers<r>", false);
const signature = comptime getSignature(fn_name, "<green>propertyMatchers<r>", false);
const fmt = signature ++ "\n\nExpected <green>propertyMatchers<r> to match properties from received object" ++
"\n\nReceived: {any}\n";
@@ -2708,14 +2809,17 @@ pub const Expect = struct {
}
}
var pretty_value: MutableString = MutableString.init(default_allocator, 0) catch unreachable;
value.jestSnapshotPrettyFormat(&pretty_value, globalThis) catch {
value.jestSnapshotPrettyFormat(pretty_value, globalThis) catch {
var formatter = JSC.ConsoleObject.Formatter{ .globalThis = globalThis };
return globalThis.throw("Failed to pretty format value: {s}", .{value.toFmt(&formatter)});
};
}
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();
try this.matchAndFmtSnapshot(globalThis, value, property_matchers, &pretty_value, fn_name);
const existing_value = Jest.runner.?.snapshots.getOrPut(this, pretty_value.slice(), hint.slice()) catch |err| {
const existing_value = Jest.runner.?.snapshots.getOrPut(this, pretty_value.slice(), hint) catch |err| {
var formatter = JSC.ConsoleObject.Formatter{ .globalThis = globalThis };
const test_file_path = Jest.runner.?.files.get(this.testScope().?.describe.file_id).source.path.text;
return switch (err) {
@@ -2734,7 +2838,7 @@ pub const Expect = struct {
}
Jest.runner.?.snapshots.failed += 1;
const signature = comptime getSignature("toMatchSnapshot", "<green>expected<r>", false);
const signature = comptime getSignature(fn_name, "<green>expected<r>", false);
const fmt = signature ++ "\n\n{any}\n";
const diff_format = DiffFormatter{
.received_string = pretty_value.slice(),
@@ -4335,8 +4439,6 @@ pub const Expect = struct {
pub const toHaveReturnedWith = notImplementedJSCFn;
pub const toHaveLastReturnedWith = notImplementedJSCFn;
pub const toHaveNthReturnedWith = notImplementedJSCFn;
pub const toThrowErrorMatchingSnapshot = notImplementedJSCFn;
pub const toThrowErrorMatchingInlineSnapshot = notImplementedJSCFn;
pub fn getStaticNot(globalThis: *JSGlobalObject, _: JSValue, _: JSValue) JSValue {
return ExpectStatic.create(globalThis, .{ .not = true });

View File

@@ -37,8 +37,10 @@ pub const Snapshots = struct {
pub const InlineSnapshotToWrite = struct {
line: c_ulong,
col: c_ulong,
value: []const u8,
value: []const u8, // owned by Snapshots.allocator
has_matchers: bool,
is_added: bool,
kind: []const u8, // static lifetime
fn lessThanFn(_: void, a: InlineSnapshotToWrite, b: InlineSnapshotToWrite) bool {
if (a.line < b.line) return true;
@@ -53,6 +55,18 @@ pub const Snapshots = struct {
file: std.fs.File,
};
pub fn addCount(this: *Snapshots, expect: *Expect, hint: []const u8) !struct { []const u8, usize } {
this.total += 1;
const snapshot_name = try expect.getSnapshotName(this.allocator, hint);
const count_entry = try this.counts.getOrPut(snapshot_name);
if (count_entry.found_existing) {
this.allocator.free(snapshot_name);
count_entry.value_ptr.* += 1;
return .{ count_entry.key_ptr.*, count_entry.value_ptr.* };
}
count_entry.value_ptr.* = 1;
return .{ count_entry.key_ptr.*, count_entry.value_ptr.* };
}
pub fn getOrPut(this: *Snapshots, expect: *Expect, target_value: []const u8, hint: string) !?string {
switch (try this.getSnapshotFile(expect.testScope().?.describe.file_id)) {
.result => {},
@@ -65,21 +79,7 @@ pub const Snapshots = struct {
},
}
const snapshot_name = try expect.getSnapshotName(this.allocator, hint);
this.total += 1;
const count_entry = try this.counts.getOrPut(snapshot_name);
const counter = brk: {
if (count_entry.found_existing) {
this.allocator.free(snapshot_name);
count_entry.value_ptr.* += 1;
break :brk count_entry.value_ptr.*;
}
count_entry.value_ptr.* = 1;
break :brk count_entry.value_ptr.*;
};
const name = count_entry.key_ptr.*;
const name, const counter = try this.addCount(expect, hint);
var counter_string_buf = [_]u8{0} ** 32;
const counter_string = try std.fmt.bufPrint(&counter_string_buf, "{d}", .{counter});
@@ -276,7 +276,7 @@ pub const Snapshots = struct {
inline_snapshot_dbg("Finding byte for {}/{}", .{ ils.line, ils.col });
const byte_offset_add = logger.Source.lineColToByteOffset(file_text[last_byte..], last_line, last_col, ils.line, ils.col) orelse {
inline_snapshot_dbg("-> Could not find byte", .{});
try log.addErrorFmt(&source, .{ .start = @intCast(uncommitted_segment_end) }, arena, "Failed to update inline snapshot: Could not find byte for line/column: {d}/{d}", .{ ils.line, ils.col });
try log.addErrorFmt(&source, .{ .start = @intCast(uncommitted_segment_end) }, arena, "Failed to update inline snapshot: Ln {d}, Col {d} not found", .{ ils.line, ils.col });
continue;
};
@@ -296,7 +296,7 @@ pub const Snapshots = struct {
},
else => {},
};
const fn_name = "toMatchInlineSnapshot";
const fn_name = ils.kind;
if (!bun.strings.startsWith(file_text[next_start..], fn_name)) {
try log.addErrorFmt(&source, .{ .start = @intCast(next_start) }, arena, "Failed to update inline snapshot: Could not find 'toMatchInlineSnapshot' here", .{});
continue;
@@ -400,6 +400,8 @@ pub const Snapshots = struct {
try result_text.appendSlice("`");
try bun.js_printer.writePreQuotedString(ils.value, @TypeOf(result_text_writer), result_text_writer, '`', false, false, .utf8);
try result_text.appendSlice("`");
if (ils.is_added) Jest.runner.?.snapshots.added += 1;
}
// commit the last segment

View File

@@ -1,3 +1,7 @@
// Bun Snapshot v1, https://goo.gl/fbAQLP
exports[`expect() toMatchSnapshot to return undefined 1`] = `"abc"`;
exports[`expect() toThrowErrorMatchingSnapshot to return undefined 1`] = `undefined`;
exports[`expect() toThrowErrorMatchingSnapshot to return undefined: undefined 1`] = `undefined`;

View File

@@ -4744,7 +4744,7 @@ describe("expect()", () => {
expect(expect("abc").toMatch("a")).toBeUndefined();
});
test.todo("toMatchInlineSnapshot to return undefined", () => {
expect(expect("abc").toMatchInlineSnapshot()).toBeUndefined();
expect(expect("abc").toMatchInlineSnapshot('"abc"')).toBeUndefined();
});
test("toMatchObject to return undefined", () => {
expect(expect({}).toMatchObject({})).toBeUndefined();
@@ -4768,11 +4768,19 @@ describe("expect()", () => {
}).toThrow(),
).toBeUndefined();
});
test.todo("toThrowErrorMatchingInlineSnapshot to return undefined", () => {
expect(expect(() => {}).toThrowErrorMatchingInlineSnapshot()).toBeUndefined();
test("toThrowErrorMatchingInlineSnapshot to return undefined", () => {
expect(
expect(() => {
throw 0;
}).toThrowErrorMatchingInlineSnapshot("undefined"),
).toBeUndefined();
});
test.todo("toThrowErrorMatchingSnapshot to return undefined", () => {
expect(expect(() => {}).toThrowErrorMatchingSnapshot()).toBeUndefined();
test("toThrowErrorMatchingSnapshot to return undefined", () => {
expect(
expect(() => {
throw 0;
}).toThrowErrorMatchingSnapshot("undefined"),
).toBeUndefined();
});
test(' " " to contain ""', () => {

View File

@@ -587,3 +587,23 @@ exports[`snapshots unicode surrogate halves 1`] = `
exports[\`abc 1\`] = \`"😊abc\\\`\\\${def} <20>, <20> "\`;
"
`;
exports[`error inline snapshots 1`] = `"hello"`;
exports[`error inline snapshots 2`] = `undefined`;
exports[`error inline snapshots 3`] = `undefined`;
exports[`error inline snapshots 4`] = `undefined`;
exports[`error inline snapshots: hint 1`] = `undefined`;
exports[`snapshot numbering 1`] = `"item one"`;
exports[`snapshot numbering 2`] = `"snap"`;
exports[`snapshot numbering 4`] = `"snap"`;
exports[`snapshot numbering 6`] = `"hello"`;
exports[`snapshot numbering: hinted 1`] = `"hello"`;

View File

@@ -483,13 +483,14 @@ class InlineSnapshotTester {
describe("inline snapshots", () => {
const bad = '"bad"';
const helper_js = /*js*/ `
import {expect} from "bun:test";
export function wrongFile(value) {
expect(value).toMatchInlineSnapshot();
}
`;
const tester = new InlineSnapshotTester({
"helper.js": /*js*/ `
import {expect} from "bun:test";
export function wrongFile(value) {
expect(value).toMatchInlineSnapshot();
}
`,
"helper.js": helper_js,
});
test("changing inline snapshot", () => {
tester.test(
@@ -718,7 +719,7 @@ describe("inline snapshots", () => {
tester.testError(
{
update: true,
msg: "Matcher error: Inline snapshot matchers must be called from the same file as the test",
msg: "Inline snapshot matchers must be called from the test file",
},
/*js*/ `
import {wrongFile} from "./helper";
@@ -727,5 +728,91 @@ describe("inline snapshots", () => {
});
`,
);
expect(readFileSync(tester.tmpdir + "/helper.js", "utf-8")).toBe(helper_js);
});
it("is right file", () => {
tester.test(
v => /*js*/ `
import {wrongFile} from "./helper";
test("cases", () => {
expect("rightfile").toMatchInlineSnapshot(${v("", '"9"', '`"rightfile"`')});
expect(wrongFile).toMatchInlineSnapshot(${v("", '"9"', "`[Function: wrongFile]`")});
});
`,
);
});
});
test("error snapshots", () => {
expect(() => {
throw new Error("hello");
}).toThrowErrorMatchingInlineSnapshot(`"hello"`);
expect(() => {
throw 0;
}).toThrowErrorMatchingInlineSnapshot(`undefined`);
expect(() => {
throw { a: "b" };
}).toThrowErrorMatchingInlineSnapshot(`undefined`);
expect(() => {
throw undefined; // this one doesn't work in jest because it doesn't think the function threw
}).toThrowErrorMatchingInlineSnapshot(`undefined`);
});
test("error inline snapshots", () => {
expect(() => {
throw new Error("hello");
}).toThrowErrorMatchingSnapshot();
expect(() => {
throw 0;
}).toThrowErrorMatchingSnapshot();
expect(() => {
throw { a: "b" };
}).toThrowErrorMatchingSnapshot();
expect(() => {
throw undefined;
}).toThrowErrorMatchingSnapshot();
expect(() => {
throw "abcdef";
}).toThrowErrorMatchingSnapshot("hint");
expect(() => {
throw new Error("😊");
}).toThrowErrorMatchingInlineSnapshot(`"😊"`);
});
test("snapshot numbering", () => {
function fails() {
throw new Error("snap");
}
expect("item one").toMatchSnapshot();
expect(fails).toThrowErrorMatchingSnapshot();
expect("1").toMatchInlineSnapshot(`"1"`);
expect(fails).toThrowErrorMatchingSnapshot();
expect(fails).toThrowErrorMatchingInlineSnapshot(`"snap"`);
expect("hello").toMatchSnapshot();
expect("hello").toMatchSnapshot("hinted");
});
test("write snapshot from filter", async () => {
const sver = (m: string, a: boolean) => /*js*/ `
test("mysnap", () => {
expect("${m}").toMatchInlineSnapshot(${a ? '`"' + m + '"`' : ""});
expect(() => {throw new Error("${m}!")}).toThrowErrorMatchingInlineSnapshot(${a ? '`"' + m + '!"`' : ""});
})
`;
const dir = tempDirWithFiles("writesnapshotfromfilter", {
"mytests": {
"snap.test.ts": sver("a", false),
"snap2.test.ts": sver("b", false),
"more": {
"testing.test.ts": sver("TEST", false),
},
},
});
await $`cd ${dir} && ${bunExe()} test mytests`;
expect(await Bun.file(dir + "/mytests/snap.test.ts").text()).toBe(sver("a", true));
expect(await Bun.file(dir + "/mytests/snap2.test.ts").text()).toBe(sver("b", true));
expect(await Bun.file(dir + "/mytests/more/testing.test.ts").text()).toBe(sver("TEST", true));
await $`cd ${dir} && ${bunExe()} test mytests`;
expect(await Bun.file(dir + "/mytests/snap.test.ts").text()).toBe(sver("a", true));
expect(await Bun.file(dir + "/mytests/snap2.test.ts").text()).toBe(sver("b", true));
expect(await Bun.file(dir + "/mytests/more/testing.test.ts").text()).toBe(sver("TEST", true));
});