zindex.test.tsx 28 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054
  1. import ReactDOM from "react-dom";
  2. import { render } from "./test-utils";
  3. import ExcalidrawApp from "../excalidraw-app";
  4. import { reseed } from "../random";
  5. import {
  6. actionSendBackward,
  7. actionBringForward,
  8. actionBringToFront,
  9. actionSendToBack,
  10. actionDuplicateSelection,
  11. } from "../actions";
  12. import { AppState } from "../types";
  13. import { API } from "./helpers/api";
  14. // Unmount ReactDOM from root
  15. ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
  16. beforeEach(() => {
  17. localStorage.clear();
  18. reseed(7);
  19. });
  20. const { h } = window;
  21. const populateElements = (
  22. elements: {
  23. id: string;
  24. isDeleted?: boolean;
  25. isSelected?: boolean;
  26. groupIds?: string[];
  27. y?: number;
  28. x?: number;
  29. width?: number;
  30. height?: number;
  31. }[],
  32. ) => {
  33. const selectedElementIds: any = {};
  34. h.elements = elements.map(
  35. ({
  36. id,
  37. isDeleted = false,
  38. isSelected = false,
  39. groupIds = [],
  40. y = 100,
  41. x = 100,
  42. width = 100,
  43. height = 100,
  44. }) => {
  45. const element = API.createElement({
  46. type: "rectangle",
  47. id,
  48. isDeleted,
  49. x,
  50. y,
  51. width,
  52. height,
  53. groupIds,
  54. });
  55. if (isSelected) {
  56. selectedElementIds[element.id] = true;
  57. }
  58. return element;
  59. },
  60. );
  61. h.setState({
  62. selectedElementIds,
  63. });
  64. return selectedElementIds;
  65. };
  66. type Actions =
  67. | typeof actionBringForward
  68. | typeof actionSendBackward
  69. | typeof actionBringToFront
  70. | typeof actionSendToBack;
  71. const assertZindex = ({
  72. elements,
  73. appState,
  74. operations,
  75. }: {
  76. elements: {
  77. id: string;
  78. isDeleted?: true;
  79. isSelected?: true;
  80. groupIds?: string[];
  81. }[];
  82. appState?: Partial<AppState>;
  83. operations: [Actions, string[]][];
  84. }) => {
  85. const selectedElementIds = populateElements(elements);
  86. h.setState({
  87. editingGroupId: appState?.editingGroupId || null,
  88. });
  89. operations.forEach(([action, expected]) => {
  90. h.app.actionManager.executeAction(action);
  91. expect(h.elements.map((element) => element.id)).toEqual(expected);
  92. expect(h.state.selectedElementIds).toEqual(selectedElementIds);
  93. });
  94. };
  95. describe("z-index manipulation", () => {
  96. beforeEach(async () => {
  97. await render(<ExcalidrawApp />);
  98. });
  99. it("send back", () => {
  100. assertZindex({
  101. elements: [
  102. { id: "A" },
  103. { id: "B", isDeleted: true },
  104. { id: "C", isDeleted: true },
  105. { id: "D", isSelected: true },
  106. ],
  107. operations: [
  108. [actionSendBackward, ["D", "A", "B", "C"]],
  109. // noop
  110. [actionSendBackward, ["D", "A", "B", "C"]],
  111. ],
  112. });
  113. assertZindex({
  114. elements: [
  115. { id: "A", isSelected: true },
  116. { id: "B", isSelected: true },
  117. { id: "C", isSelected: true },
  118. ],
  119. operations: [
  120. // noop
  121. [actionSendBackward, ["A", "B", "C"]],
  122. ],
  123. });
  124. assertZindex({
  125. elements: [
  126. { id: "A", isDeleted: true },
  127. { id: "B" },
  128. { id: "C", isDeleted: true },
  129. { id: "D", isSelected: true },
  130. ],
  131. operations: [[actionSendBackward, ["A", "D", "B", "C"]]],
  132. });
  133. assertZindex({
  134. elements: [
  135. { id: "A" },
  136. { id: "B", isDeleted: true },
  137. { id: "C", isDeleted: true },
  138. { id: "D", isSelected: true },
  139. { id: "E", isSelected: true },
  140. { id: "F" },
  141. ],
  142. operations: [
  143. [actionSendBackward, ["D", "E", "A", "B", "C", "F"]],
  144. // noop
  145. [actionSendBackward, ["D", "E", "A", "B", "C", "F"]],
  146. ],
  147. });
  148. assertZindex({
  149. elements: [
  150. { id: "A" },
  151. { id: "B" },
  152. { id: "C", isDeleted: true },
  153. { id: "D", isDeleted: true },
  154. { id: "E", isSelected: true },
  155. { id: "F" },
  156. { id: "G", isSelected: true },
  157. ],
  158. operations: [
  159. [actionSendBackward, ["A", "E", "B", "C", "D", "G", "F"]],
  160. [actionSendBackward, ["E", "A", "G", "B", "C", "D", "F"]],
  161. [actionSendBackward, ["E", "G", "A", "B", "C", "D", "F"]],
  162. // noop
  163. [actionSendBackward, ["E", "G", "A", "B", "C", "D", "F"]],
  164. ],
  165. });
  166. assertZindex({
  167. elements: [
  168. { id: "A" },
  169. { id: "B" },
  170. { id: "C", isDeleted: true },
  171. { id: "D", isSelected: true },
  172. { id: "E", isDeleted: true },
  173. { id: "F", isSelected: true },
  174. { id: "G" },
  175. ],
  176. operations: [
  177. [actionSendBackward, ["A", "D", "E", "F", "B", "C", "G"]],
  178. [actionSendBackward, ["D", "E", "F", "A", "B", "C", "G"]],
  179. // noop
  180. [actionSendBackward, ["D", "E", "F", "A", "B", "C", "G"]],
  181. ],
  182. });
  183. // grouped elements should be atomic
  184. // -------------------------------------------------------------------------
  185. assertZindex({
  186. elements: [
  187. { id: "A" },
  188. { id: "B", groupIds: ["g1"] },
  189. { id: "C", groupIds: ["g1"] },
  190. { id: "D", isDeleted: true },
  191. { id: "E", isDeleted: true },
  192. { id: "F", isSelected: true },
  193. ],
  194. operations: [
  195. [actionSendBackward, ["A", "F", "B", "C", "D", "E"]],
  196. [actionSendBackward, ["F", "A", "B", "C", "D", "E"]],
  197. // noop
  198. [actionSendBackward, ["F", "A", "B", "C", "D", "E"]],
  199. ],
  200. });
  201. assertZindex({
  202. elements: [
  203. { id: "A" },
  204. { id: "B", groupIds: ["g2", "g1"] },
  205. { id: "C", groupIds: ["g2", "g1"] },
  206. { id: "D", groupIds: ["g1"] },
  207. { id: "E", isDeleted: true },
  208. { id: "F", isSelected: true },
  209. ],
  210. operations: [
  211. [actionSendBackward, ["A", "F", "B", "C", "D", "E"]],
  212. [actionSendBackward, ["F", "A", "B", "C", "D", "E"]],
  213. // noop
  214. [actionSendBackward, ["F", "A", "B", "C", "D", "E"]],
  215. ],
  216. });
  217. assertZindex({
  218. elements: [
  219. { id: "A" },
  220. { id: "B", groupIds: ["g1"] },
  221. { id: "C", groupIds: ["g2", "g1"] },
  222. { id: "D", groupIds: ["g2", "g1"] },
  223. { id: "E", isDeleted: true },
  224. { id: "F", isSelected: true },
  225. ],
  226. operations: [
  227. [actionSendBackward, ["A", "F", "B", "C", "D", "E"]],
  228. [actionSendBackward, ["F", "A", "B", "C", "D", "E"]],
  229. // noop
  230. [actionSendBackward, ["F", "A", "B", "C", "D", "E"]],
  231. ],
  232. });
  233. assertZindex({
  234. elements: [
  235. { id: "A" },
  236. { id: "B1", groupIds: ["g1"] },
  237. { id: "C1", groupIds: ["g1"] },
  238. { id: "D2", groupIds: ["g2"], isSelected: true },
  239. { id: "E2", groupIds: ["g2"], isSelected: true },
  240. ],
  241. appState: {
  242. editingGroupId: null,
  243. },
  244. operations: [[actionSendBackward, ["A", "D2", "E2", "B1", "C1"]]],
  245. });
  246. // in-group siblings
  247. // -------------------------------------------------------------------------
  248. assertZindex({
  249. elements: [
  250. { id: "A" },
  251. { id: "B", groupIds: ["g1"] },
  252. { id: "C", groupIds: ["g2", "g1"] },
  253. { id: "D", groupIds: ["g2", "g1"], isSelected: true },
  254. ],
  255. appState: {
  256. editingGroupId: "g2",
  257. },
  258. operations: [
  259. [actionSendBackward, ["A", "B", "D", "C"]],
  260. // noop (prevented)
  261. [actionSendBackward, ["A", "B", "D", "C"]],
  262. ],
  263. });
  264. assertZindex({
  265. elements: [
  266. { id: "A" },
  267. { id: "B", groupIds: ["g2", "g1"] },
  268. { id: "C", groupIds: ["g2", "g1"] },
  269. { id: "D", groupIds: ["g1"], isSelected: true },
  270. ],
  271. appState: {
  272. editingGroupId: "g1",
  273. },
  274. operations: [
  275. [actionSendBackward, ["A", "D", "B", "C"]],
  276. // noop (prevented)
  277. [actionSendBackward, ["A", "D", "B", "C"]],
  278. ],
  279. });
  280. assertZindex({
  281. elements: [
  282. { id: "A" },
  283. { id: "B", groupIds: ["g1"] },
  284. { id: "C", groupIds: ["g2", "g1"], isSelected: true },
  285. { id: "D", groupIds: ["g2", "g1"], isDeleted: true },
  286. { id: "E", groupIds: ["g2", "g1"], isSelected: true },
  287. ],
  288. appState: {
  289. editingGroupId: "g1",
  290. },
  291. operations: [
  292. [actionSendBackward, ["A", "C", "D", "E", "B"]],
  293. // noop (prevented)
  294. [actionSendBackward, ["A", "C", "D", "E", "B"]],
  295. ],
  296. });
  297. assertZindex({
  298. elements: [
  299. { id: "A" },
  300. { id: "B", groupIds: ["g1"] },
  301. { id: "C", groupIds: ["g2", "g1"] },
  302. { id: "D", groupIds: ["g2", "g1"] },
  303. { id: "E", groupIds: ["g3", "g1"], isSelected: true },
  304. { id: "F", groupIds: ["g3", "g1"], isSelected: true },
  305. ],
  306. appState: {
  307. editingGroupId: "g1",
  308. },
  309. operations: [
  310. [actionSendBackward, ["A", "B", "E", "F", "C", "D"]],
  311. [actionSendBackward, ["A", "E", "F", "B", "C", "D"]],
  312. // noop (prevented)
  313. [actionSendBackward, ["A", "E", "F", "B", "C", "D"]],
  314. ],
  315. });
  316. // invalid z-indexes across groups (legacy) → allow to sort to next sibling
  317. assertZindex({
  318. elements: [
  319. { id: "A", groupIds: ["g1"] },
  320. { id: "B", groupIds: ["g2"] },
  321. { id: "C", groupIds: ["g1"] },
  322. { id: "D", groupIds: ["g2"], isSelected: true },
  323. { id: "E", groupIds: ["g2"], isSelected: true },
  324. ],
  325. appState: {
  326. editingGroupId: "g2",
  327. },
  328. operations: [
  329. [actionSendBackward, ["A", "D", "E", "B", "C"]],
  330. // noop
  331. [actionSendBackward, ["A", "D", "E", "B", "C"]],
  332. ],
  333. });
  334. // invalid z-indexes across groups (legacy) → allow to sort to next sibling
  335. assertZindex({
  336. elements: [
  337. { id: "A", groupIds: ["g1"] },
  338. { id: "B", groupIds: ["g2"] },
  339. { id: "C", groupIds: ["g1"] },
  340. { id: "D", groupIds: ["g2"], isSelected: true },
  341. { id: "F" },
  342. { id: "G", groupIds: ["g2"], isSelected: true },
  343. ],
  344. appState: {
  345. editingGroupId: "g2",
  346. },
  347. operations: [
  348. [actionSendBackward, ["A", "D", "G", "B", "C", "F"]],
  349. // noop
  350. [actionSendBackward, ["A", "D", "G", "B", "C", "F"]],
  351. ],
  352. });
  353. });
  354. it("bring forward", () => {
  355. assertZindex({
  356. elements: [
  357. { id: "A" },
  358. { id: "B", isSelected: true },
  359. { id: "C", isSelected: true },
  360. { id: "D", isDeleted: true },
  361. { id: "E" },
  362. ],
  363. operations: [
  364. [actionBringForward, ["A", "D", "E", "B", "C"]],
  365. // noop
  366. [actionBringForward, ["A", "D", "E", "B", "C"]],
  367. ],
  368. });
  369. assertZindex({
  370. elements: [
  371. { id: "A", isSelected: true },
  372. { id: "B", isSelected: true },
  373. { id: "C", isSelected: true },
  374. ],
  375. operations: [
  376. // noop
  377. [actionBringForward, ["A", "B", "C"]],
  378. ],
  379. });
  380. assertZindex({
  381. elements: [
  382. { id: "A", isSelected: true },
  383. { id: "B", isDeleted: true },
  384. { id: "C", isDeleted: true },
  385. { id: "D" },
  386. { id: "E", isSelected: true },
  387. { id: "F", isDeleted: true },
  388. { id: "G" },
  389. ],
  390. operations: [
  391. [actionBringForward, ["B", "C", "D", "A", "F", "G", "E"]],
  392. [actionBringForward, ["B", "C", "D", "F", "G", "A", "E"]],
  393. // noop
  394. [actionBringForward, ["B", "C", "D", "F", "G", "A", "E"]],
  395. ],
  396. });
  397. // grouped elements should be atomic
  398. // -------------------------------------------------------------------------
  399. assertZindex({
  400. elements: [
  401. { id: "A", isSelected: true },
  402. { id: "B", isDeleted: true },
  403. { id: "C", isDeleted: true },
  404. { id: "D", groupIds: ["g1"] },
  405. { id: "E", groupIds: ["g1"] },
  406. { id: "F" },
  407. ],
  408. operations: [
  409. [actionBringForward, ["B", "C", "D", "E", "A", "F"]],
  410. [actionBringForward, ["B", "C", "D", "E", "F", "A"]],
  411. // noop
  412. [actionBringForward, ["B", "C", "D", "E", "F", "A"]],
  413. ],
  414. });
  415. assertZindex({
  416. elements: [
  417. { id: "A" },
  418. { id: "B", isSelected: true },
  419. { id: "C", groupIds: ["g2", "g1"] },
  420. { id: "D", groupIds: ["g2", "g1"] },
  421. { id: "E", groupIds: ["g1"] },
  422. { id: "F" },
  423. ],
  424. operations: [
  425. [actionBringForward, ["A", "C", "D", "E", "B", "F"]],
  426. [actionBringForward, ["A", "C", "D", "E", "F", "B"]],
  427. // noop
  428. [actionBringForward, ["A", "C", "D", "E", "F", "B"]],
  429. ],
  430. });
  431. assertZindex({
  432. elements: [
  433. { id: "A" },
  434. { id: "B", isSelected: true },
  435. { id: "C", groupIds: ["g1"] },
  436. { id: "D", groupIds: ["g2", "g1"] },
  437. { id: "E", groupIds: ["g2", "g1"] },
  438. { id: "F" },
  439. ],
  440. operations: [
  441. [actionBringForward, ["A", "C", "D", "E", "B", "F"]],
  442. [actionBringForward, ["A", "C", "D", "E", "F", "B"]],
  443. // noop
  444. [actionBringForward, ["A", "C", "D", "E", "F", "B"]],
  445. ],
  446. });
  447. // in-group siblings
  448. // -------------------------------------------------------------------------
  449. assertZindex({
  450. elements: [
  451. { id: "A" },
  452. { id: "B", groupIds: ["g2", "g1"], isSelected: true },
  453. { id: "C", groupIds: ["g2", "g1"] },
  454. { id: "D", groupIds: ["g1"] },
  455. ],
  456. appState: {
  457. editingGroupId: "g2",
  458. },
  459. operations: [
  460. [actionBringForward, ["A", "C", "B", "D"]],
  461. // noop (prevented)
  462. [actionBringForward, ["A", "C", "B", "D"]],
  463. ],
  464. });
  465. assertZindex({
  466. elements: [
  467. { id: "A", groupIds: ["g1"], isSelected: true },
  468. { id: "B", groupIds: ["g2", "g1"] },
  469. { id: "C", groupIds: ["g2", "g1"] },
  470. { id: "D" },
  471. ],
  472. appState: {
  473. editingGroupId: "g1",
  474. },
  475. operations: [
  476. [actionBringForward, ["B", "C", "A", "D"]],
  477. // noop (prevented)
  478. [actionBringForward, ["B", "C", "A", "D"]],
  479. ],
  480. });
  481. assertZindex({
  482. elements: [
  483. { id: "A", groupIds: ["g2", "g1"], isSelected: true },
  484. { id: "B", groupIds: ["g2", "g1"], isSelected: true },
  485. { id: "C", groupIds: ["g1"] },
  486. { id: "D" },
  487. ],
  488. appState: {
  489. editingGroupId: "g1",
  490. },
  491. operations: [
  492. [actionBringForward, ["C", "A", "B", "D"]],
  493. // noop (prevented)
  494. [actionBringForward, ["C", "A", "B", "D"]],
  495. ],
  496. });
  497. // invalid z-indexes across groups (legacy) → allow to sort to next sibling
  498. assertZindex({
  499. elements: [
  500. { id: "A", groupIds: ["g2"], isSelected: true },
  501. { id: "B", groupIds: ["g2"], isSelected: true },
  502. { id: "C", groupIds: ["g1"] },
  503. { id: "D", groupIds: ["g2"] },
  504. { id: "E", groupIds: ["g1"] },
  505. ],
  506. appState: {
  507. editingGroupId: "g2",
  508. },
  509. operations: [
  510. [actionBringForward, ["C", "D", "A", "B", "E"]],
  511. // noop
  512. [actionBringForward, ["C", "D", "A", "B", "E"]],
  513. ],
  514. });
  515. // invalid z-indexes across groups (legacy) → allow to sort to next sibling
  516. assertZindex({
  517. elements: [
  518. { id: "A", groupIds: ["g2"], isSelected: true },
  519. { id: "B" },
  520. { id: "C", groupIds: ["g2"], isSelected: true },
  521. { id: "D", groupIds: ["g1"] },
  522. { id: "E", groupIds: ["g2"] },
  523. { id: "F", groupIds: ["g1"] },
  524. ],
  525. appState: {
  526. editingGroupId: "g2",
  527. },
  528. operations: [
  529. [actionBringForward, ["B", "D", "E", "A", "C", "F"]],
  530. // noop
  531. [actionBringForward, ["B", "D", "E", "A", "C", "F"]],
  532. ],
  533. });
  534. });
  535. it("bring to front", () => {
  536. assertZindex({
  537. elements: [
  538. { id: "0" },
  539. { id: "A", isSelected: true },
  540. { id: "B", isDeleted: true },
  541. { id: "C", isDeleted: true },
  542. { id: "D" },
  543. { id: "E", isSelected: true },
  544. { id: "F", isDeleted: true },
  545. { id: "G" },
  546. ],
  547. operations: [
  548. [actionBringToFront, ["0", "B", "C", "D", "F", "G", "A", "E"]],
  549. // noop
  550. [actionBringToFront, ["0", "B", "C", "D", "F", "G", "A", "E"]],
  551. ],
  552. });
  553. assertZindex({
  554. elements: [
  555. { id: "A", isSelected: true },
  556. { id: "B", isSelected: true },
  557. { id: "C", isSelected: true },
  558. ],
  559. operations: [
  560. // noop
  561. [actionBringToFront, ["A", "B", "C"]],
  562. ],
  563. });
  564. assertZindex({
  565. elements: [
  566. { id: "A" },
  567. { id: "B", isSelected: true },
  568. { id: "C", isSelected: true },
  569. ],
  570. operations: [
  571. // noop
  572. [actionBringToFront, ["A", "B", "C"]],
  573. ],
  574. });
  575. assertZindex({
  576. elements: [
  577. { id: "A", isSelected: true },
  578. { id: "B", isSelected: true },
  579. { id: "C" },
  580. ],
  581. operations: [
  582. [actionBringToFront, ["C", "A", "B"]],
  583. // noop
  584. [actionBringToFront, ["C", "A", "B"]],
  585. ],
  586. });
  587. // in-group sorting
  588. // -------------------------------------------------------------------------
  589. assertZindex({
  590. elements: [
  591. { id: "A" },
  592. { id: "B", groupIds: ["g1"] },
  593. { id: "C", groupIds: ["g1"], isSelected: true },
  594. { id: "D", groupIds: ["g1"] },
  595. { id: "E", groupIds: ["g1"], isSelected: true },
  596. { id: "F", groupIds: ["g2", "g1"] },
  597. { id: "G", groupIds: ["g2", "g1"] },
  598. { id: "H", groupIds: ["g3", "g1"] },
  599. { id: "I", groupIds: ["g3", "g1"] },
  600. ],
  601. appState: {
  602. editingGroupId: "g1",
  603. },
  604. operations: [
  605. [actionBringToFront, ["A", "B", "D", "F", "G", "H", "I", "C", "E"]],
  606. // noop (prevented)
  607. [actionBringToFront, ["A", "B", "D", "F", "G", "H", "I", "C", "E"]],
  608. ],
  609. });
  610. assertZindex({
  611. elements: [
  612. { id: "A" },
  613. { id: "B", groupIds: ["g2", "g1"], isSelected: true },
  614. { id: "D", groupIds: ["g2", "g1"] },
  615. { id: "C", groupIds: ["g1"] },
  616. ],
  617. appState: {
  618. editingGroupId: "g2",
  619. },
  620. operations: [
  621. [actionBringToFront, ["A", "D", "B", "C"]],
  622. // noop (prevented)
  623. [actionBringToFront, ["A", "D", "B", "C"]],
  624. ],
  625. });
  626. // invalid z-indexes across groups (legacy) → allow to sort to next sibling
  627. assertZindex({
  628. elements: [
  629. { id: "A", groupIds: ["g2", "g3"], isSelected: true },
  630. { id: "B", groupIds: ["g1", "g3"] },
  631. { id: "C", groupIds: ["g2", "g3"] },
  632. { id: "D", groupIds: ["g1", "g3"] },
  633. ],
  634. appState: {
  635. editingGroupId: "g2",
  636. },
  637. operations: [
  638. [actionBringToFront, ["B", "C", "A", "D"]],
  639. // noop
  640. [actionBringToFront, ["B", "C", "A", "D"]],
  641. ],
  642. });
  643. // invalid z-indexes across groups (legacy) → allow to sort to next sibling
  644. assertZindex({
  645. elements: [
  646. { id: "A", groupIds: ["g2"], isSelected: true },
  647. { id: "B", groupIds: ["g1"] },
  648. { id: "C", groupIds: ["g2"] },
  649. { id: "D", groupIds: ["g1"] },
  650. ],
  651. appState: {
  652. editingGroupId: "g2",
  653. },
  654. operations: [
  655. [actionBringToFront, ["B", "C", "A", "D"]],
  656. // noop
  657. [actionBringToFront, ["B", "C", "A", "D"]],
  658. ],
  659. });
  660. });
  661. it("send to back", () => {
  662. assertZindex({
  663. elements: [
  664. { id: "A" },
  665. { id: "B", isDeleted: true },
  666. { id: "C" },
  667. { id: "D", isDeleted: true },
  668. { id: "E", isSelected: true },
  669. { id: "F", isDeleted: true },
  670. { id: "G" },
  671. { id: "H", isSelected: true },
  672. { id: "I" },
  673. ],
  674. operations: [
  675. [actionSendToBack, ["E", "H", "A", "B", "C", "D", "F", "G", "I"]],
  676. // noop
  677. [actionSendToBack, ["E", "H", "A", "B", "C", "D", "F", "G", "I"]],
  678. ],
  679. });
  680. assertZindex({
  681. elements: [
  682. { id: "A", isSelected: true },
  683. { id: "B", isSelected: true },
  684. { id: "C", isSelected: true },
  685. ],
  686. operations: [
  687. // noop
  688. [actionSendToBack, ["A", "B", "C"]],
  689. ],
  690. });
  691. assertZindex({
  692. elements: [
  693. { id: "A", isSelected: true },
  694. { id: "B", isSelected: true },
  695. { id: "C" },
  696. ],
  697. operations: [
  698. // noop
  699. [actionSendToBack, ["A", "B", "C"]],
  700. ],
  701. });
  702. assertZindex({
  703. elements: [
  704. { id: "A" },
  705. { id: "B", isSelected: true },
  706. { id: "C", isSelected: true },
  707. ],
  708. operations: [
  709. [actionSendToBack, ["B", "C", "A"]],
  710. // noop
  711. [actionSendToBack, ["B", "C", "A"]],
  712. ],
  713. });
  714. // in-group sorting
  715. // -------------------------------------------------------------------------
  716. assertZindex({
  717. elements: [
  718. { id: "A" },
  719. { id: "B", groupIds: ["g2", "g1"] },
  720. { id: "C", groupIds: ["g2", "g1"] },
  721. { id: "D", groupIds: ["g3", "g1"] },
  722. { id: "E", groupIds: ["g3", "g1"] },
  723. { id: "F", groupIds: ["g1"], isSelected: true },
  724. { id: "G", groupIds: ["g1"] },
  725. { id: "H", groupIds: ["g1"], isSelected: true },
  726. { id: "I", groupIds: ["g1"] },
  727. ],
  728. appState: {
  729. editingGroupId: "g1",
  730. },
  731. operations: [
  732. [actionSendToBack, ["A", "F", "H", "B", "C", "D", "E", "G", "I"]],
  733. // noop (prevented)
  734. [actionSendToBack, ["A", "F", "H", "B", "C", "D", "E", "G", "I"]],
  735. ],
  736. });
  737. assertZindex({
  738. elements: [
  739. { id: "A" },
  740. { id: "B", groupIds: ["g1"] },
  741. { id: "C", groupIds: ["g2", "g1"] },
  742. { id: "D", groupIds: ["g2", "g1"], isSelected: true },
  743. ],
  744. appState: {
  745. editingGroupId: "g2",
  746. },
  747. operations: [
  748. [actionSendToBack, ["A", "B", "D", "C"]],
  749. // noop (prevented)
  750. [actionSendToBack, ["A", "B", "D", "C"]],
  751. ],
  752. });
  753. // invalid z-indexes across groups (legacy) → allow to sort to next sibling
  754. assertZindex({
  755. elements: [
  756. { id: "A", groupIds: ["g1", "g3"] },
  757. { id: "B", groupIds: ["g2", "g3"] },
  758. { id: "C", groupIds: ["g1", "g3"] },
  759. { id: "D", groupIds: ["g2", "g3"], isSelected: true },
  760. ],
  761. appState: {
  762. editingGroupId: "g2",
  763. },
  764. operations: [
  765. [actionSendToBack, ["A", "D", "B", "C"]],
  766. // noop
  767. [actionSendToBack, ["A", "D", "B", "C"]],
  768. ],
  769. });
  770. // invalid z-indexes across groups (legacy) → allow to sort to next sibling
  771. assertZindex({
  772. elements: [
  773. { id: "A", groupIds: ["g1"] },
  774. { id: "B", groupIds: ["g2"] },
  775. { id: "C", groupIds: ["g1"] },
  776. { id: "D", groupIds: ["g2"], isSelected: true },
  777. ],
  778. appState: {
  779. editingGroupId: "g2",
  780. },
  781. operations: [
  782. [actionSendToBack, ["A", "D", "B", "C"]],
  783. // noop
  784. [actionSendToBack, ["A", "D", "B", "C"]],
  785. ],
  786. });
  787. });
  788. it("duplicating elements should retain zindex integrity", () => {
  789. populateElements([
  790. { id: "A", isSelected: true },
  791. { id: "B", isSelected: true },
  792. ]);
  793. h.app.actionManager.executeAction(actionDuplicateSelection);
  794. expect(h.elements).toMatchObject([
  795. { id: "A" },
  796. { id: "A_copy" },
  797. { id: "B" },
  798. { id: "B_copy" },
  799. ]);
  800. populateElements([
  801. { id: "A", groupIds: ["g1"], isSelected: true },
  802. { id: "B", groupIds: ["g1"], isSelected: true },
  803. ]);
  804. h.setState({
  805. selectedGroupIds: { g1: true },
  806. });
  807. h.app.actionManager.executeAction(actionDuplicateSelection);
  808. expect(h.elements).toMatchObject([
  809. { id: "A" },
  810. { id: "B" },
  811. {
  812. id: "A_copy",
  813. groupIds: [expect.stringMatching(/.{3,}/)],
  814. },
  815. {
  816. id: "B_copy",
  817. groupIds: [expect.stringMatching(/.{3,}/)],
  818. },
  819. ]);
  820. populateElements([
  821. { id: "A", groupIds: ["g1"], isSelected: true },
  822. { id: "B", groupIds: ["g1"], isSelected: true },
  823. { id: "C" },
  824. ]);
  825. h.setState({
  826. selectedGroupIds: { g1: true },
  827. });
  828. h.app.actionManager.executeAction(actionDuplicateSelection);
  829. expect(h.elements).toMatchObject([
  830. { id: "A" },
  831. { id: "B" },
  832. {
  833. id: "A_copy",
  834. groupIds: [expect.stringMatching(/.{3,}/)],
  835. },
  836. {
  837. id: "B_copy",
  838. groupIds: [expect.stringMatching(/.{3,}/)],
  839. },
  840. { id: "C" },
  841. ]);
  842. populateElements([
  843. { id: "A", groupIds: ["g1"], isSelected: true },
  844. { id: "B", groupIds: ["g1"], isSelected: true },
  845. { id: "C", isSelected: true },
  846. ]);
  847. h.setState({
  848. selectedGroupIds: { g1: true },
  849. });
  850. h.app.actionManager.executeAction(actionDuplicateSelection);
  851. expect(h.elements.map((element) => element.id)).toEqual([
  852. "A",
  853. "B",
  854. "A_copy",
  855. "B_copy",
  856. "C",
  857. "C_copy",
  858. ]);
  859. populateElements([
  860. { id: "A", groupIds: ["g1"], isSelected: true },
  861. { id: "B", groupIds: ["g1"], isSelected: true },
  862. { id: "C", groupIds: ["g2"], isSelected: true },
  863. { id: "D", groupIds: ["g2"], isSelected: true },
  864. ]);
  865. h.setState({
  866. selectedGroupIds: { g1: true, g2: true },
  867. });
  868. h.app.actionManager.executeAction(actionDuplicateSelection);
  869. expect(h.elements.map((element) => element.id)).toEqual([
  870. "A",
  871. "B",
  872. "A_copy",
  873. "B_copy",
  874. "C",
  875. "D",
  876. "C_copy",
  877. "D_copy",
  878. ]);
  879. populateElements([
  880. { id: "A", groupIds: ["g1", "g2"], isSelected: true },
  881. { id: "B", groupIds: ["g1", "g2"], isSelected: true },
  882. { id: "C", groupIds: ["g2"], isSelected: true },
  883. ]);
  884. h.setState({
  885. selectedGroupIds: { g1: true },
  886. });
  887. h.app.actionManager.executeAction(actionDuplicateSelection);
  888. expect(h.elements.map((element) => element.id)).toEqual([
  889. "A",
  890. "B",
  891. "A_copy",
  892. "B_copy",
  893. "C",
  894. "C_copy",
  895. ]);
  896. populateElements([
  897. { id: "A", groupIds: ["g1", "g2"], isSelected: true },
  898. { id: "B", groupIds: ["g1", "g2"], isSelected: true },
  899. { id: "C", groupIds: ["g2"], isSelected: true },
  900. ]);
  901. h.setState({
  902. selectedGroupIds: { g2: true },
  903. });
  904. h.app.actionManager.executeAction(actionDuplicateSelection);
  905. expect(h.elements.map((element) => element.id)).toEqual([
  906. "A",
  907. "B",
  908. "C",
  909. "A_copy",
  910. "B_copy",
  911. "C_copy",
  912. ]);
  913. populateElements([
  914. { id: "A", groupIds: ["g1", "g2"], isSelected: true },
  915. { id: "B", groupIds: ["g1", "g2"], isSelected: true },
  916. { id: "C", groupIds: ["g2"], isSelected: true },
  917. { id: "D", groupIds: ["g3", "g4"], isSelected: true },
  918. { id: "E", groupIds: ["g3", "g4"], isSelected: true },
  919. { id: "F", groupIds: ["g4"], isSelected: true },
  920. ]);
  921. h.setState({
  922. selectedGroupIds: { g2: true, g4: true },
  923. });
  924. h.app.actionManager.executeAction(actionDuplicateSelection);
  925. expect(h.elements.map((element) => element.id)).toEqual([
  926. "A",
  927. "B",
  928. "C",
  929. "A_copy",
  930. "B_copy",
  931. "C_copy",
  932. "D",
  933. "E",
  934. "F",
  935. "D_copy",
  936. "E_copy",
  937. "F_copy",
  938. ]);
  939. populateElements([
  940. { id: "A", groupIds: ["g1", "g2"], isSelected: true },
  941. { id: "B", groupIds: ["g1", "g2"] },
  942. { id: "C", groupIds: ["g2"] },
  943. ]);
  944. h.app.actionManager.executeAction(actionDuplicateSelection);
  945. expect(h.elements.map((element) => element.id)).toEqual([
  946. "A",
  947. "A_copy",
  948. "B",
  949. "C",
  950. ]);
  951. populateElements([
  952. { id: "A", groupIds: ["g1", "g2"] },
  953. { id: "B", groupIds: ["g1", "g2"], isSelected: true },
  954. { id: "C", groupIds: ["g2"] },
  955. ]);
  956. h.app.actionManager.executeAction(actionDuplicateSelection);
  957. expect(h.elements.map((element) => element.id)).toEqual([
  958. "A",
  959. "B",
  960. "B_copy",
  961. "C",
  962. ]);
  963. populateElements([
  964. { id: "A", groupIds: ["g1", "g2"], isSelected: true },
  965. { id: "B", groupIds: ["g1", "g2"], isSelected: true },
  966. { id: "C", groupIds: ["g2"], isSelected: true },
  967. ]);
  968. h.app.actionManager.executeAction(actionDuplicateSelection);
  969. expect(h.elements.map((element) => element.id)).toEqual([
  970. "A",
  971. "A_copy",
  972. "B",
  973. "B_copy",
  974. "C",
  975. "C_copy",
  976. ]);
  977. });
  978. });