turbo-editors/turbo-jspublic Fork 0
v1.0.2
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-js.git
git clone ssh://git@rickub.com/turbo-editors/turbo-js.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

main_test.go · 205 lines · 6.9 KBGo Blame HistoryRaw
📦 Turbo JS 91999d1 k33g 12h ago1package main
2
3import (
4 "flag"
5 "os"
6 "path/filepath"
7 "slices"
8 "strings"
9 "testing"
10
11 "rickub.com/turbo-editors/turbo-core/app"
12 "rickub.com/turbo-editors/turbo-core/settings"
13 "rickub.com/turbo-editors/turbo-core/theme"
14
15 "rickub.com/turbo-editors/turbo-js/internal/jslang"
16)
17
18// withArgs runs the command line parser against a fixed argument list, and
19// restores the real one afterwards so tests do not affect each other.
20func withArgs(t *testing.T, args ...string) options {
21 t.Helper()
22
23 realArgs, realFlags := os.Args, flag.CommandLine
24 t.Cleanup(func() { os.Args, flag.CommandLine = realArgs, realFlags })
25
26 os.Args = append([]string{"turbo-js"}, args...)
27 flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ContinueOnError)
28 return parseFlags()
29}
30
31func TestMainRegistersTheLanguageExplicitly(t *testing.T) {
32 // "This editor knows JavaScript" is a line somebody can read in main.go,
33 // not an init function somewhere. Every other test here calls Register
34 // itself, so this is the only one that notices if run() stopped doing so.
35 source, err := os.ReadFile("main.go")
36 if err != nil {
37 t.Fatalf("reading main.go: %v", err)
38 }
39 if !strings.Contains(string(source), "\n\tjslang.Register()\n") {
40 t.Error("main.go no longer calls jslang.Register() as a statement of its own")
41 }
42}
43
44func TestFilesAreWhatIsLeftAfterTheFlags(t *testing.T) {
45 opts := withArgs(t, "-no-lsp", "main.js", "lib/x.mjs")
46
47 if !opts.noLSP {
48 t.Error("-no-lsp was not read")
49 }
50 if want := []string{"main.js", "lib/x.mjs"}; !slices.Equal(opts.files, want) {
51 t.Errorf("files = %v, want %v", opts.files, want)
52 }
53}
54
55func TestTheThemeFlagDefaultsToEmptyRatherThanToAName(t *testing.T) {
56 // Empty is what lets "was -theme given?" still be answered afterwards, and
57 // that is what lets the project's settings fill it in without overriding an
58 // explicit choice.
59 if opts := withArgs(t); opts.theme != "" {
60 t.Errorf("theme = %q with no flag, want the empty string", opts.theme)
61 }
62}
63
64func TestTheProjectRootIsTheDirectoryHoldingPackageJSON(t *testing.T) {
65 // typescript-language-server is given the package's boundary, which is
66 // where it resolves imports from. The walk itself is turbo-core's; what is
67 // checked here is that Turbo JS's profile asks it to look for a
68 // package.json.
69 root := t.TempDir()
70 if err := os.WriteFile(filepath.Join(root, "package.json"), []byte("{\"name\": \"x\"}\n"), 0o644); err != nil {
71 t.Fatalf("writing package.json: %v", err)
72 }
73 nested := filepath.Join(root, "src", "deep")
74 if err := os.MkdirAll(nested, 0o755); err != nil {
75 t.Fatalf("creating the tree: %v", err)
76 }
77 file := filepath.Join(nested, "deep.js")
78 if err := os.WriteFile(file, []byte("export const f = () => 1;\n"), 0o644); err != nil {
79 t.Fatalf("writing the file: %v", err)
80 }
81
82 if got := app.ProjectRoot(jslang.Profile(), []string{file}); got != root {
83 t.Errorf("ProjectRoot() = %q, want the package root %q", got, root)
84 }
85}
86
87func TestTheNearestPackageJSONWinsInAMonorepo(t *testing.T) {
88 // A workspace holds a package.json at its root and one per package. The
89 // package being edited is the nearest one going up, and that is the
90 // project the server should see.
91 workspace := t.TempDir()
92 pkg := filepath.Join(workspace, "packages", "api")
93 if err := os.MkdirAll(filepath.Join(pkg, "src"), 0o755); err != nil {
94 t.Fatalf("creating the tree: %v", err)
95 }
96 for _, dir := range []string{workspace, pkg} {
97 if err := os.WriteFile(filepath.Join(dir, "package.json"), []byte("{}\n"), 0o644); err != nil {
98 t.Fatalf("writing package.json: %v", err)
99 }
100 }
101 file := filepath.Join(pkg, "src", "index.js")
102 if err := os.WriteFile(file, []byte("\n"), 0o644); err != nil {
103 t.Fatalf("writing the file: %v", err)
104 }
105
106 if got := app.ProjectRoot(jslang.Profile(), []string{file}); got != pkg {
107 t.Errorf("ProjectRoot() = %q, want the nearest package %q", got, pkg)
108 }
109}
110
111func TestThemeNamePrefersTheFlagOverTheProject(t *testing.T) {
112 got := themeName(options{theme: "borland-light"}, settings.Settings{Theme: "turbo-dark"})
113
114 if got != "borland-light" {
115 t.Errorf("themeName() = %q; an explicit -theme must win over the project's", got)
116 }
117}
118
119func TestThemeNameUsesTheProjectWhenNoFlagWasGiven(t *testing.T) {
120 got := themeName(options{}, settings.Settings{Theme: "turbo-dark"})
121
122 if got != "turbo-dark" {
123 t.Errorf("themeName() = %q, want the project's theme", got)
124 }
125}
126
127func TestThemeNameFallsBackToTheDefault(t *testing.T) {
128 got := themeName(options{}, settings.Settings{})
129
130 if got != theme.DefaultName {
131 t.Errorf("themeName() = %q, want %q", got, theme.DefaultName)
132 }
133}
134
135func TestLoadProjectSettingsReadsTheWorkingDirectory(t *testing.T) {
136 project := t.TempDir()
137 t.Chdir(project)
138 if err := os.MkdirAll(settings.Dir(jslang.Profile(), project), 0o755); err != nil {
139 t.Fatalf("creating the settings directory: %v", err)
140 }
141 contents := "[editor]\ntheme = \"turbo-dark\"\nautosave = true\n"
142 if err := os.WriteFile(settings.Path(jslang.Profile(), project), []byte(contents), 0o644); err != nil {
143 t.Fatalf("writing the settings file: %v", err)
144 }
145
146 _, loaded := loadProjectSettings(jslang.Profile())
147
148 if loaded.Theme != "turbo-dark" {
149 t.Errorf("Theme = %q, want turbo-dark", loaded.Theme)
150 }
151 if !loaded.Autosave {
152 t.Error("Autosave = false, want the file's true")
153 }
154}
155
156func TestLoadProjectSettingsDoesNotWalkUpToAParent(t *testing.T) {
157 // "The project is where you started the editor" is the rule; a settings
158 // file one directory up belongs to a different project.
159 parent := t.TempDir()
160 if err := os.MkdirAll(settings.Dir(jslang.Profile(), parent), 0o755); err != nil {
161 t.Fatalf("creating the settings directory: %v", err)
162 }
163 if err := os.WriteFile(settings.Path(jslang.Profile(), parent), []byte("[editor]\ntheme = \"turbo-dark\"\n"), 0o644); err != nil {
164 t.Fatalf("writing the settings file: %v", err)
165 }
166 child := filepath.Join(parent, "src")
167 if err := os.Mkdir(child, 0o755); err != nil {
168 t.Fatalf("creating the child directory: %v", err)
169 }
170 t.Chdir(child)
171
172 _, loaded := loadProjectSettings(jslang.Profile())
173
174 if loaded.Theme != "" {
175 t.Errorf("Theme = %q; settings were read from a parent directory", loaded.Theme)
176 }
177}
178
179func TestLoadProjectSettingsCarriesOnWithoutAFile(t *testing.T) {
180 t.Chdir(t.TempDir())
181
182 _, loaded := loadProjectSettings(jslang.Profile())
183
184 if loaded != settings.Default() {
185 t.Errorf("loadProjectSettings() = %+v, want the defaults", loaded)
186 }
187}
188
189func TestABrokenSettingsFileDoesNotStopTheEditor(t *testing.T) {
190 // The editor is how you would fix the file, so it has to open.
191 project := t.TempDir()
192 t.Chdir(project)
193 if err := os.MkdirAll(settings.Dir(jslang.Profile(), project), 0o755); err != nil {
194 t.Fatalf("creating the settings directory: %v", err)
195 }
196 if err := os.WriteFile(settings.Path(jslang.Profile(), project), []byte("[editor\nnot toml"), 0o644); err != nil {
197 t.Fatalf("writing the settings file: %v", err)
198 }
199
200 _, loaded := loadProjectSettings(jslang.Profile())
201
202 if loaded != settings.Default() {
203 t.Errorf("loadProjectSettings() = %+v, want the defaults", loaded)
204 }
205}