mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-27 08:25:50 +00:00
Compare commits
7 Commits
jaeone/fe-
...
coderabbit
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef4c751aa1 | ||
|
|
9391663346 | ||
|
|
f84edf4651 | ||
|
|
b7d009bce0 | ||
|
|
475e6927dd | ||
|
|
dcb2a2dbaf | ||
|
|
8cc9e54b9a |
@@ -18,6 +18,8 @@ import { ComfyNodeSearchBoxV2 } from './components/ComfyNodeSearchBoxV2'
|
|||||||
import { ContextMenu } from './components/ContextMenu'
|
import { ContextMenu } from './components/ContextMenu'
|
||||||
import { SettingDialog } from './components/SettingDialog'
|
import { SettingDialog } from './components/SettingDialog'
|
||||||
import { BottomPanel } from './components/BottomPanel'
|
import { BottomPanel } from './components/BottomPanel'
|
||||||
|
import { ConfirmDialog } from './components/ConfirmDialog'
|
||||||
|
import { QueuePanel } from './components/QueuePanel'
|
||||||
import {
|
import {
|
||||||
NodeLibrarySidebarTab,
|
NodeLibrarySidebarTab,
|
||||||
WorkflowsSidebarTab
|
WorkflowsSidebarTab
|
||||||
@@ -38,7 +40,6 @@ import { SubgraphHelper } from './helpers/SubgraphHelper'
|
|||||||
import { ToastHelper } from './helpers/ToastHelper'
|
import { ToastHelper } from './helpers/ToastHelper'
|
||||||
import { WorkflowHelper } from './helpers/WorkflowHelper'
|
import { WorkflowHelper } from './helpers/WorkflowHelper'
|
||||||
import type { NodeReference } from './utils/litegraphUtils'
|
import type { NodeReference } from './utils/litegraphUtils'
|
||||||
import type { WorkspaceStore } from '../types/globals'
|
|
||||||
|
|
||||||
dotenvConfig()
|
dotenvConfig()
|
||||||
|
|
||||||
@@ -111,48 +112,6 @@ class ComfyMenu {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type KeysOfType<T, Match> = {
|
|
||||||
[K in keyof T]: T[K] extends Match ? K : never
|
|
||||||
}[keyof T]
|
|
||||||
|
|
||||||
class ConfirmDialog {
|
|
||||||
private readonly root: Locator
|
|
||||||
public readonly delete: Locator
|
|
||||||
public readonly overwrite: Locator
|
|
||||||
public readonly reject: Locator
|
|
||||||
public readonly confirm: Locator
|
|
||||||
|
|
||||||
constructor(public readonly page: Page) {
|
|
||||||
this.root = page.getByRole('dialog')
|
|
||||||
this.delete = this.root.getByRole('button', { name: 'Delete' })
|
|
||||||
this.overwrite = this.root.getByRole('button', { name: 'Overwrite' })
|
|
||||||
this.reject = this.root.getByRole('button', { name: 'Cancel' })
|
|
||||||
this.confirm = this.root.getByRole('button', { name: 'Confirm' })
|
|
||||||
}
|
|
||||||
|
|
||||||
async click(locator: KeysOfType<ConfirmDialog, Locator>) {
|
|
||||||
const loc = this[locator]
|
|
||||||
await loc.waitFor({ state: 'visible' })
|
|
||||||
await loc.click()
|
|
||||||
|
|
||||||
// Wait for the dialog mask to disappear after confirming
|
|
||||||
const mask = this.page.locator('.p-dialog-mask')
|
|
||||||
const count = await mask.count()
|
|
||||||
if (count > 0) {
|
|
||||||
await mask.first().waitFor({ state: 'hidden', timeout: 3000 })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for workflow service to finish if it's busy
|
|
||||||
await this.page.waitForFunction(
|
|
||||||
() =>
|
|
||||||
(window.app?.extensionManager as WorkspaceStore | undefined)?.workflow
|
|
||||||
?.isBusy === false,
|
|
||||||
undefined,
|
|
||||||
{ timeout: 3000 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ComfyPage {
|
export class ComfyPage {
|
||||||
public readonly url: string
|
public readonly url: string
|
||||||
// All canvas position operations are based on default view of canvas.
|
// All canvas position operations are based on default view of canvas.
|
||||||
@@ -191,6 +150,7 @@ export class ComfyPage {
|
|||||||
public readonly featureFlags: FeatureFlagHelper
|
public readonly featureFlags: FeatureFlagHelper
|
||||||
public readonly command: CommandHelper
|
public readonly command: CommandHelper
|
||||||
public readonly bottomPanel: BottomPanel
|
public readonly bottomPanel: BottomPanel
|
||||||
|
public readonly queuePanel: QueuePanel
|
||||||
public readonly perf: PerformanceHelper
|
public readonly perf: PerformanceHelper
|
||||||
public readonly queue: QueueHelper
|
public readonly queue: QueueHelper
|
||||||
|
|
||||||
@@ -237,6 +197,7 @@ export class ComfyPage {
|
|||||||
this.featureFlags = new FeatureFlagHelper(page)
|
this.featureFlags = new FeatureFlagHelper(page)
|
||||||
this.command = new CommandHelper(page)
|
this.command = new CommandHelper(page)
|
||||||
this.bottomPanel = new BottomPanel(page)
|
this.bottomPanel = new BottomPanel(page)
|
||||||
|
this.queuePanel = new QueuePanel(page)
|
||||||
this.perf = new PerformanceHelper(page)
|
this.perf = new PerformanceHelper(page)
|
||||||
this.queue = new QueueHelper(page)
|
this.queue = new QueueHelper(page)
|
||||||
}
|
}
|
||||||
@@ -510,4 +471,4 @@ export const comfyExpect = expect.extend({
|
|||||||
message: () => `Expected element to ${isFocused ? 'not ' : ''}be focused.`
|
message: () => `Expected element to ${isFocused ? 'not ' : ''}be focused.`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
64
browser_tests/fixtures/components/ConfirmDialog.ts
Normal file
64
browser_tests/fixtures/components/ConfirmDialog.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import type { Locator, Page } from '@playwright/test'
|
||||||
|
|
||||||
|
import type { WorkspaceStore } from '../../types/globals'
|
||||||
|
|
||||||
|
type KeysOfType<T, Match> = {
|
||||||
|
[K in keyof T]: T[K] extends Match ? K : never
|
||||||
|
}[keyof T]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page object for the generic confirm dialog shown via `dialogService.confirm()`.
|
||||||
|
*
|
||||||
|
* Accessible on `comfyPage.confirmDialog`.
|
||||||
|
*/
|
||||||
|
export class ConfirmDialog {
|
||||||
|
readonly root: Locator
|
||||||
|
readonly delete: Locator
|
||||||
|
readonly overwrite: Locator
|
||||||
|
/** Cancel / reject button */
|
||||||
|
readonly reject: Locator
|
||||||
|
/** Primary confirm button */
|
||||||
|
readonly confirm: Locator
|
||||||
|
|
||||||
|
constructor(public readonly page: Page) {
|
||||||
|
this.root = page.getByRole('dialog')
|
||||||
|
this.delete = this.root.getByRole('button', { name: 'Delete' })
|
||||||
|
this.overwrite = this.root.getByRole('button', { name: 'Overwrite' })
|
||||||
|
this.reject = this.root.getByRole('button', { name: 'Cancel' })
|
||||||
|
this.confirm = this.root.getByRole('button', { name: 'Confirm' })
|
||||||
|
}
|
||||||
|
|
||||||
|
async isVisible(): Promise<boolean> {
|
||||||
|
return this.root.isVisible()
|
||||||
|
}
|
||||||
|
|
||||||
|
async waitForVisible(): Promise<void> {
|
||||||
|
await this.root.waitFor({ state: 'visible' })
|
||||||
|
}
|
||||||
|
|
||||||
|
async waitForHidden(): Promise<void> {
|
||||||
|
await this.root.waitFor({ state: 'hidden' })
|
||||||
|
}
|
||||||
|
|
||||||
|
async click(locator: KeysOfType<ConfirmDialog, Locator>) {
|
||||||
|
const loc = this[locator]
|
||||||
|
await loc.waitFor({ state: 'visible' })
|
||||||
|
await loc.click()
|
||||||
|
|
||||||
|
// Wait for the dialog mask to disappear after confirming
|
||||||
|
const mask = this.page.locator('.p-dialog-mask')
|
||||||
|
const count = await mask.count()
|
||||||
|
if (count > 0) {
|
||||||
|
await mask.first().waitFor({ state: 'hidden', timeout: 3000 })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for workflow service to finish if it's busy
|
||||||
|
await this.page.waitForFunction(
|
||||||
|
() =>
|
||||||
|
(window.app?.extensionManager as WorkspaceStore | undefined)?.workflow
|
||||||
|
?.isBusy === false,
|
||||||
|
undefined,
|
||||||
|
{ timeout: 3000 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
56
browser_tests/fixtures/components/QueuePanel.ts
Normal file
56
browser_tests/fixtures/components/QueuePanel.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import type { Locator, Page } from '@playwright/test'
|
||||||
|
|
||||||
|
import { comfyExpect as expect } from '../ComfyPage'
|
||||||
|
import { TestIds } from '../selectors'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page object for the "Clear queue history?" confirmation dialog that opens
|
||||||
|
* from the queue panel's history actions menu.
|
||||||
|
*/
|
||||||
|
export class QueueClearHistoryDialog {
|
||||||
|
readonly root: Locator
|
||||||
|
readonly cancelButton: Locator
|
||||||
|
readonly clearButton: Locator
|
||||||
|
readonly closeButton: Locator
|
||||||
|
|
||||||
|
constructor(public readonly page: Page) {
|
||||||
|
this.root = page.getByRole('dialog')
|
||||||
|
this.cancelButton = this.root.getByRole('button', { name: 'Cancel' })
|
||||||
|
this.clearButton = this.root.getByRole('button', { name: 'Clear' })
|
||||||
|
this.closeButton = this.root.getByLabel('Close')
|
||||||
|
}
|
||||||
|
|
||||||
|
async isVisible(): Promise<boolean> {
|
||||||
|
return this.root.isVisible()
|
||||||
|
}
|
||||||
|
|
||||||
|
async waitForVisible(): Promise<void> {
|
||||||
|
await this.root.waitFor({ state: 'visible' })
|
||||||
|
}
|
||||||
|
|
||||||
|
async waitForHidden(): Promise<void> {
|
||||||
|
await this.root.waitFor({ state: 'hidden' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class QueuePanel {
|
||||||
|
readonly overlayToggle: Locator
|
||||||
|
readonly moreOptionsButton: Locator
|
||||||
|
readonly clearHistoryDialog: QueueClearHistoryDialog
|
||||||
|
|
||||||
|
constructor(readonly page: Page) {
|
||||||
|
this.overlayToggle = page.getByTestId(TestIds.queue.overlayToggle)
|
||||||
|
this.moreOptionsButton = page.getByLabel(/More options/i).first()
|
||||||
|
this.clearHistoryDialog = new QueueClearHistoryDialog(page)
|
||||||
|
}
|
||||||
|
|
||||||
|
async openClearHistoryDialog() {
|
||||||
|
await this.moreOptionsButton.click()
|
||||||
|
|
||||||
|
const clearHistoryAction = this.page.getByTestId(
|
||||||
|
TestIds.queue.clearHistoryAction
|
||||||
|
)
|
||||||
|
await expect(clearHistoryAction).toBeVisible()
|
||||||
|
await clearHistoryAction.click()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -84,6 +84,10 @@ export const TestIds = {
|
|||||||
user: {
|
user: {
|
||||||
currentUserIndicator: 'current-user-indicator'
|
currentUserIndicator: 'current-user-indicator'
|
||||||
},
|
},
|
||||||
|
queue: {
|
||||||
|
overlayToggle: 'queue-overlay-toggle',
|
||||||
|
clearHistoryAction: 'clear-history-action'
|
||||||
|
},
|
||||||
errors: {
|
errors: {
|
||||||
imageLoadError: 'error-loading-image',
|
imageLoadError: 'error-loading-image',
|
||||||
videoLoadError: 'error-loading-video'
|
videoLoadError: 'error-loading-video'
|
||||||
@@ -112,4 +116,5 @@ export type TestIdValue =
|
|||||||
(id: string) => string
|
(id: string) => string
|
||||||
>
|
>
|
||||||
| (typeof TestIds.user)[keyof typeof TestIds.user]
|
| (typeof TestIds.user)[keyof typeof TestIds.user]
|
||||||
|
| (typeof TestIds.queue)[keyof typeof TestIds.queue]
|
||||||
| (typeof TestIds.errors)[keyof typeof TestIds.errors]
|
| (typeof TestIds.errors)[keyof typeof TestIds.errors]
|
||||||
|
|||||||
@@ -18,15 +18,13 @@ test.describe('Confirm dialog text wrapping', { tag: ['@mobile'] }, () => {
|
|||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
}, longFilename)
|
}, longFilename)
|
||||||
|
|
||||||
const dialog = comfyPage.page.getByRole('dialog')
|
const dialog = comfyPage.confirmDialog
|
||||||
await expect(dialog).toBeVisible()
|
await dialog.waitForVisible()
|
||||||
|
|
||||||
const confirmButton = dialog.getByRole('button', { name: 'Confirm' })
|
await expect(dialog.confirm).toBeVisible()
|
||||||
await expect(confirmButton).toBeVisible()
|
await expect(dialog.confirm).toBeInViewport()
|
||||||
await expect(confirmButton).toBeInViewport()
|
|
||||||
|
|
||||||
const cancelButton = dialog.getByRole('button', { name: 'Cancel' })
|
await expect(dialog.reject).toBeVisible()
|
||||||
await expect(cancelButton).toBeVisible()
|
await expect(dialog.reject).toBeInViewport()
|
||||||
await expect(cancelButton).toBeInViewport()
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
137
browser_tests/tests/dialogs/queueClearHistory.spec.ts
Normal file
137
browser_tests/tests/dialogs/queueClearHistory.spec.ts
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
import {
|
||||||
|
comfyPageFixture as test,
|
||||||
|
comfyExpect as expect
|
||||||
|
} from '../../fixtures/ComfyPage'
|
||||||
|
|
||||||
|
test.describe('QueueClearHistoryDialog', { tag: '@ui' }, () => {
|
||||||
|
test.beforeEach(async ({ comfyPage }) => {
|
||||||
|
await comfyPage.setup()
|
||||||
|
|
||||||
|
// Expand the queue overlay so the JobHistoryActionsMenu is visible
|
||||||
|
await comfyPage.queuePanel.overlayToggle.click()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Dialog opens from queue panel history actions menu', async ({
|
||||||
|
comfyPage
|
||||||
|
}) => {
|
||||||
|
await comfyPage.queuePanel.openClearHistoryDialog()
|
||||||
|
|
||||||
|
await expect(comfyPage.queuePanel.clearHistoryDialog.root).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Dialog shows confirmation message with title, description, and assets note', async ({
|
||||||
|
comfyPage
|
||||||
|
}) => {
|
||||||
|
await comfyPage.queuePanel.openClearHistoryDialog()
|
||||||
|
|
||||||
|
const dialog = comfyPage.queuePanel.clearHistoryDialog
|
||||||
|
await expect(dialog.root).toBeVisible()
|
||||||
|
|
||||||
|
// Verify title
|
||||||
|
await expect(
|
||||||
|
dialog.root.getByText('Clear your job queue history?')
|
||||||
|
).toBeVisible()
|
||||||
|
|
||||||
|
// Verify description
|
||||||
|
await expect(
|
||||||
|
dialog.root.getByText(
|
||||||
|
'All the finished or failed jobs below will be removed from this Job queue panel.'
|
||||||
|
)
|
||||||
|
).toBeVisible()
|
||||||
|
|
||||||
|
// Verify assets note (locale uses Unicode RIGHT SINGLE QUOTATION MARK \u2019)
|
||||||
|
await expect(
|
||||||
|
dialog.root.getByText(
|
||||||
|
'Assets generated by these jobs won\u2019t be deleted and can always be viewed from the assets panel.'
|
||||||
|
)
|
||||||
|
).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Cancel button closes dialog without clearing history', async ({
|
||||||
|
comfyPage
|
||||||
|
}) => {
|
||||||
|
await comfyPage.queuePanel.openClearHistoryDialog()
|
||||||
|
|
||||||
|
const dialog = comfyPage.queuePanel.clearHistoryDialog
|
||||||
|
await expect(dialog.root).toBeVisible()
|
||||||
|
|
||||||
|
// Intercept the clear API call — it should NOT be called
|
||||||
|
let clearCalled = false
|
||||||
|
await comfyPage.page.route('**/api/history', (route) => {
|
||||||
|
if (route.request().method() === 'POST') {
|
||||||
|
clearCalled = true
|
||||||
|
}
|
||||||
|
return route.continue()
|
||||||
|
})
|
||||||
|
|
||||||
|
await dialog.cancelButton.click()
|
||||||
|
|
||||||
|
await expect(dialog.root).not.toBeVisible()
|
||||||
|
expect(clearCalled).toBe(false)
|
||||||
|
|
||||||
|
await comfyPage.page.unroute('**/api/history')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Close (X) button closes dialog without clearing history', async ({
|
||||||
|
comfyPage
|
||||||
|
}) => {
|
||||||
|
await comfyPage.queuePanel.openClearHistoryDialog()
|
||||||
|
|
||||||
|
const dialog = comfyPage.queuePanel.clearHistoryDialog
|
||||||
|
await expect(dialog.root).toBeVisible()
|
||||||
|
|
||||||
|
// Intercept the clear API call — it should NOT be called
|
||||||
|
let clearCalled = false
|
||||||
|
await comfyPage.page.route('**/api/history', (route) => {
|
||||||
|
if (route.request().method() === 'POST') {
|
||||||
|
clearCalled = true
|
||||||
|
}
|
||||||
|
return route.continue()
|
||||||
|
})
|
||||||
|
|
||||||
|
await dialog.closeButton.click()
|
||||||
|
|
||||||
|
await expect(dialog.root).not.toBeVisible()
|
||||||
|
expect(clearCalled).toBe(false)
|
||||||
|
|
||||||
|
await comfyPage.page.unroute('**/api/history')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Confirm clears queue history and closes dialog', async ({
|
||||||
|
comfyPage
|
||||||
|
}) => {
|
||||||
|
await comfyPage.queuePanel.openClearHistoryDialog()
|
||||||
|
|
||||||
|
const dialog = comfyPage.queuePanel.clearHistoryDialog
|
||||||
|
await expect(dialog.root).toBeVisible()
|
||||||
|
|
||||||
|
// Intercept the clear API call to verify it is made
|
||||||
|
const clearPromise = comfyPage.page.waitForRequest(
|
||||||
|
(req) => req.url().includes('/api/history') && req.method() === 'POST'
|
||||||
|
)
|
||||||
|
|
||||||
|
await dialog.clearButton.click()
|
||||||
|
|
||||||
|
// Verify the API call was made
|
||||||
|
const request = await clearPromise
|
||||||
|
expect(request.postDataJSON()).toEqual({ clear: true })
|
||||||
|
|
||||||
|
await expect(dialog.root).not.toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Dialog state resets after close and reopen', async ({ comfyPage }) => {
|
||||||
|
// Open and cancel
|
||||||
|
await comfyPage.queuePanel.openClearHistoryDialog()
|
||||||
|
const dialog = comfyPage.queuePanel.clearHistoryDialog
|
||||||
|
await expect(dialog.root).toBeVisible()
|
||||||
|
await dialog.cancelButton.click()
|
||||||
|
await expect(dialog.root).not.toBeVisible()
|
||||||
|
|
||||||
|
// Reopen — dialog should be fresh (Clear button enabled, not stuck)
|
||||||
|
await comfyPage.queuePanel.openClearHistoryDialog()
|
||||||
|
await expect(dialog.root).toBeVisible()
|
||||||
|
|
||||||
|
await expect(dialog.clearButton).toBeVisible()
|
||||||
|
await expect(dialog.clearButton).toBeEnabled()
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user