delete-entity.test.js 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  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. docs: [{ _id: '456def', name: 'main.tex' }],
  27. folders: [],
  28. fileRefs: []
  29. }
  30. ]
  31. render(
  32. <FileTreeRoot
  33. rootFolder={rootFolder}
  34. projectId={'123abc'}
  35. hasWritePermissions
  36. onSelect={onSelect}
  37. onInit={onInit}
  38. />
  39. )
  40. const treeitem = screen.getByRole('treeitem', { name: 'main.tex' })
  41. fireEvent.click(treeitem)
  42. const deleteButton = screen.getByRole('menuitem', { name: 'Delete' })
  43. fireEvent.click(deleteButton)
  44. })
  45. it('removes item', async function() {
  46. const fetchMatcher = /\/project\/\w+\/doc\/\w+/
  47. fetchMock.delete(fetchMatcher, 204)
  48. const modalDeleteButton = getModalDeleteButton()
  49. fireEvent.click(modalDeleteButton)
  50. window._ide.socket.socketClient.emit('removeEntity', '456def')
  51. await waitFor(() => {
  52. expect(
  53. screen.queryByRole('treeitem', {
  54. name: 'main.tex',
  55. hidden: true // treeitem might be hidden behind the modal
  56. })
  57. ).to.not.exist
  58. expect(
  59. screen.queryByRole('treeitem', {
  60. name: 'main.tex'
  61. })
  62. ).to.not.exist
  63. })
  64. const [lastFetchPath] = fetchMock.lastCall(fetchMatcher)
  65. expect(lastFetchPath).to.equal('/project/123abc/doc/456def')
  66. })
  67. it('continues delete on 404s', async function() {
  68. fetchMock.delete(/\/project\/\w+\/doc\/\w+/, 404)
  69. const modalDeleteButton = getModalDeleteButton()
  70. fireEvent.click(modalDeleteButton)
  71. window._ide.socket.socketClient.emit('removeEntity', '456def')
  72. await waitFor(() => {
  73. expect(
  74. screen.queryByRole('treeitem', {
  75. name: 'main.tex',
  76. hidden: true // treeitem might be hidden behind the modal
  77. })
  78. ).to.not.exist
  79. expect(
  80. screen.queryByRole('treeitem', {
  81. name: 'main.tex'
  82. })
  83. ).to.not.exist
  84. })
  85. })
  86. it('aborts delete on error', async function() {
  87. const fetchMatcher = /\/project\/\w+\/doc\/\w+/
  88. fetchMock.delete(fetchMatcher, 500)
  89. const modalDeleteButton = getModalDeleteButton()
  90. fireEvent.click(modalDeleteButton)
  91. // The modal should still be open, but the file should not be deleted
  92. await screen.findByRole('treeitem', { name: 'main.tex', hidden: true })
  93. })
  94. })
  95. describe('multiple entities', function() {
  96. beforeEach(function() {
  97. const rootFolder = [
  98. {
  99. docs: [{ _id: '456def', name: 'main.tex' }],
  100. folders: [],
  101. fileRefs: [{ _id: '789ghi', name: 'my.bib' }]
  102. }
  103. ]
  104. render(
  105. <FileTreeRoot
  106. rootFolder={rootFolder}
  107. projectId={'123abc'}
  108. hasWritePermissions
  109. onSelect={onSelect}
  110. onInit={onInit}
  111. />
  112. )
  113. const treeitemDoc = screen.getByRole('treeitem', { name: 'main.tex' })
  114. fireEvent.click(treeitemDoc)
  115. const treeitemFile = screen.getByRole('treeitem', { name: 'my.bib' })
  116. fireEvent.click(treeitemFile, { ctrlKey: true })
  117. const deleteButton = screen.getAllByRole('menuitem', {
  118. name: 'Delete'
  119. })[0]
  120. fireEvent.click(deleteButton)
  121. })
  122. it('removes all items', async function() {
  123. const fetchMatcher = /\/project\/\w+\/(doc|file)\/\w+/
  124. fetchMock.delete(fetchMatcher, 204)
  125. const modalDeleteButton = getModalDeleteButton()
  126. fireEvent.click(modalDeleteButton)
  127. window._ide.socket.socketClient.emit('removeEntity', '456def')
  128. window._ide.socket.socketClient.emit('removeEntity', '789ghi')
  129. await waitFor(() => {
  130. for (const name of ['main.tex', 'my.bib']) {
  131. expect(
  132. screen.queryByRole('treeitem', {
  133. name,
  134. hidden: true // treeitem might be hidden behind the modal
  135. })
  136. ).to.not.exist
  137. expect(
  138. screen.queryByRole('treeitem', {
  139. name
  140. })
  141. ).to.not.exist
  142. }
  143. })
  144. const [firstFetchPath, secondFetchPath] = fetchMock
  145. .calls()
  146. .map(([url]) => url)
  147. expect(firstFetchPath).to.equal('/project/123abc/doc/456def')
  148. expect(secondFetchPath).to.equal('/project/123abc/file/789ghi')
  149. })
  150. })
  151. function getModalDeleteButton() {
  152. return screen.getAllByRole('button', {
  153. name: 'Delete',
  154. hidden: true // FIXME: modal should not be hidden but it has the aria-hidden label due to a react-bootstrap bug
  155. })[1] // the first matched button is the toolbar button
  156. }
  157. })