Merge pull request #16511 from overleaf/ae-upgrade-uppy

Upgrade Uppy to v3

GitOrigin-RevId: ca3e366a20ac651a98aafe12bf319b1968ac6ec1
This commit is contained in:
Alf Eaton 2024-01-18 12:54:24 +00:00 committed by Copybot
parent 4bca3de8d2
commit 612c7c28b0
40 changed files with 3250 additions and 3326 deletions

1090
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -7,9 +7,13 @@ import './shared/exceptions'
import './ct/commands'
beforeEach(function () {
window.metaAttributesCache = new Map()
cy.window().then(win => {
win.metaAttributesCache = new Map()
})
})
afterEach(function () {
window.metaAttributesCache.clear()
cy.window().then(win => {
win.metaAttributesCache?.clear()
})
})

View file

@ -4,7 +4,7 @@ import { useCallback, useEffect, useState } from 'react'
import PropTypes from 'prop-types'
import Uppy from '@uppy/core'
import XHRUpload from '@uppy/xhr-upload'
import { Dashboard, useUppy } from '@uppy/react'
import { Dashboard } from '@uppy/react'
import { useFileTreeActionable } from '../../../contexts/file-tree-actionable'
import { useProjectContext } from '../../../../../shared/context/project-context'
import * as eventTracking from '../../../../../infrastructure/event-tracking'
@ -45,13 +45,13 @@ export default function FileTreeUploadDoc() {
}
// initialise the Uppy object
const uppy = useUppy(() => {
const [uppy] = useState(() => {
const endpoint = buildEndpoint(projectId, parentFolderId)
return (
new Uppy({
// logger: Uppy.debugLogger,
allowMultipleUploads: false,
allowMultipleUploadBatches: false,
restrictions: {
maxNumberOfFiles,
maxFileSize: maxFileSize || null,

View file

@ -2,7 +2,6 @@ import { useRef, useEffect, useState } from 'react'
import PropTypes from 'prop-types'
import { useTranslation } from 'react-i18next'
import getDroppedFiles from '@uppy/utils/lib/getDroppedFiles'
import { DndProvider, createDndContext, useDrag, useDrop } from 'react-dnd'
import {
HTML5Backend,

View file

@ -1,9 +1,10 @@
import BlankProjectModal from './blank-project-modal'
import ExampleProjectModal from './example-project-modal'
import importOverleafModules from '../../../../../macros/import-overleaf-module.macro'
import { JSXElementConstructor, lazy, Suspense } from 'react'
import { JSXElementConstructor, lazy, Suspense, useCallback } from 'react'
import { Nullable } from '../../../../../../types/utils'
import { FullSizeLoadingSpinner } from '@/shared/components/loading-spinner'
import { useLocation } from '@/shared/hooks/use-location'
const UploadProjectModal = lazy(() => import('./upload-project-modal'))
@ -26,6 +27,15 @@ function NewProjectButtonModal({ modal, onHide }: NewProjectButtonModalProps) {
onHide: () => void
}> = importProjectFromGithubModalWrapper?.import.default
const location = useLocation()
const openProject = useCallback(
(projectId: string) => {
location.assign(`/project/${projectId}`)
},
[location]
)
switch (modal) {
case 'blank_project':
return <BlankProjectModal onHide={onHide} />
@ -34,7 +44,7 @@ function NewProjectButtonModal({ modal, onHide }: NewProjectButtonModalProps) {
case 'upload_project':
return (
<Suspense fallback={<FullSizeLoadingSpinner delay={500} />}>
<UploadProjectModal onHide={onHide} />
<UploadProjectModal onHide={onHide} openProject={openProject} />
</Suspense>
)
case 'import_from_github':

View file

@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'
import { Button, Modal } from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
import Uppy from '@uppy/core'
import { Dashboard, useUppy } from '@uppy/react'
import { Dashboard } from '@uppy/react'
import XHRUpload from '@uppy/xhr-upload'
import AccessibleModal from '../../../../shared/components/accessible-modal'
import getMeta from '../../../../utils/meta'
@ -10,7 +10,6 @@ import { ExposedSettings } from '../../../../../../types/exposed-settings'
import '@uppy/core/dist/style.css'
import '@uppy/dashboard/dist/style.css'
import { useLocation } from '../../../../shared/hooks/use-location'
type UploadResponse = {
project_id: string
@ -18,19 +17,19 @@ type UploadResponse = {
type UploadProjectModalProps = {
onHide: () => void
openProject: (projectId: string) => void
}
function UploadProjectModal({ onHide }: UploadProjectModalProps) {
function UploadProjectModal({ onHide, openProject }: UploadProjectModalProps) {
const { t } = useTranslation()
const { maxUploadSize, projectUploadTimeout } = getMeta(
'ol-ExposedSettings'
) as ExposedSettings
const [ableToUpload, setAbleToUpload] = useState(false)
const location = useLocation()
const uppy: Uppy.Uppy<Uppy.StrictTypes> = useUppy(() => {
return Uppy({
allowMultipleUploads: false,
const [uppy] = useState(() => {
return new Uppy({
allowMultipleUploadBatches: false,
restrictions: {
maxNumberOfFiles: 1,
maxFileSize: maxUploadSize,
@ -62,7 +61,7 @@ function UploadProjectModal({ onHide }: UploadProjectModalProps) {
const { project_id: projectId }: UploadResponse = response.body
if (projectId) {
location.assign(`/project/${projectId}`)
openProject(projectId)
}
})
.on('restriction-failed', () => {

View file

@ -2,10 +2,10 @@ import { FC, useCallback, useEffect, useState } from 'react'
import { useFigureModalContext } from '../figure-modal-context'
import { useCurrentProjectFolders } from '../../../hooks/use-current-project-folders'
import { File } from '../../../utils/file'
import { Dashboard, useUppy } from '@uppy/react'
import { Dashboard } from '@uppy/react'
import '@uppy/core/dist/style.css'
import '@uppy/dashboard/dist/style.css'
import { Uppy, UppyFile } from '@uppy/core'
import { Uppy, type UppyFile } from '@uppy/core'
import XHRUpload from '@uppy/xhr-upload'
import { refreshProjectMetadata } from '../../../../file-tree/util/api'
import { useProjectContext } from '../../../../../shared/context/project-context'
@ -41,10 +41,9 @@ export const FigureModalUploadFileSource: FC = () => {
const [name, setName] = useState<string>('')
const [uploading, setUploading] = useState<boolean>(false)
const [uploadError, setUploadError] = useState<any>(null)
const uppy = useUppy(() =>
const [uppy] = useState(() =>
new Uppy({
allowMultipleUploads: false,
allowMultipleUploadBatches: false,
restrictions: {
maxNumberOfFiles: 1,
maxFileSize: maxFileSize || null,
@ -54,6 +53,7 @@ export const FigureModalUploadFileSource: FC = () => {
})
// use the basic XHR uploader
.use(XHRUpload, {
endpoint: `/project/${projectId}/upload?folder_id=${rootFile.id}`,
headers: {
'X-CSRF-TOKEN': window.csrfToken,
},
@ -91,7 +91,7 @@ export const FigureModalUploadFileSource: FC = () => {
useEffect(() => {
// broadcast doc metadata after each successful upload
const onUploadSuccess = (_file: UppyFile, response: any) => {
const onUploadSuccess = (_file: UppyFile | undefined, response: any) => {
setUploading(false)
if (response.body.entity_type === 'doc') {
window.setTimeout(() => {
@ -139,7 +139,11 @@ export const FigureModalUploadFileSource: FC = () => {
}
// handle upload errors
const onError = (_file: UppyFile, error: any, response: any) => {
const onError = (
_file: UppyFile | undefined,
error: any,
response: any
) => {
setUploading(false)
setUploadError(error)
switch (response?.status) {

View file

@ -145,3 +145,7 @@
.figure-modal .select-wrapper:not(:first-child) {
margin-top: 16px;
}
.figure-modal-upload .uppy-Dashboard-AddFiles-list {
display: none;
}

View file

@ -13,7 +13,7 @@
"test:unit:all": "npm run test:unit:run_dir -- test/unit/src modules/*/test/unit/src",
"test:unit:all:silent": "npm run test:unit:all -- --reporter dot",
"test:unit:app": "npm run test:unit:run_dir -- test/unit/src",
"test:frontend": "NODE_ENV=test TZ=GMT mocha --recursive --timeout 5000 --exit --extension js,jsx,mjs,ts,tsx --grep=$MOCHA_GREP --require test/frontend/bootstrap.js --ignore '**/*.spec.{js,jsx,ts,tsx}' test/frontend modules/*/test/frontend",
"test:frontend": "NODE_ENV=test TZ=GMT mocha --recursive --timeout 5000 --exit --extension js,jsx,mjs,ts,tsx --grep=$MOCHA_GREP --require test/frontend/bootstrap.js --ignore '**/*.spec.{js,jsx,ts,tsx}' --ignore '**/helpers/**/*.{js,jsx,ts,tsx}' test/frontend modules/*/test/frontend",
"test:frontend:coverage": "c8 --all --include 'frontend/js' --include 'modules/*/frontend/js' --exclude 'frontend/js/vendor' --reporter=lcov --reporter=text-summary npm run test:frontend",
"start": "node app.js",
"nodemon": "node --watch app.js --watch-locales",
@ -243,11 +243,11 @@
"@types/uuid": "^8.3.4",
"@typescript-eslint/eslint-plugin": "^6.7.4",
"@typescript-eslint/parser": "^6.7.4",
"@uppy/core": "^1.15.0",
"@uppy/dashboard": "^1.11.0",
"@uppy/react": "^1.11.0",
"@uppy/utils": "^4.0.7",
"@uppy/xhr-upload": "^1.6.8",
"@uppy/core": "^3.8.0",
"@uppy/dashboard": "^3.7.1",
"@uppy/react": "^3.2.1",
"@uppy/utils": "^5.7.0",
"@uppy/xhr-upload": "^3.6.0",
"abort-controller": "^3.0.0",
"acorn": "^7.1.1",
"acorn-walk": "^7.1.1",

View file

@ -0,0 +1,69 @@
import FileTreeCreateNameInput from '../../../../../../frontend/js/features/file-tree/components/file-tree-create/file-tree-create-name-input'
import FileTreeCreateNameProvider from '../../../../../../frontend/js/features/file-tree/contexts/file-tree-create-name'
describe('<FileTreeCreateNameInput/>', function () {
it('renders an empty input', function () {
cy.mount(
<FileTreeCreateNameProvider>
<FileTreeCreateNameInput />
</FileTreeCreateNameProvider>
)
cy.findByLabelText('File Name')
cy.findByPlaceholderText('File Name')
})
it('renders a custom label and placeholder', function () {
cy.mount(
<FileTreeCreateNameProvider>
<FileTreeCreateNameInput
label="File name in this project"
placeholder="Enter a file name…"
/>
</FileTreeCreateNameProvider>
)
cy.findByLabelText('File name in this project')
cy.findByPlaceholderText('Enter a file name…')
})
it('uses an initial name', function () {
cy.mount(
<FileTreeCreateNameProvider initialName="test.tex">
<FileTreeCreateNameInput />
</FileTreeCreateNameProvider>
)
cy.findByLabelText('File Name').should('have.value', 'test.tex')
})
it('focuses the name', function () {
cy.spy(window, 'requestAnimationFrame').as('requestAnimationFrame')
cy.mount(
<FileTreeCreateNameProvider initialName="test.tex">
<FileTreeCreateNameInput focusName />
</FileTreeCreateNameProvider>
)
cy.findByLabelText('File Name').as('input')
cy.get('@input').should('have.value', 'test.tex')
cy.get('@requestAnimationFrame').should('have.been.calledOnce')
// https://github.com/jsdom/jsdom/issues/2995
// "window.getSelection doesn't work with selection of <input> element"
// const selection = window.getSelection().toString()
// expect(selection).to.equal('test')
// wait for the selection to update
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(100)
cy.get<HTMLInputElement>('@input').then(element => {
expect(element.get(0).selectionStart).to.equal(0)
expect(element.get(0).selectionEnd).to.equal(4)
})
})
})

View file

@ -1,83 +0,0 @@
import { expect } from 'chai'
import { screen, waitFor, cleanup } from '@testing-library/react'
import sinon from 'sinon'
import renderWithContext from '../../helpers/render-with-context'
import FileTreeCreateNameInput from '../../../../../../frontend/js/features/file-tree/components/file-tree-create/file-tree-create-name-input'
import FileTreeCreateNameProvider from '../../../../../../frontend/js/features/file-tree/contexts/file-tree-create-name'
describe('<FileTreeCreateNameInput/>', function () {
const sandbox = sinon.createSandbox()
beforeEach(function () {
sandbox.spy(window, 'requestAnimationFrame')
})
afterEach(function () {
sandbox.restore()
cleanup()
})
it('renders an empty input', async function () {
renderWithContext(
<FileTreeCreateNameProvider>
<FileTreeCreateNameInput />
</FileTreeCreateNameProvider>
)
await screen.getByLabelText('File Name')
await screen.getByPlaceholderText('File Name')
})
it('renders a custom label and placeholder', async function () {
renderWithContext(
<FileTreeCreateNameProvider>
<FileTreeCreateNameInput
label="File name in this project"
placeholder="Enter a file name…"
/>
</FileTreeCreateNameProvider>
)
await screen.getByLabelText('File name in this project')
await screen.getByPlaceholderText('Enter a file name…')
})
it('uses an initial name', async function () {
renderWithContext(
<FileTreeCreateNameProvider initialName="test.tex">
<FileTreeCreateNameInput />
</FileTreeCreateNameProvider>
)
const input = await screen.getByLabelText('File Name')
expect(input.value).to.equal('test.tex')
})
it('focuses the name', async function () {
renderWithContext(
<FileTreeCreateNameProvider initialName="test.tex">
<FileTreeCreateNameInput focusName />
</FileTreeCreateNameProvider>
)
const input = await screen.getByLabelText('File Name')
expect(input.value).to.equal('test.tex')
await waitFor(
() => expect(window.requestAnimationFrame).to.have.been.calledOnce
)
// https://github.com/jsdom/jsdom/issues/2995
// "window.getSelection doesn't work with selection of <input> element"
// const selection = window.getSelection().toString()
// expect(selection).to.equal('test')
// wait for the selection to update
await new Promise(resolve => window.setTimeout(resolve, 100))
expect(input.selectionStart).to.equal(0)
expect(input.selectionEnd).to.equal(4)
})
})

View file

@ -0,0 +1,508 @@
import { useEffect } from 'react'
import FileTreeModalCreateFile from '../../../../../../frontend/js/features/file-tree/components/modals/file-tree-modal-create-file'
import { useFileTreeActionable } from '../../../../../../frontend/js/features/file-tree/contexts/file-tree-actionable'
import { useFileTreeData } from '../../../../../../frontend/js/shared/context/file-tree-data-context'
import { EditorProviders } from '../../../../helpers/editor-providers'
import { FileTreeProvider } from '../../helpers/file-tree-provider'
describe('<FileTreeModalCreateFile/>', function () {
it('handles invalid file names', function () {
cy.mount(
<EditorProviders>
<FileTreeProvider>
<OpenWithMode mode="doc" />
</FileTreeProvider>
</EditorProviders>
)
cy.findByLabelText('File Name').as('input')
cy.findByRole('button', { name: 'Create' }).as('submit')
cy.get('@input').should('have.value', 'name.tex')
cy.get('@submit').should('not.be.disabled')
cy.findByRole('alert').should('not.exist')
cy.get('@input').clear()
cy.get('@submit').should('be.disabled')
cy.findByRole('alert').should('contain.text', 'File name is empty')
cy.get('@input').type('test.tex')
cy.get('@submit').should('not.be.disabled')
cy.findByRole('alert').should('not.exist')
cy.get('@input').type('oops/i/did/it/again')
cy.get('@submit').should('be.disabled')
cy.findByRole('alert').should('contain.text', 'contains invalid characters')
})
it('displays an error when the file limit is reached', function () {
cy.window().then(win => {
win.ExposedSettings.maxEntitiesPerProject = 10
})
const rootFolder = [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: Array.from({ length: 10 }, (_, index) => ({
_id: `entity-${index}`,
})),
fileRefs: [],
folders: [],
},
]
cy.mount(
<EditorProviders rootFolder={rootFolder as any}>
<FileTreeProvider>
<OpenWithMode mode="doc" />
</FileTreeProvider>
</EditorProviders>
)
cy.findByRole('alert')
.invoke('text')
.should('match', /This project has reached the \d+ file limit/)
})
it('displays a warning when the file limit is nearly reached', function () {
cy.window().then(win => {
win.ExposedSettings.maxEntitiesPerProject = 10
})
const rootFolder = [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: Array.from({ length: 9 }, (_, index) => ({
_id: `entity-${index}`,
})),
fileRefs: [],
folders: [],
},
]
cy.mount(
<EditorProviders rootFolder={rootFolder as any}>
<FileTreeProvider>
<OpenWithMode mode="doc" />
</FileTreeProvider>
</EditorProviders>
)
cy.findByText(/This project is approaching the file limit \(\d+\/\d+\)/)
})
it('counts files in nested folders', function () {
cy.window().then(win => {
win.ExposedSettings.maxEntitiesPerProject = 10
})
const rootFolder = [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: 'doc-1' }],
fileRefs: [],
folders: [
{
docs: [{ _id: 'doc-2' }],
fileRefs: [],
folders: [
{
docs: [
{ _id: 'doc-3' },
{ _id: 'doc-4' },
{ _id: 'doc-5' },
{ _id: 'doc-6' },
{ _id: 'doc-7' },
],
fileRefs: [],
folders: [],
},
],
},
],
},
]
cy.mount(
<EditorProviders rootFolder={rootFolder as any}>
<FileTreeProvider>
<OpenWithMode mode="doc" />
</FileTreeProvider>
</EditorProviders>
)
cy.findByText(/This project is approaching the file limit \(\d+\/\d+\)/)
})
it('counts folders toward the limit', function () {
cy.window().then(win => {
win.ExposedSettings.maxEntitiesPerProject = 10
})
const rootFolder = [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: 'doc-1' }],
fileRefs: [],
folders: [
{ docs: [], fileRefs: [], folders: [] },
{ docs: [], fileRefs: [], folders: [] },
{ docs: [], fileRefs: [], folders: [] },
{ docs: [], fileRefs: [], folders: [] },
{ docs: [], fileRefs: [], folders: [] },
{ docs: [], fileRefs: [], folders: [] },
{ docs: [], fileRefs: [], folders: [] },
{ docs: [], fileRefs: [], folders: [] },
],
},
]
cy.mount(
<EditorProviders rootFolder={rootFolder as any}>
<FileTreeProvider>
<OpenWithMode mode="doc" />
</FileTreeProvider>
</EditorProviders>
)
cy.findByText(/This project is approaching the file limit \(\d+\/\d+\)/)
})
it('creates a new file when the form is submitted', function () {
cy.intercept('post', '/project/*/doc', {
statusCode: 204,
}).as('createDoc')
cy.mount(
<EditorProviders>
<FileTreeProvider>
<OpenWithMode mode="doc" />
</FileTreeProvider>
</EditorProviders>
)
cy.findByLabelText('File Name').type('test')
cy.findByRole('button', { name: 'Create' }).click()
cy.wait('@createDoc')
cy.get('@createDoc').its('request.body').should('deep.equal', {
parent_folder_id: 'root-folder-id',
name: 'test.tex',
})
})
it('imports a new file from a project', function () {
cy.window().then(win => {
win.ExposedSettings.hasLinkedProjectFileFeature = true
win.ExposedSettings.hasLinkedProjectOutputFileFeature = true
})
cy.intercept('/user/projects', {
body: {
projects: [
{
_id: 'test-project',
name: 'This Project',
},
{
_id: 'project-1',
name: 'Project One',
},
{
_id: 'project-2',
name: 'Project Two',
},
],
},
})
cy.intercept('/project/*/entities', {
body: {
entities: [
{
path: '/foo.tex',
},
{
path: '/bar.tex',
},
],
},
})
cy.intercept('post', '/project/*/compile', {
body: {
status: 'success',
outputFiles: [
{
build: 'test',
path: 'baz.jpg',
},
{
build: 'test',
path: 'ball.jpg',
},
],
},
})
cy.intercept('post', '/project/*/linked_file', {
statusCode: 204,
}).as('createLinkedFile')
cy.mount(
<EditorProviders>
<FileTreeProvider>
<OpenWithMode mode="project" />
</FileTreeProvider>
</EditorProviders>
)
// initial state, no project selected
cy.findByLabelText('Select a Project').should('not.be.disabled')
// the submit button should be disabled
cy.findByRole('button', { name: 'Create' }).should('be.disabled')
// the source file selector should be disabled
cy.findByLabelText('Select a File').should('be.disabled')
cy.findByLabelText('Select an Output File').should('not.exist')
// TODO: check for options length, excluding current project
// select a project
cy.findByLabelText('Select a Project').select('project-2')
// wait for the source file selector to be enabled
cy.findByLabelText('Select a File').should('not.be.disabled')
cy.findByLabelText('Select an Output File').should('not.exist')
cy.findByRole('button', { name: 'Create' }).should('be.disabled')
// TODO: check for fileInput options length, excluding current project
// click on the button to toggle between source and output files
cy.findByRole('button', {
// NOTE: When changing the label, update the other tests with this label as well.
name: 'select from output files',
}).click()
// wait for the output file selector to be enabled
cy.findByLabelText('Select an Output File').should('not.be.disabled')
cy.findByLabelText('Select a File').should('not.exist')
cy.findByRole('button', { name: 'Create' }).should('be.disabled')
// TODO: check for entityInput options length, excluding current project
cy.findByLabelText('Select an Output File').select('ball.jpg')
cy.findByRole('button', { name: 'Create' }).should('not.be.disabled')
cy.findByRole('button', { name: 'Create' }).click()
cy.get('@createLinkedFile')
.its('request.body')
.should('deep.equal', {
name: 'ball.jpg',
provider: 'project_output_file',
parent_folder_id: 'root-folder-id',
data: {
source_project_id: 'project-2',
source_output_file_path: 'ball.jpg',
build_id: 'test',
},
})
})
describe('when the output files feature is not available', function () {
beforeEach(function () {
cy.window().then(win => {
win.ExposedSettings.hasLinkedProjectFileFeature = true
win.ExposedSettings.hasLinkedProjectOutputFileFeature = false
})
})
it('should not show the import from output file mode', function () {
cy.intercept('/user/projects', {
body: {
projects: [
{
_id: 'test-project',
name: 'This Project',
},
{
_id: 'project-1',
name: 'Project One',
},
{
_id: 'project-2',
name: 'Project Two',
},
],
},
})
cy.mount(
<EditorProviders>
<FileTreeProvider>
<OpenWithMode mode="project" />
</FileTreeProvider>
</EditorProviders>
)
cy.findByLabelText('Select a File')
cy.findByRole('button', {
name: 'select from output files',
}).should('not.exist')
})
})
it('import from a URL when the form is submitted', function () {
cy.intercept('/project/*/linked_file', {
statusCode: 204,
}).as('createLinkedFile')
cy.mount(
<EditorProviders>
<FileTreeProvider>
<OpenWithMode mode="url" />
</FileTreeProvider>
</EditorProviders>
)
cy.findByLabelText('URL to fetch the file from').type(
'https://example.com/example.tex'
)
cy.findByLabelText('File Name In This Project').should(
'have.value',
'example.tex'
)
// check that the name can still be edited manually
cy.findByLabelText('File Name In This Project').clear()
cy.findByLabelText('File Name In This Project').type('test.tex')
cy.findByLabelText('File Name In This Project').should(
'have.value',
'test.tex'
)
cy.findByRole('button', { name: 'Create' }).click()
cy.get('@createLinkedFile')
.its('request.body')
.should('deep.equal', {
name: 'test.tex',
provider: 'url',
parent_folder_id: 'root-folder-id',
data: { url: 'https://example.com/example.tex' },
})
})
it('uploads a dropped file', function () {
cy.intercept('post', '/project/*/upload?folder_id=root-folder-id', {
statusCode: 204,
}).as('uploadFile')
cy.mount(
<EditorProviders>
<FileTreeProvider>
<OpenWithMode mode="upload" />
</FileTreeProvider>
</EditorProviders>
)
// the submit button should not be present
cy.findByRole('button', { name: 'Create' }).should('not.exist')
cy.get('input[type=file]')
.eq(0)
.selectFile(
{
contents: Cypress.Buffer.from('test'),
fileName: 'test.tex',
mimeType: 'text/plain',
lastModified: Date.now(),
},
{
action: 'drag-drop',
force: true, // invisible element
}
)
cy.wait('@uploadFile')
})
it('uploads a pasted file', function () {
cy.intercept('post', '/project/*/upload?folder_id=root-folder-id', {
statusCode: 204,
}).as('uploadFile')
cy.mount(
<EditorProviders>
<FileTreeProvider>
<OpenWithMode mode="upload" />
</FileTreeProvider>
</EditorProviders>
)
// the submit button should not be present
cy.findByRole('button', { name: 'Create' }).should('not.exist')
cy.wrap(null).then(() => {
const clipboardData = new DataTransfer()
clipboardData.items.add(
new File(['test'], 'test.tex', { type: 'text/plain' })
)
cy.findByLabelText('Uppy Dashboard').trigger('paste', { clipboardData })
})
cy.wait('@uploadFile')
})
it('displays upload errors', function () {
cy.intercept('post', '/project/*/upload?folder_id=root-folder-id', {
statusCode: 422,
body: { success: false, error: 'invalid_filename' },
}).as('uploadFile')
cy.mount(
<EditorProviders>
<FileTreeProvider>
<OpenWithMode mode="upload" />
</FileTreeProvider>
</EditorProviders>
)
// the submit button should not be present
cy.findByRole('button', { name: 'Create' }).should('not.exist')
cy.wrap(null).then(() => {
const clipboardData = new DataTransfer()
clipboardData.items.add(
new File(['test'], 'tes!t.tex', { type: 'text/plain' })
)
cy.findByLabelText('Uppy Dashboard').trigger('paste', { clipboardData })
})
cy.wait('@uploadFile')
cy.findByText(
`Upload failed: check that the file name doesnt contain special characters, trailing/leading whitespace or more than 150 characters`
)
})
})
function OpenWithMode({ mode }: { mode: string }) {
const { newFileCreateMode, startCreatingFile } = useFileTreeActionable()
const { fileCount } = useFileTreeData()
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => startCreatingFile(mode), [])
if (!fileCount || !newFileCreateMode) {
return null
}
return <FileTreeModalCreateFile />
}

View file

@ -1,505 +0,0 @@
import { expect } from 'chai'
import * as sinon from 'sinon'
import { useEffect } from 'react'
import { screen, fireEvent, cleanup, waitFor } from '@testing-library/react'
import fetchMock from 'fetch-mock'
import PropTypes from 'prop-types'
import renderWithContext from '../../helpers/render-with-context'
import FileTreeModalCreateFile from '../../../../../../frontend/js/features/file-tree/components/modals/file-tree-modal-create-file'
import { useFileTreeActionable } from '../../../../../../frontend/js/features/file-tree/contexts/file-tree-actionable'
import { useFileTreeData } from '../../../../../../frontend/js/shared/context/file-tree-data-context'
describe('<FileTreeModalCreateFile/>', function () {
beforeEach(function () {
window.csrfToken = 'token'
})
afterEach(function () {
delete window.csrfToken
fetchMock.restore()
cleanup()
})
it('handles invalid file names', async function () {
renderWithContext(<OpenWithMode mode="doc" />)
const submitButton = screen.getByRole('button', { name: 'Create' })
const input = screen.getByLabelText('File Name')
expect(input.value).to.equal('name.tex')
expect(submitButton.disabled).to.be.false
expect(screen.queryAllByRole('alert')).to.be.empty
fireEvent.change(input, { target: { value: '' } })
expect(submitButton.disabled).to.be.true
screen.getByRole(
(role, element) =>
role === 'alert' && element.textContent.match(/File name is empty/)
)
await fireEvent.change(input, { target: { value: 'test.tex' } })
expect(submitButton.disabled).to.be.false
expect(screen.queryAllByRole('alert')).to.be.empty
await fireEvent.change(input, { target: { value: 'oops/i/did/it/again' } })
expect(submitButton.disabled).to.be.true
screen.getByRole(
(role, element) =>
role === 'alert' &&
element.textContent.match(/contains invalid characters/)
)
})
it('displays an error when the file limit is reached', async function () {
const rootFolder = [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: Array.from({ length: 10 }, (_, index) => ({
_id: `entity-${index}`,
})),
fileRefs: [],
folders: [],
},
]
renderWithContext(<OpenWithMode mode="doc" />, {
contextProps: { rootFolder },
})
screen.getByRole(
(role, element) =>
role === 'alert' &&
element.textContent.match(/This project has reached the \d+ file limit/)
)
})
it('displays a warning when the file limit is nearly reached', async function () {
const rootFolder = [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: Array.from({ length: 9 }, (_, index) => ({
_id: `entity-${index}`,
})),
fileRefs: [],
folders: [],
},
]
renderWithContext(<OpenWithMode mode="doc" />, {
contextProps: { rootFolder },
})
screen.getByText(/This project is approaching the file limit \(\d+\/\d+\)/)
})
it('counts files in nested folders', async function () {
const rootFolder = [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: 'doc-1' }],
fileRefs: [],
folders: [
{
docs: [{ _id: 'doc-2' }],
fileRefs: [],
folders: [
{
docs: [
{ _id: 'doc-3' },
{ _id: 'doc-4' },
{ _id: 'doc-5' },
{ _id: 'doc-6' },
{ _id: 'doc-7' },
],
fileRefs: [],
folders: [],
},
],
},
],
},
]
renderWithContext(<OpenWithMode mode="doc" />, {
contextProps: { rootFolder },
})
screen.getByText(/This project is approaching the file limit \(\d+\/\d+\)/)
})
it('counts folders toward the limit', async function () {
const rootFolder = [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: 'doc-1' }],
fileRefs: [],
folders: [
{ docs: [], fileRefs: [], folders: [] },
{ docs: [], fileRefs: [], folders: [] },
{ docs: [], fileRefs: [], folders: [] },
{ docs: [], fileRefs: [], folders: [] },
{ docs: [], fileRefs: [], folders: [] },
{ docs: [], fileRefs: [], folders: [] },
{ docs: [], fileRefs: [], folders: [] },
{ docs: [], fileRefs: [], folders: [] },
],
},
]
renderWithContext(<OpenWithMode mode="doc" />, {
contextProps: { rootFolder },
})
screen.getByText(/This project is approaching the file limit \(\d+\/\d+\)/)
})
it('creates a new file when the form is submitted', async function () {
fetchMock.post('express:/project/:projectId/doc', () => 204)
renderWithContext(<OpenWithMode mode="doc" />)
const input = screen.getByLabelText('File Name')
await fireEvent.change(input, { target: { value: 'test.tex' } })
const submitButton = screen.getByRole('button', { name: 'Create' })
await fireEvent.click(submitButton)
expect(
fetchMock.called('express:/project/:projectId/doc', {
body: {
parent_folder_id: 'root-folder-id',
name: 'test.tex',
},
})
).to.be.true
})
it('imports a new file from a project', async function () {
fetchMock
.get('path:/user/projects', {
projects: [
{
_id: 'test-project',
name: 'This Project',
},
{
_id: 'project-1',
name: 'Project One',
},
{
_id: 'project-2',
name: 'Project Two',
},
],
})
.get('express:/project/:projectId/entities', {
entities: [
{
path: '/foo.tex',
},
{
path: '/bar.tex',
},
],
})
.post('express:/project/:projectId/compile', {
status: 'success',
outputFiles: [
{
build: 'test',
path: 'baz.jpg',
},
{
build: 'test',
path: 'ball.jpg',
},
],
})
.post('express:/project/:projectId/linked_file', () => 204)
renderWithContext(<OpenWithMode mode="project" />)
// initial state, no project selected
const projectInput = screen.getByLabelText('Select a Project')
expect(projectInput.disabled).to.be.true
await waitFor(() => {
expect(projectInput.disabled).to.be.false
})
// the submit button should be disabled
const submitButton = screen.getByRole('button', { name: 'Create' })
expect(submitButton.disabled).to.be.true
// the source file selector should be disabled
const fileInput = screen.getByLabelText('Select a File')
expect(fileInput.disabled).to.be.true
// TODO: check for options length, excluding current project
// select a project
await fireEvent.change(projectInput, { target: { value: 'project-2' } }) // TODO: getByRole('option')?
// wait for the source file selector to be enabled
await waitFor(() => {
expect(fileInput.disabled).to.be.false
})
expect(screen.queryByLabelText('Select a File')).not.to.be.null
expect(screen.queryByLabelText('Select an Output File')).to.be.null
expect(submitButton.disabled).to.be.true
// TODO: check for fileInput options length, excluding current project
// click on the button to toggle between source and output files
const sourceTypeButton = screen.getByRole('button', {
// NOTE: When changing the label, update the other tests with this label as well.
name: 'select from output files',
})
await fireEvent.click(sourceTypeButton)
// wait for the output file selector to be enabled
const entityInput = screen.getByLabelText('Select an Output File')
await waitFor(() => {
expect(entityInput.disabled).to.be.false
})
expect(screen.queryByLabelText('Select a File')).to.be.null
expect(screen.queryByLabelText('Select an Output File')).not.to.be.null
expect(submitButton.disabled).to.be.true
// TODO: check for entityInput options length, excluding current project
await fireEvent.change(entityInput, { target: { value: 'ball.jpg' } }) // TODO: getByRole('option')?
await waitFor(() => {
expect(submitButton.disabled).to.be.false
})
await fireEvent.click(submitButton)
expect(
fetchMock.called('express:/project/:projectId/linked_file', {
body: {
name: 'ball.jpg',
provider: 'project_output_file',
parent_folder_id: 'root-folder-id',
data: {
source_project_id: 'project-2',
source_output_file_path: 'ball.jpg',
build_id: 'test',
},
},
})
).to.be.true
})
describe('when the output files feature is not available', function () {
const flagBefore = window.ExposedSettings.hasLinkedProjectOutputFileFeature
before(function () {
window.ExposedSettings.hasLinkedProjectOutputFileFeature = false
})
after(function () {
window.ExposedSettings.hasLinkedProjectOutputFileFeature = flagBefore
})
it('should not show the import from output file mode', async function () {
fetchMock.get('path:/user/projects', {
projects: [
{
_id: 'test-project',
name: 'This Project',
},
{
_id: 'project-1',
name: 'Project One',
},
{
_id: 'project-2',
name: 'Project Two',
},
],
})
renderWithContext(<OpenWithMode mode="project" />)
// should not show the toggle
expect(
screen.queryByRole('button', {
name: 'select from output files',
})
).to.be.null
})
})
it('import from a URL when the form is submitted', async function () {
fetchMock.post('express:/project/:projectId/linked_file', () => 204)
renderWithContext(<OpenWithMode mode="url" />)
const urlInput = screen.getByLabelText('URL to fetch the file from')
const nameInput = screen.getByLabelText('File Name In This Project')
await fireEvent.change(urlInput, {
target: { value: 'https://example.com/example.tex' },
})
// check that the name has updated automatically
expect(nameInput.value).to.equal('example.tex')
await fireEvent.change(nameInput, {
target: { value: 'test.tex' },
})
// check that the name can still be edited manually
expect(nameInput.value).to.equal('test.tex')
const submitButton = screen.getByRole('button', { name: 'Create' })
await fireEvent.click(submitButton)
expect(
fetchMock.called('express:/project/:projectId/linked_file', {
body: {
name: 'test.tex',
provider: 'url',
parent_folder_id: 'root-folder-id',
data: { url: 'https://example.com/example.tex' },
},
})
).to.be.true
})
it('uploads a dropped file', async function () {
const xhr = sinon.useFakeXMLHttpRequest()
const requests = []
xhr.onCreate = request => {
requests.push(request)
}
renderWithContext(<OpenWithMode mode="upload" />)
// the submit button should not be present
expect(screen.queryByRole('button', { name: 'Create' })).to.be.null
await waitFor(() => {
const dropzone = screen.getByLabelText('File Uploader')
expect(dropzone).not.to.be.null
fireEvent.drop(dropzone, {
dataTransfer: {
files: [new File(['test'], 'test.tex', { type: 'text/plain' })],
},
})
})
await waitFor(() => expect(requests).to.have.length(1))
const [request] = requests
expect(request.url).to.equal(
'/project/123abc/upload?folder_id=root-folder-id'
)
expect(request.method).to.equal('POST')
xhr.restore()
})
it('uploads a pasted file', async function () {
const xhr = sinon.useFakeXMLHttpRequest()
const requests = []
xhr.onCreate = request => {
requests.push(request)
}
renderWithContext(<OpenWithMode mode="upload" />)
// the submit button should not be present
expect(screen.queryByRole('button', { name: 'Create' })).to.be.null
await waitFor(() => {
const dropzone = screen.getByLabelText('File Uploader')
expect(dropzone).not.to.be.null
fireEvent.paste(dropzone, {
clipboardData: {
files: [new File(['test'], 'test.tex', { type: 'text/plain' })],
},
})
})
await waitFor(() => expect(requests).to.have.length(1))
const [request] = requests
expect(request.url).to.equal(
'/project/123abc/upload?folder_id=root-folder-id'
)
expect(request.method).to.equal('POST')
xhr.restore()
})
it('displays upload errors', async function () {
const xhr = sinon.useFakeXMLHttpRequest()
const requests = []
xhr.onCreate = request => {
requests.push(request)
}
renderWithContext(<OpenWithMode mode="upload" />)
// the submit button should not be present
expect(screen.queryByRole('button', { name: 'Create' })).to.be.null
await waitFor(() => {
const dropzone = screen.getByLabelText('File Uploader')
expect(dropzone).not.to.be.null
fireEvent.paste(dropzone, {
clipboardData: {
files: [new File(['test'], 'tes!t.tex', { type: 'text/plain' })],
},
})
})
await waitFor(() => expect(requests).to.have.length(1))
const [request] = requests
expect(request.url).to.equal(
'/project/123abc/upload?folder_id=root-folder-id'
)
expect(request.method).to.equal('POST')
request.respond(
422,
{ 'Content-Type': 'application/json' },
'{ "success": false, "error": "invalid_filename" }'
)
await screen.findByText(
`Upload failed: check that the file name doesnt contain special characters, trailing/leading whitespace or more than 150 characters`
)
xhr.restore()
})
})
function OpenWithMode({ mode }) {
const { newFileCreateMode, startCreatingFile } = useFileTreeActionable()
const { fileCount } = useFileTreeData()
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => startCreatingFile(mode), [])
if (!fileCount || !newFileCreateMode) {
return null
}
return <FileTreeModalCreateFile />
}
OpenWithMode.propTypes = {
mode: PropTypes.string.isRequired,
}

View file

@ -0,0 +1,77 @@
import FileTreeDoc from '../../../../../frontend/js/features/file-tree/components/file-tree-doc'
import { EditorProviders } from '../../../helpers/editor-providers'
import { FileTreeProvider } from '../helpers/file-tree-provider'
describe('<FileTreeDoc/>', function () {
it('renders unselected', function () {
cy.mount(
<EditorProviders>
<FileTreeProvider>
<FileTreeDoc name="foo.tex" id="123abc" />
</FileTreeProvider>
</EditorProviders>
)
cy.findByRole('treeitem', { selected: false })
cy.get('i.linked-file-highlight').should('not.exist')
})
it('renders selected', function () {
const rootFolder = [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '123abc' }],
fileRefs: [],
folders: [],
},
]
cy.mount(
<EditorProviders rootFolder={rootFolder as any}>
<FileTreeProvider>
<FileTreeDoc name="foo.tex" id="123abc" />,
</FileTreeProvider>
</EditorProviders>
)
cy.findByRole('treeitem', { selected: false }).click()
cy.findByRole('treeitem', { selected: true })
})
it('renders as linked file', function () {
cy.mount(
<EditorProviders>
<FileTreeProvider>
<FileTreeDoc name="foo.tex" id="123abc" isLinkedFile />
</FileTreeProvider>
</EditorProviders>
)
cy.findByRole('treeitem')
cy.get('i.linked-file-highlight')
})
it('multi-selects', function () {
const rootFolder = [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '123abc' }],
fileRefs: [],
folders: [],
},
]
cy.mount(
<EditorProviders rootFolder={rootFolder as any}>
<FileTreeProvider>
<FileTreeDoc name="foo.tex" id="123abc" />,
</FileTreeProvider>
</EditorProviders>
)
cy.findByRole('treeitem').click({ ctrlKey: true, cmdKey: true })
cy.findByRole('treeitem', { selected: true })
})
})

View file

@ -1,91 +0,0 @@
import { expect } from 'chai'
import { screen, fireEvent } from '@testing-library/react'
import renderWithContext from '../helpers/render-with-context'
import FileTreeDoc from '../../../../../frontend/js/features/file-tree/components/file-tree-doc'
describe('<FileTreeDoc/>', function () {
it('renders unselected', function () {
const { container } = renderWithContext(
<FileTreeDoc name="foo.tex" id="123abc" isLinkedFile={false} />
)
screen.getByRole('treeitem', { selected: false })
expect(container.querySelector('i.linked-file-highlight')).to.not.exist
})
it('renders selected', function () {
renderWithContext(
<FileTreeDoc name="foo.tex" id="123abc" isLinkedFile={false} />,
{
contextProps: {
rootFolder: [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '123abc' }],
fileRefs: [],
folders: [],
},
],
},
}
)
const treeitem = screen.getByRole('treeitem', { selected: false })
fireEvent.click(treeitem)
screen.getByRole('treeitem', { selected: true })
})
it('renders as linked file', function () {
const { container } = renderWithContext(
<FileTreeDoc name="foo.tex" id="123abc" isLinkedFile />
)
screen.getByRole('treeitem')
expect(container.querySelector('i.linked-file-highlight')).to.exist
})
it('selects', function () {
renderWithContext(<FileTreeDoc name="foo.tex" id="123abc" expanded />, {
contextProps: {
rootFolder: [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '123abc' }],
fileRefs: [],
folders: [],
},
],
},
})
const treeitem = screen.getByRole('treeitem', { selected: false })
fireEvent.click(treeitem)
screen.getByRole('treeitem', { selected: true })
})
it('multi-selects', function () {
renderWithContext(<FileTreeDoc name="foo.tex" id="123abc" expanded />, {
contextProps: {
rootFolder: [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '123abc' }],
fileRefs: [],
folders: [],
},
],
},
})
const treeitem = screen.getByRole('treeitem')
fireEvent.click(treeitem, { ctrlKey: true })
screen.getByRole('treeitem', { selected: true })
})
})

View file

@ -0,0 +1,190 @@
import FileTreeFolderList from '../../../../../frontend/js/features/file-tree/components/file-tree-folder-list'
import { EditorProviders } from '../../../helpers/editor-providers'
import { FileTreeProvider } from '../helpers/file-tree-provider'
describe('<FileTreeFolderList/>', function () {
it('renders empty', function () {
cy.mount(
<EditorProviders>
<FileTreeProvider>
<FileTreeFolderList folders={[]} docs={[]} files={[]} />
</FileTreeProvider>
</EditorProviders>
)
cy.findByRole('tree')
cy.findByRole('treeitem').should('not.exist')
})
it('renders docs, files and folders', function () {
cy.mount(
<EditorProviders>
<FileTreeProvider>
<FileTreeFolderList
folders={[
{
_id: '456def',
name: 'A Folder',
folders: [],
docs: [],
fileRefs: [],
},
]}
docs={[{ _id: '789ghi', name: 'doc.tex', linkedFileData: {} }]}
files={[{ _id: '987jkl', name: 'file.bib', linkedFileData: {} }]}
/>
</FileTreeProvider>
</EditorProviders>
)
cy.findByRole('tree')
cy.findByRole('treeitem', { name: 'A Folder' })
cy.findByRole('treeitem', { name: 'doc.tex' })
cy.findByRole('treeitem', { name: 'file.bib' })
})
describe('selection and multi-selection', function () {
it('without write permissions', function () {
const rootFolder = [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '1' }, { _id: '2' }],
fileRefs: [],
folders: [],
},
]
cy.mount(
<EditorProviders
rootFolder={rootFolder as any}
permissionsLevel="readOnly"
>
<FileTreeProvider>
<FileTreeFolderList
folders={[]}
docs={[
{ _id: '1', name: '1.tex' },
{ _id: '2', name: '2.tex' },
]}
files={[]}
/>
</FileTreeProvider>
</EditorProviders>
)
// click on item 1: it gets selected
cy.findByRole('treeitem', { name: '1.tex' }).click()
cy.findByRole('treeitem', { name: '1.tex', selected: true })
cy.findByRole('treeitem', { name: '2.tex', selected: false })
// meta-click on item 2: no changes
cy.findByRole('treeitem', { name: '2.tex' }).click({
ctrlKey: true,
cmdKey: true,
})
cy.findByRole('treeitem', { name: '1.tex', selected: true })
cy.findByRole('treeitem', { name: '2.tex', selected: false })
})
it('with write permissions', function () {
const rootFolder = [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '1' }, { _id: '2' }, { _id: '3' }],
fileRefs: [],
folders: [],
},
]
cy.mount(
<EditorProviders rootFolder={rootFolder as any}>
<FileTreeProvider>
<FileTreeFolderList
folders={[]}
docs={[
{ _id: '1', name: '1.tex' },
{ _id: '2', name: '2.tex' },
{ _id: '3', name: '3.tex' },
]}
files={[]}
/>
</FileTreeProvider>
</EditorProviders>
)
// click item 1: it gets selected
cy.findByRole('treeitem', { name: '1.tex' }).click()
cy.findByRole('treeitem', { name: '1.tex', selected: true })
cy.findByRole('treeitem', { name: '2.tex', selected: false })
cy.findByRole('treeitem', { name: '3.tex', selected: false })
// click on item 2: it gets selected and item 1 is not selected anymore
cy.findByRole('treeitem', { name: '2.tex' }).click()
cy.findByRole('treeitem', { name: '1.tex', selected: false })
cy.findByRole('treeitem', { name: '2.tex', selected: true })
cy.findByRole('treeitem', { name: '3.tex', selected: false })
// meta-click on item 3: it gets selected and item 2 as well
cy.findByRole('treeitem', { name: '3.tex' }).click({
ctrlKey: true,
cmdKey: true,
})
cy.findByRole('treeitem', { name: '1.tex', selected: false })
cy.findByRole('treeitem', { name: '2.tex', selected: true })
cy.findByRole('treeitem', { name: '3.tex', selected: true })
// meta-click on item 1: add to selection
cy.findByRole('treeitem', { name: '1.tex' }).click({
ctrlKey: true,
cmdKey: true,
})
cy.findByRole('treeitem', { name: '1.tex', selected: true })
cy.findByRole('treeitem', { name: '2.tex', selected: true })
cy.findByRole('treeitem', { name: '3.tex', selected: true })
// meta-click on item 1: remove from selection
cy.findByRole('treeitem', { name: '1.tex' }).click({
ctrlKey: true,
cmdKey: true,
})
cy.findByRole('treeitem', { name: '1.tex', selected: false })
cy.findByRole('treeitem', { name: '2.tex', selected: true })
cy.findByRole('treeitem', { name: '3.tex', selected: true })
// meta-click on item 3: remove from selection
cy.findByRole('treeitem', { name: '3.tex' }).click({
ctrlKey: true,
cmdKey: true,
})
cy.findByRole('treeitem', { name: '1.tex', selected: false })
cy.findByRole('treeitem', { name: '2.tex', selected: true })
cy.findByRole('treeitem', { name: '3.tex', selected: false })
// meta-click on item 2: cannot unselect
cy.findByRole('treeitem', { name: '2.tex' }).click({
ctrlKey: true,
cmdKey: true,
})
cy.findByRole('treeitem', { name: '1.tex', selected: false })
cy.findByRole('treeitem', { name: '2.tex', selected: true })
cy.findByRole('treeitem', { name: '3.tex', selected: false })
// meta-click on item 3: add back to selection
cy.findByRole('treeitem', { name: '3.tex' }).click({
ctrlKey: true,
cmdKey: true,
})
cy.findByRole('treeitem', { name: '1.tex', selected: false })
cy.findByRole('treeitem', { name: '2.tex', selected: true })
cy.findByRole('treeitem', { name: '3.tex', selected: true })
// click on item 3: unselect other items
cy.findByRole('treeitem', { name: '3.tex' }).click()
cy.findByRole('treeitem', { name: '1.tex', selected: false })
cy.findByRole('treeitem', { name: '2.tex', selected: false })
cy.findByRole('treeitem', { name: '3.tex', selected: true })
})
})
})

View file

@ -1,155 +0,0 @@
import { expect } from 'chai'
import { screen, fireEvent } from '@testing-library/react'
import renderWithContext from '../helpers/render-with-context'
import FileTreeFolderList from '../../../../../frontend/js/features/file-tree/components/file-tree-folder-list'
describe('<FileTreeFolderList/>', function () {
it('renders empty', function () {
renderWithContext(<FileTreeFolderList folders={[]} docs={[]} files={[]} />)
screen.queryByRole('tree')
expect(screen.queryByRole('treeitem')).to.not.exist
})
it('renders docs, files and folders', function () {
const aFolder = {
_id: '456def',
name: 'A Folder',
folders: [],
docs: [],
fileRefs: [],
}
const aDoc = { _id: '789ghi', name: 'doc.tex', linkedFileData: {} }
const aFile = { _id: '987jkl', name: 'file.bib', linkedFileData: {} }
renderWithContext(
<FileTreeFolderList folders={[aFolder]} docs={[aDoc]} files={[aFile]} />
)
screen.queryByRole('tree')
screen.queryByRole('treeitem', { name: 'A Folder' })
screen.queryByRole('treeitem', { name: 'doc.tex' })
screen.queryByRole('treeitem', { name: 'file.bib' })
})
describe('selection and multi-selection', function () {
it('without write permissions', function () {
const docs = [
{ _id: '1', name: '1.tex' },
{ _id: '2', name: '2.tex' },
]
renderWithContext(
<FileTreeFolderList folders={[]} docs={docs} files={[]} />,
{
contextProps: {
permissionsLevel: 'readOnly',
rootFolder: [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '1' }, { _id: '2' }],
fileRefs: [],
folders: [],
},
],
},
}
)
const treeitem1 = screen.getByRole('treeitem', { name: '1.tex' })
const treeitem2 = screen.getByRole('treeitem', { name: '2.tex' })
// click on item 1: it gets selected
fireEvent.click(treeitem1)
screen.getByRole('treeitem', { name: '1.tex', selected: true })
screen.getByRole('treeitem', { name: '2.tex', selected: false })
// meta-click on item 2: no changes
fireEvent.click(treeitem2, { ctrlKey: true })
screen.getByRole('treeitem', { name: '1.tex', selected: true })
screen.getByRole('treeitem', { name: '2.tex', selected: false })
})
it('with write permissions', function () {
const docs = [
{ _id: '1', name: '1.tex' },
{ _id: '2', name: '2.tex' },
{ _id: '3', name: '3.tex' },
]
renderWithContext(
<FileTreeFolderList folders={[]} docs={docs} files={[]} />,
{
contextProps: {
rootFolder: [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '1' }, { _id: '2' }, { _id: '3' }],
fileRefs: [],
folders: [],
},
],
},
}
)
const treeitem1 = screen.getByRole('treeitem', { name: '1.tex' })
const treeitem2 = screen.getByRole('treeitem', { name: '2.tex' })
const treeitem3 = screen.getByRole('treeitem', { name: '3.tex' })
// click item 1: it gets selected
fireEvent.click(treeitem1)
screen.getByRole('treeitem', { name: '1.tex', selected: true })
screen.getByRole('treeitem', { name: '2.tex', selected: false })
screen.getByRole('treeitem', { name: '3.tex', selected: false })
// click on item 2: it gets selected and item 1 is not selected anymore
fireEvent.click(treeitem2)
screen.getByRole('treeitem', { name: '1.tex', selected: false })
screen.getByRole('treeitem', { name: '2.tex', selected: true })
screen.getByRole('treeitem', { name: '3.tex', selected: false })
// meta-click on item 3: it gets selected and item 2 as well
fireEvent.click(treeitem3, { ctrlKey: true })
screen.getByRole('treeitem', { name: '1.tex', selected: false })
screen.getByRole('treeitem', { name: '2.tex', selected: true })
screen.getByRole('treeitem', { name: '3.tex', selected: true })
// meta-click on item 1: add to selection
fireEvent.click(treeitem1, { ctrlKey: true })
screen.getByRole('treeitem', { name: '1.tex', selected: true })
screen.getByRole('treeitem', { name: '2.tex', selected: true })
screen.getByRole('treeitem', { name: '3.tex', selected: true })
// meta-click on item 1: remove from selection
fireEvent.click(treeitem1, { ctrlKey: true })
screen.getByRole('treeitem', { name: '1.tex', selected: false })
screen.getByRole('treeitem', { name: '2.tex', selected: true })
screen.getByRole('treeitem', { name: '3.tex', selected: true })
// meta-click on item 3: remove from selection
fireEvent.click(treeitem3, { ctrlKey: true })
screen.getByRole('treeitem', { name: '1.tex', selected: false })
screen.getByRole('treeitem', { name: '2.tex', selected: true })
screen.getByRole('treeitem', { name: '3.tex', selected: false })
// meta-click on item 2: cannot unselect
fireEvent.click(treeitem2, { ctrlKey: true })
screen.getByRole('treeitem', { name: '1.tex', selected: false })
screen.getByRole('treeitem', { name: '2.tex', selected: true })
screen.getByRole('treeitem', { name: '3.tex', selected: false })
// meta-click on item 3: add back to selection
fireEvent.click(treeitem3, { ctrlKey: true })
screen.getByRole('treeitem', { name: '1.tex', selected: false })
screen.getByRole('treeitem', { name: '2.tex', selected: true })
screen.getByRole('treeitem', { name: '3.tex', selected: true })
// click on item 3: unselect other items
fireEvent.click(treeitem3)
screen.getByRole('treeitem', { name: '1.tex', selected: false })
screen.getByRole('treeitem', { name: '2.tex', selected: false })
screen.getByRole('treeitem', { name: '3.tex', selected: true })
})
})
})

