mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #16511 from overleaf/ae-upgrade-uppy
Upgrade Uppy to v3 GitOrigin-RevId: ca3e366a20ac651a98aafe12bf319b1968ac6ec1
This commit is contained in:
parent
4bca3de8d2
commit
612c7c28b0
40 changed files with 3250 additions and 3326 deletions
1090
package-lock.json
generated
1090
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -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()
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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':
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -145,3 +145,7 @@
|
|||
.figure-modal .select-wrapper:not(:first-child) {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.figure-modal-upload .uppy-Dashboard-AddFiles-list {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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)
|
||||
})
|
||||
})
|
|
@ -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 doesn’t 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 />
|
||||
}
|
|
@ -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 doesn’t 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,
|
||||
}
|
|
@ -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 })
|
||||
})
|
||||
})
|
|
@ -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 })
|
||||
})
|
||||
})
|
|
@ -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 })
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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 })
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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')
|
||||
})
|
||||
})
|
|
@ -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')
|
||||
})
|
||||
})
|
|
@ -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')
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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')
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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')
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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' })
|
||||
})
|
||||
})
|
|
@ -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' })
|
||||
})
|
||||
})
|
|
@ -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')
|
||||
})
|
||||
})
|
|
@ -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
|
||||
})
|
||||
})
|
|
@ -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-')
|
||||
}
|
||||
})
|
|
@ -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' }))
|
||||
}
|
||||
})
|
|
@ -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')
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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' }))
|
||||
}
|
||||
})
|
|
@ -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}')
|
||||
}
|
||||
})
|
|
@ -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)
|
||||
}
|
||||
})
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
|
@ -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')
|
||||
})
|
||||
})
|
|
@ -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()
|
||||
})
|
||||
})
|
|
@ -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')
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue