@uppy/provider-views
Version:
View library for Uppy remote provider plugins.
637 lines (569 loc) • 20.6 kB
text/typescript
import type {
PartialTree,
PartialTreeFile,
PartialTreeFolderNode,
PartialTreeFolderRoot,
PartialTreeId,
} from '@uppy/core'
import type { CompanionFile } from '@uppy/utils'
import { describe, expect, it, vi } from 'vitest'
import afterFill from './afterFill.js'
import afterOpenFolder from './afterOpenFolder.js'
import afterScrollFolder from './afterScrollFolder.js'
import afterToggleCheckbox from './afterToggleCheckbox.js'
import getBreadcrumbs from './getBreadcrumbs.js'
import getCheckedFilesWithPaths from './getCheckedFilesWithPaths.js'
import getNumberOfSelectedFiles from './getNumberOfSelectedFiles.js'
const _root = (id: string, options: any = {}): PartialTreeFolderRoot => ({
type: 'root',
id,
cached: true,
nextPagePath: null,
...options,
})
const _cFile = (id: string) =>
({
id,
requestPath: id,
name: `name_${id}.jpg`,
isFolder: false,
}) as CompanionFile
const _cFolder = (id: string) =>
({
id,
requestPath: id,
name: `name_${id}`,
isFolder: true,
}) as CompanionFile
const _folder = (id: string, options: any): PartialTreeFolderNode => ({
type: 'folder',
id,
cached: true,
nextPagePath: null,
status: 'unchecked',
data: _cFolder(id),
...options,
})
const _file = (id: string, options: any): PartialTreeFile => ({
type: 'file',
id,
status: 'unchecked',
parentId: options.parentId,
data: _cFile(id),
...options,
})
const getFolder = (tree: PartialTree, id: string) =>
tree.find((i) => i.id === id) as PartialTreeFolderNode
const getFile = (tree: PartialTree, id: string) =>
tree.find((i) => i.id === id) as PartialTreeFile
describe('afterFill()', () => {
it('preserves .checked files in an already .cached folder', async () => {
// prettier-ignore
const tree: PartialTree = [
_root('ourRoot'),
_folder('1', { parentId: 'ourRoot' }),
_folder('2', { parentId: 'ourRoot', cached: true }),
_file('2_1', { parentId: '2' }),
_file('2_2', { parentId: '2', status: 'checked' }),
_file('2_3', { parentId: '2' }),
_folder('2_4', { parentId: '2' }),
_file('3', { parentId: 'ourRoot' }),
_file('4', { parentId: 'ourRoot' }),
]
const mock = vi.fn()
const enrichedTree = await afterFill(
tree,
mock,
() => null,
() => {},
)
// While we're at it - make sure we're not doing excessive api calls!
expect(mock.mock.calls.length).toEqual(0)
const checkedFiles = enrichedTree.filter(
(item) => item.type === 'file' && item.status === 'checked',
)
expect(checkedFiles.length).toEqual(1)
expect(checkedFiles[0].id).toEqual('2_2')
})
it('fetches a .checked folder', async () => {
// prettier-ignore
const tree: PartialTree = [
_root('ourRoot'),
_folder('1', { parentId: 'ourRoot' }),
_folder('2', { parentId: 'ourRoot', cached: false, status: 'checked' }),
]
const mock = (path: PartialTreeId) => {
if (path === '2') {
const items = [_cFile('2_1'), _cFile('2_2')]
return Promise.resolve({ nextPagePath: '666', items })
}
if (path === '666') {
const items = [_cFile('2_3'), _cFile('2_4')]
return Promise.resolve({ nextPagePath: null, items })
}
return Promise.reject()
}
const enrichedTree = await afterFill(
tree,
mock,
() => null,
() => {},
)
const checkedFiles = enrichedTree.filter(
(item) => item.type === 'file' && item.status === 'checked',
)
expect(checkedFiles.length).toEqual(4)
expect(checkedFiles.map((f) => f.id)).toEqual(['2_1', '2_2', '2_3', '2_4'])
})
it('fetches remaining pages in a folder', async () => {
// prettier-ignore
const tree: PartialTree = [
_root('ourRoot'),
_folder('1', { parentId: 'ourRoot' }),
_folder('2', {
parentId: 'ourRoot',
cached: true,
nextPagePath: '666',
status: 'checked',
}),
]
const mock = (path: PartialTreeId) => {
if (path === '666') {
const items = [_cFile('111'), _cFile('222')]
return Promise.resolve({ nextPagePath: null, items })
}
return Promise.reject()
}
const enrichedTree = await afterFill(
tree,
mock,
() => null,
() => {},
)
const checkedFiles = enrichedTree.filter(
(item) => item.type === 'file' && item.status === 'checked',
)
expect(checkedFiles.length).toEqual(2)
expect(checkedFiles.map((f) => f.id)).toEqual(['111', '222'])
})
it('fetches a folder two levels deep', async () => {
// prettier-ignore
const tree: PartialTree = [
_root('ourRoot'),
_folder('1', { parentId: 'ourRoot' }),
_folder('2', {
parentId: 'ourRoot',
cached: true,
nextPagePath: '2_next',
status: 'checked',
}),
_file('2_1', { parentId: '2', status: 'checked' }),
_file('2_2', { parentId: '2', status: 'checked' }),
]
const mock = (path: PartialTreeId) => {
if (path === '2_next') {
const items = [_cFile('2_3'), _cFolder('666')]
return Promise.resolve({ nextPagePath: null, items })
}
if (path === '666') {
const items = [_cFile('666_1'), _cFile('666_2')]
return Promise.resolve({ nextPagePath: null, items })
}
return Promise.reject()
}
const enrichedTree = await afterFill(
tree,
mock,
() => null,
() => {},
)
const checkedFiles = enrichedTree.filter(
(item) => item.type === 'file' && item.status === 'checked',
)
expect(checkedFiles.length).toEqual(5)
expect(checkedFiles.map((f) => f.id)).toEqual([
'2_1',
'2_2',
'2_3',
'666_1',
'666_2',
])
})
it('complex situation', async () => {
// prettier-ignore
const tree: PartialTree = [
_root('ourRoot'),
_folder('1', { parentId: 'ourRoot' }),
// folder we'll be recursively fetching really deeply
_folder('2', {
parentId: 'ourRoot',
cached: true,
nextPagePath: '2_next',
status: 'checked',
}),
_file('2_1', { parentId: '2', status: 'checked' }),
_file('2_2', { parentId: '2', status: 'checked' }),
// folder with only some files checked
_folder('3', { parentId: 'ourRoot', cached: true, status: 'partial' }),
// empty folder
_folder('0', { parentId: '3', cached: false, status: 'checked' }),
_file('3_1', { parentId: '3', status: 'checked' }),
_file('3_2', { parentId: '3', status: 'unchecked' }),
]
const mock = (path: PartialTreeId) => {
if (path === '2_next') {
const items = [_cFile('2_3'), _cFolder('666')]
return Promise.resolve({ nextPagePath: null, items })
}
if (path === '666') {
const items = [_cFile('666_1'), _cFolder('777')]
return Promise.resolve({ nextPagePath: null, items })
}
if (path === '777') {
const items = [_cFile('777_1'), _cFolder('777_2')]
return Promise.resolve({ nextPagePath: null, items })
}
if (path === '777_2') {
const items = [_cFile('777_2_1')]
return Promise.resolve({ nextPagePath: '777_2_next', items })
}
if (path === '777_2_next') {
const items = [_cFile('777_2_1_1')]
return Promise.resolve({ nextPagePath: null, items })
}
if (path === '0') {
return Promise.resolve({ nextPagePath: null, items: [] })
}
return Promise.reject()
}
const enrichedTree = await afterFill(
tree,
mock,
() => null,
() => {},
)
const checkedFiles = enrichedTree.filter(
(item) => item.type === 'file' && item.status === 'checked',
)
expect(checkedFiles.length).toEqual(8)
expect(checkedFiles.map((f) => f.id)).toEqual([
'2_1',
'2_2',
'3_1',
'2_3',
'666_1',
'777_1',
'777_2_1',
'777_2_1_1',
])
})
})
describe('afterOpenFolder()', () => {
it('open "checked" folder - all discovered files are marked as "checked"', () => {
// prettier-ignore
const oldPartialTree: PartialTree = [
_root('ourRoot'),
_folder('1', { parentId: 'ourRoot' }),
_folder('2', { parentId: 'ourRoot', cached: false, status: 'checked' }),
]
const fakeCompanionFiles = [
{ requestPath: '666', isFolder: true },
{ requestPath: '777', isFolder: false },
{ requestPath: '888', isFolder: false },
] as CompanionFile[]
const clickedFolder = oldPartialTree.find(
(f) => f.id === '2',
) as PartialTreeFolderNode
const newTree = afterOpenFolder(
oldPartialTree,
fakeCompanionFiles,
clickedFolder,
null,
() => null,
)
expect(getFolder(newTree, '666').status).toEqual('checked')
expect(getFile(newTree, '777').status).toEqual('checked')
expect(getFile(newTree, '888').status).toEqual('checked')
})
it('open "unchecked" folder - all discovered files are marked as "unchecked"', () => {
// prettier-ignore
const oldPartialTree: PartialTree = [
_root('ourRoot'),
_folder('1', { parentId: 'ourRoot' }),
_folder('2', { parentId: 'ourRoot', cached: false, status: 'unchecked' }),
]
const fakeCompanionFiles = [
{ requestPath: '666', isFolder: true },
{ requestPath: '777', isFolder: false },
{ requestPath: '888', isFolder: false },
] as CompanionFile[]
const clickedFolder = oldPartialTree.find(
(f) => f.id === '2',
) as PartialTreeFolderNode
const newTree = afterOpenFolder(
oldPartialTree,
fakeCompanionFiles,
clickedFolder,
null,
() => null,
)
expect(getFolder(newTree, '666').status).toEqual('unchecked')
expect(getFile(newTree, '777').status).toEqual('unchecked')
expect(getFile(newTree, '888').status).toEqual('unchecked')
})
})
describe('afterScrollFolder()', () => {
it('scroll "checked" folder - all discovered files are marked as "checked"', () => {
// prettier-ignore
const oldPartialTree: PartialTree = [
_root('ourRoot'),
_folder('1', { parentId: 'ourRoot' }),
_folder('2', { parentId: 'ourRoot', cached: true, status: 'checked' }),
_file('2_1', { parentId: '2' }),
_file('2_2', { parentId: '2' }),
_file('2_3', { parentId: '2' }),
]
const fakeCompanionFiles = [
{ requestPath: '666', isFolder: true },
{ requestPath: '777', isFolder: false },
{ requestPath: '888', isFolder: false },
] as CompanionFile[]
const newTree = afterScrollFolder(
oldPartialTree,
'2',
fakeCompanionFiles,
null,
() => null,
)
expect(getFolder(newTree, '666').status).toEqual('checked')
expect(getFile(newTree, '777').status).toEqual('checked')
expect(getFile(newTree, '888').status).toEqual('checked')
})
it('scroll "checked" folder - all discovered files are marked as "unchecked"', () => {
// prettier-ignore
const oldPartialTree: PartialTree = [
_root('ourRoot'),
_folder('1', { parentId: 'ourRoot' }),
_folder('2', { parentId: 'ourRoot', cached: true, status: 'unchecked' }),
_file('2_1', { parentId: '2' }),
_file('2_2', { parentId: '2' }),
_file('2_3', { parentId: '2' }),
]
const fakeCompanionFiles = [
{ requestPath: '666', isFolder: true },
{ requestPath: '777', isFolder: false },
{ requestPath: '888', isFolder: false },
] as CompanionFile[]
const newTree = afterScrollFolder(
oldPartialTree,
'2',
fakeCompanionFiles,
null,
() => null,
)
expect(getFolder(newTree, '666').status).toEqual('unchecked')
expect(getFile(newTree, '777').status).toEqual('unchecked')
expect(getFile(newTree, '888').status).toEqual('unchecked')
})
})
describe('afterToggleCheckbox()', () => {
// prettier-ignore
const oldPartialTree: PartialTree = [
_root('ourRoot'),
_folder('1', { parentId: 'ourRoot' }),
_folder('2', { parentId: 'ourRoot' }),
_file('2_1', { parentId: '2' }),
_file('2_2', { parentId: '2' }),
_file('2_3', { parentId: '2' }),
_folder('2_4', { parentId: '2' }), // click
_file('2_4_1', { parentId: '2_4' }),
_file('2_4_2', { parentId: '2_4' }),
_file('2_4_3', { parentId: '2_4' }),
_file('3', { parentId: 'ourRoot' }),
_file('4', { parentId: 'ourRoot' }),
]
it('check folder: percolates up and down', () => {
const newTree = afterToggleCheckbox(oldPartialTree, ['2_4'])
expect(getFolder(newTree, '2_4').status).toEqual('checked')
// percolates down
expect(getFile(newTree, '2_4_1').status).toEqual('checked')
expect(getFile(newTree, '2_4_2').status).toEqual('checked')
expect(getFile(newTree, '2_4_3').status).toEqual('checked')
// percolates up
expect(getFolder(newTree, '2').status).toEqual('partial')
})
it('uncheck folder: percolates up and down', () => {
const treeAfterClick1 = afterToggleCheckbox(oldPartialTree, ['2_4'])
const tree = afterToggleCheckbox(treeAfterClick1, ['2_4'])
expect(getFolder(tree, '2_4').status).toEqual('unchecked')
// percolates down
expect(getFile(tree, '2_4_1').status).toEqual('unchecked')
expect(getFile(tree, '2_4_2').status).toEqual('unchecked')
expect(getFile(tree, '2_4_3').status).toEqual('unchecked')
// percolates up
expect(getFolder(tree, '2').status).toEqual('unchecked')
})
it('gradually check all subfolders: marks parent folder as checked', () => {
const tree = afterToggleCheckbox(oldPartialTree, [
'2_4_1',
'2_4_2',
'2_4_3',
])
// marks children as checked
expect(getFolder(tree, '2_4_1').status).toEqual('checked')
expect(getFolder(tree, '2_4_2').status).toEqual('checked')
expect(getFolder(tree, '2_4_3').status).toEqual('checked')
// marks parent folder as checked
expect(getFolder(tree, '2_4').status).toEqual('checked')
// marks parent parent folder as partially checked
expect(getFolder(tree, '2').status).toEqual('partial')
// and just randomly making sure unnrelated items didn't get checked
expect(getFile(tree, '3').status).toEqual('unchecked')
expect(getFile(tree, '2_2').status).toEqual('unchecked')
})
it('clicking partial folder: partial => checked => unchecked', () => {
// 1. click on 2_4_1, thus making 2_4 "partial"
const tree_1 = afterToggleCheckbox(oldPartialTree, ['2_4_1'])
expect(getFolder(tree_1, '2_4').status).toEqual('partial')
// and test children while we're at it
expect(getFolder(tree_1, '2_4_1').status).toEqual('checked')
// 2. click on 2_4, thus making 2_4 "checked"
const tree_2 = afterToggleCheckbox(tree_1, ['2_4'])
expect(getFolder(tree_2, '2_4').status).toEqual('checked')
// and test children while we're at it
expect(getFolder(tree_2, '2_4_1').status).toEqual('checked')
expect(getFolder(tree_2, '2_4_2').status).toEqual('checked')
expect(getFolder(tree_2, '2_4_3').status).toEqual('checked')
// 3. click on 2_4, thus making 2_4 "unchecked"
const tree_3 = afterToggleCheckbox(tree_2, ['2_4'])
expect(getFolder(tree_3, '2_4').status).toEqual('unchecked')
// and test children while we're at it
expect(getFolder(tree_3, '2_4_1').status).toEqual('unchecked')
expect(getFolder(tree_3, '2_4_2').status).toEqual('unchecked')
expect(getFolder(tree_3, '2_4_3').status).toEqual('unchecked')
})
it('old partialTree is NOT mutated', () => {
const oldPartialTreeCopy = JSON.parse(JSON.stringify(oldPartialTree))
afterToggleCheckbox(oldPartialTree, ['2_4_1'])
expect(oldPartialTree).toEqual(oldPartialTreeCopy)
})
})
describe('getNumberOfSelectedFiles()', () => {
it('gets all leaf items', () => {
// prettier-ignore
const tree: PartialTree = [
_root('ourRoot'),
// leaf .checked folder
_folder('1', { parentId: 'ourRoot', cached: false, status: 'checked' }),
// NON-left .checked folder
_folder('2', { parentId: 'ourRoot', status: 'checked' }),
// leaf .checked file
_file('2_1', { parentId: '2', status: 'checked' }),
// leaf .checked file
_file('2_2', { parentId: '2', status: 'checked' }),
]
const result = getNumberOfSelectedFiles(tree)
expect(result).toEqual(3)
})
it('empty folder, even after being opened, counts as leaf node', () => {
// prettier-ignore
const tree: PartialTree = [
_root('ourRoot'),
// empty .checked .cached folder
_folder('1', { parentId: 'ourRoot', cached: true, status: 'checked' }),
]
const result = getNumberOfSelectedFiles(tree)
// This should be "1" for more pleasant UI - if the user unchecks this folder,
// they should immediately see "Selected (1)" turning into "Selected (0)".
expect(result).toEqual(1)
})
})
describe('getCheckedFilesWithPaths()', () => {
// Note that this is a tree that doesn't require any api calls, everything is cached already
// prettier-ignore
const tree: PartialTree = [
_root('ourRoot'),
_folder('1', { parentId: 'ourRoot' }),
_folder('2', { parentId: 'ourRoot' }),
_file('2_1', { parentId: '2' }),
_file('2_2', { parentId: '2', status: 'checked' }),
_file('2_3', { parentId: '2' }),
_folder('2_4', { parentId: '2', status: 'checked' }),
_file('2_4_1', { parentId: '2_4', status: 'checked' }),
_file('2_4_2', { parentId: '2_4', status: 'checked' }),
_file('2_4_3', { parentId: '2_4', status: 'checked' }),
_file('3', { parentId: 'ourRoot' }),
_file('4', { parentId: 'ourRoot' }),
]
// These test cases are based on documentation for .absolutePath and .relativePath (https://uppy.io/docs/uppy/#filemeta)
it('.absolutePath always begins with / + always ends with the file’s name.', () => {
const result = getCheckedFilesWithPaths(tree)
expect(result.find((f) => f.id === '2_2')!.absDirPath).toEqual(
'/name_2/name_2_2.jpg',
)
expect(result.find((f) => f.id === '2_4_3')!.absDirPath).toEqual(
'/name_2/name_2_4/name_2_4_3.jpg',
)
})
it('.relativePath is null when file is selected independently', () => {
const result = getCheckedFilesWithPaths(tree)
// .relDirPath should be `undefined`, which will make .relativePath `null` eventually
expect(result.find((f) => f.id === '2_2')!.relDirPath).toEqual(undefined)
})
it('.relativePath attends to highest checked folder', () => {
const result = getCheckedFilesWithPaths(tree)
expect(result.find((f) => f.id === '2_4_1')!.relDirPath).toEqual(
'name_2_4/name_2_4_1.jpg',
)
})
// (See github.com/transloadit/uppy/pull/5050#discussion_r1638523560)
it('file ids such as "hasOwnProperty" are safe', () => {
const weirdIdsTree = [
_root('ourRoot'),
_folder('1', { parentId: 'ourRoot', status: 'checked' }),
_file('hasOwnProperty', { parentId: '1', status: 'checked' }),
]
const result = getCheckedFilesWithPaths(weirdIdsTree)
expect(result.find((f) => f.id === 'hasOwnProperty')!.relDirPath).toEqual(
'name_1/name_hasOwnProperty.jpg',
)
})
})
describe('getBreadcrumbs()', () => {
// prettier-ignore
const tree: PartialTree = [
_root('ourRoot'),
_folder('1', { parentId: 'ourRoot' }),
_folder('2', { parentId: 'ourRoot' }),
_file('2_1', { parentId: '2' }),
_file('2_2', { parentId: '2' }),
_file('2_3', { parentId: '2' }),
_folder('2_4', { parentId: '2' }),
_file('2_4_1', { parentId: '2_4' }),
_file('2_4_2', { parentId: '2_4' }),
_file('2_4_3', { parentId: '2_4' }),
_file('3', { parentId: 'ourRoot' }),
_file('4', { parentId: 'ourRoot' }),
]
it('returns root folder: "/ourRoot"', () => {
const result = getBreadcrumbs(tree, 'ourRoot')
expect(result.map((f) => f.id)).toEqual(['ourRoot'])
})
it('returns nested folder: "/ourRoot/4"', () => {
const result = getBreadcrumbs(tree, '4')
expect(result.map((f) => f.id)).toEqual(['ourRoot', '4'])
})
it('returns deeply nested folder: "/ourRoot/2/2_4"', () => {
const result = getBreadcrumbs(tree, '2_4')
expect(result.map((f) => f.id)).toEqual(['ourRoot', '2', '2_4'])
})
it('returns folders when currentFolderId=null', () => {
// prettier-ignore
const treeWithNullRoot: PartialTree = [
_root(null!),
_folder('1', { parentId: null }),
]
const result = getBreadcrumbs(treeWithNullRoot, null)
expect(result.map((f) => f.id)).toEqual([null])
})
})