package main import ( "flag" "os" "path/filepath" "slices" "strings" "testing" "rickub.com/turbo-editors/turbo-core/app" "rickub.com/turbo-editors/turbo-core/settings" "rickub.com/turbo-editors/turbo-core/theme" "rickub.com/turbo-editors/turbo-js/internal/jslang" ) // withArgs runs the command line parser against a fixed argument list, and // restores the real one afterwards so tests do not affect each other. func withArgs(t *testing.T, args ...string) options { t.Helper() realArgs, realFlags := os.Args, flag.CommandLine t.Cleanup(func() { os.Args, flag.CommandLine = realArgs, realFlags }) os.Args = append([]string{"turbo-js"}, args...) flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ContinueOnError) return parseFlags() } func TestMainRegistersTheLanguageExplicitly(t *testing.T) { // "This editor knows JavaScript" is a line somebody can read in main.go, // not an init function somewhere. Every other test here calls Register // itself, so this is the only one that notices if run() stopped doing so. source, err := os.ReadFile("main.go") if err != nil { t.Fatalf("reading main.go: %v", err) } if !strings.Contains(string(source), "\n\tjslang.Register()\n") { t.Error("main.go no longer calls jslang.Register() as a statement of its own") } } func TestFilesAreWhatIsLeftAfterTheFlags(t *testing.T) { opts := withArgs(t, "-no-lsp", "main.js", "lib/x.mjs") if !opts.noLSP { t.Error("-no-lsp was not read") } if want := []string{"main.js", "lib/x.mjs"}; !slices.Equal(opts.files, want) { t.Errorf("files = %v, want %v", opts.files, want) } } func TestTheThemeFlagDefaultsToEmptyRatherThanToAName(t *testing.T) { // Empty is what lets "was -theme given?" still be answered afterwards, and // that is what lets the project's settings fill it in without overriding an // explicit choice. if opts := withArgs(t); opts.theme != "" { t.Errorf("theme = %q with no flag, want the empty string", opts.theme) } } func TestTheProjectRootIsTheDirectoryHoldingPackageJSON(t *testing.T) { // typescript-language-server is given the package's boundary, which is // where it resolves imports from. The walk itself is turbo-core's; what is // checked here is that Turbo JS's profile asks it to look for a // package.json. root := t.TempDir() if err := os.WriteFile(filepath.Join(root, "package.json"), []byte("{\"name\": \"x\"}\n"), 0o644); err != nil { t.Fatalf("writing package.json: %v", err) } nested := filepath.Join(root, "src", "deep") if err := os.MkdirAll(nested, 0o755); err != nil { t.Fatalf("creating the tree: %v", err) } file := filepath.Join(nested, "deep.js") if err := os.WriteFile(file, []byte("export const f = () => 1;\n"), 0o644); err != nil { t.Fatalf("writing the file: %v", err) } if got := app.ProjectRoot(jslang.Profile(), []string{file}); got != root { t.Errorf("ProjectRoot() = %q, want the package root %q", got, root) } } func TestTheNearestPackageJSONWinsInAMonorepo(t *testing.T) { // A workspace holds a package.json at its root and one per package. The // package being edited is the nearest one going up, and that is the // project the server should see. workspace := t.TempDir() pkg := filepath.Join(workspace, "packages", "api") if err := os.MkdirAll(filepath.Join(pkg, "src"), 0o755); err != nil { t.Fatalf("creating the tree: %v", err) } for _, dir := range []string{workspace, pkg} { if err := os.WriteFile(filepath.Join(dir, "package.json"), []byte("{}\n"), 0o644); err != nil { t.Fatalf("writing package.json: %v", err) } } file := filepath.Join(pkg, "src", "index.js") if err := os.WriteFile(file, []byte("\n"), 0o644); err != nil { t.Fatalf("writing the file: %v", err) } if got := app.ProjectRoot(jslang.Profile(), []string{file}); got != pkg { t.Errorf("ProjectRoot() = %q, want the nearest package %q", got, pkg) } } func TestThemeNamePrefersTheFlagOverTheProject(t *testing.T) { got := themeName(options{theme: "borland-light"}, settings.Settings{Theme: "turbo-dark"}) if got != "borland-light" { t.Errorf("themeName() = %q; an explicit -theme must win over the project's", got) } } func TestThemeNameUsesTheProjectWhenNoFlagWasGiven(t *testing.T) { got := themeName(options{}, settings.Settings{Theme: "turbo-dark"}) if got != "turbo-dark" { t.Errorf("themeName() = %q, want the project's theme", got) } } func TestThemeNameFallsBackToTheDefault(t *testing.T) { got := themeName(options{}, settings.Settings{}) if got != theme.DefaultName { t.Errorf("themeName() = %q, want %q", got, theme.DefaultName) } } func TestLoadProjectSettingsReadsTheWorkingDirectory(t *testing.T) { project := t.TempDir() t.Chdir(project) if err := os.MkdirAll(settings.Dir(jslang.Profile(), project), 0o755); err != nil { t.Fatalf("creating the settings directory: %v", err) } contents := "[editor]\ntheme = \"turbo-dark\"\nautosave = true\n" if err := os.WriteFile(settings.Path(jslang.Profile(), project), []byte(contents), 0o644); err != nil { t.Fatalf("writing the settings file: %v", err) } _, loaded := loadProjectSettings(jslang.Profile()) if loaded.Theme != "turbo-dark" { t.Errorf("Theme = %q, want turbo-dark", loaded.Theme) } if !loaded.Autosave { t.Error("Autosave = false, want the file's true") } } func TestLoadProjectSettingsDoesNotWalkUpToAParent(t *testing.T) { // "The project is where you started the editor" is the rule; a settings // file one directory up belongs to a different project. parent := t.TempDir() if err := os.MkdirAll(settings.Dir(jslang.Profile(), parent), 0o755); err != nil { t.Fatalf("creating the settings directory: %v", err) } if err := os.WriteFile(settings.Path(jslang.Profile(), parent), []byte("[editor]\ntheme = \"turbo-dark\"\n"), 0o644); err != nil { t.Fatalf("writing the settings file: %v", err) } child := filepath.Join(parent, "src") if err := os.Mkdir(child, 0o755); err != nil { t.Fatalf("creating the child directory: %v", err) } t.Chdir(child) _, loaded := loadProjectSettings(jslang.Profile()) if loaded.Theme != "" { t.Errorf("Theme = %q; settings were read from a parent directory", loaded.Theme) } } func TestLoadProjectSettingsCarriesOnWithoutAFile(t *testing.T) { t.Chdir(t.TempDir()) _, loaded := loadProjectSettings(jslang.Profile()) if loaded != settings.Default() { t.Errorf("loadProjectSettings() = %+v, want the defaults", loaded) } } func TestABrokenSettingsFileDoesNotStopTheEditor(t *testing.T) { // The editor is how you would fix the file, so it has to open. project := t.TempDir() t.Chdir(project) if err := os.MkdirAll(settings.Dir(jslang.Profile(), project), 0o755); err != nil { t.Fatalf("creating the settings directory: %v", err) } if err := os.WriteFile(settings.Path(jslang.Profile(), project), []byte("[editor\nnot toml"), 0o644); err != nil { t.Fatalf("writing the settings file: %v", err) } _, loaded := loadProjectSettings(jslang.Profile()) if loaded != settings.Default() { t.Errorf("loadProjectSettings() = %+v, want the defaults", loaded) } }