소스 검색

Much more thorough tests! (#1053)

vanilla_orig
Pete Hunt 5 년 전
부모
커밋
bd7856adf3
No account linked to committer's email address

+ 1
- 0
.eslintignore 파일 보기

2
 build/
2
 build/
3
 package-lock.json
3
 package-lock.json
4
 .vscode/
4
 .vscode/
5
+firebase/

+ 1
- 2
src/appState.ts 파일 보기

1
 import { AppState, FlooredNumber } from "./types";
1
 import { AppState, FlooredNumber } from "./types";
2
 import { getDateTime } from "./utils";
2
 import { getDateTime } from "./utils";
3
 
3
 
4
-const DEFAULT_PROJECT_NAME = `excalidraw-${getDateTime()}`;
5
 export const DEFAULT_FONT = "20px Virgil";
4
 export const DEFAULT_FONT = "20px Virgil";
6
 
5
 
7
 export function getDefaultAppState(): AppState {
6
 export function getDefaultAppState(): AppState {
26
     cursorX: 0,
25
     cursorX: 0,
27
     cursorY: 0,
26
     cursorY: 0,
28
     scrolledOutside: false,
27
     scrolledOutside: false,
29
-    name: DEFAULT_PROJECT_NAME,
28
+    name: `excalidraw-${getDateTime()}`,
30
     isCollaborating: false,
29
     isCollaborating: false,
31
     isResizing: false,
30
     isResizing: false,
32
     selectionElement: null,
31
     selectionElement: null,

+ 2
- 2
src/data/restore.ts 파일 보기

4
 import { AppState } from "../types";
4
 import { AppState } from "../types";
5
 import { DataState } from "./types";
5
 import { DataState } from "./types";
6
 import { isInvisiblySmallElement, normalizeDimensions } from "../element";
6
 import { isInvisiblySmallElement, normalizeDimensions } from "../element";
7
-import nanoid from "nanoid";
8
 import { calculateScrollCenter } from "../scene";
7
 import { calculateScrollCenter } from "../scene";
8
+import { randomId } from "../random";
9
 
9
 
10
 export function restore(
10
 export function restore(
11
   // we're making the elements mutable for this API because we want to
11
   // we're making the elements mutable for this API because we want to
62
         ...element,
62
         ...element,
63
         // all elements must have version > 0 so getDrawingVersion() will pick up newly added elements
63
         // all elements must have version > 0 so getDrawingVersion() will pick up newly added elements
64
         version: element.version || 1,
64
         version: element.version || 1,
65
-        id: element.id || nanoid(),
65
+        id: element.id || randomId(),
66
         fillStyle: element.fillStyle || "hachure",
66
         fillStyle: element.fillStyle || "hachure",
67
         strokeWidth: element.strokeWidth || 1,
67
         strokeWidth: element.strokeWidth || 1,
68
         roughness: element.roughness ?? 1,
68
         roughness: element.roughness ?? 1,

+ 3
- 3
src/element/mutateElement.ts 파일 보기

1
 import { ExcalidrawElement } from "./types";
1
 import { ExcalidrawElement } from "./types";
2
-import { randomSeed } from "roughjs/bin/math";
3
 import { invalidateShapeForElement } from "../renderer/renderElement";
2
 import { invalidateShapeForElement } from "../renderer/renderElement";
4
 import { globalSceneState } from "../scene";
3
 import { globalSceneState } from "../scene";
5
 import { getSizeFromPoints } from "../points";
4
 import { getSizeFromPoints } from "../points";
5
+import { randomInteger } from "../random";
6
 
6
 
7
 type ElementUpdate<TElement extends ExcalidrawElement> = Omit<
7
 type ElementUpdate<TElement extends ExcalidrawElement> = Omit<
8
   Partial<TElement>,
8
   Partial<TElement>,
42
   }
42
   }
43
 
43
 
44
   element.version++;
44
   element.version++;
45
-  element.versionNonce = randomSeed();
45
+  element.versionNonce = randomInteger();
46
 
46
 
47
   globalSceneState.informMutation();
47
   globalSceneState.informMutation();
48
 }
48
 }
54
   return {
54
   return {
55
     ...element,
55
     ...element,
56
     version: element.version + 1,
56
     version: element.version + 1,
57
-    versionNonce: randomSeed(),
57
+    versionNonce: randomInteger(),
58
     ...updates,
58
     ...updates,
59
   };
59
   };
60
 }
60
 }

+ 5
- 7
src/element/newElement.ts 파일 보기

1
-import { randomSeed } from "roughjs/bin/math";
2
-import nanoid from "nanoid";
3
-
4
 import {
1
 import {
5
   ExcalidrawElement,
2
   ExcalidrawElement,
6
   ExcalidrawTextElement,
3
   ExcalidrawTextElement,
8
   ExcalidrawGenericElement,
5
   ExcalidrawGenericElement,
9
 } from "../element/types";
6
 } from "../element/types";
10
 import { measureText } from "../utils";
7
 import { measureText } from "../utils";
8
+import { randomInteger, randomId } from "../random";
11
 
9
 
12
 type ElementConstructorOpts = {
10
 type ElementConstructorOpts = {
13
   x: ExcalidrawGenericElement["x"];
11
   x: ExcalidrawGenericElement["x"];
39
   }: ElementConstructorOpts & Partial<ExcalidrawGenericElement>,
37
   }: ElementConstructorOpts & Partial<ExcalidrawGenericElement>,
