Allow to run dynamic TypeScript and JavaScript tests (#31499)

Alexander and Piotr Osiewicz created

First of all thank you for such a fast editor!

I realized that the existing support for detecting runnable test cases
for typescript/javascript is not full. Meanwhile I can run most of test
by pressing "run button":

<img width="713" alt="image"
src="https://github.com/user-attachments/assets/e8bb1cb1-f0a5-4eb1-b9a6-7188a9fa47ae"
/>

I can't run dynamic tests:

<img width="703" alt="image"
src="https://github.com/user-attachments/assets/d7eef1bc-e99a-4f05-9d62-ec49b8194959"
/>

I was curious whether I can improve it on my own and created this pr. I
edited schemas and added minor changes in `TaskTemplate` to allow to
replace '%s' with regexp pattern, so it can match test cases:

<img width="772" alt="image"
src="https://github.com/user-attachments/assets/db3a6fe0-ad90-4853-8e98-4215e41dfe88"
/>

Release Notes:

- Allow to run dynamic TypeScript/JavaScript tests

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>

Change summary

crates/languages/src/javascript/outline.scm   | 17 +++++++++
crates/languages/src/javascript/runnables.scm | 19 ++++++++++
crates/languages/src/typescript.rs            | 39 ++++++++++++++++++--
crates/languages/src/typescript/outline.scm   | 17 +++++++++
crates/languages/src/typescript/runnables.scm | 19 ++++++++++
5 files changed, 106 insertions(+), 5 deletions(-)

Detailed changes

crates/languages/src/javascript/outline.scm 🔗

@@ -80,4 +80,21 @@
     )
 ) @item
 
