14.1 KB426 lines
Blame
1import { describe, expect, it } from 'vitest';
2
3import { Radar } from '../src/language/index.js';
4import { expectNoErrorsOrAlternatives, radarParse as parse } from './test-util.js';
5import { parse as parseAsync, MermaidParseError } from '../src/parse.js';
6
7const mutateGlobalSpacing = (context: string) => {
8 return [
9 context,
10 ` ${context} `,
11 `\t${context}\t`,
12 `
13 \t${context}
14 `,
15 ];
16};
17
18describe('radar', () => {
19 it.each([
20 ...mutateGlobalSpacing('radar-beta'),
21 ...mutateGlobalSpacing('radar-beta:'),
22 ...mutateGlobalSpacing('radar-beta :'),
23 ])('should handle regular radar', (context: string) => {
24 const result = parse(context);
25 expectNoErrorsOrAlternatives(result);
26 expect(result.value.$type).toBe(Radar.$type);
27 });
28
29 describe('should handle title, accDescr, and accTitle', () => {
30 it.each([
31 ...mutateGlobalSpacing(' title My Title'),
32 ...mutateGlobalSpacing('\n title My Title'),
33 ])('should handle title', (context: string) => {
34 const result = parse(`radar-beta${context}`);
35 expectNoErrorsOrAlternatives(result);
36 expect(result.value.$type).toBe(Radar.$type);
37
38 const { title } = result.value;
39 expect(title).toBe('My Title');
40 });
41
42 it.each([
43 ...mutateGlobalSpacing(' accDescr: My Accessible Description'),
44 ...mutateGlobalSpacing('\n accDescr: My Accessible Description'),
45 ])('should handle accDescr', (context: string) => {
46 const result = parse(`radar-beta${context}`);
47 expectNoErrorsOrAlternatives(result);
48 expect(result.value.$type).toBe(Radar.$type);
49
50 const { accDescr } = result.value;
51 expect(accDescr).toBe('My Accessible Description');
52 });
53
54 it.each([
55 ...mutateGlobalSpacing(' accTitle: My Accessible Title'),
56 ...mutateGlobalSpacing('\n accTitle: My Accessible Title'),
57 ])('should handle accTitle', (context: string) => {
58 const result = parse(`radar-beta${context}`);
59 expectNoErrorsOrAlternatives(result);
60 expect(result.value.$type).toBe(Radar.$type);
61
62 const { accTitle } = result.value;
63 expect(accTitle).toBe('My Accessible Title');
64 });
65
66 it.each([
67 ...mutateGlobalSpacing(
68 ' title My Title\n accDescr: My Accessible Description\n accTitle: My Accessible Title'
69 ),
70 ...mutateGlobalSpacing(
71 '\n title My Title\n accDescr: My Accessible Description\n accTitle: My Accessible Title'
72 ),
73 ])('should handle title + accDescr + accTitle', (context: string) => {
74 const result = parse(`radar-beta${context}`);
75 expectNoErrorsOrAlternatives(result);
76 expect(result.value.$type).toBe(Radar.$type);
77
78 const { title, accDescr, accTitle } = result.value;
79 expect(title).toBe('My Title');
80 expect(accDescr).toBe('My Accessible Description');
81 expect(accTitle).toBe('My Accessible Title');
82 });
83 });
84
85 describe('should handle axis', () => {
86 it.each([`axis my-axis`, `axis my-axis["My Axis Label"]`])(
87 'should handle one axis',
88 (context: string) => {
89 const result = parse(`radar-beta\n${context}`);
90 expectNoErrorsOrAlternatives(result);
91 expect(result.value.$type).toBe(Radar.$type);
92
93 const { axes } = result.value;
94 expect(axes).toHaveLength(1);
95 expect(axes[0].$type).toBe('Axis');
96 expect(axes[0].name).toBe('my-axis');
97 }
98 );
99
100 it.each([
101 `axis my-axis["My Axis Label"]
102 axis my-axis2`,
103 `axis my-axis, my-axis2`,
104 `axis my-axis["My Axis Label"], my-axis2`,
105 `axis my-axis, my-axis2["My Second Axis Label"]`,
106 ])('should handle multiple axes', (context: string) => {
107 const result = parse(`radar-beta\n${context}`);
108 expectNoErrorsOrAlternatives(result);
109 expect(result.value.$type).toBe(Radar.$type);
110
111 const { axes } = result.value;
112 expect(axes).toHaveLength(2);
113 expect(axes.every((axis) => axis.$type === 'Axis')).toBe(true);
114 expect(axes[0].name).toBe('my-axis');
115 expect(axes[1].name).toBe('my-axis2');
116 });
117
118 it.each([
119 `axis my-axis["My Axis Label"]
120 axis my-axis2["My Second Axis Label"]`,
121 `axis my-axis ["My Axis Label"], my-axis2\t["My Second Axis Label"]`,
122 ])('should handle axis labels', (context: string) => {
123 const result = parse(`radar-beta\n${context}`);
124 expectNoErrorsOrAlternatives(result);
125 expect(result.value.$type).toBe(Radar.$type);
126
127 const { axes } = result.value;
128 expect(axes).toHaveLength(2);
129 expect(axes[0].name).toBe('my-axis');
130 expect(axes[0].label).toBe('My Axis Label');
131 expect(axes[1].name).toBe('my-axis2');
132 expect(axes[1].label).toBe('My Second Axis Label');
133 });
134
135 it('should not allow empty axis names', () => {
136 const result = parse(`radar-beta
137 axis`);
138 expect(result.parserErrors).not.toHaveLength(0);
139 });
140
141 it('should not allow non-comma separated axis names', () => {
142 const result = parse(`radar-beta
143 axis my-axis my-axis2`);
144 expect(result.parserErrors).not.toHaveLength(0);
145 });
146 });
147
148 describe('should handle curves', () => {
149 it.each([
150 `radar-beta
151 curve my-curve`,
152 `radar-beta
153 curve my-curve["My Curve Label"]`,
154 ])('should not allow curves without axes', (context: string) => {
155 const result = parse(`radar-beta${context}`);
156 expect(result.parserErrors).not.toHaveLength(0);
157 });
158
159 it.each([
160 `radar-beta
161 axis my-axis
162 curve my-curve`,
163 `radar-beta
164 axis my-axis
165 curve my-curve["My Curve Label"]`,
166 ])('should not allow curves without entries', (context: string) => {
167 const result = parse(`radar-beta${context}`);
168 expect(result.parserErrors).not.toHaveLength(0);
169 });
170
171 it.each([
172 `curve my-curve { 1 }`,
173 `curve my-curve {
174 1
175 }`,
176 `curve my-curve {
177
178 1
179
180 }`,
181 ])('should handle one curve with one entry', (context: string) => {
182 const result = parse(`radar-beta\naxis my-axis\n${context}`);
183 expectNoErrorsOrAlternatives(result);
184 expect(result.value.$type).toBe(Radar.$type);
185
186 const { curves } = result.value;
187 expect(curves).toHaveLength(1);
188 expect(curves[0].$type).toBe('Curve');
189 expect(curves[0].name).toBe('my-curve');
190 expect(curves[0].entries).toHaveLength(1);
191 expect(curves[0].entries[0].$type).toBe('Entry');
192 expect(curves[0].entries[0].value).toBe(1);
193 });
194
195 it.each([
196 `curve my-curve { my-axis 1 }`,
197 `curve my-curve { my-axis : 1 }`,
198 `curve my-curve {
199 my-axis: 1
200 }`,
201 ])('should handle one curve with one detailed entry', (context: string) => {
202 const result = parse(`radar-beta\naxis my-axis\n${context}`);
203 expectNoErrorsOrAlternatives(result);
204 expect(result.value.$type).toBe(Radar.$type);
205
206 const { curves } = result.value;
207 expect(curves).toHaveLength(1);
208 expect(curves[0].$type).toBe('Curve');
209 expect(curves[0].name).toBe('my-curve');
210 expect(curves[0].entries).toHaveLength(1);
211 expect(curves[0].entries[0].$type).toBe('Entry');
212 expect(curves[0].entries[0].value).toBe(1);
213 expect(curves[0].entries[0]?.axis?.$refText).toBe('my-axis');
214 });
215
216 it.each([
217 `curve my-curve { ax1 1, ax2 2 }`,
218 `curve my-curve {
219 ax1 1,
220 ax2 2
221 }`,
222 `curve my-curve["My Curve Label"] {
223 ax1: 1, ax2: 2
224 }`,
225 ])('should handle one curve with multiple detailed entries', (context: string) => {
226 const result = parse(`radar-beta\naxis ax1, ax1\n${context}`);
227 expectNoErrorsOrAlternatives(result);
228 expect(result.value.$type).toBe(Radar.$type);
229
230 const { curves } = result.value;
231 expect(curves).toHaveLength(1);
232 expect(curves[0].$type).toBe('Curve');
233 expect(curves[0].name).toBe('my-curve');
234 expect(curves[0].entries).toHaveLength(2);
235 expect(curves[0].entries[0].$type).toBe('Entry');
236 expect(curves[0].entries[0].value).toBe(1);
237 expect(curves[0].entries[0]?.axis?.$refText).toBe('ax1');
238 expect(curves[0].entries[1].$type).toBe('Entry');
239 expect(curves[0].entries[1].value).toBe(2);
240 expect(curves[0].entries[1]?.axis?.$refText).toBe('ax2');
241 });
242
243 it.each([
244 `curve c1 { ax1 1, ax2 2 }
245 curve c2 { ax1 3, ax2 4 }`,
246 `curve c1 {
247 ax1 1,
248 ax2 2
249 }
250 curve c2 {
251 ax1 3,
252 ax2 4
253 }`,
254 `curve c1{ 1, 2 }, c2{ 3, 4 }`,
255 ])('should handle multiple curves', (context: string) => {
256 const result = parse(`radar-beta\naxis ax1, ax1\n${context}`);
257 expectNoErrorsOrAlternatives(result);
258 expect(result.value.$type).toBe(Radar.$type);
259
260 const { curves } = result.value;
261 expect(curves).toHaveLength(2);
262 expect(curves.every((curve) => curve.$type === 'Curve')).toBe(true);
263 expect(curves[0].name).toBe('c1');
264 expect(curves[1].name).toBe('c2');
265 });
266
267 it('should not allow empty curve names', () => {
268 const result = parse(`radar-beta
269 axis my-axis
270 curve`);
271 expect(result.parserErrors).not.toHaveLength(0);
272 });
273
274 it('should not allow number and detailed entries in the same curve', () => {
275 const result = parse(`radar-beta
276 axis ax1, ax2
277 curve my-curve { 1, ax1 2 }`);
278 expect(result.parserErrors).not.toHaveLength(0);
279 });
280
281 it('should not allow non-comma separated entries', () => {
282 const result = parse(`radar-beta
283 axis ax1, ax2
284 curve my-curve { ax1 1 ax2 2 }`);
285 expect(result.parserErrors).not.toHaveLength(0);
286 });
287 });
288
289 describe('should handle options', () => {
290 it.each([`ticks 5`, `min 50`, `max 50`])(
291 `should handle number option %s`,
292 (context: string) => {
293 const result = parse(`radar-beta
294 axis ax1, ax2
295 curve c1 { ax1 1, ax2 2 }
296 ${context}`);
297 expectNoErrorsOrAlternatives(result);
298 expect(result.value.$type).toBe(Radar.$type);
299
300 const { options } = result.value;
301 expect(options).toBeDefined();
302 const option = options.find((option) => option.name === context.split(' ')[0]);
303 expect(option).toBeDefined();
304 expect(option?.value).toBe(Number(context.split(' ')[1]));
305 }
306 );
307
308 it.each([`graticule circle`, `graticule polygon`])(
309 `should handle string option %s`,
310 (context: string) => {
311 const result = parse(`radar-beta
312 axis ax1, ax2
313 curve c1 { ax1 1, ax2 2 }
314 ${context}`);
315 expectNoErrorsOrAlternatives(result);
316 expect(result.value.$type).toBe(Radar.$type);
317
318 const { options } = result.value;
319 expect(options).toBeDefined();
320 const option = options.find((option) => option.name === context.split(' ')[0]);
321 expect(option).toBeDefined();
322 expect(option?.value).toBe(context.split(' ')[1]);
323 }
324 );
325
326 it.each([`showLegend true`, `showLegend false`])(
327 `should handle boolean option %s`,
328 (context: string) => {
329 const result = parse(`radar-beta
330 axis ax1, ax2
331 curve c1 { ax1 1, ax2 2 }
332 ${context}`);
333 expectNoErrorsOrAlternatives(result);
334 expect(result.value.$type).toBe(Radar.$type);
335
336 const { options } = result.value;
337 expect(options).toBeDefined();
338 const option = options.find((option) => option.name === context.split(' ')[0]);
339 expect(option).toBeDefined();
340 expect(option?.value).toBe(context.split(' ')[1] === 'true');
341 }
342 );
343 });
344
345 describe('error messages with line and column numbers', () => {
346 it('should include line and column numbers in parser errors for radar diagrams', async () => {
347 const invalidRadar = `radar-beta
348 title Restaurant Comparison
349 axis food["Food Quality"], service["Service"], price["Price"]
350 axis ambiance["Ambiance"],
351
352 curve a["Restaurant A"]{4, 3, 2, 4}`;
353
354 try {
355 await parseAsync('radar', invalidRadar);
356 expect.fail('Should have thrown MermaidParseError');
357 } catch (error: any) {
358 expect(error).toBeInstanceOf(MermaidParseError);
359 expect(error.message).toMatch(/line \d+/);
360 expect(error.message).toMatch(/column \d+/);
361 }
362 });
363
364 it('should include line and column numbers for missing curve entries', async () => {
365 const invalidRadar = `radar-beta
366 axis my-axis
367 curve my-curve`;
368
369 try {
370 await parseAsync('radar', invalidRadar);
371 expect.fail('Should have thrown MermaidParseError');
372 } catch (error: any) {
373 expect(error).toBeInstanceOf(MermaidParseError);
374 // Line and column may be ? if not available
375 expect(error.message).toMatch(/line (\d+|\?)/);
376 expect(error.message).toMatch(/column (\d+|\?)/);
377 }
378 });
379
380 it('should include line and column numbers for invalid axis syntax', async () => {
381 const invalidRadar = `radar-beta
382 axis my-axis my-axis2`;
383
384 try {
385 await parseAsync('radar', invalidRadar);
386 expect.fail('Should have thrown MermaidParseError');
387 } catch (error: any) {
388 expect(error).toBeInstanceOf(MermaidParseError);
389 expect(error.message).toMatch(/line \d+/);
390 expect(error.message).toMatch(/column \d+/);
391 }
392 });
393
394 it('should handle lexer errors with line and column numbers', async () => {
395 const invalidRadar = `radar-beta
396 axis A
397 curve B{1}
398 invalid@symbol`;
399
400 try {
401 await parseAsync('radar', invalidRadar);
402 expect.fail('Should have thrown MermaidParseError');
403 } catch (error: any) {
404 expect(error).toBeInstanceOf(MermaidParseError);
405 // Should have line and column in the error message
406 expect(error.message).toMatch(/line (\d+|\?)/);
407 expect(error.message).toMatch(/column (\d+|\?)/);
408 }
409 });
410
411 it('should format error message with "Parse error on line X, column Y" prefix', async () => {
412 const invalidRadar = `radar-beta
413 axis`;
414
415 try {
416 await parseAsync('radar', invalidRadar);
417 expect.fail('Should have thrown MermaidParseError');
418 } catch (error: any) {
419 expect(error).toBeInstanceOf(MermaidParseError);
420 // Line and column may be ? if not available
421 expect(error.message).toMatch(/Parse error on line (\d+|\?), column (\d+|\?):/);
422 }
423 });
424 });
425});
426