mirror of
https://github.com/transloadit/uppy.git
synced 2026-01-23 02:25:07 +00:00
@uppy/provider-views: add e2e tests for Server side search (#6015)
Tests added as discussed in [slack_discussion](https://transloadit.slack.com/archives/C0FMW9PSB/p1759931999124149?thread_ts=1759700542.941939&cid=C0FMW9PSB) directory structure mocked : ``` root/ ├── first/ │ ├── second/ │ │ ├── third/ │ │ │ ├── nested-target.pdf │ │ │ └── new-file.pdf │ │ ├── deep-file.txt │ │ ├── target.pdf │ │ └── workspace.pdf │ └── intermediate.doc ├── workspace/ │ └── project/ │ └── code.js └── readme.md ``` Some of the mocked responses in CompanionHandler.ts aren’t used in the tests, but I’ve kept them to preserve the legitimacy of the above directory structure.
This commit is contained in:
parent
46f81e2bae
commit
ec75d863ec
6 changed files with 1040 additions and 2 deletions
|
|
@ -56,6 +56,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@uppy/core": "workspace:^",
|
||||
"@uppy/dropbox": "workspace:^",
|
||||
"@uppy/google-drive": "workspace:^",
|
||||
"@uppy/url": "workspace:^",
|
||||
"@uppy/webcam": "workspace:^",
|
||||
|
|
|
|||
568
packages/@uppy/dashboard/src/GlobalSearch.browser.test.ts
Normal file
568
packages/@uppy/dashboard/src/GlobalSearch.browser.test.ts
Normal file
|
|
@ -0,0 +1,568 @@
|
|||
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') as Dashboard<any, any>
|
||||
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()
|
||||
})
|
||||
})
|
||||
447
packages/@uppy/dashboard/src/mocks/CompanionHandler.ts
Normal file
447
packages/@uppy/dashboard/src/mocks/CompanionHandler.ts
Normal file
|
|
@ -0,0 +1,447 @@
|
|||
import { HttpResponse, http } from 'msw'
|
||||
|
||||
const COMPANION_URL = 'http://companion.test'
|
||||
|
||||
/**
|
||||
* Mocked Folder structure :
|
||||
*
|
||||
|
||||
root/ (Dropbox)
|
||||
├── first/
|
||||
│ ├── second/
|
||||
│ │ ├── third/
|
||||
│ │ │ ├── nested-target.pdf
|
||||
│ │ │ └── new-file.pdf
|
||||
│ │ ├── deep-file.txt
|
||||
│ │ ├── target.pdf
|
||||
│ │ └── workspace.pdf
|
||||
│ └── intermediate.doc
|
||||
├── workspace/
|
||||
│ └── project/
|
||||
│ └── code.js
|
||||
└── readme.md
|
||||
|
||||
*/
|
||||
|
||||
export const handlers = [
|
||||
// Mock pre-auth token
|
||||
http.get(`${COMPANION_URL}/:provider/preauth`, () => {
|
||||
return HttpResponse.json({ token: 'mock-preauth-token' })
|
||||
}),
|
||||
|
||||
// Mock authentication check
|
||||
http.get(`${COMPANION_URL}/:provider/connect`, () => {
|
||||
return HttpResponse.json({
|
||||
authenticated: true,
|
||||
username: 'test-user@example.com',
|
||||
})
|
||||
}),
|
||||
|
||||
// Mock folder listing endpoint
|
||||
http.get(`${COMPANION_URL}/:provider/list/*`, ({ request }) => {
|
||||
const url = new URL(request.url)
|
||||
|
||||
// Extract path: split by '/list/' and take everything after it
|
||||
const [, afterList] = url.pathname.split('/list/')
|
||||
const pathStr = afterList ? decodeURIComponent(afterList) : ''
|
||||
|
||||
// Root folder
|
||||
if (!pathStr || pathStr === 'root') {
|
||||
return HttpResponse.json({
|
||||
username: 'test-user@example.com',
|
||||
items: [
|
||||
{
|
||||
isFolder: true,
|
||||
icon: 'folder',
|
||||
name: 'first',
|
||||
mimeType: 'folder',
|
||||
id: 'folder-first',
|
||||
thumbnail: null,
|
||||
requestPath: encodeURIComponent('/first'),
|
||||
modifiedDate: '2024-01-01T00:00:00Z',
|
||||
size: null,
|
||||
},
|
||||
{
|
||||
isFolder: true,
|
||||
icon: 'folder',
|
||||
name: 'workspace',
|
||||
mimeType: 'folder',
|
||||
id: 'folder-workspace',
|
||||
thumbnail: null,
|
||||
requestPath: encodeURIComponent('/workspace'),
|
||||
modifiedDate: '2024-01-02T00:00:00Z',
|
||||
size: null,
|
||||
},
|
||||
{
|
||||
isFolder: false,
|
||||
icon: 'file',
|
||||
name: 'readme.md',
|
||||
mimeType: 'text/markdown',
|
||||
id: 'file-readme',
|
||||
thumbnail: null,
|
||||
requestPath: encodeURIComponent('/readme.md'),
|
||||
modifiedDate: '2024-01-03T00:00:00Z',
|
||||
size: 1024,
|
||||
},
|
||||
],
|
||||
nextPagePath: null,
|
||||
})
|
||||
}
|
||||
|
||||
// first folder
|
||||
if (pathStr === '/first') {
|
||||
return HttpResponse.json({
|
||||
username: 'test-user@example.com',
|
||||
items: [
|
||||
{
|
||||
isFolder: true,
|
||||
icon: 'folder',
|
||||
name: 'second',
|
||||
mimeType: 'folder',
|
||||
id: 'folder-second',
|
||||
thumbnail: null,
|
||||
requestPath: encodeURIComponent('/first/second'),
|
||||
modifiedDate: '2024-01-04T00:00:00Z',
|
||||
size: null,
|
||||
},
|
||||
{
|
||||
isFolder: false,
|
||||
icon: 'file',
|
||||
name: 'intermediate.doc',
|
||||
mimeType: 'application/msword',
|
||||
id: 'file-intermediate',
|
||||
thumbnail: null,
|
||||
requestPath: encodeURIComponent('/first/intermediate.doc'),
|
||||
modifiedDate: '2024-01-05T00:00:00Z',
|
||||
size: 2048,
|
||||
},
|
||||
],
|
||||
nextPagePath: null,
|
||||
})
|
||||
}
|
||||
|
||||
// first/second folder (deep nested)
|
||||
if (pathStr === '/first/second') {
|
||||
return HttpResponse.json({
|
||||
username: 'test-user@example.com',
|
||||
items: [
|
||||
{
|
||||
isFolder: true,
|
||||
icon: 'folder',
|
||||
name: 'third',
|
||||
mimeType: 'folder',
|
||||
id: 'folder-third',
|
||||
thumbnail: null,
|
||||
requestPath: encodeURIComponent('/first/second/third'),
|
||||
modifiedDate: '2024-01-06T00:00:00Z',
|
||||
size: null,
|
||||
},
|
||||
{
|
||||
isFolder: false,
|
||||
icon: 'file',
|
||||
name: 'deep-file.txt',
|
||||
mimeType: 'text/plain',
|
||||
id: 'file-deep',
|
||||
thumbnail: null,
|
||||
requestPath: encodeURIComponent('/first/second/deep-file.txt'),
|
||||
modifiedDate: '2024-01-07T00:00:00Z',
|
||||
size: 512,
|
||||
},
|
||||
{
|
||||
isFolder: false,
|
||||
icon: 'file',
|
||||
name: 'target.pdf',
|
||||
mimeType: 'application/pdf',
|
||||
id: 'file-target',
|
||||
thumbnail: null,
|
||||
requestPath: encodeURIComponent('/first/second/target.pdf'),
|
||||
modifiedDate: '2024-01-08T00:00:00Z',
|
||||
size: 4096,
|
||||
},
|
||||
{
|
||||
isFolder: false,
|
||||
icon: 'file',
|
||||
name: 'workspace.pdf',
|
||||
mimeType: 'application/pdf',
|
||||
id: 'file-workspace-pdf',
|
||||
thumbnail: null,
|
||||
requestPath: encodeURIComponent('/first/second/workspace.pdf'),
|
||||
modifiedDate: '2024-01-11T00:00:00Z',
|
||||
size: 5120,
|
||||
},
|
||||
],
|
||||
nextPagePath: null,
|
||||
})
|
||||
}
|
||||
|
||||
// first/second/third folder (deepest level)
|
||||
if (pathStr === '/first/second/third') {
|
||||
return HttpResponse.json({
|
||||
username: 'test-user@example.com',
|
||||
items: [
|
||||
{
|
||||
isFolder: false,
|
||||
icon: 'file',
|
||||
name: 'nested-target.pdf',
|
||||
mimeType: 'application/pdf',
|
||||
id: 'file-nested-target',
|
||||
thumbnail: null,
|
||||
requestPath: encodeURIComponent(
|
||||
'/first/second/third/nested-target.pdf',
|
||||
),
|
||||
modifiedDate: '2024-01-09T00:00:00Z',
|
||||
size: 2048,
|
||||
},
|
||||
{
|
||||
isFolder: false,
|
||||
icon: 'file',
|
||||
name: 'new-file.pdf',
|
||||
mimeType: 'application/pdf',
|
||||
id: 'file-new',
|
||||
thumbnail: null,
|
||||
requestPath: encodeURIComponent('/first/second/third/new-file.pdf'),
|
||||
modifiedDate: '2024-01-10T00:00:00Z',
|
||||
size: 3072,
|
||||
},
|
||||
],
|
||||
nextPagePath: null,
|
||||
})
|
||||
}
|
||||
|
||||
// workspace folder
|
||||
if (pathStr === '/workspace') {
|
||||
return HttpResponse.json({
|
||||
username: 'test-user@example.com',
|
||||
items: [
|
||||
{
|
||||
isFolder: true,
|
||||
icon: 'folder',
|
||||
name: 'project',
|
||||
mimeType: 'folder',
|
||||
id: 'folder-project',
|
||||
thumbnail: null,
|
||||
requestPath: encodeURIComponent('/workspace/project'),
|
||||
modifiedDate: '2024-01-08T00:00:00Z',
|
||||
size: null,
|
||||
},
|
||||
],
|
||||
nextPagePath: null,
|
||||
})
|
||||
}
|
||||
|
||||
// workspace/project folder
|
||||
if (pathStr === '/workspace/project') {
|
||||
return HttpResponse.json({
|
||||
username: 'test-user@example.com',
|
||||
items: [
|
||||
{
|
||||
isFolder: false,
|
||||
icon: 'file',
|
||||
name: 'code.js',
|
||||
mimeType: 'application/javascript',
|
||||
id: 'file-code',
|
||||
thumbnail: null,
|
||||
requestPath: encodeURIComponent('/workspace/project/code.js'),
|
||||
modifiedDate: '2024-01-09T00:00:00Z',
|
||||
size: 3072,
|
||||
},
|
||||
],
|
||||
nextPagePath: null,
|
||||
})
|
||||
}
|
||||
|
||||
// Default empty folder
|
||||
return HttpResponse.json({
|
||||
username: 'test-user@example.com',
|
||||
items: [],
|
||||
nextPagePath: null,
|
||||
})
|
||||
}),
|
||||
|
||||
// Mock search endpoint
|
||||
http.get(`${COMPANION_URL}/:provider/search`, ({ request, params }) => {
|
||||
const url = new URL(request.url)
|
||||
const provider = params.provider as string
|
||||
const query = url.searchParams.get('q') || ''
|
||||
const searchPath = url.searchParams.get('path')
|
||||
|
||||
if (provider === 'drive') {
|
||||
return HttpResponse.json(
|
||||
{ message: 'method not implemented' },
|
||||
{ status: 500 },
|
||||
)
|
||||
}
|
||||
|
||||
// Search for "second" folder
|
||||
if (query.toLowerCase() === 'second') {
|
||||
return HttpResponse.json({
|
||||
username: 'test-user@example.com',
|
||||
items: [
|
||||
{
|
||||
isFolder: true,
|
||||
icon: 'folder',
|
||||
name: 'second',
|
||||
mimeType: 'folder',
|
||||
id: 'folder-second',
|
||||
thumbnail: null,
|
||||
requestPath: encodeURIComponent('/first/second'),
|
||||
modifiedDate: '2024-01-04T00:00:00Z',
|
||||
size: null,
|
||||
},
|
||||
],
|
||||
nextPagePath: null,
|
||||
searchedFor: query,
|
||||
})
|
||||
}
|
||||
|
||||
// Search for "target" file (global search from root)
|
||||
if (query.toLowerCase() === 'target' && !searchPath) {
|
||||
return HttpResponse.json({
|
||||
username: 'test-user@example.com',
|
||||
items: [
|
||||
{
|
||||
isFolder: false,
|
||||
icon: 'file',
|
||||
name: 'target.pdf',
|
||||
mimeType: 'application/pdf',
|
||||
id: 'file-target',
|
||||
thumbnail: null,
|
||||
requestPath: encodeURIComponent('/first/second/target.pdf'),
|
||||
modifiedDate: '2024-01-08T00:00:00Z',
|
||||
size: 4096,
|
||||
},
|
||||
{
|
||||
isFolder: false,
|
||||
icon: 'file',
|
||||
name: 'nested-target.pdf',
|
||||
mimeType: 'application/pdf',
|
||||
id: 'file-nested-target',
|
||||
thumbnail: null,
|
||||
requestPath: encodeURIComponent(
|
||||
'/first/second/third/nested-target.pdf',
|
||||
),
|
||||
modifiedDate: '2024-01-09T00:00:00Z',
|
||||
size: 2048,
|
||||
},
|
||||
],
|
||||
nextPagePath: null,
|
||||
searchedFor: query,
|
||||
})
|
||||
}
|
||||
|
||||
// Scoped search for "target" from /first directory
|
||||
if (
|
||||
query.toLowerCase() === 'target' &&
|
||||
searchPath === encodeURIComponent('/first')
|
||||
) {
|
||||
return HttpResponse.json({
|
||||
username: 'test-user@example.com',
|
||||
items: [
|
||||
{
|
||||
isFolder: false,
|
||||
icon: 'file',
|
||||
name: 'target.pdf',
|
||||
mimeType: 'application/pdf',
|
||||
id: 'file-target',
|
||||
thumbnail: null,
|
||||
requestPath: encodeURIComponent('/first/second/target.pdf'),
|
||||
modifiedDate: '2024-01-08T00:00:00Z',
|
||||
size: 4096,
|
||||
},
|
||||
{
|
||||
isFolder: false,
|
||||
icon: 'file',
|
||||
name: 'nested-target.pdf',
|
||||
mimeType: 'application/pdf',
|
||||
id: 'file-nested-target',
|
||||
thumbnail: null,
|
||||
requestPath: encodeURIComponent(
|
||||
'/first/second/third/nested-target.pdf',
|
||||
),
|
||||
modifiedDate: '2024-01-09T00:00:00Z',
|
||||
size: 2048,
|
||||
},
|
||||
],
|
||||
nextPagePath: null,
|
||||
searchedFor: query,
|
||||
})
|
||||
}
|
||||
|
||||
// Search for "deep" - deep nested file
|
||||
if (query.toLowerCase() === 'deep') {
|
||||
return HttpResponse.json({
|
||||
username: 'test-user@example.com',
|
||||
items: [
|
||||
{
|
||||
isFolder: false,
|
||||
icon: 'file',
|
||||
name: 'deep-file.txt',
|
||||
mimeType: 'text/plain',
|
||||
id: 'file-deep',
|
||||
thumbnail: null,
|
||||
requestPath: encodeURIComponent('/first/second/deep-file.txt'),
|
||||
modifiedDate: '2024-01-07T00:00:00Z',
|
||||
size: 512,
|
||||
},
|
||||
],
|
||||
nextPagePath: null,
|
||||
searchedFor: query,
|
||||
})
|
||||
}
|
||||
|
||||
// Search from subpath - "code" in workspace
|
||||
if (
|
||||
query.toLowerCase() === 'code' &&
|
||||
searchPath === encodeURIComponent('/workspace')
|
||||
) {
|
||||
return HttpResponse.json({
|
||||
username: 'test-user@example.com',
|
||||
items: [
|
||||
{
|
||||
isFolder: false,
|
||||
icon: 'file',
|
||||
name: 'code.js',
|
||||
mimeType: 'application/javascript',
|
||||
id: 'file-code',
|
||||
thumbnail: null,
|
||||
requestPath: encodeURIComponent('/workspace/project/code.js'),
|
||||
modifiedDate: '2024-01-09T00:00:00Z',
|
||||
size: 3072,
|
||||
},
|
||||
],
|
||||
nextPagePath: null,
|
||||
searchedFor: query,
|
||||
})
|
||||
}
|
||||
|
||||
// Search for "readme" file in root
|
||||
if (query.toLowerCase() === 'readme') {
|
||||
return HttpResponse.json({
|
||||
username: 'test-user@example.com',
|
||||
items: [
|
||||
{
|
||||
isFolder: false,
|
||||
icon: 'file',
|
||||
name: 'readme.md',
|
||||
mimeType: 'text/markdown',
|
||||
id: 'file-readme',
|
||||
thumbnail: null,
|
||||
requestPath: encodeURIComponent('/readme.md'),
|
||||
modifiedDate: '2024-01-03T00:00:00Z',
|
||||
size: 1024,
|
||||
},
|
||||
],
|
||||
nextPagePath: null,
|
||||
searchedFor: query,
|
||||
})
|
||||
}
|
||||
|
||||
// No results
|
||||
return HttpResponse.json({
|
||||
username: 'test-user@example.com',
|
||||
items: [],
|
||||
nextPagePath: null,
|
||||
searchedFor: query,
|
||||
})
|
||||
}),
|
||||
]
|
||||
4
packages/@uppy/dashboard/src/setup.ts
Normal file
4
packages/@uppy/dashboard/src/setup.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import { setupWorker } from 'msw/browser'
|
||||
import { handlers } from '../src/mocks/CompanionHandler.js'
|
||||
|
||||
export const worker = setupWorker(...handlers)
|
||||
|
|
@ -110,6 +110,10 @@ type RenderOpts<M extends Meta, B extends Body> = Omit<
|
|||
export default class ProviderView<M extends Meta, B extends Body> {
|
||||
static VERSION = packageJson.version
|
||||
|
||||
// Test hook (mirrors GoldenRetriever pattern): allow tests to override debounce time
|
||||
// @ts-expect-error test-only hook key
|
||||
static [Symbol.for('uppy test: searchDebounceMs')]: number | undefined
|
||||
|
||||
plugin: UnknownProviderPlugin<M, B>
|
||||
|
||||
provider: UnknownProviderPlugin<M, B>['provider']
|
||||
|
|
@ -119,6 +123,7 @@ export default class ProviderView<M extends Meta, B extends Body> {
|
|||
isHandlingScroll: boolean = false
|
||||
|
||||
previousCheckbox: string | null = null
|
||||
#searchDebounced: () => void
|
||||
|
||||
constructor(plugin: UnknownProviderPlugin<M, B>, opts: PassedOpts<M, B>) {
|
||||
this.plugin = plugin
|
||||
|
|
@ -157,6 +162,16 @@ export default class ProviderView<M extends Meta, B extends Body> {
|
|||
this.provider.provider,
|
||||
this.provider,
|
||||
)
|
||||
|
||||
// Configure debounced search with test override
|
||||
const testHookSymbol = Symbol.for('uppy test: searchDebounceMs')
|
||||
const testWait = (
|
||||
ProviderView as unknown as Record<symbol, number | undefined>
|
||||
)[testHookSymbol]
|
||||
const wait = testWait ?? 500
|
||||
const debounceOpts =
|
||||
testWait === 0 ? { leading: true, trailing: true } : undefined
|
||||
this.#searchDebounced = debounce(this.#search, wait, debounceOpts)
|
||||
}
|
||||
|
||||
resetPluginState(): void {
|
||||
|
|
@ -333,11 +348,13 @@ export default class ProviderView<M extends Meta, B extends Body> {
|
|||
this.setLoading(false)
|
||||
}
|
||||
|
||||
#searchDebounced = debounce(this.#search, 500)
|
||||
// debounced search function is initialized in the constructor
|
||||
|
||||
onSearchInput = (s: string): void => {
|
||||
this.plugin.setPluginState({ searchString: s })
|
||||
if (this.opts.supportsSearch) this.#searchDebounced()
|
||||
if (this.opts.supportsSearch) {
|
||||
this.#searchDebounced()
|
||||
}
|
||||
}
|
||||
|
||||
async openSearchResultFolder(folderId: PartialTreeId): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -10918,6 +10918,7 @@ __metadata:
|
|||
dependencies:
|
||||
"@transloadit/prettier-bytes": "npm:^0.3.4"
|
||||
"@uppy/core": "workspace:^"
|
||||
"@uppy/dropbox": "workspace:^"
|
||||
"@uppy/google-drive": "workspace:^"
|
||||
"@uppy/provider-views": "workspace:^"
|
||||
"@uppy/thumbnail-generator": "workspace:^"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue