Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 19 additions & 5 deletions src/runtime/test_runner/bun_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() })
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/runtime/test_runner/expect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)");
Expand Down
122 changes: 122 additions & 0 deletions test/js/bun/test/snapshot-tests/bun-snapshots.test.ts
Original file line number Diff line number Diff line change
@@ -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) } } });
Expand Down Expand Up @@ -62,3 +64,123 @@ describe("toMatchSnapshot errors", () => {
}).toThrow();
});
});

describe("snapshots taken in hooks", () => {
// Runs <dir>/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]);
Comment thread
claude[bot] marked this conversation as resolved.
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);
});
});
Loading