Skip to content

Commit e8d3d84

Browse files
pythongossssshuchenlei
authored andcommitted
Floating menu option (#726)
* Add floating menu * Fix * Updates * Add auto-queue change test * Fix
1 parent d223f38 commit e8d3d84

File tree

14 files changed

+630
-15
lines changed

14 files changed

+630
-15
lines changed

browser_tests/ComfyPage.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Page, Locator } from '@playwright/test'
22
import { test as base } from '@playwright/test'
3+
import { ComfyAppMenu } from './helpers/appMenu'
34
import dotenv from 'dotenv'
45
dotenv.config()
56
import * as fs from 'fs'
@@ -225,6 +226,7 @@ export class ComfyPage {
225226
// Components
226227
public readonly searchBox: ComfyNodeSearchBox
227228
public readonly menu: ComfyMenu
229+
public readonly appMenu: ComfyAppMenu
228230

229231
constructor(public readonly page: Page) {
230232
this.url = process.env.PLAYWRIGHT_TEST_URL || 'http://localhost:8188'
@@ -235,6 +237,7 @@ export class ComfyPage {
235237
this.workflowUploadInput = page.locator('#comfy-file-input')
236238
this.searchBox = new ComfyNodeSearchBox(page)
237239
this.menu = new ComfyMenu(page)
240+
this.appMenu = new ComfyAppMenu(page)
238241
}
239242

240243
async getGraphNodesCount(): Promise<number> {

browser_tests/appMenu.spec.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import type { Response } from '@playwright/test'
2+
import type { StatusWsMessage } from '../src/types/apiTypes.ts'
3+
import { expect, mergeTests } from '@playwright/test'
4+
import { comfyPageFixture } from './ComfyPage'
5+
import { webSocketFixture } from './fixtures/ws.ts'
6+
7+
const test = mergeTests(comfyPageFixture, webSocketFixture)
8+
9+
test.describe('AppMenu', () => {
10+
test.beforeEach(async ({ comfyPage }) => {
11+
await comfyPage.setSetting('Comfy.UseNewMenu', 'Floating')
12+
})
13+
14+
test.afterEach(async ({ comfyPage }) => {
15+
const currentThemeId = await comfyPage.menu.getThemeId()
16+
if (currentThemeId !== 'dark') {
17+
await comfyPage.menu.toggleTheme()
18+
}
19+
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
20+
})
21+
22+
/**
23+
* This test ensures that the autoqueue change mode can only queue one change at a time
24+
*/
25+
test('Does not auto-queue multiple changes at a time', async ({
26+
comfyPage,
27+
ws
28+
}) => {
29+
// Enable change auto-queue mode
30+
let queueOpts = await comfyPage.appMenu.queueButton.toggleOptions()
31+
expect(await queueOpts.getMode()).toBe('disabled')
32+
await queueOpts.setMode('change')
33+
await comfyPage.nextFrame()
34+
expect(await queueOpts.getMode()).toBe('change')
35+
await comfyPage.appMenu.queueButton.toggleOptions()
36+
37+
// Intercept the prompt queue endpoint
38+
let promptNumber = 0
39+
comfyPage.page.route('**/api/prompt', async (route, req) => {
40+
await new Promise((r) => setTimeout(r, 100))
41+
route.fulfill({
42+
status: 200,
43+
body: JSON.stringify({
44+
prompt_id: promptNumber,
45+
number: ++promptNumber,
46+
node_errors: {},
47+
// Include the request data to validate which prompt was queued so we can validate the width
48+
__request: req.postDataJSON()
49+
})
50+
})
51+
})
52+
53+
// Start watching for a message to prompt
54+
const requestPromise = comfyPage.page.waitForResponse('**/api/prompt')
55+
56+
// Find and set the width on the latent node
57+
const triggerChange = async (value: number) => {
58+
return await comfyPage.page.evaluate((value) => {
59+
const node = window['app'].graph._nodes.find(
60+
(n) => n.type === 'EmptyLatentImage'
61+
)
62+
node.widgets[0].value = value
63+
window['app'].workflowManager.activeWorkflow.changeTracker.checkState()
64+
}, value)
65+
}
66+
67+
// Trigger a status websocket message
68+
const triggerStatus = async (queueSize: number) => {
69+
await ws.trigger({
70+
type: 'status',
71+
data: {
72+
status: {
73+
exec_info: {
74+
queue_remaining: queueSize
75+
}
76+
}
77+
}
78+
} as StatusWsMessage)
79+
}
80+
81+
// Extract the width from the queue response
82+
const getQueuedWidth = async (resp: Promise<Response>) => {
83+
const obj = await (await resp).json()
84+
return obj['__request']['prompt']['5']['inputs']['width']
85+
}
86+
87+
// Trigger a bunch of changes
88+
const START = 32
89+
const END = 64
90+
for (let i = START; i <= END; i += 8) {
91+
await triggerChange(i)
92+
}
93+
94+
// Ensure the queued width is the first value
95+
expect(
96+
await getQueuedWidth(requestPromise),
97+
'the first queued prompt should be the first change width'
98+
).toBe(START)
99+
100+
// Ensure that no other changes are queued
101+
await expect(
102+
comfyPage.page.waitForResponse('**/api/prompt', { timeout: 250 })
103+
).rejects.toThrow()
104+
expect(
105+
promptNumber,
106+
'only 1 prompt should have been queued even though there were multiple changes'
107+
).toBe(1)
108+
109+
// Trigger a status update so auto-queue re-runs
110+
await triggerStatus(1)
111+
await triggerStatus(0)
112+
113+
// Ensure the queued width is the last queued value
114+
expect(
115+
await getQueuedWidth(comfyPage.page.waitForResponse('**/api/prompt')),
116+
'last queued prompt width should be the last change'
117+
).toBe(END)
118+
expect(promptNumber, 'queued prompt count should be 2').toBe(2)
119+
})
120+
})

browser_tests/fixtures/ws.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { test as base } from '@playwright/test'
2+
3+
export const webSocketFixture = base.extend<{
4+
ws: { trigger(data: any, url?: string): Promise<void> }
5+
}>({
6+
ws: [
7+
async ({ page }, use) => {
8+
// Each time a page loads, to catch navigations
9+
page.on('load', async () => {
10+
await page.evaluate(function () {
11+
// Create a wrapper for WebSocket that stores them globally
12+
// so we can look it up to trigger messages
13+
const store: Record<string, WebSocket> = ((window as any).__ws__ = {})
14+
window.WebSocket = class extends window.WebSocket {
15+
constructor() {
16+
// @ts-expect-error
17+
super(...arguments)
18+
store[this.url] = this
19+
}
20+
}
21+
})
22+
})
23+
24+
await use({
25+
async trigger(data, url) {
26+
// Trigger a websocket event on the page
27+
await page.evaluate(
28+
function ([data, url]) {
29+
if (!url) {
30+
// If no URL specified, use page URL
31+
const u = new URL(window.location.toString())
32+
u.protocol = 'ws:'
33+
u.pathname = '/'
34+
url = u.toString() + 'ws'
35+
}
36+
const ws: WebSocket = (window as any).__ws__[url]
37+
ws.dispatchEvent(
38+
new MessageEvent('message', {
39+
data
40+
})
41+
)
42+
},
43+
[JSON.stringify(data), url]
44+
)
45+
}
46+
})
47+
},
48+
// We need this to run automatically as the first thing so it adds handlers as soon as the page loads
49+
{ auto: true }
50+
]
51+
})

browser_tests/helpers/appMenu.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import type { Page, Locator } from '@playwright/test'
2+
3+
export class ComfyAppMenu {
4+
public readonly root: Locator
5+
public readonly queueButton: ComfyQueueButton
6+
7+
constructor(public readonly page: Page) {
8+
this.root = page.locator('.app-menu')
9+
this.queueButton = new ComfyQueueButton(this)
10+
}
11+
}
12+
13+
class ComfyQueueButton {
14+
public readonly root: Locator
15+
public readonly primaryButton: Locator
16+
public readonly dropdownButton: Locator
17+
constructor(public readonly appMenu: ComfyAppMenu) {
18+
this.root = appMenu.root.getByTestId('queue-button')
19+
this.primaryButton = this.root.locator('.p-splitbutton-button')
20+
this.dropdownButton = this.root.locator('.p-splitbutton-dropdown')
21+
}
22+
23+
public async toggleOptions() {
24+
await this.dropdownButton.click()
25+
return new ComfyQueueButtonOptions(this.appMenu.page)
26+
}
27+
}
28+
29+
class ComfyQueueButtonOptions {
30+
public readonly popup: Locator
31+
public readonly modes: {
32+
disabled: { input: Locator; wrapper: Locator }
33+
instant: { input: Locator; wrapper: Locator }
34+
change: { input: Locator; wrapper: Locator }
35+
}
36+
37+
constructor(public readonly page: Page) {
38+
this.popup = page.getByTestId('queue-options')
39+
this.modes = (['disabled', 'instant', 'change'] as const).reduce(
40+
(modes, mode) => {
41+
modes[mode] = {
42+
input: page.locator(`#autoqueue-${mode}`),
43+
wrapper: page.getByTestId(`autoqueue-${mode}`)
44+
}
45+
return modes
46+
},
47+
{} as ComfyQueueButtonOptions['modes']
48+
)
49+
}
50+
51+
public async setMode(mode: keyof ComfyQueueButtonOptions['modes']) {
52+
await this.modes[mode].input.click()
53+
}
54+
55+
public async getMode() {
56+
return (
57+
await Promise.all(
58+
Object.entries(this.modes).map(async ([mode, opt]) => [
59+
mode,
60+
await opt.wrapper.getAttribute('data-p-checked')
61+
])
62+
)
63+
).find(([, checked]) => checked === 'true')?.[0]
64+
}
65+
}

src/App.vue

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
<GlobalToast />
77
<UnloadWindowConfirmDialog />
88
<BrowserTabTitle />
9+
<AppMenu />
910
</template>
1011

1112
<script setup lang="ts">
@@ -20,6 +21,7 @@ import {
2021
import BlockUI from 'primevue/blockui'
2122
import ProgressSpinner from 'primevue/progressspinner'
2223
import QueueSidebarTab from '@/components/sidebar/tabs/QueueSidebarTab.vue'
24+
import AppMenu from '@/components/appMenu/AppMenu.vue'
2325
import { app } from './scripts/app'
2426
import { useSettingStore } from './stores/settingStore'
2527
import { useI18n } from 'vue-i18n'
@@ -34,6 +36,7 @@ import { StatusWsMessageStatus } from './types/apiTypes'
3436
import { useQueuePendingTaskCountStore } from './stores/queueStore'
3537
import type { ToastMessageOptions } from 'primevue/toast'
3638
import { useToast } from 'primevue/usetoast'
39+
import { setupAutoQueueHandler } from './services/autoQueueService'
3740
import { i18n } from './i18n'
3841
import { useExecutionStore } from './stores/executionStore'
3942
import { useWorkflowStore } from './stores/workflowStore'
@@ -56,6 +59,8 @@ watch(
5659
{ immediate: true }
5760
)
5861
62+
setupAutoQueueHandler()
63+
5964
watchEffect(() => {
6065
const fontSize = useSettingStore().get('Comfy.TextareaWidget.FontSize')
6166
document.documentElement.style.setProperty(

src/components/LiteGraphCanvasSplitterOverlay.vue

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,9 @@ const gutterClass = computed(() => {
7575
z-index: 999;
7676
border: none;
7777
}
78+
79+
.comfyui-floating-menu .splitter-overlay {
80+
top: var(--comfy-floating-menu-height);
81+
height: calc(100% - var(--comfy-floating-menu-height));
82+
}
7883
</style>

0 commit comments

Comments
 (0)