View file

@ -0,0 +1,134 @@
import FileTreeFolder from '../../../../../frontend/js/features/file-tree/components/file-tree-folder'
import { EditorProviders } from '../../../helpers/editor-providers'
import { FileTreeProvider } from '../helpers/file-tree-provider'
import { getContainerEl } from 'cypress/react'
import ReactDom from 'react-dom'
describe('<FileTreeFolder/>', function () {
it('renders unselected', function () {
cy.mount(
<EditorProviders>
<FileTreeProvider>
<FileTreeFolder
name="foo"
id="123abc"
folders={[]}
docs={[]}
files={[]}
/>
</FileTreeProvider>
</EditorProviders>
)
cy.findByRole('treeitem', { selected: false })
cy.findByRole('tree').should('not.exist')
})
it('renders selected', function () {
const rootFolder = [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '123abc' }],
fileRefs: [],
folders: [],
},
]
cy.mount(
<EditorProviders rootFolder={rootFolder as any}>
<FileTreeProvider>
<FileTreeFolder
name="foo"
id="123abc"
folders={[]}
docs={[]}
files={[]}
/>
</FileTreeProvider>
</EditorProviders>
)
cy.findByRole('treeitem', { selected: false }).click()
cy.findByRole('treeitem', { selected: true })
cy.findByRole('tree').should('not.exist')
})
it('expands', function () {
const rootFolder = [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '123abc' }],
fileRefs: [],
folders: [],
},
]
cy.mount(
<EditorProviders rootFolder={rootFolder as any}>
<FileTreeProvider>
<FileTreeFolder
name="foo"
id="123abc"
folders={[]}
docs={[]}
files={[]}
/>
</FileTreeProvider>
</EditorProviders>
)
cy.findByRole('treeitem')
cy.findByRole('button', { name: 'Expand' }).click()
cy.findByRole('tree')
})
it('saves the expanded state for the next render', function () {
const rootFolder = [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '123abc' }],
fileRefs: [],
folders: [],
},
]
cy.mount(
<EditorProviders rootFolder={rootFolder as any}>
<FileTreeProvider>
<FileTreeFolder
name="foo"
id="123abc"
folders={[]}
docs={[]}
files={[]}
/>
</FileTreeProvider>
</EditorProviders>
)
cy.findByRole('tree').should('not.exist')
cy.findByRole('button', { name: 'Expand' }).click()
cy.findByRole('tree')
cy.then(() => ReactDom.unmountComponentAtNode(getContainerEl()))
cy.mount(
<EditorProviders>
<FileTreeProvider>
<FileTreeFolder
name="foo"
id="123abc"
folders={[]}
docs={[]}
files={[]}
/>
</FileTreeProvider>
</EditorProviders>
)
cy.findByRole('tree')
})
})

