delete-entity.test.js 8.9 KB

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