diff options
| -rw-r--r-- | src/bun.js/test/jest.zig | 162 | ||||
| -rw-r--r-- | test/js/bun/test/jest-hooks.test.ts | 31 | ||||
| -rw-r--r-- | test/js/bun/test/test-test.test.ts | 6 | 
3 files changed, 130 insertions, 69 deletions
| diff --git a/src/bun.js/test/jest.zig b/src/bun.js/test/jest.zig index f43afb1a2..d3bb90747 100644 --- a/src/bun.js/test/jest.zig +++ b/src/bun.js/test/jest.zig @@ -493,8 +493,7 @@ pub const Jest = struct {          var filepath = Fs.FileSystem.instance.filename_store.append([]const u8, slice) catch unreachable;          var scope = runner_.getOrPutFile(filepath); -        DescribeScope.active = scope; -        DescribeScope.module = scope; +        scope.push();          return Bun__Jest__testModuleObject(ctx).asObjectRef();      } @@ -751,17 +750,16 @@ pub const DescribeScope = struct {      }      pub fn push(new: *DescribeScope) void { -        if (comptime is_bindgen) return undefined; -        if (new == DescribeScope.active) return; - -        new.parent = DescribeScope.active; +        if (comptime is_bindgen) return; +        std.debug.assert(DescribeScope.active != new); +        if (new.parent) |scope| std.debug.assert(scope == DescribeScope.active);          DescribeScope.active = new;      }      pub fn pop(this: *DescribeScope) void { -        if (comptime is_bindgen) return undefined; -        if (DescribeScope.active == this) -            DescribeScope.active = this.parent orelse DescribeScope.active; +        if (comptime is_bindgen) return; +        std.debug.assert(DescribeScope.active == this); +        DescribeScope.active = this.parent;      }      pub const LifecycleHook = enum { @@ -771,8 +769,7 @@ pub const DescribeScope = struct {          afterAll,      }; -    pub threadlocal var active: *DescribeScope = undefined; -    pub threadlocal var module: *DescribeScope = undefined; +    pub threadlocal var active: ?*DescribeScope = null;      const CallbackFn = *const fn (          *JSC.JSGlobalObject, @@ -781,21 +778,24 @@ pub const DescribeScope = struct {      fn createCallback(comptime hook: LifecycleHook) CallbackFn {          return struct { -            const this_hook = hook;              pub fn run(                  globalThis: *JSC.JSGlobalObject,                  callframe: *JSC.CallFrame,              ) callconv(.C) JSC.JSValue { -                const arguments_ = callframe.arguments(2); -                const arguments = arguments_.ptr[0..arguments_.len]; -                if (arguments.len == 0 or !arguments[0].isObject() or !arguments[0].isCallable(globalThis.vm())) { -                    globalThis.throwInvalidArgumentType(@tagName(this_hook), "callback", "function"); +                const arguments = callframe.arguments(2); +                if (arguments.len < 1) { +                    globalThis.throwNotEnoughArguments("callback", 1, arguments.len);                      return .zero;                  } -                arguments[0].protect(); -                const name = comptime @as(string, @tagName(this_hook)); -                @field(DescribeScope.active, name).append(getAllocator(globalThis), arguments[0]) catch unreachable; +                const cb = arguments.ptr[0]; +                if (!cb.isObject() or !cb.isCallable(globalThis.vm())) { +                    globalThis.throwInvalidArgumentType(@tagName(hook), "callback", "function"); +                    return .zero; +                } + +                cb.protect(); +                @field(DescribeScope.active.?, @tagName(hook)).append(getAllocator(globalThis), cb) catch unreachable;                  return JSC.JSValue.jsBoolean(true);              }          }.run; @@ -829,11 +829,22 @@ pub const DescribeScope = struct {      pub const beforeAll = createCallback(.beforeAll);      pub const beforeEach = createCallback(.beforeEach); -    pub fn execCallback(this: *DescribeScope, globalObject: *JSC.JSGlobalObject, comptime hook: LifecycleHook) JSValue { -        const name = comptime @as(string, @tagName(hook)); -        var hooks: []JSC.JSValue = @field(this, name).items; -        for (hooks, 0..) |cb, i| { -            if (cb.isEmpty()) continue; +    pub fn execCallback(this: *DescribeScope, globalObject: *JSC.JSGlobalObject, comptime hook: LifecycleHook) ?JSValue { +        var hooks = &@field(this, @tagName(hook)); +        defer { +            if (comptime hook == .beforeAll or hook == .afterAll) { +                hooks.clearAndFree(getAllocator(globalObject)); +            } +        } + +        for (hooks.items) |cb| { +            std.debug.assert(cb.isObject()); +            std.debug.assert(cb.isCallable(globalObject.vm())); +            defer { +                if (comptime hook == .beforeAll or hook == .afterAll) { +                    cb.unprotect(); +                } +            }              const pending_test = Jest.runner.?.pending_test;              // forbid `expect()` within hooks @@ -843,20 +854,23 @@ pub const DescribeScope = struct {              Jest.runner.?.did_pending_test_fail = false;              const vm = VirtualMachine.get(); -            var result: JSC.JSValue = if (cb.getLength(globalObject) > 0) brk: { -                this.done = false; -                const done_func = JSC.NewFunctionWithData( -                    globalObject, -                    ZigString.static("done"), -                    0, -                    DescribeScope.onDone, -                    false, -                    this, -                ); -                var result = cb.call(globalObject, &.{done_func}); -                vm.waitFor(&this.done); -                break :brk result; -            } else cb.call(globalObject, &.{}); +            var result: JSC.JSValue = switch (cb.getLength(globalObject)) { +                0 => cb.call(globalObject, &.{}), +                else => brk: { +                    this.done = false; +                    const done_func = JSC.NewFunctionWithData( +                        globalObject, +                        ZigString.static("done"), +                        0, +                        DescribeScope.onDone, +                        false, +                        this, +                    ); +                    var result = cb.call(globalObject, &.{done_func}); +                    vm.waitFor(&this.done); +                    break :brk result; +                }, +            };              if (result.asAnyPromise()) |promise| {                  if (promise.status(globalObject.vm()) == .Pending) {                      result.protect(); @@ -870,19 +884,28 @@ pub const DescribeScope = struct {              Jest.runner.?.pending_test = pending_test;              Jest.runner.?.did_pending_test_fail = orig_did_pending_test_fail;              if (result.isAnyError()) return result; - -            if (comptime hook == .beforeAll or hook == .afterAll) { -                hooks[i] = JSC.JSValue.zero; -            }          } -        return JSValue.zero; +        return null;      }      pub fn runGlobalCallbacks(globalThis: *JSC.JSGlobalObject, comptime hook: LifecycleHook) ?JSValue {          // global callbacks -        for (@field(Jest.runner.?.global_callbacks, @tagName(hook)).items) |cb| { -            if (cb.isEmpty()) continue; +        var hooks = &@field(Jest.runner.?.global_callbacks, @tagName(hook)); +        defer { +            if (comptime hook == .beforeAll or hook == .afterAll) { +                hooks.clearAndFree(getAllocator(globalThis)); +            } +        } + +        for (hooks.items) |cb| { +            std.debug.assert(cb.isObject()); +            std.debug.assert(cb.isCallable(globalThis.vm())); +            defer { +                if (comptime hook == .beforeAll or hook == .afterAll) { +                    cb.unprotect(); +                } +            }              const pending_test = Jest.runner.?.pending_test;              // forbid `expect()` within hooks @@ -909,28 +932,40 @@ pub const DescribeScope = struct {              if (result.isAnyError()) return result;          } -        if (comptime hook == .beforeAll or hook == .afterAll) { -            @field(Jest.runner.?.global_callbacks, @tagName(hook)).items.len = 0; -        } -          return null;      } +    fn runBeforeCallbacks(this: *DescribeScope, globalObject: *JSC.JSGlobalObject, comptime hook: LifecycleHook) ?JSValue { +        if (this.parent) |scope| { +            if (scope.runBeforeCallbacks(globalObject, hook)) |err| { +                return err; +            } +        } +        return this.execCallback(globalObject, hook); +    } +      pub fn runCallback(this: *DescribeScope, globalObject: *JSC.JSGlobalObject, comptime hook: LifecycleHook) JSValue { +        if (comptime hook == .afterAll or hook == .afterEach) { +            var parent: ?*DescribeScope = this; +            while (parent) |scope| { +                if (scope.execCallback(globalObject, hook)) |err| { +                    return err; +                } +                parent = scope.parent; +            } +        } +          if (runGlobalCallbacks(globalObject, hook)) |err| {              return err;          } -        var parent = this.parent; -        while (parent) |scope| { -            const ret = scope.execCallback(globalObject, hook); -            if (!ret.isEmpty()) { -                return ret; +        if (comptime hook == .beforeAll or hook == .beforeEach) { +            if (this.runBeforeCallbacks(globalObject, hook)) |err| { +                return err;              } -            parent = scope.parent;          } -        return this.execCallback(globalObject, hook); +        return .zero;      }      pub fn call(globalThis: *JSGlobalObject, callframe: *CallFrame) callconv(.C) JSValue { @@ -961,11 +996,8 @@ pub const DescribeScope = struct {          if (comptime is_bindgen) return undefined;          callback.protect();          defer callback.unprotect(); -        var original_active = active; -        defer active = original_active; -        if (this != module) -            this.parent = this.parent orelse active; -        active = this; +        this.push(); +        defer this.pop();          if (callback == .zero) {              this.runTests(globalObject); @@ -1064,9 +1096,8 @@ pub const DescribeScope = struct {          if (!this.isAllSkipped()) {              // Run the afterAll callbacks, in reverse order              // unless there were no tests for this scope -            const afterAll_result = this.execCallback(globalThis, .afterAll); -            if (!afterAll_result.isEmpty()) { -                globalThis.bunVM().runErrorHandler(afterAll_result, null); +            if (this.execCallback(globalThis, .afterAll)) |err| { +                globalThis.bunVM().runErrorHandler(err, null);              }          } @@ -1146,7 +1177,6 @@ pub const TestRunnerTask = struct {          // reset the global state for each test          // prior to the run -        DescribeScope.active = describe;          expect.active_test_expectation_counter = .{};          jsc_vm.last_reported_error_for_dedupe = .zero; @@ -1417,7 +1447,7 @@ inline fn createScope(          return .zero;      } -    const parent = DescribeScope.active; +    const parent = DescribeScope.active.?;      const allocator = getAllocator(globalThis);      const label = if (description == .zero)          "" diff --git a/test/js/bun/test/jest-hooks.test.ts b/test/js/bun/test/jest-hooks.test.ts index c99dc7759..618cdc4c6 100644 --- a/test/js/bun/test/jest-hooks.test.ts +++ b/test/js/bun/test/jest-hooks.test.ts @@ -1,5 +1,36 @@  import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from "bun:test"; +let hooks_run: string[] = []; + +beforeAll(() => hooks_run.push("global beforeAll")); +beforeEach(() => hooks_run.push("global beforeEach")); +afterAll(() => hooks_run.push("global afterAll")); +afterEach(() => hooks_run.push("global afterEach")); + +describe("describe scope", () => { +  beforeAll(() => hooks_run.push("describe beforeAll")); +  beforeEach(() => hooks_run.push("describe beforeEach")); +  afterAll(() => hooks_run.push("describe afterAll")); +  afterEach(() => hooks_run.push("describe afterEach")); + +  it("should run after beforeAll/beforeEach in the correct order", () => { +    expect(hooks_run).toEqual(["global beforeAll", "describe beforeAll", "global beforeEach", "describe beforeEach"]); +  }); + +  it("should run after afterEach/afterAll in the correct order", () => { +    expect(hooks_run).toEqual([ +      "global beforeAll", +      "describe beforeAll", +      "global beforeEach", +      "describe beforeEach", +      "describe afterEach", +      "global afterEach", +      "global beforeEach", +      "describe beforeEach", +    ]); +  }); +}); +  describe("test jest hooks in bun-test", () => {    describe("test beforeAll hook", () => {      let animal = "tiger"; diff --git a/test/js/bun/test/test-test.test.ts b/test/js/bun/test/test-test.test.ts index 7ecfdef11..5f732bb82 100644 --- a/test/js/bun/test/test-test.test.ts +++ b/test/js/bun/test/test-test.test.ts @@ -540,18 +540,18 @@ beforeEach: #2  beforeEach: TEST-FILE  beforeEach: one describe scope  -- inside one describe scope -- +afterEach: one describe scope +afterEach: TEST-FILE  afterEach: #1  afterEach: #2 -afterEach: TEST-FILE -afterEach: one describe scope  afterAll: one describe scope  beforeEach: #1  beforeEach: #2  beforeEach: TEST-FILE  -- the top-level test -- +afterEach: TEST-FILE  afterEach: #1  afterEach: #2 -afterEach: TEST-FILE  afterAll: TEST-FILE  afterAll: #1  afterAll: #2 | 
