figure-modal.spec.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418
  1. import CodemirrorEditor from '../../../../../frontend/js/features/source-editor/components/codemirror-editor'
  2. import { EditorProviders } from '../../../helpers/editor-providers'
  3. import { mockScope, rootFolderId } from '../helpers/mock-scope'
  4. import { FC } from 'react'
  5. import { FileTreePathContext } from '@/features/file-tree/contexts/file-tree-path'
  6. const Container: FC = ({ children }) => (
  7. <div style={{ width: 1500, height: 785 }}>{children}</div>
  8. )
  9. const clickToolbarButton = (text: string) => {
  10. cy.findByLabelText(text).click()
  11. cy.findByLabelText(text).trigger('mouseout')
  12. }
  13. const chooseFileFromComputer = () => {
  14. cy.get('@file-input').selectFile(
  15. {
  16. fileName: 'frog.jpg',
  17. contents: Cypress.Buffer.from('image-data'),
  18. mimeType: 'image/jpg',
  19. },
  20. {
  21. force: true,
  22. }
  23. )
  24. }
  25. const matchUrl = (urlToMatch: RegExp | string) =>
  26. Cypress.sinon.match(req => {
  27. if (!req || typeof req.url !== 'string') {
  28. return false
  29. }
  30. if (typeof urlToMatch === 'string') {
  31. return req.url.endsWith(urlToMatch)
  32. }
  33. return Boolean(req.url.match(urlToMatch))
  34. })
  35. describe('<FigureModal />', function () {
  36. // TODO: rewrite these tests to be in source mode when toolbar is added there
  37. // TODO: Write tests for width toggle, when we can match on source code
  38. beforeEach(function () {
  39. window.metaAttributesCache.set('ol-preventCompileOnLoad', true)
  40. cy.interceptMathJax()
  41. cy.interceptEvents()
  42. cy.interceptSpelling()
  43. const content = ''
  44. const scope = mockScope(content)
  45. scope.editor.showVisual = true
  46. const FileTreePathProvider: FC = ({ children }) => (
  47. <FileTreePathContext.Provider
  48. value={{
  49. dirname: cy.stub(),
  50. findEntityByPath: cy.stub(),
  51. pathInFolder: cy.stub(),
  52. previewByPath: cy
  53. .stub()
  54. .as('previewByPath')
  55. .returns({ url: 'frog.jpg', extension: 'jpg' }),
  56. }}
  57. >
  58. {children}
  59. </FileTreePathContext.Provider>
  60. )
  61. cy.mount(
  62. <Container>
  63. <EditorProviders scope={scope} providers={{ FileTreePathProvider }}>
  64. <CodemirrorEditor />
  65. </EditorProviders>
  66. </Container>
  67. )
  68. })
  69. describe('Upload from computer source', function () {
  70. beforeEach(function () {
  71. cy.interceptFileUpload()
  72. clickToolbarButton('Insert Figure')
  73. cy.findByRole('menu').within(() => {
  74. cy.findByText('Upload from computer').click()
  75. })
  76. cy.findByLabelText('Uppy Dashboard')
  77. .get('.uppy-Dashboard-input:first')
  78. .as('file-input')
  79. })
  80. it('Shows file name and size when selecting file', function () {
  81. chooseFileFromComputer()
  82. cy.findByLabelText('File name').should('have.text', 'frog.jpg')
  83. cy.findByLabelText('File size').should('have.text', '10 B')
  84. })
  85. it('Uploads file when clicking insert', function () {
  86. chooseFileFromComputer()
  87. cy.get('@uploadRequest').should('not.have.been.called')
  88. cy.findByText('Insert figure').click()
  89. cy.get('@uploadRequest').should(
  90. 'have.been.calledWith',
  91. matchUrl(`/project/test-project/upload?folder_id=${rootFolderId}`)
  92. )
  93. cy.get('.cm-content').should(
  94. 'have.text',
  95. '\\begin{figure} \\centering Enter Caption 🏷fig:enter-label\\end{figure}'
  96. )
  97. })
  98. it('Enables insert button when choosing file', function () {
  99. cy.findByText('Insert figure').should('be.disabled')
  100. chooseFileFromComputer()
  101. cy.findByText('Insert figure').should('be.enabled')
  102. })
  103. })
  104. describe('Upload from project files source', function () {
  105. beforeEach(function () {
  106. clickToolbarButton('Insert Figure')
  107. cy.findByRole('menu').within(() => {
  108. cy.findByText('From project files').click()
  109. })
  110. })
  111. it('Lists files from project', function () {
  112. cy.findByText('Select image from project files').click()
  113. cy.findByRole('listbox')
  114. .children()
  115. .should('have.length', 2)
  116. .should('contain.text', 'frog.jpg')
  117. .should('contain.text', 'unicorn.png')
  118. .should('contain.text', 'figures/')
  119. })
  120. it('Enables insert button when choosing file', function () {
  121. cy.findByText('Insert figure').should('be.disabled')
  122. cy.findByText('Select image from project files').click()
  123. cy.findByRole('listbox').within(() => {
  124. cy.findByText('frog.jpg').click()
  125. })
  126. cy.findByText('Insert figure').should('be.enabled')
  127. })
  128. it('Inserts file when pressing insert button', function () {
  129. cy.findByText('Select image from project files').click()
  130. cy.findByRole('listbox').within(() => {
  131. cy.findByText('frog.jpg').click()
  132. })
  133. cy.findByText('Insert figure').click()
  134. cy.get('.cm-content').should(
  135. 'have.text',
  136. '\\begin{figure} \\centering Enter Caption 🏷fig:enter-label\\end{figure}'
  137. )
  138. })
  139. })
  140. describe('From another project source', function () {
  141. beforeEach(function () {
  142. cy.interceptProjectListing()
  143. cy.interceptCompile()
  144. cy.interceptLinkedFile()
  145. clickToolbarButton('Insert Figure')
  146. cy.findByRole('menu').within(() => {
  147. cy.findByText('From another project').click()
  148. })
  149. cy.findByText('Select a project').parent().as('project-dropdown')
  150. cy.findByText('Select a file').parent().as('file-dropdown')
  151. })
  152. it('List projects and files in projects', function () {
  153. cy.findByText('Insert figure').should('be.disabled')
  154. cy.get('@file-dropdown').should('have.class', 'disabled')
  155. cy.get('@project-dropdown').click()
  156. cy.findByRole('listbox').as('project-select')
  157. cy.get('@project-select').children().should('have.length', 2)
  158. cy.get('@project-select').within(() => {
  159. cy.findByText('My first project').click()
  160. })
  161. cy.get('@file-dropdown').should('not.have.class', 'disabled')
  162. cy.get('@file-dropdown').click()
  163. cy.findByRole('listbox').as('file-select')
  164. cy.get('@file-select').children().should('have.length', 2)
  165. cy.get('@file-select').should('contain.text', 'frog.jpg')
  166. cy.get('@file-select').should('contain.text', 'figures/unicorn.png')
  167. cy.get('@file-select').within(() => {
  168. cy.findByText('frog.jpg').click()
  169. })
  170. cy.findByText('Insert figure').should('be.enabled')
  171. })
  172. it('Enables insert button when choosing file', function () {
  173. cy.findByText('Insert figure').should('be.disabled')
  174. cy.get('@project-dropdown').click()
  175. cy.findByRole('listbox').within(() => {
  176. cy.findByText('My first project').click()
  177. })
  178. cy.get('@file-dropdown').click()
  179. cy.findByRole('listbox').within(() => {
  180. cy.findByText('frog.jpg').click()
  181. })
  182. cy.findByText('Insert figure').should('be.enabled')
  183. })
  184. it('Creates linked file when pressing insert', function () {
  185. cy.get('@project-dropdown').click()
  186. cy.findByRole('listbox').within(() => {
  187. cy.findByText('My first project').click()
  188. })
  189. cy.get('@file-dropdown').click()
  190. cy.findByRole('listbox').within(() => {
  191. cy.findByText('frog.jpg').click()
  192. })
  193. cy.findByText('Insert figure').click()
  194. cy.get('@linked-file-request').should('have.been.calledWithMatch', {
  195. body: {
  196. provider: 'project_file',
  197. data: {
  198. source_entity_path: '/frog.jpg',
  199. source_project_id: 'fake-project-1',
  200. },
  201. },
  202. })
  203. cy.get('.cm-content').should(
  204. 'have.text',
  205. '\\begin{figure} \\centering Enter Caption 🏷fig:enter-label\\end{figure}'
  206. )
  207. })
  208. it('Creates linked output file when pressing insert', function () {
  209. cy.get('@project-dropdown').click()
  210. cy.findByRole('listbox').within(() => {
  211. cy.findByText('My first project').click()
  212. })
  213. cy.findByText('select from output files').click()
  214. cy.findByText('Select an output file').parent().as('output-file-dropdown')
  215. cy.get('@output-file-dropdown').click()
  216. cy.findByRole('listbox').within(() => {
  217. cy.findByText('output.pdf').click()
  218. })
  219. cy.findByText('Insert figure').click()
  220. cy.get('@linked-file-request').should('have.been.calledWithMatch', {
  221. body: {
  222. provider: 'project_output_file',
  223. data: {
  224. source_output_file_path: 'output.pdf',
  225. source_project_id: 'fake-project-1',
  226. },
  227. },
  228. })
  229. cy.get('.cm-content').should(
  230. 'have.text',
  231. '\\begin{figure} \\centering Enter Caption 🏷fig:enter-label\\end{figure}'
  232. )
  233. })
  234. })
  235. describe('From URL source', function () {
  236. beforeEach(function () {
  237. cy.interceptLinkedFile()
  238. clickToolbarButton('Insert Figure')
  239. cy.findByRole('menu').within(() => {
  240. cy.findByText('From URL').click()
  241. })
  242. cy.findByLabelText('File name in this project').as(
  243. 'relocated-file-name-input'
  244. )
  245. cy.findByLabelText('Image URL').as('image-url-input')
  246. cy.get('[data-cy="include-label-option"]').as('include-label-checkbox')
  247. cy.get('[data-cy="include-caption-option"]').as(
  248. 'include-caption-checkbox'
  249. )
  250. })
  251. it('Auto fills name based on url', function () {
  252. cy.get('@image-url-input').type('https://my-fake-website.com/frog.jpg')
  253. cy.get('@relocated-file-name-input').should('have.value', 'frog.jpg')
  254. cy.get('@relocated-file-name-input').type('pig')
  255. cy.get('@relocated-file-name-input').should('have.value', 'pig.jpg')
  256. })
  257. it('Enables insert button when name and url is available', function () {
  258. cy.findByText('Insert figure').should('be.disabled')
  259. cy.get('@image-url-input').type('https://my-fake-website.com/frog.jpg')
  260. cy.findByText('Insert figure').should('be.enabled')
  261. })
  262. it('Adds linked file when pressing insert', function () {
  263. cy.get('@image-url-input').type('https://my-fake-website.com/frog.jpg')
  264. cy.findByText('Insert figure').click()
  265. cy.get('@linked-file-request').should('have.been.calledWithMatch', {
  266. body: {
  267. provider: 'url',
  268. data: {
  269. url: 'https://my-fake-website.com/frog.jpg',
  270. },
  271. },
  272. })
  273. cy.get('.cm-content').should(
  274. 'have.text',
  275. '\\begin{figure} \\centering Enter Caption 🏷fig:enter-label\\end{figure}'
  276. )
  277. })
  278. })
  279. describe('Editing existing figure', function () {
  280. it('Parses existing label and caption', function () {
  281. cy.get('.cm-content').type(
  282. `\\begin{{}figure}
  283. \\centering
  284. \\includegraphics[width=0.5\\linewidth]{{}frog.jpg}
  285. \\caption{{}My caption}
  286. \\label{{}fig:my-label}
  287. \\end{{}figure}`,
  288. { delay: 0 }
  289. )
  290. cy.get('[aria-label="Edit figure"]').click()
  291. cy.get('[data-cy="include-caption-option"]').should('be.checked')
  292. cy.get('[data-cy="include-label-option"]').should('be.checked')
  293. })
  294. it('Parses existing width', function () {
  295. cy.get('.cm-content').type(
  296. `\\begin{{}figure}
  297. \\centering
  298. \\includegraphics[width=0.75\\linewidth]{{}frog.jpg}
  299. \\caption{{}My caption}
  300. \\label{{}fig:my-label}
  301. \\end{{}figure}`,
  302. { delay: 0 }
  303. )
  304. cy.get('[aria-label="Edit figure"]').click()
  305. cy.get('[value="0.75"]').should('be.checked')
  306. })
  307. it('Removes existing label when unchecked', function () {
  308. cy.get('.cm-content').type(
  309. `\\begin{{}figure}
  310. \\centering
  311. \\includegraphics[width=0.75\\linewidth]{{}frog.jpg}
  312. \\label{{}fig:my-label}
  313. \\end{{}figure}`,
  314. { delay: 0 }
  315. )
  316. cy.get('[aria-label="Edit figure"]').click()
  317. cy.get('[data-cy="include-label-option"]').click()
  318. cy.get('[data-cy="include-label-option"]').should('not.be.checked')
  319. cy.findByText('Done').click()
  320. cy.get('.cm-content').should(
  321. 'have.text',
  322. '\\begin{figure}\\centering\\end{figure}'
  323. )
  324. })
  325. it('Removes existing caption when unchecked', function () {
  326. cy.get('.cm-content').type(
  327. `\\begin{{}figure}
  328. \\centering
  329. \\includegraphics[width=0.75\\linewidth]{{}frog.jpg}
  330. \\caption{{}My caption}
  331. \\label{{}fig:my-label}
  332. \\end{{}figure}`,
  333. { delay: 0 }
  334. )
  335. cy.get('[aria-label="Edit figure"]').click()
  336. cy.get('[data-cy="include-caption-option"]').click()
  337. cy.get('[data-cy="include-caption-option"]').should('not.be.checked')
  338. cy.findByText('Done').click()
  339. cy.get('.cm-content').should(
  340. 'have.text',
  341. '\\begin{figure}\\centering🏷fig:my-label\\end{figure}'
  342. )
  343. })
  344. it('Preserves other content when removing figure', function () {
  345. cy.get('.cm-content').type(
  346. `text above
  347. \\begin{{}figure}
  348. \\centering
  349. \\includegraphics[width=0.75\\linewidth]{{}frog.jpg}
  350. \\caption{{}My caption}
  351. \\label{{}fig:my-label}
  352. \\end{{}figure}
  353. text below`,
  354. { delay: 0 }
  355. )
  356. cy.get('[aria-label="Edit figure"]').click()
  357. cy.get('[aria-label="Remove or replace figure"]').click()
  358. cy.findByText('Delete figure').click()
  359. cy.get('.cm-content').should('have.text', 'text abovetext below')
  360. })
  361. it('Opens figure modal on pasting image', function () {
  362. cy.fixture<Uint8Array>('images/gradient.png').then(gradientBuffer => {
  363. const gradientFile = new File([gradientBuffer], 'gradient.png', {
  364. type: 'image/png',
  365. })
  366. const clipboardData = new DataTransfer()
  367. clipboardData.items.add(gradientFile)
  368. cy.wrap(clipboardData.files).should('have.length', 1)
  369. cy.get('.cm-content').trigger('paste', { clipboardData })
  370. cy.findByText('Upload from computer').should('be.visible')
  371. cy.findByLabelText('File name in this project').should(
  372. 'have.value',
  373. 'gradient.png'
  374. )
  375. })
  376. })
  377. // TODO: Add tests for replacing image when we can match on image path
  378. // TODO: Add tests for changing image size when we can match on figure width
  379. })
  380. })