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

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