@khanacademy/perseus 19.1.4 → 19.1.5

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.
@@ -14,7 +14,7 @@ declare const InteractiveUtil: {
14
14
  /**
15
15
  * Turn a function or an array of functions into an array of functions
16
16
  */
17
- readonly arrayify: (funcOrArray: readonly unknown[] | ((...args: ReadonlyArray<unknown>) => unknown)) => ReadonlyArray<unknown>;
17
+ readonly arrayify: (funcOrArray: ((...args: ReadonlyArray<unknown>) => unknown) | readonly unknown[]) => ReadonlyArray<unknown>;
18
18
  /**
19
19
  * Convert all function-or-array arguments to arrays of functions
20
20
  */
@@ -1,4 +1,5 @@
1
- import type { Coord } from "./types";
1
+ import type { Constraint, Coord } from "./types";
2
+ import type { Interval } from "../util/interval";
2
3
  declare const _default: {
3
4
  add: {
4
5
  readonly constrain: () => void;
@@ -17,9 +18,9 @@ declare const _default: {
17
18
  standard: null;
18
19
  };
19
20
  constraints: {
20
- readonly fixed: () => () => boolean;
21
- readonly snap: (snap: any) => (arg1: Coord) => unknown;
22
- readonly bound: (range: any, snap: any, paddingPx: number) => (arg1: Coord, arg2: Coord, arg3: any) => Coord;
21
+ readonly fixed: () => Constraint;
22
+ readonly snap: (snap?: number | null | undefined) => Constraint;
23
+ readonly bound: (range?: [Interval, Interval] | undefined, snap?: number | Coord | undefined, optionalPaddingPx?: number | undefined) => Constraint;
23
24
  };
24
25
  onMove: {
25
26
  standard: null;
@@ -1,21 +1,98 @@
1
- import type { Coord } from "./types";
1
+ import type { Constraint, ConstraintCallbacks, Coord } from "./types";
2
2
  import type { Graphie } from "../util/graphie";
3
3
  export interface State {
4
4
  added?: boolean;
5
5
  isHovering?: boolean;
6
6
  isMouseOver?: boolean;
7
7
  isDragging?: boolean;
8
- mouseTarget?: unknown | null;
9
- cursor: unknown | null;
8
+ mouseTarget?: {
9
+ toFront(): void;
10
+ toBack(): void;
11
+ remove(): void;
12
+ getMouseTarget(): Element;
13
+ } | null;
14
+ cursor?: null;
10
15
  id: string;
11
- add: (() => void)[];
12
- modify: (() => void)[];
13
- draw: (() => void)[];
14
- remove: (() => void)[];
15
- onMoveStart: ((position: Coord) => void)[];
16
- onMove: ((end: Coord, start: Coord) => void)[];
17
- onMoveEnd: ((end: Coord, start: Coord) => void)[];
18
- onClick: ((position: Coord, start: Coord) => void)[];
16
+ add?: (() => void)[];
17
+ modify?: ((state: State, prevState?: State) => void)[];
18
+ draw?: ((state: State, prevState?: State) => void)[];
19
+ remove?: (() => void)[];
20
+ onMoveStart?: ((position: Coord, positionAgain: Coord) => void)[];
21
+ onMove?: ((end: Coord, start: Coord) => void)[];
22
+ onMoveEnd?: ((end: Coord, start: Coord) => void)[];
23
+ onClick?: ((position: Coord, start: Coord) => void)[];
24
+ constraints?: Constraint[];
25
+ }
26
+ export declare class Movable<Options extends Record<string, any>> {
27
+ graphie: Graphie;
28
+ state: State;
29
+ prevState: State | undefined;
30
+ _listenerMap: Record<string, number>;
31
+ constructor(graphie: Graphie, options: Options);
32
+ modify(options: Options): void;
33
+ _createDefaultState(): {
34
+ cursor: null;
35
+ id: string;
36
+ add: never[];
37
+ modify: never[];
38
+ draw: never[];
39
+ remove: never[];
40
+ onMoveStart: never[];
41
+ onMove: never[];
42
+ onMoveEnd: never[];
43
+ onClick: never[];
44
+ };
45
+ update(options: Options): void;
46
+ cloneState(): State;
47
+ /**
48
+ * Fire an onSomething type event to all functions in listeners
49
+ */
50
+ _fireEvent<F extends (...args: any[]) => any>(listeners: F[] | undefined, ...args: Parameters<F>): void;
51
+ /**
52
+ * Call all draw functions, and update our prevState for the next
53
+ * draw function
54
+ */
55
+ draw(): void;
56
+ /**
57
+ * Simulates a mouse grab event on the movable object.
58
+ */
59
+ grab(coord: Coord): void;
60
+ _applyConstraints(current: Coord, previous: Coord, extraOptions?: ConstraintCallbacks): any;
61
+ toBack(): void;
62
+ toFront(): void;
63
+ /**
64
+ * Add a listener to any event: startMove, constraints, onMove, onMoveEnd,
65
+ * etc. If a listener is already bound to the given eventName and id, then
66
+ * it is overwritten by func.
67
+ *
68
+ * eventName: the string name of the event to listen to. one of:
69
+ * "onMoveStart", "onMove", "onMoveEnd", "draw", "remove"
70
+ *
71
+ * id: a string id that can be used to remove this event at a later time
72
+ * note: adding multiple listeners with the same id is undefined behavior
73
+ *
74
+ * func: the function to call when the event happens, which is called
75
+ * with the event's standard parameters [usually (coord, prevCoord) or
76
+ * (state, prevState)]
77
+ */
78
+ listen(eventName: string, id: string, func: () => unknown): void;
79
+ /**
80
+ * Remove a previously added listener, by the id specified in the
81
+ * corresponding listen() call
82
+ *
83
+ * If the given id has not been registered already, this is a no-op
84
+ */
85
+ unlisten(eventName: string, id: string): void;
86
+ remove(): void;
87
+ cursor(): null | undefined;
88
+ added(): boolean | undefined;
89
+ isHovering(): boolean | undefined;
90
+ isMouseOver(): boolean | undefined;
91
+ isDragging(): boolean | undefined;
92
+ mouseTarget(): {
93
+ toFront(): void;
94
+ toBack(): void;
95
+ remove(): void;
96
+ getMouseTarget(): Element;
97
+ } | null | undefined;
19
98
  }
20
- declare const Movable: (graphie: Graphie, options: any) => void;
21
- export default Movable;
@@ -1,2 +1,8 @@
1
+ import type { Movable } from "./movable";
1
2
  export type Coord = [number, number];
2
3
  export type Line = [Coord, Coord];
4
+ export type Constraint = (this: Movable<any>, current: Coord | boolean, previous: Coord, options: ConstraintCallbacks) => Coord | boolean | undefined;
5
+ export type ConstraintCallbacks = {
6
+ onSkipRemaining(): void;
7
+ onOutOfBounds?: () => void;
8
+ };
@@ -40,6 +40,7 @@ export declare class Graphie {
40
40
  dimensions?: Coord;
41
41
  xpixels?: number;
42
42
  ypixels?: number;
43
+ isDragging?: boolean;
43
44
  constructor(el: Element);
44
45
  init(options: {
45
46
  range: [Interval, Interval];
@@ -102,6 +103,10 @@ export declare class Graphie {
102
103
  unscalePoint: (point: Coord) => any;
103
104
  unscaleVector: (point: Coord) => any;
104
105
  private processAttributes;
106
+ getMouseCoord(event: Readonly<{
107
+ pageX?: number;
108
+ pageY?: number;
109
+ }>): Coord;
105
110
  }
106
111
  type RangeSpecifier = [Interval, Interval] | Coord | number;
107
112
  export declare function normalizeRange(range: RangeSpecifier): [Interval, Interval];
@@ -212,10 +212,10 @@ declare const _default: Readonly<{
212
212
  onFocus: (blurPath: import("../types").FocusPath) => void;
213
213
  times: boolean;
214
214
  value: string;
215
- }, "trackInteraction" | "onChange" | "static" | "analytics" | "problemNum" | "questionCompleted" | "keypadElement" | "alignment" | "buttonsVisible" | "containerSizeClass" | "widgetId" | "findWidgets" | "keypadConfiguration" | "reviewModeRubric" | "isLastUsedWidget"> & {
216
- value?: string | undefined;
215
+ }, "onChange" | "trackInteraction" | "static" | "analytics" | "problemNum" | "questionCompleted" | "keypadElement" | "alignment" | "buttonsVisible" | "containerSizeClass" | "widgetId" | "findWidgets" | "keypadConfiguration" | "reviewModeRubric" | "isLastUsedWidget"> & {
217
216
  onFocus?: ((blurPath: import("../types").FocusPath) => void) | undefined;
218
217
  onBlur?: ((blurPath: import("../types").FocusPath) => void) | undefined;
218
+ value?: string | undefined;
219
219
  linterContext?: import("@khanacademy/perseus-linter").LinterContextProps | undefined;
220
220
  apiOptions?: Readonly<Readonly<{
221
221
  isArticle?: boolean | undefined;
@@ -401,10 +401,10 @@ declare const _default: Readonly<{
401
401
  onFocus: (blurPath: import("../types").FocusPath) => void;
402
402
  times: boolean;
403
403
  value: string;
404
- }, "trackInteraction" | "onChange" | "static" | "analytics" | "problemNum" | "questionCompleted" | "keypadElement" | "alignment" | "buttonsVisible" | "containerSizeClass" | "widgetId" | "findWidgets" | "keypadConfiguration" | "reviewModeRubric" | "isLastUsedWidget"> & {
405
- value?: string | undefined;
404
+ }, "onChange" | "trackInteraction" | "static" | "analytics" | "problemNum" | "questionCompleted" | "keypadElement" | "alignment" | "buttonsVisible" | "containerSizeClass" | "widgetId" | "findWidgets" | "keypadConfiguration" | "reviewModeRubric" | "isLastUsedWidget"> & {
406
405
  onFocus?: ((blurPath: import("../types").FocusPath) => void) | undefined;
407
406
  onBlur?: ((blurPath: import("../types").FocusPath) => void) | undefined;
407
+ value?: string | undefined;
408
408
  linterContext?: import("@khanacademy/perseus-linter").LinterContextProps | undefined;
409
409
  apiOptions?: Readonly<Readonly<{
410
410
  isArticle?: boolean | undefined;
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Core Perseus API (includes renderers and widgets)",
4
4
  "author": "Khan Academy",
5
5
  "license": "MIT",
6
- "version": "19.1.4",
6
+ "version": "19.1.5",
7
7
  "publishConfig": {
8
8
  "access": "public"
9
9
  },
@@ -23,8 +23,8 @@
23
23
  },
24
24
  "dependencies": {
25
25
  "@khanacademy/kas": "^0.3.7",
26
- "@khanacademy/kmath": "^0.1.10",
27
- "@khanacademy/math-input": "^17.0.3",
26
+ "@khanacademy/kmath": "^0.1.11",
27
+ "@khanacademy/math-input": "^17.0.4",
28
28
  "@khanacademy/perseus-core": "1.4.1",
29
29
  "@khanacademy/perseus-linter": "^0.3.10",
30
30
  "@khanacademy/pure-markdown": "^0.2.13",
@@ -62,6 +62,9 @@ type DefaultProps = {
62
62
 
63
63
  interface Movable {
64
64
  remove(): void;
65
+ props: unknown;
66
+ modify(graphie: GraphieDrawingContext): "reordered" | undefined;
67
+ toFront(): void;
65
68
  }
66
69
 
67
70
  class Graphie extends React.Component<Props> {
@@ -231,7 +234,12 @@ class Graphie extends React.Component<Props> {
231
234
  // @ts-expect-error - TS2322 - Type '(children: readonly any[], options: any) => ReactElement<any, string | JSXElementConstructor<any>> | readonly ReactElement<any, string | JSXElementConstructor<any>>[]' is not assignable to type '(children: readonly any[], arg2: any) => ReactElement<any, string | JSXElementConstructor<any>>'.
232
235
  _renderMovables: (
233
236
  children: ReadonlyArray<any>,
234
- arg2: any,
237
+ arg2: {
238
+ nextKey: number;
239
+ graphie: GraphieDrawingContext;
240
+ oldMovables: Record<string, Movable>;
241
+ newMovables: Record<string, Movable>;
242
+ },
235
243
  ) => React.ReactElement = (children, options) => {
236
244
  // Each leaf of `children` is a movable descriptor created by a call to
237
245
  // some `GraphieMovable`, such as `MovablePoint`.
@@ -5,39 +5,51 @@
5
5
  import _ from "underscore";
6
6
 
7
7
  import {Graphie} from "../../util/graphie";
8
- import Movable from "../movable";
8
+ import {Movable} from "../movable";
9
+
10
+ import type {Coord} from "@khanacademy/perseus";
9
11
 
10
12
  const createMock = function (): any {
11
- const movable = new Movable(new Graphie(document.createElement("div")), {
12
- mouseTarget: null,
13
- });
14
- movable.modify = function (options: any) {
15
- Movable.prototype.modify.call(movable, _.omit(options, "mouseTarget"));
16
- };
17
- _.each(
18
- ["onMoveStart", "onMove", "onMoveEnd", "onClick"],
19
- function (eventName) {
20
- movable[eventName] = function (coord: any, prevCoord) {
21
- this._fireEvent(this.state[eventName], coord, prevCoord);
22
- };
23
- },
24
- );
25
- movable.move = function (...args) {
26
- const startPoint = _.first(args);
13
+ return new MovableMock(new Graphie(document.createElement("div")), {});
14
+ };
15
+
16
+ class MovableMock extends Movable<Record<string, never>> {
17
+ modify(options: any) {
18
+ // TODO(benchristel): what is mouseTarget? It seems to be unused.
19
+ super.modify(_.omit(options, "mouseTarget"));
20
+ }
21
+
22
+ move(...args) {
23
+ const [startPoint, ...rest] = args;
27
24
  // TODO(jack): Move these into onMoveStart, onMove, and onMoveEnd
28
- movable.state.isMouseOver = true;
29
- movable.state.isHovering = true;
30
- movable.onMoveStart(startPoint, startPoint);
31
- _.each(_.rest(args), function (point, i) {
32
- movable.state.isDragging = true;
33
- movable.onMove(point, args[i]);
25
+ this.state.isMouseOver = true;
26
+ this.state.isHovering = true;
27
+ this.onMoveStart(startPoint, startPoint);
28
+ rest.forEach((point, i) => {
29
+ this.state.isDragging = true;
30
+ this.onMove(point, args[i]);
34
31
  });
35
- movable.onMoveEnd(_.last(args), startPoint);
36
- movable.state.dragging = false;
37
- movable.state.isMouseOver = false;
38
- movable.state.isHovering = false;
39
- };
40
- return movable;
41
- };
32
+ this.onMoveEnd(_.last(args), startPoint);
33
+ this.state.isDragging = false;
34
+ this.state.isMouseOver = false;
35
+ this.state.isHovering = false;
36
+ }
37
+
38
+ onMoveStart(coord: Coord, prevCoord: Coord) {
39
+ this._fireEvent(this.state.onMoveStart, coord, prevCoord);
40
+ }
41
+
42
+ onMove(coord: Coord, prevCoord: Coord) {
43
+ this._fireEvent(this.state.onMove, coord, prevCoord);
44
+ }
45
+
46
+ onMoveEnd(coord: Coord, prevCoord: Coord) {
47
+ this._fireEvent(this.state.onMoveEnd, coord, prevCoord);
48
+ }
49
+
50
+ onClick(coord: Coord, prevCoord: Coord) {
51
+ this._fireEvent(this.state.onClick, coord, prevCoord);
52
+ }
53
+ }
42
54
 
43
55
  export default createMock;
@@ -7,7 +7,8 @@ import _ from "underscore";
7
7
 
8
8
  import WrappedEllipse from "./wrapped-ellipse";
9
9
 
10
- import type {Coord} from "./types";
10
+ import type {Constraint, ConstraintCallbacks, Coord} from "./types";
11
+ import type {Interval} from "../util/interval";
11
12
 
12
13
  const add = {
13
14
  constrain: function () {
@@ -119,51 +120,62 @@ const remove = {
119
120
  remove.standard = remove.basic;
120
121
 
121
122
  const constraints = {
122
- fixed: function (): () => boolean {
123
+ fixed: function (): Constraint {
123
124
  return function () {
124
125
  return false;
125
126
  };
126
127
  },
127
128
 
128
- snap: function (snap: any): (arg1: Coord) => unknown {
129
- return function (coord: Coord): unknown {
129
+ snap: function (snap?: number | null): Constraint {
130
+ return function (coord: Coord) {
130
131
  if (snap === null) {
131
- // NOTE(kevinb): this should probably return the original point
132
+ // TODO(benchristel), NOTE(kevinb): this should probably return
133
+ // the original point
132
134
  return true;
133
135
  }
134
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
135
- snap = snap || this.graphie.snap;
136
- return kpoint.roundTo(coord, snap);
136
+ // @ts-expect-error - TS2339: Property 'snap' does not exist on type 'Graphie'.
137
+ return kpoint.roundTo(coord, snap || this.graphie.snap);
137
138
  };
138
139
  },
139
140
 
140
141
  bound: function (
141
- range: any,
142
- snap: any,
143
- paddingPx: number,
144
- ): (arg1: Coord, arg2: Coord, arg3: any) => Coord {
145
- if (paddingPx === undefined) {
146
- if (range === undefined) {
147
- paddingPx = 10;
142
+ range?: [Interval, Interval],
143
+ snap?: Coord | number,
144
+ optionalPaddingPx?: number,
145
+ ): Constraint {
146
+ const paddingPx = (() => {
147
+ if (optionalPaddingPx === undefined) {
148
+ if (range === undefined) {
149
+ return 10;
150
+ } else {
151
+ return 0;
152
+ }
148
153
  } else {
149
- paddingPx = 0;
154
+ return optionalPaddingPx;
150
155
  }
151
- }
152
- return function (coord: Coord, prev, options: any) {
153
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
156
+ })();
157
+
158
+ return function (
159
+ coord: Coord,
160
+ prev: Coord,
161
+ options: ConstraintCallbacks,
162
+ ) {
154
163
  const graphie = this.graphie;
155
164
  range = range || graphie.range;
156
165
 
157
166
  if (snap === undefined) {
167
+ // @ts-expect-error - Property 'snap' does not exist on type 'Graphie'.
158
168
  snap = graphie.snap;
159
169
  }
160
170
 
161
171
  let lower = graphie.unscalePoint([
162
172
  paddingPx,
173
+ // @ts-expect-error - 'graphie.ypixels' is possibly 'undefined'.
163
174
  graphie.ypixels - paddingPx,
164
175
  ]);
165
176
 
166
177
  let upper = graphie.unscalePoint([
178
+ // @ts-expect-error - 'graphie.ypixels' is possibly 'undefined'.
167
179
  graphie.xpixels - paddingPx,
168
180
  paddingPx,
169
181
  ]);
@@ -0,0 +1,249 @@
1
+ import {fireEvent} from "@testing-library/react";
2
+
3
+ import {Graphie} from "../util/graphie";
4
+
5
+ import {Movable} from "./movable";
6
+
7
+ // Yay for side-effect imports!
8
+ // eslint-disable-next-line import/no-unassigned-import
9
+ import "../util/interactive";
10
+
11
+ describe("Movable", () => {
12
+ const properties = [
13
+ "graphie",
14
+ "state",
15
+ "prevState",
16
+ "cursor",
17
+ "added",
18
+ "isHovering",
19
+ "isMouseOver",
20
+ "isDragging",
21
+ "mouseTarget",
22
+ "_fireEvent",
23
+ "_applyConstraints",
24
+ "draw",
25
+ "listen",
26
+ "unlisten",
27
+ "cloneState",
28
+ "_createDefaultState",
29
+ "modify",
30
+ "grab",
31
+ "update",
32
+ "remove",
33
+ "toBack",
34
+ "toFront",
35
+ ];
36
+
37
+ it.each(properties)("has a %s property", (property) => {
38
+ const dummyGraphie: any = {};
39
+ const movable = new Movable(dummyGraphie, {});
40
+ expect(movable[property]).toBeDefined();
41
+ });
42
+
43
+ const getters = [
44
+ "cursor",
45
+ "added",
46
+ "isHovering",
47
+ "isMouseOver",
48
+ "isDragging",
49
+ "mouseTarget",
50
+ ];
51
+
52
+ it.each(getters)(
53
+ "returns the corresponding state property from %s()",
54
+ (getter) => {
55
+ const dummyGraphie: any = {};
56
+ const movable = new Movable(dummyGraphie, {});
57
+ expect(movable[getter]()).toBe(movable.state[getter]);
58
+ },
59
+ );
60
+
61
+ it("sets default state when constructed", () => {
62
+ const dummyGraphie: any = {};
63
+ const movable = new Movable(dummyGraphie, {});
64
+ expect(movable.state).toEqual({
65
+ id: expect.any(String),
66
+ add: [],
67
+ added: true,
68
+ cursor: null,
69
+ draw: [],
70
+ isDragging: false,
71
+ isHovering: false,
72
+ isMouseOver: false,
73
+ modify: [],
74
+ mouseTarget: null,
75
+ onClick: [],
76
+ onMove: [],
77
+ onMoveEnd: [],
78
+ onMoveStart: [],
79
+ remove: [],
80
+ });
81
+ });
82
+
83
+ it("calls the modify listener when constructed", () => {
84
+ const dummyGraphie: any = {};
85
+ const modifySpy = jest.fn().mockName("modify");
86
+
87
+ const _ = new Movable(dummyGraphie, {modify: modifySpy});
88
+
89
+ const baseExpectedState = {
90
+ add: [],
91
+ added: false,
92
+ cursor: null,
93
+ draw: [],
94
+ id: expect.any(String),
95
+ isDragging: false,
96
+ isHovering: false,
97
+ isMouseOver: false,
98
+ modify: [modifySpy],
99
+ mouseTarget: null,
100
+ onClick: [],
101
+ onMove: [],
102
+ onMoveEnd: [],
103
+ onMoveStart: [],
104
+ remove: [],
105
+ };
106
+ expect(modifySpy.mock.calls).toEqual([
107
+ [baseExpectedState, {}],
108
+ [
109
+ {...baseExpectedState, added: true},
110
+ {...baseExpectedState, added: true},
111
+ ],
112
+ ]);
113
+ });
114
+
115
+ test("cloneState()", () => {
116
+ const movable = new Movable(
117
+ new Graphie(document.createElement("div")),
118
+ {},
119
+ );
120
+
121
+ expect(movable.cloneState()).toEqual(
122
+ expect.objectContaining({
123
+ id: expect.any(String),
124
+ add: [],
125
+ added: true,
126
+ cursor: null,
127
+ draw: [],
128
+ isDragging: false,
129
+ isHovering: false,
130
+ isMouseOver: false,
131
+ modify: [],
132
+ mouseTarget: null,
133
+ onClick: [],
134
+ onMove: [],
135
+ onMoveEnd: [],
136
+ onMoveStart: [],
137
+ remove: [],
138
+ }),
139
+ );
140
+ });
141
+
142
+ test("cloneState() makes a copy", () => {
143
+ const movable = new Movable(
144
+ new Graphie(document.createElement("div")),
145
+ {},
146
+ );
147
+
148
+ expect(movable.cloneState()).not.toBe(movable.state);
149
+ });
150
+
151
+ test("modify() retains original ID", () => {
152
+ const movable = new Movable(
153
+ new Graphie(document.createElement("div")),
154
+ {},
155
+ );
156
+ const id = movable.state.id;
157
+
158
+ movable.modify({added: true});
159
+
160
+ expect(movable.state.id).toBe(id);
161
+ });
162
+
163
+ test("grab() fires onMoveStart", () => {
164
+ const graphie = new Graphie(document.createElement("div"));
165
+ graphie.init({
166
+ range: [
167
+ [-10, 10],
168
+ [-10, 10],
169
+ ],
170
+ });
171
+ const onMoveStart = jest.fn();
172
+ const movable = new Movable(graphie, {onMoveStart});
173
+
174
+ movable.grab([0, 0]);
175
+
176
+ expect(onMoveStart).toHaveBeenCalledWith([0, 0], [0, 0]);
177
+ });
178
+
179
+ test("grab() fires onMove when mouse moves", () => {
180
+ const graphie = new Graphie(document.createElement("div"));
181
+ graphie.init({
182
+ range: [
183
+ [-10, 10],
184
+ [-10, 10],
185
+ ],
186
+ scale: [10, 10],
187
+ });
188
+ const onMove = jest.fn();
189
+ const movable = new Movable(graphie, {onMove});
190
+
191
+ movable.grab([0, 0]);
192
+ // eslint-disable-next-line testing-library/prefer-user-event
193
+ fireEvent.mouseMove(document, {clientX: 120, clientY: 80});
194
+
195
+ expect(onMove).toHaveBeenCalledWith([2, 2], [0, 0]);
196
+ });
197
+
198
+ test("grab() fires onMoveEnd when mouse up", () => {
199
+ const dummyGraphie: any = {};
200
+ const onMoveEnd = jest.fn();
201
+ const movable = new Movable(dummyGraphie, {onMoveEnd});
202
+
203
+ movable.grab([0, 0]);
204
+ // eslint-disable-next-line testing-library/prefer-user-event
205
+ fireEvent.mouseUp(document);
206
+
207
+ expect(onMoveEnd).toHaveBeenCalledWith([0, 0], [0, 0]);
208
+ });
209
+
210
+ test("grab() fires onClick if still hovering when mouse up", () => {
211
+ const dummyGraphie: any = {};
212
+ const onClick = jest.fn();
213
+ const movable = new Movable(dummyGraphie, {onClick});
214
+
215
+ movable.grab([0, 0]);
216
+ // eslint-disable-next-line testing-library/prefer-user-event
217
+ fireEvent.mouseUp(document);
218
+
219
+ expect(onClick).toHaveBeenCalledWith([0, 0], [0, 0]);
220
+ });
221
+
222
+ test("draw() fires a draw event", () => {
223
+ const dummyGraphie: any = {};
224
+ const drawSpy = jest.fn();
225
+ const movable = new Movable(dummyGraphie, {draw: drawSpy});
226
+
227
+ movable.draw();
228
+
229
+ const expectedState = {
230
+ add: [],
231
+ added: true,
232
+ cursor: null,
233
+ draw: [drawSpy],
234
+ id: expect.any(String),
235
+ isDragging: false,
236
+ isHovering: false,
237
+ isMouseOver: false,
238
+ modify: [],
239
+ mouseTarget: null,
240
+ onClick: [],
241
+ onMove: [],
242
+ onMoveEnd: [],
243
+ onMoveStart: [],
244
+ remove: [],
245
+ };
246
+
247
+ expect(drawSpy).toHaveBeenCalledWith(expectedState, expectedState);
248
+ });
249
+ });