overleaf/services/web/test/frontend/features/file-tree/components/file-tree-create/file-tree-modal-create-file.spec.tsx
Alf Eaton 612c7c28b0 Merge pull request #16511 from overleaf/ae-upgrade-uppy
Upgrade Uppy to v3

GitOrigin-RevId: ca3e366a20ac651a98aafe12bf319b1968ac6ec1
2024-01-19 09:05:05 +00:00

508 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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