+; Add support for parameterized tests
+(
+    (call_expression
+        function: (call_expression
+            function: (member_expression
+                object: [(identifier) @_name (member_expression object: (identifier) @_name)]
+                property: (property_identifier) @_property
+            )
+            (#any-of? @_name "it" "test" "describe" "context" "suite")
+            (#eq? @_property "each")
+        )
+        arguments: (
+            arguments . (string (string_fragment) @name)
+        )
+    )
+) @item
+
 (comment) @annotation

crates/languages/src/javascript/runnables.scm 🔗

@@ -19,3 +19,22 @@
 
     (#set! tag js-test)
 )
+
+; Add support for parameterized tests
+(
+    (call_expression
+        function: (call_expression
+            function: (member_expression
+                object: [(identifier) @_name (member_expression object: (identifier) @_name)]
+                property: (property_identifier) @_property
+            )
+            (#any-of? @_name "it" "test" "describe" "context" "suite")
+            (#eq? @_property "each")
+        )
+        arguments: (
+            arguments . (string (string_fragment) @run)
+        )
+    ) @_js-test
+
+    (#set! tag js-test)
+)

crates/languages/src/typescript.rs 🔗

@@ -34,11 +34,15 @@ const TYPESCRIPT_RUNNER_VARIABLE: VariableName =
     VariableName::Custom(Cow::Borrowed("TYPESCRIPT_RUNNER"));
 const TYPESCRIPT_JEST_TASK_VARIABLE: VariableName =
     VariableName::Custom(Cow::Borrowed("TYPESCRIPT_JEST"));
+const TYPESCRIPT_JEST_TEST_NAME_VARIABLE: VariableName =
+    VariableName::Custom(Cow::Borrowed("TYPESCRIPT_JEST_TEST_NAME"));
 const TYPESCRIPT_MOCHA_TASK_VARIABLE: VariableName =
     VariableName::Custom(Cow::Borrowed("TYPESCRIPT_MOCHA"));
 
 const TYPESCRIPT_VITEST_TASK_VARIABLE: VariableName =
     VariableName::Custom(Cow::Borrowed("TYPESCRIPT_VITEST"));
+const TYPESCRIPT_VITEST_TEST_NAME_VARIABLE: VariableName =
+    VariableName::Custom(Cow::Borrowed("TYPESCRIPT_VITEST_TEST_NAME"));
 const TYPESCRIPT_JASMINE_TASK_VARIABLE: VariableName =
     VariableName::Custom(Cow::Borrowed("TYPESCRIPT_JASMINE"));
 const TYPESCRIPT_BUILD_SCRIPT_TASK_VARIABLE: VariableName =
@@ -183,7 +187,10 @@ impl ContextProvider for TypeScriptContextProvider {
             args: vec![
                 TYPESCRIPT_JEST_TASK_VARIABLE.template_value(),
                 "--testNamePattern".to_owned(),
-                format!("\"{}\"", VariableName::Symbol.template_value()),
+                format!(
+                    "\"{}\"",
+                    TYPESCRIPT_JEST_TEST_NAME_VARIABLE.template_value()
+                ),
                 VariableName::RelativeFile.template_value(),
             ],
             tags: vec![
@@ -221,7 +228,7 @@ impl ContextProvider for TypeScriptContextProvider {
                 TYPESCRIPT_VITEST_TASK_VARIABLE.template_value(),
                 "run".to_owned(),
                 "--testNamePattern".to_owned(),
-                format!("\"{}\"", VariableName::Symbol.template_value()),
+                format!("\"{}\"", TYPESCRIPT_VITEST_TASK_VARIABLE.template_value()),
                 VariableName::RelativeFile.template_value(),
             ],
             tags: vec![
@@ -344,14 +351,27 @@ impl ContextProvider for TypeScriptContextProvider {
 
     fn build_context(
         &self,
-        _variables: &task::TaskVariables,
+        current_vars: &task::TaskVariables,
         location: ContextLocation<'_>,
         _project_env: Option<HashMap<String, String>>,
         _toolchains: Arc<dyn LanguageToolchainStore>,
         cx: &mut App,
     ) -> Task<Result<task::TaskVariables>> {
+        let mut vars = task::TaskVariables::default();
+
+        if let Some(symbol) = current_vars.get(&VariableName::Symbol) {
+            vars.insert(
+                TYPESCRIPT_JEST_TEST_NAME_VARIABLE,
+                replace_test_name_parameters(symbol),
+            );
+            vars.insert(
+                TYPESCRIPT_VITEST_TEST_NAME_VARIABLE,
+                replace_test_name_parameters(symbol),
+            );
+        }
+
         let Some((fs, worktree_root)) = location.fs.zip(location.worktree_root) else {
-            return Task::ready(Ok(task::TaskVariables::default()));
+            return Task::ready(Ok(vars));
         };
 
         let package_json_contents = self.last_package_json.clone();
@@ -361,7 +381,10 @@ impl ContextProvider for TypeScriptContextProvider {
                 .context("package.json context retrieval")
                 .log_err()
                 .unwrap_or_else(task::TaskVariables::default);
-            Ok(variables)
+
+            vars.extend(variables);
+
+            Ok(vars)
         })
     }
 }
@@ -426,6 +449,12 @@ fn eslint_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
     ]
 }
 
+fn replace_test_name_parameters(test_name: &str) -> String {
+    let pattern = regex::Regex::new(r"(%|\$)[0-9a-zA-Z]+").unwrap();
+
+    pattern.replace_all(test_name, "(.+?)").to_string()
+}
+
 pub struct TypeScriptLspAdapter {
     node: NodeRuntime,
 }

crates/languages/src/typescript/outline.scm 🔗

@@ -88,4 +88,21 @@
     )
 ) @item
 
+; Add support for parameterized tests
+(
+    (call_expression
+        function: (call_expression
+            function: (member_expression
+                object: [(identifier) @_name (member_expression object: (identifier) @_name)]
+                property: (property_identifier) @_property
+            )
+            (#any-of? @_name "it" "test" "describe" "context" "suite")
+            (#any-of? @_property "each")
+        )
+        arguments: (
+            arguments . (string (string_fragment) @name)
+        )
+    )
+) @item
+
 (comment) @annotation

crates/languages/src/typescript/runnables.scm 🔗

@@ -19,3 +19,22 @@
 
     (#set! tag js-test)
 )
+
+; Add support for parameterized tests
+(
+    (call_expression
+        function: (call_expression
+            function: (member_expression
+                object: [(identifier) @_name (member_expression object: (identifier) @_name)]
+                property: (property_identifier) @_property
+            )
+            (#any-of? @_name "it" "test" "describe" "context" "suite")
+            (#any-of? @_property "each")
+        )
+        arguments: (
+            arguments . (string (string_fragment) @run)
+        )
+    ) @_js-test
+
+    (#set! tag js-test)
+)