duplicate.test.tsx 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852
  1. import { pointFrom } from "@excalidraw/math";
  2. import {
  3. FONT_FAMILY,
  4. ORIG_ID,
  5. ROUNDNESS,
  6. isPrimitive,
  7. } from "@excalidraw/common";
  8. import { Excalidraw } from "@excalidraw/excalidraw";
  9. import { actionDuplicateSelection } from "@excalidraw/excalidraw/actions";
  10. import { API } from "@excalidraw/excalidraw/tests/helpers/api";
  11. import { UI, Keyboard, Pointer } from "@excalidraw/excalidraw/tests/helpers/ui";
  12. import {
  13. act,
  14. assertElements,
  15. getCloneByOrigId,
  16. render,
  17. } from "@excalidraw/excalidraw/tests/test-utils";
  18. import type { LocalPoint } from "@excalidraw/math";
  19. import { mutateElement } from "../src/mutateElement";
  20. import { duplicateElement, duplicateElements } from "../src/duplicate";
  21. import type { ExcalidrawLinearElement } from "../src/types";
  22. const { h } = window;
  23. const mouse = new Pointer("mouse");
  24. const assertCloneObjects = (source: any, clone: any) => {
  25. for (const key in clone) {
  26. if (clone.hasOwnProperty(key) && !isPrimitive(clone[key])) {
  27. expect(clone[key]).not.toBe(source[key]);
  28. if (source[key]) {
  29. assertCloneObjects(source[key], clone[key]);
  30. }
  31. }
  32. }
  33. };
  34. describe("duplicating single elements", () => {
  35. it("clones arrow element", () => {
  36. const element = API.createElement({
  37. type: "arrow",
  38. x: 0,
  39. y: 0,
  40. strokeColor: "#000000",
  41. backgroundColor: "transparent",
  42. fillStyle: "hachure",
  43. strokeWidth: 1,
  44. strokeStyle: "solid",
  45. roundness: { type: ROUNDNESS.PROPORTIONAL_RADIUS },
  46. roughness: 1,
  47. opacity: 100,
  48. });
  49. // @ts-ignore
  50. element.__proto__ = { hello: "world" };
  51. mutateElement(element, {
  52. points: [pointFrom<LocalPoint>(1, 2), pointFrom<LocalPoint>(3, 4)],
  53. });
  54. const copy = duplicateElement(null, new Map(), element, true);
  55. assertCloneObjects(element, copy);
  56. // assert we clone the object's prototype
  57. // @ts-ignore
  58. expect(copy.__proto__).toEqual({ hello: "world" });
  59. expect(copy.hasOwnProperty("hello")).toBe(false);
  60. expect(copy.points).not.toBe(element.points);
  61. expect(copy).not.toHaveProperty("shape");
  62. expect(copy.id).not.toBe(element.id);
  63. expect(typeof copy.id).toBe("string");
  64. expect(copy.seed).not.toBe(element.seed);
  65. expect(typeof copy.seed).toBe("number");
  66. expect(copy).toEqual({
  67. ...element,
  68. id: copy.id,
  69. seed: copy.seed,
  70. version: copy.version,
  71. versionNonce: copy.versionNonce,
  72. });
  73. });
  74. it("clones text element", () => {
  75. const element = API.createElement({
  76. type: "text",
  77. x: 0,
  78. y: 0,
  79. strokeColor: "#000000",
  80. backgroundColor: "transparent",
  81. fillStyle: "hachure",
  82. strokeWidth: 1,
  83. strokeStyle: "solid",
  84. roundness: null,
  85. roughness: 1,
  86. opacity: 100,
  87. text: "hello",
  88. fontSize: 20,
  89. fontFamily: FONT_FAMILY.Virgil,
  90. textAlign: "left",
  91. verticalAlign: "top",
  92. });
  93. const copy = duplicateElement(null, new Map(), element);
  94. assertCloneObjects(element, copy);
  95. expect(copy).not.toHaveProperty("points");
  96. expect(copy).not.toHaveProperty("shape");
  97. expect(copy.id).not.toBe(element.id);
  98. expect(typeof copy.id).toBe("string");
  99. expect(typeof copy.seed).toBe("number");
  100. });
  101. });
  102. describe("duplicating multiple elements", () => {
  103. it("duplicateElements should clone bindings", () => {
  104. const rectangle1 = API.createElement({
  105. type: "rectangle",
  106. id: "rectangle1",
  107. boundElements: [
  108. { id: "arrow1", type: "arrow" },
  109. { id: "arrow2", type: "arrow" },
  110. { id: "text1", type: "text" },
  111. ],
  112. });
  113. const text1 = API.createElement({
  114. type: "text",
  115. id: "text1",
  116. containerId: "rectangle1",
  117. });
  118. const arrow1 = API.createElement({
  119. type: "arrow",
  120. id: "arrow1",
  121. startBinding: {
  122. elementId: "rectangle1",
  123. focus: 0.2,
  124. gap: 7,
  125. fixedPoint: [0.5, 1],
  126. },
  127. });
  128. const arrow2 = API.createElement({
  129. type: "arrow",
  130. id: "arrow2",
  131. endBinding: {
  132. elementId: "rectangle1",
  133. focus: 0.2,
  134. gap: 7,
  135. fixedPoint: [0.5, 1],
  136. },
  137. boundElements: [{ id: "text2", type: "text" }],
  138. });
  139. const text2 = API.createElement({
  140. type: "text",
  141. id: "text2",
  142. containerId: "arrow2",
  143. });
  144. // -------------------------------------------------------------------------
  145. const origElements = [rectangle1, text1, arrow1, arrow2, text2] as const;
  146. const { duplicatedElements } = duplicateElements({
  147. type: "everything",
  148. elements: origElements,
  149. });
  150. // generic id in-equality checks
  151. // --------------------------------------------------------------------------
  152. expect(origElements.map((e) => e.type)).toEqual(
  153. duplicatedElements.map((e) => e.type),
  154. );
  155. origElements.forEach((origElement, idx) => {
  156. const clonedElement = duplicatedElements[idx];
  157. expect(origElement).toEqual(
  158. expect.objectContaining({
  159. id: expect.not.stringMatching(clonedElement.id),
  160. type: clonedElement.type,
  161. }),
  162. );
  163. if ("containerId" in origElement) {
  164. expect(origElement.containerId).not.toBe(
  165. (clonedElement as any).containerId,
  166. );
  167. }
  168. if ("endBinding" in origElement) {
  169. if (origElement.endBinding) {
  170. expect(origElement.endBinding.elementId).not.toBe(
  171. (clonedElement as any).endBinding?.elementId,
  172. );
  173. } else {
  174. expect((clonedElement as any).endBinding).toBeNull();
  175. }
  176. }
  177. if ("startBinding" in origElement) {
  178. if (origElement.startBinding) {
  179. expect(origElement.startBinding.elementId).not.toBe(
  180. (clonedElement as any).startBinding?.elementId,
  181. );
  182. } else {
  183. expect((clonedElement as any).startBinding).toBeNull();
  184. }
  185. }
  186. });
  187. // --------------------------------------------------------------------------
  188. const clonedArrows = duplicatedElements.filter(
  189. (e) => e.type === "arrow",
  190. ) as ExcalidrawLinearElement[];
  191. const [clonedRectangle, clonedText1, , clonedArrow2, clonedArrowLabel] =
  192. duplicatedElements as any as typeof origElements;
  193. expect(clonedText1.containerId).toBe(clonedRectangle.id);
  194. expect(
  195. clonedRectangle.boundElements!.find((e) => e.id === clonedText1.id),
  196. ).toEqual(
  197. expect.objectContaining({
  198. id: clonedText1.id,
  199. type: clonedText1.type,
  200. }),
  201. );
  202. expect(clonedRectangle.type).toBe("rectangle");
  203. clonedArrows.forEach((arrow) => {
  204. expect(
  205. clonedRectangle.boundElements!.find((e) => e.id === arrow.id),
  206. ).toEqual(
  207. expect.objectContaining({
  208. id: arrow.id,
  209. type: arrow.type,
  210. }),
  211. );
  212. if (arrow.endBinding) {
  213. expect(arrow.endBinding.elementId).toBe(clonedRectangle.id);
  214. }
  215. if (arrow.startBinding) {
  216. expect(arrow.startBinding.elementId).toBe(clonedRectangle.id);
  217. }
  218. });
  219. expect(clonedArrow2.boundElements).toEqual([
  220. { type: "text", id: clonedArrowLabel.id },
  221. ]);
  222. expect(clonedArrowLabel.containerId).toBe(clonedArrow2.id);
  223. });
  224. it("should remove id references of elements that aren't found", () => {
  225. const rectangle1 = API.createElement({
  226. type: "rectangle",
  227. id: "rectangle1",
  228. boundElements: [
  229. // should keep
  230. { id: "arrow1", type: "arrow" },
  231. // should drop
  232. { id: "arrow-not-exists", type: "arrow" },
  233. // should drop
  234. { id: "text-not-exists", type: "text" },
  235. ],
  236. });
  237. const arrow1 = API.createElement({
  238. type: "arrow",
  239. id: "arrow1",
  240. startBinding: {
  241. elementId: "rectangle1",
  242. focus: 0.2,
  243. gap: 7,
  244. fixedPoint: [0.5, 1],
  245. },
  246. });
  247. const text1 = API.createElement({
  248. type: "text",
  249. id: "text1",
  250. containerId: "rectangle-not-exists",
  251. });
  252. const arrow2 = API.createElement({
  253. type: "arrow",
  254. id: "arrow2",
  255. startBinding: {
  256. elementId: "rectangle1",
  257. focus: 0.2,
  258. gap: 7,
  259. fixedPoint: [0.5, 1],
  260. },
  261. endBinding: {
  262. elementId: "rectangle-not-exists",
  263. focus: 0.2,
  264. gap: 7,
  265. fixedPoint: [0.5, 1],
  266. },
  267. });
  268. const arrow3 = API.createElement({
  269. type: "arrow",
  270. id: "arrow3",
  271. startBinding: {
  272. elementId: "rectangle-not-exists",
  273. focus: 0.2,
  274. gap: 7,
  275. fixedPoint: [0.5, 1],
  276. },
  277. endBinding: {
  278. elementId: "rectangle1",
  279. focus: 0.2,
  280. gap: 7,
  281. fixedPoint: [0.5, 1],
  282. },
  283. });
  284. // -------------------------------------------------------------------------
  285. const origElements = [rectangle1, text1, arrow1, arrow2, arrow3] as const;
  286. const duplicatedElements = duplicateElements({
  287. type: "everything",
  288. elements: origElements,
  289. }).duplicatedElements as any as typeof origElements;
  290. const [
  291. clonedRectangle,
  292. clonedText1,
  293. clonedArrow1,
  294. clonedArrow2,
  295. clonedArrow3,
  296. ] = duplicatedElements;
  297. expect(clonedRectangle.boundElements).toEqual([
  298. { id: clonedArrow1.id, type: "arrow" },
  299. ]);
  300. expect(clonedText1.containerId).toBe(null);
  301. expect(clonedArrow2.startBinding).toEqual({
  302. ...arrow2.startBinding,
  303. elementId: clonedRectangle.id,
  304. });
  305. expect(clonedArrow2.endBinding).toBe(null);
  306. expect(clonedArrow3.startBinding).toBe(null);
  307. expect(clonedArrow3.endBinding).toEqual({
  308. ...arrow3.endBinding,
  309. elementId: clonedRectangle.id,
  310. });
  311. });
  312. describe("should duplicate all group ids", () => {
  313. it("should regenerate all group ids and keep them consistent across elements", () => {
  314. const rectangle1 = API.createElement({
  315. type: "rectangle",
  316. groupIds: ["g1"],
  317. });
  318. const rectangle2 = API.createElement({
  319. type: "rectangle",
  320. groupIds: ["g2", "g1"],
  321. });
  322. const rectangle3 = API.createElement({
  323. type: "rectangle",
  324. groupIds: ["g2", "g1"],
  325. });
  326. const origElements = [rectangle1, rectangle2, rectangle3] as const;
  327. const { duplicatedElements } = duplicateElements({
  328. type: "everything",
  329. elements: origElements,
  330. });
  331. const [clonedRectangle1, clonedRectangle2, clonedRectangle3] =
  332. duplicatedElements;
  333. expect(rectangle1.groupIds[0]).not.toBe(clonedRectangle1.groupIds[0]);
  334. expect(rectangle2.groupIds[0]).not.toBe(clonedRectangle2.groupIds[0]);
  335. expect(rectangle2.groupIds[1]).not.toBe(clonedRectangle2.groupIds[1]);
  336. expect(clonedRectangle1.groupIds[0]).toBe(clonedRectangle2.groupIds[1]);
  337. expect(clonedRectangle2.groupIds[0]).toBe(clonedRectangle3.groupIds[0]);
  338. expect(clonedRectangle2.groupIds[1]).toBe(clonedRectangle3.groupIds[1]);
  339. });
  340. it("should keep and regenerate ids of groups even if invalid", () => {
  341. // lone element shouldn't be able to be grouped with itself,
  342. // but hard to check against in a performant way so we ignore it
  343. const rectangle1 = API.createElement({
  344. type: "rectangle",
  345. groupIds: ["g1"],
  346. });
  347. const {
  348. duplicatedElements: [clonedRectangle1],
  349. } = duplicateElements({ type: "everything", elements: [rectangle1] });
  350. expect(typeof clonedRectangle1.groupIds[0]).toBe("string");
  351. expect(rectangle1.groupIds[0]).not.toBe(clonedRectangle1.groupIds[0]);
  352. });
  353. });
  354. });
  355. describe("group-related duplication", () => {
  356. beforeEach(async () => {
  357. await render(<Excalidraw />);
  358. });
  359. it("action-duplicating within group", async () => {
  360. const rectangle1 = API.createElement({
  361. type: "rectangle",
  362. x: 0,
  363. y: 0,
  364. groupIds: ["group1"],
  365. });
  366. const rectangle2 = API.createElement({
  367. type: "rectangle",
  368. x: 10,
  369. y: 10,
  370. groupIds: ["group1"],
  371. });
  372. API.setElements([rectangle1, rectangle2]);
  373. API.setSelectedElements([rectangle2], "group1");
  374. act(() => {
  375. h.app.actionManager.executeAction(actionDuplicateSelection);
  376. });
  377. assertElements(h.elements, [
  378. { id: rectangle1.id },
  379. { id: rectangle2.id },
  380. { [ORIG_ID]: rectangle2.id, selected: true, groupIds: ["group1"] },
  381. ]);
  382. expect(h.state.editingGroupId).toBe("group1");
  383. });
  384. it("alt-duplicating within group", async () => {
  385. const rectangle1 = API.createElement({
  386. type: "rectangle",
  387. x: 0,
  388. y: 0,
  389. groupIds: ["group1"],
  390. });
  391. const rectangle2 = API.createElement({
  392. type: "rectangle",
  393. x: 10,
  394. y: 10,
  395. groupIds: ["group1"],
  396. });
  397. API.setElements([rectangle1, rectangle2]);
  398. API.setSelectedElements([rectangle2], "group1");
  399. Keyboard.withModifierKeys({ alt: true }, () => {
  400. mouse.down(rectangle2.x + 5, rectangle2.y + 5);
  401. mouse.up(rectangle2.x + 50, rectangle2.y + 50);
  402. });
  403. assertElements(h.elements, [
  404. { id: rectangle1.id },
  405. { id: rectangle2.id },
  406. { [ORIG_ID]: rectangle2.id, selected: true, groupIds: ["group1"] },
  407. ]);
  408. expect(h.state.editingGroupId).toBe("group1");
  409. });
  410. it("alt-duplicating within group away outside frame", () => {
  411. const frame = API.createElement({
  412. type: "frame",
  413. x: 0,
  414. y: 0,
  415. width: 100,
  416. height: 100,
  417. });
  418. const rectangle1 = API.createElement({
  419. type: "rectangle",
  420. x: 0,
  421. y: 0,
  422. width: 50,
  423. height: 50,
  424. groupIds: ["group1"],
  425. frameId: frame.id,
  426. });
  427. const rectangle2 = API.createElement({
  428. type: "rectangle",
  429. x: 10,
  430. y: 10,
  431. width: 50,
  432. height: 50,
  433. groupIds: ["group1"],
  434. frameId: frame.id,
  435. });
  436. API.setElements([frame, rectangle1, rectangle2]);
  437. API.setSelectedElements([rectangle2], "group1");
  438. Keyboard.withModifierKeys({ alt: true }, () => {
  439. mouse.down(rectangle2.x + 5, rectangle2.y + 5);
  440. mouse.up(frame.x + frame.width + 50, frame.y + frame.height + 50);
  441. });
  442. // console.log(h.elements);
  443. assertElements(h.elements, [
  444. { id: frame.id },
  445. { id: rectangle1.id, frameId: frame.id },
  446. { id: rectangle2.id, frameId: frame.id },
  447. { [ORIG_ID]: rectangle2.id, selected: true, groupIds: [], frameId: null },
  448. ]);
  449. expect(h.state.editingGroupId).toBe(null);
  450. });
  451. });
  452. describe("duplication z-order", () => {
  453. beforeEach(async () => {
  454. await render(<Excalidraw />);
  455. });
  456. it("duplication z order with Cmd+D for the lowest z-ordered element should be +1 for the clone", () => {
  457. const rectangle1 = API.createElement({
  458. type: "rectangle",
  459. x: 0,
  460. y: 0,
  461. });
  462. const rectangle2 = API.createElement({
  463. type: "rectangle",
  464. x: 10,
  465. y: 10,
  466. });
  467. const rectangle3 = API.createElement({
  468. type: "rectangle",
  469. x: 20,
  470. y: 20,
  471. });
  472. API.setElements([rectangle1, rectangle2, rectangle3]);
  473. API.setSelectedElements([rectangle1]);
  474. act(() => {
  475. h.app.actionManager.executeAction(actionDuplicateSelection);
  476. });
  477. assertElements(h.elements, [
  478. { id: rectangle1.id },
  479. { [ORIG_ID]: rectangle1.id, selected: true },
  480. { id: rectangle2.id },
  481. { id: rectangle3.id },
  482. ]);
  483. });
  484. it("duplication z order with Cmd+D for the highest z-ordered element should be +1 for the clone", () => {
  485. const rectangle1 = API.createElement({
  486. type: "rectangle",
  487. x: 0,
  488. y: 0,
  489. });
  490. const rectangle2 = API.createElement({
  491. type: "rectangle",
  492. x: 10,
  493. y: 10,
  494. });
  495. const rectangle3 = API.createElement({
  496. type: "rectangle",
  497. x: 20,
  498. y: 20,
  499. });
  500. API.setElements([rectangle1, rectangle2, rectangle3]);
  501. API.setSelectedElements([rectangle3]);
  502. act(() => {
  503. h.app.actionManager.executeAction(actionDuplicateSelection);
  504. });
  505. assertElements(h.elements, [
  506. { id: rectangle1.id },
  507. { id: rectangle2.id },
  508. { id: rectangle3.id },
  509. { [ORIG_ID]: rectangle3.id, selected: true },
  510. ]);
  511. });
  512. it("duplication z order with alt+drag for the lowest z-ordered element should be +1 for the clone", () => {
  513. const rectangle1 = API.createElement({
  514. type: "rectangle",
  515. x: 0,
  516. y: 0,
  517. });
  518. const rectangle2 = API.createElement({
  519. type: "rectangle",
  520. x: 10,
  521. y: 10,
  522. });
  523. const rectangle3 = API.createElement({
  524. type: "rectangle",
  525. x: 20,
  526. y: 20,
  527. });
  528. API.setElements([rectangle1, rectangle2, rectangle3]);
  529. mouse.select(rectangle1);
  530. Keyboard.withModifierKeys({ alt: true }, () => {
  531. mouse.down(rectangle1.x + 5, rectangle1.y + 5);
  532. mouse.up(rectangle1.x + 5, rectangle1.y + 5);
  533. });
  534. assertElements(h.elements, [
  535. { id: rectangle1.id },
  536. { [ORIG_ID]: rectangle1.id, selected: true },
  537. { id: rectangle2.id },
  538. { id: rectangle3.id },
  539. ]);
  540. });
  541. it("duplication z order with alt+drag for the highest z-ordered element should be +1 for the clone", () => {
  542. const rectangle1 = API.createElement({
  543. type: "rectangle",
  544. x: 0,
  545. y: 0,
  546. });
  547. const rectangle2 = API.createElement({
  548. type: "rectangle",
  549. x: 10,
  550. y: 10,
  551. });
  552. const rectangle3 = API.createElement({
  553. type: "rectangle",
  554. x: 20,
  555. y: 20,
  556. });
  557. API.setElements([rectangle1, rectangle2, rectangle3]);
  558. mouse.select(rectangle3);
  559. Keyboard.withModifierKeys({ alt: true }, () => {
  560. mouse.down(rectangle3.x + 5, rectangle3.y + 5);
  561. mouse.up(rectangle3.x + 5, rectangle3.y + 5);
  562. });
  563. assertElements(h.elements, [
  564. { id: rectangle1.id },
  565. { id: rectangle2.id },
  566. { id: rectangle3.id },
  567. { [ORIG_ID]: rectangle3.id, selected: true },
  568. ]);
  569. });
  570. it("duplication z order with alt+drag for the lowest z-ordered element should be +1 for the clone", () => {
  571. const rectangle1 = API.createElement({
  572. type: "rectangle",
  573. x: 0,
  574. y: 0,
  575. });
  576. const rectangle2 = API.createElement({
  577. type: "rectangle",
  578. x: 10,
  579. y: 10,
  580. });
  581. const rectangle3 = API.createElement({
  582. type: "rectangle",
  583. x: 20,
  584. y: 20,
  585. });
  586. API.setElements([rectangle1, rectangle2, rectangle3]);
  587. mouse.select(rectangle1);
  588. Keyboard.withModifierKeys({ alt: true }, () => {
  589. mouse.down(rectangle1.x + 5, rectangle1.y + 5);
  590. mouse.up(rectangle1.x + 5, rectangle1.y + 5);
  591. });
  592. assertElements(h.elements, [
  593. { id: rectangle1.id },
  594. { [ORIG_ID]: rectangle1.id, selected: true },
  595. { id: rectangle2.id },
  596. { id: rectangle3.id },
  597. ]);
  598. });
  599. it("duplication z order with alt+drag with grouped elements should consider the group together when determining z-index", () => {
  600. const rectangle1 = API.createElement({
  601. type: "rectangle",
  602. x: 0,
  603. y: 0,
  604. groupIds: ["group1"],
  605. });
  606. const rectangle2 = API.createElement({
  607. type: "rectangle",
  608. x: 10,
  609. y: 10,
  610. groupIds: ["group1"],
  611. });
  612. const rectangle3 = API.createElement({
  613. type: "rectangle",
  614. x: 20,
  615. y: 20,
  616. groupIds: ["group1"],
  617. });
  618. API.setElements([rectangle1, rectangle2, rectangle3]);
  619. mouse.select(rectangle1);
  620. Keyboard.withModifierKeys({ alt: true }, () => {
  621. mouse.down(rectangle1.x + 5, rectangle1.y + 5);
  622. mouse.up(rectangle1.x + 15, rectangle1.y + 15);
  623. });
  624. assertElements(h.elements, [
  625. { id: rectangle1.id },
  626. { id: rectangle2.id },
  627. { id: rectangle3.id },
  628. { [ORIG_ID]: rectangle1.id, selected: true },
  629. { [ORIG_ID]: rectangle2.id, selected: true },
  630. { [ORIG_ID]: rectangle3.id, selected: true },
  631. ]);
  632. });
  633. it("alt-duplicating text container (in-order)", async () => {
  634. const [rectangle, text] = API.createTextContainer();
  635. API.setElements([rectangle, text]);
  636. API.setSelectedElements([rectangle]);
  637. Keyboard.withModifierKeys({ alt: true }, () => {
  638. mouse.down(rectangle.x + 5, rectangle.y + 5);
  639. mouse.up(rectangle.x + 15, rectangle.y + 15);
  640. });
  641. assertElements(h.elements, [
  642. { id: rectangle.id },
  643. { id: text.id, containerId: rectangle.id },
  644. { [ORIG_ID]: rectangle.id, selected: true },
  645. {
  646. [ORIG_ID]: text.id,
  647. containerId: getCloneByOrigId(rectangle.id)?.id,
  648. },
  649. ]);
  650. });
  651. it("alt-duplicating text container (out-of-order)", async () => {
  652. const [rectangle, text] = API.createTextContainer();
  653. API.setElements([text, rectangle]);
  654. API.setSelectedElements([rectangle]);
  655. Keyboard.withModifierKeys({ alt: true }, () => {
  656. mouse.down(rectangle.x + 5, rectangle.y + 5);
  657. mouse.up(rectangle.x + 15, rectangle.y + 15);
  658. });
  659. assertElements(h.elements, [
  660. { id: rectangle.id },
  661. { id: text.id, containerId: rectangle.id },
  662. { [ORIG_ID]: rectangle.id, selected: true },
  663. {
  664. [ORIG_ID]: text.id,
  665. containerId: getCloneByOrigId(rectangle.id)?.id,
  666. },
  667. ]);
  668. });
  669. it("alt-duplicating labeled arrows (in-order)", async () => {
  670. const [arrow, text] = API.createLabeledArrow();
  671. API.setElements([arrow, text]);
  672. API.setSelectedElements([arrow]);
  673. Keyboard.withModifierKeys({ alt: true }, () => {
  674. mouse.down(arrow.x + 5, arrow.y + 5);
  675. mouse.up(arrow.x + 15, arrow.y + 15);
  676. });
  677. assertElements(h.elements, [
  678. { id: arrow.id },
  679. { id: text.id, containerId: arrow.id },
  680. { [ORIG_ID]: arrow.id, selected: true },
  681. {
  682. [ORIG_ID]: text.id,
  683. containerId: getCloneByOrigId(arrow.id)?.id,
  684. },
  685. ]);
  686. expect(h.state.selectedLinearElement).toEqual(
  687. expect.objectContaining({ elementId: getCloneByOrigId(arrow.id)?.id }),
  688. );
  689. });
  690. it("alt-duplicating labeled arrows (out-of-order)", async () => {
  691. const [arrow, text] = API.createLabeledArrow();
  692. API.setElements([text, arrow]);
  693. API.setSelectedElements([arrow]);
  694. Keyboard.withModifierKeys({ alt: true }, () => {
  695. mouse.down(arrow.x + 5, arrow.y + 5);
  696. mouse.up(arrow.x + 15, arrow.y + 15);
  697. });
  698. assertElements(h.elements, [
  699. { id: arrow.id },
  700. { id: text.id, containerId: arrow.id },
  701. { [ORIG_ID]: arrow.id, selected: true },
  702. {
  703. [ORIG_ID]: text.id,
  704. containerId: getCloneByOrigId(arrow.id)?.id,
  705. },
  706. ]);
  707. });
  708. it("alt-duplicating bindable element with bound arrow should keep the arrow on the duplicate", async () => {
  709. const rect = UI.createElement("rectangle", {
  710. x: 0,
  711. y: 0,
  712. width: 100,
  713. height: 100,
  714. });
  715. const arrow = UI.createElement("arrow", {
  716. x: -100,
  717. y: 50,
  718. width: 95,
  719. height: 0,
  720. });
  721. expect(arrow.endBinding?.elementId).toBe(rect.id);
  722. Keyboard.withModifierKeys({ alt: true }, () => {
  723. mouse.down(5, 5);
  724. mouse.up(15, 15);
  725. });
  726. assertElements(h.elements, [
  727. {
  728. id: rect.id,
  729. boundElements: expect.arrayContaining([
  730. expect.objectContaining({ id: arrow.id }),
  731. ]),
  732. },
  733. { [ORIG_ID]: rect.id, boundElements: [], selected: true },
  734. {
  735. id: arrow.id,
  736. endBinding: expect.objectContaining({ elementId: rect.id }),
  737. },
  738. ]);
  739. });
  740. });