mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-15 01:48:06 +00:00
Compare commits
10 Commits
test/subgr
...
playwright
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99c16e5843 | ||
|
|
c0bf9a3919 | ||
|
|
6af3bee0c5 | ||
|
|
7a981998b7 | ||
|
|
880362666b | ||
|
|
b7d8bcbe57 | ||
|
|
56f8d6f4ad | ||
|
|
0d41deb747 | ||
|
|
54858da6a2 | ||
|
|
fa1a59959f |
@@ -25,9 +25,11 @@ import {
|
||||
import { Topbar } from './components/Topbar'
|
||||
import { CanvasHelper } from './helpers/CanvasHelper'
|
||||
import { PerformanceHelper } from './helpers/PerformanceHelper'
|
||||
import { QueueHelper } from './helpers/QueueHelper'
|
||||
import { ClipboardHelper } from './helpers/ClipboardHelper'
|
||||
import { CommandHelper } from './helpers/CommandHelper'
|
||||
import { DragDropHelper } from './helpers/DragDropHelper'
|
||||
import { FeatureFlagHelper } from './helpers/FeatureFlagHelper'
|
||||
import { KeyboardHelper } from './helpers/KeyboardHelper'
|
||||
import { NodeOperationsHelper } from './helpers/NodeOperationsHelper'
|
||||
import { SettingsHelper } from './helpers/SettingsHelper'
|
||||
@@ -184,9 +186,11 @@ export class ComfyPage {
|
||||
public readonly contextMenu: ContextMenu
|
||||
public readonly toast: ToastHelper
|
||||
public readonly dragDrop: DragDropHelper
|
||||
public readonly featureFlags: FeatureFlagHelper
|
||||
public readonly command: CommandHelper
|
||||
public readonly bottomPanel: BottomPanel
|
||||
public readonly perf: PerformanceHelper
|
||||
public readonly queue: QueueHelper
|
||||
|
||||
/** Worker index to test user ID */
|
||||
public readonly userIds: string[] = []
|
||||
@@ -227,9 +231,11 @@ export class ComfyPage {
|
||||
this.contextMenu = new ContextMenu(page)
|
||||
this.toast = new ToastHelper(page)
|
||||
this.dragDrop = new DragDropHelper(page, this.assetPath.bind(this))
|
||||
this.featureFlags = new FeatureFlagHelper(page)
|
||||
this.command = new CommandHelper(page)
|
||||
this.bottomPanel = new BottomPanel(page)
|
||||
this.perf = new PerformanceHelper(page)
|
||||
this.queue = new QueueHelper(page)
|
||||
}
|
||||
|
||||
get visibleToasts() {
|
||||
|
||||
73
browser_tests/fixtures/helpers/FeatureFlagHelper.ts
Normal file
73
browser_tests/fixtures/helpers/FeatureFlagHelper.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import type { Page, Route } from '@playwright/test'
|
||||
|
||||
export class FeatureFlagHelper {
|
||||
private featuresRouteHandler: ((route: Route) => void) | null = null
|
||||
|
||||
constructor(private readonly page: Page) {}
|
||||
|
||||
/**
|
||||
* Seed feature flags via `addInitScript` so they are available in
|
||||
* localStorage before the app JS executes on first load.
|
||||
* Must be called before `comfyPage.setup()` / `page.goto()`.
|
||||
*
|
||||
* Note: Playwright init scripts persist for the page lifetime and
|
||||
* cannot be removed. Call this once per test, before navigation.
|
||||
*/
|
||||
async seedFlags(flags: Record<string, unknown>): Promise<void> {
|
||||
await this.page.addInitScript((flagMap: Record<string, unknown>) => {
|
||||
for (const [key, value] of Object.entries(flagMap)) {
|
||||
localStorage.setItem(`ff:${key}`, JSON.stringify(value))
|
||||
}
|
||||
}, flags)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set feature flags at runtime via localStorage. Uses the `ff:` prefix
|
||||
* that devFeatureFlagOverride.ts reads in dev mode.
|
||||
* For flags needed before page init, use `seedFlags()` instead.
|
||||
*/
|
||||
async setFlags(flags: Record<string, unknown>): Promise<void> {
|
||||
await this.page.evaluate((flagMap: Record<string, unknown>) => {
|
||||
for (const [key, value] of Object.entries(flagMap)) {
|
||||
localStorage.setItem(`ff:${key}`, JSON.stringify(value))
|
||||
}
|
||||
}, flags)
|
||||
}
|
||||
|
||||
async setFlag(name: string, value: unknown): Promise<void> {
|
||||
await this.setFlags({ [name]: value })
|
||||
}
|
||||
|
||||
async clearFlags(): Promise<void> {
|
||||
await this.page.evaluate(() => {
|
||||
const keysToRemove: string[] = []
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i)
|
||||
if (key?.startsWith('ff:')) keysToRemove.push(key)
|
||||
}
|
||||
keysToRemove.forEach((k) => {
|
||||
localStorage.removeItem(k)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock server feature flags via route interception on /api/features.
|
||||
*/
|
||||
async mockServerFeatures(features: Record<string, unknown>): Promise<void> {
|
||||
this.featuresRouteHandler = (route: Route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(features)
|
||||
})
|
||||
await this.page.route('**/api/features', this.featuresRouteHandler)
|
||||
}
|
||||
|
||||
async clearMocks(): Promise<void> {
|
||||
if (this.featuresRouteHandler) {
|
||||
await this.page.unroute('**/api/features', this.featuresRouteHandler)
|
||||
this.featuresRouteHandler = null
|
||||
}
|
||||
}
|
||||
}
|
||||
79
browser_tests/fixtures/helpers/QueueHelper.ts
Normal file
79
browser_tests/fixtures/helpers/QueueHelper.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import type { Page, Route } from '@playwright/test'
|
||||
|
||||
export class QueueHelper {
|
||||
private queueRouteHandler: ((route: Route) => void) | null = null
|
||||
private historyRouteHandler: ((route: Route) => void) | null = null
|
||||
|
||||
constructor(private readonly page: Page) {}
|
||||
|
||||
/**
|
||||
* Mock the /api/queue endpoint to return specific queue state.
|
||||
*/
|
||||
async mockQueueState(
|
||||
running: number = 0,
|
||||
pending: number = 0
|
||||
): Promise<void> {
|
||||
this.queueRouteHandler = (route: Route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
queue_running: Array.from({ length: running }, (_, i) => [
|
||||
i,
|
||||
`running-${i}`,
|
||||
{},
|
||||
{},
|
||||
[]
|
||||
]),
|
||||
queue_pending: Array.from({ length: pending }, (_, i) => [
|
||||
i,
|
||||
`pending-${i}`,
|
||||
{},
|
||||
{},
|
||||
[]
|
||||
])
|
||||
})
|
||||
})
|
||||
await this.page.route('**/api/queue', this.queueRouteHandler)
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock the /api/history endpoint with completed/failed job entries.
|
||||
*/
|
||||
async mockHistory(
|
||||
jobs: Array<{ promptId: string; status: 'success' | 'error' }>
|
||||
): Promise<void> {
|
||||
const history: Record<string, unknown> = {}
|
||||
for (const job of jobs) {
|
||||
history[job.promptId] = {
|
||||
prompt: [0, job.promptId, {}, {}, []],
|
||||
outputs: {},
|
||||
status: {
|
||||
status_str: job.status === 'success' ? 'success' : 'error',
|
||||
completed: true
|
||||
}
|
||||
}
|
||||
}
|
||||
this.historyRouteHandler = (route: Route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(history)
|
||||
})
|
||||
await this.page.route('**/api/history**', this.historyRouteHandler)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all route mocks set by this helper.
|
||||
*/
|
||||
async clearMocks(): Promise<void> {
|
||||
if (this.queueRouteHandler) {
|
||||
await this.page.unroute('**/api/queue', this.queueRouteHandler)
|
||||
this.queueRouteHandler = null
|
||||
}
|
||||
if (this.historyRouteHandler) {
|
||||
await this.page.unroute('**/api/history**', this.historyRouteHandler)
|
||||
this.historyRouteHandler = null
|
||||
}
|
||||
}
|
||||
}
|
||||
148
browser_tests/tests/errorsTab.spec.ts
Normal file
148
browser_tests/tests/errorsTab.spec.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
import type { Page } from '@playwright/test'
|
||||
|
||||
import {
|
||||
comfyPageFixture as test,
|
||||
comfyExpect as expect
|
||||
} from '../fixtures/ComfyPage'
|
||||
|
||||
async function triggerExecutionError(comfyPage: {
|
||||
canvasOps: { disconnectEdge: () => Promise<void> }
|
||||
page: Page
|
||||
command: { executeCommand: (cmd: string) => Promise<void> }
|
||||
}) {
|
||||
await comfyPage.canvasOps.disconnectEdge()
|
||||
await comfyPage.page.keyboard.press('Escape')
|
||||
await comfyPage.command.executeCommand('Comfy.QueuePrompt')
|
||||
}
|
||||
|
||||
test.describe('Errors tab in right side panel', { tag: '@ui' }, () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
await comfyPage.settings.setSetting(
|
||||
'Comfy.RightSidePanel.ShowErrorsTab',
|
||||
true
|
||||
)
|
||||
await comfyPage.setup()
|
||||
})
|
||||
|
||||
test('Errors tab appears after execution error', async ({ comfyPage }) => {
|
||||
await triggerExecutionError(comfyPage)
|
||||
|
||||
// Dismiss the error overlay
|
||||
const overlay = comfyPage.page.locator('[data-testid="error-overlay"]')
|
||||
await expect(overlay).toBeVisible()
|
||||
await overlay.getByRole('button', { name: /Dismiss/i }).click()
|
||||
|
||||
await comfyPage.actionbar.propertiesButton.click()
|
||||
const { propertiesPanel } = comfyPage.menu
|
||||
|
||||
await expect(propertiesPanel.root).toBeVisible()
|
||||
await expect(
|
||||
propertiesPanel.root.getByRole('tab', { name: 'Errors' })
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('Error card shows locate button', async ({ comfyPage }) => {
|
||||
await triggerExecutionError(comfyPage)
|
||||
|
||||
const overlay = comfyPage.page.locator('[data-testid="error-overlay"]')
|
||||
await expect(overlay).toBeVisible()
|
||||
await overlay.getByRole('button', { name: /See Errors/i }).click()
|
||||
|
||||
const { propertiesPanel } = comfyPage.menu
|
||||
await expect(propertiesPanel.root).toBeVisible()
|
||||
|
||||
const locateButton = propertiesPanel.root.locator(
|
||||
'button .icon-\\[lucide--locate\\]'
|
||||
)
|
||||
await expect(locateButton.first()).toBeVisible()
|
||||
})
|
||||
|
||||
test('Clicking locate button focuses canvas', async ({ comfyPage }) => {
|
||||
await triggerExecutionError(comfyPage)
|
||||
|
||||
const overlay = comfyPage.page.locator('[data-testid="error-overlay"]')
|
||||
await expect(overlay).toBeVisible()
|
||||
await overlay.getByRole('button', { name: /See Errors/i }).click()
|
||||
|
||||
const { propertiesPanel } = comfyPage.menu
|
||||
await expect(propertiesPanel.root).toBeVisible()
|
||||
|
||||
const locateButton = propertiesPanel.root
|
||||
.locator('button')
|
||||
.filter({ has: comfyPage.page.locator('.icon-\\[lucide--locate\\]') })
|
||||
.first()
|
||||
await locateButton.click()
|
||||
|
||||
await expect(comfyPage.canvas).toBeVisible()
|
||||
})
|
||||
|
||||
test('Collapse all button collapses error groups', async ({ comfyPage }) => {
|
||||
await triggerExecutionError(comfyPage)
|
||||
|
||||
const overlay = comfyPage.page.locator('[data-testid="error-overlay"]')
|
||||
await expect(overlay).toBeVisible()
|
||||
await overlay.getByRole('button', { name: /See Errors/i }).click()
|
||||
|
||||
const { propertiesPanel } = comfyPage.menu
|
||||
await expect(propertiesPanel.root).toBeVisible()
|
||||
|
||||
const collapseButton = propertiesPanel.root.getByRole('button', {
|
||||
name: 'Collapse all'
|
||||
})
|
||||
|
||||
// The collapse toggle only appears when there are multiple groups.
|
||||
// If only one group exists, this test verifies the button is not shown.
|
||||
const count = await collapseButton.count()
|
||||
if (count > 0) {
|
||||
await collapseButton.click()
|
||||
const expandButton = propertiesPanel.root.getByRole('button', {
|
||||
name: 'Expand all'
|
||||
})
|
||||
await expect(expandButton).toBeVisible()
|
||||
}
|
||||
})
|
||||
|
||||
test('Search filters errors', async ({ comfyPage }) => {
|
||||
await triggerExecutionError(comfyPage)
|
||||
|
||||
const overlay = comfyPage.page.locator('[data-testid="error-overlay"]')
|
||||
await expect(overlay).toBeVisible()
|
||||
await overlay.getByRole('button', { name: /See Errors/i }).click()
|
||||
|
||||
const { propertiesPanel } = comfyPage.menu
|
||||
await expect(propertiesPanel.root).toBeVisible()
|
||||
|
||||
// Search for a term that won't match any error
|
||||
await propertiesPanel.searchBox.fill('zzz_nonexistent_zzz')
|
||||
|
||||
await expect(
|
||||
propertiesPanel.root.locator('.icon-\\[lucide--locate\\]')
|
||||
).toHaveCount(0)
|
||||
|
||||
// Clear the search to restore results
|
||||
await propertiesPanel.searchBox.fill('')
|
||||
|
||||
await expect(
|
||||
propertiesPanel.root.locator('.icon-\\[lucide--locate\\]').first()
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('Errors tab shows error message text', async ({ comfyPage }) => {
|
||||
await triggerExecutionError(comfyPage)
|
||||
|
||||
const overlay = comfyPage.page.locator('[data-testid="error-overlay"]')
|
||||
await expect(overlay).toBeVisible()
|
||||
await overlay.getByRole('button', { name: /See Errors/i }).click()
|
||||
|
||||
const { propertiesPanel } = comfyPage.menu
|
||||
await expect(propertiesPanel.root).toBeVisible()
|
||||
|
||||
// Error cards contain <p> elements with error messages
|
||||
const errorMessage = propertiesPanel.root
|
||||
.locator('.whitespace-pre-wrap')
|
||||
.first()
|
||||
await expect(errorMessage).toBeVisible()
|
||||
await expect(errorMessage).not.toHaveText('')
|
||||
})
|
||||
})
|
||||
@@ -60,6 +60,15 @@ test.describe('Graph Canvas Menu', { tag: ['@screenshot', '@canvas'] }, () => {
|
||||
await comfyPage.nextFrame()
|
||||
})
|
||||
|
||||
test('Fit view button is present and clickable', async ({ comfyPage }) => {
|
||||
const fitViewButton = comfyPage.page
|
||||
.locator('button')
|
||||
.filter({ has: comfyPage.page.locator('.icon-\\[lucide--focus\\]') })
|
||||
await expect(fitViewButton).toBeVisible()
|
||||
await fitViewButton.click()
|
||||
await comfyPage.nextFrame()
|
||||
})
|
||||
|
||||
test('Zoom controls popup opens and closes', async ({ comfyPage }) => {
|
||||
// Find the zoom button by its percentage text content
|
||||
const zoomButton = comfyPage.page.locator('button').filter({
|
||||
|
||||
110
browser_tests/tests/nodeLibraryEssentials.spec.ts
Normal file
110
browser_tests/tests/nodeLibraryEssentials.spec.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Node Library Essentials Tab', { tag: '@ui' }, () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
|
||||
// Enable the essentials feature flag via the reactive serverFeatureFlags ref.
|
||||
// In production, this flag comes via WebSocket or remoteConfig (cloud only).
|
||||
// The localhost test server has neither, so we set it directly.
|
||||
await comfyPage.page.evaluate(() => {
|
||||
window.app!.api.serverFeatureFlags.value = {
|
||||
...window.app!.api.serverFeatureFlags.value,
|
||||
node_library_essentials_enabled: true
|
||||
}
|
||||
})
|
||||
|
||||
// Register a mock essential node so the essentials tab has content.
|
||||
await comfyPage.page.evaluate(() => {
|
||||
return window.app!.registerNodeDef('TestEssentialNode', {
|
||||
name: 'TestEssentialNode',
|
||||
display_name: 'Test Essential Node',
|
||||
category: 'essentials_test',
|
||||
input: { required: {}, optional: {} },
|
||||
output: ['IMAGE'],
|
||||
output_name: ['image'],
|
||||
output_is_list: [false],
|
||||
output_node: false,
|
||||
python_module: 'comfy_essentials.nodes',
|
||||
description: 'Mock essential node for testing',
|
||||
essentials_category: 'Image Generation'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('Node library opens via sidebar', async ({ comfyPage }) => {
|
||||
const tabButton = comfyPage.page.locator('.node-library-tab-button')
|
||||
await tabButton.click()
|
||||
|
||||
const sidebarContent = comfyPage.page.locator(
|
||||
'.comfy-vue-side-bar-container'
|
||||
)
|
||||
await expect(sidebarContent).toBeVisible()
|
||||
})
|
||||
|
||||
test('Essentials tab is visible in node library', async ({ comfyPage }) => {
|
||||
const tabButton = comfyPage.page.locator('.node-library-tab-button')
|
||||
await tabButton.click()
|
||||
|
||||
const essentialsTab = comfyPage.page.getByRole('tab', {
|
||||
name: /essentials/i
|
||||
})
|
||||
await expect(essentialsTab).toBeVisible()
|
||||
})
|
||||
|
||||
test('Clicking essentials tab shows essential node cards', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const tabButton = comfyPage.page.locator('.node-library-tab-button')
|
||||
await tabButton.click()
|
||||
|
||||
const essentialsTab = comfyPage.page.getByRole('tab', {
|
||||
name: /essentials/i
|
||||
})
|
||||
await essentialsTab.click()
|
||||
|
||||
const essentialCards = comfyPage.page.locator('[data-node-name]')
|
||||
await expect(essentialCards.first()).toBeVisible()
|
||||
})
|
||||
|
||||
test('Essential node cards have node names', async ({ comfyPage }) => {
|
||||
const tabButton = comfyPage.page.locator('.node-library-tab-button')
|
||||
await tabButton.click()
|
||||
|
||||
const essentialsTab = comfyPage.page.getByRole('tab', {
|
||||
name: /essentials/i
|
||||
})
|
||||
await essentialsTab.click()
|
||||
|
||||
const firstCard = comfyPage.page.locator('[data-node-name]').first()
|
||||
await expect(firstCard).toBeVisible()
|
||||
|
||||
const nodeName = await firstCard.getAttribute('data-node-name')
|
||||
expect(nodeName).toBeTruthy()
|
||||
expect(nodeName!.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
test('Node library can switch between all and essentials tabs', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const tabButton = comfyPage.page.locator('.node-library-tab-button')
|
||||
await tabButton.click()
|
||||
|
||||
const essentialsTab = comfyPage.page.getByRole('tab', {
|
||||
name: /essentials/i
|
||||
})
|
||||
const allNodesTab = comfyPage.page.getByRole('tab', { name: /^all$/i })
|
||||
|
||||
await essentialsTab.click()
|
||||
await expect(essentialsTab).toHaveAttribute('aria-selected', 'true')
|
||||
const essentialCards = comfyPage.page.locator('[data-node-name]')
|
||||
await expect(essentialCards.first()).toBeVisible()
|
||||
|
||||
await allNodesTab.click()
|
||||
await expect(allNodesTab).toHaveAttribute('aria-selected', 'true')
|
||||
await expect(essentialsTab).toHaveAttribute('aria-selected', 'false')
|
||||
})
|
||||
})
|
||||
67
browser_tests/tests/queueNotificationBanners.spec.ts
Normal file
67
browser_tests/tests/queueNotificationBanners.spec.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import {
|
||||
comfyPageFixture as test,
|
||||
comfyExpect as expect
|
||||
} from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Queue Notification Banners', { tag: '@ui' }, () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
await comfyPage.setup()
|
||||
})
|
||||
|
||||
test('Banner appears when prompt is queued', async ({ comfyPage }) => {
|
||||
await comfyPage.command.executeCommand('Comfy.QueuePrompt')
|
||||
|
||||
const banner = comfyPage.page.locator('[role="status"][aria-live="polite"]')
|
||||
await expect(banner).toBeVisible()
|
||||
})
|
||||
|
||||
test('Banner shows queuing text initially', async ({ comfyPage }) => {
|
||||
await comfyPage.command.executeCommand('Comfy.QueuePrompt')
|
||||
|
||||
const banner = comfyPage.page.locator('[role="status"][aria-live="polite"]')
|
||||
await expect(banner).toBeVisible()
|
||||
await expect(banner).toContainText(/Job queuing|Job queued/)
|
||||
})
|
||||
|
||||
test('Error overlay appears on failed execution', async ({ comfyPage }) => {
|
||||
await comfyPage.canvasOps.disconnectEdge()
|
||||
await comfyPage.page.keyboard.press('Escape')
|
||||
await comfyPage.command.executeCommand('Comfy.QueuePrompt')
|
||||
|
||||
const errorOverlay = comfyPage.page.locator('[data-testid="error-overlay"]')
|
||||
await expect(errorOverlay).toBeVisible()
|
||||
})
|
||||
|
||||
test('Error overlay contains error description', async ({ comfyPage }) => {
|
||||
await comfyPage.canvasOps.disconnectEdge()
|
||||
await comfyPage.page.keyboard.press('Escape')
|
||||
await comfyPage.command.executeCommand('Comfy.QueuePrompt')
|
||||
|
||||
const errorOverlay = comfyPage.page.locator('[data-testid="error-overlay"]')
|
||||
await expect(errorOverlay).toBeVisible()
|
||||
await expect(errorOverlay).not.toHaveText('')
|
||||
})
|
||||
|
||||
test('Error overlay can be dismissed', async ({ comfyPage }) => {
|
||||
await comfyPage.canvasOps.disconnectEdge()
|
||||
await comfyPage.page.keyboard.press('Escape')
|
||||
await comfyPage.command.executeCommand('Comfy.QueuePrompt')
|
||||
|
||||
const errorOverlay = comfyPage.page.locator('[data-testid="error-overlay"]')
|
||||
await expect(errorOverlay).toBeVisible()
|
||||
|
||||
await errorOverlay.getByRole('button', { name: /Dismiss/i }).click()
|
||||
await expect(errorOverlay).toBeHidden()
|
||||
})
|
||||
|
||||
test('Banner auto-dismisses after display', async ({ comfyPage }) => {
|
||||
await comfyPage.command.executeCommand('Comfy.QueuePrompt')
|
||||
|
||||
const banner = comfyPage.page.locator('[role="status"][aria-live="polite"]')
|
||||
await expect(banner).toBeVisible()
|
||||
|
||||
// Banner auto-dismisses after ~4 seconds
|
||||
await expect(banner).toBeHidden({ timeout: 10000 })
|
||||
})
|
||||
})
|
||||
57
browser_tests/tests/settingsSidebar.spec.ts
Normal file
57
browser_tests/tests/settingsSidebar.spec.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Settings Sidebar', { tag: '@ui' }, () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
await comfyPage.setup()
|
||||
})
|
||||
|
||||
test('Settings button is visible in sidebar', async ({ comfyPage }) => {
|
||||
await expect(comfyPage.menu.sideToolbar).toBeVisible()
|
||||
const settingsButton = comfyPage.menu.sideToolbar.getByLabel(/settings/i)
|
||||
await expect(settingsButton).toBeVisible()
|
||||
})
|
||||
|
||||
test('Clicking settings button opens settings dialog', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const settingsButton = comfyPage.menu.sideToolbar.getByLabel(/settings/i)
|
||||
await settingsButton.click()
|
||||
await expect(comfyPage.settingDialog.root).toBeVisible()
|
||||
})
|
||||
|
||||
test('Settings dialog shows categories', async ({ comfyPage }) => {
|
||||
const settingsButton = comfyPage.menu.sideToolbar.getByLabel(/settings/i)
|
||||
await settingsButton.click()
|
||||
await expect(comfyPage.settingDialog.root).toBeVisible()
|
||||
await expect(comfyPage.settingDialog.categories.first()).toBeVisible()
|
||||
expect(await comfyPage.settingDialog.categories.count()).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
test('Settings dialog can be closed with Escape', async ({ comfyPage }) => {
|
||||
const settingsButton = comfyPage.menu.sideToolbar.getByLabel(/settings/i)
|
||||
await settingsButton.click()
|
||||
await expect(comfyPage.settingDialog.root).toBeVisible()
|
||||
await comfyPage.page.keyboard.press('Escape')
|
||||
await expect(comfyPage.settingDialog.root).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('Settings search box is functional', async ({ comfyPage }) => {
|
||||
const settingsButton = comfyPage.menu.sideToolbar.getByLabel(/settings/i)
|
||||
await settingsButton.click()
|
||||
await expect(comfyPage.settingDialog.root).toBeVisible()
|
||||
await comfyPage.settingDialog.searchBox.fill('color')
|
||||
await expect(comfyPage.settingDialog.searchBox).toHaveValue('color')
|
||||
})
|
||||
|
||||
test('Settings dialog can navigate to About panel', async ({ comfyPage }) => {
|
||||
const settingsButton = comfyPage.menu.sideToolbar.getByLabel(/settings/i)
|
||||
await settingsButton.click()
|
||||
await expect(comfyPage.settingDialog.root).toBeVisible()
|
||||
await comfyPage.settingDialog.goToAboutPanel()
|
||||
await expect(comfyPage.page.locator('.about-container')).toBeVisible()
|
||||
})
|
||||
})
|
||||
69
browser_tests/tests/vueNodes/nodeHeaderActions.spec.ts
Normal file
69
browser_tests/tests/vueNodes/nodeHeaderActions.spec.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Vue Node Header Actions', { tag: '@node' }, () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
await comfyPage.settings.setSetting('Comfy.Graph.CanvasMenu', false)
|
||||
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
|
||||
await comfyPage.setup()
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
})
|
||||
|
||||
test('Collapse button is visible on node header', async ({ comfyPage }) => {
|
||||
const vueNode = await comfyPage.vueNodes.getFixtureByTitle('KSampler')
|
||||
await expect(vueNode.collapseButton).toBeVisible()
|
||||
})
|
||||
|
||||
test('Clicking collapse button hides node body', async ({ comfyPage }) => {
|
||||
const vueNode = await comfyPage.vueNodes.getFixtureByTitle('KSampler')
|
||||
await expect(vueNode.body).toBeVisible()
|
||||
|
||||
await vueNode.toggleCollapse()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await expect(vueNode.body).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('Clicking collapse button again expands node', async ({ comfyPage }) => {
|
||||
const vueNode = await comfyPage.vueNodes.getFixtureByTitle('KSampler')
|
||||
|
||||
await vueNode.toggleCollapse()
|
||||
await comfyPage.nextFrame()
|
||||
await expect(vueNode.body).not.toBeVisible()
|
||||
|
||||
await vueNode.toggleCollapse()
|
||||
await comfyPage.nextFrame()
|
||||
await expect(vueNode.body).toBeVisible()
|
||||
})
|
||||
|
||||
test('Double-click header enters title edit mode', async ({ comfyPage }) => {
|
||||
const vueNode = await comfyPage.vueNodes.getFixtureByTitle('KSampler')
|
||||
|
||||
await vueNode.header.dblclick()
|
||||
await expect(vueNode.titleInput).toBeVisible()
|
||||
})
|
||||
|
||||
test('Title edit saves on Enter', async ({ comfyPage }) => {
|
||||
const vueNode = await comfyPage.vueNodes.getFixtureByTitle('KSampler')
|
||||
|
||||
await vueNode.setTitle('My Custom Sampler')
|
||||
expect(await vueNode.getTitle()).toBe('My Custom Sampler')
|
||||
})
|
||||
|
||||
test('Title edit cancels on Escape', async ({ comfyPage }) => {
|
||||
const vueNode = await comfyPage.vueNodes.getFixtureByTitle('KSampler')
|
||||
|
||||
await vueNode.setTitle('Renamed Sampler')
|
||||
expect(await vueNode.getTitle()).toBe('Renamed Sampler')
|
||||
|
||||
await vueNode.header.dblclick()
|
||||
await vueNode.titleInput.fill('This Should Be Cancelled')
|
||||
await vueNode.titleInput.press('Escape')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
expect(await vueNode.getTitle()).toBe('Renamed Sampler')
|
||||
})
|
||||
})
|
||||
66
browser_tests/tests/widgetCopyButton.spec.ts
Normal file
66
browser_tests/tests/widgetCopyButton.spec.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Widget copy button', { tag: '@ui' }, () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
|
||||
await comfyPage.setup()
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
})
|
||||
|
||||
test('Vue nodes render with widgets', async ({ comfyPage }) => {
|
||||
const nodeCount = await comfyPage.vueNodes.getNodeCount()
|
||||
expect(nodeCount).toBeGreaterThan(0)
|
||||
|
||||
const firstNode = comfyPage.vueNodes.nodes.first()
|
||||
await expect(firstNode).toBeVisible()
|
||||
})
|
||||
|
||||
test('Textarea widgets exist on nodes', async ({ comfyPage }) => {
|
||||
const textareas = comfyPage.page.locator('[data-node-id] textarea')
|
||||
await expect(textareas.first()).toBeVisible()
|
||||
expect(await textareas.count()).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
test('Copy button has correct aria-label', async ({ comfyPage }) => {
|
||||
const copyButton = comfyPage.page
|
||||
.locator('[data-node-id] button[aria-label]')
|
||||
.filter({ has: comfyPage.page.locator('.icon-\\[lucide--copy\\]') })
|
||||
.first()
|
||||
await expect(copyButton).toBeAttached()
|
||||
await expect(copyButton).toHaveAttribute('aria-label', /copy/i)
|
||||
})
|
||||
|
||||
test('Copy icon uses lucide copy class', async ({ comfyPage }) => {
|
||||
const copyIcon = comfyPage.page
|
||||
.locator('[data-node-id] .icon-\\[lucide--copy\\]')
|
||||
.first()
|
||||
await expect(copyIcon).toBeAttached()
|
||||
})
|
||||
|
||||
test('Widget container has group class for hover', async ({ comfyPage }) => {
|
||||
const textarea = comfyPage.page.locator('[data-node-id] textarea').first()
|
||||
await expect(textarea).toBeVisible()
|
||||
const container = textarea.locator('..')
|
||||
await expect(container).toHaveClass(/group/)
|
||||
})
|
||||
|
||||
test('Copy button exists within textarea widget group container', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const container = comfyPage.page
|
||||
.locator('[data-node-id] div.group:has(textarea)')
|
||||
.first()
|
||||
await expect(container).toBeVisible()
|
||||
await container.hover()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const copyButton = container.locator('button').filter({
|
||||
has: comfyPage.page.locator('.icon-\\[lucide--copy\\]')
|
||||
})
|
||||
await expect(copyButton.first()).toBeAttached()
|
||||
})
|
||||
})
|
||||
@@ -7,7 +7,7 @@
|
||||
<!-- Video Wrapper -->
|
||||
<div
|
||||
ref="videoWrapperEl"
|
||||
class="relative flex flex-1 overflow-hidden rounded-[5px] bg-transparent"
|
||||
class="relative flex flex-1 overflow-hidden rounded-[5px] bg-node-component-surface"
|
||||
tabindex="0"
|
||||
role="region"
|
||||
:aria-label="$t('g.videoPreview')"
|
||||
@@ -203,6 +203,7 @@ const handleDownload = () => {
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: t('g.failedToDownloadVideo'),
|
||||
life: 3000,
|
||||
group: 'video-preview'
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user