View file

@ -1,133 +0,0 @@
import { expect } from 'chai'
import { screen, fireEvent } from '@testing-library/react'
import renderWithContext from '../helpers/render-with-context'
import FileTreeFolder from '../../../../../frontend/js/features/file-tree/components/file-tree-folder'
describe('<FileTreeFolder/>', function () {
beforeEach(function () {
global.localStorage.clear()
})
it('renders unselected', function () {
renderWithContext(
<FileTreeFolder
name="foo"
id="123abc"
folders={[]}
docs={[]}
files={[]}
/>
)
screen.getByRole('treeitem', { selected: false })
expect(screen.queryByRole('tree')).to.not.exist
})
it('renders selected', function () {
renderWithContext(
<FileTreeFolder
name="foo"
id="123abc"
folders={[]}
docs={[]}
files={[]}
/>,
{
contextProps: {
rootFolder: [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '123abc' }],
fileRefs: [],
folders: [],
},
],
},
}
)
const treeitem = screen.getByRole('treeitem', { selected: false })
fireEvent.click(treeitem)
screen.getByRole('treeitem', { selected: true })
expect(screen.queryByRole('tree')).to.not.exist
})
it('expands', function () {
renderWithContext(
<FileTreeFolder
name="foo"
id="123abc"
folders={[]}
docs={[]}
files={[]}
/>,
{
contextProps: {
rootFolder: [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '123abc' }],
fileRefs: [],
folders: [],
},
],
},
}
)
screen.getByRole('treeitem')
const expandButton = screen.getByRole('button', { name: 'Expand' })
fireEvent.click(expandButton)
screen.getByRole('tree')
})
it('saves the expanded state for the next render', function () {
const { unmount } = renderWithContext(
<FileTreeFolder
name="foo"
id="123abc"
folders={[]}
docs={[]}
files={[]}
/>,
{
contextProps: {
rootFolder: [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '123abc' }],
fileRefs: [],
folders: [],
},
],
},
}
)
expect(screen.queryByRole('tree')).to.not.exist
const expandButton = screen.getByRole('button', { name: 'Expand' })
fireEvent.click(expandButton)
screen.getByRole('tree')
unmount()
renderWithContext(
<FileTreeFolder
name="foo"
id="123abc"
folders={[]}
docs={[]}
files={[]}
/>
)
screen.getByRole('tree')
})
})

