turbo-editors/turbo-jspublic Fork 0
main
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.

json_test.go · 158 lines · 4.8 KBGo Blame HistoryRaw
📦 Turbo JS 91999d1 k33g 11h ago1package jslang
2
3import (
4 "strings"
5 "testing"
6
7 "rickub.com/turbo-editors/turbo-core/syntax"
8)
9
10// jsonClassOf is classOfFirst for JSON.
11func jsonClassOf(t *testing.T, src, word string) syntax.Class {
12 t.Helper()
13 return classOfFirst(t, HighlightJSON, src, word)
14}
15
16const packageJSON = `{
17 "name": "demo",
18 "version": "1.0.0",
19 "private": true,
20 "engines": { "node": ">=24" },
21 "scripts": {
22 "start": "node main.js",
23 "test": "node --test"
24 },
25 "dependencies": {},
26 "count": -12.5e+3,
27 "nothing": null
28}
29`
30
31func TestJSONReturnsOneEntryPerLine(t *testing.T) {
32 if got, want := len(HighlightJSON(packageJSON)), strings.Count(packageJSON, "\n")+1; got != want {
33 t.Errorf("HighlightJSON() returned %d lines, want %d", got, want)
34 }
35 if got := len(HighlightJSON("")); got != 1 {
36 t.Errorf("HighlightJSON(\"\") returned %d lines, want 1", got)
37 }
38}
39
40func TestAJSONKeyIsAnAttributeAndAValueIsAString(t *testing.T) {
41 // The two sides of a colon read apart, which is the whole of what a
42 // coloured package.json is for.
43 tests := []struct {
44 word string
45 want syntax.Class
46 }{
47 {`"name"`, syntax.ClassAttribute},
48 {`"demo"`, syntax.ClassString},
49 {`"scripts"`, syntax.ClassAttribute},
50 {`"node main.js"`, syntax.ClassString},
51 {`"node"`, syntax.ClassAttribute}, // a key inside an inline object
52 {`">=24"`, syntax.ClassString},
53 }
54
55 for _, test := range tests {
56 t.Run(test.word, func(t *testing.T) {
57 if got := jsonClassOf(t, packageJSON, test.word); got != test.want {
58 t.Errorf("%s is %v, want %v", test.word, got, test.want)
59 }
60 })
61 }
62}
63
64func TestJSONConstantsNumbersAndPunctuation(t *testing.T) {
65 tests := []struct {
66 word string
67 want syntax.Class
68 }{
69 {"true", syntax.ClassConstant},
70 {"null", syntax.ClassConstant},
71 {"-12.5e+3", syntax.ClassNumber},
72 {"{", syntax.ClassPunctuation},
73 {":", syntax.ClassPunctuation},
74 {",", syntax.ClassPunctuation},
75 {"[", syntax.ClassPunctuation},
76 }
77
78 src := packageJSON + `[1, 2]`
79 for _, test := range tests {
80 t.Run(test.word, func(t *testing.T) {
81 if got := jsonClassOf(t, src, test.word); got != test.want {
82 t.Errorf("%s is %v, want %v", test.word, got, test.want)
83 }
84 })
85 }
86 wholeWordIs(t, HighlightJSON, `{"count": -12.5e+3}`, "-12.5e+3", syntax.ClassNumber)
87}
88
89func TestJSONTakesCommentsInItsStride(t *testing.T) {
90 // Strict JSON has none; tsconfig.json, .jsonc and every editor's own
91 // settings file do, and a scanner that painted them as broken would be
92 // wrong in exactly the files most likely to hold one.
93 const src = "{\n // the entry point\n \"main\": \"index.js\", /* one\n two */ \"x\": 1\n}"
94 spans := HighlightJSON(src)
95
96 if got := jsonClassOf(t, src, "// the entry point"); got != syntax.ClassComment {
97 t.Errorf("a line comment is %v, want comment", got)
98 }
99 if got := jsonClassOf(t, src, "/* one"); got != syntax.ClassComment {
100 t.Errorf("a block comment is %v, want comment", got)
101 }
102 class, ok := classAt(spans, 3, 0) // the " two */" line
103 if !ok || class != syntax.ClassComment {
104 t.Errorf("the second line of the block comment is %v (covered: %v), want comment", class, ok)
105 }
106 if got := jsonClassOf(t, src, `"x"`); got != syntax.ClassAttribute {
107 t.Errorf("the key after the block comment is %v, want attribute", got)
108 }
109}
110
111func TestAnEscapedQuoteDoesNotEndAJSONString(t *testing.T) {
112 const src = `{"say": "a \" b", "n": 1}`
113
114 if got := jsonClassOf(t, src, `"n"`); got != syntax.ClassAttribute {
115 t.Errorf("the key after an escaped quote is %v, want attribute", got)
116 }
117}
118
119func TestBrokenJSONIsStillColoured(t *testing.T) {
120 // A bare word is not JSON, and it is coloured as an identifier rather
121 // than stopping the line; an unterminated string runs to the end of its
122 // line, as a value, and the next line is JSON again.
123 tests := []struct{ src, word string }{
124 {`{"a": undefined}`, "undefined"},
125 {`{"a": "unterminated`, `"unterminated`},
126 {`{"a" 1}`, `"a"`},
127 }
128
129 for _, test := range tests {
130 t.Run(test.src, func(t *testing.T) {
131 spans := HighlightJSON(test.src)
132 if len(spans) != 1 {
133 t.Fatalf("HighlightJSON(%q) returned %d lines, want 1", test.src, len(spans))
134 }
135 if _, ok := classAt(spans, 0, strings.Index(test.src, test.word)); !ok {
136 t.Errorf("nothing covers %q in %q", test.word, test.src)
137 }
138 })
139 }
140 if got := jsonClassOf(t, `{"a": undefined}`, "undefined"); got != syntax.ClassIdentifier {
141 t.Errorf("a bare word is %v, want identifier", got)
142 }
143 if got := jsonClassOf(t, `{"a" 1}`, `"a"`); got != syntax.ClassString {
144 t.Errorf("a string with no colon after it is %v, want string — it is not a key yet", got)
145 }
146}
147
148func TestJSONSpansAreOrderedAndDoNotOverlap(t *testing.T) {
149 for line, onLine := range HighlightJSON(packageJSON) {
150 previousEnd := 0
151 for _, span := range onLine {
152 if span.Start < previousEnd {
153 t.Errorf("line %d: span %+v starts before the previous one ended at %d", line, span, previousEnd)
154 }
155 previousEnd = span.End
156 }
157 }
158}