@uppy/dashboard
Version:
Universal UI plugin for Uppy.
569 lines (470 loc) • 19.6 kB
text/typescript
import Uppy from '@uppy/core'
import Dashboard from '@uppy/dashboard'
import Dropbox from '@uppy/dropbox'
import GoogleDrive from '@uppy/google-drive'
import { ProviderViews } from '@uppy/provider-views'
import { page, userEvent } from '@vitest/browser/context'
import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vitest'
import { worker } from './setup.js'
import '@uppy/core/css/style.css'
import '@uppy/dashboard/css/style.css'
let uppy: Uppy | undefined
/**
* In Normal mode (ListItem.tsx), folders are rendered as buttons, whereas files are rendered as checkboxes with a corresponding <label>.
* In Search mode (SearchListItem.tsx), both files and folders are rendered as buttons.
* Because of this, in Normal mode, when checking whether a file exists, we need to use:
* await expect.element(page.getByRole('button', { name:'nested-target.pdf', exact: true }))
* whereas, in Search mode, we need to scope the query to the checkbox role instead when searching for a file
*/
beforeAll(async () => {
// Disable search debounce inside ProviderView during tests to avoid long sleeps
// @ts-expect-error test-only hook
ProviderViews[Symbol.for('uppy test: searchDebounceMs')] = 0
await worker.start({
onUnhandledRequest: 'bypass',
})
})
afterAll(async () => {
await worker.stop()
})
type SourceName = 'Dropbox' | 'GoogleDrive'
function initializeUppy(sources: SourceName[] = ['Dropbox']) {
document.body.innerHTML = '<div id="app"></div>'
const instance = new Uppy({ id: 'uppy-e2e' }).use(Dashboard, {
target: '#app',
inline: true,
height: 500,
})
for (const source of sources) {
if (source === 'Dropbox') {
instance.use(Dropbox, { companionUrl: 'http://companion.test' })
} else if (source === 'GoogleDrive') {
instance.use(GoogleDrive, { companionUrl: 'http://companion.test' })
}
}
return instance
}
// Removed shared beforeEach initialization. Each test initializes its own Uppy instance.
afterEach(async () => {
if (!uppy) return
// this is done to prevent the edgecase when all plugins are removed before dashboard is unmounted from UI
// causing PickerPanelContent to crash
const dashboard = uppy.getPlugin('Dashboard')
dashboard?.hideAllPanels()
const panelSelector = '[data-uppy-panelType="PickerPanel"]'
if (document.querySelector(panelSelector)) {
await expect.poll(() => document.querySelector(panelSelector)).toBeNull()
}
uppy.destroy()
uppy = undefined
})
describe('ProviderView Search E2E', () => {
test('Search for nested file in Dropbox and verify results', async () => {
uppy = initializeUppy(['Dropbox'])
await expect
.element(page.getByRole('presentation').getByText('Dropbox'))
.toBeVisible()
await page.getByRole('tab', { name: 'Dropbox' }).click()
await expect
.element(page.getByRole('heading', { name: /import from dropbox/i }))
.toBeVisible()
const panel = page.getByRole('tabpanel')
await expect.element(panel.getByText('test-user@example.com')).toBeVisible()
const searchInput = document.querySelector(
'.uppy-ProviderBrowser-searchFilterInput',
) as HTMLInputElement
expect(searchInput).toBeDefined()
// Search mode (SearchResultItem.tsx): files and folders render as buttons;
// use role=button for file assertions in search results.
await userEvent.type(searchInput, 'target')
await expect
.element(page.getByRole('button', { name: 'target.pdf', exact: true }))
.toBeVisible()
const targetPdfItem = await page.getByRole('button', {
name: 'target.pdf',
exact: true,
})
expect(targetPdfItem).toBeTruthy()
})
test('Search deep folder -> open it -> click ancestor breadcrumb and navigate correctly', async () => {
uppy = initializeUppy(['Dropbox'])
await expect
.element(page.getByRole('presentation').getByText('Dropbox'))
.toBeVisible()
await page.getByRole('tab', { name: 'Dropbox' }).click()
await expect
.element(page.getByRole('heading', { name: /import from dropbox/i }))
.toBeVisible()
const panel = page.getByRole('tabpanel')
await expect.element(panel.getByText('test-user@example.com')).toBeVisible()
await expect
.element(page.getByRole('button', { name: 'first' }))
.toBeVisible()
const searchInput = document.querySelector(
'.uppy-ProviderBrowser-searchFilterInput',
) as HTMLInputElement
await userEvent.clear(searchInput)
await userEvent.type(searchInput, 'second')
await expect
.element(page.getByRole('button', { name: 'second' }))
.toBeVisible()
const secondFolder = await page.getByRole('button', { name: 'second' })
await secondFolder.click()
// Normal mode (ListItem.tsx): files render as checkboxes with a corresponding <label>.
// Use role=checkbox for file assertions in browse view.
await expect
.element(
page.getByRole('checkbox', { name: 'deep-file.txt', exact: true }),
)
.toBeVisible()
// Click ancestor breadcrumb that was never loaded before in browse mode
const firstBreadcrumb = page.getByRole('button', { name: 'first' })
await firstBreadcrumb.click()
const hasSecondFolder = await page.getByRole('button', {
name: 'second',
exact: true,
})
expect(hasSecondFolder).toBeVisible()
})
test('Check folder in browse mode, search for nested item -> nested item should be checked', async () => {
uppy = initializeUppy(['Dropbox'])
await expect
.element(page.getByRole('presentation').getByText('Dropbox'))
.toBeVisible()
await page.getByRole('tab', { name: 'Dropbox' }).click()
await expect
.element(page.getByRole('heading', { name: /import from dropbox/i }))
.toBeVisible()
const panel = page.getByRole('tabpanel')
await expect.element(panel.getByText('test-user@example.com')).toBeVisible()
await expect
.element(page.getByRole('button', { name: 'first' }))
.toBeVisible()
const firstFolderItem = Array.from(
document.querySelectorAll('.uppy-ProviderBrowserItem'),
).find(
(item) =>
item.textContent?.includes('first') && item.querySelector('button'),
)
const firstFolderCheckbox =
firstFolderItem?.querySelector<HTMLInputElement>('input[type="checkbox"]')
expect(firstFolderCheckbox).toBeTruthy()
await firstFolderCheckbox!.click()
expect(firstFolderCheckbox!.checked).toBe(true)
const searchInput = document.querySelector(
'.uppy-ProviderBrowser-searchFilterInput',
) as HTMLInputElement
await userEvent.type(searchInput, 'second')
await expect
.element(page.getByRole('button', { name: 'second', exact: true }))
.toBeVisible()
const secondFolderItem = Array.from(
document.querySelectorAll('.uppy-ProviderBrowserItem'),
).find((item) => item.textContent?.includes('second'))
const secondFolderCheckbox =
secondFolderItem?.querySelector<HTMLInputElement>(
'input[type="checkbox"]',
)
expect(secondFolderCheckbox).toBeTruthy()
// Children inherit checked state from parent
expect(secondFolderCheckbox!.checked).toBe(true)
})
test('Search for nested item, check it, go back to normal view -> parent should be partial', async () => {
uppy = initializeUppy(['Dropbox'])
await expect
.element(page.getByRole('presentation').getByText('Dropbox'))
.toBeVisible()
await page.getByRole('tab', { name: 'Dropbox' }).click()
await expect
.element(page.getByRole('heading', { name: /import from dropbox/i }))
.toBeVisible()
const panel = page.getByRole('tabpanel')
await expect.element(panel.getByText('test-user@example.com')).toBeVisible()
await expect
.element(page.getByRole('button', { name: 'first' }))
.toBeVisible()
const searchInput = document.querySelector(
'.uppy-ProviderBrowser-searchFilterInput',
) as HTMLInputElement
await userEvent.type(searchInput, 'second')
await expect
.element(page.getByRole('button', { name: 'second', exact: true }))
.toBeVisible()
const secondFolderItem = Array.from(
document.querySelectorAll('.uppy-ProviderBrowserItem'),
).find((item) => item.textContent?.includes('second'))
const secondFolderCheckbox =
secondFolderItem?.querySelector<HTMLInputElement>(
'input[type="checkbox"]',
)
expect(secondFolderCheckbox).toBeTruthy()
await secondFolderCheckbox!.click()
expect(secondFolderCheckbox!.checked).toBe(true)
const clearSearchButton = document.querySelector(
'.uppy-ProviderBrowser-searchFilterReset',
) as HTMLButtonElement
expect(clearSearchButton).toBeDefined()
await clearSearchButton.click()
await expect
.element(page.getByRole('button', { name: 'first' }))
.toBeVisible()
const firstFolderItem = Array.from(
document.querySelectorAll('.uppy-ProviderBrowserItem'),
).find(
(item) =>
item.textContent?.includes('first') && item.querySelector('button'),
)
expect(firstFolderItem).toBeTruthy()
// Parent is partial when some (but not all) children are checked
expect(
firstFolderItem?.classList.contains(
'uppy-ProviderBrowserItem--is-partial',
),
).toBe(true)
})
test('Search for nested item, check then uncheck it, go back to normal view -> parent should be unchecked', async () => {
uppy = initializeUppy(['Dropbox'])
await expect
.element(page.getByRole('presentation').getByText('Dropbox'))
.toBeVisible()
await page.getByRole('tab', { name: 'Dropbox' }).click()
await expect
.element(page.getByRole('heading', { name: /import from dropbox/i }))
.toBeVisible()
const panel = page.getByRole('tabpanel')
await expect.element(panel.getByText('test-user@example.com')).toBeVisible()
await expect
.element(page.getByRole('button', { name: 'first' }))
.toBeVisible()
const searchInput = document.querySelector(
'.uppy-ProviderBrowser-searchFilterInput',
) as HTMLInputElement
await userEvent.type(searchInput, 'second')
await expect
.element(page.getByRole('button', { name: 'second', exact: true }))
.toBeVisible()
const secondFolderItem = Array.from(
document.querySelectorAll('.uppy-ProviderBrowserItem'),
).find((item) => item.textContent?.includes('second'))
const secondFolderCheckbox =
secondFolderItem?.querySelector<HTMLInputElement>(
'input[type="checkbox"]',
)
expect(secondFolderCheckbox).toBeTruthy()
await secondFolderCheckbox!.click()
expect(secondFolderCheckbox!.checked).toBe(true)
await secondFolderCheckbox!.click()
expect(secondFolderCheckbox!.checked).toBe(false)
const clearSearchButton = document.querySelector(
'.uppy-ProviderBrowser-searchFilterReset',
) as HTMLButtonElement
expect(clearSearchButton).toBeDefined()
await clearSearchButton.click()
await expect
.element(page.getByRole('button', { name: 'first' }))
.toBeVisible()
const firstFolderItem = Array.from(
document.querySelectorAll('.uppy-ProviderBrowserItem'),
).find(
(item) =>
item.textContent?.includes('first') && item.querySelector('button'),
)
expect(firstFolderItem).toBeTruthy()
expect(
firstFolderItem?.classList.contains(
'uppy-ProviderBrowserItem--is-checked',
),
).toBe(false)
expect(
firstFolderItem?.classList.contains(
'uppy-ProviderBrowserItem--is-partial',
),
).toBe(false)
const firstFolderCheckbox =
firstFolderItem?.querySelector<HTMLInputElement>('input[type="checkbox"]')
expect(firstFolderCheckbox).toBeTruthy()
expect(firstFolderCheckbox!.checked).toBe(false)
})
test('Navigate into folder and perform scoped search -> should find nested files at multiple levels', async () => {
uppy = initializeUppy(['Dropbox'])
await expect
.element(page.getByRole('presentation').getByText('Dropbox'))
.toBeVisible()
await page.getByRole('tab', { name: 'Dropbox' }).click()
await expect
.element(page.getByRole('heading', { name: /import from dropbox/i }))
.toBeVisible()
const panel = page.getByRole('tabpanel')
await expect.element(panel.getByText('test-user@example.com')).toBeVisible()
await expect
.element(page.getByRole('button', { name: 'first' }))
.toBeVisible()
const firstFolderButton = page.getByRole('button', { name: 'first' })
await firstFolderButton.click()
await expect
.element(page.getByRole('button', { name: 'second' }))
.toBeVisible()
// Normal mode (ListItem.tsx): files render as checkboxes with corresponding <label>; scope by role=checkbox. refer to a commment at the top of the file for more detailed explanation.
await expect
.element(
page.getByRole('checkbox', { name: 'intermediate.doc', exact: true }),
)
.toBeVisible()
const searchInput = document.querySelector(
'.uppy-ProviderBrowser-searchFilterInput',
) as HTMLInputElement
expect(searchInput).toBeDefined()
await userEvent.type(searchInput, 'target')
// Search mode (SearchResultItem.tsx): files render as buttons; scope by role=button.
await expect
.element(page.getByRole('button', { name: 'target.pdf', exact: true }))
.toBeVisible()
await expect
.element(
page.getByRole('button', { name: 'nested-target.pdf', exact: true }),
)
.toBeVisible()
const searchResults = Array.from(
document.querySelectorAll('.uppy-ProviderBrowserItem'),
)
const targetFiles = searchResults.filter((item) =>
item.textContent?.toLowerCase().includes('target'),
)
expect(targetFiles.length).toBe(2)
})
test('No duplicate items when searching and then browsing to the same file', async () => {
uppy = initializeUppy(['Dropbox'])
await expect
.element(page.getByRole('presentation').getByText('Dropbox'))
.toBeVisible()
await page.getByRole('tab', { name: 'Dropbox' }).click()
await expect
.element(page.getByRole('heading', { name: /import from dropbox/i }))
.toBeVisible()
const panel = page.getByRole('tabpanel')
await expect.element(panel.getByText('test-user@example.com')).toBeVisible()
await expect
.element(page.getByRole('button', { name: 'first' }))
.toBeVisible()
// Normal mode (ListItem.tsx): file is a checkbox; assert by role=checkbox. refer to a commment at the top of the file for more detailed explanation.
await expect
.element(page.getByRole('checkbox', { name: 'readme.md', exact: true }))
.toBeVisible()
const searchInput = document.querySelector(
'.uppy-ProviderBrowser-searchFilterInput',
) as HTMLInputElement
await userEvent.type(searchInput, 'readme')
// Search mode (SearchResultItem.tsx): file is a button; assert by role=button.
await expect
.element(page.getByRole('button', { name: 'readme.md', exact: true }))
.toBeVisible()
const searchResults = Array.from(
document.querySelectorAll('.uppy-ProviderBrowserItem'),
)
const readmeInSearch = searchResults.filter((item) =>
item.textContent?.includes('readme.md'),
)
expect(readmeInSearch.length).toBe(1)
const clearSearchButton = document.querySelector(
'.uppy-ProviderBrowser-searchFilterReset',
) as HTMLButtonElement
expect(clearSearchButton).toBeDefined()
await clearSearchButton.click()
// proceed to verify browse results directly
const browseResults = Array.from(
document.querySelectorAll('.uppy-ProviderBrowserItem'),
)
const readmeInBrowse = browseResults.filter((item) =>
item.textContent?.includes('readme.md'),
)
expect(readmeInBrowse.length).toBe(1)
const readmeCheckbox = readmeInBrowse[0]?.querySelector<HTMLInputElement>(
'input[type="checkbox"]',
)
expect(readmeCheckbox).toBeTruthy()
await readmeCheckbox!.click()
expect(readmeCheckbox!.checked).toBe(true)
// Verify checked state persists after searching again (same node in partialTree)
await userEvent.clear(searchInput)
await userEvent.type(searchInput, 'readme')
await expect
.element(page.getByRole('button', { name: 'readme.md', exact: true }))
.toBeVisible()
const searchResultsAgain = Array.from(
document.querySelectorAll('.uppy-ProviderBrowserItem'),
)
const readmeInSearchAgain = searchResultsAgain.find((item) =>
item.textContent?.includes('readme.md'),
)
const readmeCheckboxInSearch =
readmeInSearchAgain?.querySelector<HTMLInputElement>(
'input[type="checkbox"]',
)
expect(readmeCheckboxInSearch).toBeTruthy()
expect(readmeCheckboxInSearch!.checked).toBe(true)
})
test('Client-side filtering works for providers without server-side search (Google Drive)', async () => {
uppy = initializeUppy(['GoogleDrive'])
await expect
.element(page.getByRole('presentation').getByText('Google Drive'))
.toBeVisible()
await page.getByRole('tab', { name: /google drive/i }).click()
await expect
.element(page.getByRole('heading', { name: /import from google drive/i }))
.toBeVisible()
const panel = page.getByRole('tabpanel')
await expect.element(panel.getByText('test-user@example.com')).toBeVisible()
await expect
.element(page.getByRole('button', { name: 'first' }))
.toBeVisible()
await expect
.element(page.getByRole('checkbox', { name: 'workspace' }))
.toBeVisible()
await expect
.element(page.getByRole('checkbox', { name: 'readme.md' }))
.toBeVisible()
const searchInput = document.querySelector(
'.uppy-ProviderBrowser-searchFilterInput',
) as HTMLInputElement
expect(searchInput).toBeDefined()
await userEvent.type(searchInput, 'workspace')
await expect.element(page.getByText('workspace')).toBeVisible()
const visibleItems = Array.from(
document.querySelectorAll('.uppy-ProviderBrowserItem'),
)
expect(visibleItems.length).toBe(1)
const workspaceItem = visibleItems.find((item) =>
item.textContent?.includes('workspace'),
)
expect(workspaceItem).toBeTruthy()
const firstItem = visibleItems.find((item) => {
const button = item.querySelector('button.uppy-ProviderBrowserItem-inner')
return button?.textContent?.trim() === 'first'
})
const readmeItem = visibleItems.find((item) =>
item.textContent?.includes('readme.md'),
)
expect(firstItem).toBeUndefined()
expect(readmeItem).toBeUndefined()
await userEvent.clear(searchInput)
await expect
.element(page.getByRole('button', { name: 'first' }))
.toBeVisible()
const allItems = Array.from(
document.querySelectorAll('.uppy-ProviderBrowserItem'),
)
expect(allItems.length).toBe(3)
await userEvent.type(searchInput, 'readme')
await expect
.element(page.getByRole('checkbox', { name: 'readme.md' }))
.toBeVisible()
const filteredItems = Array.from(
document.querySelectorAll('.uppy-ProviderBrowserItem'),
)
expect(filteredItems.length).toBe(1)
const readmeFiltered = filteredItems.find((item) =>
item.textContent?.includes('readme.md'),
)
expect(readmeFiltered).toBeTruthy()
})
})