diff --git a/src/runtime/test_runner/bun_test.rs b/src/runtime/test_runner/bun_test.rs
index 8f2ee1f522c2..20feab3ce733 100644
--- a/src/runtime/test_runner/bun_test.rs
+++ b/src/runtime/test_runner/bun_test.rs
@@ -1463,17 +1463,31 @@ impl RefDataValue {
}
pub(crate) fn entry<'a>(&self, buntest: &'a mut BunTest) -> Option<&'a mut ExecutionEntry> {
+ let sequence = self.running_sequence(buntest)?;
+ // SAFETY: `active_entry` is a valid intrusive node while the sequence is live.
+ unsafe { sequence.active_entry.map(|p| &mut *p.as_ptr()) }
+ }
+
+ /// The entry a snapshot is named after: the test, also while its beforeEach/afterEach run (jest's currentTestName).
+ pub(crate) fn snapshot_entry<'a>(&self, buntest: &'a mut BunTest) -> Option<&'a ExecutionEntry> {
+ let sequence = self.running_sequence(buntest)?;
+ // beforeAll/afterAll sequences have no test, so their snapshots are named after the hook entry itself.
+ let entry = sequence.test_entry.or(sequence.active_entry)?;
+ // SAFETY: `test_entry`/`active_entry` are valid intrusive nodes while the sequence is live.
+ Some(unsafe { &*entry.as_ptr() })
+ }
+
+ /// The sequence this state was captured in, while it is still the one running (unknown inside a concurrent group).
+ fn running_sequence<'a>(&self, buntest: &'a mut BunTest) -> Option<&'a Execution::ExecutionSequence> {
if !matches!(self, RefDataValue::Execution { .. }) {
return None;
}
if buntest.phase != Phase::Execution {
return None;
}
- let (the_sequence, _) = buntest.execution.get_current_and_valid_execution_sequence(self)?;
- // SAFETY: `the_sequence` is a NonNull into `execution.sequences`; deref
- // at point-of-use only. `active_entry` is a valid intrusive node while
- // the sequence is live.
- unsafe { the_sequence.as_ref().active_entry.map(|p| &mut *p.as_ptr()) }
+ let (sequence, _) = buntest.execution.get_current_and_valid_execution_sequence(self)?;
+ // SAFETY: `sequence` points into `buntest.execution.sequences`, which stays borrowed for `'a`.
+ Some(unsafe { &*sequence.as_ptr() })
}
}
diff --git a/src/runtime/test_runner/expect.rs b/src/runtime/test_runner/expect.rs
index e7b3471e39c0..9da62b3398dc 100644
--- a/src/runtime/test_runner/expect.rs
+++ b/src/runtime/test_runner/expect.rs
@@ -625,7 +625,7 @@ impl Expect {
let buntest = buntest_strong.get();
let execution_entry = parent
.phase
- .entry(buntest)
+ .snapshot_entry(buntest)
.ok_or(crate::Error::SnapshotInConcurrentGroup)?;
let test_name: &[u8] = execution_entry.base.name.as_deref().unwrap_or(b"(unnamed)");
diff --git a/test/js/bun/test/snapshot-tests/bun-snapshots.test.ts b/test/js/bun/test/snapshot-tests/bun-snapshots.test.ts
index e670e343921f..5a023fa7124d 100644
--- a/test/js/bun/test/snapshot-tests/bun-snapshots.test.ts
+++ b/test/js/bun/test/snapshot-tests/bun-snapshots.test.ts
@@ -1,4 +1,6 @@
import { describe, expect, it, test } from "bun:test";
+import { writeFileSync } from "fs";
+import { bunEnv, bunExe, tempDir } from "harness";
test("it will create a snapshot file if it doesn't exist", () => {
expect({ a: { b: { c: false } }, c: 2, jkfje: 99238 }).toMatchSnapshot({ a: { b: { c: expect.any(Boolean) } } });
@@ -62,3 +64,123 @@ describe("toMatchSnapshot errors", () => {
}).toThrow();
});
});
+
+describe("snapshots taken in hooks", () => {
+ // Runs
/hooks.test.ts and returns the `key = value` entries of the .snap file it wrote, in file order.
+ async function runHooksFile(dir: string): Promise<{ exitCode: number; stderr: string; entries: string[] }> {
+ await using proc = Bun.spawn({
+ cmd: [bunExe(), "test", "./hooks.test.ts"],
+ env: { ...bunEnv, CI: "false" },
+ cwd: dir,
+ stdout: "ignore",
+ stderr: "pipe",
+ });
+ const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]);
+ const snap = await Bun.file(dir + "/__snapshots__/hooks.test.ts.snap").text();
+ const entries = [...snap.matchAll(/^exports\[`(.*)`\] = `(.*)`;$/gm)].map(m => `${m[1]} = ${m[2]}`);
+ return { exitCode, stderr, entries };
+ }
+
+ test.concurrent("beforeEach/afterEach/onTestFinished snapshots are named after the running test", async () => {
+ using dir = tempDir("snapshot-hook-names", {
+ "hooks.test.ts": /*js*/ `
+ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, onTestFinished, test } from "bun:test";
+ beforeEach(() => expect("top").toMatchSnapshot("top beforeEach"));
+ describe("outer", () => {
+ beforeEach(() => expect("outer").toMatchSnapshot("outer beforeEach"));
+ describe("inner", () => {
+ beforeAll(() => expect("ba").toMatchSnapshot("beforeAll"));
+ beforeEach(() => expect("inner").toMatchSnapshot("inner beforeEach"));
+ beforeEach(() => expect("inline").toMatchInlineSnapshot('"inline"'));
+ afterEach(() => expect("ae").toMatchSnapshot("afterEach"));
+ afterAll(() => expect("aa").toMatchSnapshot("afterAll"));
+ test("t1", () => {
+ afterEach(() => expect("ae in test").toMatchSnapshot("afterEach in test"));
+ onTestFinished(() => expect("otf").toMatchSnapshot("onTestFinished"));
+ expect("body").toMatchSnapshot("body");
+ expect("body").toMatchSnapshot();
+ });
+ test("t2", () => expect("body").toMatchSnapshot("body"));
+ });
+ });
+ `,
+ });
+ const { exitCode, stderr, entries } = await runHooksFile(String(dir));
+ expect(entries).toEqual([
+ // beforeAll/afterAll do not run for a test, so they keep the hook's own (unnamed) key
+ 'outer inner (unnamed): beforeAll 1 = "ba"',
+ 'outer inner t1: top beforeEach 1 = "top"',
+ 'outer inner t1: outer beforeEach 1 = "outer"',
+ 'outer inner t1: inner beforeEach 1 = "inner"',
+ 'outer inner t1: body 1 = "body"',
+ // the inline snapshot in beforeEach took number 1 of t1's unhinted counter, as in jest
+ 'outer inner t1 2 = "body"',
+ 'outer inner t1: afterEach in test 1 = "ae in test"',
+ 'outer inner t1: afterEach 1 = "ae"',
+ 'outer inner t1: onTestFinished 1 = "otf"',
+ 'outer inner t2: top beforeEach 1 = "top"',
+ 'outer inner t2: outer beforeEach 1 = "outer"',
+ 'outer inner t2: inner beforeEach 1 = "inner"',
+ 'outer inner t2: body 1 = "body"',
+ 'outer inner t2: afterEach 1 = "ae"',
+ 'outer inner (unnamed): afterAll 1 = "aa"',
+ ]);
+ expect(stderr).toContain(" 2 pass");
+ expect(stderr).toContain(" 0 fail");
+ expect(exitCode).toBe(0);
+ });
+
+ test.concurrent("per-test hooks share the test's counter, beforeAll/afterAll share the describe's", async () => {
+ using dir = tempDir("snapshot-hook-counters", {
+ "hooks.test.ts": /*js*/ `
+ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from "bun:test";
+ describe("d", () => {
+ beforeAll(() => expect("ba").toMatchSnapshot("setup"));
+ beforeEach(() => expect("be").toMatchSnapshot("setup"));
+ afterEach(() => expect("ae").toMatchSnapshot("setup"));
+ afterAll(() => expect("aa").toMatchSnapshot("setup"));
+ test("t1", () => expect("body").toMatchSnapshot("setup"));
+ });
+ `,
+ });
+ const { exitCode, stderr, entries } = await runHooksFile(String(dir));
+ expect(entries).toEqual([
+ 'd (unnamed): setup 1 = "ba"',
+ 'd t1: setup 1 = "be"',
+ 'd t1: setup 2 = "body"',
+ 'd t1: setup 3 = "ae"',
+ 'd (unnamed): setup 2 = "aa"',
+ ]);
+ expect(stderr).toContain(" 1 pass");
+ expect(stderr).toContain(" 0 fail");
+ expect(exitCode).toBe(0);
+ });
+
+ test.concurrent("adding a test does not invalidate the hook snapshots of the other tests", async () => {
+ const source = (extraTest: string) => /*js*/ `
+ import { afterEach, describe, expect, test } from "bun:test";
+ let value;
+ describe("d", () => {
+ afterEach(() => expect(value).toMatchSnapshot("afterEach"));
+ ${extraTest}
+ test("t1", () => { value = "from t1"; });
+ test("t2", () => { value = "from t2"; });
+ });
+ `;
+ using dir = tempDir("snapshot-hook-stability", { "hooks.test.ts": source("") });
+ const first = await runHooksFile(String(dir));
+ expect(first.entries).toEqual(['d t1: afterEach 1 = "from t1"', 'd t2: afterEach 1 = "from t2"']);
+ expect(first.exitCode).toBe(0);
+
+ writeFileSync(String(dir) + "/hooks.test.ts", source(`test("t0", () => { value = "from t0"; });`));
+ const second = await runHooksFile(String(dir));
+ expect(second.stderr).toContain(" 3 pass");
+ expect(second.stderr).toContain(" 0 fail");
+ expect(second.entries).toEqual([
+ 'd t1: afterEach 1 = "from t1"',
+ 'd t2: afterEach 1 = "from t2"',
+ 'd t0: afterEach 1 = "from t0"',
+ ]);
+ expect(second.exitCode).toBe(0);
+ });
+});