@@ -15,6 +15,8 @@ pub struct PackageJsonData {
pub mocha_package_path: Option<Arc<Path>>,
pub vitest_package_path: Option<Arc<Path>>,
pub jasmine_package_path: Option<Arc<Path>>,
+ pub bun_package_path: Option<Arc<Path>>,
+ pub node_package_path: Option<Arc<Path>>,
pub scripts: BTreeSet<(Arc<Path>, String)>,
pub package_manager: Option<&'static str>,
}
@@ -35,6 +37,8 @@ impl PackageJsonData {
let mut mocha_package_path = None;
let mut vitest_package_path = None;
let mut jasmine_package_path = None;
+ let mut bun_package_path = None;
+ let mut node_package_path = None;
if let Some(Value::Object(dependencies)) = package_json.get("devDependencies") {
if dependencies.contains_key("jest") {
jest_package_path.get_or_insert_with(|| path.clone());
@@ -48,6 +52,12 @@ impl PackageJsonData {
if dependencies.contains_key("jasmine") {
jasmine_package_path.get_or_insert_with(|| path.clone());
}
+ if dependencies.contains_key("@types/bun") {
+ bun_package_path.get_or_insert_with(|| path.clone());
+ }
+ if dependencies.contains_key("@types/node") {
+ node_package_path.get_or_insert_with(|| path.clone());
+ }
}
if let Some(Value::Object(dev_dependencies)) = package_json.get("dependencies") {
if dev_dependencies.contains_key("jest") {
@@ -62,6 +72,12 @@ impl PackageJsonData {
if dev_dependencies.contains_key("jasmine") {
jasmine_package_path.get_or_insert_with(|| path.clone());
}
+ if dev_dependencies.contains_key("@types/bun") {
+ bun_package_path.get_or_insert_with(|| path.clone());
+ }
+ if dev_dependencies.contains_key("@types/node") {
+ node_package_path.get_or_insert_with(|| path.clone());
+ }
}
let package_manager = package_json
@@ -74,6 +90,8 @@ impl PackageJsonData {
Some("yarn")
} else if value.starts_with("npm") {
Some("npm")
+ } else if value.starts_with("bun") {
+ Some("bun")
} else {
None
}
@@ -84,6 +102,8 @@ impl PackageJsonData {
mocha_package_path,
vitest_package_path,
jasmine_package_path,
+ bun_package_path,
+ node_package_path,
scripts,
package_manager,
}
@@ -100,6 +120,8 @@ impl PackageJsonData {
.jasmine_package_path
.take()
.or(other.jasmine_package_path);
+ self.bun_package_path = self.bun_package_path.take().or(other.bun_package_path);
+ self.node_package_path = self.node_package_path.take().or(other.node_package_path);
self.scripts.extend(other.scripts);
self.package_manager = self.package_manager.or(other.package_manager);
}
@@ -54,6 +54,12 @@ const TYPESCRIPT_VITEST_PACKAGE_PATH_VARIABLE: VariableName =
const TYPESCRIPT_JASMINE_PACKAGE_PATH_VARIABLE: VariableName =
VariableName::Custom(Cow::Borrowed("TYPESCRIPT_JASMINE_PACKAGE_PATH"));
+const TYPESCRIPT_BUN_PACKAGE_PATH_VARIABLE: VariableName =
+ VariableName::Custom(Cow::Borrowed("TYPESCRIPT_BUN_PACKAGE_PATH"));
+
+const TYPESCRIPT_NODE_PACKAGE_PATH_VARIABLE: VariableName =
+ VariableName::Custom(Cow::Borrowed("TYPESCRIPT_NODE_PACKAGE_PATH"));
+
#[derive(Clone, Debug, Default)]
struct PackageJsonContents(Arc<RwLock<HashMap<PathBuf, PackageJson>>>);
@@ -220,6 +226,65 @@ impl PackageJsonData {
});
}
+ if self.bun_package_path.is_some() {
+ task_templates.0.push(TaskTemplate {
+ label: format!("{} file test", "bun test".to_owned()),
+ command: "bun".to_owned(),
+ args: vec!["test".to_owned(), VariableName::File.template_value()],
+ cwd: Some(TYPESCRIPT_BUN_PACKAGE_PATH_VARIABLE.template_value()),
+ ..TaskTemplate::default()
+ });
+ task_templates.0.push(TaskTemplate {
+ label: format!("bun test {}", VariableName::Symbol.template_value(),),
+ command: "bun".to_owned(),
+ args: vec![
+ "test".to_owned(),
+ "--test-name-pattern".to_owned(),
+ format!("\"{}\"", VariableName::Symbol.template_value()),
+ VariableName::File.template_value(),
+ ],
+ tags: vec![
+ "ts-test".to_owned(),
+ "js-test".to_owned(),
+ "tsx-test".to_owned(),
+ ],
+ cwd: Some(TYPESCRIPT_BUN_PACKAGE_PATH_VARIABLE.template_value()),
+ ..TaskTemplate::default()
+ });
+ }
+
+ if self.node_package_path.is_some() {
+ task_templates.0.push(TaskTemplate {
+ label: format!("{} file test", "node test".to_owned()),
+ command: "node".to_owned(),
+ args: vec!["--test".to_owned(), VariableName::File.template_value()],
+ tags: vec![
+ "ts-test".to_owned(),
+ "js-test".to_owned(),
+ "tsx-test".to_owned(),
+ ],
+ cwd: Some(TYPESCRIPT_NODE_PACKAGE_PATH_VARIABLE.template_value()),
+ ..TaskTemplate::default()
+ });
+ task_templates.0.push(TaskTemplate {
+ label: format!("node test {}", VariableName::Symbol.template_value()),
+ command: "node".to_owned(),
+ args: vec![
+ "--test".to_owned(),
+ "--test-name-pattern".to_owned(),
+ format!("\"{}\"", VariableName::Symbol.template_value()),
+ VariableName::File.template_value(),
+ ],
+ tags: vec![
+ "ts-test".to_owned(),
+ "js-test".to_owned(),
+ "tsx-test".to_owned(),
+ ],
+ cwd: Some(TYPESCRIPT_NODE_PACKAGE_PATH_VARIABLE.template_value()),
+ ..TaskTemplate::default()
+ });
+ }
+
let script_name_counts: HashMap<_, usize> =
self.scripts
.iter()
@@ -493,6 +558,26 @@ impl ContextProvider for TypeScriptContextProvider {
.to_string(),
);
}
+
+ if let Some(path) = package_json_data.bun_package_path {
+ vars.insert(
+ TYPESCRIPT_BUN_PACKAGE_PATH_VARIABLE,
+ path.parent()
+ .unwrap_or(Path::new(""))
+ .to_string_lossy()
+ .to_string(),
+ );
+ }
+
+ if let Some(path) = package_json_data.node_package_path {
+ vars.insert(
+ TYPESCRIPT_NODE_PACKAGE_PATH_VARIABLE,
+ path.parent()
+ .unwrap_or(Path::new(""))
+ .to_string_lossy()
+ .to_string(),
+ );
+ }
}
}
Ok(vars)
@@ -1178,6 +1263,8 @@ mod tests {
mocha_package_path: Some(Path::new(path!("/root/package.json")).into()),
vitest_package_path: Some(Path::new(path!("/root/sub/package.json")).into()),
jasmine_package_path: None,
+ bun_package_path: None,
+ node_package_path: None,
scripts: [
(
Path::new(path!("/root/package.json")).into(),
@@ -1231,6 +1318,7 @@ mod tests {
]
);
}
+
#[test]
fn test_escaping_name() {
let cases = [
@@ -1264,4 +1352,110 @@ mod tests {
assert_eq!(replace_test_name_parameters(input), expected);
}
}
+
+ // The order of test runner tasks is based on inferred user preference:
+ // 1. Dedicated test runners (e.g., Jest, Vitest, Mocha, Jasmine) are prioritized.
+ // 2. Bun's built-in test runner (`bun test`) comes next.
+ // 3. Node.js's built-in test runner (`node --test`) is last.
+ // This hierarchy assumes that if a dedicated test framework is installed, it is the
+ // preferred testing mechanism. Between runtime-specific options, `bun test` is
+ // typically preferred over `node --test` when @types/bun is present.
+ #[gpui::test]
+ async fn test_task_ordering_with_multiple_test_runners(
+ executor: BackgroundExecutor,
+ cx: &mut TestAppContext,
+ ) {
+ cx.update(|cx| {
+ settings::init(cx);
+ Project::init_settings(cx);
+ language_settings::init(cx);
+ });
+
+ // Test case with all test runners present
+ let package_json_all_runners = json!({
+ "devDependencies": {
+ "@types/bun": "1.0.0",
+ "@types/node": "^20.0.0",
+ "jest": "29.0.0",
+ "mocha": "10.0.0",
+ "vitest": "1.0.0",
+ "jasmine": "5.0.0",
+ },
+ "scripts": {
+ "test": "jest"
+ }
+ })
+ .to_string();
+
+ let fs = FakeFs::new(executor);
+ fs.insert_tree(
+ path!("/root"),
+ json!({
+ "package.json": package_json_all_runners,
+ "file.js": "",
+ }),
+ )
+ .await;
+
+ let provider = TypeScriptContextProvider::new(fs.clone());
+
+ let package_json_data = cx
+ .update(|cx| {
+ provider.combined_package_json_data(
+ fs.clone(),
+ path!("/root").as_ref(),
+ rel_path("file.js"),
+ cx,
+ )
+ })
+ .await
+ .unwrap();
+
+ assert!(package_json_data.jest_package_path.is_some());
+ assert!(package_json_data.mocha_package_path.is_some());
+ assert!(package_json_data.vitest_package_path.is_some());
+ assert!(package_json_data.jasmine_package_path.is_some());
+ assert!(package_json_data.bun_package_path.is_some());
+ assert!(package_json_data.node_package_path.is_some());
+
+ let mut task_templates = TaskTemplates::default();
+ package_json_data.fill_task_templates(&mut task_templates);
+
+ let test_tasks: Vec<_> = task_templates
+ .0
+ .iter()
+ .filter(|template| {
+ template.tags.contains(&"ts-test".to_owned())
+ || template.tags.contains(&"js-test".to_owned())
+ })
+ .map(|template| &template.label)
+ .collect();
+
+ let node_test_index = test_tasks
+ .iter()
+ .position(|label| label.contains("node test"));
+ let jest_test_index = test_tasks.iter().position(|label| label.contains("jest"));
+ let bun_test_index = test_tasks
+ .iter()
+ .position(|label| label.contains("bun test"));
+
+ assert!(
+ node_test_index.is_some(),
+ "Node test tasks should be present"
+ );
+ assert!(
+ jest_test_index.is_some(),
+ "Jest test tasks should be present"
+ );
+ assert!(bun_test_index.is_some(), "Bun test tasks should be present");
+
+ assert!(
+ jest_test_index.unwrap() < bun_test_index.unwrap(),
+ "Jest should come before Bun"
+ );
+ assert!(
+ bun_test_index.unwrap() < node_test_index.unwrap(),
+ "Bun should come before Node"
+ );
+ }
}