delete-entity.test.js 7.0 KB

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