diff --git a/src/bun.js/test/ScopeFunctions.zig b/src/bun.js/test/ScopeFunctions.zig index 4d7b1c9a4a..7f4c6438ef 100644 --- a/src/bun.js/test/ScopeFunctions.zig +++ b/src/bun.js/test/ScopeFunctions.zig @@ -296,6 +296,7 @@ const ParseArgumentsResult = struct { } }; pub const CallbackMode = enum { require, allow }; +pub const FunctionKind = enum { test_or_describe, hook }; fn getDescription(gpa: std.mem.Allocator, globalThis: *jsc.JSGlobalObject, description: jsc.JSValue, signature: Signature) bun.JSError![]const u8 { if (description == .zero) { @@ -329,7 +330,7 @@ fn getDescription(gpa: std.mem.Allocator, globalThis: *jsc.JSGlobalObject, descr return globalThis.throwPretty("{s}() expects first argument to be a named class, named function, number, or string", .{signature}); } -pub fn parseArguments(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame, signature: Signature, gpa: std.mem.Allocator, cfg: struct { callback: CallbackMode }) bun.JSError!ParseArgumentsResult { +pub fn parseArguments(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame, signature: Signature, gpa: std.mem.Allocator, cfg: struct { callback: CallbackMode, kind: FunctionKind = .test_or_describe }) bun.JSError!ParseArgumentsResult { var a1, var a2, var a3 = callframe.argumentsAsArray(3); const len: enum { three, two, one, zero } = if (!a3.isUndefinedOrNull()) .three else if (!a2.isUndefinedOrNull()) .two else if (!a1.isUndefinedOrNull()) .one else .zero; @@ -338,8 +339,9 @@ pub fn parseArguments(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame // description, callback(fn), options(!fn) // description, options(!fn), callback(fn) .three => if (a2.isFunction()) .{ .description = a1, .callback = a2, .options = a3 } else .{ .description = a1, .callback = a3, .options = a2 }, + // callback(fn), options(!fn) // description, callback(fn) - .two => .{ .description = a1, .callback = a2 }, + .two => if (a1.isFunction() and !a2.isFunction()) .{ .callback = a1, .options = a2 } else .{ .description = a1, .callback = a2 }, // description // callback(fn) .one => if (a1.isFunction()) .{ .callback = a1 } else .{ .description = a1 }, @@ -352,7 +354,8 @@ pub fn parseArguments(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame } else if (callback.isFunction()) blk: { break :blk callback.withAsyncContextIfNeeded(globalThis); } else { - return globalThis.throw("{s} expects a function as the second argument", .{signature}); + const ordinal = if (cfg.kind == .hook) "first" else "second"; + return globalThis.throw("{s} expects a function as the {s} argument", .{ signature, ordinal }); }; var result: ParseArgumentsResult = .{ diff --git a/src/bun.js/test/bun_test.zig b/src/bun.js/test/bun_test.zig index 34fd701ccb..02a4c90078 100644 --- a/src/bun.js/test/bun_test.zig +++ b/src/bun.js/test/bun_test.zig @@ -44,7 +44,7 @@ pub const js_fns = struct { defer group.end(); errdefer group.log("ended in error", .{}); - var args = try ScopeFunctions.parseArguments(globalThis, callFrame, .{ .str = @tagName(tag) ++ "()" }, bun.default_allocator, .{ .callback = .require }); + var args = try ScopeFunctions.parseArguments(globalThis, callFrame, .{ .str = @tagName(tag) ++ "()" }, bun.default_allocator, .{ .callback = .require, .kind = .hook }); defer args.deinit(bun.default_allocator); const has_done_parameter = if (args.callback) |callback| try callback.getLength(globalThis) > 0 else false; diff --git a/test/regression/issue/23133.test.ts b/test/regression/issue/23133.test.ts new file mode 100644 index 0000000000..e4b20eee7c --- /dev/null +++ b/test/regression/issue/23133.test.ts @@ -0,0 +1,54 @@ +// https://github.com/oven-sh/bun/issues/23133 +// Passing HookOptions to lifecycle hooks should work +import { afterAll, afterEach, beforeAll, beforeEach, expect, test } from "bun:test"; + +const logs: string[] = []; + +// Test beforeAll with object timeout option +beforeAll( + () => { + logs.push("beforeAll with object timeout"); + }, + { timeout: 10_000 }, +); + +// Test beforeAll with numeric timeout option +beforeAll(() => { + logs.push("beforeAll with numeric timeout"); +}, 5000); + +// Test beforeEach with timeout option +beforeEach( + () => { + logs.push("beforeEach"); + }, + { timeout: 10_000 }, +); + +// Test afterEach with timeout option +afterEach( + () => { + logs.push("afterEach"); + }, + { timeout: 10_000 }, +); + +// Test afterAll with timeout option +afterAll( + () => { + logs.push("afterAll"); + }, + { timeout: 10_000 }, +); + +test("lifecycle hooks accept timeout options", () => { + expect(logs).toContain("beforeAll with object timeout"); + expect(logs).toContain("beforeAll with numeric timeout"); + expect(logs).toContain("beforeEach"); +}); + +test("beforeEach runs before each test", () => { + // beforeEach should have run twice now (once for each test) + const beforeEachCount = logs.filter(l => l === "beforeEach").length; + expect(beforeEachCount).toBe(2); +});