mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 14:30:41 +00:00
## Summary - Extend `AssetsSidebarTab` page object with selectors for search, view mode, asset cards, selection footer, context menu, and folder view navigation - Add mock data factories (`createMockJob`, `createMockJobs`, `createMockImportedFiles`) to `AssetsHelper` for generating realistic test fixtures - Write 30 E2E test cases across 10 categories covering the Assets browser sidebar panel ## Test coverage added | Category | Tests | Details | |----------|-------|---------| | Empty states | 3 | Generated/Imported empty copy, zero cards | | Tab navigation | 3 | Default tab, switching, search reset on tab change | | Grid view display | 2 | Generated card rendering, Imported tab assets | | View mode toggle | 2 | Grid↔List switching via settings menu | | Search | 4 | Input visibility, filtering, clearing, no-match state | | Selection | 5 | Click select, Ctrl+click multi, footer, deselect all, tab-switch clear | | Context menu | 7 | Right-click menu, Download/Inspect/Delete/CopyJobID/Workflow actions, bulk menu | | Bulk actions | 3 | Download/Delete buttons, selection count display | | Pagination | 1 | Large job set initial load | | Settings menu | 1 | View mode options visibility | ## Context Part of [FixIt Burndown](https://www.notion.so/comfy-org/FixIt-Burndown-32e6d73d365080609a81cdc9bc884460) — "Untested Side Panels: Assets browser" assigned to @dante01yoon. ## Test plan - [ ] Run `npx playwright test browser_tests/tests/sidebar/assets.spec.ts` against local ComfyUI backend - [ ] Verify all 30 tests pass - [ ] CI green ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-10616-test-assets-sidebar-add-comprehensive-E2E-tests-for-Assets-browser-panel-3306d73d365081eeb237e559f56689bf) by [Unito](https://www.unito.io)
366 lines
8.8 KiB
TypeScript
366 lines
8.8 KiB
TypeScript
import type { Locator, Page } from '@playwright/test'
|
|
import { expect } from '@playwright/test'
|
|
|
|
import type { WorkspaceStore } from '../../types/globals'
|
|
import { TestIds } from '../selectors'
|
|
|
|
class SidebarTab {
|
|
constructor(
|
|
public readonly page: Page,
|
|
public readonly tabId: string
|
|
) {}
|
|
|
|
get tabButton() {
|
|
return this.page.locator(`.${this.tabId}-tab-button`)
|
|
}
|
|
|
|
get selectedTabButton() {
|
|
return this.page.locator(
|
|
`.${this.tabId}-tab-button.side-bar-button-selected`
|
|
)
|
|
}
|
|
|
|
async open() {
|
|
if (await this.selectedTabButton.isVisible()) {
|
|
return
|
|
}
|
|
await this.tabButton.click()
|
|
}
|
|
async close() {
|
|
if (!this.tabButton.isVisible()) {
|
|
return
|
|
}
|
|
await this.tabButton.click()
|
|
}
|
|
}
|
|
|
|
export class NodeLibrarySidebarTab extends SidebarTab {
|
|
constructor(public override readonly page: Page) {
|
|
super(page, 'node-library')
|
|
}
|
|
|
|
get nodeLibrarySearchBoxInput() {
|
|
return this.page.getByPlaceholder('Search Nodes...')
|
|
}
|
|
|
|
get nodeLibraryTree() {
|
|
return this.page.getByTestId(TestIds.sidebar.nodeLibrary)
|
|
}
|
|
|
|
get nodePreview() {
|
|
return this.page.locator('.node-lib-node-preview')
|
|
}
|
|
|
|
get tabContainer() {
|
|
return this.page.locator('.sidebar-content-container')
|
|
}
|
|
|
|
get newFolderButton() {
|
|
return this.tabContainer.locator('.new-folder-button')
|
|
}
|
|
|
|
override async open() {
|
|
await super.open()
|
|
await this.nodeLibraryTree.waitFor({ state: 'visible' })
|
|
}
|
|
|
|
override async close() {
|
|
if (!this.tabButton.isVisible()) {
|
|
return
|
|
}
|
|
|
|
await this.tabButton.click()
|
|
await this.nodeLibraryTree.waitFor({ state: 'hidden' })
|
|
}
|
|
|
|
getFolder(folderName: string) {
|
|
return this.page.locator(
|
|
`[data-testid="node-tree-folder"][data-folder-name="${folderName}"]`
|
|
)
|
|
}
|
|
|
|
getNode(nodeName: string) {
|
|
return this.page.locator(
|
|
`[data-testid="node-tree-leaf"][data-node-name="${nodeName}"]`
|
|
)
|
|
}
|
|
|
|
nodeSelector(nodeName: string): string {
|
|
return `[data-testid="node-tree-leaf"][data-node-name="${nodeName}"]`
|
|
}
|
|
|
|
folderSelector(folderName: string): string {
|
|
return `[data-testid="node-tree-folder"][data-folder-name="${folderName}"]`
|
|
}
|
|
|
|
getNodeInFolder(nodeName: string, folderName: string) {
|
|
return this.getFolder(folderName)
|
|
.locator('xpath=ancestor::li')
|
|
.locator(`[data-testid="node-tree-leaf"][data-node-name="${nodeName}"]`)
|
|
}
|
|
}
|
|
|
|
export class WorkflowsSidebarTab extends SidebarTab {
|
|
constructor(public override readonly page: Page) {
|
|
super(page, 'workflows')
|
|
}
|
|
|
|
get root() {
|
|
return this.page.getByTestId(TestIds.sidebar.workflows)
|
|
}
|
|
|
|
async getOpenedWorkflowNames() {
|
|
return await this.root
|
|
.locator('.comfyui-workflows-open .node-label')
|
|
.allInnerTexts()
|
|
}
|
|
|
|
async getActiveWorkflowName() {
|
|
return await this.root
|
|
.locator('.comfyui-workflows-open .p-tree-node-selected .node-label')
|
|
.innerText()
|
|
}
|
|
|
|
async getTopLevelSavedWorkflowNames() {
|
|
return await this.root
|
|
.locator('.comfyui-workflows-browse .node-label')
|
|
.allInnerTexts()
|
|
}
|
|
|
|
async switchToWorkflow(workflowName: string) {
|
|
const workflowLocator = this.getOpenedItem(workflowName)
|
|
await workflowLocator.click()
|
|
}
|
|
|
|
getOpenedItem(name: string) {
|
|
return this.root.locator('.comfyui-workflows-open .node-label', {
|
|
hasText: name
|
|
})
|
|
}
|
|
|
|
getPersistedItem(name: string) {
|
|
return this.root.locator('.comfyui-workflows-browse .node-label', {
|
|
hasText: name
|
|
})
|
|
}
|
|
|
|
async renameWorkflow(locator: Locator, newName: string) {
|
|
await locator.click({ button: 'right' })
|
|
await this.page
|
|
.locator('.p-contextmenu-item-content', { hasText: 'Rename' })
|
|
.click()
|
|
await this.page.keyboard.type(newName)
|
|
await this.page.keyboard.press('Enter')
|
|
|
|
// Wait for workflow service to finish renaming
|
|
await this.page.waitForFunction(
|
|
() =>
|
|
!(window.app?.extensionManager as WorkspaceStore | undefined)?.workflow
|
|
?.isBusy,
|
|
undefined,
|
|
{ timeout: 3000 }
|
|
)
|
|
}
|
|
|
|
async insertWorkflow(locator: Locator) {
|
|
await locator.click({ button: 'right' })
|
|
await this.page
|
|
.locator('.p-contextmenu-item-content', { hasText: 'Insert' })
|
|
.click()
|
|
}
|
|
}
|
|
|
|
export class AssetsSidebarTab extends SidebarTab {
|
|
constructor(public override readonly page: Page) {
|
|
super(page, 'assets')
|
|
}
|
|
|
|
// --- Tab navigation ---
|
|
|
|
get generatedTab() {
|
|
return this.page.getByRole('tab', { name: 'Generated' })
|
|
}
|
|
|
|
get importedTab() {
|
|
return this.page.getByRole('tab', { name: 'Imported' })
|
|
}
|
|
|
|
// --- Empty state ---
|
|
|
|
get emptyStateMessage() {
|
|
return this.page.getByText(
|
|
'Upload files or generate content to see them here'
|
|
)
|
|
}
|
|
|
|
emptyStateTitle(title: string) {
|
|
return this.page.getByText(title)
|
|
}
|
|
|
|
// --- Search & filter ---
|
|
|
|
get searchInput() {
|
|
return this.page.getByPlaceholder('Search Assets...')
|
|
}
|
|
|
|
get settingsButton() {
|
|
return this.page.getByRole('button', { name: 'View settings' })
|
|
}
|
|
|
|
// --- View mode ---
|
|
|
|
get listViewOption() {
|
|
return this.page.getByText('List view')
|
|
}
|
|
|
|
get gridViewOption() {
|
|
return this.page.getByText('Grid view')
|
|
}
|
|
|
|
// --- Sort options (cloud-only, shown inside settings popover) ---
|
|
|
|
get sortNewestFirst() {
|
|
return this.page.getByText('Newest first')
|
|
}
|
|
|
|
get sortOldestFirst() {
|
|
return this.page.getByText('Oldest first')
|
|
}
|
|
|
|
// --- Asset cards ---
|
|
|
|
get assetCards() {
|
|
return this.page.locator('[role="button"][data-selected]')
|
|
}
|
|
|
|
getAssetCardByName(name: string) {
|
|
return this.page.locator('[role="button"][data-selected]', {
|
|
hasText: name
|
|
})
|
|
}
|
|
|
|
get selectedCards() {
|
|
return this.page.locator('[data-selected="true"]')
|
|
}
|
|
|
|
// --- List view items ---
|
|
|
|
get listViewItems() {
|
|
return this.page.locator(
|
|
'.sidebar-content-container [role="button"][tabindex="0"]'
|
|
)
|
|
}
|
|
|
|
// --- Selection footer ---
|
|
|
|
get selectionFooter() {
|
|
return this.page
|
|
.locator('.sidebar-content-container')
|
|
.locator('..')
|
|
.locator('[class*="h-18"]')
|
|
}
|
|
|
|
get selectionCountButton() {
|
|
return this.page.getByText(/Assets Selected: \d+/)
|
|
}
|
|
|
|
get deselectAllButton() {
|
|
return this.page.getByText('Deselect all')
|
|
}
|
|
|
|
get deleteSelectedButton() {
|
|
return this.page
|
|
.getByTestId('assets-delete-selected')
|
|
.or(this.page.locator('button:has(.icon-\\[lucide--trash-2\\])').last())
|
|
.first()
|
|
}
|
|
|
|
get downloadSelectedButton() {
|
|
return this.page
|
|
.getByTestId('assets-download-selected')
|
|
.or(this.page.locator('button:has(.icon-\\[lucide--download\\])').last())
|
|
.first()
|
|
}
|
|
|
|
// --- Context menu ---
|
|
|
|
contextMenuItem(label: string) {
|
|
return this.page.locator('.p-contextmenu').getByText(label)
|
|
}
|
|
|
|
// --- Folder view ---
|
|
|
|
get backToAssetsButton() {
|
|
return this.page.getByText('Back to all assets')
|
|
}
|
|
|
|
// --- Loading ---
|
|
|
|
get skeletonLoaders() {
|
|
return this.page.locator('.sidebar-content-container .animate-pulse')
|
|
}
|
|
|
|
// --- Helpers ---
|
|
|
|
override async open() {
|
|
// Remove any toast notifications that may overlay the sidebar button
|
|
await this.dismissToasts()
|
|
await super.open()
|
|
await this.generatedTab.waitFor({ state: 'visible' })
|
|
}
|
|
|
|
/** Dismiss all visible toast notifications by clicking their close buttons. */
|
|
async dismissToasts() {
|
|
const closeButtons = this.page.locator('.p-toast-close-button')
|
|
for (const btn of await closeButtons.all()) {
|
|
await btn.click({ force: true }).catch(() => {})
|
|
}
|
|
// Wait for all toast elements to fully animate out and detach from DOM
|
|
await expect(this.page.locator('.p-toast-message'))
|
|
.toHaveCount(0, { timeout: 5000 })
|
|
.catch(() => {})
|
|
}
|
|
|
|
async switchToImported() {
|
|
await this.dismissToasts()
|
|
await this.importedTab.click()
|
|
await expect(this.importedTab).toHaveAttribute('aria-selected', 'true', {
|
|
timeout: 3000
|
|
})
|
|
}
|
|
|
|
async switchToGenerated() {
|
|
await this.dismissToasts()
|
|
await this.generatedTab.click()
|
|
await expect(this.generatedTab).toHaveAttribute('aria-selected', 'true', {
|
|
timeout: 3000
|
|
})
|
|
}
|
|
|
|
async openSettingsMenu() {
|
|
await this.dismissToasts()
|
|
await this.settingsButton.click()
|
|
// Wait for popover content to render
|
|
await this.listViewOption
|
|
.or(this.gridViewOption)
|
|
.first()
|
|
.waitFor({ state: 'visible', timeout: 3000 })
|
|
}
|
|
|
|
async rightClickAsset(name: string) {
|
|
const card = this.getAssetCardByName(name)
|
|
await card.click({ button: 'right' })
|
|
await this.page
|
|
.locator('.p-contextmenu')
|
|
.waitFor({ state: 'visible', timeout: 3000 })
|
|
}
|
|
|
|
async waitForAssets(count?: number) {
|
|
if (count !== undefined) {
|
|
await expect(this.assetCards).toHaveCount(count, { timeout: 5000 })
|
|
} else {
|
|
await this.assetCards.first().waitFor({ state: 'visible', timeout: 5000 })
|
|
}
|
|
}
|
|
}
|