history.test.tsx 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. import { assertSelectedElements, render } from "./test-utils";
  2. import ExcalidrawApp from "../excalidraw-app";
  3. import { Keyboard, Pointer, UI } from "./helpers/ui";
  4. import { API } from "./helpers/api";
  5. import { getDefaultAppState } from "../appState";
  6. import { waitFor } from "@testing-library/react";
  7. import { createUndoAction, createRedoAction } from "../actions/actionHistory";
  8. import { EXPORT_DATA_TYPES } from "../constants";
  9. const { h } = window;
  10. const mouse = new Pointer("mouse");
  11. describe("history", () => {
  12. it("initializing scene should end up with single history entry", async () => {
  13. await render(<ExcalidrawApp />, {
  14. localStorageData: {
  15. elements: [API.createElement({ type: "rectangle", id: "A" })],
  16. appState: {
  17. zenModeEnabled: true,
  18. },
  19. },
  20. });
  21. await waitFor(() => expect(h.state.zenModeEnabled).toBe(true));
  22. await waitFor(() =>
  23. expect(h.elements).toEqual([expect.objectContaining({ id: "A" })]),
  24. );
  25. const undoAction = createUndoAction(h.history);
  26. const redoAction = createRedoAction(h.history);
  27. h.app.actionManager.executeAction(undoAction);
  28. expect(h.elements).toEqual([
  29. expect.objectContaining({ id: "A", isDeleted: false }),
  30. ]);
  31. const rectangle = UI.createElement("rectangle");
  32. expect(h.elements).toEqual([
  33. expect.objectContaining({ id: "A" }),
  34. expect.objectContaining({ id: rectangle.id }),
  35. ]);
  36. h.app.actionManager.executeAction(undoAction);
  37. expect(h.elements).toEqual([
  38. expect.objectContaining({ id: "A", isDeleted: false }),
  39. expect.objectContaining({ id: rectangle.id, isDeleted: true }),
  40. ]);
  41. // noop
  42. h.app.actionManager.executeAction(undoAction);
  43. expect(h.elements).toEqual([
  44. expect.objectContaining({ id: "A", isDeleted: false }),
  45. expect.objectContaining({ id: rectangle.id, isDeleted: true }),
  46. ]);
  47. expect(API.getStateHistory().length).toBe(1);
  48. h.app.actionManager.executeAction(redoAction);
  49. expect(h.elements).toEqual([
  50. expect.objectContaining({ id: "A", isDeleted: false }),
  51. expect.objectContaining({ id: rectangle.id, isDeleted: false }),
  52. ]);
  53. expect(API.getStateHistory().length).toBe(2);
  54. });
  55. it("scene import via drag&drop should create new history entry", async () => {
  56. await render(<ExcalidrawApp />, {
  57. localStorageData: {
  58. elements: [API.createElement({ type: "rectangle", id: "A" })],
  59. appState: {
  60. viewBackgroundColor: "#FFF",
  61. },
  62. },
  63. });
  64. await waitFor(() => expect(h.state.viewBackgroundColor).toBe("#FFF"));
  65. await waitFor(() =>
  66. expect(h.elements).toEqual([expect.objectContaining({ id: "A" })]),
  67. );
  68. API.drop(
  69. new Blob(
  70. [
  71. JSON.stringify({
  72. type: EXPORT_DATA_TYPES.excalidraw,
  73. appState: {
  74. ...getDefaultAppState(),
  75. viewBackgroundColor: "#000",
  76. },
  77. elements: [API.createElement({ type: "rectangle", id: "B" })],
  78. }),
  79. ],
  80. { type: "application/json" },
  81. ),
  82. );
  83. await waitFor(() => expect(API.getStateHistory().length).toBe(2));
  84. expect(h.state.viewBackgroundColor).toBe("#000");
  85. expect(h.elements).toEqual([
  86. expect.objectContaining({ id: "B", isDeleted: false }),
  87. ]);
  88. const undoAction = createUndoAction(h.history);
  89. const redoAction = createRedoAction(h.history);
  90. h.app.actionManager.executeAction(undoAction);
  91. expect(h.elements).toEqual([
  92. expect.objectContaining({ id: "A", isDeleted: false }),
  93. expect.objectContaining({ id: "B", isDeleted: true }),
  94. ]);
  95. expect(h.state.viewBackgroundColor).toBe("#FFF");
  96. h.app.actionManager.executeAction(redoAction);
  97. expect(h.state.viewBackgroundColor).toBe("#000");
  98. expect(h.elements).toEqual([
  99. expect.objectContaining({ id: "B", isDeleted: false }),
  100. expect.objectContaining({ id: "A", isDeleted: true }),
  101. ]);
  102. });
  103. it("undo/redo works properly with groups", async () => {
  104. await render(<ExcalidrawApp />);
  105. const rect1 = API.createElement({ type: "rectangle", groupIds: ["A"] });
  106. const rect2 = API.createElement({ type: "rectangle", groupIds: ["A"] });
  107. h.elements = [rect1, rect2];
  108. mouse.select(rect1);
  109. assertSelectedElements([rect1, rect2]);
  110. expect(h.state.selectedGroupIds).toEqual({ A: true });
  111. Keyboard.withModifierKeys({ ctrl: true }, () => {
  112. Keyboard.keyPress("d");
  113. });
  114. expect(h.elements.length).toBe(4);
  115. assertSelectedElements([h.elements[2], h.elements[3]]);
  116. expect(h.state.selectedGroupIds).not.toEqual(
  117. expect.objectContaining({ A: true }),
  118. );
  119. Keyboard.withModifierKeys({ ctrl: true }, () => {
  120. Keyboard.keyPress("z");
  121. });
  122. expect(h.elements.length).toBe(4);
  123. expect(h.elements).toEqual([
  124. expect.objectContaining({ id: rect1.id, isDeleted: false }),
  125. expect.objectContaining({ id: rect2.id, isDeleted: false }),
  126. expect.objectContaining({ id: `${rect1.id}_copy`, isDeleted: true }),
  127. expect.objectContaining({ id: `${rect2.id}_copy`, isDeleted: true }),
  128. ]);
  129. expect(h.state.selectedGroupIds).toEqual({ A: true });
  130. Keyboard.withModifierKeys({ ctrl: true, shift: true }, () => {
  131. Keyboard.keyPress("z");
  132. });
  133. expect(h.elements.length).toBe(4);
  134. expect(h.elements).toEqual([
  135. expect.objectContaining({ id: rect1.id, isDeleted: false }),
  136. expect.objectContaining({ id: rect2.id, isDeleted: false }),
  137. expect.objectContaining({ id: `${rect1.id}_copy`, isDeleted: false }),
  138. expect.objectContaining({ id: `${rect2.id}_copy`, isDeleted: false }),
  139. ]);
  140. expect(h.state.selectedGroupIds).not.toEqual(
  141. expect.objectContaining({ A: true }),
  142. );
  143. // undo again, and duplicate once more
  144. // -------------------------------------------------------------------------
  145. Keyboard.withModifierKeys({ ctrl: true }, () => {
  146. Keyboard.keyPress("z");
  147. Keyboard.keyPress("d");
  148. });
  149. expect(h.elements.length).toBe(6);
  150. expect(h.elements).toEqual(
  151. expect.arrayContaining([
  152. expect.objectContaining({ id: rect1.id, isDeleted: false }),
  153. expect.objectContaining({ id: rect2.id, isDeleted: false }),
  154. expect.objectContaining({ id: `${rect1.id}_copy`, isDeleted: true }),
  155. expect.objectContaining({ id: `${rect2.id}_copy`, isDeleted: true }),
  156. expect.objectContaining({
  157. id: `${rect1.id}_copy_copy`,
  158. isDeleted: false,
  159. }),
  160. expect.objectContaining({
  161. id: `${rect2.id}_copy_copy`,
  162. isDeleted: false,
  163. }),
  164. ]),
  165. );
  166. expect(h.state.selectedGroupIds).not.toEqual(
  167. expect.objectContaining({ A: true }),
  168. );
  169. });
  170. });