Просмотр исходного кода

Merge pull request #13077 from overleaf/mj-figure-modal-tests

[web] Add cypress tests for figure modal

GitOrigin-RevId: 4debae1c665a68fd7bfa9f0dcfc150bec38a7c64
Mathias Jakobsen 3 лет назад
Родитель
Сommit
988fc57574

+ 9 - 0
services/web/cypress/support/shared/commands/index.ts

@@ -7,6 +7,9 @@ import {
 import { interceptEvents } from './events'
 import { interceptSpelling } from './spelling'
 import { interceptAsync } from './intercept-async'
+import { interceptFileUpload } from './upload'
+import { interceptProjectListing } from './project-list'
+import { interceptLinkedFile } from './linked-file'
 
 // eslint-disable-next-line no-unused-vars,@typescript-eslint/no-namespace
 declare global {
@@ -20,6 +23,9 @@ declare global {
       interceptSpelling: typeof interceptSpelling
       waitForCompile: typeof waitForCompile
       interceptDeferredCompile: typeof interceptDeferredCompile
+      interceptFileUpload: typeof interceptFileUpload
+      interceptProjectListing: typeof interceptProjectListing
+      interceptLinkedFile: typeof interceptLinkedFile
     }
   }
 }
@@ -30,3 +36,6 @@ Cypress.Commands.add('interceptEvents', interceptEvents)
 Cypress.Commands.add('interceptSpelling', interceptSpelling)
 Cypress.Commands.add('waitForCompile', waitForCompile)
 Cypress.Commands.add('interceptDeferredCompile', interceptDeferredCompile)
+Cypress.Commands.add('interceptFileUpload', interceptFileUpload)
+Cypress.Commands.add('interceptProjectListing', interceptProjectListing)
+Cypress.Commands.add('interceptLinkedFile', interceptLinkedFile)

+ 12 - 0
services/web/cypress/support/shared/commands/linked-file.ts

@@ -0,0 +1,12 @@
+import { HttpRequestInterceptor } from 'cypress/types/net-stubbing'
+
+export const interceptLinkedFile = () => {
+  cy.intercept(
+    { method: 'POST', url: '/project/*/linked_file' },
+    cy
+      .spy((req: Parameters<HttpRequestInterceptor>[0]) => {
+        req.reply({ statusCode: 200, body: { success: true } })
+      })
+      .as('linked-file-request')
+  )
+}

+ 22 - 0
services/web/cypress/support/shared/commands/project-list.ts

@@ -0,0 +1,22 @@
+export const interceptProjectListing = () => {
+  cy.intercept('GET', '/user/projects', {
+    projects: [
+      {
+        _id: 'fake-project-1',
+        accessLevel: 'owner',
+        name: 'My first project',
+      },
+      {
+        _id: 'fake-project-2',
+        accessLevel: 'owner',
+        name: 'My second project',
+      },
+    ],
+  })
+  cy.intercept('GET', '/project/*/entities', {
+    entities: [
+      { path: '/frog.jpg', type: 'file' },
+      { path: 'figures/unicorn.png', type: 'file' },
+    ],
+  })
+}

+ 18 - 0
services/web/cypress/support/shared/commands/upload.ts

@@ -0,0 +1,18 @@
+import { HttpRequestInterceptor } from 'cypress/types/net-stubbing'
+
+export const interceptFileUpload = () => {
+  cy.intercept(
+    { method: 'POST', url: /\/project\/.*\/upload/ },
+    cy
+      .spy((req: Parameters<HttpRequestInterceptor>[0]) => {
+        const folderMatch = req.url.match(
+          /project\/.*\/upload\?folder_id=[a-f0-9]{24}/
+        )
+        if (!folderMatch) {
+          req.reply({ statusCode: 500, body: { success: false } })
+        }
+        req.reply({ statusCode: 200, body: { success: true } })
+      })
+      .as('uploadRequest')
+  )
+}

+ 6 - 0
services/web/frontend/extracted-translations.json

