go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/web/rpcexplorer/src/data/autocomplete.test.tsx (about) 1 // Copyright 2023 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 import { 16 completionForPath, getContext, State, Token, tokenizeJSON, TokenKind, 17 } from './autocomplete'; 18 import { Descriptors, FileDescriptorSet } from './prpc'; 19 import { TestDescriptor } from './testdata/descriptor'; 20 21 /* eslint-disable @typescript-eslint/no-non-null-assertion */ 22 23 describe('Autocomplete', () => { 24 it('tokenizeJSON', () => { 25 const testInput = ` 26 {}[]:, 27 0123 28 null true false 29 "abc" 30 "\\"" 31 "\\x22" 32 "incomp 33 ` + '\n\t\r ' + '"incomplete'; 34 35 const out: Token[] = []; 36 tokenizeJSON(testInput, (tok) => out.push({ 37 kind: tok.kind, 38 raw: tok.raw, 39 val: tok.val, 40 })); 41 42 const token = (kind: TokenKind, raw: string, val?: string): Token => { 43 return { 44 kind: kind, 45 raw: raw, 46 val: val == undefined ? raw : val, 47 }; 48 }; 49 50 expect(out).toEqual([ 51 token(TokenKind.Punctuation, '{'), 52 token(TokenKind.Punctuation, '}'), 53 token(TokenKind.Punctuation, '['), 54 token(TokenKind.Punctuation, ']'), 55 token(TokenKind.Punctuation, ':'), 56 token(TokenKind.Punctuation, ','), 57 token(TokenKind.EverythingElse, '0123'), 58 token(TokenKind.EverythingElse, 'null'), 59 token(TokenKind.EverythingElse, 'true'), 60 token(TokenKind.EverythingElse, 'false'), 61 token(TokenKind.String, '"abc"', 'abc'), 62 token(TokenKind.String, '"\\""', '"'), 63 token(TokenKind.BrokenString, '"\\x22"', ''), 64 token(TokenKind.IncompleteString, '"incomp', 'incomp'), 65 token(TokenKind.IncompleteString, '"incomplete', 'incomplete'), 66 ]); 67 }); 68 69 it('getContext', () => { 70 const call = (text: string): [State, string[]] | undefined => { 71 const ctx = getContext(text); 72 if (ctx == undefined) { 73 return undefined; 74 } 75 const path: string[] = []; 76 for (const item of ctx.path) { 77 let str = item.key == undefined ? '' : item.key.val; 78 if (item.value != undefined) { 79 str += ':' + item.value.val; 80 } 81 if (item.kind == 'list') { 82 str = '[' + str + ']'; 83 } 84 path.push(str); 85 } 86 return [ctx.state, path]; 87 }; 88 89 expect(call('"abc"')).toEqual([ 90 State.AfterValue, 91 [':abc'], 92 ]); 93 94 expect(call('{')).toEqual([ 95 State.BeforeKey, 96 ['', ''], 97 ]); 98 99 expect(call('{ "ab')).toEqual([ 100 State.InsideKey, 101 ['', 'ab'], 102 ]); 103 104 expect(call('{ "abc"')).toEqual([ 105 State.AfterKey, 106 ['', 'abc'], 107 ]); 108 109 expect(call('{ "abc":')).toEqual([ 110 State.BeforeValue, 111 ['', 'abc'], 112 ]); 113 114 expect(call('{ "abc": {')).toEqual([ 115 State.BeforeKey, 116 ['', 'abc', ''], 117 ]); 118 119 expect(call('{ "abc": [')).toEqual([ 120 State.BeforeValue, 121 ['', 'abc', '[]'], 122 ]); 123 124 expect(call('{ "abc": "xy')).toEqual([ 125 State.InsideValue, 126 ['', 'abc:xy'], 127 ]); 128 129 expect(call('{ "abc": {"1": "2", "3": "4"}')).toEqual([ 130 State.AfterValue, 131 ['', 'abc'], 132 ]); 133 134 expect(call('{ "abc": {"1": "2", "3": "4"}, "xyz')).toEqual([ 135 State.InsideKey, 136 ['', 'xyz'], 137 ]); 138 139 const broken = [ 140 ']', 141 '}', 142 '{{', 143 '{[', 144 '{]', 145 '[}', 146 '{,', 147 '{ "abc",', 148 '{ "abc" "def"', 149 '{ "abc": "def" : ', 150 '"abc",', 151 ]; 152 for (const str of broken) { 153 expect(call(str)).toBeUndefined(); 154 } 155 }); 156 157 it('completionForPath', () => { 158 const descs = new Descriptors(TestDescriptor as FileDescriptorSet, []); 159 const msg = descs.message('rpcexplorer.Autocomplete')!; 160 161 const fields = (path: string): string[] => { 162 const ctx = getContext(path); 163 if (!ctx) { 164 return []; 165 } 166 ctx.path.pop(); // the incomplete syntax element being edited 167 const completion = completionForPath(msg, ctx.path); 168 return completion ? completion.fields.map((f) => f.jsonName) : []; 169 }; 170 171 const values = (path: string): string[] => { 172 const ctx = getContext(path); 173 if (!ctx) { 174 return []; 175 } 176 const last = ctx.path.pop()!; 177 const field = last.key?.val || ''; 178 const completion = completionForPath(msg, ctx.path); 179 return completion ? completion.values(field).map((v) => v.value) : []; 180 }; 181 182 expect(fields('{')).toEqual([ 183 'singleInt', 184 'singleEnum', 185 'singleMsg', 186 'repeatedInt', 187 'repeatedEnum', 188 'repeatedMsg', 189 'mapInt', 190 'mapEnum', 191 'mapMsg', 192 ]); 193 194 expect(fields('{"singleMsg": {')).toEqual(['fooBar']); 195 expect(fields('{"repeatedMsg": [{')).toEqual(['fooBar']); 196 expect(fields('{"mapMsg": {0: {')).toEqual(['fooBar']); 197 198 expect(fields('{"singleMsg": [{')).toEqual([]); 199 expect(fields('{"repeatedMsg": {')).toEqual([]); 200 expect(fields('{"repeatedMsg": [')).toEqual([]); 201 expect(fields('{"mapMsg": [{')).toEqual([]); 202 expect(fields('{"mapMsg": {')).toEqual([]); 203 204 expect(fields('{"singleInt": {')).toEqual([]); 205 expect(fields('{"repeatedInt": [{')).toEqual([]); 206 expect(fields('{"mapInt": {0: {')).toEqual([]); 207 208 expect(fields('{"missing": {')).toEqual([]); 209 expect(fields('{"single_msg": {')).toEqual([]); 210 211 expect(values('{"singleEnum": ')).toEqual(['V0', 'V1']); 212 expect(values('{"repeatedEnum": [')).toEqual(['V0', 'V1']); 213 expect(values('{"mapEnum": {0: ')).toEqual(['V0', 'V1']); 214 215 expect(values('{"singleMsg": ')).toEqual([]); 216 expect(values('{"repeatedMsg": ')).toEqual([]); 217 expect(values('{"mapMsg": ')).toEqual([]); 218 }); 219 });