Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

group.command.spec.ts 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440
  1. /* eslint-disable @typescript-eslint/ban-ts-comment */
  2. import { TLDrawState } from '~state'
  3. import { mockDocument } from '~test'
  4. import { GroupShape, TLDrawShape, TLDrawShapeType } from '~types'
  5. describe('Group command', () => {
  6. const tlstate = new TLDrawState()
  7. beforeEach(() => {
  8. tlstate.loadDocument(mockDocument)
  9. })
  10. it('does, undoes and redoes command', () => {
  11. tlstate.group(['rect1', 'rect2'], 'newGroup')
  12. expect(tlstate.getShape<GroupShape>('newGroup')).toBeTruthy()
  13. tlstate.undo()
  14. expect(tlstate.getShape<GroupShape>('newGroup')).toBeUndefined()
  15. tlstate.redo()
  16. expect(tlstate.getShape<GroupShape>('newGroup')).toBeTruthy()
  17. })
  18. describe('when less than two shapes are selected', () => {
  19. it('does nothing', () => {
  20. tlstate.deselectAll()
  21. // @ts-ignore
  22. const stackLength = tlstate.stack.length
  23. tlstate.group([], 'newGroup')
  24. expect(tlstate.getShape<GroupShape>('newGroup')).toBeUndefined()
  25. // @ts-ignore
  26. expect(tlstate.stack.length).toBe(stackLength)
  27. tlstate.group(['rect1'], 'newGroup')
  28. expect(tlstate.getShape<GroupShape>('newGroup')).toBeUndefined()
  29. // @ts-ignore
  30. expect(tlstate.stack.length).toBe(stackLength)
  31. })
  32. })
  33. describe('when grouping shapes on the page', () => {
  34. /*
  35. When the parent is a page, the group is created as a child of the page
  36. and the shapes are reparented to the group. The group's child
  37. index should be the minimum child index of the selected shapes.
  38. */
  39. it('creates a group with the correct props', () => {
  40. tlstate.updateShapes(
  41. {
  42. id: 'rect1',
  43. point: [300, 300],
  44. childIndex: 3,
  45. },
  46. {
  47. id: 'rect2',
  48. point: [20, 20],
  49. childIndex: 4,
  50. }
  51. )
  52. tlstate.group(['rect1', 'rect2'], 'newGroup')
  53. const group = tlstate.getShape<GroupShape>('newGroup')
  54. expect(group).toBeTruthy()
  55. expect(group.parentId).toBe('page1')
  56. expect(group.childIndex).toBe(3)
  57. expect(group.point).toStrictEqual([20, 20])
  58. expect(group.children).toStrictEqual(['rect1', 'rect2'])
  59. })
  60. it('reparents the grouped shapes', () => {
  61. tlstate.updateShapes(
  62. {
  63. id: 'rect1',
  64. childIndex: 2.5,
  65. },
  66. {
  67. id: 'rect2',
  68. childIndex: 4.7,
  69. }
  70. )
  71. tlstate.group(['rect1', 'rect2'], 'newGroup')
  72. let rect1: TLDrawShape
  73. let rect2: TLDrawShape
  74. rect1 = tlstate.getShape('rect1')
  75. rect2 = tlstate.getShape('rect2')
  76. // Reparents the shapes
  77. expect(rect1.parentId).toBe('newGroup')
  78. expect(rect2.parentId).toBe('newGroup')
  79. // Sets and preserves the order of the grouped shapes
  80. expect(rect1.childIndex).toBe(1)
  81. expect(rect2.childIndex).toBe(2)
  82. tlstate.undo()
  83. rect1 = tlstate.getShape('rect1')
  84. rect2 = tlstate.getShape('rect2')
  85. // Restores the shapes' parentIds
  86. expect(rect1.parentId).toBe('page1')
  87. expect(rect2.parentId).toBe('page1')
  88. // Restores the shapes' childIndexs
  89. expect(rect1.childIndex).toBe(2.5)
  90. expect(rect2.childIndex).toBe(4.7)
  91. })
  92. })
  93. describe('when grouping shapes that already belong to a group', () => {
  94. /*
  95. Do not allow groups to nest. All groups should be children of
  96. the page: a group should never be the child of a different group.
  97. This is a UX decision as much as a technical one.
  98. */
  99. it('creates a new group on the page', () => {
  100. /*
  101. When the selected shapes are the children of another group, and so
  102. long as the children do not represent ALL of the group's children,
  103. then a new group should be created from the selected shapes and the
  104. original group be updated to only contain the remaining ones.
  105. */
  106. tlstate.resetDocument().createShapes(
  107. {
  108. id: 'rect1',
  109. type: TLDrawShapeType.Rectangle,
  110. childIndex: 1,
  111. },
  112. {
  113. id: 'rect2',
  114. type: TLDrawShapeType.Rectangle,
  115. childIndex: 2,
  116. },
  117. {
  118. id: 'rect3',
  119. type: TLDrawShapeType.Rectangle,
  120. childIndex: 3,
  121. },
  122. {
  123. id: 'rect4',
  124. type: TLDrawShapeType.Rectangle,
  125. childIndex: 4,
  126. }
  127. )
  128. tlstate.group(['rect1', 'rect2', 'rect3', 'rect4'], 'newGroupA')
  129. expect(tlstate.getShape<GroupShape>('newGroupA')).toBeTruthy()
  130. expect(tlstate.getShape('rect1').childIndex).toBe(1)
  131. expect(tlstate.getShape('rect2').childIndex).toBe(2)
  132. expect(tlstate.getShape('rect3').childIndex).toBe(3)
  133. expect(tlstate.getShape('rect4').childIndex).toBe(4)
  134. expect(tlstate.getShape<GroupShape>('newGroupA').children).toStrictEqual([
  135. 'rect1',
  136. 'rect2',
  137. 'rect3',
  138. 'rect4',
  139. ])
  140. tlstate.group(['rect1', 'rect3'], 'newGroupB')
  141. expect(tlstate.getShape<GroupShape>('newGroupA')).toBeTruthy()
  142. expect(tlstate.getShape('rect2').childIndex).toBe(2)
  143. expect(tlstate.getShape('rect4').childIndex).toBe(4)
  144. expect(tlstate.getShape<GroupShape>('newGroupA').children).toStrictEqual(['rect2', 'rect4'])
  145. expect(tlstate.getShape<GroupShape>('newGroupB')).toBeTruthy()
  146. expect(tlstate.getShape('rect1').childIndex).toBe(1)
  147. expect(tlstate.getShape('rect3').childIndex).toBe(2)
  148. expect(tlstate.getShape<GroupShape>('newGroupB').children).toStrictEqual(['rect1', 'rect3'])
  149. tlstate.undo()
  150. expect(tlstate.getShape<GroupShape>('newGroupA')).toBeTruthy()
  151. expect(tlstate.getShape('rect1').childIndex).toBe(1)
  152. expect(tlstate.getShape('rect2').childIndex).toBe(2)
  153. expect(tlstate.getShape('rect3').childIndex).toBe(3)
  154. expect(tlstate.getShape('rect4').childIndex).toBe(4)
  155. expect(tlstate.getShape<GroupShape>('newGroupA').children).toStrictEqual([
  156. 'rect1',
  157. 'rect2',
  158. 'rect3',
  159. 'rect4',
  160. ])
  161. expect(tlstate.getShape<GroupShape>('newGroupB')).toBeUndefined()
  162. tlstate.redo()
  163. expect(tlstate.getShape<GroupShape>('newGroupA')).toBeTruthy()
  164. expect(tlstate.getShape('rect2').childIndex).toBe(2)
  165. expect(tlstate.getShape('rect4').childIndex).toBe(4)
  166. expect(tlstate.getShape<GroupShape>('newGroupA').children).toStrictEqual(['rect2', 'rect4'])
  167. expect(tlstate.getShape<GroupShape>('newGroupB')).toBeTruthy()
  168. expect(tlstate.getShape('rect1').childIndex).toBe(1)
  169. expect(tlstate.getShape('rect3').childIndex).toBe(2)
  170. expect(tlstate.getShape<GroupShape>('newGroupB').children).toStrictEqual(['rect1', 'rect3'])
  171. })
  172. it('does nothing if all shapes in the group are selected', () => {
  173. /*
  174. If the selected shapes represent ALL of the children of the a
  175. group, then no effect should occur.
  176. */
  177. tlstate.resetDocument().createShapes(
  178. {
  179. id: 'rect1',
  180. type: TLDrawShapeType.Rectangle,
  181. childIndex: 1,
  182. },
  183. {
  184. id: 'rect2',
  185. type: TLDrawShapeType.Rectangle,
  186. childIndex: 2,
  187. },
  188. {
  189. id: 'rect3',
  190. type: TLDrawShapeType.Rectangle,
  191. childIndex: 3,
  192. }
  193. )
  194. tlstate.group(['rect1', 'rect2', 'rect3'], 'newGroupA')
  195. tlstate.group(['rect1', 'rect2', 'rect3'], 'newGroupB')
  196. expect(tlstate.getShape<GroupShape>('newGroupB')).toBeUndefined()
  197. })
  198. it('deletes any groups that no longer have children', () => {
  199. /*
  200. If the selected groups included the children of another group
  201. in addition to other shapes then that group should be destroyed.
  202. Other rules around deleted shapes should here apply: bindings
  203. connected to the group should be deleted, etc.
  204. */
  205. tlstate.resetDocument().createShapes(
  206. {
  207. id: 'rect1',
  208. type: TLDrawShapeType.Rectangle,
  209. childIndex: 1,
  210. },
  211. {
  212. id: 'rect2',
  213. type: TLDrawShapeType.Rectangle,
  214. childIndex: 2,
  215. },
  216. {
  217. id: 'rect3',
  218. type: TLDrawShapeType.Rectangle,
  219. childIndex: 3,
  220. }
  221. )
  222. tlstate.group(['rect1', 'rect2'], 'newGroupA')
  223. tlstate.group(['rect1', 'rect2', 'rect3'], 'newGroupB')
  224. expect(tlstate.getShape<GroupShape>('newGroupA')).toBeUndefined()
  225. expect(tlstate.getShape<GroupShape>('newGroupB').children).toStrictEqual([
  226. 'rect1',
  227. 'rect2',
  228. 'rect3',
  229. ])
  230. })
  231. it('merges selected groups that no longer have children', () => {
  232. /*
  233. If the user is creating a group while having selected other
  234. groups, then the selected groups should be destroyed and a new
  235. group created with the selected shapes and the group(s)' children.
  236. */
  237. tlstate.resetDocument().createShapes(
  238. {
  239. id: 'rect1',
  240. type: TLDrawShapeType.Rectangle,
  241. childIndex: 1,
  242. },
  243. {
  244. id: 'rect2',
  245. type: TLDrawShapeType.Rectangle,
  246. childIndex: 2,
  247. },
  248. {
  249. id: 'rect3',
  250. type: TLDrawShapeType.Rectangle,
  251. childIndex: 3,
  252. }
  253. )
  254. tlstate.group(['rect1', 'rect2'], 'newGroupA')
  255. tlstate.group(['newGroupA', 'rect3'], 'newGroupB')
  256. expect(tlstate.getShape<GroupShape>('newGroupA')).toBeUndefined()
  257. expect(tlstate.getShape<GroupShape>('newGroupB').children).toStrictEqual([
  258. 'rect1',
  259. 'rect2',
  260. 'rect3',
  261. ])
  262. tlstate.undo()
  263. expect(tlstate.getShape<GroupShape>('newGroupB')).toBeUndefined()
  264. expect(tlstate.getShape<GroupShape>('newGroupA')).toBeDefined()
  265. expect(tlstate.getShape<GroupShape>('newGroupA').children).toStrictEqual(['rect1', 'rect2'])
  266. tlstate.redo()
  267. expect(tlstate.getShape<GroupShape>('newGroupA')).toBeUndefined()
  268. expect(tlstate.getShape<GroupShape>('newGroupB')).toBeDefined()
  269. expect(tlstate.getShape<GroupShape>('newGroupB').children).toStrictEqual([
  270. 'rect1',
  271. 'rect2',
  272. 'rect3',
  273. ])
  274. })
  275. it('Ungroups if the only shape selected is a group', () => {
  276. tlstate.resetDocument().createShapes(
  277. {
  278. id: 'rect1',
  279. type: TLDrawShapeType.Rectangle,
  280. childIndex: 1,
  281. },
  282. {
  283. id: 'rect2',
  284. type: TLDrawShapeType.Rectangle,
  285. childIndex: 2,
  286. },
  287. {
  288. id: 'rect3',
  289. type: TLDrawShapeType.Rectangle,
  290. childIndex: 3,
  291. }
  292. )
  293. expect(tlstate.shapes.length).toBe(3)
  294. tlstate.selectAll().group()
  295. expect(tlstate.shapes.length).toBe(4)
  296. tlstate.selectAll().group()
  297. expect(tlstate.shapes.length).toBe(3)
  298. })
  299. /*
  300. The layers should be in the same order as the original layers as
  301. they would have appeared on a layers tree (lowest child index
  302. first, parent inclusive).
  303. */
  304. it.todo('preserves the child index order')
  305. /* --------------------- Nesting -------------------- */
  306. // it.todo('creates the new group as a child of the parent group')
  307. /*
  308. The new group should be a child of the parent group.
  309. */
  310. // it.todo('moves the selected layers to the new group')
  311. /*
  312. The new group should have the selected children. The old parents
  313. should no longer have the selected shapes among their children.
  314. All of the selected shapes should be assigned the new parent.
  315. */
  316. })
  317. // describe('when grouping shapes with different parents', () => {
  318. /*
  319. When two shapes with different parents are grouped, the new parent
  320. group should have the same parent as the shape nearest to the top
  321. of the layer tree. The new group's child index should be that
  322. shape's child index.
  323. For example, if the shapes are grouped in the following order:
  324. - page1
  325. - group1
  326. - arrow1
  327. - rect1 (x)
  328. - arrow2
  329. - rect2 (x)
  330. The new parent group should have the same parent as rect1.
  331. - page1
  332. - group1
  333. - arrow1
  334. - group2
  335. - rect1 (x)
  336. - rect2 (x)
  337. - arrow2
  338. If, instead, the shapes are grouped in the following order:
  339. - page1
  340. - arrow1
  341. - rect1 (x)
  342. - group1
  343. - arrow2
  344. - rect2 (x)
  345. Then the new parent group should have the same parent as
  346. rect2.
  347. - page1
  348. - arrow1
  349. - group2 (x)
  350. - rect1
  351. - rect2
  352. - group1
  353. - arrow2
  354. We can find this by searching the tree for the nearest shape to
  355. the top.
  356. */
  357. // it.todo('creates a group in the correct place')
  358. /*
  359. The new group should be a child of the nearest shape to the top
  360. of the tree.
  361. */
  362. /*
  363. If the selected groups included the children of another group, then
  364. that group should be destroyed. Other rules around deleted
  365. shapes should here apply: bindings connected to the group
  366. should be deleted, etc.
  367. */
  368. // it.todo('deletes any groups that no longer have children')
  369. // })
  370. })