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

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