View file

@ -0,0 +1,98 @@
import FileTreeitemInner from '../../../../../../frontend/js/features/file-tree/components/file-tree-item/file-tree-item-inner'
import FileTreeContextMenu from '../../../../../../frontend/js/features/file-tree/components/file-tree-context-menu'
import { EditorProviders } from '../../../../helpers/editor-providers'
import { FileTreeProvider } from '../../helpers/file-tree-provider'
describe('<FileTreeitemInner />', function () {
describe('menu', function () {
it('does not display if file is not selected', function () {
cy.mount(
<EditorProviders>
<FileTreeProvider>
<FileTreeitemInner id="123abc" name="bar.tex" isSelected={false} />,
</FileTreeProvider>
</EditorProviders>
)
cy.findByRole('menu', { hidden: true }).should('not.exist')
})
})
describe('context menu', function () {
it('does not display without write permissions', function () {
cy.mount(
<EditorProviders permissionsLevel="readOnly">
<FileTreeProvider>
<FileTreeitemInner id="123abc" name="bar.tex" isSelected />
<FileTreeContextMenu />
</FileTreeProvider>
</EditorProviders>
)
cy.get('div.entity').trigger('contextmenu')
cy.findByRole('menu', { hidden: true }).should('not.exist')
})
it('open / close', function () {
cy.mount(
<EditorProviders>
<FileTreeProvider>
<FileTreeitemInner id="123abc" name="bar.tex" isSelected />
<FileTreeContextMenu />
</FileTreeProvider>
</EditorProviders>
)
cy.findByRole('menu', { hidden: true }).should('not.exist')
// open the context menu
cy.get('div.entity').trigger('contextmenu')
cy.findByRole('menu')
// close the context menu
cy.get('div.entity').click()
cy.findByRole('menu').should('not.exist')
})
})
describe('name', function () {
it('renders name', function () {
cy.mount(
<EditorProviders>
<FileTreeProvider>
<FileTreeitemInner id="123abc" name="bar.tex" isSelected />
</FileTreeProvider>
</EditorProviders>
)
cy.findByRole('button', { name: 'bar.tex' })
cy.findByRole('textbox').should('not.exist')
})
it('starts rename on menu item click', function () {
const rootFolder = [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '123abc', name: 'bar.tex' }],
folders: [],
fileRefs: [],
},
]
cy.mount(
<EditorProviders rootDocId="123abc" rootFolder={rootFolder as any}>
<FileTreeProvider>
<FileTreeitemInner id="123abc" name="bar.tex" isSelected />
<FileTreeContextMenu />
</FileTreeProvider>
</EditorProviders>
)
cy.findByRole('button', { name: 'Menu' }).click()
cy.findByRole('menuitem', { name: 'Rename' }).click()
cy.findByRole('button', { name: 'bar.tex' }).should('not.exist')
cy.findByRole('textbox')
})
})
})

View file

@ -1,104 +0,0 @@
import { expect } from 'chai'
import sinon from 'sinon'
import { screen, fireEvent } from '@testing-library/react'
import renderWithContext from '../../helpers/render-with-context'
import FileTreeitemInner from '../../../../../../frontend/js/features/file-tree/components/file-tree-item/file-tree-item-inner'
import FileTreeContextMenu from '../../../../../../frontend/js/features/file-tree/components/file-tree-context-menu'
describe('<FileTreeitemInner />', function () {
const setContextMenuCoords = sinon.stub()
afterEach(function () {
setContextMenuCoords.reset()
})
describe('menu', function () {
it('does not display if file is not selected', function () {
renderWithContext(
<FileTreeitemInner id="123abc" name="bar.tex" isSelected={false} />,
{}
)
expect(screen.queryByRole('menu', { visible: false })).to.not.exist
})
})
describe('context menu', function () {
it('does not display without write permissions', function () {
const { container } = renderWithContext(
<>
<FileTreeitemInner id="123abc" name="bar.tex" isSelected />
<FileTreeContextMenu />
</>,
{
contextProps: { permissionsLevel: 'readOnly' },
}
)
const entityElement = container.querySelector('div.entity')
fireEvent.contextMenu(entityElement)
expect(screen.queryByRole('menu')).to.not.exist
})
it('open / close', function () {
const { container } = renderWithContext(
<>
<FileTreeitemInner id="123abc" name="bar.tex" isSelected />
<FileTreeContextMenu />
</>
)
expect(screen.queryByRole('menu')).to.be.null
// open the context menu
const entityElement = container.querySelector('div.entity')
fireEvent.contextMenu(entityElement)
screen.getByRole('menu', { visible: true })
// close the context menu
fireEvent.click(entityElement)
expect(screen.queryByRole('menu')).to.be.null
})
})
describe('name', function () {
it('renders name', function () {
renderWithContext(
<FileTreeitemInner id="123abc" name="bar.tex" isSelected />
)
screen.getByRole('button', { name: 'bar.tex' })
expect(screen.queryByRole('textbox')).to.not.exist
})
it('starts rename on menu item click', function () {
renderWithContext(
<>
<FileTreeitemInner id="123abc" name="bar.tex" isSelected />
<FileTreeContextMenu />
</>,
{
contextProps: {
rootDocId: '123abc',
rootFolder: [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '123abc', name: 'bar.tex' }],
folders: [],
fileRefs: [],
},
],
},
}
)
const toggleButton = screen.getByRole('button', { name: 'Menu' })
fireEvent.click(toggleButton)
const renameButton = screen.getByRole('menuitem', { name: 'Rename' })
fireEvent.click(renameButton)
expect(screen.queryByRole('button', { name: 'bar.tex' })).to.not.exist
screen.getByRole('textbox')
})
})
})

View file

@ -0,0 +1,108 @@
import FileTreeItemName from '../../../../../../frontend/js/features/file-tree/components/file-tree-item/file-tree-item-name'
import { EditorProviders } from '../../../../helpers/editor-providers'
import { FileTreeProvider } from '../../helpers/file-tree-provider'
describe('<FileTreeItemName />', function () {
it('renders name as button', function () {
cy.mount(
<EditorProviders>
<FileTreeProvider>
<FileTreeItemName
name="foo.tex"
isSelected
setIsDraggable={cy.stub()}
/>
</FileTreeProvider>
</EditorProviders>
)
cy.findByRole('button', { name: 'foo.tex' })
cy.findByRole('textbox').should('not.exist')
})
it("doesn't start renaming on unselected component", function () {
cy.mount(
<EditorProviders>
<FileTreeProvider>
<FileTreeItemName
name="foo.tex"
isSelected={false}
setIsDraggable={cy.stub()}
/>
</FileTreeProvider>
</EditorProviders>
)
cy.findByRole('button').click()
cy.findByRole('button').click()
cy.findByRole('button').dblclick()
cy.findByRole('textbox').should('not.exist')
})
it('start renaming on double-click', function () {
cy.mount(
<EditorProviders>
<FileTreeProvider>
<FileTreeItemName
name="foo.tex"
isSelected
setIsDraggable={cy.stub().as('setIsDraggable')}
/>
</FileTreeProvider>
</EditorProviders>
)
cy.findByRole('button').click()
cy.findByRole('button').click()
cy.findByRole('button').dblclick()
cy.findByRole('textbox')
cy.findByRole('button').should('not.exist')
cy.get('@setIsDraggable').should('have.been.calledWith', false)
})
it('cannot start renaming in read-only', function () {
cy.mount(
<EditorProviders permissionsLevel="readOnly">
<FileTreeProvider>
<FileTreeItemName
name="foo.tex"
isSelected
setIsDraggable={cy.stub()}
/>
</FileTreeProvider>
</EditorProviders>
)
cy.findByRole('button').click()
cy.findByRole('button').click()
cy.findByRole('button').dblclick()
cy.findByRole('textbox').should('not.exist')
})
describe('stop renaming', function () {
it('on Escape', function () {
cy.mount(
<EditorProviders>
<FileTreeProvider>
<FileTreeItemName
name="foo.tex"
isSelected
setIsDraggable={cy.stub().as('setIsDraggable')}
/>
</FileTreeProvider>
</EditorProviders>
)
cy.findByRole('button').click()
cy.findByRole('button').click()
cy.findByRole('button').dblclick()
cy.findByRole('textbox').clear()
cy.findByRole('textbox').type('bar.tex{esc}')
cy.findByRole('button', { name: 'foo.tex' })
cy.get('@setIsDraggable').should('have.been.calledWith', true)
})
})
})

View file

@ -1,117 +0,0 @@
import { expect } from 'chai'
import sinon from 'sinon'
import { screen, fireEvent, cleanup } from '@testing-library/react'
import renderWithContext from '../../helpers/render-with-context'
import FileTreeItemName from '../../../../../../frontend/js/features/file-tree/components/file-tree-item/file-tree-item-name'
describe('<FileTreeItemName />', function () {
const sandbox = sinon.createSandbox()
const setIsDraggable = sinon.stub()
beforeEach(function () {
sandbox.spy(window, 'requestAnimationFrame')
})
afterEach(function () {
sandbox.restore()
setIsDraggable.reset()
cleanup()
})
it('renders name as button', function () {
renderWithContext(
<FileTreeItemName
name="foo.tex"
isSelected
setIsDraggable={setIsDraggable}
/>
)
screen.getByRole('button', { name: 'foo.tex' })
expect(screen.queryByRole('textbox')).to.not.exist
})
it("doesn't start renaming on unselected component", function () {
renderWithContext(
<FileTreeItemName
name="foo.tex"
isSelected={false}
setIsDraggable={setIsDraggable}
/>
)
const button = screen.queryByRole('button')
fireEvent.click(button)
fireEvent.click(button)
fireEvent.doubleClick(button)
expect(screen.queryByRole('textbox')).to.not.exist
})
it('start renaming on double-click', function () {
renderWithContext(
<FileTreeItemName
name="foo.tex"
isSelected
setIsDraggable={setIsDraggable}
/>
)
const button = screen.queryByRole('button')
fireEvent.click(button)
fireEvent.click(button)
fireEvent.doubleClick(button)
screen.getByRole('textbox')
expect(screen.queryByRole('button')).to.not.exist
expect(window.requestAnimationFrame).to.be.calledOnce
expect(setIsDraggable).to.be.calledWith(false)
})
it('cannot start renaming in read-only', function () {
renderWithContext(
<FileTreeItemName
name="foo.tex"
isSelected
setIsDraggable={setIsDraggable}
/>,
{
contextProps: { permissionsLevel: 'readOnly' },
}
)
const button = screen.queryByRole('button')
fireEvent.click(button)
fireEvent.click(button)
fireEvent.doubleClick(button)
expect(screen.queryByRole('textbox')).to.not.exist
})
describe('stop renaming', function () {
beforeEach(function () {
renderWithContext(
<FileTreeItemName
name="foo.tex"
isSelected
setIsDraggable={setIsDraggable}
/>
)
const button = screen.getByRole('button')
fireEvent.click(button)
fireEvent.click(button)
fireEvent.doubleClick(button)
const input = screen.getByRole('textbox')
fireEvent.change(input, { target: { value: 'bar.tex' } })
})
it('on Escape', function () {
const input = screen.getByRole('textbox')
fireEvent.keyDown(input, { key: 'Escape' })
screen.getByRole('button', { name: 'foo.tex' })
expect(setIsDraggable).to.be.calledWith(true)
})
})
})

View file

