delete-entity.test.js 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. import { expect } from 'chai'
  2. import sinon from 'sinon'
  3. import { screen, fireEvent, waitFor } from '@testing-library/react'
  4. import fetchMock from 'fetch-mock'
  5. import MockedSocket from 'socket.io-mock'
  6. import {
  7. renderWithEditorContext,
  8. cleanUpContext,
  9. } from '../../../helpers/render-with-context'
  10. import FileTreeRoot from '../../../../../frontend/js/features/file-tree/components/file-tree-root'
  11. describe('FileTree Delete Entity Flow', function () {
  12. const onSelect = sinon.stub()
  13. const onInit = sinon.stub()
  14. beforeEach(function () {
  15. window.metaAttributesCache = new Map()
  16. window.metaAttributesCache.set('ol-user', { id: 'user1' })
  17. })
  18. afterEach(function () {
  19. fetchMock.restore()
  20. onSelect.reset()
  21. onInit.reset()
  22. cleanUpContext()
  23. window.metaAttributesCache = new Map()
  24. })
  25. describe('single entity', function () {
  26. beforeEach(function () {
  27. const rootFolder = [
  28. {
  29. _id: 'root-folder-id',
  30. name: 'rootFolder',
  31. docs: [{ _id: '456def', name: 'main.tex' }],
  32. folders: [],
  33. fileRefs: [],
  34. },
  35. ]
  36. renderWithEditorContext(
  37. <FileTreeRoot
  38. refProviders={{}}
  39. reindexReferences={() => null}
  40. setRefProviderEnabled={() => null}
  41. setStartedFreeTrial={() => null}
  42. onSelect={onSelect}
  43. onInit={onInit}
  44. isConnected
  45. />,
  46. {
  47. socket: new MockedSocket(),
  48. rootFolder,
  49. projectId: '123abc',
  50. }
  51. )
  52. const treeitem = screen.getByRole('treeitem', { name: 'main.tex' })
  53. fireEvent.click(treeitem)
  54. const toggleButton = screen.getByRole('button', { name: 'Menu' })
  55. fireEvent.click(toggleButton)
  56. const deleteButton = screen.getByRole('menuitem', { name: 'Delete' })
  57. fireEvent.click(deleteButton)
  58. })
  59. it('removes item', async function () {
  60. const fetchMatcher = /\/project\/\w+\/doc\/\w+/
  61. fetchMock.delete(fetchMatcher, 204)
  62. const modalDeleteButton = await getModalDeleteButton()
  63. fireEvent.click(modalDeleteButton)
  64. window._ide.socket.socketClient.emit('removeEntity', '456def')
  65. await waitFor(() => {
  66. expect(
  67. screen.queryByRole('treeitem', {
  68. name: 'main.tex',
  69. hidden: true, // treeitem might be hidden behind the modal
  70. })
  71. ).to.not.exist
  72. expect(
  73. screen.queryByRole('treeitem', {
  74. name: 'main.tex',
  75. })
  76. ).to.not.exist
  77. // check that the confirmation modal is closed
  78. expect(screen.queryByText(/Are you sure/)).to.not.exist
  79. })
  80. const [lastFetchPath] = fetchMock.lastCall(fetchMatcher)
  81. expect(lastFetchPath).to.equal('/project/123abc/doc/456def')
  82. })
  83. it('continues delete on 404s', async function () {
  84. fetchMock.delete(/\/project\/\w+\/doc\/\w+/, 404)
  85. const modalDeleteButton = await getModalDeleteButton()
  86. fireEvent.click(modalDeleteButton)
  87. window._ide.socket.socketClient.emit('removeEntity', '456def')
  88. // check that the confirmation modal is open
  89. screen.getByText(/Are you sure/)
  90. await waitFor(() => {
  91. expect(
  92. screen.queryByRole('treeitem', {
  93. name: 'main.tex',
  94. hidden: true, // treeitem might be hidden behind the modal
  95. })
  96. ).to.not.exist
  97. expect(
  98. screen.queryByRole('treeitem', {
  99. name: 'main.tex',
  100. })
  101. ).to.not.exist
  102. // check that the confirmation modal is closed
  103. // is not, the 404 probably triggered a bug
  104. expect(screen.queryByText(/Are you sure/)).to.not.exist
  105. })
  106. })
  107. it('aborts delete on error', async function () {
  108. const fetchMatcher = /\/project\/\w+\/doc\/\w+/
  109. fetchMock.delete(fetchMatcher, 500)
  110. const modalDeleteButton = await getModalDeleteButton()
  111. fireEvent.click(modalDeleteButton)
  112. // The modal should still be open, but the file should not be deleted
  113. await screen.findByRole('treeitem', { name: 'main.tex', hidden: true })
  114. })
  115. })
  116. describe('folders', function () {
  117. beforeEach(function () {
  118. const rootFolder = [
  119. {
  120. _id: 'root-folder-id',
  121. name: 'rootFolder',
  122. docs: [{ _id: '456def', name: 'main.tex' }],
  123. folders: [
  124. {
  125. _id: '123abc',
  126. name: 'folder',
  127. docs: [],
  128. folders: [],
  129. fileRefs: [{ _id: '789ghi', name: 'my.bib' }],
  130. },
  131. ],
  132. fileRefs: [],
  133. },
  134. ]
  135. renderWithEditorContext(
  136. <FileTreeRoot
  137. refProviders={{}}
  138. reindexReferences={() => null}
  139. setRefProviderEnabled={() => null}
  140. setStartedFreeTrial={() => null}
  141. onSelect={onSelect}
  142. onInit={onInit}
  143. isConnected
  144. />,
  145. {
  146. socket: new MockedSocket(),
  147. rootFolder,
  148. projectId: '123abc',
  149. }
  150. )
  151. const expandButton = screen.queryByRole('button', { name: 'Expand' })
  152. if (expandButton) fireEvent.click(expandButton)
  153. const treeitemDoc = screen.getByRole('treeitem', { name: 'main.tex' })
  154. fireEvent.click(treeitemDoc)
  155. const treeitemFile = screen.getByRole('treeitem', { name: 'my.bib' })
  156. fireEvent.click(treeitemFile, { ctrlKey: true })
  157. window._ide.socket.socketClient.emit('removeEntity', '123abc')
  158. })
  159. it('removes the folder', function () {
  160. expect(screen.queryByRole('treeitem', { name: 'folder' })).to.not.exist
  161. })
  162. it('leaves the main file selected', function () {
  163. screen.getByRole('treeitem', { name: 'main.tex', selected: true })
  164. })
  165. it('unselect the child entity', async function () {
  166. // as a proxy to check that the child entity has been unselect we start
  167. // a delete and ensure the modal is displayed (the cancel button can be
  168. // selected) This is needed to make sure the test fail.
  169. const toggleButton = screen.getByRole('button', { name: 'Menu' })
  170. fireEvent.click(toggleButton)
  171. const deleteButton = screen.getByRole('menuitem', { name: 'Delete' })
  172. fireEvent.click(deleteButton)
  173. await waitFor(() => screen.getByRole('button', { name: 'Cancel' }))
  174. })
  175. })
  176. describe('multiple entities', function () {
  177. beforeEach(async function () {
  178. const rootFolder = [
  179. {
  180. _id: 'root-folder-id',
  181. name: 'rootFolder',
  182. docs: [{ _id: '456def', name: 'main.tex' }],
  183. folders: [],
  184. fileRefs: [{ _id: '789ghi', name: 'my.bib' }],
  185. },
  186. ]
  187. renderWithEditorContext(
  188. <FileTreeRoot
  189. refProviders={{}}
  190. reindexReferences={() => null}
  191. setRefProviderEnabled={() => null}
  192. setStartedFreeTrial={() => null}
  193. onSelect={onSelect}
  194. onInit={onInit}
  195. isConnected
  196. />,
  197. {
  198. socket: new MockedSocket(),
  199. rootFolder,
  200. projectId: '123abc',
  201. }
  202. )
  203. // select two files
  204. const treeitemDoc = screen.getByRole('treeitem', { name: 'main.tex' })
  205. fireEvent.click(treeitemDoc)
  206. const treeitemFile = screen.getByRole('treeitem', { name: 'my.bib' })
  207. fireEvent.click(treeitemFile, { ctrlKey: true })
  208. // open the context menu
  209. const treeitemButton = screen.getByRole('button', { name: 'my.bib' })
  210. fireEvent.contextMenu(treeitemButton)
  211. // make sure the menu has opened, with only a "Delete" item (as multiple files are selected)
  212. screen.getByRole('menu')
  213. const menuItems = await screen.findAllByRole('menuitem')
  214. expect(menuItems.length).to.equal(1)
  215. // select the Delete menu item
  216. const deleteButton = screen.getByRole('menuitem', { name: 'Delete' })
  217. fireEvent.click(deleteButton)
  218. })
  219. it('removes all items', async function () {
  220. const fetchMatcher = /\/project\/\w+\/(doc|file)\/\w+/
  221. fetchMock.delete(fetchMatcher, 204)
  222. const modalDeleteButton = await getModalDeleteButton()
  223. fireEvent.click(modalDeleteButton)
  224. window._ide.socket.socketClient.emit('removeEntity', '456def')
  225. window._ide.socket.socketClient.emit('removeEntity', '789ghi')
  226. await waitFor(() => {
  227. for (const name of ['main.tex', 'my.bib']) {
  228. expect(
  229. screen.queryByRole('treeitem', {
  230. name,
  231. hidden: true, // treeitem might be hidden behind the modal
  232. })
  233. ).to.not.exist
  234. expect(
  235. screen.queryByRole('treeitem', {
  236. name,
  237. })
  238. ).to.not.exist
  239. // check that the confirmation modal is closed
  240. expect(screen.queryByText(/Are you sure/)).to.not.exist
  241. }
  242. })
  243. const [firstFetchPath, secondFetchPath] = fetchMock
  244. .calls()
  245. .map(([url]) => url)
  246. expect(firstFetchPath).to.equal('/project/123abc/doc/456def')
  247. expect(secondFetchPath).to.equal('/project/123abc/file/789ghi')
  248. })
  249. })
  250. async function getModalDeleteButton() {
  251. return waitFor(() => screen.getByRole('button', { name: 'Delete' }))
  252. }
  253. })