Compare commits

...

1 Commits

Author SHA1 Message Date
Jarred Sumner
c8af055f35 very wip 2024-05-21 20:56:51 -07:00
5 changed files with 201 additions and 6 deletions

View File

@@ -2346,6 +2346,149 @@ pub const Expect = struct {
return .undefined;
}
pub fn toMatchInlineSnapshot(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue {
defer this.postMatch(globalObject);
const thisValue = callFrame.this();
const _arguments = callFrame.arguments(2);
var arguments: []const JSValue = _arguments.ptr[0.._arguments.len];
incrementExpectCallCounter();
if (arguments.len == 0) {
globalObject.throwPretty("toMatchInlineSnapshot() must be statically analyzable and cannot be used dynamically", .{});
return .zero;
}
const byte_offset_value = arguments[arguments.len - 1];
if (!byte_offset_value.isNumber()) {
globalObject.throwPretty("toMatchInlineSnapshot() must be statically analyzable and cannot be used dynamically", .{});
return .zero;
}
const byte_offset_in_source = byte_offset_value.coerce(i32, globalObject);
_ = byte_offset_in_source; // autofix
arguments = arguments[0 .. arguments.len - 1];
const raw_inline_snapshot_contents: bun.String = brk: {
if (arguments.len > 0 and arguments[arguments.len - 1].isString()) {
arguments = arguments[0 .. arguments.len - 1];
break :brk arguments[arguments.len - 1].toBunString(globalObject);
}
break :brk bun.String.dead;
};
_ = raw_inline_snapshot_contents; // autofix
const not = this.flags.not;
if (not) {
const signature = comptime getSignature("toMatchInlineSnapshot", "", true);
const fmt = signature ++ "\n\n<b>Matcher error<r>: Snapshot matchers cannot be used with <b>not<r>\n";
globalObject.throwPretty(fmt, .{});
}
if (this.testScope() == null) {
const signature = comptime getSignature("toMatchInlineSnapshot", "", true);
const fmt = signature ++ "\n\n<b>Matcher error<r>: Snapshot matchers cannot be used outside of a test\n";
globalObject.throwPretty(fmt, .{});
return .zero;
}
// var hint_string: ZigString = ZigString.Empty;
var property_matchers: ?JSValue = null;
switch (arguments.len) {
0 => {},
1 => {
if (arguments[0].isObject()) {
property_matchers = arguments[0];
}
},
else => {
if (!arguments[0].isObject()) {
const signature = comptime getSignature("toMatchInlineSnapshot", "<green>properties<r><d>, <r>hint", false);
const fmt = signature ++ "\n\nMatcher error: Expected <green>properties<r> must be an object\n";
globalObject.throwPretty(fmt, .{});
return .zero;
}
property_matchers = arguments[0];
// if (arguments[1].isString()) {
// arguments[1].toZigString(&hint_string, globalObject);
// }
},
}
// var hint = hint_string.toSlice(default_allocator);
// defer hint.deinit();
const value: JSValue = this.getValue(globalObject, thisValue, "toMatchInlineSnapshot", "<green>properties<r><d>, <r>hint") orelse return .zero;
if (!value.isObject() and property_matchers != null) {
const signature = comptime getSignature("toMatchInlineSnapshot", "<green>properties<r><d>, <r>hint", false);
const fmt = signature ++ "\n\n<b>Matcher error: <red>received<r> values must be an object when the matcher has <green>properties<r>\n";
globalObject.throwPretty(fmt, .{});
return .zero;
}
if (property_matchers) |_prop_matchers| {
const prop_matchers = _prop_matchers;
if (!value.jestDeepMatch(prop_matchers, globalObject, true)) {
// TODO: print diff with properties from propertyMatchers
const signature = comptime getSignature("toMatchSnapshot", "<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 = globalObject };
globalObject.throwPretty(fmt, .{value.toFmt(globalObject, &formatter)});
return .zero;
}
}
const result = Jest.runner.?.snapshots.getOrPut(this, value, hint.slice(), globalObject) catch |err| {
var formatter = JSC.ConsoleObject.Formatter{ .globalThis = globalObject };
const test_file_path = Jest.runner.?.files.get(this.testScope().?.describe.file_id).source.path.text;
switch (err) {
error.FailedToOpenSnapshotFile => globalObject.throw("Failed to open snapshot file for test file: {s}", .{test_file_path}),
error.FailedToMakeSnapshotDirectory => globalObject.throw("Failed to make snapshot directory for test file: {s}", .{test_file_path}),
error.FailedToWriteSnapshotFile => globalObject.throw("Failed write to snapshot file: {s}", .{test_file_path}),
error.ParseError => globalObject.throw("Failed to parse snapshot file for: {s}", .{test_file_path}),
else => globalObject.throw("Failed to snapshot value: {any}", .{value.toFmt(globalObject, &formatter)}),
}
return .zero;
};
if (result) |saved_value| {
var pretty_value: MutableString = MutableString.init(default_allocator, 0) catch unreachable;
value.jestSnapshotPrettyFormat(&pretty_value, globalObject) catch {
var formatter = JSC.ConsoleObject.Formatter{ .globalThis = globalObject };
globalObject.throw("Failed to pretty format value: {s}", .{value.toFmt(globalObject, &formatter)});
return .zero;
};
defer pretty_value.deinit();
if (strings.eqlLong(pretty_value.toOwnedSliceLeaky(), saved_value, true)) {
Jest.runner.?.snapshots.passed += 1;
return .undefined;
}
Jest.runner.?.snapshots.failed += 1;
const signature = comptime getSignature("toMatchSnapshot", "<green>expected<r>", false);
const fmt = signature ++ "\n\n{any}\n";
const diff_format = DiffFormatter{
.received_string = pretty_value.toOwnedSliceLeaky(),
.expected_string = saved_value,
.globalObject = globalObject,
};
globalObject.throwPretty(fmt, .{diff_format});
return .zero;
}
return .undefined;
}
pub fn toBeEmpty(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue {
defer this.postMatch(globalObject);

View File

@@ -1144,6 +1144,7 @@ pub const Bundler = struct {
.minify_syntax = bundler.options.minify_syntax,
.minify_identifiers = bundler.options.minify_identifiers,
.transform_only = bundler.options.transform_only,
.jest_expect_ref = ast.jest.expect,
.runtime_transpiler_cache = runtime_transpiler_cache,
},
enable_source_map,
@@ -1167,6 +1168,7 @@ pub const Bundler = struct {
.minify_identifiers = bundler.options.minify_identifiers,
.transform_only = bundler.options.transform_only,
.import_meta_ref = ast.import_meta_ref,
.jest_expect_ref = ast.jest.expect,
.runtime_transpiler_cache = runtime_transpiler_cache,
},
enable_source_map,
@@ -1189,6 +1191,7 @@ pub const Bundler = struct {
.minify_syntax = bundler.options.minify_syntax,
.minify_identifiers = bundler.options.minify_identifiers,
.transform_only = bundler.options.transform_only,
.jest_expect_ref = ast.jest.expect,
.module_type = if (is_bun and bundler.options.transform_only)
// this is for when using `bun build --no-bundle`
// it should copy what was passed for the cli
@@ -1417,9 +1420,13 @@ pub const Bundler = struct {
opts.features.jsx_optimization_hoist = bundler.options.jsx_optimization_hoist orelse opts.features.jsx_optimization_inline;
opts.features.inject_jest_globals = this_parse.inject_jest_globals;
opts.features.minify_syntax = bundler.options.minify_syntax;
opts.features.minify_identifiers = bundler.options.minify_identifiers;
opts.features.dead_code_elimination = bundler.options.dead_code_elimination;
// We disable these things only for *.test.* files
// The goal is to improve the reliability of sourcemaps in those files.
opts.features.minify_syntax = !opts.features.inject_jest_globals and bundler.options.minify_syntax;
opts.features.minify_identifiers = !opts.features.inject_jest_globals and bundler.options.minify_identifiers;
opts.features.dead_code_elimination = !opts.features.inject_jest_globals and bundler.options.dead_code_elimination;
opts.features.remove_cjs_module_wrapper = this_parse.remove_cjs_module_wrapper;
if (bundler.macro_context == null) {

View File

@@ -6397,6 +6397,8 @@ pub const Ast = struct {
commonjs_export_names: []string = &([_]string{}),
import_meta_ref: Ref = Ref.None,
jest: @import("./js_parser.zig").Jest = .{},
pub const CommonJSNamedExport = struct {
loc_ref: LocRef,
needs_decl: bool = true,

View File

@@ -1220,6 +1220,16 @@ pub const ImportScanner = struct {
};
}
if (record.tag == .bun_test) {
for (st.items) |item| {
if (strings.eqlComptime(item.alias, "expect")) {
var symbol = &p.symbols.items[p.jest.expect.innerIndex()];
symbol.link = item.name.ref.?;
break;
}
}
}
if (record.was_originally_require) {
var symbol = &p.symbols.items[namespace_ref.innerIndex()];
symbol.namespace_alias = G.NamespaceAlias{
@@ -3939,7 +3949,7 @@ pub const Parser = struct {
if (p.options.features.inject_jest_globals) outer: {
var jest: *Jest = &p.jest;
for (p.import_records.items) |*item| {
for (@as([]ImportRecord, p.import_records.items)) |*item| {
// skip if they did import it
if (strings.eqlComptime(item.path.text, "bun:test") or strings.eqlComptime(item.path.text, "@jest/globals") or strings.eqlComptime(item.path.text, "vitest")) {
if (p.options.features.runtime_transpiler_cache) |cache| {
@@ -3948,7 +3958,7 @@ pub const Parser = struct {
cache.input_hash = null;
}
}
item.tag = .bun_test;
break :outer;
}
}
@@ -4591,7 +4601,7 @@ pub const MacroState = struct {
}
};
const Jest = struct {
pub const Jest = struct {
expect: Ref = Ref.None,
describe: Ref = Ref.None,
@"test": Ref = Ref.None,
@@ -22939,6 +22949,7 @@ fn NewParser_(
.import_keyword = p.esm_import_keyword,
.export_keyword = p.esm_export_keyword,
.top_level_symbols_to_parts = top_level_symbols_to_parts,
.jest = p.jest,
.char_freq = p.computeCharacterFrequency(),
// Assign slots to symbols in nested scopes. This is some precomputation for

View File

@@ -531,6 +531,8 @@ pub const Options = struct {
/// Used for cross-module inlining of import items when bundling
const_values: std.HashMapUnmanaged(Ref, Expr, Ref.HashCtx, 80) = .{},
jest_expect_ref: Ref = Ref.None,
// TODO: remove this
// The reason for this is:
// 1. You're bundling a React component
@@ -2405,6 +2407,36 @@ fn NewPrinter(
p.printExpr(e.target, .postfix, ExprFlag.None());
p.print(")");
} else {
// Replace all
// expect(foo).toMatchInlineSnapshot(...args)
//
// expect(foo).toMatchInlineSnapshot(...args, location)
if (comptime is_bun_platform) {
if (!p.options.jest_expect_ref.isNull()) {
if (e.target.data == .e_dot and
e.target.data.e_dot.name.len == "toMatchInlineSnapshot".len)
{
const dot = e.target.data.e_dot;
const parent_target = dot.target;
if (parent_target.data == .e_call) {
if (switch (parent_target.data.e_call.target.data) {
.e_identifier => |ident| ident.ref.eql(p.options.jest_expect_ref),
.e_import_identifier => |ident| ident.ref.eql(p.symbols().follow(p.options.jest_expect_ref)),
else => false,
}) {
if (strings.eqlComptimeIgnoreLen(dot.name, "toMatchInlineSnapshot")) {
e.args.append(p.options.allocator, &.{
js_ast.Expr.init(E.Number, E.Number{
.value = @floatFromInt(e.target.loc.start),
}, e.target.loc),
}) catch unreachable;
}
}
}
}
}
}
p.printExpr(e.target, .postfix, target_flags);
}