40
 ) {
38
 ) {
41
   return {
39
   return {
42
-    id: rest.id || nanoid(),
40
+    id: rest.id || randomId(),
43
     type,
41
     type,
44
     x,
42
     x,
45
     y,
43
     y,
51
     strokeWidth,
49
     strokeWidth,
52
     roughness,
50
     roughness,
53
     opacity,
51
     opacity,
54
-    seed: rest.seed ?? randomSeed(),
52
+    seed: rest.seed ?? randomInteger(),
55
     version: rest.version || 1,
53
     version: rest.version || 1,
56
     versionNonce: rest.versionNonce ?? 0,
54
     versionNonce: rest.versionNonce ?? 0,
57
     isDeleted: rest.isDeleted ?? false,
55
     isDeleted: rest.isDeleted ?? false,
145
   overrides?: Partial<TElement>,
143
   overrides?: Partial<TElement>,
146
 ): TElement {
144
 ): TElement {
147
   let copy: TElement = _duplicateElement(element);
145
   let copy: TElement = _duplicateElement(element);
148
-  copy.id = nanoid();
149
-  copy.seed = randomSeed();
146
+  copy.id = randomId();
147
+  copy.seed = randomInteger();
150
   if (overrides) {
148
   if (overrides) {
151
     copy = Object.assign(copy, overrides);
149
     copy = Object.assign(copy, overrides);
152
   }
150
   }

+ 8
- 0
src/history.ts 파일 보기

14
   private stateHistory: string[] = [];
14
   private stateHistory: string[] = [];
15
   private redoStack: string[] = [];
15
   private redoStack: string[] = [];
16
 
16
 
17
+  getSnapshotForTest() {
18
+    return {
19
+      recording: this.recording,
20
+      stateHistory: this.stateHistory.map((s) => JSON.parse(s)),
21
+      redoStack: this.redoStack.map((s) => JSON.parse(s)),
22
+    };
23
+  }
24
+
17
   clear() {
25
   clear() {
18
     this.stateHistory.length = 0;
26
     this.stateHistory.length = 0;
19
     this.redoStack.length = 0;
27
     this.redoStack.length = 0;

+ 2
- 0
src/keys.ts 파일 보기

14
   SPACE: " ",
14
   SPACE: " ",
15
 } as const;
15
 } as const;
16
 
16
 
17
+export type Key = keyof typeof KEYS;
18
+
17
 export function isArrowKey(keyCode: string) {
19
 export function isArrowKey(keyCode: string) {
18
   return (
20
   return (
19
     keyCode === KEYS.ARROW_LEFT ||
21
     keyCode === KEYS.ARROW_LEFT ||

+ 18
- 0
src/random.ts 파일 보기

1
+import { Random } from "roughjs/bin/math";
2
+import nanoid from "nanoid";
3
+
4
+let random = new Random(Date.now());
5
+let testIdBase = 0;
6
+
7
+export function randomInteger() {
8
+  return Math.floor(random.next() * 2 ** 31);
9
+}
10
+
11
+export function reseed(seed: number) {
12
+  random = new Random(seed);
13
+  testIdBase = 0;
14
+}
15
+
16
+export function randomId() {
17
+  return process.env.NODE_ENV === "test" ? `id${testIdBase++}` : nanoid();
18
+}

+ 136
- 0
src/tests/__snapshots__/dragCreate.test.tsx.snap 파일 보기

1
+// Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+exports[`add element to the scene when pointer dragging long enough arrow 1`] = `1`;
4
+
5
+exports[`add element to the scene when pointer dragging long enough arrow 2`] = `
6
+Object {
7
+  "backgroundColor": "transparent",
8
+  "fillStyle": "hachure",
9
+  "height": 50,
10
+  "id": "id0",
11
+  "isDeleted": false,
12
+  "lastCommittedPoint": null,
13
+  "opacity": 100,
14
+  "points": Array [
15
+    Array [
16
+      0,
17
+      0,
18
+    ],
19
+    Array [
20
+      30,
21
+      50,
22
+    ],
23
+  ],
24
+  "roughness": 1,
25
+  "seed": 337897,
26
+  "strokeColor": "#000000",
27
+  "strokeWidth": 1,
28
+  "type": "arrow",
29
+  "version": 3,
30
+  "versionNonce": 449462985,
31
+  "width": 30,
32
+  "x": 30,
33
+  "y": 20,
34
+}
35
+`;
36
+
37
+exports[`add element to the scene when pointer dragging long enough diamond 1`] = `1`;
38
+
39
+exports[`add element to the scene when pointer dragging long enough diamond 2`] = `
40
+Object {
41
+  "backgroundColor": "transparent",
42
+  "fillStyle": "hachure",
43
+  "height": 50,
44
+  "id": "id0",
45
+  "isDeleted": false,
46
+  "opacity": 100,
47
+  "roughness": 1,
48
+  "seed": 337897,
49
+  "strokeColor": "#000000",
50
+  "strokeWidth": 1,
51
+  "type": "diamond",
52
+  "version": 2,
53
+  "versionNonce": 1278240551,
54
+  "width": 30,
55
+  "x": 30,
56
+  "y": 20,
57
+}
58
+`;
59
+
60
+exports[`add element to the scene when pointer dragging long enough ellipse 1`] = `1`;
61
+
62
+exports[`add element to the scene when pointer dragging long enough ellipse 2`] = `
63
+Object {
64
+  "backgroundColor": "transparent",
65
+  "fillStyle": "hachure",
66
+  "height": 50,
67
+  "id": "id0",
68
+  "isDeleted": false,
69
+  "opacity": 100,
70
+  "roughness": 1,
71
+  "seed": 337897,
72
+  "strokeColor": "#000000",
73
+  "strokeWidth": 1,
74
+  "type": "ellipse",
75
+  "version": 2,
76
+  "versionNonce": 1278240551,
77
+  "width": 30,
78
+  "x": 30,
79
+  "y": 20,
80
+}
81
+`;
82
+
83
+exports[`add element to the scene when pointer dragging long enough line 1`] = `
84
+Object {
85
+  "backgroundColor": "transparent",
86
+  "fillStyle": "hachure",
87
+  "height": 50,
88
+  "id": "id0",
89
+  "isDeleted": false,
90
+  "lastCommittedPoint": null,
91
+  "opacity": 100,
92
+  "points": Array [
93
+    Array [
94
+      0,
95
+      0,
96
+    ],
97
+    Array [
98
+      30,
99
+      50,
100
+    ],
101
+  ],
102
+  "roughness": 1,
103
+  "seed": 337897,
104
+  "strokeColor": "#000000",
105
+  "strokeWidth": 1,
106
+  "type": "line",
107
+  "version": 3,
108
+  "versionNonce": 449462985,
109
+  "width": 30,
110
+  "x": 30,
111
+  "y": 20,
112
+}
113
+`;
114
+
115
+exports[`add element to the scene when pointer dragging long enough rectangle 1`] = `1`;
116
+
117
+exports[`add element to the scene when pointer dragging long enough rectangle 2`] = `
118
+Object {
119
+  "backgroundColor": "transparent",
120
+  "fillStyle": "hachure",
121
+  "height": 50,
122
+  "id": "id0",
123
+  "isDeleted": false,
124
+  "opacity": 100,
125
+  "roughness": 1,
126
+  "seed": 337897,
127
+  "strokeColor": "#000000",
128
+  "strokeWidth": 1,
129
+  "type": "rectangle",
130
+  "version": 2,
131
+  "versionNonce": 1278240551,
132
+  "width": 30,
133
+  "x": 30,
134
+  "y": 20,
135
+}
136
+`;

+ 64
- 0
src/tests/__snapshots__/move.test.tsx.snap 파일 보기

1
+// Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+exports[`duplicate element on move when ALT is clicked rectangle 1`] = `
4
+Object {
5
+  "backgroundColor": "transparent",
6
+  "fillStyle": "hachure",
7
+  "height": 50,
8
+  "id": "id1",
9
+  "isDeleted": false,
10
+  "opacity": 100,
11
+  "roughness": 1,
12
+  "seed": 453191,
13
+  "strokeColor": "#000000",
14
+  "strokeWidth": 1,
15
+  "type": "rectangle",
16
+  "version": 2,
17
+  "versionNonce": 1278240551,
18
+  "width": 30,
19
+  "x": 30,
20
+  "y": 20,
21
+}
22
+`;
23
+
24
+exports[`duplicate element on move when ALT is clicked rectangle 2`] = `
25
+Object {
26
+  "backgroundColor": "transparent",
27
+  "fillStyle": "hachure",
28
+  "height": 50,
29
+  "id": "id0",
30
+  "isDeleted": false,
31
+  "opacity": 100,
32
+  "roughness": 1,
33
+  "seed": 337897,
34
+  "strokeColor": "#000000",
35
+  "strokeWidth": 1,
36
+  "type": "rectangle",
37
+  "version": 3,
38
+  "versionNonce": 2019559783,
39
+  "width": 30,
40
+  "x": 0,
41
+  "y": 40,
42
+}
43
+`;
44
+
45
+exports[`move element rectangle 1`] = `
46
+Object {
47
+  "backgroundColor": "transparent",
48
+  "fillStyle": "hachure",
49
+  "height": 50,
50
+  "id": "id0",
51
+  "isDeleted": false,
52
+  "opacity": 100,
53
+  "roughness": 1,
54
+  "seed": 337897,
55
+  "strokeColor": "#000000",
56
+  "strokeWidth": 1,
57
+  "type": "rectangle",
58
+  "version": 3,
59
+  "versionNonce": 401146281,
60
+  "width": 30,
61
+  "x": 0,
62
+  "y": 40,
63
+}
64
+`;

+ 79
- 0
src/tests/__snapshots__/multiPointCreate.test.tsx.snap 파일 보기

1
+// Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+exports[`multi point mode in linear elements arrow 1`] = `
4
+Object {
5
+  "backgroundColor": "transparent",
6
+  "fillStyle": "hachure",
7
+  "height": 110,
8
+  "id": "id0",
9
+  "isDeleted": false,
10
+  "lastCommittedPoint": Array [
11
+    70,
12
+    110,
13
+  ],
14
+  "opacity": 100,
15
+  "points": Array [
16
+    Array [
17
+      0,
18
+      0,
19
+    ],
20
+    Array [
21
+      20,
22
+      30,
23
+    ],
24
+    Array [
25
+      70,
26
+      110,
27
+    ],
28
+  ],
29
+  "roughness": 1,
30
+  "seed": 337897,
31
+  "strokeColor": "#000000",
32
+  "strokeWidth": 1,
33
+  "type": "arrow",
34
+  "version": 7,
35
+  "versionNonce": 1116226695,
36
+  "width": 70,
37
+  "x": 30,
38
+  "y": 30,
39
+}
40
+`;
41
+
42
+exports[`multi point mode in linear elements line 1`] = `
43
+Object {
44
+  "backgroundColor": "transparent",
45
+  "fillStyle": "hachure",
46
+  "height": 110,
47
+  "id": "id0",
48
+  "isDeleted": false,
49
+  "lastCommittedPoint": Array [
50
+    70,
51
+    110,
52
+  ],
53
+  "opacity": 100,
54
+  "points": Array [
55
+    Array [
56
+      0,
57
+      0,
58
+    ],
59
+    Array [
60
+      20,
61
+      30,
62
+    ],
63
+    Array [
64
+      70,
65
+      110,
66
+    ],
67
+  ],
68
+  "roughness": 1,
69
+  "seed": 337897,
70
+  "strokeColor": "#000000",
71
+  "strokeWidth": 1,
72
+  "type": "line",
73
+  "version": 7,
74
+  "versionNonce": 1116226695,
75
+  "width": 70,
76
+  "x": 30,
77
+  "y": 30,
78
+}
79
+`;

+ 10762
- 0
src/tests/__snapshots__/regressionTests.test.tsx.snap
파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
파일 보기


+ 43
- 0
src/tests/__snapshots__/resize.test.tsx.snap 파일 보기

1
+// Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+exports[`resize element rectangle 1`] = `
4
+Object {
5
+  "backgroundColor": "transparent",
6
+  "fillStyle": "hachure",
7
+  "height": 50,
8
+  "id": "id0",
9
+  "isDeleted": false,
10
+  "opacity": 100,
11
+  "roughness": 1,
12
+  "seed": 337897,
13
+  "strokeColor": "#000000",
14
+  "strokeWidth": 1,
15
+  "type": "rectangle",
16
+  "version": 3,
17
+  "versionNonce": 1150084233,
18
+  "width": 30,
19
+  "x": 29,
20
+  "y": 47,
21
+}
22
+`;
23
+
24
+exports[`resize element with aspect ratio when SHIFT is clicked rectangle 1`] = `
25
+Object {
26
+  "backgroundColor": "transparent",
27
+  "fillStyle": "hachure",
28
+  "height": 50,
29
+  "id": "id0",
30
+  "isDeleted": false,
31
+  "opacity": 100,
32
+  "roughness": 1,
33
+  "seed": 337897,
34
+  "strokeColor": "#000000",
35
+  "strokeWidth": 1,
36
+  "type": "rectangle",
37
+  "version": 3,
38
+  "versionNonce": 1150084233,
39
+  "width": 30,
40
+  "x": 29,
41
+  "y": 47,
42
+}
43
+`;

+ 128
- 0
src/tests/__snapshots__/selection.test.tsx.snap 파일 보기

1
+// Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+exports[`select single element on the scene arrow 1`] = `
4
+Object {
5
+  "backgroundColor": "transparent",
6
+  "fillStyle": "hachure",
7
+  "height": 50,
8
+  "id": "id0",
9
+  "isDeleted": false,
10
+  "lastCommittedPoint": null,
11
+  "opacity": 100,
12
+  "points": Array [
13
+    Array [
14
+      0,
15
+      0,
16
+    ],
17
+    Array [
18
+      30,
19
+      50,
20
+    ],
21
+  ],
22
+  "roughness": 1,
23
+  "seed": 337897,
24
+  "strokeColor": "#000000",
25
+  "strokeWidth": 1,
26
+  "type": "arrow",
27
+  "version": 3,
28
+  "versionNonce": 449462985,
29
+  "width": 30,
30
+  "x": 30,
31
+  "y": 20,
32
+}
33
+`;
34
+
35
+exports[`select single element on the scene arrow escape 1`] = `
36
+Object {
37
+  "backgroundColor": "transparent",
38
+  "fillStyle": "hachure",
39
+  "height": 50,
40
+  "id": "id0",
41
+  "isDeleted": false,
42
+  "lastCommittedPoint": null,
43
+  "opacity": 100,
44
+  "points": Array [
45
+    Array [
46
+      0,
47
+      0,
48
+    ],
49
+    Array [
50
+      30,
51
+      50,
52
+    ],
53
+  ],
54
+  "roughness": 1,
55
+  "seed": 337897,
56
+  "strokeColor": "#000000",
57
+  "strokeWidth": 1,
58
+  "type": "line",
59
+  "version": 3,
60
+  "versionNonce": 449462985,
61
+  "width": 30,
62
+  "x": 30,
63
+  "y": 20,
64
+}
65
+`;
66
+
67
+exports[`select single element on the scene diamond 1`] = `
68
+Object {
69
+  "backgroundColor": "transparent",
70
+  "fillStyle": "hachure",
71
+  "height": 50,
72
+  "id": "id0",
73
+  "isDeleted": false,
74
+  "opacity": 100,
75
+  "roughness": 1,
76
+  "seed": 337897,
77
+  "strokeColor": "#000000",
78
+  "strokeWidth": 1,
79
+  "type": "diamond",
80
+  "version": 2,
81
+  "versionNonce": 1278240551,
82
+  "width": 30,
83
+  "x": 30,
84
+  "y": 20,
85
+}
86
+`;
87
+
88
+exports[`select single element on the scene ellipse 1`] = `
89
+Object {
90
+  "backgroundColor": "transparent",
91
+  "fillStyle": "hachure",
92
+  "height": 50,
93
+  "id": "id0",
94
+  "isDeleted": false,
95
+  "opacity": 100,
96
+  "roughness": 1,
97
+  "seed": 337897,
98
+  "strokeColor": "#000000",
99
+  "strokeWidth": 1,
100
+  "type": "ellipse",
101
+  "version": 2,
102
+  "versionNonce": 1278240551,
103
+  "width": 30,
104
+  "x": 30,
105
+  "y": 20,
106
+}
107
+`;
108
+
109
+exports[`select single element on the scene rectangle 1`] = `
110
+Object {
111
+  "backgroundColor": "transparent",
112
+  "fillStyle": "hachure",
113
+  "height": 50,
114
+  "id": "id0",
115
+  "isDeleted": false,
116
+  "opacity": 100,
117
+  "roughness": 1,
118
+  "seed": 337897,
119
+  "strokeColor": "#000000",
120
+  "strokeWidth": 1,
121
+  "type": "rectangle",
122
+  "version": 2,
123
+  "versionNonce": 1278240551,
124
+  "width": 30,
125
+  "x": 30,
126
+  "y": 20,
127
+}
128
+`;

+ 16
- 0
src/tests/dragCreate.test.tsx 파일 보기

5
 import { KEYS } from "../keys";
5
 import { KEYS } from "../keys";
6
 import { render, fireEvent } from "./test-utils";
6
 import { render, fireEvent } from "./test-utils";
7
 import { ExcalidrawLinearElement } from "../element/types";
7
 import { ExcalidrawLinearElement } from "../element/types";
8
+import { reseed } from "../random";
8
 
9
 
9
 // Unmount ReactDOM from root
10
 // Unmount ReactDOM from root
10
 ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
11
 ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
13
 beforeEach(() => {
14
 beforeEach(() => {
14
   localStorage.clear();
15
   localStorage.clear();
15
   renderScene.mockClear();
16
   renderScene.mockClear();
17
+  reseed(7);
16
 });
18
 });
17
 
19
 
18
 const { h } = window;
20
 const { h } = window;
44
     expect(h.elements[0].y).toEqual(20);
46
     expect(h.elements[0].y).toEqual(20);
45
     expect(h.elements[0].width).toEqual(30); // 60 - 30
47
     expect(h.elements[0].width).toEqual(30); // 60 - 30
46
     expect(h.elements[0].height).toEqual(50); // 70 - 20
48
     expect(h.elements[0].height).toEqual(50); // 70 - 20
49
+
50
+    expect(h.elements.length).toMatchSnapshot();
51
+    h.elements.forEach((element) => expect(element).toMatchSnapshot());
47
   });
52
   });
48
 
53
 
49
   it("ellipse", () => {
54
   it("ellipse", () => {
72
     expect(h.elements[0].y).toEqual(20);
77
     expect(h.elements[0].y).toEqual(20);
73
     expect(h.elements[0].width).toEqual(30); // 60 - 30
78
     expect(h.elements[0].width).toEqual(30); // 60 - 30
74
     expect(h.elements[0].height).toEqual(50); // 70 - 20
79
     expect(h.elements[0].height).toEqual(50); // 70 - 20
80
+
81
+    expect(h.elements.length).toMatchSnapshot();
82
+    h.elements.forEach((element) => expect(element).toMatchSnapshot());
75
   });
83
   });
76
 
84
 
77
   it("diamond", () => {
85
   it("diamond", () => {
100
     expect(h.elements[0].y).toEqual(20);
108
     expect(h.elements[0].y).toEqual(20);
101
     expect(h.elements[0].width).toEqual(30); // 60 - 30
109
     expect(h.elements[0].width).toEqual(30); // 60 - 30
102
     expect(h.elements[0].height).toEqual(50); // 70 - 20
110
     expect(h.elements[0].height).toEqual(50); // 70 - 20
111
+
112
+    expect(h.elements.length).toMatchSnapshot();
113
+    h.elements.forEach((element) => expect(element).toMatchSnapshot());
103
   });
114
   });
104
 
115
 
105
   it("arrow", () => {
116
   it("arrow", () => {
132
     expect(element.points.length).toEqual(2);
143
     expect(element.points.length).toEqual(2);
133
     expect(element.points[0]).toEqual([0, 0]);
144
     expect(element.points[0]).toEqual([0, 0]);
134
     expect(element.points[1]).toEqual([30, 50]); // (60 - 30, 70 - 20)
145
     expect(element.points[1]).toEqual([30, 50]); // (60 - 30, 70 - 20)
146
+
147
+    expect(h.elements.length).toMatchSnapshot();
148
+    h.elements.forEach((element) => expect(element).toMatchSnapshot());
135
   });
149
   });
136
 
150
 
137
   it("line", () => {
151
   it("line", () => {
164
     expect(element.points.length).toEqual(2);
178
     expect(element.points.length).toEqual(2);
165
     expect(element.points[0]).toEqual([0, 0]);
179
     expect(element.points[0]).toEqual([0, 0]);
166
     expect(element.points[1]).toEqual([30, 50]); // (60 - 30, 70 - 20)
180
     expect(element.points[1]).toEqual([30, 50]); // (60 - 30, 70 - 20)
181
+
182
+    h.elements.forEach((element) => expect(element).toMatchSnapshot());
167
   });
183
   });
168
 });
184
 });
169
 
185
 

+ 6
- 0
src/tests/move.test.tsx 파일 보기

3
 import { render, fireEvent } from "./test-utils";
3
 import { render, fireEvent } from "./test-utils";
4
 import { App } from "../components/App";
4
 import { App } from "../components/App";
5
 import * as Renderer from "../renderer/renderScene";
5
 import * as Renderer from "../renderer/renderScene";
6
+import { reseed } from "../random";
6
 
7
 
7
 // Unmount ReactDOM from root
8
 // Unmount ReactDOM from root
8
 ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
9
 ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
11
 beforeEach(() => {
12
 beforeEach(() => {
12
   localStorage.clear();
13
   localStorage.clear();
13
   renderScene.mockClear();
14
   renderScene.mockClear();
15
+  reseed(7);
14
 });
16
 });
15
 
17
 
16
 const { h } = window;
18
 const { h } = window;
45
     expect(h.state.selectionElement).toBeNull();
47
     expect(h.state.selectionElement).toBeNull();
46
     expect(h.elements.length).toEqual(1);
48
     expect(h.elements.length).toEqual(1);
47
     expect([h.elements[0].x, h.elements[0].y]).toEqual([0, 40]);
49
     expect([h.elements[0].x, h.elements[0].y]).toEqual([0, 40]);
50
+
51
+    h.elements.forEach((element) => expect(element).toMatchSnapshot());
48
   });
52
   });
49
 });
53
 });
50
 
54
 
81
     // previous element should stay intact
85
     // previous element should stay intact
82
     expect([h.elements[0].x, h.elements[0].y]).toEqual([30, 20]);
86
     expect([h.elements[0].x, h.elements[0].y]).toEqual([30, 20]);
83
     expect([h.elements[1].x, h.elements[1].y]).toEqual([0, 40]);
87
     expect([h.elements[1].x, h.elements[1].y]).toEqual([0, 40]);
88
+
89
+    h.elements.forEach((element) => expect(element).toMatchSnapshot());
84
   });
90
   });
85
 });
91
 });

+ 6
- 0
src/tests/multiPointCreate.test.tsx 파일 보기

5
 import * as Renderer from "../renderer/renderScene";
5
 import * as Renderer from "../renderer/renderScene";
6
 import { KEYS } from "../keys";
6
 import { KEYS } from "../keys";
7
 import { ExcalidrawLinearElement } from "../element/types";
7
 import { ExcalidrawLinearElement } from "../element/types";
8
+import { reseed } from "../random";
8
 
9
 
9
 // Unmount ReactDOM from root
10
 // Unmount ReactDOM from root
10
 ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
11
 ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
13
 beforeEach(() => {
14
 beforeEach(() => {
14
   localStorage.clear();
15
   localStorage.clear();
15
   renderScene.mockClear();
16
   renderScene.mockClear();
17
+  reseed(7);
16
 });
18
 });
17
 
19
 
18
 const { h } = window;
20
 const { h } = window;
99
       [20, 30],
101
       [20, 30],
100
       [70, 110],
102
       [70, 110],
101
     ]);
103
     ]);
104
+
105
+    h.elements.forEach((element) => expect(element).toMatchSnapshot());
102
   });
106
   });
103
 
107
 
104
   it("line", () => {
108
   it("line", () => {
138
       [20, 30],
142
       [20, 30],
139
       [70, 110],
143
       [70, 110],
140
     ]);
144
     ]);
145
+
146
+    h.elements.forEach((element) => expect(element).toMatchSnapshot());
141
   });
147
   });
142
 });
148
 });

+ 12
- 10
src/tests/queries/toolQueries.ts 파일 보기

1
 import { queries, buildQueries } from "@testing-library/react";
1
 import { queries, buildQueries } from "@testing-library/react";
2
 
2
 
3
-const _getAllByToolName = (container: HTMLElement, tool: string) => {
4
-  const toolMap: { [propKey: string]: string } = {
5
-    selection: "Selection — S, 1",
6
-    rectangle: "Rectangle — R, 2",
7
-    diamond: "Diamond — D, 3",
8
-    ellipse: "Ellipse — E, 4",
9
-    arrow: "Arrow — A, 5",
10
-    line: "Line — L, 6",
11
-  };
3
+const toolMap = {
4
+  selection: "Selection — S, 1",
5
+  rectangle: "Rectangle — R, 2",
6
+  diamond: "Diamond — D, 3",
7
+  ellipse: "Ellipse — E, 4",
8
+  arrow: "Arrow — A, 5",
9
+  line: "Line — L, 6",
10
+};
12
 
11
 
13
-  const toolTitle = toolMap[tool as string];
12
+export type ToolName = keyof typeof toolMap;
13
+
14
+const _getAllByToolName = (container: HTMLElement, tool: string) => {
15
+  const toolTitle = toolMap[tool as ToolName];
14
   return queries.getAllByTitle(container, toolTitle);
16
   return queries.getAllByTitle(container, toolTitle);
15
 };
17
 };
16
 
18
 

+ 564
- 0
src/tests/regressionTests.test.tsx 파일 보기

1
+import { reseed } from "../random";
2
+import React from "react";
3
+import ReactDOM from "react-dom";
4
+import * as Renderer from "../renderer/renderScene";
5
+import { render, fireEvent } from "./test-utils";
6
+import { App } from "../components/App";
7
+import { ToolName } from "./queries/toolQueries";
8
+import { KEYS, Key } from "../keys";
9
+import { setDateTimeForTests } from "../utils";
10
+import { ExcalidrawElement } from "../element/types";
11
+import { handlerRectangles } from "../element";
12
+
13
+const { h } = window;
14
+
15
+const renderScene = jest.spyOn(Renderer, "renderScene");
16
+let getByToolName: (name: string) => HTMLElement = null!;
17
+let canvas: HTMLCanvasElement = null!;
18
+
19
+function clickTool(toolName: ToolName) {
20
+  fireEvent.click(getByToolName(toolName));
21
+}
22
+
23
+let lastClientX = 0;
24
+let lastClientY = 0;
25
+let pointerType: "mouse" | "pen" | "touch" = "mouse";
26
+
27
+function pointerDown(
28
+  clientX: number = lastClientX,
29
+  clientY: number = lastClientY,
30
+  altKey: boolean = false,
31
+  shiftKey: boolean = false,
32
+) {
33
+  lastClientX = clientX;
34
+  lastClientY = clientY;
35
+  fireEvent.pointerDown(canvas, {
36
+    clientX,
37
+    clientY,
38
+    altKey,
39
+    shiftKey,
40
+    pointerId: 1,
41
+    pointerType,
42
+  });
43
+}
44
+
45
+function pointer2Down(clientX: number, clientY: number) {
46
+  fireEvent.pointerDown(canvas, {
47
+    clientX,
48
+    clientY,
49
+    pointerId: 2,
50
+    pointerType,
51
+  });
52
+}
53
+
54
+function pointer2Move(clientX: number, clientY: number) {
55
+  fireEvent.pointerMove(canvas, {
56
+    clientX,
57
+    clientY,
58
+    pointerId: 2,
59
+    pointerType,
60
+  });
61
+}
62
+
63
+function pointer2Up(clientX: number, clientY: number) {
64
+  fireEvent.pointerUp(canvas, {
65
+    clientX,
66
+    clientY,
67
+    pointerId: 2,
68
+    pointerType,
69
+  });
70
+}
71
+
72
+function pointerMove(
73
+  clientX: number = lastClientX,
74
+  clientY: number = lastClientY,
75
+  altKey: boolean = false,
76
+  shiftKey: boolean = false,
77
+) {
78
+  lastClientX = clientX;
79
+  lastClientY = clientY;
80
+  fireEvent.pointerMove(canvas, {
81
+    clientX,
82
+    clientY,
83
+    altKey,
84
+    shiftKey,
85
+    pointerId: 1,
86
+    pointerType,
87
+  });
88
+}
89
+
90
+function pointerUp(
91
+  clientX: number = lastClientX,
92
+  clientY: number = lastClientY,
93
+  altKey: boolean = false,
94
+  shiftKey: boolean = false,
95
+) {
96
+  lastClientX = clientX;
97
+  lastClientY = clientY;
98
+  fireEvent.pointerUp(canvas, { pointerId: 1, pointerType, shiftKey, altKey });
99
+}
100
+
101
+function hotkeyDown(key: Key) {
102
+  fireEvent.keyDown(document, { key: KEYS[key] });
103
+}
104
+
105
+function hotkeyUp(key: Key) {
106
+  fireEvent.keyUp(document, {
107
+    key: KEYS[key],
108
+  });
109
+}
110
+
111
+function keyDown(
112
+  key: string,
113
+  ctrlKey: boolean = false,
114
+  shiftKey: boolean = false,
115
+) {
116
+  fireEvent.keyDown(document, { key, ctrlKey, shiftKey });
117
+}
118
+
119
+function keyUp(
120
+  key: string,
121
+  ctrlKey: boolean = false,
122
+  shiftKey: boolean = false,
123
+) {
124
+  fireEvent.keyUp(document, {
125
+    key,
126
+    ctrlKey,
127
+    shiftKey,
128
+  });
129
+}
130
+
131
+function hotkeyPress(key: Key) {
132
+  hotkeyDown(key);
133
+  hotkeyUp(key);
134
+}
135
+
136
+function keyPress(
137
+  key: string,
138
+  ctrlKey: boolean = false,
139
+  shiftKey: boolean = false,
140
+) {
141
+  keyDown(key, ctrlKey, shiftKey);
142
+  keyUp(key, ctrlKey, shiftKey);
143
+}
144
+
145
+function clickLabeledElement(label: string) {
146
+  const element = document.querySelector(`[aria-label='${label}']`);
147
+  if (!element) {
148
+    throw new Error(`No labeled element found: ${label}`);
149
+  }
150
+  fireEvent.click(element);
151
+}
152
+
153
+function getSelectedElement(): ExcalidrawElement {
154
+  const selectedElements = h.elements.filter(
155
+    (element) => h.state.selectedElementIds[element.id],
156
+  );
157
+  if (selectedElements.length !== 1) {
158
+    throw new Error(
159
+      `expected 1 selected element; got ${selectedElements.length}`,
160
+    );
161
+  }
162
+  return selectedElements[0];
163
+}
164
+
165
+function getResizeHandles() {
166
+  const rects = handlerRectangles(
167
+    getSelectedElement(),
168
+    h.state.zoom,
169
+    pointerType,
170
+  );
171
+
172
+  const rv: { [K in keyof typeof rects]: [number, number] } = {} as any;
173
+
174
+  for (const handlePos in rects) {
175
+    const [x, y, width, height] = rects[handlePos as keyof typeof rects];
176
+
177
+    rv[handlePos as keyof typeof rects] = [x + width / 2, y + height / 2];
178
+  }
179
+
180
+  return rv;
181
+}
182
+
183
+/**
184
+ * This is always called at the end of your test, so usually you don't need to call it.
185
+ * However, if you have a long test, you might want to call it during the test so it's easier
186
+ * to debug where a test failure came from.
187
+ */
188
+function checkpoint(name: string) {
189
+  expect(renderScene.mock.calls.length).toMatchSnapshot(
190
+    `[${name}] number of renders`,
191
+  );
192
+  expect(h.state).toMatchSnapshot(`[${name}] appState`);
193
+  expect(h.history.getSnapshotForTest()).toMatchSnapshot(`[${name}] history`);
194
+  expect(h.elements.length).toMatchSnapshot(`[${name}] number of elements`);
195
+  h.elements.forEach((element, i) =>
196
+    expect(element).toMatchSnapshot(`[${name}] element ${i}`),
197
+  );
198
+}
199
+
200
+beforeEach(() => {
201
+  // Unmount ReactDOM from root
202
+  ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
203
+
204
+  localStorage.clear();
205
+  renderScene.mockClear();
206
+  h.history.clear();
207
+  reseed(7);
208
+  setDateTimeForTests("201933152653");
209
+  pointerType = "mouse";
210
+
211
+  const renderResult = render(<App />);
212
+
213
+  getByToolName = renderResult.getByToolName;
214
+  canvas = renderResult.container.querySelector("canvas")!;
215
+});
216
+
217
+afterEach(() => {
218
+  checkpoint("end of test");
219
+});
220
+
221
+describe("regression tests", () => {
222
+  it("draw every type of shape", () => {
223
+    clickTool("rectangle");
224
+    pointerDown(10, 10);
225
+    pointerMove(20, 20);
226
+    pointerUp();
227
+
228
+    clickTool("diamond");
229
+    pointerDown(30, 10);
230
+    pointerMove(40, 20);
231
+    pointerUp();
232
+
233
+    clickTool("ellipse");
234
+    pointerDown(50, 10);
235
+    pointerMove(60, 20);
236
+    pointerUp();
237
+
238
+    clickTool("arrow");
239
+    pointerDown(70, 10);
240
+    pointerMove(80, 20);
241
+    pointerUp();
242
+
243
+    clickTool("line");
244
+    pointerDown(90, 10);
245
+    pointerMove(100, 20);
246
+    pointerUp();
247
+
248
+    clickTool("arrow");
249
+    pointerDown(10, 30);
250
+    pointerUp();
251
+    pointerMove(20, 40);
252
+    pointerUp();
253
+    pointerMove(10, 50);
254
+    pointerUp();
255
+    hotkeyPress("ENTER");
256
+
257
+    clickTool("line");
258
+    pointerDown(30, 30);
259
+    pointerUp();
260
+    pointerMove(40, 40);
261
+    pointerUp();
262
+    pointerMove(30, 50);
263
+    pointerUp();
264
+    hotkeyPress("ENTER");
265
+  });
266
+
267
+  it("click to select a shape", () => {
268
+    clickTool("rectangle");
269
+    pointerDown(10, 10);
270
+    pointerMove(20, 20);
271
+    pointerUp();
272
+
273
+    clickTool("rectangle");
274
+    pointerDown(30, 10);
275
+    pointerMove(40, 20);
276
+    pointerUp();
277
+
278
+    const prevSelectedId = getSelectedElement().id;
279
+    pointerDown(10, 10);
280
+    pointerUp();
281
+    expect(getSelectedElement().id).not.toEqual(prevSelectedId);
282
+  });
283
+
284
+  for (const [keys, shape] of [
285
+    ["2r", "rectangle"],
286
+    ["3d", "diamond"],
287
+    ["4e", "ellipse"],
288
+    ["5a", "arrow"],
289
+    ["6l", "line"],
290
+  ] as [string, ExcalidrawElement["type"]][]) {
291
+    for (const key of keys) {
292
+      it(`hotkey ${key} selects ${shape} tool`, () => {
293
+        keyPress(key);
294
+
295
+        pointerDown(10, 10);
296
+        pointerMove(20, 20);
297
+        pointerUp();
298
+
299
+        expect(getSelectedElement().type).toBe(shape);
300
+      });
301
+    }
302
+  }
303
+
304
+  it("change the properties of a shape", () => {
305
+    clickTool("rectangle");
306
+    pointerDown(10, 10);
307
+    pointerMove(20, 20);
308
+    pointerUp();
309
+
310
+    clickLabeledElement("Background");
311
+    clickLabeledElement("#fa5252");
312
+    clickLabeledElement("Stroke");
313
+    clickLabeledElement("#5f3dc4");
314
+    expect(getSelectedElement().backgroundColor).toBe("#fa5252");
315
+    expect(getSelectedElement().strokeColor).toBe("#5f3dc4");
316
+  });
317
+
318
+  it("resize an element, trying every resize handle", () => {
319
+    clickTool("rectangle");
320
+    pointerDown(10, 10);
321
+    pointerMove(20, 20);
322
+    pointerUp();
323
+
324
+    const resizeHandles = getResizeHandles();
325
+    for (const handlePos in resizeHandles) {
326
+      const [x, y] = resizeHandles[handlePos as keyof typeof resizeHandles];
327
+      const { width: prevWidth, height: prevHeight } = getSelectedElement();
328
+      pointerDown(x, y);
329
+      pointerMove(x - 5, y - 5);
330
+      pointerUp();
331
+      const {
332
+        width: nextWidthNegative,
333
+        height: nextHeightNegative,
334
+      } = getSelectedElement();
335
+      expect(
336
+        prevWidth !== nextWidthNegative || prevHeight !== nextHeightNegative,
337
+      ).toBeTruthy();
338
+      checkpoint(`resize handle ${handlePos} (-5, -5)`);
339
+
340
+      pointerDown();
341
+      pointerMove(x, y);
342
+      pointerUp();
343
+      const { width, height } = getSelectedElement();
344
+      expect(width).toBe(prevWidth);
345
+      expect(height).toBe(prevHeight);
346
+      checkpoint(`unresize handle ${handlePos} (-5, -5)`);
347
+
348
+      pointerDown(x, y);
349
+      pointerMove(x + 5, y + 5);
350
+      pointerUp();
351
+      const {
352
+        width: nextWidthPositive,
353
+        height: nextHeightPositive,
354
+      } = getSelectedElement();
355
+      expect(
356
+        prevWidth !== nextWidthPositive || prevHeight !== nextHeightPositive,
357
+      ).toBeTruthy();
358
+      checkpoint(`resize handle ${handlePos} (+5, +5)`);
359
+
360
+      pointerDown();
361
+      pointerMove(x, y);
362
+      pointerUp();
363
+      const { width: finalWidth, height: finalHeight } = getSelectedElement();
364
+      expect(finalWidth).toBe(prevWidth);
365
+      expect(finalHeight).toBe(prevHeight);
366
+
367
+      checkpoint(`unresize handle ${handlePos} (+5, +5)`);
368
+    }
369
+  });
370
+
371
+  it("click on an element and drag it", () => {
372
+    clickTool("rectangle");
373
+    pointerDown(10, 10);
374
+    pointerMove(20, 20);
375
+    pointerUp();
376
+
377
+    const { x: prevX, y: prevY } = getSelectedElement();
378
+    pointerDown(10, 10);
379
+    pointerMove(20, 20);
380
+    pointerUp();
381
+
382
+    const { x: nextX, y: nextY } = getSelectedElement();
383
+    expect(nextX).toBeGreaterThan(prevX);
384
+    expect(nextY).toBeGreaterThan(prevY);
385
+
386
+    checkpoint("dragged");
387
+
388
+    pointerDown();
389
+    pointerMove(10, 10);
390
+    pointerUp();
391
+
392
+    const { x, y } = getSelectedElement();
393
+    expect(x).toBe(prevX);
394
+    expect(y).toBe(prevY);
395
+  });
396
+
397
+  it("alt-drag duplicates an element", () => {
398
+    clickTool("rectangle");
399
+    pointerDown(10, 10);
400
+    pointerMove(20, 20);
401
+    pointerUp();
402
+
403
+    expect(
404
+      h.elements.filter((element) => element.type === "rectangle").length,
405
+    ).toBe(1);
406
+    pointerDown(10, 10, true);
407
+    pointerMove(20, 20, true);
408
+    pointerUp(20, 20, true);
409
+    expect(
410
+      h.elements.filter((element) => element.type === "rectangle").length,
411
+    ).toBe(2);
412
+  });
413
+
414
+  it("click-drag to select a group", () => {
415
+    clickTool("rectangle");
416
+    pointerDown(10, 10);
417
+    pointerMove(20, 20);
418
+    pointerUp();
419
+
420
+    clickTool("rectangle");
421
+    pointerDown(30, 10);
422
+    pointerMove(40, 20);
423
+    pointerUp();
424
+
425
+    clickTool("rectangle");
426
+    pointerDown(50, 10);
427
+    pointerMove(60, 20);
428
+    pointerUp();
429
+
430
+    pointerDown(0, 0);
431
+    pointerMove(45, 25);
432
+    pointerUp();
433
+
434
+    expect(
435
+      h.elements.filter((element) => h.state.selectedElementIds[element.id])
436
+        .length,
437
+    ).toBe(2);
438
+  });
439
+
440
+  it("shift-click to select a group, then drag", () => {
441
+    clickTool("rectangle");
442
+    pointerDown(10, 10);
443
+    pointerMove(20, 20);
444
+    pointerUp();
445
+
446
+    clickTool("rectangle");
447
+    pointerDown(30, 10);
448
+    pointerMove(40, 20);
449
+    pointerUp();
450
+
451
+    const prevRectsXY = h.elements
452
+      .filter((element) => element.type === "rectangle")
453
+      .map((element) => ({ x: element.x, y: element.y }));
454
+    pointerDown(10, 10);
455
+    pointerUp();
456
+    pointerDown(30, 10, false, true);
457
+    pointerUp();
458
+    pointerDown(30, 10);
459
+    pointerMove(40, 20);
460
+    pointerUp();
461
+    h.elements
462
+      .filter((element) => element.type === "rectangle")
463
+      .forEach((element, i) => {
464
+        expect(element.x).toBeGreaterThan(prevRectsXY[i].x);
465
+        expect(element.y).toBeGreaterThan(prevRectsXY[i].y);
466
+      });
467
+  });
468
+
469
+  it("pinch-to-zoom works", () => {
470
+    expect(h.state.zoom).toBe(1);
471
+    pointerType = "touch";
472
+    pointerDown(50, 50);
473
+    pointer2Down(60, 50);
474
+    pointerMove(40, 50);
475
+    pointer2Move(60, 50);
476
+    expect(h.state.zoom).toBeGreaterThan(1);
477
+    const zoomed = h.state.zoom;
478
+    pointerMove(45, 50);
479
+    pointer2Move(55, 50);
480
+    expect(h.state.zoom).toBeLessThan(zoomed);
481
+    pointerUp(45, 50);
482
+    pointer2Up(55, 50);
483
+  });
484
+
485
+  it("two-finger scroll works", () => {
486
+    const startScrollY = h.state.scrollY;
487
+    pointerDown(50, 50);
488
+    pointer2Down(60, 50);
489
+    pointerMove(50, 40);
490
+    pointer2Move(60, 40);
491
+    pointerUp(50, 40);
492
+    pointer2Up(60, 40);
493
+    expect(h.state.scrollY).toBeLessThan(startScrollY);
494
+
495
+    const startScrollX = h.state.scrollX;
496
+    pointerDown(50, 50);
497
+    pointer2Down(50, 60);
498
+    pointerMove(60, 50);
499
+    pointer2Move(60, 60);
500
+    pointerUp(60, 50);
501
+    pointer2Up(60, 60);
502
+    expect(h.state.scrollX).toBeGreaterThan(startScrollX);
503
+  });
504
+
505
+  it("spacebar + drag scrolls the canvas", () => {
506
+    const { scrollX: startScrollX, scrollY: startScrollY } = h.state;
507
+    hotkeyDown("SPACE");
508
+    pointerDown(50, 50);
509
+    pointerMove(60, 60);
510
+    pointerUp();
511
+    hotkeyUp("SPACE");
512
+    const { scrollX, scrollY } = h.state;
513
+    expect(scrollX).not.toEqual(startScrollX);
514
+    expect(scrollY).not.toEqual(startScrollY);
515
+  });
516
+
517
+  it("arrow keys", () => {
518
+    clickTool("rectangle");
519
+    pointerDown(10, 10);
520
+    pointerMove(20, 20);
521
+    pointerUp();
522
+    hotkeyPress("ARROW_LEFT");
523
+    hotkeyPress("ARROW_LEFT");
524
+    hotkeyPress("ARROW_RIGHT");
525
+    hotkeyPress("ARROW_UP");
526
+    hotkeyPress("ARROW_UP");
527
+    hotkeyPress("ARROW_DOWN");
528
+  });
529
+
530
+  it("undo/redo drawing an element", () => {
531
+    clickTool("rectangle");
532
+    pointerDown(10, 10);
533
+    pointerMove(20, 20);
534
+    pointerUp();
535
+
536
+    clickTool("rectangle");
537
+    pointerDown(30, 10);
538
+    pointerMove(40, 20);
539
+    pointerUp();
540
+
541
+    clickTool("rectangle");
542
+    pointerDown(50, 10);
543
+    pointerMove(60, 20);
544
+    pointerUp();
545
+
546
+    expect(h.elements.filter((element) => !element.isDeleted).length).toBe(3);
547
+    keyPress("z", true);
548
+    expect(h.elements.filter((element) => !element.isDeleted).length).toBe(2);
549
+    keyPress("z", true);
550
+    expect(h.elements.filter((element) => !element.isDeleted).length).toBe(1);
551
+    keyPress("z", true, true);
552
+    expect(h.elements.filter((element) => !element.isDeleted).length).toBe(2);
553
+  });
554
+
555
+  it("zoom hotkeys", () => {
556
+    expect(h.state.zoom).toBe(1);
557
+    fireEvent.keyDown(document, { code: "Equal", ctrlKey: true });
558
+    fireEvent.keyUp(document, { code: "Equal", ctrlKey: true });
559
+    expect(h.state.zoom).toBeGreaterThan(1);
560
+    fireEvent.keyDown(document, { code: "Minus", ctrlKey: true });
561
+    fireEvent.keyUp(document, { code: "Minus", ctrlKey: true });
562
+    expect(h.state.zoom).toBe(1);
563
+  });
564
+});

+ 6
- 0
src/tests/resize.test.tsx 파일 보기

3
 import { render, fireEvent } from "./test-utils";
3
 import { render, fireEvent } from "./test-utils";
4
 import { App } from "../components/App";
4
 import { App } from "../components/App";
5
 import * as Renderer from "../renderer/renderScene";
5
 import * as Renderer from "../renderer/renderScene";
6
+import { reseed } from "../random";
6
 
7
 
7
 // Unmount ReactDOM from root
8
 // Unmount ReactDOM from root
8
 ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
9
 ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
11
 beforeEach(() => {
12
 beforeEach(() => {
12
   localStorage.clear();
13
   localStorage.clear();
13
   renderScene.mockClear();
14
   renderScene.mockClear();
15
+  reseed(7);
14
 });
16
 });
15
 
17
 
16
 const { h } = window;
18
 const { h } = window;
53
     expect(h.elements.length).toEqual(1);
55
     expect(h.elements.length).toEqual(1);
54
     expect([h.elements[0].x, h.elements[0].y]).toEqual([29, 47]);
56
     expect([h.elements[0].x, h.elements[0].y]).toEqual([29, 47]);
55
     expect([h.elements[0].width, h.elements[0].height]).toEqual([30, 50]);
57
     expect([h.elements[0].width, h.elements[0].height]).toEqual([30, 50]);
58
+
59
+    h.elements.forEach((element) => expect(element).toMatchSnapshot());
56
   });
60
   });
57
 });
61
 });
58
 
62
 
94
     expect(h.elements.length).toEqual(1);
98
     expect(h.elements.length).toEqual(1);
95
     expect([h.elements[0].x, h.elements[0].y]).toEqual([29, 47]);
99
     expect([h.elements[0].x, h.elements[0].y]).toEqual([29, 47]);
96
     expect([h.elements[0].width, h.elements[0].height]).toEqual([30, 50]);
100
     expect([h.elements[0].width, h.elements[0].height]).toEqual([30, 50]);
101
+
102
+    h.elements.forEach((element) => expect(element).toMatchSnapshot());
97
   });
103
   });
98
 });
104
 });

+ 11
- 0
src/tests/selection.test.tsx 파일 보기

4
 import { App } from "../components/App";
4
 import { App } from "../components/App";
5
 import * as Renderer from "../renderer/renderScene";
5
 import * as Renderer from "../renderer/renderScene";
6
 import { KEYS } from "../keys";
6
 import { KEYS } from "../keys";
7
+import { reseed } from "../random";
7
 
8
 
8
 // Unmount ReactDOM from root
9
 // Unmount ReactDOM from root
9
 ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
10
 ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
12
 beforeEach(() => {
13
 beforeEach(() => {
13
   localStorage.clear();
14
   localStorage.clear();
14
   renderScene.mockClear();
15
   renderScene.mockClear();
16
+  reseed(7);
15
 });
17
 });
16
 
18
 
17
 const { h } = window;
19
 const { h } = window;
98
     expect(h.state.selectionElement).toBeNull();
100
     expect(h.state.selectionElement).toBeNull();
99
     expect(h.elements.length).toEqual(1);
101
     expect(h.elements.length).toEqual(1);
100
     expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
102
     expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
103
+
104
+    h.elements.forEach((element) => expect(element).toMatchSnapshot());
101
   });
105
   });
102
 
106
 
103
   it("diamond", () => {
107
   it("diamond", () => {
123
     expect(h.state.selectionElement).toBeNull();
127
     expect(h.state.selectionElement).toBeNull();
124
     expect(h.elements.length).toEqual(1);
128
     expect(h.elements.length).toEqual(1);
125
     expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
129
     expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
130
+
131
+    h.elements.forEach((element) => expect(element).toMatchSnapshot());
126
   });
132
   });
127
 
133
 
128
   it("ellipse", () => {
134
   it("ellipse", () => {
148
     expect(h.state.selectionElement).toBeNull();
154
     expect(h.state.selectionElement).toBeNull();
149
     expect(h.elements.length).toEqual(1);
155
     expect(h.elements.length).toEqual(1);
150
     expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
156
     expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
157
+
158
+    h.elements.forEach((element) => expect(element).toMatchSnapshot());
151
   });
159
   });
152
 
160
 
153
   it("arrow", () => {
161
   it("arrow", () => {
186
     expect(h.state.selectionElement).toBeNull();
194
     expect(h.state.selectionElement).toBeNull();
187
     expect(h.elements.length).toEqual(1);
195
     expect(h.elements.length).toEqual(1);
188
     expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
196
     expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
197
+    h.elements.forEach((element) => expect(element).toMatchSnapshot());
189
   });
198
   });
190
 
199
 
191
   it("arrow escape", () => {
200
   it("arrow escape", () => {
224
     expect(h.state.selectionElement).toBeNull();
233
     expect(h.state.selectionElement).toBeNull();
225
     expect(h.elements.length).toEqual(1);
234
     expect(h.elements.length).toEqual(1);
226
     expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
235
     expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
236
+
237
+    h.elements.forEach((element) => expect(element).toMatchSnapshot());
227
   });
238
   });
228
 });
239
 });

+ 10
- 0
src/utils.ts 파일 보기

3
 
3
 
4
 export const SVG_NS = "http://www.w3.org/2000/svg";
4
 export const SVG_NS = "http://www.w3.org/2000/svg";
5
 
5
 
6
+let mockDateTime: string | null = null;
7
+
8
+export function setDateTimeForTests(dateTime: string) {
9
+  mockDateTime = dateTime;
10
+}
11
+
6
 export function getDateTime() {
12
 export function getDateTime() {
13
+  if (mockDateTime) {
14
+    return mockDateTime;
15
+  }
16
+
7
   const date = new Date();
17
   const date = new Date();
8
   const year = date.getFullYear();
18
   const year = date.getFullYear();
9
   const month = date.getMonth() + 1;
19
   const month = date.getMonth() + 1;

Loading…
취소
저장