@ -0,0 +1,394 @@
// @ts-ignore
import MockedSocket from 'socket.io-mock'
import FileTreeRoot from '../../../../../frontend/js/features/file-tree/components/file-tree-root'
import { EditorProviders } from '../../../helpers/editor-providers'
describe('<FileTreeRoot/>', function () {
beforeEach(function () {
cy.window().then(win => {
win.metaAttributesCache.set('ol-user', { id: 'user1' })
})
})
it('renders', function () {
const rootFolder = [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '456def', name: 'main.tex' }],
folders: [],
fileRefs: [],
},
]
cy.mount(
<EditorProviders
rootFolder={rootFolder as any}
projectId="123abc"
rootDocId="456def"
features={{} as any}
permissionsLevel="owner"
>
<FileTreeRoot
refProviders={{}}
reindexReferences={cy.stub()}
setRefProviderEnabled={cy.stub()}
setStartedFreeTrial={cy.stub()}
onSelect={cy.stub()}
onInit={cy.stub()}
isConnected
/>
</EditorProviders>
)
cy.findByRole('tree')
cy.findByRole('treeitem')
cy.findByRole('treeitem', { name: 'main.tex', selected: true })
cy.get('.disconnected-overlay').should('not.exist')
})
it('renders with invalid selected doc in local storage', function () {
global.localStorage.setItem(
'doc.open_id.123abc',
JSON.stringify('not-a-valid-id')
)
const rootFolder = [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '456def', name: 'main.tex' }],
folders: [],
fileRefs: [],
},
]
cy.mount(
<EditorProviders
rootFolder={rootFolder as any}
projectId="123abc"
rootDocId="456def"
features={{} as any}
permissionsLevel="owner"
>
<FileTreeRoot
refProviders={{}}
reindexReferences={cy.stub()}
setRefProviderEnabled={cy.stub()}
setStartedFreeTrial={cy.stub()}
onSelect={cy.stub()}
onInit={cy.stub()}
isConnected
/>
</EditorProviders>
)
// as a proxy to check that the invalid entity has not been select we start
// a delete and ensure the modal is displayed (the cancel button can be
// selected) This is needed to make sure the test fail.
cy.findByRole('treeitem', { name: 'main.tex' }).click({
ctrlKey: true,
cmdKey: true,
})
cy.findByRole('button', { name: 'Menu' }).click()
cy.findByRole('menuitem', { name: 'Delete' }).click()
cy.findByRole('button', { name: 'Cancel' })
})
it('renders disconnected overlay', function () {
const rootFolder = [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '456def', name: 'main.tex' }],
folders: [],
fileRefs: [],
},
]
cy.mount(
<EditorProviders
rootFolder={rootFolder as any}
projectId="123abc"
rootDocId="456def"
features={{} as any}
permissionsLevel="owner"
>
<FileTreeRoot
refProviders={{}}
reindexReferences={cy.stub()}
setRefProviderEnabled={cy.stub()}
setStartedFreeTrial={cy.stub()}
onSelect={cy.stub()}
onInit={cy.stub()}
isConnected={false}
/>
</EditorProviders>
)
cy.get('.disconnected-overlay')
})
it('fire onSelect', function () {
const rootFolder = [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [
{ _id: '456def', name: 'main.tex' },
{ _id: '789ghi', name: 'other.tex' },
],
folders: [],
fileRefs: [],
},
]
cy.mount(
<EditorProviders
rootFolder={rootFolder as any}
projectId="123abc"
rootDocId="456def"
features={{} as any}
permissionsLevel="readOnly"
>
<FileTreeRoot
refProviders={{}}
reindexReferences={cy.stub()}
setRefProviderEnabled={cy.stub()}
setStartedFreeTrial={cy.stub()}
onSelect={cy.stub().as('onSelect')}
onInit={cy.stub()}
isConnected
/>
</EditorProviders>
)
cy.get('@onSelect').should('have.been.calledOnceWith', [
Cypress.sinon.match({
entity: Cypress.sinon.match({ _id: '456def', name: 'main.tex' }),
}),
])
cy.findByRole('tree')
cy.findByRole('treeitem', { name: 'other.tex' }).click()
cy.get('@onSelect').should('have.been.calledWith', [
Cypress.sinon.match({
entity: Cypress.sinon.match({ _id: '789ghi', name: 'other.tex' }),
}),
])
})
it('listen to editor.openDoc', function () {
const rootFolder = [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [
{ _id: '456def', name: 'main.tex' },
{ _id: '789ghi', name: 'other.tex' },
],
folders: [],
fileRefs: [],
},
]
cy.mount(
<EditorProviders
rootFolder={rootFolder as any}
projectId="123abc"
rootDocId="456def"
features={{} as any}
permissionsLevel="readOnly"
>
<FileTreeRoot
refProviders={{}}
reindexReferences={cy.stub()}
setRefProviderEnabled={cy.stub()}
setStartedFreeTrial={cy.stub()}
onSelect={cy.stub()}
onInit={cy.stub()}
isConnected
/>
</EditorProviders>
)
cy.findByRole('treeitem', { name: 'main.tex', selected: true })
// entities not found should be ignored
cy.document().trigger('editor.openDoc', { detail: 'not-an-id' })
cy.findByRole('treeitem', { name: 'main.tex', selected: true })
cy.document().trigger('editor.openDoc', { detail: '789ghi' })
cy.findByRole('treeitem', { name: 'main.tex', selected: false })
cy.findByRole('treeitem', { name: 'other.tex', selected: true })
})
it('only shows a menu button when a single item is selected', function () {
const rootFolder = [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [
{ _id: '456def', name: 'main.tex' },
{ _id: '789ghi', name: 'other.tex' },
],
folders: [],
fileRefs: [],
},
]
cy.mount(
<EditorProviders
rootFolder={rootFolder as any}
projectId="123abc"
rootDocId="456def"
features={{} as any}
permissionsLevel="owner"
>
<FileTreeRoot
refProviders={{}}
reindexReferences={cy.stub()}
setRefProviderEnabled={cy.stub()}
setStartedFreeTrial={cy.stub()}
onSelect={cy.stub()}
onInit={cy.stub()}
isConnected
/>
</EditorProviders>
)
cy.findByRole('treeitem', { name: 'main.tex', selected: true })
cy.findByRole('treeitem', { name: 'other.tex', selected: false })
// single item selected: menu button is visible
cy.findAllByRole('button', { name: 'Menu' }).should('have.length', 1)
// select the other item
cy.findByRole('treeitem', { name: 'other.tex' }).click()
cy.findByRole('treeitem', { name: 'main.tex', selected: false })
cy.findByRole('treeitem', { name: 'other.tex', selected: true })
// single item selected: menu button is visible
cy.findAllByRole('button', { name: 'Menu' }).should('have.length', 1)
// multi-select the main item
cy.findByRole('treeitem', { name: 'main.tex' }).click({
ctrlKey: true,
cmdKey: true,
})
cy.findByRole('treeitem', { name: 'main.tex', selected: true })
cy.findByRole('treeitem', { name: 'other.tex', selected: true })
// multiple items selected: no menu button is visible
cy.findAllByRole('button', { name: 'Menu' }).should('have.length', 0)
})
describe('when deselecting files', function () {
beforeEach(function () {
const rootFolder = [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '123abc', name: 'main.tex' }],
folders: [
{
_id: '789ghi',
name: 'thefolder',
docs: [{ _id: '456def', name: 'sub.tex' }],
fileRefs: [],
folders: [],
},
],
fileRefs: [],
},
]
cy.mount(
<EditorProviders
rootFolder={rootFolder as any}
projectId="123abc"
rootDocId="456def"
features={{} as any}
permissionsLevel="owner"
socket={new MockedSocket()}
>
<FileTreeRoot
refProviders={{}}
reindexReferences={cy.stub()}
setRefProviderEnabled={cy.stub()}
setStartedFreeTrial={cy.stub()}
onSelect={cy.stub()}
onInit={cy.stub()}
isConnected
/>
</EditorProviders>
)
// select the sub file
cy.findByRole('treeitem', { name: 'sub.tex' }).click()
cy.findByRole('treeitem', { name: 'sub.tex' }).should(
'have.attr',
'aria-selected',
'true'
)
// click on empty area (after giving it extra height below the tree)
cy.findByTestId('file-tree-inner')
.invoke('attr', 'style', 'height: 400px')
.click()
})
it('removes the selected indicator', function () {
cy.findByRole('treeitem', { selected: true }).should('not.exist')
})
it('disables the "rename" and "delete" buttons', function () {
cy.findByRole('button', { name: 'Rename' }).should('not.exist')
cy.findByRole('button', { name: 'Delete' }).should('not.exist')
})
it('creates new file in the root folder', function () {
cy.intercept('project/*/doc', { statusCode: 200 })
cy.findByRole('button', { name: /new file/i }).click()
cy.findByRole('button', { name: /create/i }).click()
cy.window().then(win => {
// @ts-ignore
win._ide.socket.socketClient.emit('reciveNewDoc', 'root-folder-id', {
_id: '12345',
name: 'abcdef.tex',
docs: [],
fileRefs: [],
folders: [],
})
})
cy.findByRole('treeitem', { name: 'abcdef.tex' }).then($itemEl => {
cy.findByTestId('file-tree-list-root').then($rootEl => {
expect($itemEl.get(0).parentNode).to.equal($rootEl.get(0))
})
})
})
it('starts a new selection', function () {
cy.findByRole('treeitem', { name: 'sub.tex' }).should(
'have.attr',
'aria-selected',
'false'
)
cy.findByRole('treeitem', { name: 'main.tex' }).click({
ctrlKey: true,
cmdKey: true,
})
cy.findByRole('treeitem', { name: 'main.tex' }).should(
'have.attr',
'aria-selected',
'true'
)
})
})
})

View file

@ -1,409 +0,0 @@
import { expect } from 'chai'
import sinon from 'sinon'
import { screen, fireEvent, waitFor } from '@testing-library/react'
import fetchMock from 'fetch-mock'
import MockedSocket from 'socket.io-mock'
import {
renderWithEditorContext,
cleanUpContext,
} from '../../../helpers/render-with-context'
import FileTreeRoot from '../../../../../frontend/js/features/file-tree/components/file-tree-root'
describe('<FileTreeRoot/>', function () {
const onSelect = sinon.stub()
const onInit = sinon.stub()
beforeEach(function () {
window.metaAttributesCache = new Map()
window.metaAttributesCache.set('ol-user', { id: 'user1' })
})
afterEach(function () {
fetchMock.restore()
onSelect.reset()
onInit.reset()
cleanUpContext()
global.localStorage.clear()
window.metaAttributesCache = new Map()
})
it('renders', function () {
const rootFolder = [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '456def', name: 'main.tex' }],
folders: [],
fileRefs: [],
},
]
const { container } = renderWithEditorContext(
<FileTreeRoot
refProviders={{}}
reindexReferences={() => null}
setRefProviderEnabled={() => null}
setStartedFreeTrial={() => null}
onSelect={onSelect}
onInit={onInit}
isConnected
/>,
{
rootFolder,
projectId: '123abc',
rootDocId: '456def',
features: {},
permissionsLevel: 'owner',
}
)
screen.queryByRole('tree')
screen.getByRole('treeitem')
screen.getByRole('treeitem', { name: 'main.tex', selected: true })
expect(container.querySelector('.disconnected-overlay')).to.not.exist
})
it('renders with invalid selected doc in local storage', async function () {
global.localStorage.setItem(
'doc.open_id.123abc',
JSON.stringify('not-a-valid-id')
)
const rootFolder = [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '456def', name: 'main.tex' }],
folders: [],
fileRefs: [],
},
]
renderWithEditorContext(
<FileTreeRoot
refProviders={{}}
reindexReferences={() => null}
setRefProviderEnabled={() => null}
setStartedFreeTrial={() => null}
onSelect={onSelect}
onInit={onInit}
isConnected
/>,
{
rootFolder,
projectId: '123abc',
rootDocId: '456def',
features: {},
permissionsLevel: 'owner',
}
)
// as a proxy to check that the invalid entity ha not been select we start
// a delete and ensure the modal is displayed (the cancel button can be
// selected) This is needed to make sure the test fail.
const treeitemFile = screen.getByRole('treeitem', { name: 'main.tex' })
fireEvent.click(treeitemFile, { ctrlKey: true })
const toggleButton = screen.getByRole('button', { name: 'Menu' })
fireEvent.click(toggleButton)
const deleteButton = screen.getByRole('menuitem', { name: 'Delete' })
fireEvent.click(deleteButton)
await waitFor(() => screen.getByRole('button', { name: 'Cancel' }))
})
it('renders disconnected overlay', function () {
const rootFolder = [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '456def', name: 'main.tex' }],
folders: [],
fileRefs: [],
},
]
const { container } = renderWithEditorContext(
<FileTreeRoot
onSelect={onSelect}
onInit={onInit}
isConnected={false}
refProviders={{}}
reindexReferences={() => null}
setRefProviderEnabled={() => null}
setStartedFreeTrial={() => null}
/>,
{
rootFolder,
projectId: '123abc',
rootDocId: '456def',
features: {},
permissionsLevel: 'owner',
}
)
expect(container.querySelector('.disconnected-overlay')).to.exist
})
it('fire onSelect', function () {
const rootFolder = [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [
{ _id: '456def', name: 'main.tex' },
{ _id: '789ghi', name: 'other.tex' },
],
folders: [],
fileRefs: [],
},
]
renderWithEditorContext(
<FileTreeRoot
refProviders={{}}
reindexReferences={() => null}
setRefProviderEnabled={() => null}
setStartedFreeTrial={() => null}
onSelect={onSelect}
onInit={onInit}
isConnected
/>,
{
rootFolder,
projectId: '123abc',
rootDocId: '456def',
features: {},
permissionsLevel: 'readOnly',
}
)
sinon.assert.calledOnce(onSelect)
sinon.assert.calledWithMatch(onSelect, [
sinon.match({
entity: {
_id: '456def',
name: 'main.tex',
},
}),
])
onSelect.reset()
screen.queryByRole('tree')
const treeitem = screen.getByRole('treeitem', { name: 'other.tex' })
fireEvent.click(treeitem)
sinon.assert.calledOnce(onSelect)
sinon.assert.calledWithMatch(onSelect, [
sinon.match({
entity: {
_id: '789ghi',
name: 'other.tex',
},
}),
])
})
it('listen to editor.openDoc', function () {
const rootFolder = [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [
{ _id: '456def', name: 'main.tex' },
{ _id: '789ghi', name: 'other.tex' },
],
folders: [],
fileRefs: [],
},
]
renderWithEditorContext(
<FileTreeRoot
refProviders={{}}
reindexReferences={() => null}
setRefProviderEnabled={() => null}
setStartedFreeTrial={() => null}
onSelect={onSelect}
onInit={onInit}
isConnected
/>,
{
rootFolder,
projectId: '123abc',
rootDocId: '456def',
features: {},
permissionsLevel: 'owner',
}
)
screen.getByRole('treeitem', { name: 'main.tex', selected: true })
// entities not found should be ignored
window.dispatchEvent(
new CustomEvent('editor.openDoc', { detail: 'not-an-id' })
)
screen.getByRole('treeitem', { name: 'main.tex', selected: true })
window.dispatchEvent(
new CustomEvent('editor.openDoc', { detail: '789ghi' })
)
screen.getByRole('treeitem', { name: 'main.tex', selected: false })
screen.getByRole('treeitem', { name: 'other.tex', selected: true })
})
it('only shows a menu button when a single item is selected', function () {
const rootFolder = [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [
{ _id: '456def', name: 'main.tex' },
{ _id: '789ghi', name: 'other.tex' },
],
folders: [],
fileRefs: [],
},
]
renderWithEditorContext(
<FileTreeRoot
refProviders={{}}
reindexReferences={() => null}
setRefProviderEnabled={() => null}
setStartedFreeTrial={() => null}
onSelect={onSelect}
onInit={onInit}
isConnected
/>,
{
rootFolder,
projectId: '123abc',
rootDocId: '456def',
features: {},
permissionsLevel: 'owner',
}
)
const main = screen.getByRole('treeitem', {
name: 'main.tex',
selected: true,
})
const other = screen.getByRole('treeitem', {
name: 'other.tex',
selected: false,
})
// single item selected: menu button is visible
expect(screen.queryAllByRole('button', { name: 'Menu' })).to.have.length(1)
// select the other item
fireEvent.click(other)
screen.getByRole('treeitem', { name: 'main.tex', selected: false })
screen.getByRole('treeitem', { name: 'other.tex', selected: true })
// single item selected: menu button is visible
expect(screen.queryAllByRole('button', { name: 'Menu' })).to.have.length(1)
// multi-select the main item
fireEvent.click(main, { ctrlKey: true })
screen.getByRole('treeitem', { name: 'main.tex', selected: true })
screen.getByRole('treeitem', { name: 'other.tex', selected: true })
// multiple items selected: no menu button is visible
expect(screen.queryAllByRole('button', { name: 'Menu' })).to.have.length(0)
})
describe('when deselecting files', function () {
beforeEach(function () {
const rootFolder = [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '123abc', name: 'main.tex' }],
folders: [
{
_id: '789ghi',
name: 'thefolder',
docs: [{ _id: '456def', name: 'sub.tex' }],
fileRefs: [],
folders: [],
},
],
fileRefs: [],
},
]
renderWithEditorContext(
<FileTreeRoot
refProviders={{}}
reindexReferences={() => null}
setRefProviderEnabled={() => null}
setStartedFreeTrial={() => null}
onSelect={onSelect}
onInit={onInit}
isConnected
/>,
{
rootFolder,
projectId: '123abc',
rootDocId: '456def',
features: {},
permissionsLevel: 'owner',
socket: new MockedSocket(),
}
)
// select the sub file
const subDoc = screen.getByRole('treeitem', { name: 'sub.tex' })
fireEvent.click(subDoc)
expect(subDoc.getAttribute('aria-selected')).to.equal('true')
// click on empty area
fireEvent.click(screen.getByTestId('file-tree-inner'))
})
afterEach(function () {
fetchMock.reset()
})
it('removes the selected indicator', function () {
expect(screen.queryByRole('treeitem', { selected: true })).to.be.null
})
it('disables the "rename" and "delete" buttons', function () {
expect(screen.queryByRole('button', { name: 'Rename' })).to.be.null
expect(screen.queryByRole('button', { name: 'Delete' })).to.be.null
})
it('creates new file in the root folder', async function () {
fetchMock.post('express:/project/:projectId/doc', () => 200)
fireEvent.click(screen.getByRole('button', { name: /new file/i }))
fireEvent.click(screen.getByRole('button', { name: /create/i }))
const socketData = {
_id: '12345',
name: 'abcdef.tex',
docs: [],
fileRefs: [],
folders: [],
}
window._ide.socket.socketClient.emit(
'reciveNewDoc',
'root-folder-id',
socketData
)
await fetchMock.flush(true)
const newItem = screen.getByRole('treeitem', { name: socketData.name })
const rootEl = screen.getByTestId('file-tree-list-root')
expect(newItem.parentNode).to.equal(rootEl)
})
it('starts a new selection', function () {
const subDoc = screen.getByRole('treeitem', { name: 'sub.tex' })
expect(subDoc.getAttribute('aria-selected')).to.equal('false')
const mainDoc = screen.getByRole('treeitem', { name: 'main.tex' })
fireEvent.click(mainDoc, { ctrlKey: true })
expect(mainDoc.getAttribute('aria-selected')).to.equal('true')
expect(subDoc.getAttribute('aria-selected')).to.equal('false')
})
})
})

View file

@ -0,0 +1,59 @@
import FileTreeToolbar from '../../../../../frontend/js/features/file-tree/components/file-tree-toolbar'
import { EditorProviders } from '../../../helpers/editor-providers'
import { FileTreeProvider } from '../helpers/file-tree-provider'
describe('<FileTreeToolbar/>', function () {
it('without selected files', function () {
cy.mount(
<EditorProviders>
<FileTreeProvider>
<FileTreeToolbar />
</FileTreeProvider>
</EditorProviders>
)
cy.findAllByRole('button', { name: 'New File' })
cy.findAllByRole('button', { name: 'New Folder' })
cy.findAllByRole('button', { name: 'Upload' })
cy.findAllByRole('button', { name: 'Rename' }).should('not.exist')
cy.findAllByRole('button', { name: 'Delete' }).should('not.exist')
})
it('read-only', function () {
cy.mount(
<EditorProviders permissionsLevel="readOnly">
<FileTreeProvider>
<FileTreeToolbar />
</FileTreeProvider>
</EditorProviders>
)
cy.findAllByRole('button').should('not.exist')
})
it('with one selected file', function () {
const rootFolder = [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '456def', name: 'main.tex' }],
folders: [],
fileRefs: [],
},
]
cy.mount(
<EditorProviders rootDocId="456def" rootFolder={rootFolder as any}>
<FileTreeProvider>
<FileTreeToolbar />
</FileTreeProvider>
</EditorProviders>
)
cy.findAllByRole('button', { name: 'New File' })
cy.findAllByRole('button', { name: 'New Folder' })
cy.findAllByRole('button', { name: 'Upload' })
cy.findAllByRole('button', { name: 'Rename' })
cy.findAllByRole('button', { name: 'Delete' })
})
})

