delete-entity.test.js 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  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 = 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 = 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 = 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('multiple entities', function() {
  97. beforeEach(function() {
  98. const rootFolder = [
  99. {
  100. _id: 'root-folder-id',
  101. docs: [{ _id: '456def', name: 'main.tex' }],
  102. folders: [],
  103. fileRefs: [{ _id: '789ghi', name: 'my.bib' }]
  104. }
  105. ]
  106. render(
  107. <FileTreeRoot
  108. rootFolder={rootFolder}
  109. projectId="123abc"
  110. hasWritePermissions
  111. onSelect={onSelect}
  112. onInit={onInit}
  113. />
  114. )
  115. const treeitemDoc = screen.getByRole('treeitem', { name: 'main.tex' })
  116. fireEvent.click(treeitemDoc)
  117. const treeitemFile = screen.getByRole('treeitem', { name: 'my.bib' })
  118. fireEvent.click(treeitemFile, { ctrlKey: true })
  119. const deleteButton = screen.getAllByRole('menuitem', {
  120. name: 'Delete'
  121. })[0]
  122. fireEvent.click(deleteButton)
  123. })
  124. it('removes all items', async function() {
  125. const fetchMatcher = /\/project\/\w+\/(doc|file)\/\w+/
  126. fetchMock.delete(fetchMatcher, 204)
  127. const modalDeleteButton = getModalDeleteButton()
  128. fireEvent.click(modalDeleteButton)
  129. window._ide.socket.socketClient.emit('removeEntity', '456def')
  130. window._ide.socket.socketClient.emit('removeEntity', '789ghi')
  131. await waitFor(() => {
  132. for (const name of ['main.tex', 'my.bib']) {
  133. expect(
  134. screen.queryByRole('treeitem', {
  135. name,
  136. hidden: true // treeitem might be hidden behind the modal
  137. })
  138. ).to.not.exist
  139. expect(
  140. screen.queryByRole('treeitem', {
  141. name
  142. })
  143. ).to.not.exist
  144. }
  145. })
  146. const [firstFetchPath, secondFetchPath] = fetchMock
  147. .calls()
  148. .map(([url]) => url)
  149. expect(firstFetchPath).to.equal('/project/123abc/doc/456def')
  150. expect(secondFetchPath).to.equal('/project/123abc/file/789ghi')
  151. })
  152. })
  153. function getModalDeleteButton() {
  154. return screen.getAllByRole('button', {
  155. name: 'Delete',
  156. hidden: true // FIXME: modal should not be hidden but it has the aria-hidden label due to a react-bootstrap bug
  157. })[1] // the first matched button is the toolbar button
  158. }
  159. })