Переглянути джерело

Fix library dnd (#2314)

vanilla_orig
David Luzar 5 роки тому
джерело
коміт
ba3f548b91
Аккаунт користувача з таким Email не знайдено

+ 1
- 0
.gitignore Переглянути файл

@@ -14,3 +14,4 @@ yarn-error.log*
14 14
 yarn.lock
15 15
 .idea
16 16
 dist/
17
+.eslintcache

+ 3
- 3
src/actions/actionAddToLibrary.ts Переглянути файл

@@ -2,7 +2,7 @@ import { register } from "./register";
2 2
 import { getSelectedElements } from "../scene";
3 3
 import { getNonDeletedElements } from "../element";
4 4
 import { deepCopyElement } from "../element/newElement";
5
-import { loadLibrary, saveLibrary } from "../data/localStorage";
5
+import { Library } from "../data/library";
6 6
 
7 7
 export const actionAddToLibrary = register({
8 8
   name: "addToLibrary",
@@ -12,8 +12,8 @@ export const actionAddToLibrary = register({
12 12
       appState,
13 13
     );
14 14
 
15
-    loadLibrary().then((items) => {
16
-      saveLibrary([...items, selectedElements.map(deepCopyElement)]);
15
+    Library.loadLibrary().then((items) => {
16
+      Library.saveLibrary([...items, selectedElements.map(deepCopyElement)]);
17 17
     });
18 18
 
19 19
     return false;

+ 4
- 5
src/components/App.tsx Переглянути файл

@@ -145,7 +145,6 @@ import {
145 145
   isBindingElementType,
146 146
 } from "../element/typeChecks";
147 147
 import { actionFinalize, actionDeleteSelected } from "../actions";
148
-import { loadLibrary } from "../data/localStorage";
149 148
 
150 149
 import throttle from "lodash.throttle";
151 150
 import { LinearElementEditor } from "../element/linearElementEditor";
@@ -1266,7 +1265,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
1266 1265
     history.resumeRecording();
1267 1266
     this.scene.replaceAllElements(this.scene.getElements());
1268 1267
 
1269
-    this.initializeSocketClient({ showLoadingState: false });
1268
+    await this.initializeSocketClient({ showLoadingState: false });
1270 1269
   };
1271 1270
 
1272 1271
   closePortal = () => {
@@ -3729,7 +3728,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
3729 3728
       });
3730 3729
     }
3731 3730
 
3732
-    const libraryShapes = event.dataTransfer.getData(MIME_TYPES.excalidraw);
3731
+    const libraryShapes = event.dataTransfer.getData(MIME_TYPES.excalidrawlib);
3733 3732
     if (libraryShapes !== "") {
3734 3733
       this.addElementsFromPasteOrLibrary(
3735 3734
         JSON.parse(libraryShapes),
@@ -4040,7 +4039,7 @@ declare global {
4040 4039
       setState: React.Component<any, AppState>["setState"];
4041 4040
       history: SceneHistory;
4042 4041
       app: InstanceType<typeof App>;
4043
-      library: ReturnType<typeof loadLibrary>;
4042
+      library: typeof Library;
4044 4043
     };
4045 4044
   }
4046 4045
 }
@@ -4064,7 +4063,7 @@ if (
4064 4063
       get: () => history,
4065 4064
     },
4066 4065
     library: {
4067
-      get: () => loadLibrary(),
4066
+      value: Library,
4068 4067
     },
4069 4068
   });
4070 4069
 }

+ 6
- 6
src/components/LayerUI.tsx Переглянути файл

@@ -39,12 +39,12 @@ import { Tooltip } from "./Tooltip";
39 39
 
40 40
 import "./LayerUI.scss";
41 41
 import { LibraryUnit } from "./LibraryUnit";
42
-import { loadLibrary, saveLibrary } from "../data/localStorage";
43 42
 import { ToolButton } from "./ToolButton";
44 43
 import { saveLibraryAsJSON, importLibraryFromJSON } from "../data/json";
45 44
 import { muteFSAbortError } from "../utils";
46 45
 import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle";
47 46
 import clsx from "clsx";
47
+import { Library } from "../data/library";
48 48
 
