mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 10:28:47 +00:00
support $variables in test.each (#21061)
This commit is contained in:
@@ -2007,6 +2007,30 @@ fn consumeArg(
|
||||
args_idx.* += 1;
|
||||
}
|
||||
|
||||
fn resolvePropertyPath(globalThis: *JSGlobalObject, obj: JSValue, path: []const u8) !?JSValue {
|
||||
var current = obj;
|
||||
var parts = std.mem.tokenizeScalar(u8, path, '.');
|
||||
|
||||
while (parts.next()) |part| {
|
||||
if (current.isEmptyOrUndefinedOrNull()) return null;
|
||||
|
||||
if (std.fmt.parseInt(u32, part, 10)) |index| {
|
||||
if (current.jsType().isArray()) {
|
||||
if (index >= try current.getLength(globalThis)) return null;
|
||||
current = try current.getIndex(globalThis, index);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else |_| if (current.isObject()) {
|
||||
current = try current.get(globalThis, part) orelse return null;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return if (current.isEmptyOrUndefinedOrNull()) null else current;
|
||||
}
|
||||
|
||||
// Generate test label by positionally injecting parameters with printf formatting
|
||||
fn formatLabel(globalThis: *JSGlobalObject, label: string, function_args: []JSValue, test_idx: usize) !string {
|
||||
const allocator = bun.default_allocator;
|
||||
@@ -2016,7 +2040,50 @@ fn formatLabel(globalThis: *JSGlobalObject, label: string, function_args: []JSVa
|
||||
|
||||
while (idx < label.len) {
|
||||
const char = label[idx];
|
||||
if (char == '%' and (idx + 1 < label.len) and !(args_idx >= function_args.len)) {
|
||||
|
||||
if (char == '$' and idx + 1 < label.len and function_args.len > 0 and function_args[0].isObject()) {
|
||||
const var_start = idx + 1;
|
||||
var var_end = var_start;
|
||||
|
||||
if (std.ascii.isAlphabetic(label[var_end]) or label[var_end] == '_' or label[var_end] == '$') {
|
||||
var_end += 1;
|
||||
|
||||
while (var_end < label.len) {
|
||||
const c = label[var_end];
|
||||
if (c == '.') {
|
||||
const next_char = label[var_end + 1];
|
||||
if (var_end + 1 < label.len and (std.ascii.isAlphanumeric(next_char) or next_char == '_' or next_char == '$')) {
|
||||
var_end += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else if (std.ascii.isAlphanumeric(c) or c == '_' or c == '$') {
|
||||
var_end += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const var_path = label[var_start..var_end];
|
||||
if (try resolvePropertyPath(globalThis, function_args[0], var_path)) |value| {
|
||||
var formatter = JSC.ConsoleObject.Formatter{ .globalThis = globalThis, .quote_strings = true };
|
||||
defer formatter.deinit();
|
||||
const value_str = std.fmt.allocPrint(allocator, "{}", .{value.toFmt(&formatter)}) catch bun.outOfMemory();
|
||||
defer allocator.free(value_str);
|
||||
list.appendSlice(allocator, value_str) catch bun.outOfMemory();
|
||||
idx = var_end;
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
while (var_end < label.len and (std.ascii.isAlphanumeric(label[var_end]) or label[var_end] == '_')) {
|
||||
var_end += 1;
|
||||
}
|
||||
}
|
||||
|
||||
list.append(allocator, '$') catch bun.outOfMemory();
|
||||
list.appendSlice(allocator, label[var_start..var_end]) catch bun.outOfMemory();
|
||||
idx = var_end;
|
||||
} else if (char == '%' and (idx + 1 < label.len) and !(args_idx >= function_args.len)) {
|
||||
const current_arg = function_args[args_idx];
|
||||
|
||||
switch (label[idx + 1]) {
|
||||
|
||||
@@ -881,6 +881,262 @@ describe("bun test", () => {
|
||||
expect(stderr).toContain(`%`);
|
||||
});
|
||||
test.todo("check formatting for %p", () => {});
|
||||
|
||||
describe("$variable syntax", () => {
|
||||
test("should replace $variables with object properties in test names", () => {
|
||||
const cases = [
|
||||
{ a: 1, b: 2, expected: 3 },
|
||||
{ a: 5, b: 5, expected: 10 },
|
||||
{ a: -1, b: 1, expected: 0 },
|
||||
];
|
||||
|
||||
const stderr = runTest({
|
||||
args: [],
|
||||
input: `
|
||||
import { test, expect } from "bun:test";
|
||||
|
||||
const cases = ${JSON.stringify(cases)};
|
||||
test.each(cases)('$a + $b = $expected', ({ a, b, expected }) => {
|
||||
expect(a + b).toBe(expected);
|
||||
});
|
||||
`,
|
||||
});
|
||||
|
||||
expect(stderr).toContain("(pass) 1 + 2 = 3");
|
||||
expect(stderr).toContain("(pass) 5 + 5 = 10");
|
||||
expect(stderr).toContain("(pass) -1 + 1 = 0");
|
||||
expect(stderr).toContain("3 pass");
|
||||
});
|
||||
|
||||
test("should show $variable literal when property doesn't exist", () => {
|
||||
const cases = [{ a: 1 }, { a: 2 }];
|
||||
|
||||
const stderr = runTest({
|
||||
args: [],
|
||||
input: `
|
||||
import { test, expect } from "bun:test";
|
||||
|
||||
const cases = ${JSON.stringify(cases)};
|
||||
test.each(cases)('value $a with missing $nonexistent', ({ a }) => {
|
||||
expect(a).toBeDefined();
|
||||
});
|
||||
`,
|
||||
});
|
||||
|
||||
expect(stderr).toContain("(pass) value 1 with missing $nonexistent");
|
||||
expect(stderr).toContain("(pass) value 2 with missing $nonexistent");
|
||||
expect(stderr).toContain("2 pass");
|
||||
});
|
||||
|
||||
test("should work with describe.each", () => {
|
||||
const cases = [
|
||||
{ module: "fs", method: "readFile" },
|
||||
{ module: "path", method: "join" },
|
||||
];
|
||||
|
||||
const stderr = runTest({
|
||||
args: [],
|
||||
input: `
|
||||
import { test, expect, describe } from "bun:test";
|
||||
|
||||
const cases = ${JSON.stringify(cases)};
|
||||
describe.each(cases)('$module module', ({ module, method }) => {
|
||||
test('has $method', () => {
|
||||
const mod = require(module);
|
||||
expect(mod).toHaveProperty(method);
|
||||
});
|
||||
});
|
||||
`,
|
||||
});
|
||||
|
||||
expect(stderr).toContain('"fs" module > has $method');
|
||||
expect(stderr).toContain('"path" module > has $method');
|
||||
expect(stderr).toContain("2 pass");
|
||||
});
|
||||
|
||||
test("should work with complex property names", () => {
|
||||
const cases = [
|
||||
{ user_name: "john_doe", age: 30, is_active: true },
|
||||
{ user_name: "jane_smith", age: 25, is_active: false },
|
||||
];
|
||||
|
||||
const stderr = runTest({
|
||||
args: [],
|
||||
input: `
|
||||
import { test, expect } from "bun:test";
|
||||
|
||||
const cases = ${JSON.stringify(cases)};
|
||||
test.each(cases)('user $user_name age $age active $is_active', ({ user_name, age, is_active }) => {
|
||||
expect(user_name).toBeDefined();
|
||||
expect(age).toBeGreaterThan(0);
|
||||
expect(typeof is_active).toBe('boolean');
|
||||
});
|
||||
`,
|
||||
});
|
||||
|
||||
expect(stderr).toContain('(pass) user "john_doe" age 30 active true');
|
||||
expect(stderr).toContain('(pass) user "jane_smith" age 25 active false');
|
||||
expect(stderr).toContain("2 pass");
|
||||
});
|
||||
|
||||
test("should coexist with % formatting for arrays", () => {
|
||||
const numbers = [
|
||||
[1, 2, 3],
|
||||
[5, 5, 10],
|
||||
];
|
||||
|
||||
const stderr = runTest({
|
||||
args: [],
|
||||
input: `
|
||||
import { test, expect } from "bun:test";
|
||||
|
||||
test.each(${JSON.stringify(numbers)})('%i + %i = %i', (a, b, expected) => {
|
||||
expect(a + b).toBe(expected);
|
||||
});
|
||||
`,
|
||||
});
|
||||
|
||||
expect(stderr).toContain("(pass) 1 + 2 = 3");
|
||||
expect(stderr).toContain("(pass) 5 + 5 = 10");
|
||||
expect(stderr).toContain("2 pass");
|
||||
});
|
||||
|
||||
test("should support nested property access", () => {
|
||||
const cases = [
|
||||
{
|
||||
user: { name: "Alice", profile: { city: "NYC" } },
|
||||
expected: "Alice from NYC",
|
||||
},
|
||||
{
|
||||
user: { name: "Bob", profile: { city: "LA" } },
|
||||
expected: "Bob from LA",
|
||||
},
|
||||
];
|
||||
|
||||
const stderr = runTest({
|
||||
args: [],
|
||||
input: `
|
||||
import { test, expect } from "bun:test";
|
||||
|
||||
const cases = ${JSON.stringify(cases)};
|
||||
test.each(cases)('$user.name from $user.profile.city', ({ user, expected }) => {
|
||||
expect(\`\${user.name} from \${user.profile.city}\`).toBe(expected);
|
||||
});
|
||||
`,
|
||||
});
|
||||
|
||||
expect(stderr).toContain('(pass) "Alice" from "NYC"');
|
||||
expect(stderr).toContain('(pass) "Bob" from "LA"');
|
||||
expect(stderr).toContain("2 pass");
|
||||
});
|
||||
|
||||
test("should support array indexing with dot notation", () => {
|
||||
const cases = [
|
||||
{
|
||||
users: [{ name: "Alice" }, { name: "Bob" }],
|
||||
first: "Alice",
|
||||
},
|
||||
{
|
||||
users: [{ name: "Carol" }, { name: "Dave" }],
|
||||
first: "Carol",
|
||||
},
|
||||
];
|
||||
|
||||
const stderr = runTest({
|
||||
args: [],
|
||||
input: `
|
||||
import { test, expect } from "bun:test";
|
||||
|
||||
const cases = ${JSON.stringify(cases)};
|
||||
test.each(cases)('first user is $users.0.name', ({ users, first }) => {
|
||||
expect(users[0].name).toBe(first);
|
||||
});
|
||||
`,
|
||||
});
|
||||
|
||||
expect(stderr).toContain('(pass) first user is "Alice"');
|
||||
expect(stderr).toContain('(pass) first user is "Carol"');
|
||||
expect(stderr).toContain("2 pass");
|
||||
});
|
||||
|
||||
test("handles edge cases with underscores and invalid identifiers", () => {
|
||||
const cases = [
|
||||
{
|
||||
_valid: "underscore",
|
||||
$dollar: "dollar",
|
||||
_123mix: "mix",
|
||||
"123invalid": "invalid",
|
||||
"has-dash": "dash",
|
||||
"has space": "space",
|
||||
},
|
||||
];
|
||||
|
||||
const stderr = runTest({
|
||||
args: [],
|
||||
input: `
|
||||
import { test, expect } from "bun:test";
|
||||
|
||||
const cases = ${JSON.stringify(cases)};
|
||||
test.each(cases)('Edge: $_valid | $$dollar | $_123mix | $123invalid | $has-dash | $has space', (obj) => {
|
||||
expect(obj).toBeDefined();
|
||||
});
|
||||
`,
|
||||
});
|
||||
|
||||
expect(stderr).toContain('"underscore"');
|
||||
expect(stderr).toContain('"dollar"');
|
||||
expect(stderr).toContain('"mix"');
|
||||
expect(stderr).toContain("$123invalid");
|
||||
expect(stderr).toContain("$hasdash");
|
||||
expect(stderr).toContain("$hasspace");
|
||||
});
|
||||
|
||||
test("handles deeply nested properties with arrays", () => {
|
||||
const cases = [
|
||||
{
|
||||
data: {
|
||||
users: [
|
||||
{ name: "Alice", tags: ["admin", "user"] },
|
||||
{ name: "Bob", tags: ["user"] },
|
||||
],
|
||||
count: 2,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const stderr = runTest({
|
||||
args: [],
|
||||
input: `
|
||||
import { test, expect } from "bun:test";
|
||||
|
||||
const cases = ${JSON.stringify(cases)};
|
||||
test.each(cases)('First user: $data.users.0.name with tag: $data.users.0.tags.0', (obj) => {
|
||||
expect(obj).toBeDefined();
|
||||
});
|
||||
`,
|
||||
});
|
||||
|
||||
expect(stderr).toContain('First user: "Alice" with tag: "admin"');
|
||||
});
|
||||
|
||||
test("handles missing properties gracefully", () => {
|
||||
const cases = [{ a: 1 }];
|
||||
|
||||
const stderr = runTest({
|
||||
args: [],
|
||||
input: `
|
||||
import { test, expect } from "bun:test";
|
||||
|
||||
const cases = ${JSON.stringify(cases)};
|
||||
test.each(cases)('$a | $missing | $a.b.c | $a', ({ a }) => {
|
||||
expect(a).toBe(1);
|
||||
});
|
||||
`,
|
||||
});
|
||||
|
||||
expect(stderr).toContain("1 | $missing| $a.b.c| 1");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("Prints error when no test matches", () => {
|
||||
|
||||
Reference in New Issue
Block a user