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.

profile_test.go · 323 lines · 11.3 KBGo Blame HistoryRaw
📦 Turbo JS 91999d1 k33g 17h ago1package jslang_test
2
3import (
4 "slices"
5 "strings"
6 "testing"
7
8 "rickub.com/turbo-editors/turbo-core/profile"
9 "rickub.com/turbo-editors/turbo-core/syntax"
10
11 "rickub.com/turbo-editors/turbo-js/internal/jslang"
12)
13
14// fixedMenuHotKeys are the hot keys turbo-core's own menus take. The toolchain
15// menu may not claim one of them, or one of the two would be unreachable from
16// the keyboard and nothing would say so.
17var fixedMenuHotKeys = []rune{'F', 'E', 'S', 'R', 'C', 'O', 'W', 'N', 'H'}
18
19func TestProfileNamesTheEditor(t *testing.T) {
20 p := jslang.Profile()
21
22 if p.Name != "Turbo JS" {
23 t.Errorf("Name = %q, want %q", p.Name, "Turbo JS")
24 }
25 if p.Slug != "turbo-js" {
26 t.Errorf("Slug = %q, want %q", p.Slug, "turbo-js")
27 }
28 // The About box and the status bar read this out. It names the language,
29 // not the runtime: JavaScript is what the files are written in, and Node
30 // is what the toolchain runs them on.
31 if p.Language != "JavaScript" {
32 t.Errorf("Language = %q, want %q", p.Language, "JavaScript")
33 }
34}
35
36func TestSlugDerivesEveryPath(t *testing.T) {
37 p := jslang.Profile()
38
39 if got := p.ProjectDir(); got != ".turbo-js" {
40 t.Errorf("ProjectDir() = %q, want %q", got, ".turbo-js")
41 }
42 for name, got := range map[string]string{
43 "DirEnvVar": p.DirEnvVar(),
44 "ThemeDirEnvVar": p.ThemeDirEnvVar(),
45 "SnippetDirEnvVar": p.SnippetDirEnvVar(),
46 } {
47 if !strings.HasPrefix(got, "TURBO_JS_") {
48 t.Errorf("%s() = %q, want a TURBO_JS_ prefix", name, got)
49 }
50 }
51}
52
53func TestThemeDirFollowsItsEnvironmentVariable(t *testing.T) {
54 p := jslang.Profile()
55 want := t.TempDir()
56 t.Setenv(p.ThemeDirEnvVar(), want)
57
58 if got := p.ThemeDir(); got != want {
59 t.Errorf("ThemeDir() = %q, want %q", got, want)
60 }
61}
62
63func TestToolsMenuHotKeyClashesWithNoFixedMenu(t *testing.T) {
64 label := jslang.Profile().ToolsMenu
65
66 hotKey, ok := hotKeyOf(label)
67 if !ok {
68 t.Fatalf("ToolsMenu = %q, which marks no hot key between tildes", label)
69 }
70 if slices.Contains(fixedMenuHotKeys, hotKey) {
71 t.Errorf("ToolsMenu hot key %q is already taken by a fixed menu", hotKey)
72 }
73 if got := strings.ReplaceAll(label, "~", ""); got != "JavaScript" {
74 t.Errorf("ToolsMenu reads %q once the tildes are removed, want %q", got, "JavaScript")
75 }
76}
77
78// hotKeyOf returns the upper-case letter a menu label marks between tildes.
79func hotKeyOf(label string) (rune, bool) {
80 open := strings.Index(label, "~")
81 if open < 0 {
82 return 0, false
83 }
84 rest := label[open+1:]
85 shut := strings.Index(rest, "~")
86 if shut != 1 {
87 return 0, false
88 }
89 return []rune(strings.ToUpper(rest))[0], true
90}
91
92func TestTheRootMarkerIsPackageJSON(t *testing.T) {
93 // package.json is a Node project's boundary, and the nearest one going up
94 // is the package being edited. main_test.go checks the walk from the
95 // other side.
96 if got := jslang.Profile().RootMarkers; !slices.Equal(got, []string{"package.json"}) {
97 t.Errorf("RootMarkers = %v, want exactly [package.json]", got)
98 }
99}
100
101func TestServerIsTypeScriptLanguageServerOverStdio(t *testing.T) {
102 server := jslang.Profile().Server
103
104 if server.Command != "typescript-language-server" {
105 t.Errorf("Server.Command = %q, want %q", server.Command, "typescript-language-server")
106 }
107 // Without --stdio the server prints its usage and exits, which the
108 // editor reports as a server that died at start-up.
109 if !slices.Equal(server.Args, []string{"--stdio"}) {
110 t.Errorf("Server.Args = %v, want exactly [--stdio]", server.Args)
111 }
112 if server.InstallHint == "" {
113 t.Error("Server.InstallHint is empty; a missing server would say nothing useful")
114 }
115 // It is shown on the status bar, so it has to fit on a narrow line.
116 if len(server.InstallHint) > 72 {
117 t.Errorf("InstallHint is %d characters, too long for a status bar", len(server.InstallHint))
118 }
119 // The server does not depend on the typescript package it wraps, so a
120 // hint that installs it alone leaves a server that starts and then says
121 // tsserver is missing — and so does one that installs the *current*
122 // typescript, which is the native TypeScript 7 and ships no tsserver.js.
123 // The pin to 6 is the whole difference between completion and nothing.
124 if !strings.Contains(server.InstallHint, "typescript-language-server") || !strings.HasSuffix(server.InstallHint, " typescript@6") {
125 t.Errorf("InstallHint = %q, want it to install both the server and typescript@6", server.InstallHint)
126 }
127}
128
129func TestServerArgsCannotBeAppendedToByACaller(t *testing.T) {
130 first := jslang.ServerArgs()
131 first = append(first, "--nonsense")
132
133 if second := jslang.ServerArgs(); slices.Contains(second, "--nonsense") {
134 t.Errorf("ServerArgs() = %v after a caller appended to an earlier result", second)
135 }
136}
137
138func TestServerIsLookedForInTheSystemPrefixes(t *testing.T) {
139 // `npm install -g` with a system-wide Node writes into /usr/local/bin, or
140 // /opt/homebrew/bin under Homebrew on Apple silicon. Both are on PATH on
141 // nearly every machine, and the editor searches them anyway for the one
142 // where they are not.
143 dirs := jslang.Profile().Server.Dirs
144
145 for _, want := range []string{"/usr/local/bin", "/opt/homebrew/bin"} {
146 if !slices.Contains(dirs, want) {
147 t.Errorf("Server.Dirs = %v, want it to contain %s", dirs, want)
148 }
149 }
150}
151
152func TestServerDirsFollowTheVersionManagersVariables(t *testing.T) {
153 // Each variable names the one directory its tool writes global binaries
154 // into. Set, it is searched; unset, the tool is not in use and nothing is
155 // guessed for it.
156 t.Setenv("NVM_BIN", "/versions/node/v24/bin")
157 t.Setenv("PNPM_HOME", "/pnpm/home")
158 t.Setenv("VOLTA_HOME", "/volta")
159 t.Setenv("NPM_CONFIG_PREFIX", "/npm/prefix")
160
161 dirs := jslang.ServerDirs()
162
163 for _, want := range []string{"/versions/node/v24/bin", "/pnpm/home", "/volta/bin", "/npm/prefix/bin"} {
164 if !slices.Contains(dirs, want) {
165 t.Errorf("ServerDirs() = %v, want it to contain %s", dirs, want)
166 }
167 }
168 // The most specific answer — the version nvm has selected — comes first,
169 // because it is the one that changes when the user runs `nvm use`.
170 if dirs[0] != "/versions/node/v24/bin" {
171 t.Errorf("ServerDirs() starts with %q, want nvm's directory first", dirs[0])
172 }
173}
174
175func TestServerDirsGuessNothingForAToolThatIsNotSetUp(t *testing.T) {
176 t.Setenv("NVM_BIN", "")
177 t.Setenv("PNPM_HOME", "")
178
179 if got := jslang.NvmBinDir(); got != "" {
180 t.Errorf("NvmBinDir() = %q with no NVM_BIN, want nothing", got)
181 }
182 if got := jslang.PnpmHome(); got != "" {
183 t.Errorf("PnpmHome() = %q with no PNPM_HOME, want nothing", got)
184 }
185}
186
187func TestTheNpmPrefixDefaultsToTheDocumentedPerUserOne(t *testing.T) {
188 // ~/.npm-global is the prefix npm's documentation tells a user to set up
189 // so that installing globally needs no sudo. It is where the install
190 // hint's command lands on such a machine, so it is the one that matters
191 // to somebody who followed the hint and found nothing.
192 t.Setenv("NPM_CONFIG_PREFIX", "")
193 t.Setenv("VOLTA_HOME", "")
194
195 if got := jslang.NpmPrefixBinDir(); !strings.HasSuffix(got, "/.npm-global/bin") {
196 t.Errorf("NpmPrefixBinDir() = %q, want it under ~/.npm-global/bin", got)
197 }
198 if got := jslang.VoltaBinDir(); !strings.HasSuffix(got, "/.volta/bin") {
199 t.Errorf("VoltaBinDir() = %q, want it under ~/.volta/bin", got)
200 }
201}
202
203func TestProfileHandsOutAFreshCopyEveryTime(t *testing.T) {
204 // A caller that appends to Server.Dirs must not be appending to the
205 // package's one copy, or the next caller would inherit its guess.
206 first := jslang.Profile()
207 first.Server.Dirs = append(first.Server.Dirs, "/somewhere/else")
208
209 if second := jslang.Profile(); slices.Contains(second.Server.Dirs, "/somewhere/else") {
210 t.Errorf("Profile().Server.Dirs = %v after a caller appended to an earlier result", second.Server.Dirs)
211 }
212}
213
214func TestTemplatesAreAllFilledIn(t *testing.T) {
215 templates := jslang.Profile().Templates
216
217 for name, template := range map[string]string{
218 "Settings": templates.Settings,
219 "Snippets": templates.Snippets,
220 "Tools": templates.Tools,
221 "Agents": templates.Agents,
222 } {
223 if template == "" {
224 t.Errorf("Templates.%s is empty; the editor would offer to write nothing", name)
225 }
226 }
227}
228
229func TestRegisterTeachesTheLibraryJavaScriptAndJSON(t *testing.T) {
230 jslang.Register()
231
232 for _, want := range []syntax.Language{jslang.Language, jslang.LanguageJSON} {
233 if !slices.Contains(syntax.Registered(), want) {
234 t.Errorf("Registered() = %v, want it to contain %q", syntax.Registered(), want)
235 }
236 }
237
238 for path, want := range map[string]syntax.Language{
239 "main.js": jslang.Language,
240 "lib/server.mjs": jslang.Language,
241 "config/legacy.cjs": jslang.Language,
242 "DEEP/nested/x.JS": jslang.Language,
243 "package.json": jslang.LanguageJSON,
244 "tsconfig.json": jslang.LanguageJSON,
245 ".vscode/tasks.jsonc": jslang.LanguageJSON,
246 } {
247 if got := syntax.LanguageOf(path, ""); got != want {
248 t.Errorf("LanguageOf(%q) = %q, want %q", path, got, want)
249 }
250 }
251}
252
253func TestRegisterReplacesTheLibrarysOwnJavaScriptScanner(t *testing.T) {
254 // The library colours JavaScript too, without regular expressions: it
255 // reads /x/ as two divisions. Registering under the same name is what
256 // puts this editor's scanner in its place, and a regular expression
257 // coming out coloured as one is the proof that it did.
258 jslang.Register()
259
260 const src = "return /x/.test(s);"
261 spans := syntax.Highlight(jslang.Language, src)
262 at := strings.Index(src, "/x/")
263
264 for _, span := range spans[0] {
265 if span.Start <= at && at < span.End {
266 if span.Class != syntax.ClassChar {
267 t.Errorf("/x/ is %v; the library's scanner is still in charge of JavaScript", span.Class)
268 }
269 return
270 }
271 }
272 t.Error("nothing covers the regular expression at all")
273}
274
275func TestAScriptWithANodeShebangIsJavaScript(t *testing.T) {
276 // A command-line tool written in JavaScript is a file with no extension
277 // whose first line names node. Node reads that line as a comment, and
278 // the editor reads it before choosing a scanner.
279 jslang.Register()
280
281 for _, firstLine := range []string{"#!/usr/bin/env node", "#!/usr/local/bin/node", "#!/usr/bin/env -S node --no-warnings"} {
282 if got := syntax.LanguageOf("bin/cli", firstLine); got != jslang.Language {
283 t.Errorf("LanguageOf(%q) = %q, want %q", firstLine, got, jslang.Language)
284 }
285 }
286}
287
288func TestRegisterLeavesOtherFilesAlone(t *testing.T) {
289 jslang.Register()
290
291 cases := map[string]syntax.Language{
292 "README.md": syntax.LanguageMarkdown,
293 "Dockerfile": syntax.LanguageDockerfile,
294 "index.html": syntax.LanguageHTML,
295 "main.py": syntax.LanguageNone,
296 "main.rs": syntax.LanguageNone,
297 "main.go": syntax.LanguageNone,
298 // TypeScript is served by the same language server but is not
299 // coloured here: it is a different language with its own keywords,
300 // and a deliberate boundary the reference documents.
301 "main.ts": syntax.LanguageNone,
302 // A JavaScript file's neighbour with a different extension is not
303 // JavaScript however JavaScript-flavoured its name.
304 "javascript.toml": syntax.LanguageTOML,
305 }
306 for path, want := range cases {
307 if got := syntax.LanguageOf(path, ""); got != want {
308 t.Errorf("LanguageOf(%q) = %q, want %q", path, got, want)
309 }
310 }
311 // A shebang naming another interpreter is not JavaScript either.
312 if got := syntax.LanguageOf("script", "#!/usr/bin/env python3"); got == jslang.Language {
313 t.Errorf("LanguageOf with a python shebang = %q, want anything but JavaScript", got)
314 }
315}
316
317// A profile is a literal, so this example is the whole of how one is used.
318func ExampleProfile() {
319 jslang.Register()
320
321 var p profile.Profile = jslang.Profile()
322 _ = p.ProjectDir() // ".turbo-js"
323}