@khanacademy/perseus 19.0.0 → 19.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/dist/es/index.css +209 -104
  3. package/dist/es/index.js +806 -877
  4. package/dist/es/index.js.map +1 -1
  5. package/dist/index.css +209 -104
  6. package/dist/index.d.ts +3 -2
  7. package/dist/index.js +849 -920
  8. package/dist/index.js.map +1 -1
  9. package/dist/mixins/changeable.d.ts +1 -1
  10. package/dist/perseus-api.d.ts +1 -0
  11. package/dist/renderer.d.ts +2 -4
  12. package/dist/server-item-renderer.d.ts +1 -0
  13. package/dist/types.d.ts +4 -0
  14. package/dist/util/geometry.d.ts +2 -0
  15. package/dist/util/graphie.d.ts +107 -1
  16. package/dist/util/math.d.ts +2 -0
  17. package/dist/util/parse-perseus-json.d.ts +3 -3
  18. package/dist/widgets/expression.d.ts +6 -0
  19. package/dist/widgets/label-image.d.ts +2 -0
  20. package/package.json +3 -4
  21. package/src/__testdata__/renderer.testdata.ts +152 -0
  22. package/src/__tests__/renderer.test.tsx +17 -0
  23. package/src/components/math-input.tsx +4 -6
  24. package/src/index.ts +8 -3
  25. package/src/mixins/changeable.ts +1 -1
  26. package/src/renderer.tsx +14 -8
  27. package/src/types.ts +9 -0
  28. package/src/util/geometry.test.ts +75 -0
  29. package/src/util/geometry.ts +19 -0
  30. package/src/util/graphie.test.ts +1291 -0
  31. package/src/util/graphie.ts +832 -784
  32. package/src/util/interactive.ts +23 -51
  33. package/src/util/math.test.ts +11 -0
  34. package/src/util/math.ts +8 -0
  35. package/src/util/parse-perseus-json.test.ts +3 -3
  36. package/src/util/parse-perseus-json.ts +4 -4
  37. package/src/widgets/__tests__/__snapshots__/input-number.test.ts.snap +4 -2
  38. package/src/widgets/__tests__/__snapshots__/numeric-input.test.ts.snap +4 -2
  39. package/src/widgets/__tests__/expression-mobile.test.tsx +8 -5
  40. package/src/widgets/expression.tsx +1 -23
  41. package/src/widgets/label-image.tsx +0 -1
  42. package/tsconfig-build.tsbuildinfo +1 -1