View file

@ -1,52 +0,0 @@
import { expect } from 'chai'
import { screen } from '@testing-library/react'
import renderWithContext from '../helpers/render-with-context'
import FileTreeToolbar from '../../../../../frontend/js/features/file-tree/components/file-tree-toolbar'
describe('<FileTreeToolbar/>', function () {
beforeEach(function () {
global.localStorage.clear()
})
it('without selected files', function () {
renderWithContext(<FileTreeToolbar />)
screen.getByRole('button', { name: 'New File' })
screen.getByRole('button', { name: 'New Folder' })
screen.getByRole('button', { name: 'Upload' })
expect(screen.queryByRole('button', { name: 'Rename' })).to.not.exist
expect(screen.queryByRole('button', { name: 'Delete' })).to.not.exist
})
it('read-only', function () {
renderWithContext(<FileTreeToolbar />, {
contextProps: { permissionsLevel: 'readOnly' },
})
expect(screen.queryByRole('button')).to.not.exist
})
it('with one selected file', function () {
renderWithContext(<FileTreeToolbar />, {
contextProps: {
rootDocId: '456def',
rootFolder: [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '456def', name: 'main.tex' }],
folders: [],
fileRefs: [],
},
],
},
})
screen.getByRole('button', { name: 'New File' })
screen.getByRole('button', { name: 'New Folder' })
screen.getByRole('button', { name: 'Upload' })
screen.getByRole('button', { name: 'Rename' })
screen.getByRole('button', { name: 'Delete' })
})
})

View file

@ -0,0 +1,117 @@
import FileTreeRoot from '../../../../../frontend/js/features/file-tree/components/file-tree-root'
import { EditorProviders } from '../../../helpers/editor-providers'
describe('FileTree Context Menu Flow', function () {
beforeEach(function () {
cy.window().then(win => {
win.metaAttributesCache.set('ol-user', { id: 'user1' })
})
})
it('opens on contextMenu event', function () {
const rootFolder = [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '456def', name: 'main.tex' }],
folders: [],
fileRefs: [],
},
]
cy.mount(
<EditorProviders
rootFolder={rootFolder as any}
projectId="123abc"
rootDocId="456def"
>
<FileTreeRoot
refProviders={{}}
reindexReferences={cy.stub()}
setRefProviderEnabled={cy.stub()}
setStartedFreeTrial={cy.stub()}
onSelect={cy.stub()}
onInit={cy.stub()}
isConnected
/>
</EditorProviders>
)
cy.findByRole('menu').should('not.exist')
cy.findByRole('button', { name: 'main.tex' }).trigger('contextmenu')
cy.findByRole('menu')
})
it('closes when a new selection is started', function () {
const rootFolder = [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [
{ _id: '456def', name: 'main.tex' },
{ _id: '456def', name: 'foo.tex' },
],
folders: [],
fileRefs: [],
},
]
cy.mount(
<EditorProviders
rootFolder={rootFolder as any}
projectId="123abc"
rootDocId="456def"
>
<FileTreeRoot
refProviders={{}}
reindexReferences={cy.stub()}
setRefProviderEnabled={cy.stub()}
setStartedFreeTrial={cy.stub()}
onSelect={cy.stub()}
onInit={cy.stub()}
isConnected
/>
</EditorProviders>
)
cy.findByRole('menu').should('not.exist')
cy.findByRole('button', { name: 'main.tex' }).trigger('contextmenu')
cy.findByRole('menu')
cy.findAllByRole('button', { name: 'foo.tex' }).click()
cy.findByRole('menu').should('not.exist')
})
it("doesn't open in read only mode", function () {
const rootFolder = [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '456def', name: 'main.tex' }],
folders: [],
fileRefs: [],
},
]
cy.mount(
<EditorProviders
rootFolder={rootFolder as any}
projectId="123abc"
rootDocId="456def"
permissionsLevel="readOnly"
>
<FileTreeRoot
refProviders={{}}
reindexReferences={cy.stub()}
setRefProviderEnabled={cy.stub()}
setStartedFreeTrial={cy.stub()}
onSelect={cy.stub()}
onInit={cy.stub()}
isConnected
/>
</EditorProviders>
)
cy.findAllByRole('button', { name: 'main.tex' }).trigger('contextmenu')
cy.findByRole('menu').should('not.exist')
})
})

View file

@ -1,134 +0,0 @@
import { expect } from 'chai'
import sinon from 'sinon'
import { screen, fireEvent } from '@testing-library/react'
import {
renderWithEditorContext,
cleanUpContext,
} from '../../../helpers/render-with-context'
import FileTreeRoot from '../../../../../frontend/js/features/file-tree/components/file-tree-root'
describe('FileTree Context Menu Flow', function () {
const onSelect = sinon.stub()
const onInit = sinon.stub()
beforeEach(function () {
window.metaAttributesCache = new Map()
window.metaAttributesCache.set('ol-user', { id: 'user1' })
})
afterEach(function () {
onSelect.reset()
onInit.reset()
cleanUpContext()
window.metaAttributesCache = new Map()
})
it('opens on contextMenu event', async function () {
const rootFolder = [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '456def', name: 'main.tex' }],
folders: [],
fileRefs: [],
},
]
renderWithEditorContext(
<FileTreeRoot
refProviders={{}}
reindexReferences={() => null}
setRefProviderEnabled={() => null}
setStartedFreeTrial={() => null}
onSelect={onSelect}
onInit={onInit}
isConnected
/>,
{
rootFolder,
projectId: '123abc',
rootDocId: '456def',
}
)
const treeitem = screen.getByRole('button', { name: 'main.tex' })
expect(screen.queryByRole('menu')).to.be.null
fireEvent.contextMenu(treeitem)
screen.getByRole('menu')
})
it('closes when a new selection is started', async function () {
const rootFolder = [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [
{ _id: '456def', name: 'main.tex' },
{ _id: '456def', name: 'foo.tex' },
],
folders: [],
fileRefs: [],
},
]
renderWithEditorContext(
<FileTreeRoot
refProviders={{}}
reindexReferences={() => null}
setRefProviderEnabled={() => null}
setStartedFreeTrial={() => null}
onSelect={onSelect}
onInit={onInit}
isConnected
/>,
{
rootFolder,
projectId: '123abc',
rootDocId: '456def',
}
)
const treeitem = screen.getByRole('button', { name: 'main.tex' })
expect(screen.queryByRole('menu')).to.be.null
fireEvent.contextMenu(treeitem)
screen.getByRole('menu')
screen.getByRole('button', { name: 'foo.tex' }).click()
expect(screen.queryByRole('menu')).to.be.null
})
it("doesn't open in read only mode", async function () {
const rootFolder = [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '456def', name: 'main.tex' }],
folders: [],
fileRefs: [],
},
]
renderWithEditorContext(
<FileTreeRoot
refProviders={{}}
reindexReferences={() => null}
setRefProviderEnabled={() => null}
setStartedFreeTrial={() => null}
onSelect={onSelect}
onInit={onInit}
isConnected
/>,
{
rootFolder,
projectId: '123abc',
rootDocId: '456def',
permissionsLevel: 'readOnly',
}
)
const treeitem = screen.getByRole('button', { name: 'main.tex' })
fireEvent.contextMenu(treeitem)
expect(screen.queryByRole('menu')).to.not.exist
})
})

View file

@ -0,0 +1,287 @@
// @ts-ignore
import MockedSocket from 'socket.io-mock'
import FileTreeRoot from '../../../../../frontend/js/features/file-tree/components/file-tree-root'
import { EditorProviders } from '../../../helpers/editor-providers'
describe('FileTree Create Folder Flow', function () {
beforeEach(function () {
cy.window().then(win => {
win.metaAttributesCache.set('ol-user', { id: 'user1' })
})
})
it('add to root when no files are selected', function () {
const rootFolder = [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '456def', name: 'main.tex' }],
folders: [],
fileRefs: [],
},
]
cy.mount(
<EditorProviders
rootFolder={rootFolder as any}
projectId="123abc"
socket={new MockedSocket()}
>
<FileTreeRoot
refProviders={{}}
reindexReferences={cy.stub()}
setRefProviderEnabled={cy.stub()}
setStartedFreeTrial={cy.stub()}
onSelect={cy.stub()}
onInit={cy.stub()}
isConnected
/>
</EditorProviders>
)
const name = 'Foo Bar In Root'
cy.intercept('post', '/project/*/folder', {
body: {
folders: [],
fileRefs: [],
docs: [],
_id: fakeId(),
name,
},
}).as('createFolder')
createFolder(name)
cy.get('@createFolder').its('request.body').should('deep.equal', {
parent_folder_id: 'root-folder-id',
name,
})
cy.window().then(win => {
// @ts-ignore
win._ide.socket.socketClient.emit('reciveNewFolder', 'root-folder-id', {
_id: fakeId(),
name,
docs: [],
fileRefs: [],
folders: [],
})
})
cy.findByRole('treeitem', { name })
})
it('add to folder from folder', function () {
const rootFolder = [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [],
folders: [
{
_id: '789ghi',
name: 'thefolder',
docs: [],
fileRefs: [],
folders: [],
},
],
fileRefs: [],
},
]
cy.mount(
<EditorProviders
rootFolder={rootFolder as any}
projectId="123abc"
rootDocId="789ghi"
socket={new MockedSocket()}
>
<FileTreeRoot
refProviders={{}}
reindexReferences={cy.stub()}
setRefProviderEnabled={cy.stub()}
setStartedFreeTrial={cy.stub()}
onSelect={cy.stub()}
onInit={cy.stub()}
isConnected
/>
</EditorProviders>
)
cy.findByRole('button', { name: 'Expand' }).click()
const name = 'Foo Bar In thefolder'
cy.intercept('post', '/project/*/folder', {
body: {
folders: [],
fileRefs: [],
docs: [],
_id: fakeId(),
name,
},
}).as('createFolder')
createFolder(name)
cy.get('@createFolder').its('request.body').should('deep.equal', {
parent_folder_id: '789ghi',
name,
})
cy.window().then(win => {
// @ts-ignore
win._ide.socket.socketClient.emit('reciveNewFolder', '789ghi', {
_id: fakeId(),
name,
docs: [],
fileRefs: [],
folders: [],
})
})
// find the created folder
cy.findByRole('treeitem', { name })
// collapse the parent folder; created folder should not be rendered anymore
cy.findByRole('button', { name: 'Collapse' }).click()
cy.findByRole('treeitem', { name }).should('not.exist')
})
it('add to folder from child', function () {
const rootFolder = [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [],
folders: [
{
_id: '789ghi',
name: 'thefolder',
docs: [],
fileRefs: [{ _id: '456def', name: 'sub.tex' }],
folders: [],
},
],
fileRefs: [],
},
]
cy.mount(
<EditorProviders
rootFolder={rootFolder as any}
projectId="123abc"
rootDocId="456def"
socket={new MockedSocket()}
>
<FileTreeRoot
refProviders={{}}
reindexReferences={cy.stub()}
setRefProviderEnabled={cy.stub()}
setStartedFreeTrial={cy.stub()}
onSelect={cy.stub()}
onInit={cy.stub()}
isConnected
/>
</EditorProviders>
)
const name = 'Foo Bar In thefolder'
cy.intercept('post', '/project/*/folder', {
body: {
folders: [],
fileRefs: [],
docs: [],
_id: fakeId(),
name,
},
}).as('createFolder')
createFolder(name)
cy.get('@createFolder').its('request.body').should('deep.equal', {
parent_folder_id: '789ghi',
name,
})
cy.window().then(win => {
// @ts-ignore
win._ide.socket.socketClient.emit('reciveNewFolder', '789ghi', {
_id: fakeId(),
name,
docs: [],
fileRefs: [],
folders: [],
})
})
// find the created folder
cy.findByRole('treeitem', { name })
// collapse the parent folder; created folder should not be rendered anymore
cy.findByRole('button', { name: 'Collapse' }).click()
cy.findByRole('treeitem', { name }).should('not.exist')
})
it('prevents adding duplicate or invalid names', function () {
const rootFolder = [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '456def', name: 'existingFile' }],
folders: [],
fileRefs: [],
},
]
cy.mount(
<EditorProviders
rootFolder={rootFolder as any}
projectId="123abc"
rootDocId="456def"
socket={new MockedSocket()}
>
<FileTreeRoot
refProviders={{}}
reindexReferences={cy.stub()}
setRefProviderEnabled={cy.stub()}
setStartedFreeTrial={cy.stub()}
onSelect={cy.stub()}
onInit={cy.stub()}
isConnected
/>
</EditorProviders>
)
const name = 'existingFile'
cy.intercept('post', '/project/*/folder', cy.spy().as('createFolder'))
createFolder(name)
cy.get('@createFolder').should('not.have.been.called')
cy.findByRole('alert', {
name: 'A file or folder with this name already exists',
})
cy.findByRole('textbox').type('in/valid ')
cy.findByRole('alert', {
name: 'File name is empty or contains invalid characters',
})
})
function createFolder(name: string) {
cy.findByRole('button', { name: 'New Folder' }).click()
cy.findByRole('textbox').type(name)
cy.findByRole('button', { name: 'Create' }).click()
}
function fakeId() {
return Math.random().toString(16).replace(/0\./, 'random-test-id-')
}
})

View file

@ -1,296 +0,0 @@
import { expect } from 'chai'
import sinon from 'sinon'
import { screen, fireEvent, waitFor } from '@testing-library/react'
import fetchMock from 'fetch-mock'
import MockedSocket from 'socket.io-mock'
import {
renderWithEditorContext,
cleanUpContext,
} from '../../../helpers/render-with-context'
import FileTreeRoot from '../../../../../frontend/js/features/file-tree/components/file-tree-root'
describe('FileTree Create Folder Flow', function () {
const onSelect = sinon.stub()
const onInit = sinon.stub()
beforeEach(function () {
window.metaAttributesCache = new Map()
window.metaAttributesCache.set('ol-user', { id: 'user1' })
})
afterEach(function () {
fetchMock.restore()
onSelect.reset()
onInit.reset()
cleanUpContext()
window.metaAttributesCache = new Map()
})
it('add to root when no files are selected', async function () {
const rootFolder = [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '456def', name: 'main.tex' }],
folders: [],
fileRefs: [],
},
]
renderWithEditorContext(
<FileTreeRoot
refProviders={{}}
reindexReferences={() => null}
setRefProviderEnabled={() => null}
setStartedFreeTrial={() => null}
onSelect={onSelect}
onInit={onInit}
isConnected
/>,
{
socket: new MockedSocket(),
rootFolder,
projectId: '123abc',
}
)
const newFolderName = 'Foo Bar In Root'
const matcher = /\/project\/\w+\/folder/
const response = {
folders: [],
fileRefs: [],
docs: [],
_id: fakeId(),
name: newFolderName,
}
fetchMock.post(matcher, response)
await fireCreateFolder(newFolderName)
const lastCallBody = JSON.parse(fetchMock.lastCall(matcher)[1].body)
expect(lastCallBody.name).to.equal(newFolderName)
expect(lastCallBody.parent_folder_id).to.equal('root-folder-id')
window._ide.socket.socketClient.emit('reciveNewFolder', 'root-folder-id', {
_id: fakeId(),
name: newFolderName,
docs: [],
fileRefs: [],
folders: [],
})
await screen.findByRole('treeitem', { name: newFolderName })
})
it('add to folder from folder', async function () {
const rootFolder = [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [],
folders: [
{
_id: '789ghi',
name: 'thefolder',
docs: [],
fileRefs: [],
folders: [],
},
],
fileRefs: [],
},
]
renderWithEditorContext(
<FileTreeRoot
refProviders={{}}
reindexReferences={() => null}
setRefProviderEnabled={() => null}
setStartedFreeTrial={() => null}
onSelect={onSelect}
onInit={onInit}
isConnected
/>,
{
socket: new MockedSocket(),
rootFolder,
projectId: '123abc',
rootDocId: '789ghi',
}
)
const expandButton = screen.getByRole('button', { name: 'Expand' })
fireEvent.click(expandButton)
const newFolderName = 'Foo Bar In thefolder'
const matcher = /\/project\/\w+\/folder/
const response = {
folders: [],
fileRefs: [],
docs: [],
_id: fakeId(),
name: newFolderName,
}
fetchMock.post(matcher, response)
await fireCreateFolder(newFolderName)
const lastCallBody = JSON.parse(fetchMock.lastCall(matcher)[1].body)
expect(lastCallBody.name).to.equal(newFolderName)
expect(lastCallBody.parent_folder_id).to.equal('789ghi')
window._ide.socket.socketClient.emit('reciveNewFolder', '789ghi', {
_id: fakeId(),
name: newFolderName,
docs: [],
fileRefs: [],
folders: [],
})
// find the created folder
await screen.findByRole('treeitem', { name: newFolderName })
// collapse the parent folder; created folder should not be rendered anymore
fireEvent.click(expandButton)
expect(screen.queryByRole('treeitem', { name: newFolderName })).to.not.exist
})
it('add to folder from child', async function () {
const rootFolder = [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [],
folders: [
{
_id: '789ghi',
name: 'thefolder',
docs: [],
fileRefs: [{ _id: '456def', name: 'sub.tex' }],
folders: [],
},
],
fileRefs: [],
},
]
renderWithEditorContext(
<FileTreeRoot
refProviders={{}}
reindexReferences={() => null}
setRefProviderEnabled={() => null}
setStartedFreeTrial={() => null}
onSelect={onSelect}
onInit={onInit}
isConnected
/>,
{
socket: new MockedSocket(),
rootFolder,
projectId: '123abc',
rootDocId: '456def',
}
)
const newFolderName = 'Foo Bar In thefolder'
const matcher = /\/project\/\w+\/folder/
const response = {
folders: [],
fileRefs: [],
docs: [],
_id: fakeId(),
name: newFolderName,
}
fetchMock.post(matcher, response)
await fireCreateFolder(newFolderName)
const lastCallBody = JSON.parse(fetchMock.lastCall(matcher)[1].body)
expect(lastCallBody.name).to.equal(newFolderName)
expect(lastCallBody.parent_folder_id).to.equal('789ghi')
window._ide.socket.socketClient.emit('reciveNewFolder', '789ghi', {
_id: fakeId(),
name: newFolderName,
docs: [],
fileRefs: [],
folders: [],
})
// find the created folder
await screen.findByRole('treeitem', { name: newFolderName })
// collapse the parent folder; created folder should not be rendered anymore
fireEvent.click(screen.getByRole('button', { name: 'Collapse' }))
expect(screen.queryByRole('treeitem', { name: newFolderName })).to.not.exist
})
it('prevents adding duplicate or invalid names', async function () {
const rootFolder = [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '456def', name: 'existingFile' }],
folders: [],
fileRefs: [],
},
]
renderWithEditorContext(
<FileTreeRoot
refProviders={{}}
reindexReferences={() => null}
setRefProviderEnabled={() => null}
setStartedFreeTrial={() => null}
onSelect={onSelect}
onInit={onInit}
isConnected
/>,
{
socket: new MockedSocket(),
rootFolder,
projectId: '123abc',
rootDocId: '456def',
}
)
let newFolderName = 'existingFile'
await fireCreateFolder(newFolderName)
expect(fetchMock.called()).to.be.false
await screen.findByRole('alert', {
name: 'A file or folder with this name already exists',
hidden: true,
})
newFolderName = 'in/valid '
setFolderName(newFolderName)
await screen.findByRole('alert', {
name: 'File name is empty or contains invalid characters',
hidden: true,
})
})
async function fireCreateFolder(name) {
const createFolderButton = screen.getByRole('button', {
name: 'New Folder',
})
fireEvent.click(createFolderButton)
setFolderName(name)
const modalCreateButton = await getModalCreateButton()
fireEvent.click(modalCreateButton)
}
function setFolderName(name) {
const input = screen.getByRole('textbox')
fireEvent.change(input, { target: { value: name } })
}
function fakeId() {
return Math.random().toString(16).replace(/0\./, 'random-test-id-')
}
async function getModalCreateButton() {
return waitFor(() => screen.getByRole('button', { name: 'Create' }))
}
})

