Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[1.3.0] Beta menu rework / Model library sidebar tab / etc #960

Merged
merged 26 commits into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
25300f2
Floating menu option (#726)
pythongosssss Sep 16, 2024
803f9e0
Proxy ComfyWorkflow objects (#869)
huchenlei Sep 18, 2024
6ded7a3
Move workflow dropdown to sidebar tab (#893)
huchenlei Sep 21, 2024
e148cbf
Fix workflow search cannot find uppercase letters (#908)
webfiltered Sep 22, 2024
ec5ba0c
Fix routing and layout issue (#923)
dmx974 Sep 22, 2024
729a521
Revert "Fix routing and layout issue (#923)" (#930)
huchenlei Sep 23, 2024
c8e62bf
Relands "Fix routing and layout issue" (#931)
huchenlei Sep 23, 2024
586a1ed
Fix routing (#929)
dmx974 Sep 23, 2024
bbe2de9
Resolve merge conflict
huchenlei Sep 23, 2024
b6dc11f
Remove support of Top/Bottom in menu positions (#933)
huchenlei Sep 23, 2024
966834b
Workflow templates (#938)
pythongosssss Sep 23, 2024
b9670a0
[Draft] Model library sidebar tab (#837)
mcmonkey4eva Sep 24, 2024
571d130
Add topbar dropdown menu (#937)
huchenlei Sep 24, 2024
8223241
Fix action bar commands (#946)
huchenlei Sep 24, 2024
63b7a1b
don't show redundant model previews (#949)
mcmonkey4eva Sep 24, 2024
0b8acf4
Fix routing (#939)
mcmonkey4eva Sep 24, 2024
5f4e8ae
Show opened workflows as topbar tabs (#952)
huchenlei Sep 24, 2024
1463c0e
Migrate deprecated setting values (#954)
huchenlei Sep 24, 2024
9bd4557
better badges for empty/loading model library folders (#953)
mcmonkey4eva Sep 24, 2024
06136de
Support redo with Ctrl+Shift+Z (#957)
AustinMroz Sep 24, 2024
36c3ecd
initial download-folder-selector interface (#890)
mcmonkey4eva Sep 24, 2024
7d39581
Move clipspace from action bar to topbar dropdown menu (#956)
huchenlei Sep 24, 2024
b3441b6
Reduce SearchBox margins for lower resolutions. (#959)
AustinMroz Sep 24, 2024
c2a3f36
Rework queue button (#968)
huchenlei Sep 25, 2024
c43bb20
Reset FileInput value after load (#958)
AustinMroz Sep 25, 2024
2f7758f
Backward compatibility with extension injections on legacy menu bar (…
huchenlei Sep 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env_example
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ DEPLOY_COMFYUI_DIR=/home/ComfyUI/web
EXAMPLE_REPO_PATH=tests-ui/ComfyUI_examples

# Whether to enable minification of the frontend code.
ENABLE_MINIFY=true
ENABLE_MINIFY=true
159 changes: 142 additions & 17 deletions browser_tests/ComfyPage.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import type { Page, Locator } from '@playwright/test'
import { test as base } from '@playwright/test'
import type { Page, Locator, APIRequestContext } from '@playwright/test'
import { expect } from '@playwright/test'
import { test as base } from '@playwright/test'
import { ComfyAppMenu } from './helpers/appMenu'
import dotenv from 'dotenv'
dotenv.config()
import * as fs from 'fs'
import { NodeBadgeMode } from '../src/types/nodeSource'
import { NodeId } from '../src/types/comfyWorkflow'
import { ManageGroupNode } from './helpers/manageGroupNode'
import { ComfyTemplates } from './helpers/templates'

interface Position {
x: number
Expand Down Expand Up @@ -96,9 +98,11 @@ class ComfyNodeSearchBox {
}
}

class NodeLibrarySidebarTab {
public readonly tabId: string = 'node-library'
constructor(public readonly page: Page) {}
class SidebarTab {
constructor(
public readonly page: Page,
public readonly tabId: string
) {}

get tabButton() {
return this.page.locator(`.${this.tabId}-tab-button`)
Expand All @@ -110,6 +114,19 @@ class NodeLibrarySidebarTab {
)
}

async open() {
if (await this.selectedTabButton.isVisible()) {
return
}
await this.tabButton.click()
}
}

class NodeLibrarySidebarTab extends SidebarTab {
constructor(public readonly page: Page) {
super(page, 'node-library')
}

get nodeLibrarySearchBoxInput() {
return this.page.locator('.node-lib-search-box input[type="text"]')
}
Expand All @@ -131,11 +148,7 @@ class NodeLibrarySidebarTab {
}

async open() {
if (await this.selectedTabButton.isVisible()) {
return
}

await this.tabButton.click()
await super.open()
await this.nodeLibraryTree.waitFor({ state: 'visible' })
}

Expand All @@ -156,6 +169,59 @@ class NodeLibrarySidebarTab {
}
}

class WorkflowsSidebarTab extends SidebarTab {
constructor(public readonly page: Page) {
super(page, 'workflows')
}

get browseGalleryButton() {
return this.page.locator('.browse-templates-button')
}

get newBlankWorkflowButton() {
return this.page.locator('.new-blank-workflow-button')
}

get browseWorkflowsButton() {
return this.page.locator('.browse-workflows-button')
}

get newDefaultWorkflowButton() {
return this.page.locator('.new-default-workflow-button')
}

async getOpenedWorkflowNames() {
return await this.page
.locator('.comfyui-workflows-open .node-label')
.allInnerTexts()
}

async getTopLevelSavedWorkflowNames() {
return await this.page
.locator('.comfyui-workflows-browse .node-label')
.allInnerTexts()
}

async switchToWorkflow(workflowName: string) {
const workflowLocator = this.page.locator(
'.comfyui-workflows-open .node-label',
{ hasText: workflowName }
)
await workflowLocator.click()
await this.page.waitForTimeout(300)
}
}

class Topbar {
constructor(public readonly page: Page) {}

async getTabNames(): Promise<string[]> {
return await this.page
.locator('.workflow-tabs .workflow-label')
.allInnerTexts()
}
}

class ComfyMenu {
public readonly sideToolbar: Locator
public readonly themeToggleButton: Locator
Expand Down Expand Up @@ -188,6 +254,14 @@ class ComfyMenu {
return new NodeLibrarySidebarTab(this.page)
}

get workflowsTab() {
return new WorkflowsSidebarTab(this.page)
}

get topbar() {
return new Topbar(this.page)
}

async toggleTheme() {
await this.themeToggleButton.click()
await this.page.evaluate(() => {
Expand All @@ -212,6 +286,10 @@ class ComfyMenu {
}
}

type FolderStructure = {
[key: string]: FolderStructure | string
}

export class ComfyPage {
public readonly url: string
// All canvas position operations are based on default view of canvas.
Expand All @@ -228,8 +306,13 @@ export class ComfyPage {
// Components
public readonly searchBox: ComfyNodeSearchBox
public readonly menu: ComfyMenu
public readonly appMenu: ComfyAppMenu
public readonly templates: ComfyTemplates

constructor(public readonly page: Page) {
constructor(
public readonly page: Page,
public readonly request: APIRequestContext
) {
this.url = process.env.PLAYWRIGHT_TEST_URL || 'http://localhost:8188'
this.canvas = page.locator('#graph-canvas')
this.widgetTextBox = page.getByPlaceholder('text').nth(1)
Expand All @@ -238,6 +321,23 @@ export class ComfyPage {
this.workflowUploadInput = page.locator('#comfy-file-input')
this.searchBox = new ComfyNodeSearchBox(page)
this.menu = new ComfyMenu(page)
this.appMenu = new ComfyAppMenu(page)
this.templates = new ComfyTemplates(page)
}

convertLeafToContent(structure: FolderStructure): FolderStructure {
const result: FolderStructure = {}

for (const [key, value] of Object.entries(structure)) {
if (typeof value === 'string') {
const filePath = this.assetPath(value)
result[key] = fs.readFileSync(filePath, 'utf-8')
} else {
result[key] = this.convertLeafToContent(value)
}
}

return result
}

async getGraphNodesCount(): Promise<number> {
Expand All @@ -246,7 +346,25 @@ export class ComfyPage {
})
}

async setup() {
async setupWorkflowsDirectory(structure: FolderStructure) {
const resp = await this.request.post(
`${this.url}/api/devtools/setup_folder_structure`,
{
data: {
tree_structure: this.convertLeafToContent(structure),
base_path: 'user/default/workflows'
}
}
)

if (resp.status() !== 200) {
throw new Error(
`Failed to setup workflows directory: ${await resp.text()}`
)
}
}

async setup({ resetView = true } = {}) {
await this.goto()
await this.page.evaluate(() => {
localStorage.clear()
Expand All @@ -273,9 +391,11 @@ export class ComfyPage {
window['app']['canvas'].show_info = false
})
await this.nextFrame()
// Reset view to force re-rendering of canvas. So that info fields like fps
// become hidden.
await this.resetView()
if (resetView) {
// Reset view to force re-rendering of canvas. So that info fields like fps
// become hidden.
await this.resetView()
}

// Hide all badges by default.
await this.setSetting('Comfy.NodeBadge.NodeIdBadgeMode', NodeBadgeMode.None)
Expand Down Expand Up @@ -618,6 +738,11 @@ export class ComfyPage {
await this.nextFrame()
}

async closeDialog() {
await this.page.locator('.p-dialog-close-button').click()
await expect(this.page.locator('.p-dialog')).toBeHidden()
}

async resizeNode(
nodePos: Position,
nodeSize: Size,
Expand Down Expand Up @@ -891,8 +1016,8 @@ class NodeReference {
}

export const comfyPageFixture = base.extend<{ comfyPage: ComfyPage }>({
comfyPage: async ({ page }, use) => {
const comfyPage = new ComfyPage(page)
comfyPage: async ({ page, request }, use) => {
const comfyPage = new ComfyPage(page, request)
await comfyPage.setup()
await use(comfyPage)
}
Expand Down
116 changes: 116 additions & 0 deletions browser_tests/appMenu.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import type { Response } from '@playwright/test'
import type { StatusWsMessage } from '../src/types/apiTypes.ts'
import { expect, mergeTests } from '@playwright/test'
import { comfyPageFixture } from './ComfyPage'
import { webSocketFixture } from './fixtures/ws.ts'

const test = mergeTests(comfyPageFixture, webSocketFixture)

test.describe('AppMenu', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Floating')
})

test.afterEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})

/**
* This test ensures that the autoqueue change mode can only queue one change at a time
*/
test('Does not auto-queue multiple changes at a time', async ({
comfyPage,
ws
}) => {
// Enable change auto-queue mode
const queueOpts = await comfyPage.appMenu.queueButton.toggleOptions()
expect(await queueOpts.getMode()).toBe('disabled')
await queueOpts.setMode('change')
await comfyPage.nextFrame()
expect(await queueOpts.getMode()).toBe('change')
await comfyPage.appMenu.queueButton.toggleOptions()

// Intercept the prompt queue endpoint
let promptNumber = 0
comfyPage.page.route('**/api/prompt', async (route, req) => {
await new Promise((r) => setTimeout(r, 100))
route.fulfill({
status: 200,
body: JSON.stringify({
prompt_id: promptNumber,
number: ++promptNumber,
node_errors: {},
// Include the request data to validate which prompt was queued so we can validate the width
__request: req.postDataJSON()
})
})
})

// Start watching for a message to prompt
const requestPromise = comfyPage.page.waitForResponse('**/api/prompt')

// Find and set the width on the latent node
const triggerChange = async (value: number) => {
return await comfyPage.page.evaluate((value) => {
const node = window['app'].graph._nodes.find(
(n) => n.type === 'EmptyLatentImage'
)
node.widgets[0].value = value
window['app'].workflowManager.activeWorkflow.changeTracker.checkState()
}, value)
}

// Trigger a status websocket message
const triggerStatus = async (queueSize: number) => {
await ws.trigger({
type: 'status',
data: {
status: {
exec_info: {
queue_remaining: queueSize
}
}
}
} as StatusWsMessage)
}

// Extract the width from the queue response
const getQueuedWidth = async (resp: Promise<Response>) => {
const obj = await (await resp).json()
return obj['__request']['prompt']['5']['inputs']['width']
}

// Trigger a bunch of changes
const START = 32
const END = 64
for (let i = START; i <= END; i += 8) {
await triggerChange(i)
}

// Ensure the queued width is the first value
expect(
await getQueuedWidth(requestPromise),
'the first queued prompt should be the first change width'
).toBe(START)

// Ensure that no other changes are queued
await expect(
comfyPage.page.waitForResponse('**/api/prompt', { timeout: 250 })
).rejects.toThrow()
expect(
promptNumber,
'only 1 prompt should have been queued even though there were multiple changes'
).toBe(1)

// Trigger a status update so auto-queue re-runs
await triggerStatus(1)
await triggerStatus(0)

// Ensure the queued width is the last queued value
expect(
await getQueuedWidth(comfyPage.page.waitForResponse('**/api/prompt')),
'last queued prompt width should be the last change'
).toBe(END)
expect(promptNumber, 'queued prompt count should be 2').toBe(2)
})
})
Loading
Loading