Compare commits

...

6 Commits

Author SHA1 Message Date
Jarred Sumner
bffc24fa99 Merge branch 'main' into codex/fix-test.each-value-substitution-issue 2025-06-23 14:43:25 -07:00
Jarred Sumner
326361ece5 Finish it 2025-06-22 22:00:02 -07:00
claude[bot]
cf8a6971dd bun run prettier 2025-06-23 02:41:34 +00:00
claude[bot]
390b9f3261 fix: correct JSValue enum initialization syntax in jest.zig
Fixes compilation error where JSValue{ .zero = {} } was using struct
initialization syntax on an enum. Changed to JSValue.zero.

Co-authored-by: Jarred Sumner <Jarred-Sumner@users.noreply.github.com>
2025-06-23 02:39:33 +00:00
Jarred-Sumner
da1f91bed6 bun run prettier 2025-06-22 22:50:23 +00:00
Jarred Sumner
75610479b8 test(jest): cover more object placeholder cases (#20566) 2025-06-22 15:47:35 -07:00
2 changed files with 299 additions and 9 deletions

View File

@@ -1940,10 +1940,38 @@ fn formatLabel(globalThis: *JSGlobalObject, label: string, function_args: []JSVa
const allocator = bun.default_allocator;
var idx: usize = 0;
var args_idx: usize = 0;
var list = std.ArrayListUnmanaged(u8).initCapacity(allocator, label.len) catch bun.outOfMemory();
var list = try std.ArrayListUnmanaged(u8).initCapacity(allocator, label.len);
const object_arg = if (function_args.len == 1 and function_args[0] != .zero and function_args[0].jsType().isObject())
function_args[0]
else
JSValue.zero;
while (idx < label.len) {
const char = label[idx];
if (char == '$' and object_arg != .zero) {
var start = idx + 1;
while (start < label.len) {
const c = label[start];
if ((c >= 'a' and c <= 'z') or (c >= 'A' and c <= 'Z') or (c >= '0' and c <= '9') or c == '_') {
start += 1;
} else {
break;
}
}
if (start > idx + 1) {
const key = label[idx + 1 .. start];
if (try object_arg.getOwn(globalThis, key)) |value| {
const owned_slice = try value.toSliceOrNull(globalThis);
defer owned_slice.deinit();
try list.appendSlice(allocator, owned_slice.slice());
}
idx = start;
continue;
}
}
if (char == '%' and (idx + 1 < label.len) and !(args_idx >= function_args.len)) {
const current_arg = function_args[args_idx];
@@ -1964,9 +1992,9 @@ fn formatLabel(globalThis: *JSGlobalObject, label: string, function_args: []JSVa
var str = bun.String.empty;
defer str.deref();
current_arg.jsonStringify(globalThis, 0, &str);
const owned_slice = str.toOwnedSlice(allocator) catch bun.outOfMemory();
const owned_slice = try str.toOwnedSlice(allocator);
defer allocator.free(owned_slice);
list.appendSlice(allocator, owned_slice) catch bun.outOfMemory();
try list.appendSlice(allocator, owned_slice);
idx += 1;
args_idx += 1;
},
@@ -1974,27 +2002,27 @@ fn formatLabel(globalThis: *JSGlobalObject, label: string, function_args: []JSVa
var formatter = JSC.ConsoleObject.Formatter{ .globalThis = globalThis, .quote_strings = true };
defer formatter.deinit();
const value_fmt = current_arg.toFmt(&formatter);
const test_index_str = std.fmt.allocPrint(allocator, "{}", .{value_fmt}) catch bun.outOfMemory();
const test_index_str = try std.fmt.allocPrint(allocator, "{}", .{value_fmt});
defer allocator.free(test_index_str);
list.appendSlice(allocator, test_index_str) catch bun.outOfMemory();
try list.appendSlice(allocator, test_index_str);
idx += 1;
args_idx += 1;
},
'#' => {
const test_index_str = std.fmt.allocPrint(allocator, "{d}", .{test_idx}) catch bun.outOfMemory();
const test_index_str = try std.fmt.allocPrint(allocator, "{d}", .{test_idx});
defer allocator.free(test_index_str);
list.appendSlice(allocator, test_index_str) catch bun.outOfMemory();
try list.appendSlice(allocator, test_index_str);
idx += 1;
},
'%' => {
list.append(allocator, '%') catch bun.outOfMemory();
try list.append(allocator, '%');
idx += 1;
},
else => {
// ignore unrecognized fmt
},
}
} else list.append(allocator, char) catch bun.outOfMemory();
} else try list.append(allocator, char);
idx += 1;
}