View file

@ -0,0 +1,282 @@
// @ts-ignore
import MockedSocket from 'socket.io-mock'
import FileTreeRoot from '../../../../../frontend/js/features/file-tree/components/file-tree-root'
import { EditorProviders } from '../../../helpers/editor-providers'
describe('FileTree Delete Entity Flow', function () {
beforeEach(function () {
cy.window().then(win => {
win.metaAttributesCache.set('ol-user', { id: 'user1' })
})
})
describe('single entity', function () {
beforeEach(function () {
const rootFolder = [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [
{ _id: '123abc', name: 'foo.tex' },
{ _id: '456def', name: 'main.tex' },
],
folders: [],
fileRefs: [],
},
]
cy.mount(
<EditorProviders
rootFolder={rootFolder as any}
projectId="123abc"
socket={new MockedSocket()}
>
<FileTreeRoot
refProviders={{}}
reindexReferences={cy.stub().as('reindexReferences')}
setRefProviderEnabled={cy.stub()}
setStartedFreeTrial={cy.stub()}
onSelect={cy.stub()}
onInit={cy.stub()}
isConnected
/>
</EditorProviders>
)
cy.findByRole('treeitem', { name: 'main.tex' }).click()
cy.findByRole('button', { name: 'Menu' }).click()
cy.findByRole('menuitem', { name: 'Delete' }).click()
})
it('removes item', function () {
cy.intercept('delete', '/project/*/doc/*', { statusCode: 204 }).as(
'deleteDoc'
)
// check that the confirmation modal is open
cy.findByText(
'Are you sure you want to permanently delete the following files?'
)
cy.findByRole('button', { name: 'Delete' }).click()
cy.wait('@deleteDoc')
cy.window().then(win => {
// @ts-ignore
win._ide.socket.socketClient.emit('removeEntity', '456def')
})
cy.findByRole('treeitem', {
name: 'main.tex',
hidden: true, // treeitem might be hidden behind the modal
}).should('not.exist')
cy.findByRole('treeitem', {
name: 'main.tex',
}).should('not.exist')
// check that the confirmation modal is closed
cy.findByText(
'Are you sure you want to permanently delete the following files?'
).should('not.exist')
cy.get('@deleteDoc.all').should('have.length', 1)
cy.get('@reindexReferences').should('not.have.been.called')
})
it('continues delete on 404s', function () {
cy.intercept('delete', '/project/*/doc/*', { statusCode: 404 }).as(
'deleteDoc'
)
// check that the confirmation modal is open
cy.findByText(
'Are you sure you want to permanently delete the following files?'
)
cy.findByRole('button', { name: 'Delete' }).click()
cy.window().then(win => {
// @ts-ignore
win._ide.socket.socketClient.emit('removeEntity', '456def')
})
cy.findByRole('treeitem', {
name: 'main.tex',
hidden: true, // treeitem might be hidden behind the modal
}).should('not.exist')
cy.findByRole('treeitem', {
name: 'main.tex',
}).should('not.exist')
// check that the confirmation modal is closed
// is not, the 404 probably triggered a bug
cy.findByText(
'Are you sure you want to permanently delete the following files?'
).should('not.exist')
})
it('aborts delete on error', function () {
cy.intercept('delete', '/project/*/doc/*', { statusCode: 500 }).as(
'deleteDoc'
)
cy.findByRole('button', { name: 'Delete' }).click()
// The modal should still be open, but the file should not be deleted
cy.findByRole('treeitem', { name: 'main.tex', hidden: true })
})
})
describe('folders', function () {
beforeEach(function () {
const rootFolder = [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '456def', name: 'main.tex' }],
folders: [
{
_id: '123abc',
name: 'folder',
docs: [],
folders: [],
fileRefs: [{ _id: '789ghi', name: 'my.bib' }],
},
],
fileRefs: [],
},
]
cy.mount(
<EditorProviders
rootFolder={rootFolder as any}
projectId="123abc"
socket={new MockedSocket()}
>
<FileTreeRoot
refProviders={{}}
reindexReferences={cy.stub().as('reindexReferences')}
setRefProviderEnabled={cy.stub()}
setStartedFreeTrial={cy.stub()}
onSelect={cy.stub()}
onInit={cy.stub()}
isConnected
/>
</EditorProviders>
)
cy.findByRole('button', { name: 'Expand' }).click()
cy.findByRole('treeitem', { name: 'main.tex' }).click()
cy.findByRole('treeitem', { name: 'my.bib' }).click({
ctrlKey: true,
cmdKey: true,
})
cy.window().then(win => {
// @ts-ignore
win._ide.socket.socketClient.emit('removeEntity', '123abc')
})
})
it('removes the folder', function () {
cy.findByRole('treeitem', { name: 'folder' }).should('not.exist')
})
it('leaves the main file selected', function () {
cy.findByRole('treeitem', { name: 'main.tex', selected: true })
})
it('unselect the child entity', function () {
// as a proxy to check that the child entity has been unselect we start
// a delete and ensure the modal is displayed (the cancel button can be
// selected) This is needed to make sure the test fail.
cy.findByRole('button', { name: 'Menu' }).click()
cy.findByRole('menuitem', { name: 'Delete' }).click()
cy.findByRole('button', { name: 'Cancel' })
})
})
describe('multiple entities', function () {
beforeEach(function () {
const rootFolder = [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '456def', name: 'main.tex' }],
folders: [],
fileRefs: [{ _id: '789ghi', name: 'my.bib' }],
},
]
cy.mount(
<EditorProviders
rootFolder={rootFolder as any}
projectId="123abc"
socket={new MockedSocket()}
>
<FileTreeRoot
refProviders={{}}
reindexReferences={cy.stub().as('reindexReferences')}
setRefProviderEnabled={cy.stub()}
setStartedFreeTrial={cy.stub()}
onSelect={cy.stub()}
onInit={cy.stub()}
isConnected
/>
</EditorProviders>
)
// select two files
cy.findByRole('treeitem', { name: 'main.tex' }).click()
cy.findByRole('treeitem', { name: 'my.bib' }).click({
ctrlKey: true,
cmdKey: true,
})
// open the context menu
cy.findByRole('button', { name: 'my.bib' }).trigger('contextmenu')
// make sure the menu has opened, with only a "Delete" item (as multiple files are selected)
cy.findByRole('menu')
cy.findAllByRole('menuitem').should('have.length', 1)
// select the Delete menu item
cy.findByRole('menuitem', { name: 'Delete' }).click()
})
it('removes all items and reindexes references after deleting .bib file', function () {
cy.intercept('delete', '/project/123abc/doc/456def', {
statusCode: 204,
}).as('deleteDoc')
cy.intercept('delete', '/project/123abc/file/789ghi', {
statusCode: 204,
}).as('deleteFile')
cy.findByRole('button', { name: 'Delete' }).click()
cy.window().then(win => {
// @ts-ignore
win._ide.socket.socketClient.emit('removeEntity', '456def')
// @ts-ignore
win._ide.socket.socketClient.emit('removeEntity', '789ghi')
})
for (const name of ['main.tex', 'my.bib']) {
for (const hidden of [true, false]) {
cy.findByRole('treeitem', { name, hidden }).should('not.exist')
}
}
// check that the confirmation modal is closed
cy.findByText('Are you sure').should('not.exist')
cy.get('@deleteDoc.all').should('have.length', 1)
cy.get('@deleteFile.all').should('have.length', 1)
cy.get('@reindexReferences').should('have.been.calledOnce')
})
})
})

View file

@ -1,302 +0,0 @@
import { expect } from 'chai'
import sinon from 'sinon'
import { screen, fireEvent, waitFor } from '@testing-library/react'
import fetchMock from 'fetch-mock'
import MockedSocket from 'socket.io-mock'
import {
renderWithEditorContext,
cleanUpContext,
} from '../../../helpers/render-with-context'
import FileTreeRoot from '../../../../../frontend/js/features/file-tree/components/file-tree-root'
describe('FileTree Delete Entity Flow', function () {
const onSelect = sinon.stub()
const onInit = sinon.stub()
const reindexReferences = sinon.stub()
beforeEach(function () {
window.metaAttributesCache = new Map()
window.metaAttributesCache.set('ol-user', { id: 'user1' })
})
afterEach(function () {
fetchMock.restore()
onSelect.reset()
onInit.reset()
reindexReferences.reset()
cleanUpContext()
window.metaAttributesCache = new Map()
})
describe('single entity', function () {
beforeEach(function () {
const rootFolder = [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '456def', name: 'main.tex' }],
folders: [],
fileRefs: [],
},
]
renderWithEditorContext(
<FileTreeRoot
refProviders={{}}
reindexReferences={reindexReferences}
setRefProviderEnabled={() => null}
setStartedFreeTrial={() => null}
onSelect={onSelect}
onInit={onInit}
isConnected
/>,
{
socket: new MockedSocket(),
rootFolder,
projectId: '123abc',
}
)
const treeitem = screen.getByRole('treeitem', { name: 'main.tex' })
fireEvent.click(treeitem)
const toggleButton = screen.getByRole('button', { name: 'Menu' })
fireEvent.click(toggleButton)
const deleteButton = screen.getByRole('menuitem', { name: 'Delete' })
fireEvent.click(deleteButton)
})
it('removes item', async function () {
const fetchMatcher = /\/project\/\w+\/doc\/\w+/
fetchMock.delete(fetchMatcher, 204)
const modalDeleteButton = await getModalDeleteButton()
fireEvent.click(modalDeleteButton)
window._ide.socket.socketClient.emit('removeEntity', '456def')
await waitFor(() => {
expect(
screen.queryByRole('treeitem', {
name: 'main.tex',
hidden: true, // treeitem might be hidden behind the modal
})
).to.not.exist
expect(
screen.queryByRole('treeitem', {
name: 'main.tex',
})
).to.not.exist
// check that the confirmation modal is closed
expect(screen.queryByText(/Are you sure/)).to.not.exist
})
const [lastFetchPath] = fetchMock.lastCall(fetchMatcher)
expect(lastFetchPath).to.equal('/project/123abc/doc/456def')
expect(reindexReferences).not.to.have.been.called
})
it('continues delete on 404s', async function () {
fetchMock.delete(/\/project\/\w+\/doc\/\w+/, 404)
const modalDeleteButton = await getModalDeleteButton()
fireEvent.click(modalDeleteButton)
window._ide.socket.socketClient.emit('removeEntity', '456def')
// check that the confirmation modal is open
screen.getByText(/Are you sure/)
await waitFor(() => {
expect(
screen.queryByRole('treeitem', {
name: 'main.tex',
hidden: true, // treeitem might be hidden behind the modal
})
).to.not.exist
expect(
screen.queryByRole('treeitem', {
name: 'main.tex',
})
).to.not.exist
// check that the confirmation modal is closed
// is not, the 404 probably triggered a bug
expect(screen.queryByText(/Are you sure/)).to.not.exist
})
})
it('aborts delete on error', async function () {
const fetchMatcher = /\/project\/\w+\/doc\/\w+/
fetchMock.delete(fetchMatcher, 500)
const modalDeleteButton = await getModalDeleteButton()
fireEvent.click(modalDeleteButton)
// The modal should still be open, but the file should not be deleted
await screen.findByRole('treeitem', { name: 'main.tex', hidden: true })
})
})
describe('folders', function () {
beforeEach(function () {
const rootFolder = [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '456def', name: 'main.tex' }],
folders: [
{
_id: '123abc',
name: 'folder',
docs: [],
folders: [],
fileRefs: [{ _id: '789ghi', name: 'my.bib' }],
},
],
fileRefs: [],
},
]
renderWithEditorContext(
<FileTreeRoot
refProviders={{}}
reindexReferences={() => null}
setRefProviderEnabled={() => null}
setStartedFreeTrial={() => null}
onSelect={onSelect}
onInit={onInit}
isConnected
/>,
{
socket: new MockedSocket(),
rootFolder,
projectId: '123abc',
}
)
const expandButton = screen.queryByRole('button', { name: 'Expand' })
if (expandButton) fireEvent.click(expandButton)
const treeitemDoc = screen.getByRole('treeitem', { name: 'main.tex' })
fireEvent.click(treeitemDoc)
const treeitemFile = screen.getByRole('treeitem', { name: 'my.bib' })
fireEvent.click(treeitemFile, { ctrlKey: true })
window._ide.socket.socketClient.emit('removeEntity', '123abc')
})
it('removes the folder', function () {
expect(screen.queryByRole('treeitem', { name: 'folder' })).to.not.exist
})
it('leaves the main file selected', function () {
screen.getByRole('treeitem', { name: 'main.tex', selected: true })
})
it('unselect the child entity', async function () {
// as a proxy to check that the child entity has been unselect we start
// a delete and ensure the modal is displayed (the cancel button can be
// selected) This is needed to make sure the test fail.
const toggleButton = screen.getByRole('button', { name: 'Menu' })
fireEvent.click(toggleButton)
const deleteButton = screen.getByRole('menuitem', { name: 'Delete' })
fireEvent.click(deleteButton)
await waitFor(() => screen.getByRole('button', { name: 'Cancel' }))
})
})
describe('multiple entities', function () {
beforeEach(async function () {
const rootFolder = [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '456def', name: 'main.tex' }],
folders: [],
fileRefs: [{ _id: '789ghi', name: 'my.bib' }],
},
]
renderWithEditorContext(
<FileTreeRoot
refProviders={{}}
reindexReferences={reindexReferences}
setRefProviderEnabled={() => null}
setStartedFreeTrial={() => null}
onSelect={onSelect}
onInit={onInit}
isConnected
/>,
{
socket: new MockedSocket(),
rootFolder,
projectId: '123abc',
}
)
// select two files
const treeitemDoc = screen.getByRole('treeitem', { name: 'main.tex' })
fireEvent.click(treeitemDoc)
const treeitemFile = screen.getByRole('treeitem', { name: 'my.bib' })
fireEvent.click(treeitemFile, { ctrlKey: true })
// open the context menu
const treeitemButton = screen.getByRole('button', { name: 'my.bib' })
fireEvent.contextMenu(treeitemButton)
// make sure the menu has opened, with only a "Delete" item (as multiple files are selected)
screen.getByRole('menu')
const menuItems = await screen.findAllByRole('menuitem')
expect(menuItems.length).to.equal(1)
// select the Delete menu item
const deleteButton = screen.getByRole('menuitem', { name: 'Delete' })
fireEvent.click(deleteButton)
})
it('removes all items and reindexes references after deleting .bib file', async function () {
const fetchMatcher = /\/project\/\w+\/(doc|file)\/\w+/
fetchMock.delete(fetchMatcher, 204)
const modalDeleteButton = await getModalDeleteButton()
fireEvent.click(modalDeleteButton)
window._ide.socket.socketClient.emit('removeEntity', '456def')
window._ide.socket.socketClient.emit('removeEntity', '789ghi')
await waitFor(() => {
for (const name of ['main.tex', 'my.bib']) {
expect(
screen.queryByRole('treeitem', {
name,
hidden: true, // treeitem might be hidden behind the modal
})
).to.not.exist
expect(
screen.queryByRole('treeitem', {
name,
})
).to.not.exist
// check that the confirmation modal is closed
expect(screen.queryByText(/Are you sure/)).to.not.exist
}
})
const [firstFetchPath, secondFetchPath] = fetchMock
.calls()
.map(([url]) => url)
expect(firstFetchPath).to.equal('/project/123abc/doc/456def')
expect(secondFetchPath).to.equal('/project/123abc/file/789ghi')
expect(reindexReferences).to.have.been.called
})
})
async function getModalDeleteButton() {
return waitFor(() => screen.getByRole('button', { name: 'Delete' }))
}
})

View file

