figure-modal.spec.tsx 17 KB

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