diff --git a/internal/runner/jest.go b/internal/runner/jest.go index 05bc2959..83544c6d 100644 --- a/internal/runner/jest.go +++ b/internal/runner/jest.go @@ -185,11 +185,23 @@ func (j Jest) commandNameAndArgs(cmd string, testCases []string) (string, []stri if err != nil { return "", []string{}, err } + + // Escape parentheses and brackets in test file paths since Jest + // interprets CLI arguments as regex patterns. These characters are + // commonly used in Next.js App Router paths (e.g., (main) for route + // groups, [id] for dynamic routes) and cause regex matching issues. + // We don't escape all regex metacharacters (like dots) because they + // work fine in practice and escaping them breaks existing behavior. + escapedTestCases := make([]string, len(testCases)) + for i, testCase := range testCases { + escapedTestCases[i] = escapeJestPathPattern(testCase) + } + idx := slices.Index(words, "{{testExamples}}") if idx < 0 { - words = append(words, testCases...) + words = append(words, escapedTestCases...) } else { - words = slices.Replace(words, idx, idx+1, testCases...) + words = slices.Replace(words, idx, idx+1, escapedTestCases...) } outputIdx := slices.Index(words, "{{resultPath}}") @@ -241,3 +253,16 @@ func (j Jest) retryCommandNameAndArgs(cmd string, testCases []string, testPaths func (j Jest) GetExamples(files []string) ([]plan.TestCase, error) { return nil, fmt.Errorf("not supported in Jest") } + +// escapeJestPathPattern escapes characters in file paths that cause issues +// when Jest interprets them as regex patterns. This specifically targets +// parentheses and brackets used in Next.js App Router conventions. +func escapeJestPathPattern(path string) string { + replacer := strings.NewReplacer( + "(", "\\(", + ")", "\\)", + "[", "\\[", + "]", "\\]", + ) + return replacer.Replace(path) +} diff --git a/internal/runner/jest_test.go b/internal/runner/jest_test.go index 811a371c..10ca831a 100644 --- a/internal/runner/jest_test.go +++ b/internal/runner/jest_test.go @@ -354,6 +354,7 @@ func TestJestCommandNameAndArgs_WithInterpolationPlaceholder(t *testing.T) { } wantName := "jest" + // Paths with parentheses and brackets are escaped for regex since Jest interprets them as patterns wantArgs := []string{"spec/user.spec.js", "spec/billing.spec.js", "--outputFile", "jest.json"} if diff := cmp.Diff(gotName, wantName); diff != "" { @@ -379,6 +380,7 @@ func TestJestCommandNameAndArgs_WithoutInterpolationPlaceholder(t *testing.T) { } wantName := "jest" + // Paths with parentheses and brackets are escaped for regex since Jest interprets them as patterns wantArgs := []string{"--json", "--outputFile", "jest.json", "spec/user.spec.js", "spec/billing.spec.js"} if diff := cmp.Diff(gotName, wantName); diff != "" { @@ -413,6 +415,42 @@ func TestJestCommandNameAndArgs_InvalidTestCommand(t *testing.T) { } } +func TestJestCommandNameAndArgs_WithSpecialCharactersInPath(t *testing.T) { + // Test paths with special regex characters like parentheses (Next.js route groups) + // and square brackets (Next.js dynamic routes) + testCases := []string{ + "src/app/(main)/page.test.tsx", + "src/app/(main)/[catalogId]/product.test.tsx", + } + testCommand := "jest {{testExamples}} --outputFile {{resultPath}}" + + jest := NewJest(RunnerConfig{ + TestCommand: testCommand, + ResultPath: "jest.json", + }) + + gotName, gotArgs, err := jest.commandNameAndArgs(testCommand, testCases) + if err != nil { + t.Errorf("commandNameAndArgs(%q, %q) error = %v", testCases, testCommand, err) + } + + wantName := "jest" + // Parentheses and brackets should be escaped for regex (Next.js App Router conventions) + wantArgs := []string{ + `src/app/\(main\)/page.test.tsx`, + `src/app/\(main\)/\[catalogId\]/product.test.tsx`, + "--outputFile", + "jest.json", + } + + if diff := cmp.Diff(gotName, wantName); diff != "" { + t.Errorf("commandNameAndArgs(%q, %q) diff (-got +want):\n%s", testCases, testCommand, diff) + } + if diff := cmp.Diff(gotArgs, wantArgs); diff != "" { + t.Errorf("commandNameAndArgs(%q, %q) diff (-got +want):\n%s", testCases, testCommand, diff) + } +} + func TestJestRetryCommandNameAndArgs_HappyPath(t *testing.T) { testCases := []string{"this will fail", "this other one will fail"} testPaths := []string{"spec/user.spec.js", "spec/billing.spec.js"}