language_provider.test.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. import Plain from 'slate-plain-serializer';
  2. import LanguageProvider from '../language_provider';
  3. describe('Language completion provider', () => {
  4. const datasource = {
  5. metadataRequest: () => ({ data: { data: [] } }),
  6. };
  7. describe('empty query suggestions', () => {
  8. it('returns default suggestions on emtpty context', () => {
  9. const instance = new LanguageProvider(datasource);
  10. const value = Plain.deserialize('');
  11. const result = instance.provideCompletionItems({ text: '', prefix: '', value, wrapperClasses: [] });
  12. expect(result.context).toBeUndefined();
  13. expect(result.refresher).toBeUndefined();
  14. expect(result.suggestions).toMatchObject([
  15. {
  16. label: 'Functions',
  17. },
  18. ]);
  19. });
  20. it('returns default suggestions with metrics on emtpty context when metrics were provided', () => {
  21. const instance = new LanguageProvider(datasource, { metrics: ['foo', 'bar'] });
  22. const value = Plain.deserialize('');
  23. const result = instance.provideCompletionItems({ text: '', prefix: '', value, wrapperClasses: [] });
  24. expect(result.context).toBeUndefined();
  25. expect(result.refresher).toBeUndefined();
  26. expect(result.suggestions).toMatchObject([
  27. {
  28. label: 'Functions',
  29. },
  30. {
  31. label: 'Metrics',
  32. },
  33. ]);
  34. });
  35. });
  36. describe('range suggestions', () => {
  37. it('returns range suggestions in range context', () => {
  38. const instance = new LanguageProvider(datasource);
  39. const value = Plain.deserialize('1');
  40. const result = instance.provideCompletionItems({
  41. text: '1',
  42. prefix: '1',
  43. value,
  44. wrapperClasses: ['context-range'],
  45. });
  46. expect(result.context).toBe('context-range');
  47. expect(result.refresher).toBeUndefined();
  48. expect(result.suggestions).toEqual([
  49. {
  50. items: [{ label: '1m' }, { label: '5m' }, { label: '10m' }, { label: '30m' }, { label: '1h' }],
  51. label: 'Range vector',
  52. },
  53. ]);
  54. });
  55. });
  56. describe('metric suggestions', () => {
  57. it('returns metrics and function suggestions in an unknown context', () => {
  58. const instance = new LanguageProvider(datasource, { metrics: ['foo', 'bar'] });
  59. const value = Plain.deserialize('a');
  60. const result = instance.provideCompletionItems({ text: 'a', prefix: 'a', value, wrapperClasses: [] });
  61. expect(result.context).toBeUndefined();
  62. expect(result.refresher).toBeUndefined();
  63. expect(result.suggestions).toMatchObject([
  64. {
  65. label: 'Functions',
  66. },
  67. {
  68. label: 'Metrics',
  69. },
  70. ]);
  71. });
  72. it('returns metrics and function suggestions after a binary operator', () => {
  73. const instance = new LanguageProvider(datasource, { metrics: ['foo', 'bar'] });
  74. const value = Plain.deserialize('*');
  75. const result = instance.provideCompletionItems({ text: '*', prefix: '', value, wrapperClasses: [] });
  76. expect(result.context).toBeUndefined();
  77. expect(result.refresher).toBeUndefined();
  78. expect(result.suggestions).toMatchObject([
  79. {
  80. label: 'Functions',
  81. },
  82. {
  83. label: 'Metrics',
  84. },
  85. ]);
  86. });
  87. it('returns no suggestions at the beginning of a non-empty function', () => {
  88. const instance = new LanguageProvider(datasource, { metrics: ['foo', 'bar'] });
  89. const value = Plain.deserialize('sum(up)');
  90. const range = value.selection.merge({
  91. anchorOffset: 4,
  92. });
  93. const valueWithSelection = value.change().select(range).value;
  94. const result = instance.provideCompletionItems({
  95. text: '',
  96. prefix: '',
  97. value: valueWithSelection,
  98. wrapperClasses: [],
  99. });
  100. expect(result.context).toBeUndefined();
  101. expect(result.refresher).toBeUndefined();
  102. expect(result.suggestions.length).toEqual(0);
  103. });
  104. });
  105. describe('label suggestions', () => {
  106. it('returns default label suggestions on label context and no metric', () => {
  107. const instance = new LanguageProvider(datasource);
  108. const value = Plain.deserialize('{}');
  109. const range = value.selection.merge({
  110. anchorOffset: 1,
  111. });
  112. const valueWithSelection = value.change().select(range).value;
  113. const result = instance.provideCompletionItems({
  114. text: '',
  115. prefix: '',
  116. wrapperClasses: ['context-labels'],
  117. value: valueWithSelection,
  118. });
  119. expect(result.context).toBe('context-labels');
  120. expect(result.suggestions).toEqual([{ items: [{ label: 'job' }, { label: 'instance' }], label: 'Labels' }]);
  121. });
  122. it('returns label suggestions on label context and metric', () => {
  123. const instance = new LanguageProvider(datasource, { labelKeys: { '{__name__="metric"}': ['bar'] } });
  124. const value = Plain.deserialize('metric{}');
  125. const range = value.selection.merge({
  126. anchorOffset: 7,
  127. });
  128. const valueWithSelection = value.change().select(range).value;
  129. const result = instance.provideCompletionItems({
  130. text: '',
  131. prefix: '',
  132. wrapperClasses: ['context-labels'],
  133. value: valueWithSelection,
  134. });
  135. expect(result.context).toBe('context-labels');
  136. expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]);
  137. });
  138. it('returns label suggestions on label context but leaves out labels that already exist', () => {
  139. const instance = new LanguageProvider(datasource, {
  140. labelKeys: { '{job1="foo",job2!="foo",job3=~"foo"}': ['bar', 'job1', 'job2', 'job3'] },
  141. });
  142. const value = Plain.deserialize('{job1="foo",job2!="foo",job3=~"foo",}');
  143. const range = value.selection.merge({
  144. anchorOffset: 36,
  145. });
  146. const valueWithSelection = value.change().select(range).value;
  147. const result = instance.provideCompletionItems({
  148. text: '',
  149. prefix: '',
  150. wrapperClasses: ['context-labels'],
  151. value: valueWithSelection,
  152. });
  153. expect(result.context).toBe('context-labels');
  154. expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]);
  155. });
  156. it('returns label value suggestions inside a label value context after a negated matching operator', () => {
  157. const instance = new LanguageProvider(datasource, {
  158. labelKeys: { '{}': ['label'] },
  159. labelValues: { '{}': { label: ['a', 'b', 'c'] } },
  160. });
  161. const value = Plain.deserialize('{label!=}');
  162. const range = value.selection.merge({ anchorOffset: 8 });
  163. const valueWithSelection = value.change().select(range).value;
  164. const result = instance.provideCompletionItems({
  165. text: '!=',
  166. prefix: '',
  167. wrapperClasses: ['context-labels'],
  168. labelKey: 'label',
  169. value: valueWithSelection,
  170. });
  171. expect(result.context).toBe('context-label-values');
  172. expect(result.suggestions).toEqual([
  173. {
  174. items: [{ label: 'a' }, { label: 'b' }, { label: 'c' }],
  175. label: 'Label values for "label"',
  176. },
  177. ]);
  178. });
  179. it('returns a refresher on label context and unavailable metric', () => {
  180. const instance = new LanguageProvider(datasource, { labelKeys: { '{__name__="foo"}': ['bar'] } });
  181. const value = Plain.deserialize('metric{}');
  182. const range = value.selection.merge({
  183. anchorOffset: 7,
  184. });
  185. const valueWithSelection = value.change().select(range).value;
  186. const result = instance.provideCompletionItems({
  187. text: '',
  188. prefix: '',
  189. wrapperClasses: ['context-labels'],
  190. value: valueWithSelection,
  191. });
  192. expect(result.context).toBeUndefined();
  193. expect(result.refresher).toBeInstanceOf(Promise);
  194. expect(result.suggestions).toEqual([]);
  195. });
  196. it('returns label values on label context when given a metric and a label key', () => {
  197. const instance = new LanguageProvider(datasource, {
  198. labelKeys: { '{__name__="metric"}': ['bar'] },
  199. labelValues: { '{__name__="metric"}': { bar: ['baz'] } },
  200. });
  201. const value = Plain.deserialize('metric{bar=ba}');
  202. const range = value.selection.merge({
  203. anchorOffset: 13,
  204. });
  205. const valueWithSelection = value.change().select(range).value;
  206. const result = instance.provideCompletionItems({
  207. text: '=ba',
  208. prefix: 'ba',
  209. wrapperClasses: ['context-labels'],
  210. labelKey: 'bar',
  211. value: valueWithSelection,
  212. });
  213. expect(result.context).toBe('context-label-values');
  214. expect(result.suggestions).toEqual([{ items: [{ label: 'baz' }], label: 'Label values for "bar"' }]);
  215. });
  216. it('returns label suggestions on aggregation context and metric w/ selector', () => {
  217. const instance = new LanguageProvider(datasource, { labelKeys: { '{__name__="metric",foo="xx"}': ['bar'] } });
  218. const value = Plain.deserialize('sum(metric{foo="xx"}) by ()');
  219. const range = value.selection.merge({
  220. anchorOffset: 26,
  221. });
  222. const valueWithSelection = value.change().select(range).value;
  223. const result = instance.provideCompletionItems({
  224. text: '',
  225. prefix: '',
  226. wrapperClasses: ['context-aggregation'],
  227. value: valueWithSelection,
  228. });
  229. expect(result.context).toBe('context-aggregation');
  230. expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]);
  231. });
  232. it('returns label suggestions on aggregation context and metric w/o selector', () => {
  233. const instance = new LanguageProvider(datasource, { labelKeys: { '{__name__="metric"}': ['bar'] } });
  234. const value = Plain.deserialize('sum(metric) by ()');
  235. const range = value.selection.merge({
  236. anchorOffset: 16,
  237. });
  238. const valueWithSelection = value.change().select(range).value;
  239. const result = instance.provideCompletionItems({
  240. text: '',
  241. prefix: '',
  242. wrapperClasses: ['context-aggregation'],
  243. value: valueWithSelection,
  244. });
  245. expect(result.context).toBe('context-aggregation');
  246. expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]);
  247. });
  248. it('returns label suggestions inside a multi-line aggregation context', () => {
  249. const instance = new LanguageProvider(datasource, {
  250. labelKeys: { '{__name__="metric"}': ['label1', 'label2', 'label3'] },
  251. });
  252. const value = Plain.deserialize('sum(\nmetric\n)\nby ()');
  253. const aggregationTextBlock = value.document.getBlocksAsArray()[3];
  254. const range = value.selection.moveToStartOf(aggregationTextBlock).merge({ anchorOffset: 4 });
  255. const valueWithSelection = value.change().select(range).value;
  256. const result = instance.provideCompletionItems({
  257. text: '',
  258. prefix: '',
  259. wrapperClasses: ['context-aggregation'],
  260. value: valueWithSelection,
  261. });
  262. expect(result.context).toBe('context-aggregation');
  263. expect(result.suggestions).toEqual([
  264. {
  265. items: [{ label: 'label1' }, { label: 'label2' }, { label: 'label3' }],
  266. label: 'Labels',
  267. },
  268. ]);
  269. });
  270. it('returns label suggestions inside an aggregation context with a range vector', () => {
  271. const instance = new LanguageProvider(datasource, {
  272. labelKeys: { '{__name__="metric"}': ['label1', 'label2', 'label3'] },
  273. });
  274. const value = Plain.deserialize('sum(rate(metric[1h])) by ()');
  275. const range = value.selection.merge({
  276. anchorOffset: 26,
  277. });
  278. const valueWithSelection = value.change().select(range).value;
  279. const result = instance.provideCompletionItems({
  280. text: '',
  281. prefix: '',
  282. wrapperClasses: ['context-aggregation'],
  283. value: valueWithSelection,
  284. });
  285. expect(result.context).toBe('context-aggregation');
  286. expect(result.suggestions).toEqual([
  287. {
  288. items: [{ label: 'label1' }, { label: 'label2' }, { label: 'label3' }],
  289. label: 'Labels',
  290. },
  291. ]);
  292. });
  293. it('returns label suggestions inside an aggregation context with a range vector and label', () => {
  294. const instance = new LanguageProvider(datasource, {
  295. labelKeys: { '{__name__="metric",label1="value"}': ['label1', 'label2', 'label3'] },
  296. });
  297. const value = Plain.deserialize('sum(rate(metric{label1="value"}[1h])) by ()');
  298. const range = value.selection.merge({
  299. anchorOffset: 42,
  300. });
  301. const valueWithSelection = value.change().select(range).value;
  302. const result = instance.provideCompletionItems({
  303. text: '',
  304. prefix: '',
  305. wrapperClasses: ['context-aggregation'],
  306. value: valueWithSelection,
  307. });
  308. expect(result.context).toBe('context-aggregation');
  309. expect(result.suggestions).toEqual([
  310. {
  311. items: [{ label: 'label1' }, { label: 'label2' }, { label: 'label3' }],
  312. label: 'Labels',
  313. },
  314. ]);
  315. });
  316. it('returns no suggestions inside an unclear aggregation context using alternate syntax', () => {
  317. const instance = new LanguageProvider(datasource, {
  318. labelKeys: { '{__name__="metric"}': ['label1', 'label2', 'label3'] },
  319. });
  320. const value = Plain.deserialize('sum by ()');
  321. const range = value.selection.merge({
  322. anchorOffset: 8,
  323. });
  324. const valueWithSelection = value.change().select(range).value;
  325. const result = instance.provideCompletionItems({
  326. text: '',
  327. prefix: '',
  328. wrapperClasses: ['context-aggregation'],
  329. value: valueWithSelection,
  330. });
  331. expect(result.context).toBe('context-aggregation');
  332. expect(result.suggestions).toEqual([]);
  333. });
  334. it('returns label suggestions inside an aggregation context using alternate syntax', () => {
  335. const instance = new LanguageProvider(datasource, {
  336. labelKeys: { '{__name__="metric"}': ['label1', 'label2', 'label3'] },
  337. });
  338. const value = Plain.deserialize('sum by () (metric)');
  339. const range = value.selection.merge({
  340. anchorOffset: 8,
  341. });
  342. const valueWithSelection = value.change().select(range).value;
  343. const result = instance.provideCompletionItems({
  344. text: '',
  345. prefix: '',
  346. wrapperClasses: ['context-aggregation'],
  347. value: valueWithSelection,
  348. });
  349. expect(result.context).toBe('context-aggregation');
  350. expect(result.suggestions).toEqual([
  351. {
  352. items: [{ label: 'label1' }, { label: 'label2' }, { label: 'label3' }],
  353. label: 'Labels',
  354. },
  355. ]);
  356. });
  357. });
  358. });