mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
implement toThrowErrorMatchingSnapshot, toThrowErrorMatchingInlineSnapshot (#15607)
This commit is contained in:
@@ -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 %}
|
||||
|
||||
34
packages/bun-types/test.d.ts
vendored
34
packages/bun-types/test.d.ts
vendored
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`;
|
||||
|
||||
@@ -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 ""', () => {
|
||||
|
||||
@@ -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"`;
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user