@@ -274,9 +274,11 @@
   "file_already_exists": "",
   "file_already_exists_in_this_location": "",
   "file_name": "",
+  "file_name_figure_modal": "",
   "file_name_in_this_project": "",
   "file_name_in_this_project_figure_modal": "",
   "file_outline": "",
+  "file_size": "",
   "files_cannot_include_invalid_characters": "",
   "find_out_more": "",
   "find_out_more_about_institution_login": "",
@@ -745,6 +747,7 @@
   "remove_collaborator": "",
   "remove_from_group": "",
   "remove_manager": "",
+  "remove_or_replace_figure": "",
   "remove_tag": "",
   "removing": "",
   "rename": "",
@@ -799,11 +802,14 @@
   "search_whole_word": "",
   "search_within_selection": "",
   "select_a_file": "",
+  "select_a_file_figure_modal": "",
   "select_a_payment_method": "",
   "select_a_project": "",
+  "select_a_project_figure_modal": "",
   "select_all": "",
   "select_all_projects": "",
   "select_an_output_file": "",
+  "select_an_output_file_figure_modal": "",
   "select_folder_from_project": "",
   "select_from_output_files": "",
   "select_from_project_files": "",

+ 2 - 0
services/web/frontend/js/features/source-editor/components/figure-modal/figure-modal-options.tsx

