duplicate.test.tsx 23 KB

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