View File

@@ -840,6 +840,268 @@ describe("bun test", () => {
});
expect(stderr).toContain(`with an object: ${JSON.stringify(input[0])}`);
});
describe("substitutes object placeholders", () => {
test("basic", () => {
const cases = [
{ a: 1, b: 2, expected: 3, 1: "one", a1b2: "alpha", MixedCase: "X", dup: "A" },
{ a: 2, b: 3, expected: 5, 1: "two", a1b2: "beta", MixedCase: "Y", dup: "B" },
];
const stderr = runTest({
args: [],
input: `
import { test, expect } from "bun:test";
test.each(${JSON.stringify(cases)})
("$a+$b=$expected [$1 $a1b2 $MixedCase $missing $dup $dup]", data => {
expect(data.a + data.b).toBe(data.expected);
});
`,
});
cases.forEach(c => {
const expected = `${c.a}+${c.b}=${c.expected} [` + `${c[1]} ${c.a1b2} ${c.MixedCase} ${c.dup} ${c.dup}]`;
expect(stderr).toContain(expected);
});
});
test("basic property access", () => {
const cases = [
{ name: "Alice", age: 30 },
{ name: "Bob", age: 25 },
];
const stderr = runTest({
args: [],
input: `
import { test, expect } from "bun:test";
test.each(${JSON.stringify(cases)})("User $name is $age years old", data => {
expect(data.name).toBeDefined();
});
`,
});
cases.forEach(c => {
expect(stderr).toContain(`User ${c.name} is ${c.age} years old`);
});
});
test("numeric property names", () => {
const cases = [
{ 0: "zero", 1: "one", 2: "two", 42: "answer" },
{ 0: "null", 1: "uno", 2: "dos", 42: "respuesta" },
];
const stderr = runTest({
args: [],
input: `
import { test, expect } from "bun:test";
test.each(${JSON.stringify(cases)})("Numbers: $0, $1, $2, $42", data => {
expect(data[0]).toBeDefined();
});
`,
});
cases.forEach(c => {
expect(stderr).toContain(`Numbers: ${c[0]}, ${c[1]}, ${c[2]}, ${c[42]}`);
});
});
test("underscore properties", () => {
const cases = [
{ _private: "secret", __dunder: "magic", _1: "underscore_one", test_case: "snake" },
{ _private: "hidden", __dunder: "special", _1: "underscore_two", test_case: "case" },
];
const stderr = runTest({
args: [],
input: `
import { test, expect } from "bun:test";
test.each(${JSON.stringify(cases)})("Props: $_private, $__dunder, $_1, $test_case", data => {
expect(data._private).toBeDefined();
});
`,
});
cases.forEach(c => {
expect(stderr).toContain(`Props: ${c._private}, ${c.__dunder}, ${c._1}, ${c.test_case}`);
});
});
test("missing properties", () => {
const cases = [{ existing: "value1" }, { existing: "value2" }];
const stderr = runTest({
args: [],
input: `
import { test, expect } from "bun:test";
test.each(${JSON.stringify(cases)})("Has $existing but not $missing", data => {
expect(data.existing).toBeDefined();
});
`,
});
cases.forEach(c => {
expect(stderr).toContain(`Has ${c.existing} but not `);
});
});
test("special characters in values", () => {
const cases = [
{ special: "hello\nworld", unicode: "café", symbols: "!@#$%^&*()" },
{ special: "tab\there", unicode: "naïve", symbols: "<>{}[]|\\`~" },
];
const stderr = runTest({
args: [],
input: `
import { test, expect } from "bun:test";
test.each(${JSON.stringify(cases)})("Special: '$special', Unicode: '$unicode', Symbols: '$symbols'", data => {
expect(data.special).toBeDefined();
});
`,
});
cases.forEach(c => {
expect(stderr).toContain(`Special: '${c.special}', Unicode: '${c.unicode}', Symbols: '${c.symbols}'`);
});
});
test("boolean and null values", () => {
const cases = [
{ bool_true: true, bool_false: false, null_val: null, undef_val: undefined },
{ bool_true: false, bool_false: true, null_val: null, undef_val: undefined },
];
const stderr = runTest({
args: [],
input: `
import { test, expect } from "bun:test";
test.each(${JSON.stringify(cases)})("Bool: $bool_true/$bool_false, Null: $null_val, Undef: $undef_val", data => {
expect(typeof data.bool_true).toBe('boolean');
});
`,
});
cases.forEach(c => {
expect(stderr).toContain(`Bool: ${c.bool_true}/${c.bool_false}, Null: ${c.null_val}, Undef: `);
});
});
test("nested objects", () => {
const cases = [
{ nested: { deep: { value: "deep1" } }, flat: "flat1" },
{ nested: { deep: { value: "deep2" } }, flat: "flat2" },
];
const stderr = runTest({
args: [],
input: `
import { test, expect } from "bun:test";
test.each(${JSON.stringify(cases)})("Flat: $flat, Nested: $nested", data => {
expect(data.flat).toBeDefined();
});
`,
});
cases.forEach(c => {
expect(stderr).toContain(`Flat: ${c.flat}, Nested: [object Object]`);
});
});
test("array values", () => {
const cases = [
{ arr: [1, 2, 3], str: "test1" },
{ arr: ["a", "b", "c"], str: "test2" },
];
const stderr = runTest({
args: [],
input: `
import { test, expect } from "bun:test";
test.each(${JSON.stringify(cases)})("Array: $arr, String: $str", data => {
expect(Array.isArray(data.arr)).toBe(true);
});
`,
});
cases.forEach(c => {
expect(stderr).toContain(`Array: ${c.arr.toString()}, String: ${c.str}`);
});
});
test("long property names", () => {
const cases = [
{ veryLongPropertyNameThatShouldStillWork: "long1", a: "short1" },
{ veryLongPropertyNameThatShouldStillWork: "long2", a: "short2" },
];
const stderr = runTest({
args: [],
input: `
import { test, expect } from "bun:test";
test.each(${JSON.stringify(cases)})("Long: $veryLongPropertyNameThatShouldStillWork, Short: $a", data => {
expect(data.a).toBeDefined();
});
`,
});
cases.forEach(c => {
expect(stderr).toContain(`Long: ${c.veryLongPropertyNameThatShouldStillWork}, Short: ${c.a}`);
});
});
test("case sensitivity", () => {
const cases = [
{ Name: "uppercase", name: "lowercase", NAME: "allcaps" },
{ Name: "Upper", name: "lower", NAME: "CAPS" },
];
const stderr = runTest({
args: [],
input: `
import { test, expect } from "bun:test";
test.each(${JSON.stringify(cases)})("Cases: $Name, $name, $NAME", data => {
expect(data.Name).toBeDefined();
});
`,
});
cases.forEach(c => {
expect(stderr).toContain(`Cases: ${c.Name}, ${c.name}, ${c.NAME}`);
});
});
test("no substitution when not object", () => {
const cases = [1, 2, 3];
const stderr = runTest({
args: [],
input: `
import { test, expect } from "bun:test";
test.each(${JSON.stringify(cases)})("Value: $value, Number: %i", (num) => {
expect(typeof num).toBe('number');
});
`,
});
cases.forEach(c => {
expect(stderr).toContain(`Value: $value, Number: ${c}`);
});
});
});
test("check formatting for %#", () => {
const numbers = [
[1, 2, 3],