rename to test.filePatterns

This commit is contained in:
RiskyMH
2025-08-20 14:05:06 +10:00
parent 63157f00d1
commit fba0dd7e03
8 changed files with 548 additions and 354 deletions

View File

@@ -141,17 +141,17 @@ The root directory to run tests from. Default `.`.
root = "./__tests__"
```
### `test.glob`
### `test.filePatterns`
Customize the file patterns used to identify test files. Can be a single string or an array of strings. Patterns are resolved relative to the bunfig.toml directory.
Customize the file patterns used to identify test files. Can be a single string or an array of strings.
```toml
[test]
# Single pattern
glob = "*.mytest.js"
filePatterns = "*.mytest.js"
# Multiple patterns
glob = ["*.mytest.js", "*.spec.ts", "**/*.unit.js"]
filePatterns = ["*.mytest.js", "*.spec.ts", "**/*.unit.js"]
```
By default, Bun uses these patterns:
@@ -163,6 +163,11 @@ By default, Bun uses these patterns:
When custom patterns are specified, they completely replace the default patterns.
**Path resolution:**
- Patterns starting with `./` or `../` are resolved relative to the directory containing your `bunfig.toml` file
- Other glob patterns like `**/*.test.ts` are matched against relative paths from the current working directory
- Absolute paths are used as-is
### `test.preload`
Same as the top-level `preload` field, but only applies to `bun test`.

View File

@@ -20,18 +20,18 @@ The `root` option specifies a root directory for test discovery, overriding the
root = "src" # Only scan for tests in the src directory
```
#### glob
#### filePatterns
The `glob` option allows you to customize the patterns used to identify test files, overriding the default patterns. You can specify a single string or an array of strings.
The `filePatterns` option allows you to customize the patterns used to identify test files, overriding the default patterns. You can specify a single string or an array of strings.
```toml
[test]
glob = "*.mytest.js" # Single pattern
filePatterns = "*.mytest.js" # Single pattern
```
```toml
[test]
glob = ["*.mytest.js", "*.spec.ts", "**/*.unit.js"] # Multiple patterns
filePatterns = ["*.mytest.js", "*.spec.ts", "**/*.unit.js"] # Multiple patterns
```
By default, `bun test` searches for files matching these patterns:
@@ -41,7 +41,12 @@ By default, `bun test` searches for files matching these patterns:
- `*.spec.{js|jsx|ts|tsx}`
- `*_spec.{js|jsx|ts|tsx}`
When you specify custom `glob` patterns, these default patterns are completely replaced with your custom ones. The patterns are resolved relative to the directory containing your `bunfig.toml` file.
When you specify custom patterns, these default patterns are completely replaced with your custom ones.
**Path resolution:**
- Patterns starting with `./` or `../` are resolved relative to the directory containing your `bunfig.toml` file
- Other glob patterns like `**/*.test.ts` are matched against relative paths from the current working directory
- Absolute paths are used as-is
### Reporters

View File

@@ -353,7 +353,7 @@ pub const Bunfig = struct {
}
}
if (test_.get("glob")) |expr| brk: {
if (test_.get("filePatterns")) |expr| brk: {
// Get the directory containing the bunfig file for relative path resolution
const bunfig_dir = std.fs.path.dirname(this.source.path.text) orelse "./";
@@ -361,35 +361,52 @@ pub const Bunfig = struct {
.e_string => |str| {
const pattern = try str.string(allocator);
const patterns = try allocator.alloc(string, 1);
// Resolve pattern relative to bunfig.toml directory if it's not absolute
// Only resolve relative to bunfig.toml if it starts with ./ or ../
if (std.fs.path.isAbsolute(pattern)) {
patterns[0] = pattern;
} else if (strings.startsWith(pattern, "./") or strings.startsWith(pattern, "../")) {
// Resolve any ../ in the path
var buffer: bun.PathBuffer = undefined;
const resolved = bun.path.joinAbsStringBuf(bunfig_dir, &buffer, &[_][]const u8{pattern}, .auto);
patterns[0] = try allocator.dupe(u8, resolved);
} else {
patterns[0] = try std.fs.path.join(allocator, &[_][]const u8{ bunfig_dir, pattern });
// Use pattern as-is for glob patterns like **/*.test.ts
patterns[0] = pattern;
}
this.ctx.test_options.glob_patterns = patterns;
this.ctx.test_options.file_patterns = patterns;
},
.e_array => |arr| {
if (arr.items.len == 0) break :brk;
// Set empty array to explicitly disable test discovery
if (arr.items.len == 0) {
const patterns = try allocator.alloc(string, 0);
this.ctx.test_options.file_patterns = patterns;
break :brk;
}
const patterns = try allocator.alloc(string, arr.items.len);
for (arr.items.slice(), 0..) |item, i| {
if (item.data != .e_string) {
try this.addError(item.loc, "test.glob array must contain only strings");
try this.addError(item.loc, "test.filePatterns array must contain only strings");
return;
}
const pattern = try item.data.e_string.string(allocator);
// Resolve pattern relative to bunfig.toml directory if it's not absolute
// Only resolve relative to bunfig.toml if it starts with ./ or ../
if (std.fs.path.isAbsolute(pattern)) {
patterns[i] = pattern;
} else if (strings.startsWith(pattern, "./") or strings.startsWith(pattern, "../")) {
// Resolve any ../ in the path
var buffer: bun.PathBuffer = undefined;
const resolved = bun.path.joinAbsStringBuf(bunfig_dir, &buffer, &[_][]const u8{pattern}, .auto);
patterns[i] = try allocator.dupe(u8, resolved);
} else {
patterns[i] = try std.fs.path.join(allocator, &[_][]const u8{ bunfig_dir, pattern });
// Use pattern as-is for glob patterns like **/*.test.ts
patterns[i] = pattern;
}
}
this.ctx.test_options.glob_patterns = patterns;
this.ctx.test_options.file_patterns = patterns;
},
else => {
try this.addError(expr.loc, "test.glob must be a string or array of strings");
try this.addError(expr.loc, "test.filePatterns must be a string or array of strings");
return;
},
}

