duplicate.test.tsx 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849
  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. assertElements(h.elements, [
  442. { id: frame.id },
  443. { id: rectangle1.id, frameId: frame.id },
  444. { id: rectangle2.id, frameId: frame.id },
  445. { [ORIG_ID]: rectangle2.id, selected: true, groupIds: [], frameId: null },
  446. ]);
  447. expect(h.state.editingGroupId).toBe(null);
  448. });
  449. });
  450. describe("duplication z-order", () => {
  451. beforeEach(async () => {
  452. await render(<Excalidraw />);
  453. });
  454. it("duplication z order with Cmd+D for the lowest z-ordered element should be +1 for the clone", () => {
  455. const rectangle1 = API.createElement({
  456. type: "rectangle",
  457. x: 0,
  458. y: 0,
  459. });
  460. const rectangle2 = API.createElement({
  461. type: "rectangle",
  462. x: 10,
  463. y: 10,
  464. });
  465. const rectangle3 = API.createElement({
  466. type: "rectangle",
  467. x: 20,
  468. y: 20,
  469. });
  470. API.setElements([rectangle1, rectangle2, rectangle3]);
  471. API.setSelectedElements([rectangle1]);
  472. act(() => {
  473. h.app.actionManager.executeAction(actionDuplicateSelection);
  474. });
  475. assertElements(h.elements, [
  476. { id: rectangle1.id },
  477. { [ORIG_ID]: rectangle1.id, selected: true },
  478. { id: rectangle2.id },
  479. { id: rectangle3.id },
  480. ]);
  481. });
  482. it("duplication z order with Cmd+D for the highest z-ordered element should be +1 for the clone", () => {
  483. const rectangle1 = API.createElement({
  484. type: "rectangle",
  485. x: 0,
  486. y: 0,
  487. });
  488. const rectangle2 = API.createElement({
  489. type: "rectangle",
  490. x: 10,
  491. y: 10,
  492. });
  493. const rectangle3 = API.createElement({
  494. type: "rectangle",
  495. x: 20,
  496. y: 20,
  497. });
  498. API.setElements([rectangle1, rectangle2, rectangle3]);
  499. API.setSelectedElements([rectangle3]);
  500. act(() => {
  501. h.app.actionManager.executeAction(actionDuplicateSelection);
  502. });
  503. assertElements(h.elements, [
  504. { id: rectangle1.id },
  505. { id: rectangle2.id },
  506. { id: rectangle3.id },
  507. { [ORIG_ID]: rectangle3.id, selected: true },
  508. ]);
  509. });
  510. it("duplication z order with alt+drag for the lowest z-ordered element should be +1 for the clone", () => {
  511. const rectangle1 = API.createElement({
  512. type: "rectangle",
  513. x: 0,
  514. y: 0,
  515. });
  516. const rectangle2 = API.createElement({
  517. type: "rectangle",
  518. x: 10,
  519. y: 10,
  520. });
  521. const rectangle3 = API.createElement({
  522. type: "rectangle",
  523. x: 20,
  524. y: 20,
  525. });
  526. API.setElements([rectangle1, rectangle2, rectangle3]);
  527. mouse.select(rectangle1);
  528. Keyboard.withModifierKeys({ alt: true }, () => {
  529. mouse.down(rectangle1.x + 5, rectangle1.y + 5);
  530. mouse.up(rectangle1.x + 5, rectangle1.y + 5);
  531. });
  532. assertElements(h.elements, [
  533. { id: rectangle1.id },
  534. { [ORIG_ID]: rectangle1.id, selected: true },
  535. { id: rectangle2.id },
  536. { id: rectangle3.id },
  537. ]);
  538. });
  539. it("duplication z order with alt+drag for the highest z-ordered element should be +1 for the clone", () => {
  540. const rectangle1 = API.createElement({
  541. type: "rectangle",
  542. x: 0,
  543. y: 0,
  544. });
  545. const rectangle2 = API.createElement({
  546. type: "rectangle",
  547. x: 10,
  548. y: 10,
  549. });
  550. const rectangle3 = API.createElement({
  551. type: "rectangle",
  552. x: 20,
  553. y: 20,
  554. });
  555. API.setElements([rectangle1, rectangle2, rectangle3]);
  556. mouse.select(rectangle3);
  557. Keyboard.withModifierKeys({ alt: true }, () => {
  558. mouse.down(rectangle3.x + 5, rectangle3.y + 5);
  559. mouse.up(rectangle3.x + 5, rectangle3.y + 5);
  560. });
  561. assertElements(h.elements, [
  562. { id: rectangle1.id },
  563. { id: rectangle2.id },
  564. { id: rectangle3.id },
  565. { [ORIG_ID]: rectangle3.id, selected: true },
  566. ]);
  567. });
  568. it("duplication z order with alt+drag for the lowest z-ordered element should be +1 for the clone", () => {
  569. const rectangle1 = API.createElement({
  570. type: "rectangle",
  571. x: 0,
  572. y: 0,
  573. });
  574. const rectangle2 = API.createElement({
  575. type: "rectangle",
  576. x: 10,
  577. y: 10,
  578. });
  579. const rectangle3 = API.createElement({
  580. type: "rectangle",
  581. x: 20,
  582. y: 20,
  583. });
  584. API.setElements([rectangle1, rectangle2, rectangle3]);
  585. mouse.select(rectangle1);
  586. Keyboard.withModifierKeys({ alt: true }, () => {
  587. mouse.down(rectangle1.x + 5, rectangle1.y + 5);
  588. mouse.up(rectangle1.x + 5, rectangle1.y + 5);
  589. });
  590. assertElements(h.elements, [
  591. { id: rectangle1.id },
  592. { [ORIG_ID]: rectangle1.id, selected: true },
  593. { id: rectangle2.id },
  594. { id: rectangle3.id },
  595. ]);
  596. });
  597. it("duplication z order with alt+drag with grouped elements should consider the group together when determining z-index", () => {
  598. const rectangle1 = API.createElement({
  599. type: "rectangle",
  600. x: 0,
  601. y: 0,
  602. groupIds: ["group1"],
  603. });
  604. const rectangle2 = API.createElement({
  605. type: "rectangle",
  606. x: 10,
  607. y: 10,
  608. groupIds: ["group1"],
  609. });
  610. const rectangle3 = API.createElement({
  611. type: "rectangle",
  612. x: 20,
  613. y: 20,
  614. groupIds: ["group1"],
  615. });
  616. API.setElements([rectangle1, rectangle2, rectangle3]);
  617. mouse.select(rectangle1);
  618. Keyboard.withModifierKeys({ alt: true }, () => {
  619. mouse.down(rectangle1.x + 5, rectangle1.y + 5);
  620. mouse.up(rectangle1.x + 15, rectangle1.y + 15);
  621. });
  622. assertElements(h.elements, [
  623. { id: rectangle1.id },
  624. { id: rectangle2.id },
  625. { id: rectangle3.id },
  626. { [ORIG_ID]: rectangle1.id, selected: true },
  627. { [ORIG_ID]: rectangle2.id, selected: true },
  628. { [ORIG_ID]: rectangle3.id, selected: true },
  629. ]);
  630. });
  631. it("alt-duplicating text container (in-order)", async () => {
  632. const [rectangle, text] = API.createTextContainer();
  633. API.setElements([rectangle, text]);
  634. API.setSelectedElements([rectangle]);
  635. Keyboard.withModifierKeys({ alt: true }, () => {
  636. mouse.down(rectangle.x + 5, rectangle.y + 5);
  637. mouse.up(rectangle.x + 15, rectangle.y + 15);
  638. });
  639. assertElements(h.elements, [
  640. { id: rectangle.id },
  641. { id: text.id, containerId: rectangle.id },
  642. { [ORIG_ID]: rectangle.id, selected: true },
  643. {
  644. [ORIG_ID]: text.id,
  645. containerId: getCloneByOrigId(rectangle.id)?.id,
  646. },
  647. ]);
  648. });
  649. it("alt-duplicating text container (out-of-order)", async () => {
  650. const [rectangle, text] = API.createTextContainer();
  651. API.setElements([text, rectangle]);
  652. API.setSelectedElements([rectangle]);
  653. Keyboard.withModifierKeys({ alt: true }, () => {
  654. mouse.down(rectangle.x + 5, rectangle.y + 5);
  655. mouse.up(rectangle.x + 15, rectangle.y + 15);
  656. });
  657. assertElements(h.elements, [
  658. { id: rectangle.id },
  659. { id: text.id, containerId: rectangle.id },
  660. { [ORIG_ID]: rectangle.id, selected: true },
  661. {
  662. [ORIG_ID]: text.id,
  663. containerId: getCloneByOrigId(rectangle.id)?.id,
  664. },
  665. ]);
  666. });
  667. it("alt-duplicating labeled arrows (in-order)", async () => {
  668. const [arrow, text] = API.createLabeledArrow();
  669. API.setElements([arrow, text]);
  670. API.setSelectedElements([arrow]);
  671. Keyboard.withModifierKeys({ alt: true }, () => {
  672. mouse.down(arrow.x + 5, arrow.y + 5);
  673. mouse.up(arrow.x + 15, arrow.y + 15);
  674. });
  675. assertElements(h.elements, [
  676. { id: arrow.id },
  677. { id: text.id, containerId: arrow.id },
  678. { [ORIG_ID]: arrow.id, selected: true },
  679. {
  680. [ORIG_ID]: text.id,
  681. containerId: getCloneByOrigId(arrow.id)?.id,
  682. },
  683. ]);
  684. expect(h.state.selectedLinearElement).toEqual(
  685. expect.objectContaining({ elementId: getCloneByOrigId(arrow.id)?.id }),
  686. );
  687. });
  688. it("alt-duplicating labeled arrows (out-of-order)", async () => {
  689. const [arrow, text] = API.createLabeledArrow();
  690. API.setElements([text, arrow]);
  691. API.setSelectedElements([arrow]);
  692. Keyboard.withModifierKeys({ alt: true }, () => {
  693. mouse.down(arrow.x + 5, arrow.y + 5);
  694. mouse.up(arrow.x + 15, arrow.y + 15);
  695. });
  696. assertElements(h.elements, [
  697. { id: arrow.id },
  698. { id: text.id, containerId: arrow.id },
  699. { [ORIG_ID]: arrow.id, selected: true },
  700. {
  701. [ORIG_ID]: text.id,
  702. containerId: getCloneByOrigId(arrow.id)?.id,
  703. },
  704. ]);
  705. });
  706. it("alt-duplicating bindable element with bound arrow should keep the arrow on the duplicate", async () => {
  707. const rect = UI.createElement("rectangle", {
  708. x: 0,
  709. y: 0,
  710. width: 100,
  711. height: 100,
  712. });
  713. const arrow = UI.createElement("arrow", {
  714. x: -100,
  715. y: 50,
  716. width: 95,
  717. height: 0,
  718. });
  719. expect(arrow.endBinding?.elementId).toBe(rect.id);
  720. Keyboard.withModifierKeys({ alt: true }, () => {
  721. mouse.down(5, 5);
  722. mouse.up(15, 15);
  723. });
  724. assertElements(h.elements, [
  725. {
  726. id: rect.id,
  727. boundElements: expect.arrayContaining([
  728. expect.objectContaining({ id: arrow.id }),
  729. ]),
  730. },
  731. { [ORIG_ID]: rect.id, boundElements: [], selected: true },
  732. {
  733. id: arrow.id,
  734. endBinding: expect.objectContaining({ elementId: rect.id }),
  735. },
  736. ]);
  737. });
  738. });