浏览代码

Fix library dnd (#2314)

vanilla_orig
David Luzar 5 年前
父节点
当前提交
ba3f548b91
没有帐户链接到提交者的电子邮件

+ 1
- 0
.gitignore 查看文件

14
 yarn.lock
14
 yarn.lock
15
 .idea
15
 .idea
16
 dist/
16
 dist/
17
+.eslintcache

+ 3
- 3
src/actions/actionAddToLibrary.ts 查看文件

2
 import { getSelectedElements } from "../scene";
2
 import { getSelectedElements } from "../scene";
3
 import { getNonDeletedElements } from "../element";
3
 import { getNonDeletedElements } from "../element";
4
 import { deepCopyElement } from "../element/newElement";
4
 import { deepCopyElement } from "../element/newElement";
5
-import { loadLibrary, saveLibrary } from "../data/localStorage";
5
+import { Library } from "../data/library";
6
 
6
 
7
 export const actionAddToLibrary = register({
7
 export const actionAddToLibrary = register({
8
   name: "addToLibrary",
8
   name: "addToLibrary",
12
       appState,
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
     return false;
19
     return false;

+ 4
- 5
src/components/App.tsx 查看文件

145
   isBindingElementType,
145
   isBindingElementType,
146
 } from "../element/typeChecks";
146
 } from "../element/typeChecks";
147
 import { actionFinalize, actionDeleteSelected } from "../actions";
147
 import { actionFinalize, actionDeleteSelected } from "../actions";
148
-import { loadLibrary } from "../data/localStorage";
149
 
148
 
150
 import throttle from "lodash.throttle";
149
 import throttle from "lodash.throttle";
151
 import { LinearElementEditor } from "../element/linearElementEditor";
150
 import { LinearElementEditor } from "../element/linearElementEditor";
1266
     history.resumeRecording();
1265
     history.resumeRecording();
1267
     this.scene.replaceAllElements(this.scene.getElements());
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
   closePortal = () => {
1271
   closePortal = () => {
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
     if (libraryShapes !== "") {
3732
     if (libraryShapes !== "") {
3734
       this.addElementsFromPasteOrLibrary(
3733
       this.addElementsFromPasteOrLibrary(
3735
         JSON.parse(libraryShapes),
3734
         JSON.parse(libraryShapes),
4040
       setState: React.Component<any, AppState>["setState"];
4039
       setState: React.Component<any, AppState>["setState"];
4041
       history: SceneHistory;
4040
       history: SceneHistory;
4042
       app: InstanceType<typeof App>;
4041
       app: InstanceType<typeof App>;
4043
-      library: ReturnType<typeof loadLibrary>;
4042
+      library: typeof Library;
4044
     };
4043
     };
4045
   }
4044
   }
4046
 }
4045
 }
4064
       get: () => history,
4063
       get: () => history,
4065
     },
4064
     },
4066
     library: {
4065
     library: {
4067
-      get: () => loadLibrary(),
4066
+      value: Library,
4068
     },
4067
     },
4069
   });
4068
   });
4070
 }
4069
 }

+ 6
- 6
src/components/LayerUI.tsx 查看文件

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

+ 7
- 0
src/constants.ts 查看文件

89
   excalidraw: "application/vnd.excalidraw+json",
89
   excalidraw: "application/vnd.excalidraw+json",
90
   excalidrawlib: "application/vnd.excalidrawlib+json",
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
   return contents;
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
   if (/\.(excalidraw|json)$/.test(name)) {
68
   if (/\.(excalidraw|json)$/.test(name)) {
64
     return "application/json";
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
   return "";
77
   return "";
67
 };
78
 };

+ 1
- 2
src/data/json.ts 查看文件

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

+ 52
- 3
src/data/library.ts 查看文件

1
 import { loadLibraryFromBlob } from "./blob";
1
 import { loadLibraryFromBlob } from "./blob";
2
 import { LibraryItems, LibraryItem } from "../types";
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
 export class Library {
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
   /** imports library (currently merges, removing duplicates) */
14
   /** imports library (currently merges, removing duplicates) */
7
   static async importLibrary(blob: Blob) {
15
   static async importLibrary(blob: Blob) {
8
     const libraryFile = await loadLibraryFromBlob(blob);
16
     const libraryFile = await loadLibraryFromBlob(blob);
34
       });
42
       });
