delete-entity.test.jsx 9.3 KB

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