delete-entity.test.js 7.5 KB

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