49 49
 interface LayerUIProps {
50 50
   actionManager: ActionManager;
@@ -223,7 +223,7 @@ const LibraryMenu = ({
223 223
           resolve("loading");
224 224
         }, 100);
225 225
       }),
226
-      loadLibrary().then((items) => {
226
+      Library.loadLibrary().then((items) => {
227 227
         setLibraryItems(items);
228 228
         setIsLoading("ready");
229 229
       }),
@@ -238,18 +238,18 @@ const LibraryMenu = ({
238 238
   }, []);
239 239
 
240 240
   const removeFromLibrary = useCallback(async (indexToRemove) => {
241
-    const items = await loadLibrary();
241
+    const items = await Library.loadLibrary();
242 242
     const nextItems = items.filter((_, index) => index !== indexToRemove);
243
-    saveLibrary(nextItems);
243
+    Library.saveLibrary(nextItems);
244 244
     setLibraryItems(nextItems);
245 245
   }, []);
246 246
 
247 247
   const addToLibrary = useCallback(
248 248
     async (elements: LibraryItem) => {
249
-      const items = await loadLibrary();
249
+      const items = await Library.loadLibrary();
250 250
       const nextItems = [...items, elements];
251 251
       onAddToLibrary();
252
-      saveLibrary(nextItems);
252
+      Library.saveLibrary(nextItems);
253 253
       setLibraryItems(nextItems);
254 254
     },
255 255
     [onAddToLibrary],

+ 7
- 0
src/constants.ts Переглянути файл

@@ -89,3 +89,10 @@ export const MIME_TYPES = {
89 89
   excalidraw: "application/vnd.excalidraw+json",
90 90
   excalidrawlib: "application/vnd.excalidrawlib+json",
91 91
 };
92
+
93
+export const STORAGE_KEYS = {
94
+  LOCAL_STORAGE_ELEMENTS: "excalidraw",
95
+  LOCAL_STORAGE_APP_STATE: "excalidraw-state",
96
+  LOCAL_STORAGE_COLLAB: "excalidraw-collab",
97
+  LOCAL_STORAGE_LIBRARY: "excalidraw-library",
98
+};

+ 15
- 4
src/data/blob.ts Переглянути файл

@@ -55,13 +55,24 @@ export const parseFileContents = async (blob: Blob | File) => {
55 55
   return contents;
56 56
 };
57 57
 
58
-const getMimeType = (blob: Blob): string => {
59
-  if (blob.type) {
60
-    return blob.type;
58
+export const getMimeType = (blob: Blob | string): string => {
59
+  let name: string;
60
+  if (typeof blob === "string") {
61
+    name = blob;
62
+  } else {
63
+    if (blob.type) {
64
+      return blob.type;
65
+    }
66
+    name = blob.name || "";
61 67
   }
62
-  const name = blob.name || "";
63 68
   if (/\.(excalidraw|json)$/.test(name)) {
64 69
     return "application/json";
70
+  } else if (/\.png$/.test(name)) {
71
+    return "image/png";
72
+  } else if (/\.jpe?g$/.test(name)) {
73
+    return "image/jpeg";
74
+  } else if (/\.svg$/.test(name)) {
75
+    return "image/svg+xml";
65 76
   }
66 77
   return "";
67 78
 };

+ 1
- 2
src/data/json.ts Переглянути файл

@@ -4,7 +4,6 @@ import { cleanAppStateForExport } from "../appState";
4 4
 
5 5
 import { fileOpen, fileSave } from "browser-nativefs";
6 6
 import { loadFromBlob } from "./blob";
7
-import { loadLibrary } from "./localStorage";
8 7
 import { Library } from "./library";
9 8
 import { MIME_TYPES } from "../constants";
10 9
 
@@ -65,7 +64,7 @@ export const isValidLibrary = (json: any) => {
65 64
 };
66 65
 
67 66
 export const saveLibraryAsJSON = async () => {
68
-  const library = await loadLibrary();
67
+  const library = await Library.loadLibrary();
69 68
   const serialized = JSON.stringify(
70 69
     {
71 70
       type: "excalidrawlib",

+ 52
- 3
src/data/library.ts Переглянути файл

@@ -1,8 +1,16 @@
1 1
 import { loadLibraryFromBlob } from "./blob";
2 2
 import { LibraryItems, LibraryItem } from "../types";
3
-import { loadLibrary, saveLibrary } from "./localStorage";
3
+import { restoreElements } from "./restore";
4
+import { STORAGE_KEYS } from "../constants";
4 5
 
5 6
 export class Library {
7
+  private static libraryCache: LibraryItems | null = null;
8
+
9
+  static resetLibrary = () => {
10
+    Library.libraryCache = null;
11
+    localStorage.removeItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY);
12
+  };
13
+
6 14
   /** imports library (currently merges, removing duplicates) */
7 15
   static async importLibrary(blob: Blob) {
8 16
     const libraryFile = await loadLibraryFromBlob(blob);
@@ -34,10 +42,51 @@ export class Library {
34 42
       });
35 43
     };
36 44
 
37
-    const existingLibraryItems = await loadLibrary();
45
+    const existingLibraryItems = await Library.loadLibrary();
38 46
     const filtered = libraryFile.library!.filter((libraryItem) =>
39 47
       isUniqueitem(existingLibraryItems, libraryItem),
40 48
     );
41
-    saveLibrary([...existingLibraryItems, ...filtered]);
49
+    Library.saveLibrary([...existingLibraryItems, ...filtered]);
42 50
   }
51
+
52
+  static loadLibrary = (): Promise<LibraryItems> => {
53
+    return new Promise(async (resolve) => {
54
+      if (Library.libraryCache) {
55
+        return resolve(JSON.parse(JSON.stringify(Library.libraryCache)));
56
+      }
57
+
58
+      try {
59
+        const data = localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY);
60
+        if (!data) {
61
+          return resolve([]);
62
+        }
63
+
64
+        const items = (JSON.parse(data) as LibraryItems).map((elements) =>
65
+          restoreElements(elements),
66
+        ) as Mutable<LibraryItems>;
67
+
68
+        // clone to ensure we don't mutate the cached library elements in the app
69
+        Library.libraryCache = JSON.parse(JSON.stringify(items));
70
+
71
+        resolve(items);
72
+      } catch (e) {
73
+        console.error(e);
74
+        resolve([]);
75
+      }
76
+    });
77
+  };
78
+
79
+  static saveLibrary = (items: LibraryItems) => {
80
+    const prevLibraryItems = Library.libraryCache;
81
+    try {
82
+      const serializedItems = JSON.stringify(items);
83
+      // cache optimistically so that consumers have access to the latest
84
+      //  immediately
85
+      Library.libraryCache = JSON.parse(serializedItems);
86
+      localStorage.setItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY, serializedItems);
87
+    } catch (e) {
88
+      Library.libraryCache = prevLibraryItems;
89
+      console.error(e);
90
+    }
91
+  };
43 92
 }

+ 8
- 55
src/data/localStorage.ts Переглянути файл

@@ -1,59 +1,12 @@
1 1
 import { ExcalidrawElement } from "../element/types";
2
-import { AppState, LibraryItems } from "../types";
2
+import { AppState } from "../types";
3 3
 import { clearAppStateForLocalStorage, getDefaultAppState } from "../appState";
4
-import { restoreElements } from "./restore";
5
-
6
-const LOCAL_STORAGE_KEY = "excalidraw";
7
-const LOCAL_STORAGE_KEY_STATE = "excalidraw-state";
8
-const LOCAL_STORAGE_KEY_COLLAB = "excalidraw-collab";
9
-const LOCAL_STORAGE_KEY_LIBRARY = "excalidraw-library";
10
-
11
-let _LATEST_LIBRARY_ITEMS: LibraryItems | null = null;
12
-export const loadLibrary = (): Promise<LibraryItems> => {
13
-  return new Promise(async (resolve) => {
14
-    if (_LATEST_LIBRARY_ITEMS) {
15
-      return resolve(JSON.parse(JSON.stringify(_LATEST_LIBRARY_ITEMS)));
16
-    }
17
-
18
-    try {
19
-      const data = localStorage.getItem(LOCAL_STORAGE_KEY_LIBRARY);
20
-      if (!data) {
21
-        return resolve([]);
22
-      }
23
-
24
-      const items = (JSON.parse(data) as LibraryItems).map((elements) =>
25
-        restoreElements(elements),
26
-      ) as Mutable<LibraryItems>;
27
-
28
-      // clone to ensure we don't mutate the cached library elements in the app
29
-      _LATEST_LIBRARY_ITEMS = JSON.parse(JSON.stringify(items));
30
-
31
-      resolve(items);
32
-    } catch (e) {
33
-      console.error(e);
34
-      resolve([]);
35
-    }
36
-  });
37
-};
38
-
39
-export const saveLibrary = (items: LibraryItems) => {
40
-  const prevLibraryItems = _LATEST_LIBRARY_ITEMS;
41
-  try {
42
-    const serializedItems = JSON.stringify(items);
43
-    // cache optimistically so that consumers have access to the latest
44
-    //  immediately
45
-    _LATEST_LIBRARY_ITEMS = JSON.parse(serializedItems);
46
-    localStorage.setItem(LOCAL_STORAGE_KEY_LIBRARY, serializedItems);
47
-  } catch (e) {
48
-    _LATEST_LIBRARY_ITEMS = prevLibraryItems;
49
-    console.error(e);
50
-  }
51
-};
4
+import { STORAGE_KEYS } from "../constants";
52 5
 
53 6
 export const saveUsernameToLocalStorage = (username: string) => {
54 7
   try {
55 8
     localStorage.setItem(
56
-      LOCAL_STORAGE_KEY_COLLAB,
9
+      STORAGE_KEYS.LOCAL_STORAGE_COLLAB,
57 10
       JSON.stringify({ username }),
58 11
     );
59 12
   } catch (error) {
@@ -64,7 +17,7 @@ export const saveUsernameToLocalStorage = (username: string) => {
64 17
 
65 18
 export const importUsernameFromLocalStorage = (): string | null => {
66 19
   try {
67
-    const data = localStorage.getItem(LOCAL_STORAGE_KEY_COLLAB);
20
+    const data = localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_COLLAB);
68 21
     if (data) {
69 22
       return JSON.parse(data).username;
70 23
     }
@@ -82,11 +35,11 @@ export const saveToLocalStorage = (
82 35
 ) => {
83 36
   try {
84 37
     localStorage.setItem(
85
-      LOCAL_STORAGE_KEY,
38
+      STORAGE_KEYS.LOCAL_STORAGE_ELEMENTS,
86 39
       JSON.stringify(elements.filter((element) => !element.isDeleted)),
87 40
     );
88 41
     localStorage.setItem(
89
-      LOCAL_STORAGE_KEY_STATE,
42
+      STORAGE_KEYS.LOCAL_STORAGE_APP_STATE,
90 43
       JSON.stringify(clearAppStateForLocalStorage(appState)),
91 44
     );
92 45
   } catch (error) {
@@ -100,8 +53,8 @@ export const importFromLocalStorage = () => {
100 53
   let savedState = null;
101 54
 
102 55
   try {
103
-    savedElements = localStorage.getItem(LOCAL_STORAGE_KEY);
104
-    savedState = localStorage.getItem(LOCAL_STORAGE_KEY_STATE);
56
+    savedElements = localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_ELEMENTS);
57
+    savedState = localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_APP_STATE);
105 58
   } catch (error) {
106 59
     // Unable to access localStorage
107 60
     console.error(error);

+ 14
- 6
src/tests/appState.test.tsx Переглянути файл

@@ -28,12 +28,20 @@ describe("appState", () => {
28 28
       expect(h.state.viewBackgroundColor).toBe("#F00");
29 29
     });
30 30
 
31
-    API.dropFile({
32
-      appState: {
33
-        viewBackgroundColor: "#000",
34
-      },
35
-      elements: [API.createElement({ type: "rectangle", id: "A" })],
36
-    });
31
+    API.drop(
32
+      new Blob(
33
+        [
34
+          JSON.stringify({
35
+            type: "excalidraw",
36
+            appState: {
37
+              viewBackgroundColor: "#000",
38
+            },
39
+            elements: [API.createElement({ type: "rectangle", id: "A" })],
40
+          }),
41
+        ],
42
+        { type: "application/json" },
43
+      ),
44
+    );
37 45
 
38 46
     await waitFor(() => {
39 47
       expect(h.elements).toEqual([expect.objectContaining({ id: "A" })]);

+ 12
- 1
src/tests/collab.test.tsx Переглянути файл

@@ -29,6 +29,17 @@ jest.mock("../data/firebase.ts", () => {
29 29
   };
30 30
 });
31 31
 
32
+jest.mock("socket.io-client", () => {
33
+  return () => {
34
+    return {
35
+      close: () => {},
36
+      on: () => {},
37
+      off: () => {},
38
+      emit: () => {},
39
+    };
40
+  };
41
+});
42
+
32 43
 describe("collaboration", () => {
33 44
   it("creating room should reset deleted elements", async () => {
34 45
     render(
@@ -50,7 +61,7 @@ describe("collaboration", () => {
50 61
       expect(API.getStateHistory().length).toBe(1);
51 62
     });
52 63
 
53
-    h.app.openPortal();
64
+    await h.app.openPortal();
54 65
     await waitFor(() => {
55 66
       expect(h.elements).toEqual([expect.objectContaining({ id: "A" })]);
56 67
       expect(API.getStateHistory().length).toBe(1);

+ 7
- 57
src/tests/export.test.tsx Переглянути файл

@@ -9,12 +9,6 @@ import {
9 9
 } from "../data/image";
10 10
 import { serializeAsJSON } from "../data/json";
11 11
 
12
-import fs from "fs";
13
-import util from "util";
14
-import path from "path";
15
-
16
-const readFile = util.promisify(fs.readFile);
17
-
18 12
 const { h } = window;
19 13
 
20 14
 const testElements = [
@@ -43,22 +37,18 @@ Object.defineProperty(window, "TextDecoder", {
43 37
   },
44 38
 });
45 39
 
46
-describe("appState", () => {
40
+describe("export", () => {
47 41
   beforeEach(() => {
48 42
     render(<App />);
49 43
   });
50 44
 
51 45
   it("export embedded png and reimport", async () => {
52
-    const pngBlob = new Blob(
53
-      [await readFile(path.resolve(__dirname, "./fixtures/smiley.png"))],
54
-      { type: "image/png" },
55
-    );
56
-
46
+    const pngBlob = await API.loadFile("./fixtures/smiley.png");
57 47
     const pngBlobEmbedded = await encodePngMetadata({
58 48
       blob: pngBlob,
59 49
       metadata: serializeAsJSON(testElements, h.state),
60 50
     });
61
-    API.dropFile(pngBlobEmbedded);
51
+    API.drop(pngBlobEmbedded);
62 52
 
63 53
     await waitFor(() => {
64 54
       expect(h.elements).toEqual([
@@ -78,17 +68,7 @@ describe("appState", () => {
78 68
   });
79 69
 
80 70
   it("import embedded png (legacy v1)", async () => {
81
-    const pngBlob = new Blob(
82
-      [
83
-        await readFile(
84
-          path.resolve(__dirname, "./fixtures/test_embedded_v1.png"),
85
-        ),
86
-      ],
87
-      { type: "image/png" },
88
-    );
89
-
90
-    API.dropFile(pngBlob);
91
-
71
+    API.drop(await API.loadFile("./fixtures/test_embedded_v1.png"));
92 72
     await waitFor(() => {
93 73
       expect(h.elements).toEqual([
94 74
         expect.objectContaining({ type: "text", text: "test" }),
@@ -97,17 +77,7 @@ describe("appState", () => {
97 77
   });
98 78
 
99 79
   it("import embedded png (v2)", async () => {
100
-    const pngBlob = new Blob(
101
-      [
102
-        await readFile(
103
-          path.resolve(__dirname, "./fixtures/smiley_embedded_v2.png"),
104
-        ),
105
-      ],
106
-      { type: "image/png" },
107
-    );
108
-
109
-    API.dropFile(pngBlob);
110
-
80
+    API.drop(await API.loadFile("./fixtures/smiley_embedded_v2.png"));
111 81
     await waitFor(() => {
112 82
       expect(h.elements).toEqual([
113 83
         expect.objectContaining({ type: "text", text: "😀" }),
@@ -116,17 +86,7 @@ describe("appState", () => {
116 86
   });
117 87
 
118 88
   it("import embedded svg (legacy v1)", async () => {
119
-    const svgBlob = new Blob(
120
-      [
121
-        await readFile(
122
-          path.resolve(__dirname, "./fixtures/test_embedded_v1.svg"),
123
-        ),
124
-      ],
125
-      { type: "image/svg+xml" },
126
-    );
127
-
128
-    API.dropFile(svgBlob);
129
-
89
+    API.drop(await API.loadFile("./fixtures/test_embedded_v1.svg"));
130 90
     await waitFor(() => {
131 91
       expect(h.elements).toEqual([
132 92
         expect.objectContaining({ type: "text", text: "test" }),
@@ -135,17 +95,7 @@ describe("appState", () => {
135 95
   });
136 96
 
137 97
   it("import embedded svg (v2)", async () => {
138
-    const svgBlob = new Blob(
139
-      [
140
-        await readFile(
141
-          path.resolve(__dirname, "./fixtures/smiley_embedded_v2.svg"),
142
-        ),
143
-      ],
144
-      { type: "image/svg+xml" },
145
-    );
146
-
147
-    API.dropFile(svgBlob);
148
-
98
+    API.drop(await API.loadFile("./fixtures/smiley_embedded_v2.svg"));
149 99
     await waitFor(() => {
150 100
       expect(h.elements).toEqual([
151 101
         expect.objectContaining({ type: "text", text: "😀" }),

+ 31
- 0
src/tests/fixtures/fixture_library.excalidrawlib Переглянути файл

@@ -0,0 +1,31 @@
1
+{
2
+  "type": "excalidrawlib",
3
+  "version": 1,
4
+  "library": [
5
+    [
6
+      {
7
+        "type": "rectangle",
8
+        "version": 38,
9
+        "versionNonce": 1046419680,
10
+        "isDeleted": false,
11
+        "id": "A",
12
+        "fillStyle": "hachure",
13
+        "strokeWidth": 1,
14
+        "strokeStyle": "solid",
15
+        "roughness": 1,
16
+        "opacity": 100,
17
+        "angle": 0,
18
+        "x": 21801,
19
+        "y": 719.5,
20
+        "strokeColor": "#c92a2a",
21
+        "backgroundColor": "#e64980",
22
+        "width": 50,
23
+        "height": 30,
24
+        "seed": 117297479,
25
+        "groupIds": [],
26
+        "strokeSharpness": "sharp",
27
+        "boundElementIds": []
28
+      }
29
+    ]
30
+  ]
31
+}

+ 42
- 19
src/tests/helpers/api.ts Переглянути файл

@@ -8,7 +8,12 @@ import { newElement, newTextElement, newLinearElement } from "../../element";
8 8
 import { DEFAULT_VERTICAL_ALIGN } from "../../constants";
9 9
 import { getDefaultAppState } from "../../appState";
10 10
 import { GlobalTestState, createEvent, fireEvent } from "../test-utils";
11
-import { ImportedDataState } from "../../data/types";
11
+import fs from "fs";
12
+import util from "util";
13
+import path from "path";
14
+import { getMimeType } from "../../data/blob";
15
+
16
+const readFile = util.promisify(fs.readFile);
12 17
 
13 18
 const { h } = window;
14 19
 
@@ -138,30 +143,48 @@ export class API {
138 143
     return element as any;
139 144
   };
140 145
 
141
-  static dropFile(data: ImportedDataState | Blob) {
146
+  static readFile = async <T extends "utf8" | null>(
147
+    filepath: string,
148
+    encoding?: T,
149
+  ): Promise<T extends "utf8" ? string : Buffer> => {
150
+    filepath = path.isAbsolute(filepath)
151
+      ? filepath
152
+      : path.resolve(path.join(__dirname, "../", filepath));
153
+    return readFile(filepath, { encoding }) as any;
154
+  };
155
+
156
+  static loadFile = async (filepath: string) => {
157
+    const { base, ext } = path.parse(filepath);
158
+    return new File([await API.readFile(filepath, null)], base, {
159
+      type: getMimeType(ext),
160
+    });
161
+  };
162
+
163
+  static drop = async (blob: Blob) => {
142 164
     const fileDropEvent = createEvent.drop(GlobalTestState.canvas);
143
-    const file =
144
-      data instanceof Blob
145
-        ? data
146
-        : new Blob(
147
-            [
148
-              JSON.stringify({
149
-                type: "excalidraw",
150
-                ...data,
151
-              }),
152
-            ],
153
-            {
154
-              type: "application/json",
155
-            },
156
-          );
165
+    const text = await new Promise<string>((resolve, reject) => {
166
+      try {
167
+        const reader = new FileReader();
168
+        reader.onload = () => {
169
+          resolve(reader.result as string);
170
+        };
171
+        reader.readAsText(blob);
172
+      } catch (error) {
173
+        reject(error);
174
+      }
175
+    });
176
+
157 177
     Object.defineProperty(fileDropEvent, "dataTransfer", {
158 178
       value: {
159
-        files: [file],
160
-        getData: (_type: string) => {
179
+        files: [blob],
180
+        getData: (type: string) => {
181
+          if (type === blob.type) {
182
+            return text;
183
+          }
161 184
           return "";
162 185
         },
163 186
       },
164 187
     });
165 188
     fireEvent(GlobalTestState.canvas, fileDropEvent);
166
-  }
189
+  };
167 190
 }

+ 15
- 7
src/tests/history.test.tsx Переглянути файл

@@ -78,13 +78,21 @@ describe("history", () => {
78 78
       expect(h.elements).toEqual([expect.objectContaining({ id: "A" })]),
79 79
     );
80 80
 
81
-    API.dropFile({
82
-      appState: {
83
-        ...getDefaultAppState(),
84
-        viewBackgroundColor: "#000",
85
-      },
86
-      elements: [API.createElement({ type: "rectangle", id: "B" })],
87
-    });
81
+    API.drop(
82
+      new Blob(
83
+        [
84
+          JSON.stringify({
85
+            type: "excalidraw",
86
+            appState: {
87
+              ...getDefaultAppState(),
88
+              viewBackgroundColor: "#000",
89
+            },
90
+            elements: [API.createElement({ type: "rectangle", id: "B" })],
91
+          }),
92
+        ],
93
+        { type: "application/json" },
94
+      ),
95
+    );
88 96
 
89 97
     await waitFor(() => expect(API.getStateHistory().length).toBe(2));
90 98
     expect(h.state.viewBackgroundColor).toBe("#000");

+ 43
- 0
src/tests/library.test.tsx Переглянути файл

@@ -0,0 +1,43 @@
1
+import React from "react";
2
+import { render, waitFor } from "./test-utils";
3
+import App from "../components/App";
4
+import { API } from "./helpers/api";
5
+import { MIME_TYPES } from "../constants";
6
+import { LibraryItem } from "../types";
7
+
8
+const { h } = window;
9
+
10
+describe("library", () => {
11
+  beforeEach(() => {
12
+    h.library.resetLibrary();
13
+    render(<App />);
14
+  });
15
+
16
+  it("import library via drag&drop", async () => {
17
+    expect(await h.library.loadLibrary()).toEqual([]);
18
+    await API.drop(
19
+      await API.loadFile("./fixtures/fixture_library.excalidrawlib"),
20
+    );
21
+    await waitFor(async () => {
22
+      expect(await h.library.loadLibrary()).toEqual([
23
+        [expect.objectContaining({ id: "A" })],
24
+      ]);
25
+    });
26
+  });
27
+
28
+  // NOTE: mocked to test logic, not actual drag&drop via UI
29
+  it("drop library item onto canvas", async () => {
30
+    expect(h.elements).toEqual([]);
31
+    const libraryItems: LibraryItem = JSON.parse(
32
+      await API.readFile("./fixtures/fixture_library.excalidrawlib", "utf8"),
33
+    ).library[0];
34
+    await API.drop(
35
+      new Blob([JSON.stringify(libraryItems)], {
36
+        type: MIME_TYPES.excalidrawlib,
37
+      }),
38
+    );
39
+    await waitFor(() => {
40
+      expect(h.elements).toEqual([expect.objectContaining({ id: "A_copy" })]);
41
+    });
42
+  });
43
+});

Завантаження…
Відмінити
Зберегти