@@ -0,0 +1,1291 @@
1
+ // eslint-disable-next-line import/no-extraneous-dependencies
2
+ import Raphael from "raphael";
3
+
4
+ import {testDependencies} from "../../../../testing/test-dependencies";
5
+ import * as Dependencies from "../dependencies";
6
+
7
+ import GraphUtils, {normalizeRange} from "./graphie";
8
+
9
+ import type {Graphie} from "./graphie";
10
+
11
+ function createMockRaphaelElement(name = "mockRaphaelElement") {
12
+ return {
13
+ constructor: {prototype: Raphael.el},
14
+ attr: jest.fn().mockName(`${name}.attr`),
15
+ };
16
+ }
17
+
18
+ function createMockRaphael() {
19
+ return {
20
+ setSize: jest.fn().mockName("raphael.setSize"),
21
+ ellipse: jest.fn().mockName("raphael.ellipse"),
22
+ rect: jest.fn().mockName("raphael.rect"),
23
+ path: jest.fn().mockName("raphael.path"),
24
+ set: jest
25
+ .fn()
26
+ .mockName("raphael.set")
27
+ .mockImplementation(createFakeRaphaelSet),
28
+ };
29
+ }
30
+
31
+ function createFakeRaphaelSet() {
32
+ const contents: any[] = [];
33
+ return {
34
+ constructor: {prototype: Raphael.st},
35
+ push(...items) {
36
+ return contents.push(...items);
37
+ },
38
+ attr: jest.fn().mockName("raphael.set().attr"),
39
+ };
40
+ }
41
+
42
+ function createAndInitGraphie(): Graphie {
43
+ const graphie = GraphUtils.createGraphie();
44
+ graphie.raphael = createMockRaphael();
45
+
46
+ // The graph is 50px by 50px
47
+ graphie.init({
48
+ range: [
49
+ [0, 10],
50
+ [0, 10],
51
+ ],
52
+ scale: 5,
53
+ isMobile: false,
54
+ });
55
+
56
+ return graphie;
57
+ }
58
+
59
+ describe("Graphie drawing tools", () => {
60
+ it("sets the canvas size when initialized", () => {
61
+ const graphie = createAndInitGraphie();
62
+ expect(graphie.raphael.setSize).toHaveBeenCalledWith(50, 50);
63
+ });
64
+
65
+ describe("circle", () => {
66
+ it("uses the given center and radius", () => {
67
+ const graphie = createAndInitGraphie();
68
+
69
+ const mockRaphaelElement = createMockRaphaelElement();
70
+ graphie.raphael.ellipse.mockReturnValue(mockRaphaelElement);
71
+
72
+ graphie.circle([1, 2], 3);
73
+
74
+ expect(graphie.raphael.ellipse).toHaveBeenCalledWith(5, 40, 15, 15);
75
+ });
76
+
77
+ it("uses the style, if given", () => {
78
+ const graphie = createAndInitGraphie();
79
+
80
+ const mockRaphaelElement = createMockRaphaelElement();
81
+ graphie.raphael.ellipse.mockReturnValue(mockRaphaelElement);
82
+
83
+ graphie.circle([0, 0], 1, {fill: "#112233", stroke: "#445566"});
84
+
85
+ expect(mockRaphaelElement.attr).toHaveBeenCalledWith(
86
+ expect.objectContaining({
87
+ fill: "#112233",
88
+ stroke: "#445566",
89
+ }),
90
+ );
91
+ });
92
+
93
+ it("uses the default style, if none given", () => {
94
+ const graphie = createAndInitGraphie();
95
+ const mockRaphaelElement = createMockRaphaelElement();
96
+ graphie.raphael.ellipse.mockReturnValue(mockRaphaelElement);
97
+
98
+ graphie.circle([0, 0], 1);
99
+
100
+ expect(mockRaphaelElement.attr).toHaveBeenCalledWith({
101
+ // The defaults
102
+ fill: "none",
103
+ "stroke-width": 2,
104
+ });
105
+ });
106
+
107
+ it("restores the previous style after drawing", () => {
108
+ const graphie = createAndInitGraphie();
109
+
110
+ const mockRaphaelElement1 = createMockRaphaelElement(
111
+ "mockRaphaelElement1",
112
+ );
113
+ const mockRaphaelElement2 = createMockRaphaelElement(
114
+ "mockRaphaelElement2",
115
+ );
116
+ graphie.raphael.ellipse.mockReturnValueOnce(mockRaphaelElement1);
117
+ graphie.raphael.ellipse.mockReturnValueOnce(mockRaphaelElement2);
118
+
119
+ graphie.circle([0, 0], 1, {fill: "#112233", stroke: "#445566"});
120
+ graphie.circle([0, 0], 1);
121
+
122
+ expect(mockRaphaelElement2.attr.mock.calls).toEqual([
123
+ [
124
+ {
125
+ fill: "none",
126
+ "stroke-width": 2,
127
+ },
128
+ ],
129
+ ]);
130
+ });
131
+
132
+ it("dasherizes Raphael attribute names (e.g. strokeWidth -> stroke-width)", () => {
133
+ const graphie = createAndInitGraphie();
134
+ const mockRaphaelElement = createMockRaphaelElement();
135
+ graphie.raphael.ellipse.mockReturnValue(mockRaphaelElement);
136
+
137
+ graphie.circle([0, 0], 1, {strokeWidth: 42});
138
+
139
+ expect(mockRaphaelElement.attr).toHaveBeenCalledWith(
140
+ expect.objectContaining({
141
+ "stroke-width": 42,
142
+ }),
143
+ );
144
+ });
145
+
146
+ it("returns the Raphael element", () => {
147
+ const graphie = createAndInitGraphie();
148
+ const mockRaphaelElement = createMockRaphaelElement();
149
+ graphie.raphael.ellipse.mockReturnValue(mockRaphaelElement);
150
+
151
+ const result = graphie.circle([0, 0], 1);
152
+
153
+ expect(result).toBe(mockRaphaelElement);
154
+ });
155
+ });
156
+
157
+ describe("rect", () => {
158
+ it("uses the given bottom left corner, width, and height", () => {
159
+ const graphie = createAndInitGraphie();
160
+ const mockRaphaelElement = createMockRaphaelElement();
161
+ graphie.raphael.rect.mockReturnValue(mockRaphaelElement);
162
+
163
+ graphie.rect(1, 2, 3, 4);
164
+
165
+ // Raphael.rect() takes the *top*-left corner of the rectangle in
166
+ // pixel coordinates.
167
+ expect(graphie.raphael.rect).toHaveBeenCalledWith(5, 20, 15, 20);
168
+ });
169
+
170
+ it("uses the style, if given", () => {
171
+ const graphie = createAndInitGraphie();
172
+ const mockRaphaelElement = createMockRaphaelElement();
173
+ graphie.raphael.rect.mockReturnValue(mockRaphaelElement);
174
+
175
+ graphie.rect(0, 0, 1, 1, {fill: "#112233", stroke: "#445566"});
176
+
177
+ expect(mockRaphaelElement.attr).toHaveBeenCalledWith(
178
+ expect.objectContaining({
179
+ fill: "#112233",
180
+ stroke: "#445566",
181
+ }),
182
+ );
183
+ });
184
+
185
+ it("uses the default style, if none given", () => {
186
+ const graphie = createAndInitGraphie();
187
+ const mockRaphaelElement = createMockRaphaelElement();
188
+ graphie.raphael.rect.mockReturnValue(mockRaphaelElement);
189
+
190
+ graphie.rect(0, 0, 1, 1);
191
+
192
+ expect(mockRaphaelElement.attr).toHaveBeenCalledWith({
193
+ fill: "none",
194
+ "stroke-width": 2,
195
+ });
196
+ });
197
+
198
+ it("restores the previous style after drawing", () => {
199
+ const graphie = createAndInitGraphie();
200
+ const el1 = createMockRaphaelElement("el1");
201
+ const el2 = createMockRaphaelElement("el2");
202
+ graphie.raphael.rect.mockReturnValueOnce(el1);
203
+ graphie.raphael.rect.mockReturnValueOnce(el2);
204
+
205
+ graphie.rect(0, 0, 1, 1, {fill: "#123456"});
206
+ graphie.rect(0, 0, 1, 1);
207
+
208
+ expect(el2.attr).toHaveBeenCalledWith({
209
+ fill: "none",
210
+ "stroke-width": 2,
211
+ });
212
+ });
213
+
214
+ it("dasherizes Raphael attribute names (e.g. strokeWidth -> stroke-width)", () => {
215
+ const graphie = createAndInitGraphie();
216
+ const mockRaphaelElement = createMockRaphaelElement();
217
+ graphie.raphael.rect.mockReturnValueOnce(mockRaphaelElement);
218
+
219
+ graphie.rect(0, 0, 1, 1, {strokeWidth: 42});
220
+
221
+ expect(mockRaphaelElement.attr).toHaveBeenCalledWith(
222
+ expect.objectContaining({
223
+ "stroke-width": 42,
224
+ }),
225
+ );
226
+ });
227
+
228
+ it("returns the Raphael element", () => {
229
+ const graphie = createAndInitGraphie();
230
+ const mockRaphaelElement = createMockRaphaelElement();
231
+ graphie.raphael.rect.mockReturnValueOnce(mockRaphaelElement);
232
+
233
+ const result = graphie.rect(0, 0, 1, 1);
234
+
235
+ expect(result).toBe(mockRaphaelElement);
236
+ });
237
+ });
238
+
239
+ describe("ellipse", () => {
240
+ it("uses the given center and radii", () => {
241
+ const graphie = createAndInitGraphie();
242
+ const mockRaphaelElement = createMockRaphaelElement();
243
+ graphie.raphael.ellipse.mockReturnValue(mockRaphaelElement);
244
+
245
+ graphie.ellipse([1, 2], [3, 4]);
246
+
247
+ expect(graphie.raphael.ellipse).toHaveBeenCalledWith(5, 40, 15, 20);
248
+ });
249
+
250
+ it("uses the style, if given", () => {
251
+ const graphie = createAndInitGraphie();
252
+ const mockRaphaelElement = createMockRaphaelElement();
253
+ graphie.raphael.ellipse.mockReturnValue(mockRaphaelElement);
254
+
255
+ graphie.ellipse([0, 0], [1, 1], {fill: "#123456"});
256
+
257
+ expect(mockRaphaelElement.attr).toHaveBeenCalledWith(
258
+ expect.objectContaining({
259
+ fill: "#123456",
260
+ }),
261
+ );
262
+ });
263
+
264
+ it("uses the default style, if none given", () => {
265
+ const graphie = createAndInitGraphie();
266
+ const mockRaphaelElement = createMockRaphaelElement();
267
+ graphie.raphael.ellipse.mockReturnValue(mockRaphaelElement);
268
+
269
+ graphie.ellipse([0, 0], [1, 1]);
270
+
271
+ expect(mockRaphaelElement.attr).toHaveBeenCalledWith({
272
+ fill: "none",
273
+ "stroke-width": 2,
274
+ });
275
+ });
276
+
277
+ it("restores the previous style after drawing", () => {
278
+ const graphie = createAndInitGraphie();
279
+ const el1 = createMockRaphaelElement("el1");
280
+ const el2 = createMockRaphaelElement("el2");
281
+ graphie.raphael.ellipse.mockReturnValueOnce(el1);
282
+ graphie.raphael.ellipse.mockReturnValueOnce(el2);
283
+
284
+ graphie.ellipse([0, 0], [1, 1], {fill: "#123456"});
285
+ graphie.ellipse([0, 0], [1, 1]);
286
+
287
+ expect(el2.attr).toHaveBeenCalledWith({
288
+ fill: "none",
289
+ "stroke-width": 2,
290
+ });
291
+ });
292
+
293
+ it("dasherizes Raphael attribute names (e.g. strokeWidth -> stroke-width)", () => {
294
+ const graphie = createAndInitGraphie();
295
+ const mockRaphaelElement = createMockRaphaelElement();
296
+ graphie.raphael.ellipse.mockReturnValue(mockRaphaelElement);
297
+
298
+ graphie.ellipse([0, 0], [1, 1], {strokeWidth: 42});
299
+
300
+ expect(mockRaphaelElement.attr).toHaveBeenCalledWith(
301
+ expect.objectContaining({
302
+ "stroke-width": 42,
303
+ }),
304
+ );
305
+ });
306
+
307
+ it("returns the Raphael element", () => {
308
+ const graphie = createAndInitGraphie();
309
+ const mockRaphaelElement = createMockRaphaelElement();
310
+ graphie.raphael.ellipse.mockReturnValue(mockRaphaelElement);
311
+
312
+ const result = graphie.ellipse([0, 0], [1, 1]);
313
+
314
+ expect(result).toBe(mockRaphaelElement);
315
+ });
316
+ });
317
+
318
+ describe("fixedEllipse", () => {
319
+ it("uses the given center and radii to position and size the wrapper element", () => {
320
+ const graphie = createAndInitGraphie();
321
+
322
+ const {wrapper, visibleShape} = graphie.fixedEllipse(
323
+ [1, 2],
324
+ [3, 4],
325
+ 1,
326
+ 0,
327
+ );
328
+
329
+ expect(wrapper.style.width).toBe("30px");
330
+ expect(wrapper.style.height).toBe("40px");
331
+ expect(wrapper.style.top).toBe("20px");
332
+ expect(wrapper.style.left).toBe("-10px");
333
+ expect(visibleShape.attrs).toEqual(
334
+ expect.objectContaining({
335
+ cx: 15,
336
+ cy: 20,
337
+ rx: 15,
338
+ ry: 20,
339
+ }),
340
+ );
341
+ expect(visibleShape.type).toBe("ellipse");
342
+ });
343
+ });
344
+
345
+ describe("arc", () => {
346
+ it("uses the given center, radius, and angles", () => {
347
+ const graphie = createAndInitGraphie();
348
+ const mockRaphaelElement = createMockRaphaelElement();
349
+ graphie.raphael.path.mockReturnValue(mockRaphaelElement);
350
+
351
+ graphie.arc([0, 0], [1, 1], 0, 90, false);
352
+
353
+ expect(graphie.raphael.path).toHaveBeenCalledWith(
354
+ "M5 50A5 5 0 0 0 0 45",
355
+ );
356
+ });
357
+
358
+ it("draws an arc larger than 180 degrees", () => {
359
+ const graphie = createAndInitGraphie();
360
+ const mockRaphaelElement = createMockRaphaelElement();
361
+ graphie.raphael.path.mockReturnValue(mockRaphaelElement);
362
+
363
+ graphie.arc([0, 0], [1, 1], 0, 270, false);
364
+
365
+ expect(graphie.raphael.path).toHaveBeenCalledWith(
366
+ "M5 50A5 5 0 1 0 0 55",
367
+ );
368
+ });
369
+
370
+ it("uses the style, if given", () => {
371
+ const graphie = createAndInitGraphie();
372
+ const mockRaphaelElement = createMockRaphaelElement();
373
+ graphie.raphael.path.mockReturnValue(mockRaphaelElement);
374
+
375
+ graphie.arc([0, 0], [1, 1], 0, 90, false, {
376
+ fill: "#112233",
377
+ stroke: "#445566",
378
+ });
379
+
380
+ expect(mockRaphaelElement.attr).toHaveBeenCalledWith(
381
+ expect.objectContaining({
382
+ fill: "#112233",
383
+ stroke: "#445566",
384
+ }),
385
+ );
386
+ });
387
+
388
+ it("uses the default style, if none given", () => {
389
+ const graphie = createAndInitGraphie();
390
+ const mockRaphaelElement = createMockRaphaelElement();
391
+ graphie.raphael.path.mockReturnValue(mockRaphaelElement);
392
+
393
+ graphie.arc([0, 0], [1, 1], 0, 90, false);
394
+
395
+ expect(mockRaphaelElement.attr).toHaveBeenCalledWith({
396
+ fill: "none",
397
+ "stroke-width": 2,
398
+ });
399
+ });
400
+
401
+ it("restores the previous style after drawing", () => {
402
+ const graphie = createAndInitGraphie();
403
+ const el1 = createMockRaphaelElement("el1");
404
+ const el2 = createMockRaphaelElement("el2");
405
+ graphie.raphael.path.mockReturnValue(el1);
406
+ graphie.raphael.path.mockReturnValue(el2);
407
+
408
+ graphie.arc([0, 0], [1, 1], 0, 90, false, {fill: "#112233"});
409
+ graphie.arc([0, 0], [1, 1], 0, 90, false);
410
+
411
+ expect(el2.attr).toHaveBeenCalledWith({
412
+ fill: "none",
413
+ "stroke-width": 2,
414
+ });
415
+ });
416
+
417
+ it("dasherizes Raphael attribute names (e.g. strokeWidth -> stroke-width)", () => {
418
+ const graphie = createAndInitGraphie();
419
+ const mockRaphaelElement = createMockRaphaelElement();
420
+ graphie.raphael.path.mockReturnValue(mockRaphaelElement);
421
+
422
+ graphie.arc([0, 0], [1, 1], 0, 90, false, {strokeWidth: 42});
423
+
424
+ expect(mockRaphaelElement.attr).toHaveBeenCalledWith(
425
+ expect.objectContaining({
426
+ "stroke-width": 42,
427
+ }),
428
+ );
429
+ });
430
+
431
+ it("returns the Raphael element", () => {
432
+ const graphie = createAndInitGraphie();
433
+ const mockRaphaelElement = createMockRaphaelElement();
434
+ graphie.raphael.path.mockReturnValue(mockRaphaelElement);
435
+
436
+ const result = graphie.arc([0, 0], [1, 1], 0, 90, false);
437
+
438
+ expect(result).toBe(mockRaphaelElement);
439
+ });
440
+ });
441
+
442
+ describe("path", () => {
443
+ it("draws an SVG path connecting the given points", () => {
444
+ const graphie = createAndInitGraphie();
445
+ const mockRaphaelElement = createMockRaphaelElement();
446
+ graphie.raphael.path.mockReturnValue(mockRaphaelElement);
447
+
448
+ graphie.path([
449
+ [0, 0],
450
+ [1, 1],
451
+ ]);
452
+
453
+ expect(graphie.raphael.path).toHaveBeenCalledWith("M0 50L5 45");
454
+ });
455
+
456
+ it("uses the style, if given", () => {
457
+ const graphie = createAndInitGraphie();
458
+ const mockRaphaelElement = createMockRaphaelElement();
459
+ graphie.raphael.path.mockReturnValue(mockRaphaelElement);
460
+
461
+ graphie.path(
462
+ [
463
+ [0, 0],
464
+ [1, 1],
465
+ ],
466
+ {fill: "#112233"},
467
+ );
468
+
469
+ expect(mockRaphaelElement.attr).toHaveBeenCalledWith(
470
+ expect.objectContaining({
471
+ fill: "#112233",
472
+ }),
473
+ );
474
+ });
475
+
476
+ it("uses the default style, if none given", () => {
477
+ const graphie = createAndInitGraphie();
478
+ const mockRaphaelElement = createMockRaphaelElement();
479
+ graphie.raphael.path.mockReturnValue(mockRaphaelElement);
480
+
481
+ graphie.path([
482
+ [0, 0],
483
+ [1, 1],
484
+ ]);
485
+
486
+ expect(mockRaphaelElement.attr).toHaveBeenCalledWith({
487
+ fill: "none",
488
+ "stroke-width": 2,
489
+ });
490
+ });
491
+
492
+ it("restores the previous style after drawing", () => {
493
+ const graphie = createAndInitGraphie();
494
+ const el1 = createMockRaphaelElement("el1");
495
+ const el2 = createMockRaphaelElement("el2");
496
+ graphie.raphael.path.mockReturnValueOnce(el1);
497
+ graphie.raphael.path.mockReturnValueOnce(el2);
498
+
499
+ graphie.path(
500
+ [
501
+ [0, 0],
502
+ [1, 1],
503
+ ],
504
+ {fill: "#999999"},
505
+ );
506
+ graphie.path([
507
+ [0, 0],
508
+ [1, 1],
509
+ ]);
510
+
511
+ expect(el2.attr).toHaveBeenCalledWith({
512
+ fill: "none",
513
+ "stroke-width": 2,
514
+ });
515
+ });
516
+
517
+ it("dasherizes Raphael attribute names (e.g. strokeWidth -> stroke-width)", () => {
518
+ const graphie = createAndInitGraphie();
519
+ const mockRaphaelElement = createMockRaphaelElement();
520
+ graphie.raphael.path.mockReturnValue(mockRaphaelElement);
521
+
522
+ graphie.path(
523
+ [
524
+ [0, 0],
525
+ [1, 1],
526
+ ],
527
+ {strokeWidth: 42},
528
+ );
529
+
530
+ expect(mockRaphaelElement.attr).toHaveBeenCalledWith(
531
+ expect.objectContaining({
532
+ "stroke-width": 42,
533
+ }),
534
+ );
535
+ });
536
+
537
+ it("returns the Raphael element", () => {
538
+ const graphie = createAndInitGraphie();
539
+ const mockRaphaelElement = createMockRaphaelElement();
540
+ graphie.raphael.path.mockReturnValue(mockRaphaelElement);
541
+
542
+ const result = graphie.path([
543
+ [0, 0],
544
+ [1, 1],
545
+ ]);
546
+
547
+ expect(result).toBe(mockRaphaelElement);
548
+ });
549
+ });
550
+
551
+ describe("fixedPath", () => {
552
+ it("creates a container element the size of the path, with 2px padding on each side", () => {
553
+ const graphie = createAndInitGraphie();
554
+
555
+ const {wrapper, visibleShape} = graphie.fixedPath(
556
+ [
557
+ [0, 0],
558
+ [1, 1],
559
+ ],
560
+ [0, 0],
561
+ (points) => "M" + points.map(([x, y]) => `${x},${y}`).join("L"),
562
+ );
563
+
564
+ expect(wrapper.style.position).toBe("absolute");
565
+ expect(wrapper.style.top).toBe("43px");
566
+ expect(wrapper.style.left).toBe("-2px");
567
+ expect(wrapper.style.height).toBe("9px");
568
+ expect(wrapper.style.width).toBe("9px");
569
+ expect(visibleShape[0].getAttribute("d")).toBe("M2,7L7,2");
570
+ });
571
+ });
572
+
573
+ describe("scaledPath", () => {
574
+ it("creates a path from points specified *in pixel coordinates*", () => {
575
+ const graphie = createAndInitGraphie();
576
+ graphie.raphael.path.mockReturnValue(createMockRaphaelElement());
577
+
578
+ graphie.scaledPath([
579
+ [3, 42],
580
+ [5, 7],
581
+ ]);
582
+
583
+ expect(graphie.raphael.path).toHaveBeenCalledWith("M3 42L5 7");
584
+ });
585
+
586
+ it("uses the style, if given", () => {
587
+ const graphie = createAndInitGraphie();
588
+ const mockRaphaelElement = createMockRaphaelElement();
589
+ graphie.raphael.path.mockReturnValue(mockRaphaelElement);
590
+
591
+ graphie.scaledPath(
592
+ [
593
+ [1, 1],
594
+ [2, 2],
595
+ ],
596
+ {stroke: "#112233"},
597
+ );
598
+
599
+ expect(mockRaphaelElement.attr).toHaveBeenCalledWith(
600
+ expect.objectContaining({stroke: "#112233"}),
601
+ );
602
+ });
603
+
604
+ it("uses the default style, if none given", () => {
605
+ const graphie = createAndInitGraphie();
606
+ const mockRaphaelElement = createMockRaphaelElement();
607
+ graphie.raphael.path.mockReturnValue(mockRaphaelElement);
608
+
609
+ graphie.scaledPath([
610
+ [1, 1],
611
+ [2, 2],
612
+ ]);
613
+
614
+ expect(mockRaphaelElement.attr).toHaveBeenCalledWith({
615
+ fill: "none",
616
+ "stroke-width": 2,
617
+ });
618
+ });
619
+
620
+ it("restores the previous style after drawing", () => {
621
+ const graphie = createAndInitGraphie();
622
+ const el1 = createMockRaphaelElement("el1");
623
+ const el2 = createMockRaphaelElement("el2");
624
+ graphie.raphael.path.mockReturnValueOnce(el1);
625
+ graphie.raphael.path.mockReturnValueOnce(el2);
626
+
627
+ graphie.scaledPath(
628
+ [
629
+ [1, 1],
630
+ [2, 2],
631
+ ],
632
+ {fill: "#112233"},
633
+ );
634
+ graphie.scaledPath([
635
+ [1, 1],
636
+ [2, 2],
637
+ ]);
638
+
639
+ expect(el2.attr).toHaveBeenCalledWith({
640
+ fill: "none",
641
+ "stroke-width": 2,
642
+ });
643
+ });
644
+
645
+ it("dasherizes Raphael attribute names (e.g. strokeWidth -> stroke-width)", () => {
646
+ const graphie = createAndInitGraphie();
647
+ const mockRaphaelElement = createMockRaphaelElement();
648
+ graphie.raphael.path.mockReturnValue(mockRaphaelElement);
649
+
650
+ graphie.scaledPath(
651
+ [
652
+ [1, 1],
653
+ [2, 2],
654
+ ],
655
+ {strokeWidth: 42},
656
+ );
657
+
658
+ expect(mockRaphaelElement.attr).toHaveBeenCalledWith(
659
+ expect.objectContaining({
660
+ "stroke-width": 42,
661
+ }),
662
+ );
663
+ });
664
+
665
+ it("returns the Raphael element", () => {
666
+ const graphie = createAndInitGraphie();
667
+ const mockRaphaelElement = createMockRaphaelElement();
668
+ graphie.raphael.path.mockReturnValue(mockRaphaelElement);
669
+
670
+ const result = graphie.scaledPath([
671
+ [1, 1],
672
+ [2, 2],
673
+ ]);
674
+
675
+ expect(result).toBe(mockRaphaelElement);
676
+ });
677
+ });
678
+
679
+ describe("line", () => {
680
+ it("draws a path between two points", () => {
681
+ const graphie = createAndInitGraphie();
682
+ const mockRaphaelElement = createMockRaphaelElement();
683
+ graphie.raphael.path.mockReturnValue(mockRaphaelElement);
684
+
685
+ graphie.line([0, 0], [1, 1]);
686
+
687
+ expect(graphie.raphael.path).toHaveBeenCalledWith("M0 50L5 45");
688
+ });
689
+
690
+ it("uses the style, if given", () => {
691
+ const graphie = createAndInitGraphie();
692
+ const mockRaphaelElement = createMockRaphaelElement();
693
+ graphie.raphael.path.mockReturnValue(mockRaphaelElement);
694
+
695
+ graphie.line([0, 0], [1, 1], {stroke: "#112233"});
696
+
697
+ expect(mockRaphaelElement.attr).toHaveBeenCalledWith(
698
+ expect.objectContaining({
699
+ stroke: "#112233",
700
+ }),
701
+ );
702
+ });
703
+
704
+ it("uses the default style, if none given", () => {
705
+ const graphie = createAndInitGraphie();
706
+ const mockRaphaelElement = createMockRaphaelElement();
707
+ graphie.raphael.path.mockReturnValue(mockRaphaelElement);
708
+
709
+ graphie.line([0, 0], [1, 1]);
710
+
711
+ expect(mockRaphaelElement.attr).toHaveBeenCalledWith({
712
+ fill: "none",
713
+ "stroke-width": 2,
714
+ });
715
+ });
716
+
717
+ it("restores the previous style after drawing", () => {
718
+ const graphie = createAndInitGraphie();
719
+ const el1 = createMockRaphaelElement("el1");
720
+ const el2 = createMockRaphaelElement("el2");
721
+ graphie.raphael.path.mockReturnValueOnce(el1);
722
+ graphie.raphael.path.mockReturnValueOnce(el2);
723
+
724
+ graphie.line([0, 0], [1, 1], {stroke: "#112233"});
725
+ graphie.line([0, 0], [1, 1]);
726
+
727
+ expect(el2.attr).toHaveBeenCalledWith({
728
+ fill: "none",
729
+ "stroke-width": 2,
730
+ });
731
+ });
732
+
733
+ it("dasherizes Raphael attribute names (e.g. strokeWidth -> stroke-width)", () => {
734
+ const graphie = createAndInitGraphie();
735
+ const mockRaphaelElement = createMockRaphaelElement();
736
+ graphie.raphael.path.mockReturnValue(mockRaphaelElement);
737
+
738
+ graphie.line([0, 0], [1, 1], {strokeWidth: 42});
739
+
740
+ expect(mockRaphaelElement.attr).toHaveBeenCalledWith(
741
+ expect.objectContaining({
742
+ "stroke-width": 42,
743
+ }),
744
+ );
745
+ });
746
+
747
+ it("returns the Raphael element", () => {
748
+ const graphie = createAndInitGraphie();
749
+ const mockRaphaelElement = createMockRaphaelElement();
750
+ graphie.raphael.path.mockReturnValue(mockRaphaelElement);
751
+
752
+ const result = graphie.line([0, 0], [1, 1]);
753
+
754
+ expect(result).toBe(mockRaphaelElement);
755
+ });
756
+ });
757
+
758
+ describe("parabola", () => {
759
+ it("draws a path using the quadratic 'Q' SVG command", () => {
760
+ const graphie = createAndInitGraphie();
761
+ const mockRaphaelElement = createMockRaphaelElement();
762
+ graphie.raphael.path.mockReturnValue(mockRaphaelElement);
763
+
764
+ graphie.parabola(1, 2, 3);
765
+
766
+ expect(graphie.raphael.path.mock.calls[0]).toMatchInlineSnapshot(`
767
+ [
768
+ "M-60,-565 Q-5,645 50,-565",
769
+ ]
770
+ `);
771
+ });
772
+
773
+ it("uses the style, if given", () => {
774
+ const graphie = createAndInitGraphie();
775
+ const mockRaphaelElement = createMockRaphaelElement();
776
+ graphie.raphael.path.mockReturnValue(mockRaphaelElement);
777
+
778
+ graphie.parabola(1, 2, 3, {stroke: "#112233"});
779
+
780
+ expect(mockRaphaelElement.attr).toHaveBeenCalledWith(
781
+ expect.objectContaining({stroke: "#112233"}),
782
+ );
783
+ });
784
+
785
+ it("uses the default style, if none given", () => {
786
+ const graphie = createAndInitGraphie();
787
+ const mockRaphaelElement = createMockRaphaelElement();
788
+ graphie.raphael.path.mockReturnValue(mockRaphaelElement);
789
+
790
+ graphie.parabola(1, 2, 3);
791
+
792
+ expect(mockRaphaelElement.attr).toHaveBeenCalledWith({
793
+ fill: "none",
794
+ "stroke-width": 2,
795
+ });
796
+ });
797
+
798
+ it("restores the previous style after drawing", () => {
799
+ const graphie = createAndInitGraphie();
800
+ const el1 = createMockRaphaelElement("el1");
801
+ const el2 = createMockRaphaelElement("el2");
802
+ graphie.raphael.path.mockReturnValueOnce(el1);
803
+ graphie.raphael.path.mockReturnValueOnce(el2);
804
+
805
+ graphie.parabola(1, 2, 3, {stroke: "#112233"});
806
+ graphie.parabola(1, 2, 3);
807
+
808
+ expect(el2.attr).toHaveBeenCalledWith({
809
+ fill: "none",
810
+ "stroke-width": 2,
811
+ });
812
+ });
813
+
814
+ it("dasherizes Raphael attribute names (e.g. strokeWidth -> stroke-width)", () => {
815
+ const graphie = createAndInitGraphie();
816
+ const mockRaphaelElement = createMockRaphaelElement();
817
+ graphie.raphael.path.mockReturnValue(mockRaphaelElement);
818
+
819
+ graphie.parabola(1, 2, 3, {strokeWidth: 42});
820
+
821
+ expect(mockRaphaelElement.attr).toHaveBeenCalledWith(
822
+ expect.objectContaining({
823
+ "stroke-width": 42,
824
+ }),
825
+ );
826
+ });
827
+
828
+ it("returns the Raphael element", () => {
829
+ const graphie = createAndInitGraphie();
830
+ const mockRaphaelElement = createMockRaphaelElement();
831
+ graphie.raphael.path.mockReturnValue(mockRaphaelElement);
832
+
833
+ const result = graphie.parabola(1, 2, 3);
834
+
835
+ expect(result).toBe(mockRaphaelElement);
836
+ });
837
+ });
838
+
839
+ describe("fixedLine", () => {
840
+ it("creates a container element the size of the line", () => {
841
+ const graphie = createAndInitGraphie();
842
+
843
+ const {wrapper, visibleShape} = graphie.fixedLine(
844
+ [0, 0],
845
+ [1, 1],
846
+ 6,
847
+ );
848
+
849
+ expect(wrapper.style.position).toBe("absolute");
850
+ expect(wrapper.style.top).toBe("42px");
851
+ expect(wrapper.style.left).toBe("-3px");
852
+ expect(wrapper.style.height).toBe("11px");
853
+ expect(wrapper.style.width).toBe("11px");
854
+ expect(visibleShape[0].getAttribute("d")).toBe("M3,8L8,3");
855
+ });
856
+ });
857
+
858
+ describe("sinusoid", () => {
859
+ it("draws a path", () => {
860
+ const graphie = createAndInitGraphie();
861
+ const mockRaphaelElement = createMockRaphaelElement();
862
+ graphie.raphael.path.mockReturnValue(mockRaphaelElement);
863
+
864
+ graphie.sinusoid(1, 2, 3, 4);
865
+
866
+ expect(graphie.raphael.path).toHaveBeenCalled();
867
+ });
868
+
869
+ it("uses the style, if given", () => {
870
+ const graphie = createAndInitGraphie();
871
+ const mockRaphaelElement = createMockRaphaelElement();
872
+ graphie.raphael.path.mockReturnValue(mockRaphaelElement);
873
+
874
+ graphie.sinusoid(1, 2, 3, 4, {stroke: "#112233"});
875
+
876
+ expect(mockRaphaelElement.attr).toHaveBeenCalledWith(
877
+ expect.objectContaining({stroke: "#112233"}),
878
+ );
879
+ });
880
+
881
+ it("uses the default style, if none given", () => {
882
+ const graphie = createAndInitGraphie();
883
+ const mockRaphaelElement = createMockRaphaelElement();
884
+ graphie.raphael.path.mockReturnValue(mockRaphaelElement);
885
+
886
+ graphie.sinusoid(1, 2, 3, 4);
887
+
888
+ expect(mockRaphaelElement.attr).toHaveBeenCalledWith({
889
+ fill: "none",
890
+ "stroke-width": 2,
891
+ });
892
+ });
893
+
894
+ it("restores the previous style after drawing", () => {
895
+ const graphie = createAndInitGraphie();
896
+ const el1 = createMockRaphaelElement("el1");
897
+ const el2 = createMockRaphaelElement("el2");
898
+ graphie.raphael.path.mockReturnValueOnce(el1);
899
+ graphie.raphael.path.mockReturnValueOnce(el2);
900
+
901
+ graphie.sinusoid(1, 2, 3, 4, {stroke: "#112233"});
902
+ graphie.sinusoid(1, 2, 3, 4);
903
+
904
+ expect(el2.attr).toHaveBeenCalledWith({
905
+ fill: "none",
906
+ "stroke-width": 2,
907
+ });
908
+ });
909
+
910
+ it("dasherizes Raphael attribute names (e.g. strokeWidth -> stroke-width)", () => {
911
+ const graphie = createAndInitGraphie();
912
+ const mockRaphaelElement = createMockRaphaelElement();
913
+ graphie.raphael.path.mockReturnValue(mockRaphaelElement);
914
+
915
+ graphie.sinusoid(1, 2, 3, 4, {strokeWidth: 42});
916
+
917
+ expect(mockRaphaelElement.attr).toHaveBeenCalledWith(
918
+ expect.objectContaining({
919
+ "stroke-width": 42,
920
+ }),
921
+ );
922
+ });
923
+
924
+ it("returns the Raphael element", () => {
925
+ const graphie = createAndInitGraphie();
926
+ const mockRaphaelElement = createMockRaphaelElement();
927
+ graphie.raphael.path.mockReturnValue(mockRaphaelElement);
928
+
929
+ const result = graphie.sinusoid(1, 2, 3, 4);
930
+
931
+ expect(result).toBe(mockRaphaelElement);
932
+ });
933
+ });
934
+
935
+ describe("grid", () => {
936
+ it("draws each gridline", () => {
937
+ const graphie = createAndInitGraphie();
938
+ const mockRaphaelElement = createMockRaphaelElement();
939
+ graphie.raphael.path.mockReturnValue(mockRaphaelElement);
940
+
941
+ graphie.grid([0, 10], [0, 10]);
942
+
943
+ expect(graphie.raphael.path).toHaveBeenCalledTimes(22);
944
+ });
945
+
946
+ it("spaces out the gridlines by `step` units", () => {
947
+ const graphie = createAndInitGraphie();
948
+ const mockRaphaelElement = createMockRaphaelElement();
949
+ graphie.raphael.path.mockReturnValue(mockRaphaelElement);
950
+
951
+ graphie.grid([0, 10], [0, 10], {step: [2, 2]});
952
+
953
+ expect(graphie.raphael.path).toHaveBeenCalledTimes(12);
954
+ });
955
+
956
+ it("uses the given style", () => {
957
+ const graphie = createAndInitGraphie();
958
+ const mockRaphaelElement = createMockRaphaelElement();
959
+ graphie.raphael.path.mockReturnValue(mockRaphaelElement);
960
+ const fakeSet = createFakeRaphaelSet();
961
+ graphie.raphael.set.mockReturnValue(fakeSet);
962
+
963
+ graphie.grid([0, 10], [0, 10], {stroke: "#112233"});
964
+
965
+ expect(fakeSet.attr).toHaveBeenCalledWith(
966
+ expect.objectContaining({stroke: "#112233"}),
967
+ );
968
+ });
969
+
970
+ it("resets `step` to the default if it is not given", () => {
971
+ const graphie = createAndInitGraphie();
972
+ const mockRaphaelElement = createMockRaphaelElement();
973
+ graphie.raphael.path.mockReturnValue(mockRaphaelElement);
974
+
975
+ graphie.grid([0, 10], [0, 10], {step: [2, 2]});
976
+ expect(graphie.raphael.path).toHaveBeenCalledTimes(12);
977
+ graphie.raphael.path.mockClear();
978
+
979
+ graphie.grid([0, 10], [0, 10]);
980
+ expect(graphie.raphael.path).toHaveBeenCalledTimes(22);
981
+ });
982
+
983
+ it("dasherizes Raphael attribute names (e.g. strokeWidth -> stroke-width)", () => {
984
+ const graphie = createAndInitGraphie();
985
+ const mockRaphaelElement = createMockRaphaelElement();
986
+ graphie.raphael.path.mockReturnValue(mockRaphaelElement);
987
+ const fakeSet = createFakeRaphaelSet();
988
+ graphie.raphael.set.mockReturnValue(fakeSet);
989
+
990
+ graphie.grid([0, 10], [0, 10], {strokeWidth: 42});
991
+
992
+ expect(fakeSet.attr).toHaveBeenCalledWith(
993
+ expect.objectContaining({
994
+ "stroke-width": 42,
995
+ }),
996
+ );
997
+ });
998
+
999
+ it("returns a Raphael `Set` of the elements drawn", () => {
1000
+ const graphie = createAndInitGraphie();
1001
+ const mockRaphaelElement = createMockRaphaelElement();
1002
+ graphie.raphael.path.mockReturnValue(mockRaphaelElement);
1003
+ const fakeSet = createFakeRaphaelSet();
1004
+ graphie.raphael.set.mockReturnValue(fakeSet);
1005
+
1006
+ const result = graphie.grid([0, 10], [0, 10]);
1007
+
1008
+ expect(result).toBe(fakeSet);
1009
+ });
1010
+ });
1011
+
1012
+ describe("label", () => {
1013
+ it("creates a span containing the text", () => {
1014
+ jest.spyOn(Dependencies, "getDependencies").mockReturnValue(
1015
+ testDependencies,
1016
+ );
1017
+ const graphie = createAndInitGraphie();
1018
+
1019
+ const $span = graphie.label(
1020
+ [0, 0],
1021
+ "this is the text",
1022
+ "center",
1023
+ false,
1024
+ );
1025
+
1026
+ expect($span[0].innerHTML).toBe("this is the text");
1027
+ });
1028
+
1029
+ it("puts the specified amount of padding around the label", () => {
1030
+ jest.spyOn(Dependencies, "getDependencies").mockReturnValue(
1031
+ testDependencies,
1032
+ );
1033
+ const graphie = createAndInitGraphie();
1034
+
1035
+ const $span = graphie.label(
1036
+ [0, 0],
1037
+ "this is the text",
1038
+ "center",
1039
+ false,
1040
+ {labelDistance: 42},
1041
+ );
1042
+
1043
+ expect($span[0].style.padding).toBe("42px");
1044
+ });
1045
+
1046
+ it("defaults the padding to 7px if not specified", () => {
1047
+ jest.spyOn(Dependencies, "getDependencies").mockReturnValue(
1048
+ testDependencies,
1049
+ );
1050
+ const graphie = createAndInitGraphie();
1051
+
1052
+ const $span = graphie.label(
1053
+ [0, 0],
1054
+ "this is the text",
1055
+ "center",
1056
+ false,
1057
+ );
1058
+
1059
+ expect($span[0].style.padding).toBe("7px");
1060
+ });
1061
+
1062
+ it("resets the padding to the default after each call", () => {
1063
+ jest.spyOn(Dependencies, "getDependencies").mockReturnValue(
1064
+ testDependencies,
1065
+ );
1066
+ const graphie = createAndInitGraphie();
1067
+
1068
+ graphie.label([0, 0], "this is the text", "center", false, {
1069
+ labelDistance: 42,
1070
+ });
1071
+ const $span = graphie.label(
1072
+ [0, 0],
1073
+ "this is the text",
1074
+ "center",
1075
+ false,
1076
+ );
1077
+
1078
+ expect($span[0].style.padding).toBe("7px");
1079
+ });
1080
+ });
1081
+
1082
+ describe("plotParametric", () => {
1083
+ it("plots the given parametric curve", () => {
1084
+ const graphie = createAndInitGraphie();
1085
+ graphie.raphael.path.mockReturnValue(createMockRaphaelElement());
1086
+
1087
+ graphie.plotParametric((t) => [t, 0], [0, 1]);
1088
+
1089
+ expect(graphie.raphael.path).toHaveBeenCalledWith(
1090
+ expect.stringMatching(/^M0 5/),
1091
+ );
1092
+ });
1093
+
1094
+ it("uses the style, if given", () => {
1095
+ const graphie = createAndInitGraphie();
1096
+ graphie.raphael.path.mockReturnValue(createMockRaphaelElement());
1097
+ const fakeRaphaelSet = createFakeRaphaelSet();
1098
+ graphie.raphael.set.mockReturnValue(fakeRaphaelSet);
1099
+
1100
+ graphie.plotParametric((t) => [t, 0], [0, 1], {stroke: "#112233"});
1101
+
1102
+ expect(fakeRaphaelSet.attr).toHaveBeenCalledWith(
1103
+ expect.objectContaining({stroke: "#112233"}),
1104
+ );
1105
+ });
1106
+
1107
+ it("uses the default style, if none given", () => {
1108
+ const graphie = createAndInitGraphie();
1109
+ graphie.raphael.path.mockReturnValue(createMockRaphaelElement());
1110
+ const fakeRaphaelSet = createFakeRaphaelSet();
1111
+ graphie.raphael.set.mockReturnValue(fakeRaphaelSet);
1112
+
1113
+ graphie.plotParametric((t) => [t, 0], [0, 1]);
1114
+
1115
+ expect(fakeRaphaelSet.attr).toHaveBeenCalledWith({
1116
+ fill: "none",
1117
+ "stroke-width": 2,
1118
+ });
1119
+ });
1120
+
1121
+ it("restores the previous style after drawing", () => {
1122
+ const graphie = createAndInitGraphie();
1123
+ graphie.raphael.path.mockReturnValue(createMockRaphaelElement());
1124
+ const set1 = createFakeRaphaelSet();
1125
+ const set2 = createFakeRaphaelSet();
1126
+ graphie.raphael.set.mockReturnValue(set1);
1127
+ graphie.raphael.set.mockReturnValue(set2);
1128
+
1129
+ graphie.plotParametric((t) => [t, 0], [0, 1], {stroke: "#112233"});
1130
+ graphie.plotParametric((t) => [t, 0], [0, 1]);
1131
+
1132
+ expect(set2.attr).toHaveBeenCalledWith({
1133
+ fill: "none",
1134
+ "stroke-width": 2,
1135
+ });
1136
+ });
1137
+
1138
+ it("dasherizes Raphael attribute names (e.g. strokeWidth -> stroke-width)", () => {
1139
+ const graphie = createAndInitGraphie();
1140
+ graphie.raphael.path.mockReturnValue(createMockRaphaelElement());
1141
+ const fakeRaphaelSet = createFakeRaphaelSet();
1142
+ graphie.raphael.set.mockReturnValue(fakeRaphaelSet);
1143
+
1144
+ graphie.plotParametric((t) => [t, 0], [0, 1], {strokeWidth: 42});
1145
+
1146
+ expect(fakeRaphaelSet.attr).toHaveBeenCalledWith(
1147
+ expect.objectContaining({"stroke-width": 42}),
1148
+ );
1149
+ });
1150
+
1151
+ it("returns the Raphael set", () => {
1152
+ const graphie = createAndInitGraphie();
1153
+ graphie.raphael.path.mockReturnValue(createMockRaphaelElement());
1154
+ const fakeRaphaelSet = createFakeRaphaelSet();
1155
+ graphie.raphael.set.mockReturnValue(fakeRaphaelSet);
1156
+
1157
+ const result = graphie.plotParametric((t) => [t, 0], [0, 1]);
1158
+
1159
+ expect(result).toBe(fakeRaphaelSet);
1160
+ });
1161
+ });
1162
+
1163
+ describe("plot", () => {
1164
+ it("plots the given function", () => {
1165
+ const graphie = createAndInitGraphie();
1166
+ graphie.raphael.path.mockReturnValue(createMockRaphaelElement());
1167
+
1168
+ graphie.plot((x) => 1, [0, 1]);
1169
+
1170
+ expect(graphie.raphael.path).toHaveBeenCalledWith(
1171
+ expect.stringMatching(/^M0 45/),
1172
+ );
1173
+ });
1174
+
1175
+ it("uses the style, if given", () => {
1176
+ const graphie = createAndInitGraphie();
1177
+ graphie.raphael.path.mockReturnValue(createMockRaphaelElement());
1178
+ const fakeRaphaelSet = createFakeRaphaelSet();
1179
+ graphie.raphael.set.mockReturnValue(fakeRaphaelSet);
1180
+
1181
+ graphie.plot((x) => 1, [0, 1], {stroke: "#112233"});
1182
+
1183
+ expect(fakeRaphaelSet.attr).toHaveBeenCalledWith(
1184
+ expect.objectContaining({stroke: "#112233"}),
1185
+ );
1186
+ });
1187
+
1188
+ it("uses the default style, if none given", () => {
1189
+ const graphie = createAndInitGraphie();
1190
+ graphie.raphael.path.mockReturnValue(createMockRaphaelElement());
1191
+ const fakeRaphaelSet = createFakeRaphaelSet();
1192
+ graphie.raphael.set.mockReturnValue(fakeRaphaelSet);
1193
+
1194
+ graphie.plot((x) => 1, [0, 1]);
1195
+
1196
+ expect(fakeRaphaelSet.attr).toHaveBeenCalledWith({
1197
+ fill: "none",
1198
+ "plot-points": 10,
1199
+ "stroke-width": 2,
1200
+ });
1201
+ });
1202
+
1203
+ it("restores the previous style after drawing", () => {
1204
+ const graphie = createAndInitGraphie();
1205
+ graphie.raphael.path.mockReturnValue(createMockRaphaelElement());
1206
+ const set1 = createFakeRaphaelSet();
1207
+ const set2 = createFakeRaphaelSet();
1208
+ graphie.raphael.set.mockReturnValue(set1);
1209
+ graphie.raphael.set.mockReturnValue(set2);
1210
+
1211
+ graphie.plot((x) => 1, [0, 1], {stroke: "#112233"});
1212
+ graphie.plot((x) => 1, [0, 1]);
1213
+
1214
+ expect(set2.attr).toHaveBeenCalledWith({
1215
+ fill: "none",
1216
+ "plot-points": 10,
1217
+ "stroke-width": 2,
1218
+ });
1219
+ });
1220
+
1221
+ it("dasherizes Raphael attribute names (e.g. strokeWidth -> stroke-width)", () => {
1222
+ const graphie = createAndInitGraphie();
1223
+ graphie.raphael.path.mockReturnValue(createMockRaphaelElement());
1224
+ const fakeRaphaelSet = createFakeRaphaelSet();
1225
+ graphie.raphael.set.mockReturnValue(fakeRaphaelSet);
1226
+
1227
+ graphie.plot((x) => 1, [0, 1], {strokeWidth: 42});
1228
+
1229
+ expect(fakeRaphaelSet.attr).toHaveBeenCalledWith(
1230
+ expect.objectContaining({"stroke-width": 42}),
1231
+ );
1232
+ });
1233
+
1234
+ it("returns the Raphael set", () => {
1235
+ const graphie = createAndInitGraphie();
1236
+ graphie.raphael.path.mockReturnValue(createMockRaphaelElement());
1237
+ const fakeRaphaelSet = createFakeRaphaelSet();
1238
+ graphie.raphael.set.mockReturnValue(fakeRaphaelSet);
1239
+
1240
+ const result = graphie.plot((x) => 1, [0, 1]);
1241
+
1242
+ expect(result).toBe(fakeRaphaelSet);
1243
+ });
1244
+ });
1245
+ });
1246
+
1247
+ describe("GraphUtils", () => {
1248
+ describe("snapCoord", () => {
1249
+ it("snaps to the nearest integer coordinates", () => {
1250
+ expect(GraphUtils.snapCoord([0.1, 0.9], [1, 1])).toEqual([0, 1]);
1251
+ });
1252
+
1253
+ it("snaps to the nearest half", () => {
1254
+ expect(GraphUtils.snapCoord([0.4, 2.2], [0.5, 0.5])).toEqual([
1255
+ 0.5, 2,
1256
+ ]);
1257
+ });
1258
+ });
1259
+ });
1260
+
1261
+ describe("normalizeRange", () => {
1262
+ it("does nothing to a range specified as [[xMin, xMax], [yMin, yMax]]", () => {
1263
+ expect(
1264
+ normalizeRange([
1265
+ [-1, 2],
1266
+ [-3, 4],
1267
+ ]),
1268
+ ).toEqual([
1269
+ [-1, 2],
1270
+ [-3, 4],
1271
+ ]);
1272
+ });
1273
+
1274
+ it("treats a single number as the magnitude of min and max for x and y", () => {
1275
+ expect(normalizeRange(7)).toEqual([
1276
+ [-7, 7],
1277
+ [-7, 7],
1278
+ ]);
1279
+ });
1280
+
1281
+ it("treats a pair of number as the magnitudes for x and y, respectively", () => {
1282
+ expect(normalizeRange([3, 5])).toEqual([
1283
+ [-3, 3],
1284
+ [-5, 5],
1285
+ ]);
1286
+ });
1287
+
1288
+ it("passes undefined through", () => {
1289
+ expect(normalizeRange(undefined)).toBe(undefined);
1290
+ });
1291
+ });