2
0

duplicate.test.tsx 23 KB

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