View File

@@ -328,8 +328,8 @@ pub const Command = struct {
test_filter_pattern: ?[]const u8 = null,
test_filter_regex: ?*RegularExpression = null,
/// Test file glob patterns. If specified, these override the default test discovery patterns.
glob_patterns: ?[]const string = null,
/// Test file patterns. If specified, these override the default test discovery patterns.
file_patterns: ?[]const string = null,
file_reporter: ?TestCommand.FileReporter = null,
reporter_outfile: ?[]const u8 = null,

View File

@@ -15,7 +15,7 @@ options: *BundleOptions,
has_iterated: bool = false,
search_count: usize = 0,
/// Custom glob patterns for test files. If set, these override the default patterns.
custom_glob_patterns: ?[]const string = null,
custom_file_patterns: ?[]const string = null,
const log = bun.Output.scoped(.jest, .hidden);
const Fifo = std.fifo.LinearFifo(ScanEntry, .Dynamic);
@@ -34,7 +34,7 @@ pub fn init(
alloc: Allocator,
transpiler: *Transpiler,
initial_results_capacity: usize,
custom_glob_patterns: ?[]const string,
custom_file_patterns: ?[]const string,
) Allocator.Error!Scanner {
const results = try std.ArrayListUnmanaged(bun.PathString).initCapacity(
alloc,
@@ -45,7 +45,7 @@ pub fn init(
.options = &transpiler.options,
.fs = transpiler.fs,
.test_files = results,
.custom_glob_patterns = custom_glob_patterns,
.custom_file_patterns = custom_file_patterns,
};
}
@@ -134,14 +134,8 @@ pub fn couldBeTestFile(this: *Scanner, name: []const u8, comptime needs_test_suf
if (extname.len == 0 or !this.options.loader(extname).isJavaScriptLike()) return false;
if (comptime !needs_test_suffix) return true;
// If custom glob patterns are provided, use them instead of default patterns
if (this.custom_glob_patterns) |patterns| {
for (patterns) |pattern| {
if (bun.glob.match(bun.default_allocator, pattern, name).matches()) {
return true;
}
}
return false;
if (this.custom_file_patterns != null) {
return true;
}
// Fall back to default test name suffixes
@@ -206,18 +200,41 @@ pub fn next(this: *Scanner, entry: *FileSystem.Entry, fd: bun.StoredFileDescript
if (!entry.abs_path.isEmpty()) return;
this.search_count += 1;
if (!this.couldBeTestFile(name, true)) return;
const parts = &[_][]const u8{ entry.dir, entry.base() };
const path = this.fs.absBuf(parts, &this.open_dir_buf);
if (this.custom_file_patterns) |patterns| {
if (patterns.len == 0) return;
if (!this.doesAbsolutePathMatchFilter(path)) {
const parts = &[_][]const u8{ entry.dir, entry.base() };
const path = this.fs.absBuf(parts, &this.open_dir_buf);
const rel_path = bun.path.relative(this.fs.top_level_dir, path);
if (!this.doesPathMatchFilter(rel_path)) return;
}
entry.abs_path = bun.PathString.init(this.fs.filename_store.append(@TypeOf(path), path) catch unreachable);
this.test_files.append(this.allocator(), entry.abs_path) catch unreachable;
var matches = false;
for (patterns) |pattern| {
const path_to_match = if (std.fs.path.isAbsolute(pattern)) path else rel_path;
if (bun.glob.match(bun.default_allocator, pattern, path_to_match).matches()) {
matches = true;
break;
}
}
if (!matches) return;
entry.abs_path = bun.PathString.init(this.fs.filename_store.append(@TypeOf(path), path) catch unreachable);
this.test_files.append(this.allocator(), entry.abs_path) catch unreachable;
} else {
if (!this.couldBeTestFile(name, true)) return;
const parts = &[_][]const u8{ entry.dir, entry.base() };
const path = this.fs.absBuf(parts, &this.open_dir_buf);
if (!this.doesAbsolutePathMatchFilter(path)) {
const rel_path = bun.path.relative(this.fs.top_level_dir, path);
if (!this.doesPathMatchFilter(rel_path)) return;
}
entry.abs_path = bun.PathString.init(this.fs.filename_store.append(@TypeOf(path), path) catch unreachable);
this.test_files.append(this.allocator(), entry.abs_path) catch unreachable;
}
},
}
}

View File

@@ -1424,7 +1424,7 @@ pub const TestCommand = struct {
//
try vm.ensureDebugger(false);
var scanner = Scanner.init(ctx.allocator, &vm.transpiler, ctx.positionals.len, ctx.test_options.glob_patterns) catch bun.outOfMemory();
var scanner = Scanner.init(ctx.allocator, &vm.transpiler, ctx.positionals.len, ctx.test_options.file_patterns) catch bun.outOfMemory();
defer scanner.deinit();
const has_relative_path = for (ctx.positionals) |arg| {
if (std.fs.path.isAbsolute(arg) or

View File

@@ -0,0 +1,463 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDirWithFiles } from "harness";
test("bunfig test.filePatterns with single string pattern", async () => {
const dir = tempDirWithFiles("test-filepatterns-single", {
"bunfig.toml": `
[test]
filePatterns = "*.mytest.js"
`,
"example.mytest.js": `
import { test, expect } from "bun:test";
test("custom glob test", () => {
expect(1).toBe(1);
});
`,
"example.test.js": `
import { test, expect } from "bun:test";
test("default pattern test", () => {
expect(1).toBe(1);
});
`,
"example.spec.js": `
import { test, expect } from "bun:test";
test("spec test", () => {
expect(1).toBe(1);
});
`,
});
const proc = Bun.spawn({
cmd: [bunExe(), "test"],
env: bunEnv,
cwd: dir,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(exitCode).toBe(0);
// Test output goes to stderr by default
expect(stderr).toContain("1 pass");
// Verify only the mytest.js file was run (1 test)
expect(stderr).toContain("Ran 1 test");
});
test("bunfig test.filePatterns with array of patterns", async () => {
const dir = tempDirWithFiles("test-filepatterns-array", {
"bunfig.toml": `
[test]
filePatterns = ["*.custom.js", "*.mytest.ts"]
`,
"example.custom.js": `
import { test, expect } from "bun:test";
test("custom js test", () => {
expect(1).toBe(1);
});
`,
"example.mytest.ts": `
import { test, expect } from "bun:test";
test("custom ts test", () => {
expect(1).toBe(1);
});
`,
"example.test.js": `
import { test, expect } from "bun:test";
test("default pattern test", () => {
expect(1).toBe(1);
});
`,
});
const proc = Bun.spawn({
cmd: [bunExe(), "test", "--reporter", "junit", "--reporter-outfile", "results.xml"],
env: bunEnv,
cwd: dir,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(exitCode).toBe(0);
// 2 tests should run (custom.js and mytest.ts)
expect(stderr).toContain("2 pass");
expect(stderr).toContain("Ran 2 tests");
});
test("bunfig test.filePatterns with nested directories", async () => {
const dir = tempDirWithFiles("test-filepatterns-nested", {
"bunfig.toml": `
[test]
filePatterns = "**/*.custom.js"
`,
"src/example.custom.js": `
import { test, expect } from "bun:test";
test("nested custom test", () => {
expect(1).toBe(1);
});
`,
"lib/utils/helper.custom.js": `
import { test, expect } from "bun:test";
test("deeply nested custom test", () => {
expect(1).toBe(1);
});
`,
"example.test.js": `
import { test, expect } from "bun:test";
test("root test", () => {
expect(1).toBe(1);
});
`,
});
const proc = Bun.spawn({
cmd: [bunExe(), "test", "--reporter", "junit", "--reporter-outfile", "results.xml"],
env: bunEnv,
cwd: dir,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(exitCode).toBe(0);
// 2 custom tests in nested directories
expect(stderr).toContain("2 pass");
expect(stderr).toContain("Ran 2 tests");
});
test("bunfig test.filePatterns with relative paths", async () => {
const dir = tempDirWithFiles("test-filepatterns-relative", {
"bunfig.toml": `
[test]
filePatterns = "tests/*.unit.js"
`,
"tests/example.unit.js": `
import { test, expect } from "bun:test";
test("unit test", () => {
expect(1).toBe(1);
});
`,
"tests/example.integration.js": `
import { test, expect } from "bun:test";
test("integration test", () => {
expect(1).toBe(1);
});
`,
"example.test.js": `
import { test, expect } from "bun:test";
test("default test", () => {
expect(1).toBe(1);
});
`,
});
const proc = Bun.spawn({
cmd: [bunExe(), "test", "--reporter", "junit", "--reporter-outfile", "results.xml"],
env: bunEnv,
cwd: dir,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(exitCode).toBe(0);
// Only 1 test from tests/*.unit.js
expect(stderr).toContain("1 pass");
expect(stderr).toContain("Ran 1 test");
});
test("bunfig test.filePatterns error handling for invalid type", async () => {
const dir = tempDirWithFiles("test-filepatterns-invalid", {
"bunfig.toml": `
[test]
filePatterns = 123
`,
"example.test.js": `
import { test, expect } from "bun:test";
test("test", () => {
expect(1).toBe(1);
});
`,
});
const proc = Bun.spawn({
cmd: [bunExe(), "test"],
env: bunEnv,
cwd: dir,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(exitCode).toBe(1);
expect(stderr).toContain("test.filePatterns must be a string or array of strings");
});
test("bunfig test.filePatterns error handling for invalid array element", async () => {
const dir = tempDirWithFiles("test-filepatterns-invalid-array", {
"bunfig.toml": `
[test]
filePatterns = ["*.test.js", 123]
`,
"example.test.js": `
import { test, expect } from "bun:test";
test("test", () => {
expect(1).toBe(1);
});
`,
});
const proc = Bun.spawn({
cmd: [bunExe(), "test"],
env: bunEnv,
cwd: dir,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(exitCode).toBe(1);
expect(stderr).toContain("test.filePatterns array must contain only strings");
});
test("bunfig test.filePatterns fallback to default patterns when not specified", async () => {
const dir = tempDirWithFiles("test-filepatterns-fallback", {
"bunfig.toml": `
[test]
# No glob specified
`,
"example.test.js": `
import { test, expect } from "bun:test";
test("default test pattern", () => {
expect(1).toBe(1);
});
`,
"example.spec.ts": `
import { test, expect } from "bun:test";
test("default spec pattern", () => {
expect(1).toBe(1);
});
`,
"example.custom.js": `
import { test, expect } from "bun:test";
test("non-matching pattern", () => {
expect(1).toBe(1);
});
`,
});
const proc = Bun.spawn({
cmd: [bunExe(), "test", "--reporter", "junit", "--reporter-outfile", "results.xml"],
env: bunEnv,
cwd: dir,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(exitCode).toBe(0);
// Should run default test patterns (test and spec files)
expect(stderr).toContain("2 pass");
expect(stderr).toContain("Ran 2 tests");
});
test("bunfig test.filePatterns resolves paths relative to bunfig.toml location", async () => {
const dir = tempDirWithFiles("test-filepatterns-cwd", {
"bunfig.toml": `
[test]
filePatterns = "mydir/*.mytest.js"
`,
"mydir/example.mytest.js": `
import { test, expect } from "bun:test";
test("relative path test", () => {
expect(1).toBe(1);
});
`,
"example.test.js": `
import { test, expect } from "bun:test";
test("root test", () => {
expect(1).toBe(1);
});
`,
});
const proc = Bun.spawn({
cmd: [bunExe(), "test", "--reporter", "junit", "--reporter-outfile", "results.xml"],
env: bunEnv,
cwd: dir,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(exitCode).toBe(0);
// Only the nested test file should run
expect(stderr).toContain("1 pass");
expect(stderr).toContain("Ran 1 test");
});
test("bunfig test.filePatterns with ./ relative path resolution", async () => {
// Test that ./ patterns are resolved relative to bunfig.toml location
const dir = tempDirWithFiles("test-filepatterns-relative-dot", {
"project/bunfig.toml": `
[test]
filePatterns = "./tests/*.mytest.js"
`,
"project/tests/example.mytest.js": `
import { test, expect } from "bun:test";
test("relative dot test", () => {
expect(1).toBe(1);
});
`,
"tests/example.mytest.js": `
import { test, expect } from "bun:test";
test("should not run", () => {
expect(1).toBe(1);
});
`,
"project/example.test.js": `
import { test, expect } from "bun:test";
test("default test", () => {
expect(1).toBe(1);
});
`,
});
// Run from the project directory where bunfig.toml is located
const proc = Bun.spawn({
cmd: [bunExe(), "test"],
env: bunEnv,
cwd: `${dir}/project`,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(exitCode).toBe(0);
// Should only run the test in ./tests relative to bunfig.toml location
expect(stderr).toContain("1 pass");
expect(stderr).toContain("Ran 1 test");
});
test.skip("bunfig test.filePatterns with ../ relative path resolution", async () => {
// Note: This test is skipped because bun test doesn't scan parent directories
// when running from a subdirectory, which is expected behavior
const dir = tempDirWithFiles("test-filepatterns-relative-dotdot", {
"project/config/bunfig.toml": `
[test]
filePatterns = "../tests/*.mytest.js"
`,
"project/tests/example.mytest.js": `
import { test, expect } from "bun:test";
test("relative parent test", () => {
expect(1).toBe(1);
});
`,
"project/config/tests/example.mytest.js": `
import { test, expect } from "bun:test";
test("should not run", () => {
expect(1).toBe(1);
});
`,
"project/config/example.test.js": `
import { test, expect } from "bun:test";
test("default test", () => {
expect(1).toBe(1);
});
`,
});
// Run from the config directory where bunfig.toml is located
const proc = Bun.spawn({
cmd: [bunExe(), "test"],
env: bunEnv,
cwd: `${dir}/project/config`,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(exitCode).toBe(0);
// Should only run the test in ../tests relative to bunfig.toml
expect(stderr).toContain("1 pass");
expect(stderr).toContain("Ran 1 test");
});
test("bunfig test.filePatterns with absolute paths", async () => {
const dir = tempDirWithFiles("test-filepatterns-absolute", {
"bunfig.toml": "", // Will be written after we know the absolute path
"tests/example.mytest.js": `
import { test, expect } from "bun:test";
test("absolute path test", () => {
expect(1).toBe(1);
});
`,
"example.test.js": `
import { test, expect } from "bun:test";
test("default test", () => {
expect(1).toBe(1);
});
`,
});
// Write bunfig.toml with absolute path
const absolutePath = `${dir}/tests/*.mytest.js`;
await Bun.write(`${dir}/bunfig.toml`, `
[test]
filePatterns = "${absolutePath}"
`);
const proc = Bun.spawn({
cmd: [bunExe(), "test"],
env: bunEnv,
cwd: dir,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(exitCode).toBe(0);
// Should only run the test specified by absolute path
expect(stderr).toContain("1 pass");
expect(stderr).toContain("Ran 1 test");
});
test("bunfig test.filePatterns with empty array should not match any files", async () => {
const dir = tempDirWithFiles("test-filepatterns-empty", {
"bunfig.toml": `
[test]
filePatterns = []
`,
"example.test.js": `
import { test, expect } from "bun:test";
test("test", () => {
expect(1).toBe(1);
});
`,
});
const proc = Bun.spawn({
cmd: [bunExe(), "test"],
env: bunEnv,
cwd: dir,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
// When no test files are found, bun test exits with code 1
expect(exitCode).toBe(1);
expect(stderr).toContain("0 test files matching");
});

View File

@@ -1,313 +0,0 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDirWithFiles } from "harness";
test("bunfig test.glob with single string pattern", async () => {
const dir = tempDirWithFiles("test-glob-single", {
"bunfig.toml": `
[test]
glob = "*.mytest.js"
`,
"example.mytest.js": `
import { test, expect } from "bun:test";
test("custom glob test", () => {
expect(1).toBe(1);
});
`,
"example.test.js": `
import { test, expect } from "bun:test";
test("default pattern test", () => {
expect(1).toBe(1);
});
`,
"example.spec.js": `
import { test, expect } from "bun:test";
test("spec test", () => {
expect(1).toBe(1);
});
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "test", "--reporter", "junit", "--reporter-outfile", "results.xml"],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(exitCode).toBe(0);
expect(stdout).toContain("custom glob test");
expect(stdout).not.toContain("default pattern test");
expect(stdout).not.toContain("spec test");
});
test("bunfig test.glob with array of patterns", async () => {
const dir = tempDirWithFiles("test-glob-array", {
"bunfig.toml": `
[test]
glob = ["*.custom.js", "*.mytest.ts"]
`,
"example.custom.js": `
import { test, expect } from "bun:test";
test("custom js test", () => {
expect(1).toBe(1);
});
`,
"example.mytest.ts": `
import { test, expect } from "bun:test";
test("custom ts test", () => {
expect(1).toBe(1);
});
`,
"example.test.js": `
import { test, expect } from "bun:test";
test("default pattern test", () => {
expect(1).toBe(1);
});
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "test", "--reporter", "junit", "--reporter-outfile", "results.xml"],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(exitCode).toBe(0);
expect(stdout).toContain("custom js test");
expect(stdout).toContain("custom ts test");
expect(stdout).not.toContain("default pattern test");
});
test("bunfig test.glob with nested directories", async () => {
const dir = tempDirWithFiles("test-glob-nested", {
"bunfig.toml": `
[test]
glob = "**/*.custom.js"
`,
"src/example.custom.js": `
import { test, expect } from "bun:test";
test("nested custom test", () => {
expect(1).toBe(1);
});
`,
"lib/utils/helper.custom.js": `
import { test, expect } from "bun:test";
test("deeply nested custom test", () => {
expect(1).toBe(1);
});
`,
"example.test.js": `
import { test, expect } from "bun:test";
test("root test", () => {
expect(1).toBe(1);
});
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "test", "--reporter", "junit", "--reporter-outfile", "results.xml"],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(exitCode).toBe(0);
expect(stdout).toContain("nested custom test");
expect(stdout).toContain("deeply nested custom test");
expect(stdout).not.toContain("root test");
});
test("bunfig test.glob with relative paths", async () => {
const dir = tempDirWithFiles("test-glob-relative", {
"bunfig.toml": `
[test]
glob = "tests/*.unit.js"
`,
"tests/example.unit.js": `
import { test, expect } from "bun:test";
test("unit test", () => {
expect(1).toBe(1);
});
`,
"tests/example.integration.js": `
import { test, expect } from "bun:test";
test("integration test", () => {
expect(1).toBe(1);
});
`,
"example.test.js": `
import { test, expect } from "bun:test";
test("default test", () => {
expect(1).toBe(1);
});
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "test", "--reporter", "junit", "--reporter-outfile", "results.xml"],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(exitCode).toBe(0);
expect(stdout).toContain("unit test");
expect(stdout).not.toContain("integration test");
expect(stdout).not.toContain("default test");
});
test("bunfig test.glob error handling for invalid type", async () => {
const dir = tempDirWithFiles("test-glob-invalid", {
"bunfig.toml": `
[test]
glob = 123
`,
"example.test.js": `
import { test, expect } from "bun:test";
test("test", () => {
expect(1).toBe(1);
});
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "test"],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(exitCode).toBe(1);
expect(stderr).toContain("test.glob must be a string or array of strings");
});
test("bunfig test.glob error handling for invalid array element", async () => {
const dir = tempDirWithFiles("test-glob-invalid-array", {
"bunfig.toml": `
[test]
glob = ["*.test.js", 123]
`,
"example.test.js": `
import { test, expect } from "bun:test";
test("test", () => {
expect(1).toBe(1);
});
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "test"],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(exitCode).toBe(1);
expect(stderr).toContain("test.glob array must contain only strings");
});
test("bunfig test.glob fallback to default patterns when not specified", async () => {
const dir = tempDirWithFiles("test-glob-fallback", {
"bunfig.toml": `
[test]
# No glob specified
`,
"example.test.js": `
import { test, expect } from "bun:test";
test("default test pattern", () => {
expect(1).toBe(1);
});
`,
"example.spec.ts": `
import { test, expect } from "bun:test";
test("default spec pattern", () => {
expect(1).toBe(1);
});
`,
"example.custom.js": `
import { test, expect } from "bun:test";
test("non-matching pattern", () => {
expect(1).toBe(1);
});
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "test", "--reporter", "junit", "--reporter-outfile", "results.xml"],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(exitCode).toBe(0);
expect(stdout).toContain("default test pattern");
expect(stdout).toContain("default spec pattern");
expect(stdout).not.toContain("non-matching pattern");
});
test("bunfig test.glob resolves paths relative to bunfig.toml location", async () => {
const dir = tempDirWithFiles("test-glob-cwd", {
"bunfig.toml": `
[test]
glob = "mydir/*.mytest.js"
`,
"mydir/example.mytest.js": `
import { test, expect } from "bun:test";
test("relative path test", () => {
expect(1).toBe(1);
});
`,
"example.test.js": `
import { test, expect } from "bun:test";
test("root test", () => {
expect(1).toBe(1);
});
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "test", "--reporter", "junit", "--reporter-outfile", "results.xml"],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(exitCode).toBe(0);
expect(stdout).toContain("relative path test");
expect(stdout).not.toContain("root test");
});
test("bunfig test.glob with empty array should not match any files", async () => {
const dir = tempDirWithFiles("test-glob-empty", {
"bunfig.toml": `
[test]
glob = []
`,
"example.test.js": `
import { test, expect } from "bun:test";
test("test", () => {
expect(1).toBe(1);
});
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "test"],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
// When no test files are found, bun test should exit with code 0 but find 0 tests
expect(stdout).toContain("0 pass");
});