delete-entity.test.js 8.9 KB

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