delete-entity.test.js 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  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. })
  66. const [lastFetchPath] = fetchMock.lastCall(fetchMatcher)
  67. expect(lastFetchPath).to.equal('/project/123abc/doc/456def')
  68. })
  69. it('continues delete on 404s', async function() {
  70. fetchMock.delete(/\/project\/\w+\/doc\/\w+/, 404)
  71. const modalDeleteButton = await getModalDeleteButton()
  72. fireEvent.click(modalDeleteButton)
  73. window._ide.socket.socketClient.emit('removeEntity', '456def')
  74. await waitFor(() => {
  75. expect(
  76. screen.queryByRole('treeitem', {
  77. name: 'main.tex',
  78. hidden: true // treeitem might be hidden behind the modal
  79. })
  80. ).to.not.exist
  81. expect(
  82. screen.queryByRole('treeitem', {
  83. name: 'main.tex'
  84. })
  85. ).to.not.exist
  86. })
  87. })
  88. it('aborts delete on error', async function() {
  89. const fetchMatcher = /\/project\/\w+\/doc\/\w+/
  90. fetchMock.delete(fetchMatcher, 500)
  91. const modalDeleteButton = await getModalDeleteButton()
  92. fireEvent.click(modalDeleteButton)
  93. // The modal should still be open, but the file should not be deleted
  94. await screen.findByRole('treeitem', { name: 'main.tex', hidden: true })
  95. })
  96. })
  97. describe('folders', function() {
  98. beforeEach(function() {
  99. const rootFolder = [
  100. {
  101. docs: [{ _id: '456def', name: 'main.tex' }],
  102. folders: [
  103. {
  104. _id: '123abc',
  105. name: 'folder',
  106. docs: [],
  107. folders: [],
  108. fileRefs: [{ _id: '789ghi', name: 'my.bib' }]
  109. }
  110. ],
  111. fileRefs: []
  112. }
  113. ]
  114. render(
  115. <FileTreeRoot
  116. rootFolder={rootFolder}
  117. projectId="123abc"
  118. hasWritePermissions
  119. onSelect={onSelect}
  120. onInit={onInit}
  121. isConnected
  122. />
  123. )
  124. const expandButton = screen.queryByRole('button', { name: 'Expand' })
  125. if (expandButton) fireEvent.click(expandButton)
  126. const treeitemDoc = screen.getByRole('treeitem', { name: 'main.tex' })
  127. fireEvent.click(treeitemDoc)
  128. const treeitemFile = screen.getByRole('treeitem', { name: 'my.bib' })
  129. fireEvent.click(treeitemFile, { ctrlKey: true })
  130. window._ide.socket.socketClient.emit('removeEntity', '123abc')
  131. })
  132. it('removes the folder', function() {
  133. expect(screen.queryByRole('treeitem', { name: 'folder' })).to.not.exist
  134. })
  135. it('leaves the main file selected', function() {
  136. screen.getByRole('treeitem', { name: 'main.tex', selected: true })
  137. })
  138. it('unselect the child entity', async function() {
  139. // as a proxy to check that the child entity has been unselect we start
  140. // a delete and ensure the modal is displayed (the cancel button can be
  141. // selected) This is needed to make sure the test fail.
  142. const deleteButton = screen.getByRole('menuitem', { name: 'Delete' })
  143. fireEvent.click(deleteButton)
  144. await waitFor(() => screen.getByRole('button', { name: 'Cancel' }))
  145. })
  146. })
  147. describe('multiple entities', function() {
  148. beforeEach(function() {
  149. const rootFolder = [
  150. {
  151. _id: 'root-folder-id',
  152. docs: [{ _id: '456def', name: 'main.tex' }],
  153. folders: [],
  154. fileRefs: [{ _id: '789ghi', name: 'my.bib' }]
  155. }
  156. ]
  157. render(
  158. <FileTreeRoot
  159. rootFolder={rootFolder}
  160. projectId="123abc"
  161. hasWritePermissions
  162. onSelect={onSelect}
  163. onInit={onInit}
  164. isConnected
  165. />
  166. )
  167. const treeitemDoc = screen.getByRole('treeitem', { name: 'main.tex' })
  168. fireEvent.click(treeitemDoc)
  169. const treeitemFile = screen.getByRole('treeitem', { name: 'my.bib' })
  170. fireEvent.click(treeitemFile, { ctrlKey: true })
  171. const deleteButton = screen.getAllByRole('menuitem', {
  172. name: 'Delete'
  173. })[0]
  174. fireEvent.click(deleteButton)
  175. })
  176. it('removes all items', async function() {
  177. const fetchMatcher = /\/project\/\w+\/(doc|file)\/\w+/
  178. fetchMock.delete(fetchMatcher, 204)
  179. const modalDeleteButton = await getModalDeleteButton()
  180. fireEvent.click(modalDeleteButton)
  181. window._ide.socket.socketClient.emit('removeEntity', '456def')
  182. window._ide.socket.socketClient.emit('removeEntity', '789ghi')
  183. await waitFor(() => {
  184. for (const name of ['main.tex', 'my.bib']) {
  185. expect(
  186. screen.queryByRole('treeitem', {
  187. name,
  188. hidden: true // treeitem might be hidden behind the modal
  189. })
  190. ).to.not.exist
  191. expect(
  192. screen.queryByRole('treeitem', {
  193. name
  194. })
  195. ).to.not.exist
  196. }
  197. })
  198. const [firstFetchPath, secondFetchPath] = fetchMock
  199. .calls()
  200. .map(([url]) => url)
  201. expect(firstFetchPath).to.equal('/project/123abc/doc/456def')
  202. expect(secondFetchPath).to.equal('/project/123abc/file/789ghi')
  203. })
  204. })
  205. async function getModalDeleteButton() {
  206. return waitFor(() => screen.getByRole('button', { name: 'Delete' }))
  207. }
  208. })