mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-06-26 01:27:23 +00:00
Compare commits
3 Commits
DynamicGro
...
glary/enab
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82f873b1a9 | ||
|
|
46d1e0bcad | ||
|
|
0f549a9729 |
@@ -27,6 +27,7 @@
|
||||
],
|
||||
"rules": {
|
||||
"no-async-promise-executor": "off",
|
||||
"curly": ["error", "multi-or-nest", "consistent"],
|
||||
"no-console": [
|
||||
"error",
|
||||
{
|
||||
|
||||
@@ -278,9 +278,8 @@ export class ComfyPage {
|
||||
}
|
||||
)
|
||||
|
||||
if (resp.status() !== 200) {
|
||||
if (resp.status() !== 200)
|
||||
throw new Error(`Failed to setup settings: ${await resp.text()}`)
|
||||
}
|
||||
}
|
||||
|
||||
async setup({
|
||||
@@ -358,9 +357,7 @@ export class ComfyPage {
|
||||
}
|
||||
|
||||
async idleFrames(count: number) {
|
||||
for (let i = 0; i < count; i++) {
|
||||
await this.nextFrame()
|
||||
}
|
||||
for (let i = 0; i < count; i++) await this.nextFrame()
|
||||
}
|
||||
|
||||
async delay(ms: number) {
|
||||
@@ -382,9 +379,7 @@ export class ComfyPage {
|
||||
const { runInCI = false, fullPage = false } = options
|
||||
|
||||
// Skip in CI unless explicitly requested
|
||||
if (process.env.CI && !runInCI) {
|
||||
return
|
||||
}
|
||||
if (process.env.CI && !runInCI) return
|
||||
|
||||
const testInfo = comfyPageFixture.info()
|
||||
await testInfo.attach(name, {
|
||||
@@ -486,9 +481,7 @@ export const comfyPageFixture = base.extend<{
|
||||
initialSettings: [{}, { option: true }],
|
||||
|
||||
page: async ({ page, browserName }, use) => {
|
||||
if (browserName !== 'chromium' || !COLLECT_COVERAGE) {
|
||||
return use(page)
|
||||
}
|
||||
if (browserName !== 'chromium' || !COLLECT_COVERAGE) return use(page)
|
||||
|
||||
await page.coverage.startJSCoverage({ resetOnNavigation: false })
|
||||
await use(page)
|
||||
@@ -547,19 +540,14 @@ export const comfyPageFixture = base.extend<{
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
if (testInfo.tags.includes('@cloud')) {
|
||||
await comfyPage.cloudAuth.mockAuth()
|
||||
}
|
||||
if (testInfo.tags.includes('@cloud')) await comfyPage.cloudAuth.mockAuth()
|
||||
|
||||
if (Object.keys(initialFeatureFlags).length > 0) {
|
||||
if (Object.keys(initialFeatureFlags).length > 0)
|
||||
await comfyPage.featureFlags.seedFlags(initialFeatureFlags)
|
||||
}
|
||||
|
||||
await comfyPage.setup()
|
||||
|
||||
if (isVueNodes) {
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
}
|
||||
if (isVueNodes) await comfyPage.vueNodes.waitForNodes()
|
||||
|
||||
const needsPerf =
|
||||
testInfo.tags.includes('@perf') || testInfo.tags.includes('@audit')
|
||||
|
||||
@@ -42,9 +42,8 @@ class ComfyQueueButton {
|
||||
|
||||
public async openOptions() {
|
||||
const options = new ComfyQueueButtonOptions(this.actionbar.page)
|
||||
if (!(await options.menu.isVisible())) {
|
||||
await this.dropdownButton.click()
|
||||
}
|
||||
if (!(await options.menu.isVisible())) await this.dropdownButton.click()
|
||||
|
||||
return options
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,16 +67,14 @@ export class BottomPanel {
|
||||
async toggleLogs() {
|
||||
await this.toggleButton.click()
|
||||
await this.logs.tab.waitFor({ state: 'visible' })
|
||||
if ((await this.logs.tab.getAttribute('aria-selected')) !== 'true') {
|
||||
if ((await this.logs.tab.getAttribute('aria-selected')) !== 'true')
|
||||
await this.logs.tab.click()
|
||||
}
|
||||
}
|
||||
|
||||
async resizeByDragging(deltaY: number): Promise<void> {
|
||||
const gutterBox = await this.resizeGutter.boundingBox()
|
||||
if (!gutterBox) {
|
||||
if (!gutterBox)
|
||||
throw new Error('Bottom panel resize gutter should have layout')
|
||||
}
|
||||
|
||||
const gutterCenterX = gutterBox.x + gutterBox.width / 2
|
||||
const gutterCenterY = gutterBox.y + gutterBox.height / 2
|
||||
|
||||
@@ -19,15 +19,13 @@ class SidebarTab {
|
||||
}
|
||||
|
||||
async open() {
|
||||
if (await this.selectedTabButton.isVisible()) {
|
||||
return
|
||||
}
|
||||
if (await this.selectedTabButton.isVisible()) return
|
||||
|
||||
await this.tabButton.click()
|
||||
}
|
||||
async close() {
|
||||
if (!this.tabButton.isVisible()) {
|
||||
return
|
||||
}
|
||||
if (!this.tabButton.isVisible()) return
|
||||
|
||||
await this.tabButton.click()
|
||||
}
|
||||
}
|
||||
@@ -54,9 +52,7 @@ export class NodeLibrarySidebarTab extends SidebarTab {
|
||||
}
|
||||
|
||||
override async close() {
|
||||
if (!this.tabButton.isVisible()) {
|
||||
return
|
||||
}
|
||||
if (!this.tabButton.isVisible()) return
|
||||
|
||||
await this.tabButton.click()
|
||||
await this.nodeLibraryTree.waitFor({ state: 'hidden' })
|
||||
@@ -124,9 +120,7 @@ export class NodeLibrarySidebarTabV2 extends SidebarTab {
|
||||
async expandFolder(folderName: string) {
|
||||
const folder = this.getFolder(folderName)
|
||||
const isExpanded = await folder.getAttribute('aria-expanded')
|
||||
if (isExpanded !== 'true') {
|
||||
await folder.click()
|
||||
}
|
||||
if (isExpanded !== 'true') await folder.click()
|
||||
}
|
||||
|
||||
override async open() {
|
||||
@@ -395,17 +389,15 @@ export class AssetsSidebarTab extends SidebarTab {
|
||||
await this.dismissToasts()
|
||||
await super.open()
|
||||
await this.generatedTab.waitFor({ state: 'visible' })
|
||||
if (waitForAssets) {
|
||||
await this.waitForAssets()
|
||||
}
|
||||
if (waitForAssets) await this.waitForAssets()
|
||||
}
|
||||
|
||||
/** 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()) {
|
||||
for (const btn of await closeButtons.all())
|
||||
await btn.click().catch(() => {})
|
||||
}
|
||||
|
||||
// Wait for all toast elements to fully animate out and detach from DOM
|
||||
await expect(this.page.locator('.p-toast-message'))
|
||||
.toHaveCount(0)
|
||||
@@ -466,10 +458,8 @@ export class AssetsSidebarTab extends SidebarTab {
|
||||
}
|
||||
|
||||
async waitForAssets(count?: number) {
|
||||
if (count !== undefined) {
|
||||
await expect(this.assetCards).toHaveCount(count)
|
||||
} else {
|
||||
if (count !== undefined) await expect(this.assetCards).toHaveCount(count)
|
||||
else
|
||||
await this.assetCards.first().waitFor({ state: 'visible', timeout: 5000 })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,9 +36,8 @@ export class Topbar {
|
||||
* Get a menu item by its label, optionally within a specific parent container
|
||||
*/
|
||||
getMenuItem(itemLabel: string, parent?: Locator): Locator {
|
||||
if (parent) {
|
||||
if (parent)
|
||||
return parent.locator(`.p-tieredmenu-item:has-text("${itemLabel}")`)
|
||||
}
|
||||
|
||||
return this.page.locator(`.p-menubar-item-label:text-is("${itemLabel}")`)
|
||||
}
|
||||
@@ -119,9 +118,7 @@ export class Topbar {
|
||||
const confirmationDialog = this.page
|
||||
.getByRole('dialog')
|
||||
.filter({ hasText: 'Overwrite' })
|
||||
if (await confirmationDialog.isVisible()) {
|
||||
return
|
||||
}
|
||||
if (await confirmationDialog.isVisible()) return
|
||||
}
|
||||
|
||||
async openTopbarMenu() {
|
||||
@@ -201,9 +198,7 @@ export class Topbar {
|
||||
}
|
||||
|
||||
async triggerTopbarCommand(path: string[]) {
|
||||
if (path.length < 1) {
|
||||
throw new Error('Path cannot be empty')
|
||||
}
|
||||
if (path.length < 1) throw new Error('Path cannot be empty')
|
||||
|
||||
const menu = await this.openTopbarMenu()
|
||||
const tabName = path[0]
|
||||
|
||||
@@ -37,9 +37,8 @@ type AssetOperator = (config: AssetConfig) => AssetConfig
|
||||
|
||||
function addAssets(config: AssetConfig, newAssets: Asset[]): AssetConfig {
|
||||
const merged = new Map(config.assets)
|
||||
for (const asset of newAssets) {
|
||||
merged.set(asset.id, asset)
|
||||
}
|
||||
for (const asset of newAssets) merged.set(asset.id, asset)
|
||||
|
||||
return { ...config, assets: merged }
|
||||
}
|
||||
export function withModels(
|
||||
@@ -221,9 +220,7 @@ export class AssetHelper {
|
||||
const offset = parseInt(url.searchParams.get('offset') ?? '0', 10)
|
||||
|
||||
let filtered = this.getFilteredAssets(includeTags, excludeTags)
|
||||
if (limit > 0) {
|
||||
filtered = filtered.slice(offset, offset + limit)
|
||||
}
|
||||
if (limit > 0) filtered = filtered.slice(offset, offset + limit)
|
||||
|
||||
const response: ListAssetsResponse = {
|
||||
assets: filtered,
|
||||
@@ -288,9 +285,9 @@ export class AssetHelper {
|
||||
}
|
||||
|
||||
async clearMocks(): Promise<void> {
|
||||
for (const { pattern, handler } of this.routeHandlers) {
|
||||
for (const { pattern, handler } of this.routeHandlers)
|
||||
await this.page.unroute(pattern, handler)
|
||||
}
|
||||
|
||||
this.routeHandlers = []
|
||||
this.store.clear()
|
||||
this.mutations = []
|
||||
|
||||
@@ -136,17 +136,15 @@ export function createJobsWithExecutionTimes(
|
||||
|
||||
function parseLimit(url: URL, total: number): number {
|
||||
const value = Number(url.searchParams.get('limit'))
|
||||
if (!Number.isInteger(value) || value <= 0) {
|
||||
return total
|
||||
}
|
||||
if (!Number.isInteger(value) || value <= 0) return total
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
function parseOffset(url: URL): number {
|
||||
const value = Number(url.searchParams.get('offset'))
|
||||
if (!Number.isInteger(value) || value < 0) {
|
||||
return 0
|
||||
}
|
||||
if (!Number.isInteger(value) || value < 0) return 0
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
@@ -181,9 +179,7 @@ export class AssetsHelper {
|
||||
async mockOutputHistory(jobs: RawJobListItem[]): Promise<void> {
|
||||
this.generatedJobs = [...jobs]
|
||||
|
||||
if (this.jobsRouteHandler) {
|
||||
return
|
||||
}
|
||||
if (this.jobsRouteHandler) return
|
||||
|
||||
this.jobsRouteHandler = async (route: Route) => {
|
||||
const url = new URL(route.request().url())
|
||||
@@ -254,9 +250,7 @@ export class AssetsHelper {
|
||||
async mockCloudAssets(response: ListAssetsResponse): Promise<void> {
|
||||
this.cloudAssetsResponse = response
|
||||
|
||||
if (this.cloudAssetsRouteHandler) {
|
||||
return
|
||||
}
|
||||
if (this.cloudAssetsRouteHandler) return
|
||||
|
||||
this.cloudAssetsRouteHandler = async (route: Route) => {
|
||||
await route.fulfill({
|
||||
@@ -286,9 +280,7 @@ export class AssetsHelper {
|
||||
this.assetExportRequests = []
|
||||
this.assetExportResponse = response
|
||||
|
||||
if (this.assetExportRouteHandler) {
|
||||
return this.assetExportRequests
|
||||
}
|
||||
if (this.assetExportRouteHandler) return this.assetExportRequests
|
||||
|
||||
this.assetExportRouteHandler = async (route: Route) => {
|
||||
this.assetExportRequests.push(
|
||||
@@ -311,9 +303,7 @@ export class AssetsHelper {
|
||||
const pattern = `**/api/jobs/${encodeURIComponent(jobId)}`
|
||||
const existingHandler = this.jobDetailRouteHandlers.get(pattern)
|
||||
|
||||
if (existingHandler) {
|
||||
await this.page.unroute(pattern, existingHandler)
|
||||
}
|
||||
if (existingHandler) await this.page.unroute(pattern, existingHandler)
|
||||
|
||||
const handler = async (route: Route) => {
|
||||
await route.fulfill({
|
||||
@@ -330,9 +320,7 @@ export class AssetsHelper {
|
||||
async mockInputFiles(files: string[]): Promise<void> {
|
||||
this.importedFiles = [...files]
|
||||
|
||||
if (this.inputFilesRouteHandler) {
|
||||
return
|
||||
}
|
||||
if (this.inputFilesRouteHandler) return
|
||||
|
||||
this.inputFilesRouteHandler = async (route: Route) => {
|
||||
await route.fulfill({
|
||||
@@ -424,9 +412,9 @@ export class AssetsHelper {
|
||||
this.deleteHistoryRouteHandler = null
|
||||
}
|
||||
|
||||
for (const [pattern, handler] of this.jobDetailRouteHandlers) {
|
||||
for (const [pattern, handler] of this.jobDetailRouteHandlers)
|
||||
await this.page.unroute(pattern, handler)
|
||||
}
|
||||
|
||||
this.jobDetailRouteHandlers.clear()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,18 +12,17 @@ export class CanvasHelper {
|
||||
) {}
|
||||
|
||||
async resetView(): Promise<void> {
|
||||
if (await this.resetViewButton.isVisible()) {
|
||||
if (await this.resetViewButton.isVisible())
|
||||
await this.resetViewButton.click()
|
||||
}
|
||||
|
||||
await this.page.mouse.move(10, 10)
|
||||
await nextFrame(this.page)
|
||||
}
|
||||
|
||||
async zoom(deltaY: number, steps: number = 1): Promise<void> {
|
||||
await this.page.mouse.move(10, 10)
|
||||
for (let i = 0; i < steps; i++) {
|
||||
await this.page.mouse.wheel(0, deltaY)
|
||||
}
|
||||
for (let i = 0; i < steps; i++) await this.page.mouse.wheel(0, deltaY)
|
||||
|
||||
await nextFrame(this.page)
|
||||
}
|
||||
|
||||
|
||||
@@ -75,9 +75,8 @@ export class CloudAuthHelper {
|
||||
request.onerror = () => reject(request.error)
|
||||
request.onupgradeneeded = () => {
|
||||
const db = request.result
|
||||
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
||||
if (!db.objectStoreNames.contains(STORE_NAME))
|
||||
db.createObjectStore(STORE_NAME)
|
||||
}
|
||||
}
|
||||
request.onsuccess = () => {
|
||||
const db = request.result
|
||||
@@ -87,9 +86,8 @@ export class CloudAuthHelper {
|
||||
upgradeReq.onerror = () => reject(upgradeReq.error)
|
||||
upgradeReq.onupgradeneeded = () => {
|
||||
const upgradedDb = upgradeReq.result
|
||||
if (!upgradedDb.objectStoreNames.contains(STORE_NAME)) {
|
||||
if (!upgradedDb.objectStoreNames.contains(STORE_NAME))
|
||||
upgradedDb.createObjectStore(STORE_NAME)
|
||||
}
|
||||
}
|
||||
upgradeReq.onsuccess = () => {
|
||||
const upgradedDb = upgradeReq.result
|
||||
|
||||
@@ -147,9 +147,7 @@ export class DragDropHelper {
|
||||
}
|
||||
}, evaluateParams)
|
||||
|
||||
if (uploadResponsePromise) {
|
||||
await uploadResponsePromise
|
||||
}
|
||||
if (uploadResponsePromise) await uploadResponsePromise
|
||||
|
||||
await nextFrame(this.page)
|
||||
}
|
||||
|
||||
@@ -15,9 +15,8 @@ export class FeatureFlagHelper {
|
||||
*/
|
||||
async seedFlags(flags: Record<string, unknown>): Promise<void> {
|
||||
await this.page.addInitScript((flagMap: Record<string, unknown>) => {
|
||||
for (const [key, value] of Object.entries(flagMap)) {
|
||||
for (const [key, value] of Object.entries(flagMap))
|
||||
localStorage.setItem(`ff:${key}`, JSON.stringify(value))
|
||||
}
|
||||
}, flags)
|
||||
}
|
||||
|
||||
@@ -28,9 +27,8 @@ export class FeatureFlagHelper {
|
||||
*/
|
||||
async setFlags(flags: Record<string, unknown>): Promise<void> {
|
||||
await this.page.evaluate((flagMap: Record<string, unknown>) => {
|
||||
for (const [key, value] of Object.entries(flagMap)) {
|
||||
for (const [key, value] of Object.entries(flagMap))
|
||||
localStorage.setItem(`ff:${key}`, JSON.stringify(value))
|
||||
}
|
||||
}, flags)
|
||||
}
|
||||
|
||||
|
||||
@@ -94,9 +94,9 @@ class MaskEditorHelper {
|
||||
if (!ctx) return null
|
||||
const data = ctx.getImageData(0, 0, canvas.width, canvas.height)
|
||||
let nonTransparentPixels = 0
|
||||
for (let i = 3; i < data.data.length; i += 4) {
|
||||
for (let i = 3; i < data.data.length; i += 4)
|
||||
if (data.data[i] > 0) nonTransparentPixels++
|
||||
}
|
||||
|
||||
return { nonTransparentPixels, totalPixels: data.data.length / 4 }
|
||||
}, canvasIndex)
|
||||
}
|
||||
|
||||
@@ -97,9 +97,8 @@ export class ModelLibraryHelper {
|
||||
async mockFoldersWithFiles(config: Record<string, string[]>): Promise<void> {
|
||||
const folderNames = Object.keys(config)
|
||||
await this.mockModelFolders(createMockModelFolders(folderNames))
|
||||
for (const [folder, files] of Object.entries(config)) {
|
||||
for (const [folder, files] of Object.entries(config))
|
||||
await this.mockModelFiles(folder, createMockModelFiles(files))
|
||||
}
|
||||
}
|
||||
|
||||
async clearMocks(): Promise<void> {
|
||||
|
||||
@@ -157,9 +157,7 @@ export class NodeOperationsHelper {
|
||||
try {
|
||||
for (const nodeTitle of nodeTitles) {
|
||||
const nodes = await this.getNodeRefsByTitle(nodeTitle)
|
||||
for (const node of nodes) {
|
||||
await node.click('title')
|
||||
}
|
||||
for (const node of nodes) await node.click('title')
|
||||
}
|
||||
} finally {
|
||||
await this.page.keyboard.up('Control')
|
||||
|
||||
@@ -93,9 +93,9 @@ export class PerformanceHelper {
|
||||
if (!state) return 0
|
||||
|
||||
// Flush any queued-but-undelivered entries into our accumulator
|
||||
for (const entry of state.observer.takeRecords()) {
|
||||
for (const entry of state.observer.takeRecords())
|
||||
if (entry.duration > 50) state.tbtMs += entry.duration - 50
|
||||
}
|
||||
|
||||
const result = state.tbtMs
|
||||
state.tbtMs = 0
|
||||
return result
|
||||
@@ -124,9 +124,9 @@ export class PerformanceHelper {
|
||||
return
|
||||
}
|
||||
const durations: number[] = []
|
||||
for (let i = 1; i < timestamps.length; i++) {
|
||||
for (let i = 1; i < timestamps.length; i++)
|
||||
durations.push(timestamps[i] - timestamps[i - 1])
|
||||
}
|
||||
|
||||
resolve(durations)
|
||||
}
|
||||
}
|
||||
@@ -153,9 +153,8 @@ export class PerformanceHelper {
|
||||
observer: PerformanceObserver
|
||||
tbtMs: number
|
||||
}
|
||||
for (const entry of list.getEntries()) {
|
||||
for (const entry of list.getEntries())
|
||||
if (entry.duration > 50) self.tbtMs += entry.duration - 50
|
||||
}
|
||||
}),
|
||||
tbtMs: 0
|
||||
}
|
||||
|
||||
@@ -189,9 +189,9 @@ class PublishApiHelper {
|
||||
}
|
||||
|
||||
async cleanup(): Promise<void> {
|
||||
for (const { pattern, handler } of this.routeHandlers) {
|
||||
for (const { pattern, handler } of this.routeHandlers)
|
||||
await this.page.unroute(pattern, handler)
|
||||
}
|
||||
|
||||
this.routeHandlers = []
|
||||
}
|
||||
|
||||
@@ -207,9 +207,9 @@ class PublishApiHelper {
|
||||
const handlers = this.routeHandlers.filter(
|
||||
(route) => route.pattern === pattern
|
||||
)
|
||||
for (const { handler } of handlers) {
|
||||
for (const { handler } of handlers)
|
||||
await this.page.unroute(pattern, handler)
|
||||
}
|
||||
|
||||
this.routeHandlers = this.routeHandlers.filter(
|
||||
(route) => route.pattern !== pattern
|
||||
)
|
||||
|
||||
@@ -61,13 +61,10 @@ export class SubgraphHelper {
|
||||
slotType === 'input' ? subgraph.inputNode : subgraph.outputNode
|
||||
const slots = slotType === 'input' ? subgraph.inputs : subgraph.outputs
|
||||
|
||||
if (!node) {
|
||||
throw new Error(`No ${slotType} node found in subgraph`)
|
||||
}
|
||||
if (!node) throw new Error(`No ${slotType} node found in subgraph`)
|
||||
|
||||
if (!slots || slots.length === 0) {
|
||||
if (!slots || slots.length === 0)
|
||||
throw new Error(`No ${slotType} slots found in subgraph`)
|
||||
}
|
||||
|
||||
// Filter slots based on target name and action type
|
||||
const slotsToTry = targetSlotName
|
||||
@@ -115,9 +112,8 @@ export class SubgraphHelper {
|
||||
} else if (action === 'doubleClick') {
|
||||
// Double-click: use first slot with bounding rect center
|
||||
const slot = slotsToTry[0]
|
||||
if (!slot.boundingRect) {
|
||||
if (!slot.boundingRect)
|
||||
throw new Error(`${slotType} slot bounding rect not found`)
|
||||
}
|
||||
|
||||
const rect = slot.boundingRect
|
||||
const testX = rect[0] + rect[2] / 2 // x + width/2
|
||||
@@ -398,9 +394,7 @@ export class SubgraphHelper {
|
||||
|
||||
await this.comfyPage.nextFrame()
|
||||
await expect.poll(async () => this.isInSubgraph()).toBe(false)
|
||||
if (this.comfyPage.isVueNodes) {
|
||||
await this.comfyPage.vueNodes.waitForNodes()
|
||||
}
|
||||
if (this.comfyPage.isVueNodes) await this.comfyPage.vueNodes.waitForNodes()
|
||||
}
|
||||
|
||||
async countGraphPseudoPreviewEntries(): Promise<number> {
|
||||
@@ -461,11 +455,9 @@ export class SubgraphHelper {
|
||||
}
|
||||
|
||||
async removeSlot(type: 'input' | 'output', slotName?: string): Promise<void> {
|
||||
if (type === 'input') {
|
||||
await this.rightClickInputSlot(slotName)
|
||||
} else {
|
||||
await this.rightClickOutputSlot(slotName)
|
||||
}
|
||||
if (type === 'input') await this.rightClickInputSlot(slotName)
|
||||
else await this.rightClickOutputSlot(slotName)
|
||||
|
||||
await this.comfyPage.contextMenu.clickLitegraphMenuItem('Remove Slot')
|
||||
await this.comfyPage.contextMenu.waitForHidden()
|
||||
}
|
||||
@@ -592,9 +584,7 @@ export class SubgraphHelper {
|
||||
const warnings: string[] = []
|
||||
const handler = (msg: ConsoleMessage) => {
|
||||
const text = msg.text()
|
||||
if (patterns.some((p) => text.includes(p))) {
|
||||
warnings.push(text)
|
||||
}
|
||||
if (patterns.some((p) => text.includes(p))) warnings.push(text)
|
||||
}
|
||||
page.on('console', handler)
|
||||
return { warnings, dispose: () => page.off('console', handler) }
|
||||
|
||||
@@ -113,9 +113,9 @@ export class TemplateHelper {
|
||||
}
|
||||
|
||||
async clearMocks(): Promise<void> {
|
||||
for (const { pattern, handler } of this.routeHandlers) {
|
||||
for (const { pattern, handler } of this.routeHandlers)
|
||||
await this.page.unroute(pattern, handler)
|
||||
}
|
||||
|
||||
this.routeHandlers = []
|
||||
this.templates = []
|
||||
this.index = null
|
||||
|
||||
@@ -27,9 +27,7 @@ export class ToastHelper {
|
||||
const toastCloseButtons = await this.page
|
||||
.locator('.p-toast-close-button')
|
||||
.all()
|
||||
for (const button of toastCloseButtons) {
|
||||
await button.click()
|
||||
}
|
||||
for (const button of toastCloseButtons) await button.click()
|
||||
|
||||
// Assert all toasts are closed
|
||||
await expect(this.visibleToasts).toHaveCount(0)
|
||||
|
||||
@@ -18,9 +18,10 @@ export class UserDataHelper {
|
||||
`${this.baseUrl}/api/userdata/${encodeURIComponent(file)}`,
|
||||
{ method: 'DELETE', headers: { 'Comfy-User': this.userId } }
|
||||
)
|
||||
if (!res.ok() && res.status() !== 404)
|
||||
if (!res.ok() && res.status() !== 404) {
|
||||
throw new Error(
|
||||
`Failed to delete userdata file "${file}": HTTP ${res.status()}`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,9 +83,8 @@ export class WorkflowHelper {
|
||||
if (
|
||||
typeof index.updatedAt === 'number' &&
|
||||
index.updatedAt >= indexUpdatedSince
|
||||
) {
|
||||
)
|
||||
return true
|
||||
}
|
||||
} catch {
|
||||
// Ignore malformed storage while waiting for persistence.
|
||||
}
|
||||
@@ -120,9 +119,8 @@ export class WorkflowHelper {
|
||||
)
|
||||
await this.waitForWorkflowIdle()
|
||||
await this.comfyPage.nextFrame()
|
||||
if (test.info().tags.includes('@vue-nodes')) {
|
||||
if (test.info().tags.includes('@vue-nodes'))
|
||||
await this.comfyPage.vueNodes.waitForNodes()
|
||||
}
|
||||
}
|
||||
|
||||
async deleteWorkflow(
|
||||
|
||||
@@ -145,12 +145,10 @@ class JobsRouteMocker {
|
||||
}
|
||||
|
||||
async mockJobsScenario({ history, queue }: JobsScenario): Promise<void> {
|
||||
if (history) {
|
||||
if (history)
|
||||
await this.mockJobsHistory(history, defaultScenarioHistoryLimit)
|
||||
}
|
||||
if (queue) {
|
||||
await this.mockJobsQueue(queue)
|
||||
}
|
||||
|
||||
if (queue) await this.mockJobsQueue(queue)
|
||||
}
|
||||
|
||||
async mockJobsList(route: JobsListRoute): Promise<void> {
|
||||
|
||||
@@ -219,9 +219,8 @@ async function mockSharedWorkflowImportFlow(
|
||||
body: JSON.stringify(response)
|
||||
})
|
||||
|
||||
if (isAfterImportPublicInclusiveInputAssetRequest) {
|
||||
if (isAfterImportPublicInclusiveInputAssetRequest)
|
||||
resolvePublicInclusiveInputAssetResponseAfterImport()
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
|
||||
@@ -56,9 +56,8 @@ export async function measureSelectionBounds(
|
||||
const node = window.app!.graph._nodes.find(
|
||||
(n: { id: number | string }) => String(n.id) === id
|
||||
)
|
||||
if (!node) {
|
||||
throw new Error(`Node ${id} not found in graph`)
|
||||
}
|
||||
if (!node) throw new Error(`Node ${id} not found in graph`)
|
||||
|
||||
const rect = node.boundingRect
|
||||
nodeVisualBounds[id] = {
|
||||
x: rect[0],
|
||||
@@ -74,9 +73,8 @@ export async function measureSelectionBounds(
|
||||
'[data-testid="subgraph-enter-button"], [data-testid="node-footer"]'
|
||||
)
|
||||
let bottom = domRect.bottom
|
||||
for (const footerEl of footerEls) {
|
||||
for (const footerEl of footerEls)
|
||||
bottom = Math.max(bottom, footerEl.getBoundingClientRect().bottom)
|
||||
}
|
||||
|
||||
nodeVisualBounds[id] = {
|
||||
x: (domRect.left - canvasRect.left) / ds.scale - ds.offset[0],
|
||||
|
||||
@@ -46,9 +46,8 @@ export async function setupBuilder(
|
||||
await appMode.enterBuilder()
|
||||
await appMode.steps.goToInputs()
|
||||
|
||||
for (const name of inputWidgets) {
|
||||
for (const name of inputWidgets)
|
||||
await appMode.select.selectInputWidget(inputNodeTitle, name)
|
||||
}
|
||||
|
||||
await appMode.steps.goToOutputs()
|
||||
await appMode.select.selectOutputNode('Save Image')
|
||||
|
||||
@@ -14,11 +14,8 @@ function makeMatcher<T>(
|
||||
) {
|
||||
await expect(async () => {
|
||||
const value = await getValue(node)
|
||||
if (this.isNot) {
|
||||
expect(value, 'Node is ' + type).not.toBeTruthy()
|
||||
} else {
|
||||
expect(value, 'Node is not ' + type).toBeTruthy()
|
||||
}
|
||||
if (this.isNot) expect(value, 'Node is ' + type).not.toBeTruthy()
|
||||
else expect(value, 'Node is not ' + type).toBeTruthy()
|
||||
}).toPass({ timeout: 5000, ...options })
|
||||
return {
|
||||
pass: !this.isNot,
|
||||
|
||||
@@ -28,9 +28,9 @@ export async function fitToViewInstant(
|
||||
|
||||
const canvas = app.canvas
|
||||
const items = (() => {
|
||||
if (selectionOnly && canvas.selectedItems?.size) {
|
||||
if (selectionOnly && canvas.selectedItems?.size)
|
||||
return Array.from(canvas.selectedItems)
|
||||
}
|
||||
|
||||
try {
|
||||
return Array.from(canvas.positionableItems ?? [])
|
||||
} catch {
|
||||
|
||||
@@ -35,22 +35,18 @@ export class SubgraphSlotReference {
|
||||
|
||||
const slots =
|
||||
type === 'input' ? currentGraph.inputs : currentGraph.outputs
|
||||
if (!slots || slots.length === 0) {
|
||||
if (!slots || slots.length === 0)
|
||||
throw new Error(`No ${type} slots found in subgraph`)
|
||||
}
|
||||
|
||||
// Find the specific slot or use the first one if no name specified
|
||||
const slot = slotName
|
||||
? slots.find((s) => s.name === slotName)
|
||||
: slots[0]
|
||||
|
||||
if (!slot) {
|
||||
throw new Error(`${type} slot '${slotName}' not found`)
|
||||
}
|
||||
if (!slot) throw new Error(`${type} slot '${slotName}' not found`)
|
||||
|
||||
if (!slot.pos) {
|
||||
if (!slot.pos)
|
||||
throw new Error(`${type} slot '${slotName}' has no position`)
|
||||
}
|
||||
|
||||
// Convert from offset to canvas coordinates
|
||||
const canvasPos = window.app!.canvas.ds.convertOffsetToCanvas([
|
||||
@@ -83,9 +79,7 @@ export class SubgraphSlotReference {
|
||||
const node =
|
||||
type === 'input' ? currentGraph.inputNode : currentGraph.outputNode
|
||||
|
||||
if (!node) {
|
||||
throw new Error(`No ${type} node found in subgraph`)
|
||||
}
|
||||
if (!node) throw new Error(`No ${type} node found in subgraph`)
|
||||
|
||||
// Convert from offset to canvas coordinates
|
||||
const canvasPos = window.app!.canvas.ds.convertOffsetToCanvas([
|
||||
@@ -148,9 +142,8 @@ class NodeSlotReference {
|
||||
([type, id, index]) => {
|
||||
const node = window.app!.canvas.graph!.getNodeById(id)
|
||||
if (!node) throw new Error(`Node ${id} not found.`)
|
||||
if (type === 'input') {
|
||||
return node.inputs[index].link == null ? 0 : 1
|
||||
}
|
||||
if (type === 'input') return node.inputs[index].link == null ? 0 : 1
|
||||
|
||||
return node.outputs[index].links?.length ?? 0
|
||||
},
|
||||
[this.type, this.node.id, this.index] as const
|
||||
@@ -161,11 +154,8 @@ class NodeSlotReference {
|
||||
([type, id, index]) => {
|
||||
const node = window.app!.canvas.graph!.getNodeById(id)
|
||||
if (!node) throw new Error(`Node ${id} not found.`)
|
||||
if (type === 'input') {
|
||||
node.disconnectInput(index)
|
||||
} else {
|
||||
node.disconnectOutput(index)
|
||||
}
|
||||
if (type === 'input') node.disconnectInput(index)
|
||||
else node.disconnectOutput(index)
|
||||
},
|
||||
[this.type, this.node.id, this.index] as const
|
||||
)
|
||||
@@ -426,9 +416,8 @@ export class NodeReference {
|
||||
|
||||
const widgetIndex =
|
||||
node.widgets?.findIndex((widget) => widget.name === widgetName) ?? -1
|
||||
if (widgetIndex < 0) {
|
||||
if (widgetIndex < 0)
|
||||
throw new Error(`Widget "${widgetName}" not found on node ${id}`)
|
||||
}
|
||||
|
||||
return widgetIndex
|
||||
},
|
||||
@@ -460,14 +449,11 @@ export class NodeReference {
|
||||
}
|
||||
|
||||
const moveMouseToEmptyArea = options?.moveMouseToEmptyArea
|
||||
if (options) {
|
||||
delete options.moveMouseToEmptyArea
|
||||
}
|
||||
if (options) delete options.moveMouseToEmptyArea
|
||||
|
||||
await this.comfyPage.canvasOps.mouseClickAt(clickPos, options)
|
||||
if (moveMouseToEmptyArea) {
|
||||
if (moveMouseToEmptyArea)
|
||||
await this.comfyPage.canvasOps.moveMouseToEmptyArea()
|
||||
}
|
||||
}
|
||||
async copy() {
|
||||
await this.click('title')
|
||||
|
||||
@@ -27,9 +27,8 @@ export async function hasCanvasContent(canvas: Locator): Promise<boolean> {
|
||||
const ctx = el.getContext('2d')
|
||||
if (!ctx) return false
|
||||
const { data } = ctx.getImageData(0, 0, el.width, el.height)
|
||||
for (let i = 3; i < data.length; i += 4) {
|
||||
if (data[i] > 0) return true
|
||||
}
|
||||
for (let i = 3; i < data.length; i += 4) if (data[i] > 0) return true
|
||||
|
||||
return false
|
||||
})
|
||||
}
|
||||
@@ -58,14 +57,12 @@ export async function triggerSerialization(page: Page): Promise<void> {
|
||||
}
|
||||
|
||||
const widgetIndex = node.widgets?.findIndex((w) => w.name === 'mask') ?? -1
|
||||
if (widgetIndex === -1) {
|
||||
if (widgetIndex === -1)
|
||||
throw new Error('Widget "mask" not found on target node 1.')
|
||||
}
|
||||
|
||||
const widget = node.widgets?.[widgetIndex]
|
||||
if (!widget) {
|
||||
if (!widget)
|
||||
throw new Error(`Widget index ${widgetIndex} not found on target node 1.`)
|
||||
}
|
||||
|
||||
if (typeof widget.serializeValue !== 'function') {
|
||||
throw new Error(
|
||||
|
||||
@@ -53,15 +53,14 @@ export function preview3dRestoreCameraStatesMatch(
|
||||
a: unknown,
|
||||
b: unknown
|
||||
): boolean {
|
||||
if (!isPreview3dCameraStatePayload(a) || !isPreview3dCameraStatePayload(b)) {
|
||||
if (!isPreview3dCameraStatePayload(a) || !isPreview3dCameraStatePayload(b))
|
||||
return false
|
||||
}
|
||||
|
||||
if (a.cameraType !== b.cameraType) return false
|
||||
const zoomA = typeof a.zoom === 'number' ? a.zoom : 0
|
||||
const zoomB = typeof b.zoom === 'number' ? b.zoom : 0
|
||||
if (Math.abs(zoomA - zoomB) > PREVIEW3D_CAMERA_ZOOM_RESTORE_EPS) {
|
||||
return false
|
||||
}
|
||||
if (Math.abs(zoomA - zoomB) > PREVIEW3D_CAMERA_ZOOM_RESTORE_EPS) return false
|
||||
|
||||
return (
|
||||
vecWithinEps(a.position, b.position, PREVIEW3D_CAMERA_AXIS_RESTORE_EPS) &&
|
||||
vecWithinEps(a.target, b.target, PREVIEW3D_CAMERA_AXIS_RESTORE_EPS)
|
||||
@@ -73,9 +72,9 @@ export function preview3dCameraStatesDiffer(
|
||||
b: unknown,
|
||||
eps: number
|
||||
): boolean {
|
||||
if (!isPreview3dCameraStatePayload(a) || !isPreview3dCameraStatePayload(b)) {
|
||||
if (!isPreview3dCameraStatePayload(a) || !isPreview3dCameraStatePayload(b))
|
||||
return true
|
||||
}
|
||||
|
||||
if (a.cameraType !== b.cameraType) return true
|
||||
const zoomA = typeof a.zoom === 'number' ? a.zoom : 0
|
||||
const zoomB = typeof b.zoom === 'number' ? b.zoom : 0
|
||||
|
||||
@@ -7,9 +7,7 @@ export async function openMoreOptionsMenu(
|
||||
nodeTitle: string
|
||||
) {
|
||||
const nodes = await comfyPage.nodeOps.getNodeRefsByTitle(nodeTitle)
|
||||
if (nodes.length === 0) {
|
||||
throw new Error(`No "${nodeTitle}" nodes found`)
|
||||
}
|
||||
if (nodes.length === 0) throw new Error(`No "${nodeTitle}" nodes found`)
|
||||
|
||||
await nodes[0].centerOnNode()
|
||||
await nodes[0].click('title')
|
||||
|
||||
@@ -103,9 +103,8 @@ export class VueNodeFixture {
|
||||
}> {
|
||||
await this.header.click()
|
||||
const box = await this.boundingBox()
|
||||
if (!box) {
|
||||
throw new Error('Node bounding box not found after select')
|
||||
}
|
||||
if (!box) throw new Error('Node bounding box not found after select')
|
||||
|
||||
return box
|
||||
}
|
||||
|
||||
|
||||
@@ -83,9 +83,7 @@ test.describe('Actionbar', { tag: '@ui' }, () => {
|
||||
// Trigger a bunch of changes
|
||||
const START = 32
|
||||
const END = 64
|
||||
for (let i = START; i <= END; i += 8) {
|
||||
await triggerChange(i)
|
||||
}
|
||||
for (let i = START; i <= END; i += 8) await triggerChange(i)
|
||||
|
||||
// Ensure the queued width is the first value
|
||||
expect(
|
||||
|
||||
@@ -39,9 +39,8 @@ function isClippedByAnyAncestor(el: Element): boolean {
|
||||
child.bottom > p.bottom ||
|
||||
child.left < p.left ||
|
||||
child.right > p.right
|
||||
) {
|
||||
)
|
||||
return true
|
||||
}
|
||||
}
|
||||
parent = parent.parentElement
|
||||
}
|
||||
|
||||
@@ -87,8 +87,7 @@ test.describe('App mode widget values in prompt', { tag: '@ui' }, () => {
|
||||
|
||||
const prompt = await appMode.widgets.runAndCapturePrompt()
|
||||
|
||||
for (const { nodeId, widgetName, expected } of WIDGET_TEST_DATA) {
|
||||
for (const { nodeId, widgetName, expected } of WIDGET_TEST_DATA)
|
||||
expect(prompt[nodeId].inputs[widgetName]).toBe(expected)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -99,9 +99,9 @@ test.describe('CanvasModeSelector', { tag: '@canvas' }, () => {
|
||||
test(`clicking "${mode.label}" sets canvas readOnly=${mode.isReadOnly}`, async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
if (!mode.isReadOnly) {
|
||||
if (!mode.isReadOnly)
|
||||
await comfyPage.command.executeCommand('Comfy.Canvas.Lock')
|
||||
}
|
||||
|
||||
const { trigger, menu, selectItem, handItem } = getLocators(
|
||||
comfyPage.page
|
||||
)
|
||||
|
||||
@@ -41,9 +41,8 @@ async function getChangeTrackerDebugState(comfyPage: ComfyPage) {
|
||||
const workflow = workflowStore.workflow
|
||||
.activeWorkflow as ActiveWorkflowLike | null
|
||||
const tracker = workflow?.changeTracker
|
||||
if (!workflow || !tracker) {
|
||||
if (!workflow || !tracker)
|
||||
throw new Error('Active workflow change tracker is not available')
|
||||
}
|
||||
|
||||
const currentState = JSON.parse(
|
||||
JSON.stringify(window.app!.rootGraph.serialize())
|
||||
|
||||
@@ -46,9 +46,7 @@ test.describe('CSS Containment Audit', { tag: ['@audit'] }, () => {
|
||||
test('scan large graph for containment candidates', async ({ comfyPage }) => {
|
||||
await comfyPage.workflow.loadWorkflow('large-graph-workflow')
|
||||
|
||||
for (let i = 0; i < STABILIZATION_FRAMES; i++) {
|
||||
await comfyPage.nextFrame()
|
||||
}
|
||||
for (let i = 0; i < STABILIZATION_FRAMES; i++) await comfyPage.nextFrame()
|
||||
|
||||
// Walk the DOM and find candidates
|
||||
const candidates = await comfyPage.page.evaluate((): ContainCandidate[] => {
|
||||
@@ -147,9 +145,8 @@ test.describe('CSS Containment Audit', { tag: ['@audit'] }, () => {
|
||||
|
||||
// Measure baseline performance (idle)
|
||||
await comfyPage.perf.startMeasuring()
|
||||
for (let i = 0; i < STABILIZATION_FRAMES; i++) {
|
||||
await comfyPage.nextFrame()
|
||||
}
|
||||
for (let i = 0; i < STABILIZATION_FRAMES; i++) await comfyPage.nextFrame()
|
||||
|
||||
const baseline = await comfyPage.perf.stopMeasuring('baseline-idle')
|
||||
|
||||
// Take a baseline screenshot for visual comparison
|
||||
@@ -177,15 +174,12 @@ test.describe('CSS Containment Audit', { tag: ['@audit'] }, () => {
|
||||
|
||||
if (applied === 0) continue
|
||||
|
||||
for (let i = 0; i < SETTLE_FRAMES; i++) {
|
||||
await comfyPage.nextFrame()
|
||||
}
|
||||
for (let i = 0; i < SETTLE_FRAMES; i++) await comfyPage.nextFrame()
|
||||
|
||||
// Measure with containment
|
||||
await comfyPage.perf.startMeasuring()
|
||||
for (let i = 0; i < STABILIZATION_FRAMES; i++) {
|
||||
await comfyPage.nextFrame()
|
||||
}
|
||||
for (let i = 0; i < STABILIZATION_FRAMES; i++) await comfyPage.nextFrame()
|
||||
|
||||
const withContain = await comfyPage.perf.stopMeasuring(
|
||||
`contain-${candidate.selector}`
|
||||
)
|
||||
@@ -199,15 +193,11 @@ test.describe('CSS Containment Audit', { tag: ['@audit'] }, () => {
|
||||
// Remove containment
|
||||
await comfyPage.page.evaluate((sel: string) => {
|
||||
document.querySelectorAll(sel).forEach((el) => {
|
||||
if (el instanceof HTMLElement) {
|
||||
el.style.contain = ''
|
||||
}
|
||||
if (el instanceof HTMLElement) el.style.contain = ''
|
||||
})
|
||||
}, candidate.selector)
|
||||
|
||||
for (let i = 0; i < SETTLE_FRAMES; i++) {
|
||||
await comfyPage.nextFrame()
|
||||
}
|
||||
for (let i = 0; i < SETTLE_FRAMES; i++) await comfyPage.nextFrame()
|
||||
|
||||
results.push({
|
||||
candidate,
|
||||
|
||||
@@ -52,9 +52,8 @@ test.describe('Queue Clear History Dialog', { tag: '@ui' }, () => {
|
||||
|
||||
let clearCalled = false
|
||||
await comfyPage.page.route('**/api/history', (route) => {
|
||||
if (route.request().method() === 'POST') {
|
||||
clearCalled = true
|
||||
}
|
||||
if (route.request().method() === 'POST') clearCalled = true
|
||||
|
||||
return route.continue()
|
||||
})
|
||||
|
||||
@@ -75,9 +74,8 @@ test.describe('Queue Clear History Dialog', { tag: '@ui' }, () => {
|
||||
|
||||
let clearCalled = false
|
||||
await comfyPage.page.route('**/api/history', (route) => {
|
||||
if (route.request().method() === 'POST') {
|
||||
clearCalled = true
|
||||
}
|
||||
if (route.request().method() === 'POST') clearCalled = true
|
||||
|
||||
return route.continue()
|
||||
})
|
||||
|
||||
|
||||
@@ -22,9 +22,8 @@ test.describe('DOM Widget', { tag: '@widget' }, () => {
|
||||
await expect(lastMultiline).toBeVisible()
|
||||
|
||||
const nodes = await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode')
|
||||
for (const node of nodes) {
|
||||
await node.click('collapse')
|
||||
}
|
||||
for (const node of nodes) await node.click('collapse')
|
||||
|
||||
await expect(firstMultiline).toBeHidden()
|
||||
await expect(lastMultiline).toBeHidden()
|
||||
})
|
||||
|
||||
@@ -42,9 +42,8 @@ function buildPreviewAnyValidationError(): NodeError {
|
||||
function expectPartialExecutionRootNodes(requestBody: unknown): void {
|
||||
const prompt = (requestBody as PromptRequestBody).prompt ?? {}
|
||||
|
||||
for (const nodeId of PARTIAL_EXECUTION_ROOT_NODE_IDS) {
|
||||
for (const nodeId of PARTIAL_EXECUTION_ROOT_NODE_IDS)
|
||||
expect(prompt[nodeId]).toMatchObject({ class_type: 'PreviewAny' })
|
||||
}
|
||||
}
|
||||
|
||||
async function getValidationErrorMessage(comfyPage: ComfyPage) {
|
||||
|
||||
@@ -26,9 +26,8 @@ test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => {
|
||||
WebSocket.prototype.send = function (data) {
|
||||
try {
|
||||
const parsed = JSON.parse(data as string)
|
||||
if (parsed.type === 'feature_flags') {
|
||||
if (parsed.type === 'feature_flags')
|
||||
window.__capturedMessages!.clientFeatureFlags = parsed
|
||||
}
|
||||
} catch (e) {
|
||||
// Not JSON, ignore
|
||||
}
|
||||
|
||||
@@ -40,8 +40,7 @@ test.describe('i18n locale fallback', () => {
|
||||
.allTextContents()
|
||||
|
||||
expect(labelTexts.length).toBeGreaterThan(0)
|
||||
for (const text of labelTexts) {
|
||||
for (const text of labelTexts)
|
||||
expect(text).not.toContain('sideToolbar.labels')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -54,9 +54,9 @@ test.describe('Image Compare', { tag: ['@widget', '@vue-nodes'] }, () => {
|
||||
})
|
||||
.toBe(true)
|
||||
const box = await containerLocator.boundingBox()
|
||||
if (!box || box.width <= 0 || box.height <= 0) {
|
||||
if (!box || box.width <= 0 || box.height <= 0)
|
||||
throw new Error('Slider move target has no layout box')
|
||||
}
|
||||
|
||||
await page.mouse.move(
|
||||
box.x + box.width * (percentage / 100),
|
||||
box.y + box.height / 2
|
||||
@@ -261,9 +261,8 @@ test.describe('Image Compare', { tag: ['@widget', '@vue-nodes'] }, () => {
|
||||
.toBe(true)
|
||||
|
||||
const box = await compareArea.boundingBox()
|
||||
if (!box || box.width <= 0 || box.height <= 0) {
|
||||
if (!box || box.width <= 0 || box.height <= 0)
|
||||
throw new Error('Compare viewport layout not ready')
|
||||
}
|
||||
|
||||
await comfyPage.page.mouse.move(box.x, box.y + box.height / 2)
|
||||
await expect
|
||||
|
||||
@@ -57,9 +57,9 @@ test.describe('Node Interaction', () => {
|
||||
}) => {
|
||||
const clipNodes =
|
||||
await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode')
|
||||
for (const node of clipNodes) {
|
||||
for (const node of clipNodes)
|
||||
await node.click('title', { modifiers: [modifier] })
|
||||
}
|
||||
|
||||
await expect
|
||||
.poll(() => comfyPage.nodeOps.getSelectedGraphNodesCount())
|
||||
.toBe(clipNodes.length)
|
||||
@@ -854,9 +854,8 @@ test.describe('Load workflow', { tag: '@screenshot' }, () => {
|
||||
if (!json) continue
|
||||
try {
|
||||
const index = JSON.parse(json)
|
||||
if (typeof index.updatedAt === 'number' && index.updatedAt >= since) {
|
||||
if (typeof index.updatedAt === 'number' && index.updatedAt >= since)
|
||||
return true
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
@@ -889,9 +888,7 @@ test.describe('Load workflow', { tag: '@screenshot' }, () => {
|
||||
await comfyPage.page.waitForFunction(() => {
|
||||
for (let i = 0; i < window.sessionStorage.length; i++) {
|
||||
const key = window.sessionStorage.key(i)
|
||||
if (key?.startsWith('Comfy.Workflow.OpenPaths:')) {
|
||||
return true
|
||||
}
|
||||
if (key?.startsWith('Comfy.Workflow.OpenPaths:')) return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
@@ -962,9 +959,7 @@ test.describe('Load workflow', { tag: '@screenshot' }, () => {
|
||||
await comfyPage.page.waitForFunction(() => {
|
||||
for (let i = 0; i < window.localStorage.length; i++) {
|
||||
const key = window.localStorage.key(i)
|
||||
if (key?.startsWith('Comfy.Workflow.LastOpenPaths:')) {
|
||||
return true
|
||||
}
|
||||
if (key?.startsWith('Comfy.Workflow.LastOpenPaths:')) return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
@@ -1104,9 +1099,7 @@ test.describe('Viewport settings', () => {
|
||||
await changeTab(tabB)
|
||||
|
||||
await comfyMouse.move(DefaultGraphPositions.emptySpace)
|
||||
for (let i = 0; i < 4; i++) {
|
||||
await comfyMouse.wheel(0, 60)
|
||||
}
|
||||
for (let i = 0; i < 4; i++) await comfyMouse.wheel(0, 60)
|
||||
|
||||
await comfyPage.nextFrame()
|
||||
const screenshotB = (await comfyPage.canvas.screenshot()).toString('base64')
|
||||
|
||||
@@ -103,9 +103,8 @@ test.describe('Keybinding Presets', { tag: '@keyboard' }, () => {
|
||||
const discardButton = page.getByRole('button', {
|
||||
name: /Discard and Switch/i
|
||||
})
|
||||
if (await discardButton.isVisible({ timeout: 2000 }).catch(() => false)) {
|
||||
if (await discardButton.isVisible({ timeout: 2000 }).catch(() => false))
|
||||
await discardButton.click()
|
||||
}
|
||||
|
||||
await expect(presetTrigger).toContainText('Default Preset')
|
||||
|
||||
|
||||
@@ -15,9 +15,8 @@ function hasCanvasContent(canvas: Locator): Promise<boolean> {
|
||||
const ctx = el.getContext('2d')
|
||||
if (!ctx) return false
|
||||
const { data } = ctx.getImageData(0, 0, el.width, el.height)
|
||||
for (let i = 3; i < data.length; i += 4) {
|
||||
if (data[i] > 0) return true
|
||||
}
|
||||
for (let i = 3; i < data.length; i += 4) if (data[i] > 0) return true
|
||||
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
@@ -19,9 +19,8 @@ test.describe('Node Badge', { tag: ['@screenshot', '@smoke', '@node'] }, () => {
|
||||
const graph = app.graph
|
||||
const nodes = graph.nodes
|
||||
|
||||
for (const node of nodes) {
|
||||
for (const node of nodes)
|
||||
node.badges = [new LGraphBadge({ text: 'Test Badge' })]
|
||||
}
|
||||
|
||||
graph.setDirtyCanvas(true, true)
|
||||
})
|
||||
|
||||
@@ -18,9 +18,7 @@ test.describe(
|
||||
async function openMoreOptions(comfyPage: ComfyPage) {
|
||||
const ksamplerNodes =
|
||||
await comfyPage.nodeOps.getNodeRefsByTitle('KSampler')
|
||||
if (ksamplerNodes.length === 0) {
|
||||
throw new Error('No KSampler nodes found')
|
||||
}
|
||||
if (ksamplerNodes.length === 0) throw new Error('No KSampler nodes found')
|
||||
|
||||
// Drag the KSampler toward the lower-left so the menu has limited space below it.
|
||||
const nodePos = await ksamplerNodes[0].getPosition()
|
||||
|
||||
@@ -7,9 +7,7 @@ type ComfyPage = Parameters<Parameters<typeof test>[2]>[0]['comfyPage']
|
||||
|
||||
async function setVueMode(comfyPage: ComfyPage, enabled: boolean) {
|
||||
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', enabled)
|
||||
if (enabled) {
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
}
|
||||
if (enabled) await comfyPage.vueNodes.waitForNodes()
|
||||
}
|
||||
|
||||
async function addGhostAtCenter(comfyPage: ComfyPage) {
|
||||
|
||||
@@ -43,9 +43,8 @@ async function setLocaleAndWaitForWorkflowReload(
|
||||
const workflow = (window.app!.extensionManager as WorkspaceStore).workflow
|
||||
.activeWorkflow
|
||||
|
||||
if (!workflow) {
|
||||
if (!workflow)
|
||||
throw new Error('No active workflow while waiting for locale reload')
|
||||
}
|
||||
|
||||
const changeTracker = workflow.changeTracker.constructor as unknown as {
|
||||
isLoadingGraph: boolean
|
||||
@@ -56,9 +55,7 @@ async function setLocaleAndWaitForWorkflowReload(
|
||||
const timeoutAt = performance.now() + 5000
|
||||
|
||||
const tick = () => {
|
||||
if (changeTracker.isLoadingGraph) {
|
||||
sawLoading = true
|
||||
}
|
||||
if (changeTracker.isLoadingGraph) sawLoading = true
|
||||
|
||||
if (sawLoading && !changeTracker.isLoadingGraph) {
|
||||
resolve()
|
||||
@@ -99,9 +96,8 @@ test.describe('Node Help', { tag: ['@slow', '@ui'] }, () => {
|
||||
// Select a single node (KSampler) using node references
|
||||
const ksamplerNodes =
|
||||
await comfyPage.nodeOps.getNodeRefsByType('KSampler')
|
||||
if (ksamplerNodes.length === 0) {
|
||||
if (ksamplerNodes.length === 0)
|
||||
throw new Error('No KSampler nodes found in the workflow')
|
||||
}
|
||||
|
||||
// Select the node with panning to ensure toolbox is visible
|
||||
await selectNodeWithPan(comfyPage, ksamplerNodes[0])
|
||||
|
||||
@@ -248,9 +248,7 @@ test.describe('Painter', { tag: ['@widget', '@vue-nodes'] }, () => {
|
||||
await expect(sizeDisplay).toHaveText('20')
|
||||
|
||||
await sizeSlider.focus()
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await sizeSlider.press('ArrowRight')
|
||||
}
|
||||
for (let i = 0; i < 10; i++) await sizeSlider.press('ArrowRight')
|
||||
|
||||
await expect(sizeDisplay).toHaveText('30')
|
||||
})
|
||||
@@ -268,9 +266,7 @@ test.describe('Painter', { tag: ['@widget', '@vue-nodes'] }, () => {
|
||||
await expect(hardnessDisplay).toHaveText('100%')
|
||||
|
||||
await hardnessSlider.focus()
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await hardnessSlider.press('ArrowLeft')
|
||||
}
|
||||
for (let i = 0; i < 10; i++) await hardnessSlider.press('ArrowLeft')
|
||||
|
||||
await expect(hardnessDisplay).toHaveText('90%')
|
||||
})
|
||||
@@ -344,9 +340,9 @@ test.describe('Painter', { tag: ['@widget', '@vue-nodes'] }, () => {
|
||||
const cx = Math.floor(el.width / 2)
|
||||
const cy = Math.floor(el.height / 2)
|
||||
const { data } = ctx.getImageData(cx - 20, cy - 20, 40, 40)
|
||||
for (let i = 3; i < data.length; i += 4) {
|
||||
for (let i = 3; i < data.length; i += 4)
|
||||
if (data[i] > 50 && data[i] < 230) return true
|
||||
}
|
||||
|
||||
return false
|
||||
}),
|
||||
{
|
||||
@@ -677,9 +673,7 @@ test.describe('Painter', { tag: ['@widget', '@vue-nodes'] }, () => {
|
||||
).toHaveText('20')
|
||||
|
||||
await sizeSlider.focus()
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await sizeSlider.press('ArrowRight')
|
||||
}
|
||||
for (let i = 0; i < 10; i++) await sizeSlider.press('ArrowRight')
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
@@ -764,9 +758,8 @@ test.describe('Painter', { tag: ['@widget', '@vue-nodes'] }, () => {
|
||||
if (!ctx) return false
|
||||
const cy = Math.floor(el.height * y)
|
||||
const { data } = ctx.getImageData(0, cy - 5, el.width, 10)
|
||||
for (let i = 3; i < data.length; i += 4) {
|
||||
if (data[i] > 0) return true
|
||||
}
|
||||
for (let i = 3; i < data.length; i += 4) if (data[i] > 0) return true
|
||||
|
||||
return false
|
||||
}, yFraction)
|
||||
|
||||
|
||||
@@ -13,9 +13,7 @@ test.describe('Performance', { tag: ['@perf'] }, () => {
|
||||
|
||||
// Let the canvas idle for 2 seconds — no user interaction.
|
||||
// Measures baseline style recalcs from reactive state + render loop.
|
||||
for (let i = 0; i < 120; i++) {
|
||||
await comfyPage.nextFrame()
|
||||
}
|
||||
for (let i = 0; i < 120; i++) await comfyPage.nextFrame()
|
||||
|
||||
const m = await comfyPage.perf.stopMeasuring('canvas-idle')
|
||||
recordMeasurement(m)
|
||||
@@ -77,9 +75,7 @@ test.describe('Performance', { tag: ['@perf'] }, () => {
|
||||
await comfyPage.workflow.loadWorkflow('subgraphs/nested-subgraph')
|
||||
await comfyPage.perf.startMeasuring()
|
||||
|
||||
for (let i = 0; i < 120; i++) {
|
||||
await comfyPage.nextFrame()
|
||||
}
|
||||
for (let i = 0; i < 120; i++) await comfyPage.nextFrame()
|
||||
|
||||
const m = await comfyPage.perf.stopMeasuring('subgraph-idle')
|
||||
recordMeasurement(m)
|
||||
@@ -118,9 +114,7 @@ test.describe('Performance', { tag: ['@perf'] }, () => {
|
||||
|
||||
// Let the large graph idle for 2 seconds — measures compositor and
|
||||
// style recalculation cost at scale (245 nodes).
|
||||
for (let i = 0; i < 120; i++) {
|
||||
await comfyPage.nextFrame()
|
||||
}
|
||||
for (let i = 0; i < 120; i++) await comfyPage.nextFrame()
|
||||
|
||||
const m = await comfyPage.perf.stopMeasuring('large-graph-idle')
|
||||
recordMeasurement(m)
|
||||
@@ -263,9 +257,7 @@ test.describe('Performance', { tag: ['@perf'] }, () => {
|
||||
await comfyPage.perf.startMeasuring()
|
||||
|
||||
// Idle for 2 seconds with minimap open and 245 nodes
|
||||
for (let i = 0; i < 120; i++) {
|
||||
await comfyPage.nextFrame()
|
||||
}
|
||||
for (let i = 0; i < 120; i++) await comfyPage.nextFrame()
|
||||
|
||||
const m = await comfyPage.perf.stopMeasuring('minimap-idle')
|
||||
recordMeasurement(m)
|
||||
@@ -284,9 +276,7 @@ test.describe('Performance', { tag: ['@perf'] }, () => {
|
||||
test('idle', async ({ comfyPage }) => {
|
||||
await comfyPage.perf.startMeasuring()
|
||||
|
||||
for (let i = 0; i < 120; i++) {
|
||||
await comfyPage.nextFrame()
|
||||
}
|
||||
for (let i = 0; i < 120; i++) await comfyPage.nextFrame()
|
||||
|
||||
const m = await comfyPage.perf.stopMeasuring('vue-large-graph-idle')
|
||||
recordMeasurement(m)
|
||||
@@ -324,9 +314,7 @@ test.describe('Performance', { tag: ['@perf'] }, () => {
|
||||
|
||||
// Zoom out far enough that nodes become < 4px screen size
|
||||
// (triggers size-based culling in isNodeInViewport)
|
||||
for (let i = 0; i < 20; i++) {
|
||||
await comfyPage.canvasOps.zoom(100)
|
||||
}
|
||||
for (let i = 0; i < 20; i++) await comfyPage.canvasOps.zoom(100)
|
||||
|
||||
// Verify we actually entered the culling regime.
|
||||
// isNodeTooSmall triggers when max(width, height) * scale < 4px.
|
||||
@@ -334,14 +322,10 @@ test.describe('Performance', { tag: ['@perf'] }, () => {
|
||||
await expect.poll(() => comfyPage.canvasOps.getScale()).toBeLessThan(0.02)
|
||||
|
||||
// Idle at extreme zoom-out — most nodes should be culled
|
||||
for (let i = 0; i < 60; i++) {
|
||||
await comfyPage.nextFrame()
|
||||
}
|
||||
for (let i = 0; i < 60; i++) await comfyPage.nextFrame()
|
||||
|
||||
// Zoom back in
|
||||
for (let i = 0; i < 20; i++) {
|
||||
await comfyPage.canvasOps.zoom(-100)
|
||||
}
|
||||
for (let i = 0; i < 20; i++) await comfyPage.canvasOps.zoom(-100)
|
||||
|
||||
const m = await comfyPage.perf.stopMeasuring('vue-zoom-culling')
|
||||
recordMeasurement(m)
|
||||
|
||||
@@ -20,9 +20,8 @@ test.describe('Preview as Text node', () => {
|
||||
await comfyPage.page.evaluate(() => {
|
||||
const node = window.app!.graph.nodes.find((n) => n.type === 'PreviewAny')!
|
||||
for (const widget of node.widgets ?? []) {
|
||||
if (widget.name?.startsWith('preview_')) {
|
||||
if (widget.name?.startsWith('preview_'))
|
||||
widget.value = 'rendered preview content from previous execution'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -98,28 +98,22 @@ function setComboInputOptions(
|
||||
values: string[]
|
||||
) {
|
||||
const nodeInfo = objectInfo[nodeType]
|
||||
if (!nodeInfo) {
|
||||
throw new Error(`Missing object_info entry for ${nodeType}`)
|
||||
}
|
||||
if (!nodeInfo) throw new Error(`Missing object_info entry for ${nodeType}`)
|
||||
|
||||
const requiredInputs = nodeInfo.input?.required
|
||||
if (!requiredInputs) {
|
||||
if (!requiredInputs)
|
||||
throw new Error(`Missing required inputs for ${nodeType}`)
|
||||
}
|
||||
|
||||
const input = requiredInputs[inputName]
|
||||
if (!Array.isArray(input)) {
|
||||
if (!Array.isArray(input))
|
||||
throw new Error(`Expected ${nodeType}.${inputName} to be a combo input`)
|
||||
}
|
||||
|
||||
const [valuesOrType, options] = input
|
||||
const optionsObject =
|
||||
options && typeof options === 'object' && !Array.isArray(options)
|
||||
if (Array.isArray(valuesOrType)) {
|
||||
input[0] = values
|
||||
} else if (valuesOrType !== 'COMBO') {
|
||||
if (Array.isArray(valuesOrType)) input[0] = values
|
||||
else if (valuesOrType !== 'COMBO')
|
||||
throw new Error(`Expected ${nodeType}.${inputName} to have combo options`)
|
||||
}
|
||||
|
||||
if (optionsObject) {
|
||||
Object.assign(options, { options: values })
|
||||
|
||||
@@ -390,9 +390,8 @@ test.describe('Errors tab - Mode-aware errors', { tag: '@ui' }, () => {
|
||||
|
||||
await comfyPage.page.evaluate((value) => {
|
||||
const hostNode = window.app!.graph!.getNodeById(2)
|
||||
if (!hostNode?.isSubgraphNode()) {
|
||||
if (!hostNode?.isSubgraphNode())
|
||||
throw new Error('Expected subgraph host node')
|
||||
}
|
||||
|
||||
const interiorNode = hostNode.subgraph.getNodeById(1)
|
||||
const widget = interiorNode?.widgets?.find(
|
||||
@@ -410,9 +409,8 @@ test.describe('Errors tab - Mode-aware errors', { tag: '@ui' }, () => {
|
||||
}
|
||||
const settableWidget = widget as SettableWidget | undefined
|
||||
|
||||
if (!settableWidget?.setValue) {
|
||||
if (!settableWidget?.setValue)
|
||||
throw new Error('Expected concrete ckpt_name widget')
|
||||
}
|
||||
|
||||
settableWidget.setValue(value, {
|
||||
e: new PointerEvent('pointerup'),
|
||||
|
||||
@@ -151,9 +151,8 @@ test.describe('Remote COMBO Widget', { tag: '@widget' }, () => {
|
||||
let requestWasMade = false
|
||||
|
||||
comfyPage.page.on('request', (request) => {
|
||||
if (request.url().includes('/api/models/checkpoints')) {
|
||||
if (request.url().includes('/api/models/checkpoints'))
|
||||
requestWasMade = true
|
||||
}
|
||||
})
|
||||
|
||||
expect(requestWasMade).toBe(false)
|
||||
|
||||
@@ -62,9 +62,8 @@ async function expectNodePositionStable(
|
||||
|
||||
async function setVueMode(comfyPage: ComfyPage, enabled: boolean) {
|
||||
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', enabled)
|
||||
if (enabled) {
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
}
|
||||
if (enabled) await comfyPage.vueNodes.waitForNodes()
|
||||
|
||||
await comfyPage.nextFrame()
|
||||
}
|
||||
|
||||
|
||||
@@ -123,9 +123,8 @@ test.describe(
|
||||
const canvas = window['app']?.canvas
|
||||
if (!canvas?.renderedPaths) return null
|
||||
for (const segment of canvas.renderedPaths) {
|
||||
if (segment.id === 5 && segment._pos) {
|
||||
if (segment.id === 5 && segment._pos)
|
||||
return { x: segment._pos[0], y: segment._pos[1] }
|
||||
}
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
@@ -98,9 +98,7 @@ test.describe(
|
||||
await expect(renameItem).toBeVisible()
|
||||
|
||||
// Wait for multiple frames to allow PrimeVue's outside click handler to initialize
|
||||
for (let i = 0; i < 30; i++) {
|
||||
await comfyPage.nextFrame()
|
||||
}
|
||||
for (let i = 0; i < 30; i++) await comfyPage.nextFrame()
|
||||
|
||||
await comfyPage.canvasOps.mouseClickAt({ x: 0, y: 50 })
|
||||
await expect(
|
||||
|
||||
@@ -126,9 +126,8 @@ test.describe('Assets sidebar - media type filter', { tag: '@cloud' }, () => {
|
||||
tab.filterVideoCheckbox,
|
||||
tab.filterAudioCheckbox,
|
||||
tab.filter3DCheckbox
|
||||
]) {
|
||||
])
|
||||
await expect(cb).toHaveAttribute('aria-checked', 'false')
|
||||
}
|
||||
})
|
||||
|
||||
test('Selecting only "Image" hides non-image assets', async ({
|
||||
|
||||
@@ -11,9 +11,9 @@ test.describe('Sidebar splitter width independence', () => {
|
||||
|
||||
async function dismissToasts(comfyPage: ComfyPage) {
|
||||
const buttons = await comfyPage.page.locator('.p-toast-close-button').all()
|
||||
for (const btn of buttons) {
|
||||
for (const btn of buttons)
|
||||
await btn.click({ timeout: 2000 }).catch(() => {})
|
||||
}
|
||||
|
||||
// Brief wait for animations
|
||||
await comfyPage.nextFrame()
|
||||
}
|
||||
|
||||
@@ -40,9 +40,8 @@ test.describe('Subgraph CRUD', { tag: ['@slow', '@subgraph'] }, () => {
|
||||
const result = await comfyPage.page.evaluate(() => {
|
||||
const graph = window.app!.graph!
|
||||
const subgraphNode = graph.nodes.find((n) => n.isSubgraphNode())
|
||||
if (!subgraphNode || !subgraphNode.isSubgraphNode()) {
|
||||
if (!subgraphNode || !subgraphNode.isSubgraphNode())
|
||||
return { error: 'No subgraph node found' }
|
||||
}
|
||||
|
||||
graph.unpackSubgraph(subgraphNode)
|
||||
|
||||
|
||||
@@ -12,9 +12,8 @@ async function openSubgraphById(comfyPage: ComfyPage, nodeId: string) {
|
||||
const node = window.app!.rootGraph.nodes.find(
|
||||
(candidate) => String(candidate.id) === targetNodeId
|
||||
)
|
||||
if (!node || !('subgraph' in node) || !node.subgraph) {
|
||||
if (!node || !('subgraph' in node) || !node.subgraph)
|
||||
throw new Error(`Subgraph node ${targetNodeId} not found`)
|
||||
}
|
||||
|
||||
window.app!.canvas.openSubgraph(node.subgraph, node)
|
||||
}, nodeId)
|
||||
|
||||
@@ -42,9 +42,8 @@ async function getPrimitiveFanoutSnapshot(
|
||||
return comfyPage.page.evaluate((id) => {
|
||||
const graph = window.app!.canvas.graph!
|
||||
const hostNode = graph.getNodeById(Number(id))
|
||||
if (!hostNode?.isSubgraphNode?.()) {
|
||||
if (!hostNode?.isSubgraphNode?.())
|
||||
throw new Error(`Host node ${id} is not a SubgraphNode`)
|
||||
}
|
||||
|
||||
const [primitiveNode] = hostNode.subgraph.findNodesByType(
|
||||
'PrimitiveNode',
|
||||
@@ -335,9 +334,8 @@ test.describe('Subgraph Serialization', { tag: ['@subgraph'] }, () => {
|
||||
const widgets = await getPromotedWidgets(comfyPage, '11')
|
||||
expect(widgets.length).toBeGreaterThan(0)
|
||||
|
||||
for (const [interiorNodeId] of widgets) {
|
||||
for (const [interiorNodeId] of widgets)
|
||||
expect(Number(interiorNodeId)).toBeGreaterThan(0)
|
||||
}
|
||||
|
||||
await expectPromotedWidgetsToResolveToInteriorNodes(
|
||||
comfyPage,
|
||||
@@ -609,9 +607,9 @@ test.describe('Subgraph Serialization', { tag: ['@subgraph'] }, () => {
|
||||
g: typeof graph
|
||||
): string | null => {
|
||||
if (isSentinelNodeId(id)) return null
|
||||
if (typeof id !== 'number' || !g._nodes_by_id[id]) {
|
||||
if (typeof id !== 'number' || !g._nodes_by_id[id])
|
||||
return `${label}: ${kind} ${id} invalid or not found`
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
@@ -267,9 +267,8 @@ test.describe('Subgraph Slots', { tag: ['@slow', '@subgraph'] }, () => {
|
||||
app.canvas.linkConnector
|
||||
)
|
||||
|
||||
if (app.canvas.pointer.onDoubleClick) {
|
||||
if (app.canvas.pointer.onDoubleClick)
|
||||
app.canvas.pointer.onDoubleClick(leftClickEvent)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -8,9 +8,9 @@ import { TestIds } from '@e2e/fixtures/selectors'
|
||||
|
||||
async function ensurePropertiesPanel(comfyPage: ComfyPage) {
|
||||
const panel = comfyPage.menu.propertiesPanel.root
|
||||
if (!(await panel.isVisible())) {
|
||||
if (!(await panel.isVisible()))
|
||||
await comfyPage.actionbar.propertiesButton.click()
|
||||
}
|
||||
|
||||
await expect(panel).toBeVisible()
|
||||
return panel
|
||||
}
|
||||
@@ -64,9 +64,8 @@ test.describe(
|
||||
await expect(toggleButtons.first()).toBeVisible()
|
||||
|
||||
const count = await toggleButtons.count()
|
||||
for (let i = 0; i < count; i++) {
|
||||
for (let i = 0; i < count; i++)
|
||||
await expect(toggleButtons.nth(i)).toBeDisabled()
|
||||
}
|
||||
})
|
||||
|
||||
test('linked promoted widgets show link icon instead of eye icon', async ({
|
||||
|
||||
@@ -59,25 +59,21 @@ async function getNodeGroupCenteringErrors(
|
||||
const app = window.app!
|
||||
const node = app.graph.nodes[0] as GraphNode | undefined
|
||||
|
||||
if (!node) {
|
||||
throw new Error('Expected a node in the loaded workflow')
|
||||
}
|
||||
if (!node) throw new Error('Expected a node in the loaded workflow')
|
||||
|
||||
const nodeElement = document.querySelector<HTMLElement>(
|
||||
`[data-node-id="${node.id}"]`
|
||||
)
|
||||
|
||||
if (!nodeElement) {
|
||||
if (!nodeElement)
|
||||
throw new Error(`Vue node element not found for node ${node.id}`)
|
||||
}
|
||||
|
||||
const groups = app.graph.groups as GraphGroup[]
|
||||
const innerGroup = groups.find((group) => group.title === 'Inner Group')
|
||||
const outerGroup = groups.find((group) => group.title === 'Outer Group')
|
||||
|
||||
if (!innerGroup || !outerGroup) {
|
||||
if (!innerGroup || !outerGroup)
|
||||
throw new Error('Expected both Inner Group and Outer Group in graph')
|
||||
}
|
||||
|
||||
const nodeRect = nodeElement.getBoundingClientRect()
|
||||
|
||||
|
||||
@@ -492,9 +492,7 @@ test.describe(
|
||||
await comfyMouse.drop()
|
||||
dropped = true
|
||||
} finally {
|
||||
if (!dropped) {
|
||||
await comfyMouse.drop().catch(() => {})
|
||||
}
|
||||
if (!dropped) await comfyMouse.drop().catch(() => {})
|
||||
}
|
||||
|
||||
await expect
|
||||
@@ -1155,9 +1153,8 @@ test.describe('Vue Node Widget Link Position', { tag: '@vue-nodes' }, () => {
|
||||
schedulerIndex: findIndex('scheduler')
|
||||
}
|
||||
})
|
||||
if (!ksampler) {
|
||||
throw new Error('KSampler should be present in fixture')
|
||||
}
|
||||
if (!ksampler) throw new Error('KSampler should be present in fixture')
|
||||
|
||||
expect(
|
||||
ksampler.denoiseIndex,
|
||||
'denoise input slot not found'
|
||||
|
||||
@@ -80,17 +80,13 @@ test.describe(
|
||||
await expect.poll(node.pollWidth).toBeGreaterThan(box.width)
|
||||
await expect.poll(node.pollHeight).toBeGreaterThan(box.height)
|
||||
|
||||
if (hasWestEdge(corner)) {
|
||||
if (hasWestEdge(corner))
|
||||
await expect.poll(node.pollLeftEdge).toBeLessThan(box.x)
|
||||
} else {
|
||||
await expect.poll(node.pollLeftEdge).toBeCloseTo(box.x, 0)
|
||||
}
|
||||
else await expect.poll(node.pollLeftEdge).toBeCloseTo(box.x, 0)
|
||||
|
||||
if (hasNorthEdge(corner)) {
|
||||
if (hasNorthEdge(corner))
|
||||
await expect.poll(node.pollTopEdge).toBeLessThan(box.y)
|
||||
} else {
|
||||
await expect.poll(node.pollTopEdge).toBeCloseTo(box.y, 0)
|
||||
}
|
||||
else await expect.poll(node.pollTopEdge).toBeCloseTo(box.y, 0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -142,9 +142,9 @@ test.describe('Vue Node Error', { tag: '@vue-nodes' }, () => {
|
||||
const node = window.app!.graph.getNodeById(nodeId)
|
||||
const index =
|
||||
node?.inputs?.findIndex((input) => input.name === inputName) ?? -1
|
||||
if (index < 0) {
|
||||
if (index < 0)
|
||||
throw new Error(`Input slot "${inputName}" not found`)
|
||||
}
|
||||
|
||||
return index
|
||||
},
|
||||
{ nodeId: ksamplerId, inputName: KSAMPLER_MODEL_INPUT_NAME }
|
||||
@@ -264,9 +264,8 @@ test.describe('Vue Node Error', { tag: '@vue-nodes' }, () => {
|
||||
await test.step('drop an image onto the Load Image node', async () => {
|
||||
const dropPosition =
|
||||
await comfyPage.canvasOps.getNodeCenterByTitle('Load Image')
|
||||
if (!dropPosition) {
|
||||
if (!dropPosition)
|
||||
throw new Error('Load Image node center must be available for drop')
|
||||
}
|
||||
|
||||
await comfyPage.dragDrop.dragAndDropFile(LOAD_IMAGE_UPLOAD_FILE, {
|
||||
dropPosition,
|
||||
|
||||
@@ -49,9 +49,7 @@ test.describe('Vue Combo Widget', { tag: ['@vue-nodes', '@widget'] }, () => {
|
||||
await expect.poll(() => viewport.boundingBox()).not.toBeNull()
|
||||
|
||||
const box = await viewport.boundingBox()
|
||||
if (!box) {
|
||||
throw new Error('Widget select viewport is not visible')
|
||||
}
|
||||
if (!box) throw new Error('Widget select viewport is not visible')
|
||||
|
||||
return box
|
||||
}
|
||||
@@ -268,9 +266,7 @@ test.describe('Vue Combo Widget', { tag: ['@vue-nodes', '@widget'] }, () => {
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
|
||||
const [ksamplerNode] = await comfyPage.nodeOps.getNodeRefsByType('KSampler')
|
||||
if (!ksamplerNode) {
|
||||
throw new Error('KSampler node not found after reload')
|
||||
}
|
||||
if (!ksamplerNode) throw new Error('KSampler node not found after reload')
|
||||
|
||||
const schedulerWidget = await ksamplerNode.getWidgetByName('scheduler')
|
||||
await expect.poll(() => schedulerWidget.getValue()).toBe('karras')
|
||||
|
||||
@@ -68,9 +68,7 @@ test.describe('Vue Float Widget', { tag: '@vue-nodes' }, () => {
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
|
||||
const [ksamplerNode] = await comfyPage.nodeOps.getNodeRefsByType('KSampler')
|
||||
if (!ksamplerNode) {
|
||||
throw new Error('KSampler node not found after reload')
|
||||
}
|
||||
if (!ksamplerNode) throw new Error('KSampler node not found after reload')
|
||||
|
||||
const cfgWidgetAfterReload = await ksamplerNode.getWidgetByName('cfg')
|
||||
await expect.poll(() => cfgWidgetAfterReload.getValue()).toBe(7.5)
|
||||
|
||||
@@ -340,9 +340,9 @@ test.describe('Image Crop', { tag: ['@widget', '@vue-nodes'] }, () => {
|
||||
v.y <= valueBefore.y ||
|
||||
v.width !== valueBefore.width ||
|
||||
v.height !== valueBefore.height
|
||||
) {
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
return v
|
||||
},
|
||||
{
|
||||
@@ -811,9 +811,9 @@ test.describe('Image Crop', { tag: ['@widget', '@vue-nodes'] }, () => {
|
||||
.poll(
|
||||
async () => {
|
||||
const v = await getCropValue(comfyPage, 2)
|
||||
if (!v || v.width <= before.width || v.height <= before.height) {
|
||||
if (!v || v.width <= before.width || v.height <= before.height)
|
||||
return null
|
||||
}
|
||||
|
||||
const r = v.width / v.height
|
||||
if (Math.abs(r - ratio) > 0.06) return null
|
||||
return v
|
||||
|
||||
@@ -16,12 +16,9 @@ const waitForWorkflowTabState = async (comfyPage: ComfyPage, minPaths = 2) => {
|
||||
|
||||
for (let i = 0; i < window.sessionStorage.length; i++) {
|
||||
const key = window.sessionStorage.key(i)
|
||||
if (key?.startsWith('Comfy.Workflow.ActivePath:')) {
|
||||
hasActivePath = true
|
||||
}
|
||||
if (!key?.startsWith('Comfy.Workflow.OpenPaths:')) {
|
||||
continue
|
||||
}
|
||||
if (key?.startsWith('Comfy.Workflow.ActivePath:')) hasActivePath = true
|
||||
|
||||
if (!key?.startsWith('Comfy.Workflow.OpenPaths:')) continue
|
||||
|
||||
const raw = window.sessionStorage.getItem(key)
|
||||
if (!raw) continue
|
||||
|
||||
@@ -30,9 +30,8 @@ test.describe('Workflow Tab Thumbnails', { tag: '@workflow' }, () => {
|
||||
const popover = comfyPage.page.locator('.workflow-popover-fade')
|
||||
await expect(popover).toHaveCount(1)
|
||||
await expect(popover).toBeVisible()
|
||||
if (name) {
|
||||
await expect(popover).toContainText(name)
|
||||
}
|
||||
if (name) await expect(popover).toContainText(name)
|
||||
|
||||
return popover
|
||||
}
|
||||
|
||||
|
||||
@@ -121,11 +121,10 @@ test.describe('WebSocket reconnect with stale job', { tag: '@ui' }, () => {
|
||||
|
||||
await triggerReconnect(comfyPage, ws, scenario, jobId)
|
||||
|
||||
if (scenario.expectsActiveAfter) {
|
||||
if (scenario.expectsActiveAfter)
|
||||
await expect(firstSkeleton).toBeVisible()
|
||||
} else {
|
||||
else
|
||||
await expect(comfyPage.appMode.outputHistory.skeletons).toHaveCount(0)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -200,11 +199,9 @@ test.describe('WebSocket reconnect with stale job', { tag: '@ui' }, () => {
|
||||
|
||||
await triggerReconnect(comfyPage, ws, scenario, jobId)
|
||||
|
||||
if (scenario.expectsActiveAfter) {
|
||||
if (scenario.expectsActiveAfter)
|
||||
await expect(ksamplerNode).toHaveClass(EXECUTING_CLASS)
|
||||
} else {
|
||||
await expect(ksamplerNode).not.toHaveClass(EXECUTING_CLASS)
|
||||
}
|
||||
else await expect(ksamplerNode).not.toHaveClass(EXECUTING_CLASS)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -76,9 +76,8 @@ test.describe('Zoom Controls', { tag: '@canvas' }, () => {
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const zoomOut = comfyPage.page.getByTestId(TestIds.canvas.zoomOutAction)
|
||||
for (let i = 0; i < 30; i++) {
|
||||
await zoomOut.click()
|
||||
}
|
||||
for (let i = 0; i < 30; i++) await zoomOut.click()
|
||||
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await expect.poll(() => comfyPage.canvasOps.getScale()).toBeCloseTo(0.1, 1)
|
||||
|
||||
14
src/App.vue
14
src/App.vue
@@ -27,9 +27,8 @@ const isLoading = computed<boolean>(() => workspaceStore.spinner)
|
||||
watch(
|
||||
isLoading,
|
||||
(loading, prevLoading) => {
|
||||
if (prevLoading && !loading) {
|
||||
if (prevLoading && !loading)
|
||||
document.getElementById('splash-loader')?.remove()
|
||||
}
|
||||
},
|
||||
{ flush: 'post' }
|
||||
)
|
||||
@@ -61,9 +60,7 @@ function handleResourceError(url: string, tagName: string) {
|
||||
onMounted(() => {
|
||||
window['__COMFYUI_FRONTEND_VERSION__'] = config.app_version
|
||||
|
||||
if (isDesktop) {
|
||||
document.addEventListener('contextmenu', showContextMenu)
|
||||
}
|
||||
if (isDesktop) document.addEventListener('contextmenu', showContextMenu)
|
||||
|
||||
// Handle preload errors that occur during dynamic imports (e.g., stale chunks after deployment)
|
||||
// See: https://vite.dev/guide/build#load-error-handling
|
||||
@@ -111,14 +108,13 @@ onMounted(() => {
|
||||
'error',
|
||||
(event) => {
|
||||
const target = event.target
|
||||
if (target instanceof HTMLScriptElement) {
|
||||
if (target instanceof HTMLScriptElement)
|
||||
handleResourceError(target.src, 'script')
|
||||
} else if (
|
||||
else if (
|
||||
target instanceof HTMLLinkElement &&
|
||||
target.rel === 'stylesheet'
|
||||
) {
|
||||
)
|
||||
handleResourceError(target.href, 'link')
|
||||
}
|
||||
},
|
||||
true
|
||||
)
|
||||
|
||||
@@ -24,9 +24,7 @@ export function assert(condition: unknown, message: string): asserts condition {
|
||||
const formatted = `[Assertion failed]: ${message}`
|
||||
console.error(formatted)
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
throw new Error(formatted)
|
||||
}
|
||||
if (import.meta.env.DEV) throw new Error(formatted)
|
||||
|
||||
try {
|
||||
reporter?.(formatted)
|
||||
|
||||
@@ -47,9 +47,7 @@ export let runWhenGlobalIdle: (
|
||||
// Fallback implementation for browsers without native support (e.g., Safari)
|
||||
_runWhenIdle = (_targetWindow, runner, _timeout?) => {
|
||||
setTimeout(() => {
|
||||
if (disposed) {
|
||||
return
|
||||
}
|
||||
if (disposed) return
|
||||
|
||||
// Simulate IdleDeadline - give 15ms window (one frame at ~64fps)
|
||||
const end = Date.now() + 15
|
||||
@@ -66,9 +64,8 @@ export let runWhenGlobalIdle: (
|
||||
let disposed = false
|
||||
return {
|
||||
dispose() {
|
||||
if (disposed) {
|
||||
return
|
||||
}
|
||||
if (disposed) return
|
||||
|
||||
disposed = true
|
||||
}
|
||||
}
|
||||
@@ -84,9 +81,8 @@ export let runWhenGlobalIdle: (
|
||||
let disposed = false
|
||||
return {
|
||||
dispose() {
|
||||
if (disposed) {
|
||||
return
|
||||
}
|
||||
if (disposed) return
|
||||
|
||||
disposed = true
|
||||
targetWindow.cancelIdleCallback(handle)
|
||||
}
|
||||
|
||||
@@ -33,9 +33,8 @@ function triggerLinkDownload(href: string, filename: string): void {
|
||||
* @throws {Error} If the URL is invalid or empty
|
||||
*/
|
||||
export function downloadFile(url: string, filename?: string): void {
|
||||
if (!url || typeof url !== 'string' || url.trim().length === 0) {
|
||||
if (!url || typeof url !== 'string' || url.trim().length === 0)
|
||||
throw new Error('Invalid URL provided for download')
|
||||
}
|
||||
|
||||
const inferredFilename =
|
||||
filename || extractFilenameFromUrl(url) || DEFAULT_DOWNLOAD_FILENAME
|
||||
@@ -103,15 +102,11 @@ export function extractFilenameFromContentDisposition(
|
||||
|
||||
// Try simple quoted format: filename="..."
|
||||
const quotedMatch = header.match(/filename="([^"]+)"/i)
|
||||
if (quotedMatch?.[1]) {
|
||||
return quotedMatch[1]
|
||||
}
|
||||
if (quotedMatch?.[1]) return quotedMatch[1]
|
||||
|
||||
// Try unquoted format: filename=...
|
||||
const unquotedMatch = header.match(/filename=([^;\s]+)/i)
|
||||
if (unquotedMatch?.[1]) {
|
||||
return unquotedMatch[1]
|
||||
}
|
||||
if (unquotedMatch?.[1]) return unquotedMatch[1]
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -122,9 +117,9 @@ export function extractFilenameFromContentDisposition(
|
||||
*/
|
||||
async function fetchAsBlob(url: string): Promise<Response> {
|
||||
const response = await fetch(url)
|
||||
if (!response.ok) {
|
||||
if (!response.ok)
|
||||
throw new Error(`Failed to fetch ${url}: ${response.status}`)
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
|
||||
@@ -21,9 +21,8 @@ const formatNumber = ({
|
||||
typeof merged.maximumFractionDigits === 'number' &&
|
||||
typeof merged.minimumFractionDigits === 'number' &&
|
||||
merged.maximumFractionDigits < merged.minimumFractionDigits
|
||||
) {
|
||||
)
|
||||
merged.minimumFractionDigits = merged.maximumFractionDigits
|
||||
}
|
||||
|
||||
return new Intl.NumberFormat(locale, merged).format(value)
|
||||
}
|
||||
|
||||
@@ -38,9 +38,9 @@ function hasWebViewBridge(): boolean {
|
||||
win.webkit !== null &&
|
||||
typeof (win.webkit as Record<string, unknown>).messageHandlers ===
|
||||
'object'
|
||||
) {
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
if (win.ReactNativeWebView != null) return true
|
||||
} catch {
|
||||
// Access to bridge objects may throw in sandboxed contexts
|
||||
|
||||
@@ -205,9 +205,9 @@ const sidebarTabKey = computed(() => {
|
||||
|
||||
const sidebarStateKey = computed(() => {
|
||||
const base = sidebarTabKey.value
|
||||
if (sidebarLocation.value === 'left' && !showOffsideSplitter.value) {
|
||||
if (sidebarLocation.value === 'left' && !showOffsideSplitter.value)
|
||||
return base
|
||||
}
|
||||
|
||||
const suffix = showOffsideSplitter.value ? '-with-offside' : ''
|
||||
return `${base}-${sidebarLocation.value}${suffix}`
|
||||
})
|
||||
@@ -241,9 +241,9 @@ function normalizeSavedSizes() {
|
||||
!Array.isArray(parsed) ||
|
||||
parsed.length === 0 ||
|
||||
parsed.some((s) => typeof s !== 'number' || !Number.isFinite(s))
|
||||
) {
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const sum = parsed.reduce((a, b) => a + b, 0)
|
||||
if (sum <= 0 || Math.abs(sum - 100) <= 0.5) return
|
||||
localStorage.setItem(
|
||||
@@ -265,17 +265,17 @@ const splitterRefreshKey = computed(() => {
|
||||
|
||||
const firstPanelStyle = computed(() => {
|
||||
if (focusMode.value) return { display: 'none' }
|
||||
if (sidebarLocation.value === 'left') {
|
||||
if (sidebarLocation.value === 'left')
|
||||
return { display: sidebarPanelVisible.value ? 'flex' : 'none' }
|
||||
}
|
||||
|
||||
return undefined
|
||||
})
|
||||
|
||||
const lastPanelStyle = computed(() => {
|
||||
if (focusMode.value) return { display: 'none' }
|
||||
if (sidebarLocation.value === 'right') {
|
||||
if (sidebarLocation.value === 'right')
|
||||
return { display: sidebarPanelVisible.value ? 'flex' : 'none' }
|
||||
}
|
||||
|
||||
return undefined
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -34,13 +34,9 @@ const exitFocusMode = () => {
|
||||
}
|
||||
|
||||
watchEffect(() => {
|
||||
if (settingStore.get('Comfy.UseNewMenu') !== 'Disabled') {
|
||||
return
|
||||
}
|
||||
if (workspaceState.focusMode) {
|
||||
app.ui.menuContainer.style.display = 'none'
|
||||
} else {
|
||||
app.ui.menuContainer.style.display = 'block'
|
||||
}
|
||||
if (settingStore.get('Comfy.UseNewMenu') !== 'Disabled') return
|
||||
|
||||
if (workspaceState.focusMode) app.ui.menuContainer.style.display = 'none'
|
||||
else app.ui.menuContainer.style.display = 'block'
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -167,9 +167,9 @@ function getLegacyCommandsContainer(container: Element): HTMLElement {
|
||||
const legacyContainer = container.querySelector(
|
||||
'[data-testid="legacy-topbar-container"]'
|
||||
)
|
||||
if (!(legacyContainer instanceof HTMLElement)) {
|
||||
if (!(legacyContainer instanceof HTMLElement))
|
||||
throw new Error('Expected legacy commands container to be present')
|
||||
}
|
||||
|
||||
return legacyContainer
|
||||
}
|
||||
|
||||
|
||||
@@ -244,9 +244,9 @@ function updateProgressTarget(target: HTMLElement | null) {
|
||||
progressTarget.value = target
|
||||
}
|
||||
const inlineProgressSummaryTarget = computed(() => {
|
||||
if (!shouldShowInlineProgressSummary.value || !isActionbarFloating.value) {
|
||||
if (!shouldShowInlineProgressSummary.value || !isActionbarFloating.value)
|
||||
return null
|
||||
}
|
||||
|
||||
return progressTarget.value
|
||||
})
|
||||
const shouldHideInlineProgressSummary = computed(
|
||||
|
||||
@@ -87,9 +87,7 @@ const incrementButtonClass = 'rounded-tr-none border-b border-border-subtle'
|
||||
const decrementButtonClass = 'rounded-br-none'
|
||||
|
||||
watch(batchCount, (nextBatchCount) => {
|
||||
if (!isEditing.value) {
|
||||
batchCountInput.value = String(nextBatchCount)
|
||||
}
|
||||
if (!isEditing.value) batchCountInput.value = String(nextBatchCount)
|
||||
})
|
||||
|
||||
const clampBatchCount = (nextBatchCount: number): number =>
|
||||
|
||||
@@ -178,9 +178,7 @@ const setInitialPosition = () => {
|
||||
const menuWidth = panel.offsetWidth
|
||||
const menuHeight = panel.offsetHeight
|
||||
|
||||
if (menuWidth === 0 || menuHeight === 0) {
|
||||
return
|
||||
}
|
||||
if (menuWidth === 0 || menuHeight === 0) return
|
||||
|
||||
// Check if stored position exists and is within bounds
|
||||
if (storedPosition.value.x !== 0 || storedPosition.value.y !== 0) {
|
||||
@@ -212,9 +210,7 @@ async function comfyRunButtonResolved() {
|
||||
}
|
||||
|
||||
watch(visible, async (newVisible) => {
|
||||
if (newVisible) {
|
||||
await nextTick(setInitialPosition)
|
||||
}
|
||||
if (newVisible) await nextTick(setInitialPosition)
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -314,15 +310,11 @@ const isMouseOverDropZone = ref(false)
|
||||
|
||||
// Mouse event handlers for self-contained drop zone
|
||||
const onMouseEnterDropZone = () => {
|
||||
if (isDragging.value) {
|
||||
isMouseOverDropZone.value = true
|
||||
}
|
||||
if (isDragging.value) isMouseOverDropZone.value = true
|
||||
}
|
||||
|
||||
const onMouseLeaveDropZone = () => {
|
||||
if (isDragging.value) {
|
||||
isMouseOverDropZone.value = false
|
||||
}
|
||||
if (isDragging.value) isMouseOverDropZone.value = false
|
||||
}
|
||||
|
||||
const inlineProgressTarget = computed(() => {
|
||||
@@ -330,9 +322,9 @@ const inlineProgressTarget = computed(() => {
|
||||
!visible.value ||
|
||||
!isQueuePanelV2Enabled.value ||
|
||||
!isRunProgressBarEnabled.value
|
||||
) {
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
if (isDocked.value) return topMenuContainer ?? null
|
||||
return panelElement.value
|
||||
})
|
||||
@@ -351,14 +343,11 @@ watch(
|
||||
watch(isDragging, (dragging) => {
|
||||
if (dragging) {
|
||||
// Starting to drag - undock if docked
|
||||
if (isDocked.value) {
|
||||
isDocked.value = false
|
||||
}
|
||||
if (isDocked.value) isDocked.value = false
|
||||
} else {
|
||||
// Stopped dragging - dock if mouse is over drop zone
|
||||
if (isMouseOverDropZone.value) {
|
||||
isDocked.value = true
|
||||
}
|
||||
if (isMouseOverDropZone.value) isDocked.value = true
|
||||
|
||||
// Reset drop zone state
|
||||
isMouseOverDropZone.value = false
|
||||
}
|
||||
|
||||
@@ -187,37 +187,28 @@ const queueMenuTriggerClass =
|
||||
const queueMenuItemButtonClass = 'w-full justify-start font-normal'
|
||||
|
||||
const iconClass = computed(() => {
|
||||
if (isStopInstantAction.value) {
|
||||
return 'icon-[lucide--square]'
|
||||
}
|
||||
if (hasMissingNodes.value) {
|
||||
return 'icon-[lucide--triangle-alert]'
|
||||
}
|
||||
if (workspaceStore.shiftDown) {
|
||||
return 'icon-[lucide--list-start]'
|
||||
}
|
||||
if (queueMode.value === 'disabled') {
|
||||
return 'icon-[lucide--play]'
|
||||
}
|
||||
if (isInstantMode(queueMode.value)) {
|
||||
return 'icon-[lucide--fast-forward]'
|
||||
}
|
||||
if (queueMode.value === 'change') {
|
||||
return 'icon-[lucide--step-forward]'
|
||||
}
|
||||
if (isStopInstantAction.value) return 'icon-[lucide--square]'
|
||||
|
||||
if (hasMissingNodes.value) return 'icon-[lucide--triangle-alert]'
|
||||
|
||||
if (workspaceStore.shiftDown) return 'icon-[lucide--list-start]'
|
||||
|
||||
if (queueMode.value === 'disabled') return 'icon-[lucide--play]'
|
||||
|
||||
if (isInstantMode(queueMode.value)) return 'icon-[lucide--fast-forward]'
|
||||
|
||||
if (queueMode.value === 'change') return 'icon-[lucide--step-forward]'
|
||||
|
||||
return 'icon-[lucide--play]'
|
||||
})
|
||||
|
||||
const queueButtonTooltip = computed(() => {
|
||||
if (isStopInstantAction.value) {
|
||||
return t('menu.stopRunInstantTooltip')
|
||||
}
|
||||
if (hasMissingNodes.value) {
|
||||
return t('menu.runWorkflowDisabled')
|
||||
}
|
||||
if (workspaceStore.shiftDown) {
|
||||
return t('menu.runWorkflowFront')
|
||||
}
|
||||
if (isStopInstantAction.value) return t('menu.stopRunInstantTooltip')
|
||||
|
||||
if (hasMissingNodes.value) return t('menu.runWorkflowDisabled')
|
||||
|
||||
if (workspaceStore.shiftDown) return t('menu.runWorkflowFront')
|
||||
|
||||
return t('menu.runWorkflow')
|
||||
})
|
||||
|
||||
@@ -233,9 +224,7 @@ const queuePrompt = async (e: Event) => {
|
||||
? 'Comfy.QueuePromptFront'
|
||||
: 'Comfy.QueuePrompt'
|
||||
|
||||
if (isInstantMode(queueMode.value)) {
|
||||
queueMode.value = 'instant-running'
|
||||
}
|
||||
if (isInstantMode(queueMode.value)) queueMode.value = 'instant-running'
|
||||
|
||||
if (batchCount.value > 1) {
|
||||
useTelemetry()?.trackUiButtonClicked({
|
||||
|
||||
@@ -64,9 +64,8 @@ const { subcategories } = defineProps<{
|
||||
const filteredSubcategories = computed(() => {
|
||||
const result: Record<string, ComfyCommandImpl[]> = {}
|
||||
|
||||
for (const [subcategory, commands] of Object.entries(subcategories)) {
|
||||
for (const [subcategory, commands] of Object.entries(subcategories))
|
||||
result[subcategory] = commands.filter((cmd) => !!cmd.keybinding)
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
@@ -81,9 +81,7 @@ const handleCopy = async () => {
|
||||
if (selectedText) {
|
||||
await navigator.clipboard.writeText(selectedText)
|
||||
|
||||
if (shouldSelectAll) {
|
||||
terminal.clearSelection()
|
||||
}
|
||||
if (shouldSelectAll) terminal.clearSelection()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,9 +90,7 @@ const showContextMenu = (event: MouseEvent) => {
|
||||
electronAPI()?.showContextMenu({ type: 'text' })
|
||||
}
|
||||
|
||||
if (isDesktop) {
|
||||
useEventListener(terminalEl, 'contextmenu', showContextMenu)
|
||||
}
|
||||
if (isDesktop) useEventListener(terminalEl, 'contextmenu', showContextMenu)
|
||||
|
||||
onMounted(() => {
|
||||
selectionDisposable = terminal.onSelectionChange(() => {
|
||||
|
||||
@@ -117,16 +117,14 @@ describe('WidgetBoundingBox', () => {
|
||||
describe('Disabled state', () => {
|
||||
it('disables all four inputs when disabled=true', () => {
|
||||
renderBox({ x: 0, y: 0, width: 1, height: 1 }, true)
|
||||
for (const input of screen.getAllByRole('spinbutton')) {
|
||||
for (const input of screen.getAllByRole('spinbutton'))
|
||||
expect(input).toBeDisabled()
|
||||
}
|
||||
})
|
||||
|
||||
it('leaves all four inputs enabled when disabled=false', () => {
|
||||
renderBox({ x: 0, y: 0, width: 1, height: 1 }, false)
|
||||
for (const input of screen.getAllByRole('spinbutton')) {
|
||||
for (const input of screen.getAllByRole('spinbutton'))
|
||||
expect(input).not.toBeDisabled()
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -178,9 +178,7 @@ watch(breadcrumbElement, (el) => {
|
||||
const totalWidth = itemsWidth + separatorsWidth + gapsWidth
|
||||
const containerWidth = el.clientWidth
|
||||
|
||||
if (totalWidth <= containerWidth) {
|
||||
collapseTabs.value = false
|
||||
}
|
||||
if (totalWidth <= containerWidth) collapseTabs.value = false
|
||||
}
|
||||
} else if (isOverflowing) {
|
||||
collapseTabs.value = true
|
||||
@@ -191,9 +189,7 @@ watch(breadcrumbElement, (el) => {
|
||||
|
||||
// If e.g. the workflow name changes, we need to check the overflow again
|
||||
onUpdated(() => {
|
||||
if (!overflowObserver?.disposed.value) {
|
||||
overflowObserver?.checkOverflow()
|
||||
}
|
||||
if (!overflowObserver?.disposed.value) overflowObserver?.checkOverflow()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -138,9 +138,9 @@ const rename = async (
|
||||
const isRoot = item.key === 'root'
|
||||
|
||||
const tooltipText = computed(() => {
|
||||
if (hasMissingNodes.value && isRoot) {
|
||||
if (hasMissingNodes.value && isRoot)
|
||||
return t('breadcrumbsMenu.missingNodesWarning')
|
||||
}
|
||||
|
||||
return item.label
|
||||
})
|
||||
|
||||
@@ -158,9 +158,8 @@ const startRename = async () => {
|
||||
if (itemInputRef.value?.$el) {
|
||||
itemInputRef.value.$el.focus()
|
||||
itemInputRef.value.$el.select()
|
||||
if (wrapperRef.value) {
|
||||
if (wrapperRef.value)
|
||||
itemInputRef.value.$el.style.width = `${Math.max(200, wrapperRef.value.offsetWidth)}px`
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -168,16 +167,11 @@ const startRename = async () => {
|
||||
const { menuItems } = useWorkflowActionsMenu(startRename, { isRoot })
|
||||
|
||||
const handleClick = (event: MouseEvent) => {
|
||||
if (isEditing.value) {
|
||||
return
|
||||
}
|
||||
if (isEditing.value) return
|
||||
|
||||
if (event.detail === 1) {
|
||||
if (isActive) {
|
||||
menu.value?.toggle(event)
|
||||
} else {
|
||||
item.command?.({ item: item, originalEvent: event })
|
||||
}
|
||||
if (isActive) menu.value?.toggle(event)
|
||||
else item.command?.({ item: item, originalEvent: event })
|
||||
} else if (isActive && event.detail === 2) {
|
||||
menu.value?.hide()
|
||||
event.stopPropagation()
|
||||
@@ -187,9 +181,7 @@ const handleClick = (event: MouseEvent) => {
|
||||
}
|
||||
|
||||
const inputBlur = async (doRename: boolean) => {
|
||||
if (doRename) {
|
||||
await rename(itemLabel.value, item.label as string)
|
||||
}
|
||||
if (doRename) await rename(itemLabel.value, item.label as string)
|
||||
|
||||
isEditing.value = false
|
||||
}
|
||||
|
||||
@@ -63,9 +63,9 @@ const mappedSelections = computed((): WidgetEntry[] => {
|
||||
const { entityId, node, widget, config } = entry
|
||||
if (node.mode !== LGraphEventMode.ALWAYS) return []
|
||||
|
||||
if (!nodeDataByNode.has(node)) {
|
||||
if (!nodeDataByNode.has(node))
|
||||
nodeDataByNode.set(node, nodeToNodeData(node))
|
||||
}
|
||||
|
||||
const fullNodeData = nodeDataByNode.get(node)!
|
||||
|
||||
const matchingWidget = fullNodeData.widgets?.find((vueWidget) => {
|
||||
@@ -133,9 +133,10 @@ function nodeToNodeData(node: LGraphNode) {
|
||||
|
||||
async function handleDragDrop() {
|
||||
const onDragDrop = async (e: DragEvent) => {
|
||||
for (const { nodeData } of mappedSelections.value)
|
||||
for (const { nodeData } of mappedSelections.value) {
|
||||
if (nodeData?.onDragOver?.(e) && (await nodeData.onDragDrop?.(e)))
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -35,18 +35,20 @@ function onEditComplete(newName: string) {
|
||||
|
||||
const entries = computed(() => {
|
||||
const items = []
|
||||
if (canRename)
|
||||
if (canRename) {
|
||||
items.push({
|
||||
label: t('g.rename'),
|
||||
command: () => setTimeout(() => (isEditing.value = true)),
|
||||
icon: 'icon-[lucide--pencil]'
|
||||
})
|
||||
if (remove)
|
||||
}
|
||||
if (remove) {
|
||||
items.push({
|
||||
label: t('g.delete'),
|
||||
command: remove,
|
||||
icon: 'icon-[lucide--trash-2]'
|
||||
})
|
||||
}
|
||||
return items
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -50,9 +50,9 @@ const settingStore = useSettingStore()
|
||||
const dontShowAgain = ref(false)
|
||||
|
||||
function dismiss() {
|
||||
if (dontShowAgain.value) {
|
||||
if (dontShowAgain.value)
|
||||
void settingStore.set('Comfy.AppBuilder.VueNodeSwitchDismissed', true)
|
||||
}
|
||||
|
||||
appModeStore.showVueNodeSwitchPopup = false
|
||||
}
|
||||
</script>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user