@@ -20,6 +20,7 @@ export const FigureModalFigureOptions: FC = () => {
         <input
           type="checkbox"
           id="figure-modal-caption"
+          data-cy="include-caption-option"
           defaultChecked={includeCaption}
           onChange={event => dispatch({ includeCaption: event.target.checked })}
         />
@@ -31,6 +32,7 @@ export const FigureModalFigureOptions: FC = () => {
         <input
           type="checkbox"
           id="figure-modal-label"
+          data-cy="include-label-option"
           defaultChecked={includeLabel}
           onChange={event => dispatch({ includeLabel: event.target.checked })}
         />

+ 3 - 3
services/web/frontend/js/features/source-editor/components/figure-modal/file-sources/figure-modal-other-project-source.tsx

@@ -126,7 +126,7 @@ export const FigureModalOtherProjectSource: FC = () => {
         items={projects ?? []}
         itemToString={project => (project ? project.name : '')}
         itemToKey={item => item._id}
-        defaultText={t('select_a_project')}
+        defaultText={t('select_a_project_figure_modal')}
         label={t('project_figure_modal')}
         disabled={projectsLoading}
         onSelectedItemChanged={item => {
@@ -201,7 +201,7 @@ const SelectFile = <T extends { path: string }>({
   onSelectedItemChange?: (item: T | null | undefined) => any
 }) => {
   const { t } = useTranslation()
-  defaultText = defaultText ?? t('select_a_file')
+  defaultText = defaultText ?? t('select_a_file_figure_modal')
   label = label ?? t('image_file')
   const imageFiles = useMemo(() => files?.filter(isImageEntity), [files])
   const empty = loading || !imageFiles || imageFiles.length === 0
@@ -257,7 +257,7 @@ const SelectFromProjectOutputFiles: FC<{
   return (
     <SelectFile
       label={t('output_file')}
-      defaultText={t('select_an_output_file')}
+      defaultText={t('select_an_output_file_figure_modal')}
       loading={loading}
       files={entities}
       disabled={!projectId}

+ 7 - 2
services/web/frontend/js/features/source-editor/components/figure-modal/file-sources/figure-modal-upload-source.tsx

@@ -251,6 +251,7 @@ export const FileContainer: FC<{
   status: FileUploadStatus
   onDelete?: () => any
 }> = ({ name, size, status, onDelete }) => {
+  const { t } = useTranslation()
   let icon
   switch (status) {
     case FileUploadStatus.ERROR:
@@ -280,7 +281,9 @@ export const FileContainer: FC<{
           )}
         />
         <div className="file-info">
-          <span className="file-name">{name}</span>
+          <span className="file-name" aria-label={t('file_name_figure_modal')}>
+            {name}
+          </span>
           {size !== undefined && (
             <FileSize size={size} className="text-small" />
           )}
@@ -288,6 +291,7 @@ export const FileContainer: FC<{
         <Button
           bsStyle={null}
           className="btn btn-link p-0"
+          aria-label={t('remove_or_replace_figure')}
           onClick={() => onDelete && onDelete()}
         >
           <Icon fw type="times-circle" className="file-action file-icon" />
@@ -301,6 +305,7 @@ const FileSize: FC<{ size: number; className?: string }> = ({
   size,
   className,
 }) => {
+  const { t } = useTranslation()
   const BYTE_UNITS: [string, number][] = [
     ['B', 1],
     ['KB', 1e3],
@@ -317,7 +322,7 @@ const FileSize: FC<{ size: number; className?: string }> = ({
   const [label, bytesPerUnit] = BYTE_UNITS[labelIndex]
   const sizeInUnits = Math.round(size / bytesPerUnit)
   return (
-    <span className={className}>
+    <span aria-label={t('file_size')} className={className}>
       {sizeInUnits} {label}
     </span>
   )

+ 2 - 0
services/web/frontend/js/features/source-editor/components/toolbar/button-menu.tsx

@@ -15,6 +15,7 @@ export const ToolbarButtonMenu: FC<{
     <Button
       type="button"
       className="ol-cm-toolbar-button"
+      aria-label={label}
       bsStyle={null}
       onMouseDown={event => {
         event.preventDefault()
@@ -45,6 +46,7 @@ export const ToolbarButtonMenu: FC<{
         className="ol-cm-toolbar-button-menu-popover"
       >
         <ListGroup
+          role="menu"
           onClick={() => {
             onToggle(false)
           }}

+ 1 - 0
services/web/frontend/js/features/source-editor/extensions/visual/visual-widgets/editable-graphics.ts

@@ -38,6 +38,7 @@ export class EditableGraphicsWidget extends GraphicsWidget {
 
   createEditButton(view: EditorView) {
     const button = document.createElement('button')
+    button.setAttribute('aria-label', view.state.phrase('edit_figure'))
     this.setEditDispatcher(button, view)
     button.classList.add('btn', 'btn-secondary', 'ol-cm-graphics-edit-button')
     const buttonLabel = document.createElement('span')

+ 6 - 0
services/web/locales/en.json

@@ -495,12 +495,14 @@
   "file_already_exists_in_this_location": "An item named <0>__fileName__</0> already exists in this location. If you wish to move this file, rename or remove the conflicting file and try again.",
   "file_has_been_deleted": "__filename__ has been deleted",
   "file_name": "File Name",
+  "file_name_figure_modal": "File name",
   "file_name_in_this_project": "File Name In This Project",
   "file_name_in_this_project_figure_modal": "File name in this project",
   "file_outline": "File outline",
   "file_restored": "Your file (__filename__) has been recovered.",
   "file_restored_back_to_editor": "You can go back to the editor and work on it again.",
   "file_restored_back_to_editor_btn": "Back to editor",
+  "file_size": "File size",
   "file_too_large": "File too large",
   "files_cannot_include_invalid_characters": "File name is empty or contains invalid characters",
   "files_selected": "files selected.",
@@ -1261,6 +1263,7 @@
   "remove_collaborator": "Remove collaborator",
   "remove_from_group": "Remove from group",
   "remove_manager": "Remove manager",
+  "remove_or_replace_figure": "Remove or replace figure",
   "remove_tag": "Remove tag __tagName__",
   "removed": "removed",
   "removing": "Removing",
@@ -1349,12 +1352,15 @@
   "see_changes_in_your_documents_live": "See changes in your documents, live",
   "see_what_has_been": "See what has been ",
   "select_a_file": "Select a File",
+  "select_a_file_figure_modal": "Select a file",
   "select_a_payment_method": "Select a payment method",
   "select_a_project": "Select a Project",
+  "select_a_project_figure_modal": "Select a project",
   "select_a_zip_file": "Select a .zip file",
   "select_all": "Select all",
   "select_all_projects": "Select all projects",
   "select_an_output_file": "Select an Output File",
+  "select_an_output_file_figure_modal": "Select an output file",
   "select_folder_from_project": "Select folder from project",
   "select_from_output_files": "select from output files",
   "select_from_project_files": "select from project files",

+ 390 - 0
services/web/test/frontend/features/source-editor/components/figure-modal.spec.tsx

@@ -0,0 +1,390 @@
+import CodemirrorEditor from '../../../../../frontend/js/features/source-editor/components/codemirror-editor'
+import { EditorProviders } from '../../../helpers/editor-providers'
+import { mockScope, rootFolderId } from '../helpers/mock-scope'
+import { FC } from 'react'
+
+const Container: FC = ({ children }) => (
+  <div style={{ width: 785, height: 785 }}>{children}</div>
+)
+
+const clickToolbarButton = (text: string) => {
+  cy.findByLabelText(text).click()
+  cy.findByLabelText(text).trigger('mouseout')
+}
+
+const chooseFileFromComputer = () => {
+  cy.get('@file-input').selectFile(
+    {
+      fileName: 'frog.jpg',
+      contents: Cypress.Buffer.from('image-data'),
+      mimeType: 'image/jpg',
+    },
+    {
+      force: true,
+    }
+  )
+}
+
+const matchUrl = (urlToMatch: RegExp | string) =>
+  Cypress.sinon.match(req => {
+    if (!req || typeof req.url !== 'string') {
+      return false
+    }
+    if (typeof urlToMatch === 'string') {
+      return req.url.endsWith(urlToMatch)
+    }
+    return Boolean(req.url.match(urlToMatch))
+  })
+
+describe('<FigureModal />', function () {
+  // TODO: rewrite these tests to be in source mode when toolbar is added there
+  // TODO: Write tests for width toggle, when we can match on source code
+  beforeEach(function () {
+    window.metaAttributesCache.set('ol-preventCompileOnLoad', true)
+    window.metaAttributesCache.set('ol-splitTestVariants', {
+      'figure-modal': 'enabled',
+    })
+    window.metaAttributesCache.set(
+      'ol-mathJax3Path',
+      'https://unpkg.com/mathjax@3.2.2/es5/tex-svg-full.js'
+    )
+    cy.interceptEvents()
+    cy.interceptSpelling()
+
+    const content = ''
+
+    const scope = mockScope(content)
+    scope.editor.showVisual = true
+
+    cy.mount(
+      <Container>
+        <EditorProviders scope={scope}>
+          <CodemirrorEditor />
+        </EditorProviders>
+      </Container>
+    )
+  })
+
+  describe('Upload from computer source', function () {
+    beforeEach(function () {
+      cy.interceptFileUpload()
+      clickToolbarButton('Insert Figure')
+      cy.findByRole('menu').within(() => {
+        cy.findByText('Upload from computer').click()
+      })
+      cy.findByLabelText('File Uploader')
+        .get('.uppy-Dashboard-input:first')
+        .as('file-input')
+    })
+
+    it('Shows file name and size when selecting file', function () {
+      chooseFileFromComputer()
+      cy.findByLabelText('File name').should('have.text', 'frog.jpg')
+      cy.findByLabelText('File size').should('have.text', '10 B')
+    })
+
+    it('Uploads file when clicking insert', function () {
+      chooseFileFromComputer()
+      cy.get('@uploadRequest').should('not.have.been.called')
+      cy.findByText('Insert figure').click()
+      cy.get('@uploadRequest').should(
+        'have.been.calledWith',
+        matchUrl(`/project/test-project/upload?folder_id=${rootFolderId}`)
+      )
+
+      cy.get('.cm-content').should(
+        'have.text',
+        '\\begin{figure}\\centeringEnter Caption🏷fig:enter-label\\end{figure}'
+      )
+    })
+
+    it('Enables insert button when choosing file', function () {
+      cy.findByText('Insert figure').should('be.disabled')
+      chooseFileFromComputer()
+      cy.findByText('Insert figure').should('be.enabled')
+    })
+  })
+
+  describe('Upload from project files source', function () {
+    beforeEach(function () {
+      clickToolbarButton('Insert Figure')
+      cy.findByRole('menu').within(() => {
+        cy.findByText('From project files').click()
+      })
+    })
+
+    it('Lists files from project', function () {
+      cy.findByText('Select image from project files').click()
+      cy.findByRole('listbox')
+        .children()
+        .should('have.length', 2)
+        .should('contain.text', 'frog.jpg')
+        .should('contain.text', 'unicorn.png')
+        .should('contain.text', 'figures/')
+    })
+
+    it('Enables insert button when choosing file', function () {
+      cy.findByText('Insert figure').should('be.disabled')
+      cy.findByText('Select image from project files').click()
+      cy.findByRole('listbox').within(() => {
+        cy.findByText('frog.jpg').click()
+      })
+      cy.findByText('Insert figure').should('be.enabled')
+    })
+
+    it('Inserts file when pressing insert button', function () {
+      cy.findByText('Select image from project files').click()
+      cy.findByRole('listbox').within(() => {
+        cy.findByText('frog.jpg').click()
+      })
+      cy.findByText('Insert figure').click()
+      cy.get('.cm-content').should(
+        'have.text',
+        '\\begin{figure}\\centeringEnter Caption🏷fig:enter-label\\end{figure}'
+      )
+    })
+  })
+
+  describe('From another project source', function () {
+    beforeEach(function () {
+      cy.interceptProjectListing()
+      cy.interceptCompile()
+      cy.interceptLinkedFile()
+      clickToolbarButton('Insert Figure')
+      cy.findByRole('menu').within(() => {
+        cy.findByText('From another project').click()
+      })
+      cy.findByText('Select a project').parent().as('project-dropdown')
+      cy.findByText('Select a file').parent().as('file-dropdown')
+    })
+
+    it('List projects and files in projects', function () {
+      cy.findByText('Insert figure').should('be.disabled')
+      cy.get('@file-dropdown').should('have.class', 'disabled')
+      cy.get('@project-dropdown').click()
+      cy.findByRole('listbox').as('project-select')
+      cy.get('@project-select').children().should('have.length', 2)
+      cy.get('@project-select').within(() => {
+        cy.findByText('My first project').click()
+      })
+      cy.get('@file-dropdown').should('not.have.class', 'disabled')
+      cy.get('@file-dropdown').click()
+      cy.findByRole('listbox').as('file-select')
+      cy.get('@file-select').children().should('have.length', 2)
+      cy.get('@file-select').should('contain.text', 'frog.jpg')
+      cy.get('@file-select').should('contain.text', 'figures/unicorn.png')
+      cy.get('@file-select').within(() => {
+        cy.findByText('frog.jpg').click()
+      })
+      cy.findByText('Insert figure').should('be.enabled')
+    })
+
+    it('Enables insert button when choosing file', function () {
+      cy.findByText('Insert figure').should('be.disabled')
+      cy.get('@project-dropdown').click()
+      cy.findByRole('listbox').within(() => {
+        cy.findByText('My first project').click()
+      })
+      cy.get('@file-dropdown').click()
+      cy.findByRole('listbox').within(() => {
+        cy.findByText('frog.jpg').click()
+      })
+      cy.findByText('Insert figure').should('be.enabled')
+    })
+
+    it('Creates linked file when pressing insert', function () {
+      cy.get('@project-dropdown').click()
+      cy.findByRole('listbox').within(() => {
+        cy.findByText('My first project').click()
+      })
+      cy.get('@file-dropdown').click()
+      cy.findByRole('listbox').within(() => {
+        cy.findByText('frog.jpg').click()
+      })
+      cy.findByText('Insert figure').click()
+      cy.get('@linked-file-request').should('have.been.calledWithMatch', {
+        body: {
+          provider: 'project_file',
+          data: {
+            source_entity_path: '/frog.jpg',
+            source_project_id: 'fake-project-1',
+          },
+        },
+      })
+
+      cy.get('.cm-content').should(
+        'have.text',
+        '\\begin{figure}\\centeringEnter Caption🏷fig:enter-label\\end{figure}'
+      )
+    })
+
+    it('Creates linked output file when pressing insert', function () {
+      cy.get('@project-dropdown').click()
+      cy.findByRole('listbox').within(() => {
+        cy.findByText('My first project').click()
+      })
+      cy.findByText('select from output files').click()
+      cy.findByText('Select an output file').parent().as('output-file-dropdown')
+      cy.get('@output-file-dropdown').click()
+      cy.findByRole('listbox').within(() => {
+        cy.findByText('output.pdf').click()
+      })
+      cy.findByText('Insert figure').click()
+      cy.get('@linked-file-request').should('have.been.calledWithMatch', {
+        body: {
+          provider: 'project_output_file',
+          data: {
+            source_output_file_path: 'output.pdf',
+            source_project_id: 'fake-project-1',
+          },
+        },
+      })
+
+      cy.get('.cm-content').should(
+        'have.text',
+        '\\begin{figure}\\centeringEnter Caption🏷fig:enter-label\\end{figure}'
+      )
+    })
+  })
+
+  describe('From URL source', function () {
+    beforeEach(function () {
+      cy.interceptLinkedFile()
+      clickToolbarButton('Insert Figure')
+      cy.findByRole('menu').within(() => {
+        cy.findByText('From URL').click()
+      })
+      cy.findByLabelText('File name in this project').as(
+        'relocated-file-name-input'
+      )
+      cy.findByLabelText('Image URL').as('image-url-input')
+      cy.get('[data-cy="include-label-option"]').as('include-label-checkbox')
+      cy.get('[data-cy="include-caption-option"]').as(
+        'include-caption-checkbox'
+      )
+    })
+
+    it('Auto fills name based on url', function () {
+      cy.get('@image-url-input').type('https://my-fake-website.com/frog.jpg')
+      cy.get('@relocated-file-name-input').should('have.value', 'frog.jpg')
+      cy.get('@relocated-file-name-input').type('pig')
+      cy.get('@relocated-file-name-input').should('have.value', 'pig.jpg')
+    })
+
+    it('Enables insert button when name and url is available', function () {
+      cy.findByText('Insert figure').should('be.disabled')
+      cy.get('@image-url-input').type('https://my-fake-website.com/frog.jpg')
+      cy.findByText('Insert figure').should('be.enabled')
+    })
+
+    it('Adds linked file when pressing insert', function () {
+      cy.get('@image-url-input').type('https://my-fake-website.com/frog.jpg')
+      cy.findByText('Insert figure').click()
+
+      cy.get('@linked-file-request').should('have.been.calledWithMatch', {
+        body: {
+          provider: 'url',
+          data: {
+            url: 'https://my-fake-website.com/frog.jpg',
+          },
+        },
+      })
+
+      cy.get('.cm-content').should(
+        'have.text',
+        '\\begin{figure}\\centeringEnter Caption🏷fig:enter-label\\end{figure}'
+      )
+    })
+  })
+
+  describe('Editing existing figure', function () {
+    it('Parses existing label and caption', function () {
+      cy.get('.cm-content').type(
+        `\\begin{{}figure}
+\\centering
+\\includegraphics[width=0.5\\linewidth]{{}frog.jpg}
+\\caption{{}My caption}
+\\label{{}fig:my-label}
+\\end{{}figure}`,
+        { delay: 0 }
+      )
+      cy.get('[aria-label="Edit figure"]').click()
+      cy.get('[data-cy="include-caption-option"]').should('be.checked')
+      cy.get('[data-cy="include-label-option"]').should('be.checked')
+    })
+
+    it('Parses existing width', function () {
+      cy.get('.cm-content').type(
+        `\\begin{{}figure}
+\\centering
+\\includegraphics[width=0.75\\linewidth]{{}frog.jpg}
+\\caption{{}My caption}
+\\label{{}fig:my-label}
+\\end{{}figure}`,
+        { delay: 0 }
+      )
+      cy.get('[aria-label="Edit figure"]').click()
+      cy.get('[value="0.75"]').should('be.checked')
+    })
+
+    it('Removes existing label when unchecked', function () {
+      cy.get('.cm-content').type(
+        `\\begin{{}figure}
+\\centering
+\\includegraphics[width=0.75\\linewidth]{{}frog.jpg}
+\\label{{}fig:my-label}
+\\end{{}figure}`,
+        { delay: 0 }
+      )
+      cy.get('[aria-label="Edit figure"]').click()
+      cy.get('[data-cy="include-label-option"]').click()
+      cy.get('[data-cy="include-label-option"]').should('not.be.checked')
+      cy.findByText('Done').click()
+      cy.get('.cm-content').should(
+        'have.text',
+        '\\begin{figure}\\centering\\end{figure}'
+      )
+    })
+
+    it('Removes existing caption when unchecked', function () {
+      cy.get('.cm-content').type(
+        `\\begin{{}figure}
+\\centering
+\\includegraphics[width=0.75\\linewidth]{{}frog.jpg}
+\\caption{{}My caption}
+\\label{{}fig:my-label}
+\\end{{}figure}`,
+        { delay: 0 }
+      )
+      cy.get('[aria-label="Edit figure"]').click()
+      cy.get('[data-cy="include-caption-option"]').click()
+      cy.get('[data-cy="include-caption-option"]').should('not.be.checked')
+      cy.findByText('Done').click()
+      cy.get('.cm-content').should(
+        'have.text',
+        '\\begin{figure}\\centering🏷fig:my-label\\end{figure}'
+      )
+    })
+
+    it('Preserves other content when removing figure', function () {
+      cy.get('.cm-content').type(
+        `text above
+\\begin{{}figure}
+\\centering
+\\includegraphics[width=0.75\\linewidth]{{}frog.jpg}
+\\caption{{}My caption}
+\\label{{}fig:my-label}
+\\end{{}figure}
+text below`,
+        { delay: 0 }
+      )
+      cy.get('[aria-label="Edit figure"]').click()
+      cy.get('[aria-label="Remove or replace figure"]').click()
+      cy.findByText('Delete figure').click()
+      cy.get('.cm-content').should('have.text', 'text abovetext below')
+    })
+
+    // TODO: Add tests for replacing image when we can match on image path
+    // TODO: Add tests for changing image size when we can match on figure width
+  })
+})

+ 36 - 1
services/web/test/frontend/features/source-editor/helpers/mock-scope.ts

@@ -1,8 +1,43 @@
 import { docId, mockDoc } from './mock-doc'
 import { Folder } from '../../../../../types/folder'
-
+export const rootFolderId = '012345678901234567890123'
+export const figuresFolderId = '123456789012345678901234'
+export const figureId = '234567890123456789012345'
 export const mockScope = (content?: string) => {
   return {
+    rootFolder: {
+      id: rootFolderId,
+      name: 'rootFolder',
+      selected: false,
+      children: [
+        {
+          id: docId,
+          name: 'test.tex',
+          selected: false,
+          type: 'doc',
+        },
+        {
+          id: figuresFolderId,
+          name: 'figures',
+          selected: false,
+          type: 'folder',
+          children: [
+            {
+              id: figureId,
+              name: 'frog.jpg',
+              selected: false,
+              type: 'file',
+            },
+            {
+              id: 'fake-figure-id',
+              name: 'unicorn.png',
+              selected: false,
+              type: 'file',
+            },
+          ],
+        },
+      ],
+    },
     settings: {
       fontSize: 12,
       fontFamily: 'monaco',