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

[web] Add cypress tests for figure modal

GitOrigin-RevId: 4debae1c665a68fd7bfa9f0dcfc150bec38a7c64
This commit is contained in:
Mathias Jakobsen 2023-05-17 08:48:21 +01:00 committed by Copybot
parent f4408c3fa6
commit 988fc57574
13 changed files with 514 additions and 6 deletions

View file

@ -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)

View file

@ -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')
)
}

View file

@ -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' },
],
})
}

View file

@ -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')
)
}

View file

@ -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": "",

View file

@ -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 })}
/>

View file

@ -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}

View file

@ -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>
)

View file

@ -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)
}}

View file

@ -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')

View file

@ -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",

View file

@ -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
})
})

View file

@ -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',