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  });