file-tree-modal-create-file.spec.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498
  1. import '../../../../helpers/bootstrap-3'
  2. import { useEffect } from 'react'
  3. import FileTreeModalCreateFile from '../../../../../../frontend/js/features/file-tree/components/modals/file-tree-modal-create-file'
  4. import { useFileTreeActionable } from '../../../../../../frontend/js/features/file-tree/contexts/file-tree-actionable'
  5. import { useFileTreeData } from '../../../../../../frontend/js/shared/context/file-tree-data-context'
  6. import { EditorProviders } from '../../../../helpers/editor-providers'
  7. import { FileTreeProvider } from '../../helpers/file-tree-provider'
  8. import getMeta from '@/utils/meta'
  9. describe('<FileTreeModalCreateFile/>', function () {
  10. it('handles invalid file names', function () {
  11. cy.mount(
  12. <EditorProviders>
  13. <FileTreeProvider>
  14. <OpenWithMode mode="doc" />
  15. </FileTreeProvider>
  16. </EditorProviders>
  17. )
  18. cy.findByLabelText('File Name').as('input')
  19. cy.findByRole('button', { name: 'Create' }).as('submit')
  20. cy.get('@input').should('have.value', 'name.tex')
  21. cy.get('@submit').should('not.be.disabled')
  22. cy.findByRole('alert').should('not.exist')
  23. cy.get('@input').clear()
  24. cy.get('@submit').should('be.disabled')
  25. cy.findByRole('alert').should('contain.text', 'File name is empty')
  26. cy.get('@input').type('test.tex')
  27. cy.get('@submit').should('not.be.disabled')
  28. cy.findByRole('alert').should('not.exist')
  29. cy.get('@input').type('oops/i/did/it/again')
  30. cy.get('@submit').should('be.disabled')
  31. cy.findByRole('alert').should('contain.text', 'contains invalid characters')
  32. })
  33. it('displays an error when the file limit is reached', function () {
  34. getMeta('ol-ExposedSettings').maxEntitiesPerProject = 10
  35. const rootFolder = [
  36. {
  37. _id: 'root-folder-id',
  38. name: 'rootFolder',
  39. docs: Array.from({ length: 10 }, (_, index) => ({
  40. _id: `entity-${index}`,
  41. })),
  42. fileRefs: [],
  43. folders: [],
  44. },
  45. ]
  46. cy.mount(
  47. <EditorProviders rootFolder={rootFolder as any}>
  48. <FileTreeProvider>
  49. <OpenWithMode mode="doc" />
  50. </FileTreeProvider>
  51. </EditorProviders>
  52. )
  53. cy.findByRole('alert')
  54. .invoke('text')
  55. .should('match', /This project has reached the \d+ file limit/)
  56. })
  57. it('displays a warning when the file limit is nearly reached', function () {
  58. getMeta('ol-ExposedSettings').maxEntitiesPerProject = 10
  59. const rootFolder = [
  60. {
  61. _id: 'root-folder-id',
  62. name: 'rootFolder',
  63. docs: Array.from({ length: 9 }, (_, index) => ({
  64. _id: `entity-${index}`,
  65. })),
  66. fileRefs: [],
  67. folders: [],
  68. },
  69. ]
  70. cy.mount(
  71. <EditorProviders rootFolder={rootFolder as any}>
  72. <FileTreeProvider>
  73. <OpenWithMode mode="doc" />
  74. </FileTreeProvider>
  75. </EditorProviders>
  76. )
  77. cy.findByText(/This project is approaching the file limit \(\d+\/\d+\)/)
  78. })
  79. it('counts files in nested folders', function () {
  80. getMeta('ol-ExposedSettings').maxEntitiesPerProject = 10
  81. const rootFolder = [
  82. {
  83. _id: 'root-folder-id',
  84. name: 'rootFolder',
  85. docs: [{ _id: 'doc-1' }],
  86. fileRefs: [],
  87. folders: [
  88. {
  89. docs: [{ _id: 'doc-2' }],
  90. fileRefs: [],
  91. folders: [
  92. {
  93. docs: [
  94. { _id: 'doc-3' },
  95. { _id: 'doc-4' },
  96. { _id: 'doc-5' },
  97. { _id: 'doc-6' },
  98. { _id: 'doc-7' },
  99. ],
  100. fileRefs: [],
  101. folders: [],
  102. },
  103. ],
  104. },
  105. ],
  106. },
  107. ]
  108. cy.mount(
  109. <EditorProviders rootFolder={rootFolder as any}>
  110. <FileTreeProvider>
  111. <OpenWithMode mode="doc" />
  112. </FileTreeProvider>
  113. </EditorProviders>
  114. )
  115. cy.findByText(/This project is approaching the file limit \(\d+\/\d+\)/)
  116. })
  117. it('counts folders toward the limit', function () {
  118. getMeta('ol-ExposedSettings').maxEntitiesPerProject = 10
  119. const rootFolder = [
  120. {
  121. _id: 'root-folder-id',
  122. name: 'rootFolder',
  123. docs: [{ _id: 'doc-1' }],
  124. fileRefs: [],
  125. folders: [
  126. { docs: [], fileRefs: [], folders: [] },
  127. { docs: [], fileRefs: [], folders: [] },
  128. { docs: [], fileRefs: [], folders: [] },
  129. { docs: [], fileRefs: [], folders: [] },
  130. { docs: [], fileRefs: [], folders: [] },
  131. { docs: [], fileRefs: [], folders: [] },
  132. { docs: [], fileRefs: [], folders: [] },
  133. { docs: [], fileRefs: [], folders: [] },
  134. ],
  135. },
  136. ]
  137. cy.mount(
  138. <EditorProviders rootFolder={rootFolder as any}>
  139. <FileTreeProvider>
  140. <OpenWithMode mode="doc" />
  141. </FileTreeProvider>
  142. </EditorProviders>
  143. )
  144. cy.findByText(/This project is approaching the file limit \(\d+\/\d+\)/)
  145. })
  146. it('creates a new file when the form is submitted', function () {
  147. cy.intercept('post', '/project/*/doc', {
  148. statusCode: 204,
  149. }).as('createDoc')
  150. cy.mount(
  151. <EditorProviders>
  152. <FileTreeProvider>
  153. <OpenWithMode mode="doc" />
  154. </FileTreeProvider>
  155. </EditorProviders>
  156. )
  157. cy.findByLabelText('File Name').type('test')
  158. cy.findByRole('button', { name: 'Create' }).click()
  159. cy.wait('@createDoc')
  160. cy.get('@createDoc').its('request.body').should('deep.equal', {
  161. parent_folder_id: 'root-folder-id',
  162. name: 'test.tex',
  163. })
  164. })
  165. it('imports a new file from a project', function () {
  166. getMeta('ol-ExposedSettings').hasLinkedProjectFileFeature = true
  167. getMeta('ol-ExposedSettings').hasLinkedProjectOutputFileFeature = true
  168. cy.intercept('/user/projects', {
  169. body: {
  170. projects: [
  171. {
  172. _id: 'test-project',
  173. name: 'This Project',
  174. },
  175. {
  176. _id: 'project-1',
  177. name: 'Project One',
  178. },
  179. {
  180. _id: 'project-2',
  181. name: 'Project Two',
  182. },
  183. ],
  184. },
  185. })
  186. cy.intercept('/project/*/entities', {
  187. body: {
  188. entities: [
  189. {
  190. path: '/foo.tex',
  191. },
  192. {
  193. path: '/bar.tex',
  194. },
  195. ],
  196. },
  197. })
  198. cy.intercept('post', '/project/*/compile', {
  199. body: {
  200. status: 'success',
  201. outputFiles: [
  202. {
  203. build: 'test',
  204. path: 'baz.jpg',
  205. },
  206. {
  207. build: 'test',
  208. path: 'ball.jpg',
  209. },
  210. ],
  211. },
  212. })
  213. cy.intercept('post', '/project/*/linked_file', {
  214. statusCode: 204,
  215. }).as('createLinkedFile')
  216. cy.mount(
  217. <EditorProviders>
  218. <FileTreeProvider>
  219. <OpenWithMode mode="project" />
  220. </FileTreeProvider>
  221. </EditorProviders>
  222. )
  223. // initial state, no project selected
  224. cy.findByLabelText('Select a Project').should('not.be.disabled')
  225. // the submit button should be disabled
  226. cy.findByRole('button', { name: 'Create' }).should('be.disabled')
  227. // the source file selector should be disabled
  228. cy.findByLabelText('Select a File').should('be.disabled')
  229. cy.findByLabelText('Select an Output File').should('not.exist')
  230. // TODO: check for options length, excluding current project
  231. // select a project
  232. cy.findByLabelText('Select a Project').select('project-2')
  233. // wait for the source file selector to be enabled
  234. cy.findByLabelText('Select a File').should('not.be.disabled')
  235. cy.findByLabelText('Select an Output File').should('not.exist')
  236. cy.findByRole('button', { name: 'Create' }).should('be.disabled')
  237. // TODO: check for fileInput options length, excluding current project
  238. // click on the button to toggle between source and output files
  239. cy.findByRole('button', {
  240. // NOTE: When changing the label, update the other tests with this label as well.
  241. name: 'select from output files',
  242. }).click()
  243. // wait for the output file selector to be enabled
  244. cy.findByLabelText('Select an Output File').should('not.be.disabled')
  245. cy.findByLabelText('Select a File').should('not.exist')
  246. cy.findByRole('button', { name: 'Create' }).should('be.disabled')
  247. // TODO: check for entityInput options length, excluding current project
  248. cy.findByLabelText('Select an Output File').select('ball.jpg')
  249. cy.findByRole('button', { name: 'Create' }).should('not.be.disabled')
  250. cy.findByRole('button', { name: 'Create' }).click()
  251. cy.get('@createLinkedFile')
  252. .its('request.body')
  253. .should('deep.equal', {
  254. name: 'ball.jpg',
  255. provider: 'project_output_file',
  256. parent_folder_id: 'root-folder-id',
  257. data: {
  258. source_project_id: 'project-2',
  259. source_output_file_path: 'ball.jpg',
  260. build_id: 'test',
  261. },
  262. })
  263. })
  264. describe('when the output files feature is not available', function () {
  265. beforeEach(function () {
  266. getMeta('ol-ExposedSettings').hasLinkedProjectFileFeature = true
  267. getMeta('ol-ExposedSettings').hasLinkedProjectOutputFileFeature = false
  268. })
  269. it('should not show the import from output file mode', function () {
  270. cy.intercept('/user/projects', {
  271. body: {
  272. projects: [
  273. {
  274. _id: 'test-project',
  275. name: 'This Project',
  276. },
  277. {
  278. _id: 'project-1',
  279. name: 'Project One',
  280. },
  281. {
  282. _id: 'project-2',
  283. name: 'Project Two',
  284. },
  285. ],
  286. },
  287. })
  288. cy.mount(
  289. <EditorProviders>
  290. <FileTreeProvider>
  291. <OpenWithMode mode="project" />
  292. </FileTreeProvider>
  293. </EditorProviders>
  294. )
  295. cy.findByLabelText('Select a File')
  296. cy.findByRole('button', {
  297. name: 'select from output files',
  298. }).should('not.exist')
  299. })
  300. })
  301. it('import from a URL when the form is submitted', function () {
  302. cy.intercept('/project/*/linked_file', {
  303. statusCode: 204,
  304. }).as('createLinkedFile')
  305. cy.mount(
  306. <EditorProviders>
  307. <FileTreeProvider>
  308. <OpenWithMode mode="url" />
  309. </FileTreeProvider>
  310. </EditorProviders>
  311. )
  312. cy.findByLabelText('URL to fetch the file from').type(
  313. 'https://example.com/example.tex'
  314. )
  315. cy.findByLabelText('File Name In This Project').should(
  316. 'have.value',
  317. 'example.tex'
  318. )
  319. // check that the name can still be edited manually
  320. cy.findByLabelText('File Name In This Project').clear()
  321. cy.findByLabelText('File Name In This Project').type('test.tex')
  322. cy.findByLabelText('File Name In This Project').should(
  323. 'have.value',
  324. 'test.tex'
  325. )
  326. cy.findByRole('button', { name: 'Create' }).click()
  327. cy.get('@createLinkedFile')
  328. .its('request.body')
  329. .should('deep.equal', {
  330. name: 'test.tex',
  331. provider: 'url',
  332. parent_folder_id: 'root-folder-id',
  333. data: { url: 'https://example.com/example.tex' },
  334. })
  335. })
  336. it('uploads a dropped file', function () {
  337. cy.intercept('post', '/project/*/upload?folder_id=root-folder-id', {
  338. statusCode: 204,
  339. }).as('uploadFile')
  340. cy.mount(
  341. <EditorProviders>
  342. <FileTreeProvider>
  343. <OpenWithMode mode="upload" />
  344. </FileTreeProvider>
  345. </EditorProviders>
  346. )
  347. // the submit button should not be present
  348. cy.findByRole('button', { name: 'Create' }).should('not.exist')
  349. cy.get('input[type=file]')
  350. .eq(0)
  351. .selectFile(
  352. {
  353. contents: Cypress.Buffer.from('test'),
  354. fileName: 'test.tex',
  355. mimeType: 'text/plain',
  356. lastModified: Date.now(),
  357. },
  358. {
  359. action: 'drag-drop',
  360. force: true, // invisible element
  361. }
  362. )
  363. cy.wait('@uploadFile')
  364. })
  365. it('uploads a pasted file', function () {
  366. cy.intercept('post', '/project/*/upload?folder_id=root-folder-id', {
  367. statusCode: 204,
  368. }).as('uploadFile')
  369. cy.mount(
  370. <EditorProviders>
  371. <FileTreeProvider>
  372. <OpenWithMode mode="upload" />
  373. </FileTreeProvider>
  374. </EditorProviders>
  375. )
  376. // the submit button should not be present
  377. cy.findByRole('button', { name: 'Create' }).should('not.exist')
  378. cy.wrap(null).then(() => {
  379. const clipboardData = new DataTransfer()
  380. clipboardData.items.add(
  381. new File(['test'], 'test.tex', { type: 'text/plain' })
  382. )
  383. cy.findByLabelText('Uppy Dashboard').trigger('paste', { clipboardData })
  384. })
  385. cy.wait('@uploadFile')
  386. })
  387. it('displays upload errors', function () {
  388. cy.intercept('post', '/project/*/upload?folder_id=root-folder-id', {
  389. statusCode: 422,
  390. body: { success: false, error: 'invalid_filename' },
  391. }).as('uploadFile')
  392. cy.mount(
  393. <EditorProviders>
  394. <FileTreeProvider>
  395. <OpenWithMode mode="upload" />
  396. </FileTreeProvider>
  397. </EditorProviders>
  398. )
  399. // the submit button should not be present
  400. cy.findByRole('button', { name: 'Create' }).should('not.exist')
  401. cy.wrap(null).then(() => {
  402. const clipboardData = new DataTransfer()
  403. clipboardData.items.add(
  404. new File(['test'], 'tes!t.tex', { type: 'text/plain' })
  405. )
  406. cy.findByLabelText('Uppy Dashboard').trigger('paste', { clipboardData })
  407. })
  408. cy.wait('@uploadFile')
  409. cy.findByText(
  410. `Upload failed: check that the file name doesn’t contain special characters, trailing/leading whitespace or more than 150 characters`
  411. )
  412. })
  413. })
  414. function OpenWithMode({ mode }: { mode: string }) {
  415. const { newFileCreateMode, startCreatingFile } = useFileTreeActionable()
  416. const { fileCount } = useFileTreeData()
  417. // eslint-disable-next-line react-hooks/exhaustive-deps
  418. useEffect(() => startCreatingFile(mode), [])
  419. if (!fileCount || !newFileCreateMode) {
  420. return null
  421. }
  422. return <FileTreeModalCreateFile />
  423. }