35
     };
43
     };
36
 
44
 
37
-    const existingLibraryItems = await loadLibrary();
45
+    const existingLibraryItems = await Library.loadLibrary();
38
     const filtered = libraryFile.library!.filter((libraryItem) =>
46
     const filtered = libraryFile.library!.filter((libraryItem) =>
39
       isUniqueitem(existingLibraryItems, libraryItem),
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
 import { ExcalidrawElement } from "../element/types";
1
 import { ExcalidrawElement } from "../element/types";
2
-import { AppState, LibraryItems } from "../types";
2
+import { AppState } from "../types";
3
 import { clearAppStateForLocalStorage, getDefaultAppState } from "../appState";
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
 export const saveUsernameToLocalStorage = (username: string) => {
6
 export const saveUsernameToLocalStorage = (username: string) => {
54
   try {
7
   try {
55
     localStorage.setItem(
8
     localStorage.setItem(
56
-      LOCAL_STORAGE_KEY_COLLAB,
9
+      STORAGE_KEYS.LOCAL_STORAGE_COLLAB,
57
       JSON.stringify({ username }),
10
       JSON.stringify({ username }),
58
     );
11
     );
59
   } catch (error) {
12
   } catch (error) {
64
 
17
 
65
 export const importUsernameFromLocalStorage = (): string | null => {
18
 export const importUsernameFromLocalStorage = (): string | null => {
66
   try {
19
   try {
67
-    const data = localStorage.getItem(LOCAL_STORAGE_KEY_COLLAB);
20
+    const data = localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_COLLAB);
68
     if (data) {
21
     if (data) {
69
       return JSON.parse(data).username;
22
       return JSON.parse(data).username;
70
     }
23
     }
82
 ) => {
35
 ) => {
83
   try {
36
   try {
84
     localStorage.setItem(
37
     localStorage.setItem(
85
-      LOCAL_STORAGE_KEY,
38
+      STORAGE_KEYS.LOCAL_STORAGE_ELEMENTS,
86
       JSON.stringify(elements.filter((element) => !element.isDeleted)),
39
       JSON.stringify(elements.filter((element) => !element.isDeleted)),
87
     );
40
     );
88
     localStorage.setItem(
41
     localStorage.setItem(
89
-      LOCAL_STORAGE_KEY_STATE,
42
+      STORAGE_KEYS.LOCAL_STORAGE_APP_STATE,
90
       JSON.stringify(clearAppStateForLocalStorage(appState)),
43
       JSON.stringify(clearAppStateForLocalStorage(appState)),
91
     );
44
     );
92
   } catch (error) {
45
   } catch (error) {
100
   let savedState = null;
53
   let savedState = null;
101
 
54
 
102
   try {
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
   } catch (error) {
58
   } catch (error) {
106
     // Unable to access localStorage
59
     // Unable to access localStorage
107
     console.error(error);
60
     console.error(error);

+ 14
- 6
src/tests/appState.test.tsx 查看文件

28
       expect(h.state.viewBackgroundColor).toBe("#F00");
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
     await waitFor(() => {
46
     await waitFor(() => {
39
       expect(h.elements).toEqual([expect.objectContaining({ id: "A" })]);
47
       expect(h.elements).toEqual([expect.objectContaining({ id: "A" })]);

+ 12
- 1
src/tests/collab.test.tsx 查看文件

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
 describe("collaboration", () => {
43
 describe("collaboration", () => {
33
   it("creating room should reset deleted elements", async () => {
44
   it("creating room should reset deleted elements", async () => {
34
     render(
45
     render(
50
       expect(API.getStateHistory().length).toBe(1);
61
       expect(API.getStateHistory().length).toBe(1);
51
     });
62
     });
52
 
63
 
53
-    h.app.openPortal();
64
+    await h.app.openPortal();
54
     await waitFor(() => {
65
     await waitFor(() => {
55
       expect(h.elements).toEqual([expect.objectContaining({ id: "A" })]);
66
       expect(h.elements).toEqual([expect.objectContaining({ id: "A" })]);
56
       expect(API.getStateHistory().length).toBe(1);
67
       expect(API.getStateHistory().length).toBe(1);

+ 7
- 57
src/tests/export.test.tsx 查看文件

9
 } from "../data/image";
9
 } from "../data/image";
10
 import { serializeAsJSON } from "../data/json";
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
 const { h } = window;
12
 const { h } = window;
19
 
13
 
20
 const testElements = [
14
 const testElements = [
43
   },
37
   },
44
 });
38
 });
45
 
39
 
46
-describe("appState", () => {
40
+describe("export", () => {
47
   beforeEach(() => {
41
   beforeEach(() => {
48
     render(<App />);
42
     render(<App />);
49
   });
43
   });
50
 
44
 
51
   it("export embedded png and reimport", async () => {
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
     const pngBlobEmbedded = await encodePngMetadata({
47
     const pngBlobEmbedded = await encodePngMetadata({
58
       blob: pngBlob,
48
       blob: pngBlob,
59
       metadata: serializeAsJSON(testElements, h.state),
49
       metadata: serializeAsJSON(testElements, h.state),
60
     });
50
     });
61
-    API.dropFile(pngBlobEmbedded);
51
+    API.drop(pngBlobEmbedded);
62
 
52
 
63
     await waitFor(() => {
53
     await waitFor(() => {
64
       expect(h.elements).toEqual([
54
       expect(h.elements).toEqual([
78
   });
68
   });
79
 
69
 
80
   it("import embedded png (legacy v1)", async () => {
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
     await waitFor(() => {
72
     await waitFor(() => {
93
       expect(h.elements).toEqual([
73
       expect(h.elements).toEqual([
94
         expect.objectContaining({ type: "text", text: "test" }),
74
         expect.objectContaining({ type: "text", text: "test" }),
97
   });
77
   });
98
 
78
 
99
   it("import embedded png (v2)", async () => {
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
     await waitFor(() => {
81
     await waitFor(() => {
112
       expect(h.elements).toEqual([
82
       expect(h.elements).toEqual([
113
         expect.objectContaining({ type: "text", text: "😀" }),
83
         expect.objectContaining({ type: "text", text: "😀" }),
116
   });
86
   });
117
 
87
 
118
   it("import embedded svg (legacy v1)", async () => {
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
     await waitFor(() => {
90
     await waitFor(() => {
131
       expect(h.elements).toEqual([
91
       expect(h.elements).toEqual([
132
         expect.objectContaining({ type: "text", text: "test" }),
92
         expect.objectContaining({ type: "text", text: "test" }),
135
   });
95
   });
136
 
96
 
137
   it("import embedded svg (v2)", async () => {
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
     await waitFor(() => {
99
     await waitFor(() => {
150
       expect(h.elements).toEqual([
100
       expect(h.elements).toEqual([
151
         expect.objectContaining({ type: "text", text: "😀" }),
101
         expect.objectContaining({ type: "text", text: "😀" }),

+ 31
- 0
src/tests/fixtures/fixture_library.excalidrawlib 查看文件

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
 import { DEFAULT_VERTICAL_ALIGN } from "../../constants";
8
 import { DEFAULT_VERTICAL_ALIGN } from "../../constants";
9
 import { getDefaultAppState } from "../../appState";
9
 import { getDefaultAppState } from "../../appState";
10
 import { GlobalTestState, createEvent, fireEvent } from "../test-utils";
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
 const { h } = window;
18
 const { h } = window;
14
 
19
 
138
     return element as any;
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
     const fileDropEvent = createEvent.drop(GlobalTestState.canvas);
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
     Object.defineProperty(fileDropEvent, "dataTransfer", {
177
     Object.defineProperty(fileDropEvent, "dataTransfer", {
158
       value: {
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
           return "";
184
           return "";
162
         },
185
         },
163
       },
186
       },
164
     });
187
     });
165
     fireEvent(GlobalTestState.canvas, fileDropEvent);
188
     fireEvent(GlobalTestState.canvas, fileDropEvent);
166
-  }
189
+  };
167
 }
190
 }

+ 15
- 7
src/tests/history.test.tsx 查看文件

78
       expect(h.elements).toEqual([expect.objectContaining({ id: "A" })]),
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
     await waitFor(() => expect(API.getStateHistory().length).toBe(2));
97
     await waitFor(() => expect(API.getStateHistory().length).toBe(2));
90
     expect(h.state.viewBackgroundColor).toBe("#000");
98
     expect(h.state.viewBackgroundColor).toBe("#000");

+ 43
- 0
src/tests/library.test.tsx 查看文件

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
+});

正在加载...
取消
保存