@ -0,0 +1,167 @@
// @ts-ignore
import MockedSocket from 'socket.io-mock'
import FileTreeRoot from '../../../../../frontend/js/features/file-tree/components/file-tree-root'
import { EditorProviders } from '../../../helpers/editor-providers'
describe('FileTree Rename Entity Flow', function () {
beforeEach(function () {
cy.window().then(win => {
win.metaAttributesCache.set('ol-user', { id: 'user1' })
})
})
beforeEach(function () {
const rootFolder = [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '456def', name: 'a.tex' }],
folders: [
{
_id: '987jkl',
name: 'folder',
docs: [],
fileRefs: [
{ _id: '789ghi', name: 'c.tex' },
{ _id: '981gkp', name: 'e.tex' },
],
folders: [],
},
],
fileRefs: [],
},
]
cy.mount(
<EditorProviders
rootFolder={rootFolder as any}
projectId="123abc"
socket={new MockedSocket()}
>
<FileTreeRoot
refProviders={{}}
reindexReferences={cy.stub()}
setRefProviderEnabled={cy.stub()}
setStartedFreeTrial={cy.stub()}
onSelect={cy.stub().as('onSelect')}
onInit={cy.stub()}
isConnected
/>
</EditorProviders>
)
})
it('renames doc', function () {
cy.intercept('/project/*/doc/*/rename', { statusCode: 204 }).as('renameDoc')
renameItem('a.tex', 'b.tex')
cy.findByRole('treeitem', { name: 'b.tex' })
cy.get('@renameDoc').its('request.body').should('deep.equal', {
name: 'b.tex',
})
})
it('renames folder', function () {
cy.intercept('/project/*/folder/*/rename', { statusCode: 204 }).as(
'renameFolder'
)
renameItem('folder', 'new folder name')
cy.findByRole('treeitem', { name: 'new folder name' })
cy.get('@renameFolder').its('request.body').should('deep.equal', {
name: 'new folder name',
})
})
it('renames file in subfolder', function () {
cy.intercept('/project/*/file/*/rename', { statusCode: 204 }).as(
'renameFile'
)
cy.findByRole('button', { name: 'Expand' }).click()
renameItem('c.tex', 'd.tex')
cy.findByRole('treeitem', { name: 'folder' })
cy.findByRole('treeitem', { name: 'd.tex' })
cy.get('@renameFile').its('request.body').should('deep.equal', {
name: 'd.tex',
})
})
it('reverts rename on error', function () {
cy.intercept('/project/*/doc/*/rename', { statusCode: 500 })
renameItem('a.tex', 'b.tex')
cy.findByRole('treeitem', { name: 'a.tex' })
})
it('shows error modal on invalid filename', function () {
renameItem('a.tex', '///')
cy.findByRole('alert', {
name: 'File name is empty or contains invalid characters',
hidden: true,
})
})
it('shows error modal on duplicate filename', function () {
renameItem('a.tex', 'folder')
cy.findByRole('alert', {
name: 'A file or folder with this name already exists',
hidden: true,
})
})
it('shows error modal on duplicate filename in subfolder', function () {
cy.findByRole('button', { name: 'Expand' }).click()
renameItem('c.tex', 'e.tex')
cy.findByRole('alert', {
name: 'A file or folder with this name already exists',
hidden: true,
})
})
it('shows error modal on blocked filename', function () {
renameItem('a.tex', 'prototype')
cy.findByRole('alert', {
name: 'This file name is blocked.',
hidden: true,
})
})
describe('via socket event', function () {
it('renames doc', function () {
cy.findByRole('treeitem', { name: 'a.tex' })
cy.window().then(win => {
// @ts-ignore
win._ide.socket.socketClient.emit(
'reciveEntityRename',
'456def',
'socket.tex'
)
})
cy.findByRole('treeitem', { name: 'socket.tex' })
})
})
function renameItem(from: string, to: string) {
cy.findByRole('treeitem', { name: from }).click()
cy.findByRole('button', { name: 'Menu' }).click()
cy.findByRole('menuitem', { name: 'Rename' }).click()
cy.findByRole('textbox').clear()
cy.findByRole('textbox').type(to + '{enter}')
}
})

View file

@ -1,208 +0,0 @@
import { expect } from 'chai'
import sinon from 'sinon'
import { screen, fireEvent } from '@testing-library/react'
import fetchMock from 'fetch-mock'
import MockedSocket from 'socket.io-mock'
import {
renderWithEditorContext,
cleanUpContext,
} from '../../../helpers/render-with-context'
import FileTreeRoot from '../../../../../frontend/js/features/file-tree/components/file-tree-root'
describe('FileTree Rename Entity Flow', function () {
const onSelect = sinon.stub()
const onInit = sinon.stub()
beforeEach(function () {
window.metaAttributesCache = new Map()
window.metaAttributesCache.set('ol-user', { id: 'user1' })
})
afterEach(function () {
fetchMock.restore()
onSelect.reset()
onInit.reset()
cleanUpContext()
window.metaAttributesCache = new Map()
})
beforeEach(function () {
const rootFolder = [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '456def', name: 'a.tex' }],
folders: [
{
_id: '987jkl',
name: 'folder',
docs: [],
fileRefs: [
{ _id: '789ghi', name: 'c.tex' },
{ _id: '981gkp', name: 'e.tex' },
],
folders: [],
},
],
fileRefs: [],
},
]
renderWithEditorContext(
<FileTreeRoot
refProviders={{}}
reindexReferences={() => null}
setRefProviderEnabled={() => null}
setStartedFreeTrial={() => null}
onSelect={onSelect}
onInit={onInit}
isConnected
/>,
{
socket: new MockedSocket(),
rootFolder,
projectId: '123abc',
}
)
onSelect.reset()
})
it('renames doc', function () {
const fetchMatcher = /\/project\/\w+\/doc\/\w+\/rename/
fetchMock.post(fetchMatcher, 204)
const input = initItemRename('a.tex')
fireEvent.change(input, { target: { value: 'b.tex' } })
fireEvent.keyDown(input, { key: 'Enter' })
screen.getByRole('treeitem', { name: 'b.tex' })
const lastFetchBody = getLastFetchBody(fetchMatcher)
expect(lastFetchBody.name).to.equal('b.tex')
// onSelect should have been called once only: when the doc was selected for
// rename
sinon.assert.calledOnce(onSelect)
})
it('renames folder', function () {
const fetchMatcher = /\/project\/\w+\/folder\/\w+\/rename/
fetchMock.post(fetchMatcher, 204)
const input = initItemRename('folder')
fireEvent.change(input, { target: { value: 'new folder name' } })
fireEvent.keyDown(input, { key: 'Enter' })
screen.getByRole('treeitem', { name: 'new folder name' })
const lastFetchBody = getLastFetchBody(fetchMatcher)
expect(lastFetchBody.name).to.equal('new folder name')
})
it('renames file in subfolder', function () {
const fetchMatcher = /\/project\/\w+\/file\/\w+\/rename/
fetchMock.post(fetchMatcher, 204)
const expandButton = screen.queryByRole('button', { name: 'Expand' })
if (expandButton) fireEvent.click(expandButton)
const input = initItemRename('c.tex')
fireEvent.change(input, { target: { value: 'd.tex' } })
fireEvent.keyDown(input, { key: 'Enter' })
screen.getByRole('treeitem', { name: 'folder' })
screen.getByRole('treeitem', { name: 'd.tex' })
const lastFetchBody = getLastFetchBody(fetchMatcher)
expect(lastFetchBody.name).to.equal('d.tex')
})
it('reverts rename on error', async function () {
const fetchMatcher = /\/project\/\w+\/doc\/\w+\/rename/
fetchMock.post(fetchMatcher, 500)
const input = initItemRename('a.tex')
fireEvent.change(input, { target: { value: 'b.tex' } })
fireEvent.keyDown(input, { key: 'Enter' })
screen.getByRole('treeitem', { name: 'b.tex' })
})
it('shows error modal on invalid filename', async function () {
const input = initItemRename('a.tex')
fireEvent.change(input, { target: { value: '///' } })
fireEvent.keyDown(input, { key: 'Enter' })
await screen.findByRole('alert', {
name: 'File name is empty or contains invalid characters',
hidden: true,
})
})
it('shows error modal on duplicate filename', async function () {
const input = initItemRename('a.tex')
fireEvent.change(input, { target: { value: 'folder' } })
fireEvent.keyDown(input, { key: 'Enter' })
await screen.findByRole('alert', {
name: 'A file or folder with this name already exists',
hidden: true,
})
})
it('shows error modal on duplicate filename in subfolder', async function () {
const expandButton = screen.queryByRole('button', { name: 'Expand' })
if (expandButton) fireEvent.click(expandButton)
const input = initItemRename('c.tex')
fireEvent.change(input, { target: { value: 'e.tex' } })
fireEvent.keyDown(input, { key: 'Enter' })
await screen.findByRole('alert', {
name: 'A file or folder with this name already exists',
hidden: true,
})
})
it('shows error modal on blocked filename', async function () {
const input = initItemRename('a.tex')
fireEvent.change(input, { target: { value: 'prototype' } })
fireEvent.keyDown(input, { key: 'Enter' })
await screen.findByRole('alert', {
name: 'This file name is blocked.',
hidden: true,
})
})
describe('via socket event', function () {
it('renames doc', function () {
screen.getByRole('treeitem', { name: 'a.tex' })
window._ide.socket.socketClient.emit(
'reciveEntityRename',
'456def',
'socket.tex'
)
screen.getByRole('treeitem', { name: 'socket.tex' })
})
})
function initItemRename(treeitemName) {
const treeitem = screen.getByRole('treeitem', { name: treeitemName })
fireEvent.click(treeitem)
const toggleButton = screen.getByRole('button', { name: 'Menu' })
fireEvent.click(toggleButton)
const renameButton = screen.getByRole('menuitem', { name: 'Rename' })
fireEvent.click(renameButton)
return screen.getByRole('textbox')
}
function getLastFetchBody(matcher) {
const [, { body }] = fetchMock.lastCall(matcher)
return JSON.parse(body)
}
})

View file

@ -0,0 +1,18 @@
import { FC } from 'react'
import FileTreeContext from '@/features/file-tree/components/file-tree-context'
export const FileTreeProvider: FC<{
refProviders?: Record<string, boolean>
}> = ({ children, refProviders = {} }) => {
return (
<FileTreeContext
refProviders={refProviders}
reindexReferences={cy.stub().as('reindexReferences')}
setRefProviderEnabled={cy.stub().as('setRefProviderEnabled')}
setStartedFreeTrial={cy.stub().as('setStartedFreeTrial')}
onSelect={() => {}}
>
<>{children}</>
</FileTreeContext>
)
}

View file

@ -1,52 +0,0 @@
import FileTreeContext from '../../../../../frontend/js/features/file-tree/components/file-tree-context'
import { renderWithEditorContext } from '../../../helpers/render-with-context'
import { debugConsole } from '@/utils/debugging'
export default (children, options = {}) => {
let { contextProps = {}, ...renderOptions } = options
contextProps = {
projectId: '123abc',
rootFolder: [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [],
fileRefs: [],
folders: [],
},
],
refProviders: {},
reindexReferences: () => {
debugConsole.warn('reindex references')
},
setRefProviderEnabled: provider => {
debugConsole.warn(`ref provider ${provider} enabled`)
},
setStartedFreeTrial: () => {
debugConsole.warn('started free trial')
},
onSelect: () => {},
...contextProps,
}
const {
refProviders,
reindexReferences,
setRefProviderEnabled,
setStartedFreeTrial,
onSelect,
...editorContextProps
} = contextProps
return renderWithEditorContext(
<FileTreeContext
refProviders={refProviders}
reindexReferences={reindexReferences}
setRefProviderEnabled={setRefProviderEnabled}
setStartedFreeTrial={setStartedFreeTrial}
onSelect={onSelect}
>
{children}
</FileTreeContext>,
editorContextProps,
renderOptions
)
}

View file

@ -0,0 +1,105 @@
import UploadProjectModal from '../../../../../../frontend/js/features/project-list/components/new-project-button/upload-project-modal'
describe('<UploadProjectModal />', function () {
const maxUploadSize = 10 * 1024 * 1024 // 10 MB
beforeEach(function () {
cy.window().then(win => {
win.metaAttributesCache.set('ol-ExposedSettings', { maxUploadSize })
})
})
it('uploads a dropped file', function () {
cy.intercept('post', '/project/new/upload', {
body: { success: true, project_id: '123abc' },
}).as('uploadProject')
cy.mount(
<UploadProjectModal
onHide={cy.stub()}
openProject={cy.stub().as('openProject')}
/>
)
cy.findByRole('button', {
name: 'Select a .zip file',
}).trigger('drop', {
dataTransfer: {
files: [new File(['test'], 'test.zip', { type: 'application/zip' })],
},
})
cy.wait('@uploadProject')
cy.get('@openProject').should('have.been.calledOnceWith', '123abc')
})
it('shows error on file type other than zip', function () {
cy.mount(
<UploadProjectModal
onHide={cy.stub()}
openProject={cy.stub().as('openProject')}
/>
)
cy.findByRole('button', {
name: 'Select a .zip file',
}).trigger('drop', {
dataTransfer: {
files: [new File(['test'], 'test.png', { type: 'image/png' })],
},
})
cy.findByText('You can only upload: .zip')
cy.get('@openProject').should('not.have.been.called')
})
it('shows error for files bigger than maxUploadSize', function () {
cy.mount(
<UploadProjectModal
onHide={cy.stub()}
openProject={cy.stub().as('openProject')}
/>
)
const file = new File(['test'], 'test.zip', { type: 'application/zip' })
Object.defineProperty(file, 'size', { value: maxUploadSize + 1 })
cy.findByRole('button', {
name: 'Select a .zip file',
}).trigger('drop', {
dataTransfer: {
files: [file],
},
})
cy.findByText('test.zip exceeds maximum allowed size of 10 MB')
cy.get('@openProject').should('not.have.been.called')
})
it('handles server error', function () {
cy.intercept('post', '/project/new/upload', {
statusCode: 422,
body: { success: false },
}).as('uploadProject')
cy.mount(
<UploadProjectModal
onHide={cy.stub()}
openProject={cy.stub().as('openProject')}
/>
)
cy.findByRole('button', {
name: 'Select a .zip file',
}).trigger('drop', {
dataTransfer: {
files: [new File(['test'], 'test.zip', { type: 'application/zip' })],
},
})
cy.wait('@uploadProject')
cy.findByText('Upload failed')
cy.get('@openProject').should('not.have.been.called')
})
})

View file

@ -1,152 +0,0 @@
import sinon from 'sinon'
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import UploadProjectModal from '../../../../../../frontend/js/features/project-list/components/new-project-button/upload-project-modal'
import { expect } from 'chai'
import * as useLocationModule from '../../../../../../frontend/js/shared/hooks/use-location'
describe('<UploadProjectModal />', function () {
const originalWindowCSRFToken = window.csrfToken
const maxUploadSize = 10 * 1024 * 1024 // 10 MB
let assignStub: sinon.SinonStub
beforeEach(function () {
assignStub = sinon.stub()
this.locationStub = sinon.stub(useLocationModule, 'useLocation').returns({
assign: assignStub,
reload: sinon.stub(),
})
window.metaAttributesCache.set('ol-ExposedSettings', {
maxUploadSize,
})
window.csrfToken = 'token'
})
afterEach(function () {
this.locationStub.restore()
window.metaAttributesCache = new Map()
window.csrfToken = originalWindowCSRFToken
})
it('uploads a dropped file', async function () {
const xhr = sinon.useFakeXMLHttpRequest()
const requests: sinon.SinonFakeXMLHttpRequest[] = []
xhr.onCreate = request => {
requests.push(request)
}
render(<UploadProjectModal onHide={() => {}} />)
const uploadButton = screen.getByRole('button', {
name: 'Select a .zip file',
})
expect(uploadButton).not.to.be.null
fireEvent.drop(uploadButton, {
dataTransfer: {
files: [new File(['test'], 'test.zip', { type: 'application/zip' })],
},
})
await waitFor(() => expect(requests).to.have.length(1))
const [request] = requests
expect(request.url).to.equal('/project/new/upload')
expect(request.method).to.equal('POST')
const projectId = '123abc'
request.respond(
200,
{ 'Content-Type': 'application/json' },
JSON.stringify({ success: true, project_id: projectId })
)
await waitFor(() => {
sinon.assert.calledOnce(assignStub)
sinon.assert.calledWith(assignStub, `/project/${projectId}`)
})
xhr.restore()
})
it('shows error on file type other than zip', async function () {
render(<UploadProjectModal onHide={() => {}} />)
const uploadButton = screen.getByRole('button', {
name: 'Select a .zip file',
})
expect(uploadButton).not.to.be.null
fireEvent.drop(uploadButton, {
dataTransfer: {
files: [new File(['test'], 'test.png', { type: 'image/png' })],
},
})
await waitFor(() => screen.getByText('You can only upload: .zip'))
})
it('shows error for files bigger than maxUploadSize', async function () {
render(<UploadProjectModal onHide={() => {}} />)
const uploadButton = screen.getByRole('button', {
name: 'Select a .zip file',
})
expect(uploadButton).not.to.be.null
const filename = 'test.zip'
const file = new File(['test'], filename, { type: 'application/zip' })
Object.defineProperty(file, 'size', { value: maxUploadSize + 1 })
fireEvent.drop(uploadButton, {
dataTransfer: {
files: [file],
},
})
await waitFor(() =>
screen.getByText(`${filename} exceeds maximum allowed size of 10 MB`)
)
})
it('handles server error', async function () {
const xhr = sinon.useFakeXMLHttpRequest()
const requests: sinon.SinonFakeXMLHttpRequest[] = []
xhr.onCreate = request => {
requests.push(request)
}
render(<UploadProjectModal onHide={() => {}} />)
const uploadButton = screen.getByRole('button', {
name: 'Select a .zip file',
})
expect(uploadButton).not.to.be.null
fireEvent.drop(uploadButton, {
dataTransfer: {
files: [new File(['test'], 'test.zip', { type: 'application/zip' })],
},
})
await waitFor(() => expect(requests).to.have.length(1))
const [request] = requests
expect(request.url).to.equal('/project/new/upload')
expect(request.method).to.equal('POST')
request.respond(
422,
{ 'Content-Type': 'application/json' },
JSON.stringify({ success: false })
)
await waitFor(() => {
sinon.assert.notCalled(assignStub)
screen.getByText('Upload failed')
})
xhr.restore()
})
})

View file

@ -83,7 +83,7 @@ describe('<FigureModal />', function () {
cy.findByRole('menu').within(() => {
cy.findByText('Upload from computer').click()
})
cy.findByLabelText('File Uploader')
cy.findByLabelText('Uppy Dashboard')
.get('.uppy-Dashboard-input:first')
.as('file-input')
})