diff --git a/browser_tests/tests/menu.spec.ts b/browser_tests/tests/menu.spec.ts index 97db79d71b..7c999f879d 100644 --- a/browser_tests/tests/menu.spec.ts +++ b/browser_tests/tests/menu.spec.ts @@ -32,701 +32,6 @@ test.describe('Menu', () => { expect(newChildrenCount).toBe(initialChildrenCount + 1) }) - test.describe('Node library sidebar', () => { - test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', []) - await comfyPage.setSetting('Comfy.NodeLibrary.BookmarksCustomization', {}) - // Open the sidebar - const tab = comfyPage.menu.nodeLibraryTab - await tab.open() - }) - - test('Node preview and drag to canvas', async ({ comfyPage }) => { - const tab = comfyPage.menu.nodeLibraryTab - await tab.getFolder('sampling').click() - - // Hover over a node to display the preview - const nodeSelector = '.p-tree-node-leaf' - await comfyPage.page.hover(nodeSelector) - - // Verify the preview is displayed - const previewVisible = await comfyPage.page.isVisible( - '.node-lib-node-preview' - ) - expect(previewVisible).toBe(true) - - const count = await comfyPage.getGraphNodesCount() - // Drag the node onto the canvas - const canvasSelector = '#graph-canvas' - - // Get the bounding box of the canvas element - const canvasBoundingBox = (await comfyPage.page - .locator(canvasSelector) - .boundingBox())! - - // Calculate the center position of the canvas - const targetPosition = { - x: canvasBoundingBox.x + canvasBoundingBox.width / 2, - y: canvasBoundingBox.y + canvasBoundingBox.height / 2 - } - - await comfyPage.page.dragAndDrop(nodeSelector, canvasSelector, { - targetPosition - }) - - // Verify the node is added to the canvas - expect(await comfyPage.getGraphNodesCount()).toBe(count + 1) - }) - - test('Bookmark node', async ({ comfyPage }) => { - const tab = comfyPage.menu.nodeLibraryTab - await tab.getFolder('sampling').click() - - // Bookmark the node - await tab - .getNode('KSampler (Advanced)') - .locator('.bookmark-button') - .click() - - // Verify the bookmark is added to the bookmarks tab - expect( - await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2') - ).toEqual(['KSamplerAdvanced']) - // Verify the bookmark node with the same name is added to the tree. - expect(await tab.getNode('KSampler (Advanced)').count()).toBe(2) - - // Hover on the bookmark node to display the preview - await comfyPage.page.hover('.node-lib-bookmark-tree-explorer .tree-leaf') - expect(await comfyPage.page.isVisible('.node-lib-node-preview')).toBe( - true - ) - }) - - test('Ignores unrecognized node', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo']) - - const tab = comfyPage.menu.nodeLibraryTab - expect(await tab.getFolder('sampling').count()).toBe(1) - expect(await tab.getNode('foo').count()).toBe(0) - }) - - test('Displays empty bookmarks folder', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/']) - const tab = comfyPage.menu.nodeLibraryTab - expect(await tab.getFolder('foo').count()).toBe(1) - }) - - test('Can add new bookmark folder', async ({ comfyPage }) => { - const tab = comfyPage.menu.nodeLibraryTab - await tab.newFolderButton.click() - const textInput = comfyPage.page.locator('.editable-text input') - await textInput.waitFor({ state: 'visible' }) - await textInput.fill('New Folder') - await textInput.press('Enter') - expect(await tab.getFolder('New Folder').count()).toBe(1) - expect( - await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2') - ).toEqual(['New Folder/']) - }) - - test('Can add nested bookmark folder', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/']) - const tab = comfyPage.menu.nodeLibraryTab - - await tab.getFolder('foo').click({ button: 'right' }) - await comfyPage.page.getByLabel('New Folder').click() - const textInput = comfyPage.page.locator('.editable-text input') - await textInput.waitFor({ state: 'visible' }) - await textInput.fill('bar') - await textInput.press('Enter') - - expect(await tab.getFolder('bar').count()).toBe(1) - expect( - await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2') - ).toEqual(['foo/', 'foo/bar/']) - }) - - test('Can delete bookmark folder', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/']) - const tab = comfyPage.menu.nodeLibraryTab - - await tab.getFolder('foo').click({ button: 'right' }) - await comfyPage.page.getByLabel('Delete').click() - - expect( - await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2') - ).toEqual([]) - }) - - test('Can rename bookmark folder', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/']) - const tab = comfyPage.menu.nodeLibraryTab - - await tab.getFolder('foo').click({ button: 'right' }) - await comfyPage.page - .locator('.p-contextmenu-item-label:has-text("Rename")') - .click() - await comfyPage.page.keyboard.insertText('bar') - await comfyPage.page.keyboard.press('Enter') - - expect( - await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2') - ).toEqual(['bar/']) - }) - - test('Can add bookmark by dragging node to bookmark folder', async ({ - comfyPage - }) => { - await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/']) - const tab = comfyPage.menu.nodeLibraryTab - await tab.getFolder('sampling').click() - await comfyPage.page.dragAndDrop( - tab.nodeSelector('KSampler (Advanced)'), - tab.folderSelector('foo') - ) - expect( - await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2') - ).toEqual(['foo/', 'foo/KSamplerAdvanced']) - }) - - test('Can add bookmark by clicking bookmark button', async ({ - comfyPage - }) => { - const tab = comfyPage.menu.nodeLibraryTab - await tab.getFolder('sampling').click() - await tab - .getNode('KSampler (Advanced)') - .locator('.bookmark-button') - .click() - expect( - await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2') - ).toEqual(['KSamplerAdvanced']) - }) - - test('Can unbookmark node (Top level bookmark)', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [ - 'KSamplerAdvanced' - ]) - const tab = comfyPage.menu.nodeLibraryTab - await tab - .getNode('KSampler (Advanced)') - .locator('.bookmark-button') - .click() - expect( - await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2') - ).toEqual([]) - }) - - test('Can unbookmark node (Library node bookmark)', async ({ - comfyPage - }) => { - await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [ - 'KSamplerAdvanced' - ]) - const tab = comfyPage.menu.nodeLibraryTab - await tab.getFolder('sampling').click() - await comfyPage.page - .locator(tab.nodeSelector('KSampler (Advanced)')) - .nth(1) - .locator('.bookmark-button') - .click() - expect( - await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2') - ).toEqual([]) - }) - test('Can customize icon', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/']) - const tab = comfyPage.menu.nodeLibraryTab - await tab.getFolder('foo').click({ button: 'right' }) - await comfyPage.page.getByLabel('Customize').click() - await comfyPage.page - .locator('.icon-field .p-selectbutton > *:nth-child(2)') - .click() - await comfyPage.page - .locator('.color-field .p-selectbutton > *:nth-child(2)') - .click() - await comfyPage.page.getByLabel('Confirm').click() - await comfyPage.nextFrame() - expect( - await comfyPage.getSetting('Comfy.NodeLibrary.BookmarksCustomization') - ).toEqual({ - 'foo/': { - icon: 'pi-folder', - color: '#007bff' - } - }) - }) - // If color is left as default, it should not be saved - test('Can customize icon (default field)', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/']) - const tab = comfyPage.menu.nodeLibraryTab - await tab.getFolder('foo').click({ button: 'right' }) - await comfyPage.page.getByLabel('Customize').click() - await comfyPage.page - .locator('.icon-field .p-selectbutton > *:nth-child(2)') - .click() - await comfyPage.page.getByLabel('Confirm').click() - await comfyPage.nextFrame() - expect( - await comfyPage.getSetting('Comfy.NodeLibrary.BookmarksCustomization') - ).toEqual({ - 'foo/': { - icon: 'pi-folder' - } - }) - }) - - test('Can customize bookmark color after interacting with color options', async ({ - comfyPage - }) => { - // Open customization dialog - await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/']) - const tab = comfyPage.menu.nodeLibraryTab - await tab.getFolder('foo').click({ button: 'right' }) - await comfyPage.page.getByLabel('Customize').click() - - // Click a color option multiple times - const customColorOption = comfyPage.page.locator( - '.p-togglebutton-content > .pi-palette' - ) - await customColorOption.click() - await customColorOption.click() - - // Use the color picker - await comfyPage.page - .getByLabel('Customize Folder') - .getByRole('textbox') - .click() - await comfyPage.page.locator('.p-colorpicker-color-background').click() - - // Finalize the customization - await comfyPage.page - .locator('.icon-field .p-selectbutton > *:nth-child(2)') - .click() - await comfyPage.page.getByLabel('Confirm').click() - await comfyPage.nextFrame() - - // Verify the color selection is saved - const setting = await comfyPage.getSetting( - 'Comfy.NodeLibrary.BookmarksCustomization' - ) - await expect(setting).toHaveProperty(['foo/', 'color']) - await expect(setting['foo/'].color).not.toBeNull() - await expect(setting['foo/'].color).not.toBeUndefined() - await expect(setting['foo/'].color).not.toBe('') - }) - - test('Can rename customized bookmark folder', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/']) - await comfyPage.setSetting('Comfy.NodeLibrary.BookmarksCustomization', { - 'foo/': { - icon: 'pi-folder', - color: '#007bff' - } - }) - const tab = comfyPage.menu.nodeLibraryTab - await tab.getFolder('foo').click({ button: 'right' }) - await comfyPage.page - .locator('.p-contextmenu-item-label:has-text("Rename")') - .click() - await comfyPage.page.keyboard.insertText('bar') - await comfyPage.page.keyboard.press('Enter') - await comfyPage.nextFrame() - expect( - await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2') - ).toEqual(['bar/']) - expect( - await comfyPage.getSetting('Comfy.NodeLibrary.BookmarksCustomization') - ).toEqual({ - 'bar/': { - icon: 'pi-folder', - color: '#007bff' - } - }) - }) - - test('Can delete customized bookmark folder', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/']) - await comfyPage.setSetting('Comfy.NodeLibrary.BookmarksCustomization', { - 'foo/': { - icon: 'pi-folder', - color: '#007bff' - } - }) - const tab = comfyPage.menu.nodeLibraryTab - await tab.getFolder('foo').click({ button: 'right' }) - await comfyPage.page.getByLabel('Delete').click() - await comfyPage.nextFrame() - expect( - await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2') - ).toEqual([]) - expect( - await comfyPage.getSetting('Comfy.NodeLibrary.BookmarksCustomization') - ).toEqual({}) - }) - - test('Can filter nodes in both trees', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [ - 'foo/', - 'foo/KSamplerAdvanced', - 'KSampler' - ]) - - const tab = comfyPage.menu.nodeLibraryTab - await tab.nodeLibrarySearchBoxInput.fill('KSampler') - // Node search box is debounced and may take some time to update. - await comfyPage.page.waitForTimeout(1000) - expect(await tab.getNode('KSampler (Advanced)').count()).toBe(2) - }) - }) - - test.describe('Workflows sidebar', () => { - test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting( - 'Comfy.Workflow.WorkflowTabsPosition', - 'Sidebar' - ) - - // Open the sidebar - const tab = comfyPage.menu.workflowsTab - await tab.open() - }) - - test.afterEach(async ({ comfyPage }) => { - await comfyPage.setupWorkflowsDirectory({}) - }) - - test('Can create new blank workflow', async ({ comfyPage }) => { - const tab = comfyPage.menu.workflowsTab - expect(await tab.getOpenedWorkflowNames()).toEqual([ - '*Unsaved Workflow.json' - ]) - - await comfyPage.executeCommand('Comfy.NewBlankWorkflow') - expect(await tab.getOpenedWorkflowNames()).toEqual([ - '*Unsaved Workflow.json', - '*Unsaved Workflow (2).json' - ]) - }) - - test('Can show top level saved workflows', async ({ comfyPage }) => { - await comfyPage.setupWorkflowsDirectory({ - 'workflow1.json': 'default.json', - 'workflow2.json': 'default.json' - }) - await comfyPage.setup() - - const tab = comfyPage.menu.workflowsTab - await tab.open() - expect(await tab.getTopLevelSavedWorkflowNames()).toEqual( - expect.arrayContaining(['workflow1.json', 'workflow2.json']) - ) - }) - - test('Can duplicate workflow', async ({ comfyPage }) => { - const tab = comfyPage.menu.workflowsTab - await comfyPage.menu.topbar.saveWorkflow('workflow1.json') - - expect(await tab.getTopLevelSavedWorkflowNames()).toEqual( - expect.arrayContaining(['workflow1.json']) - ) - - await comfyPage.executeCommand('Comfy.DuplicateWorkflow') - expect(await tab.getOpenedWorkflowNames()).toEqual([ - 'workflow1.json', - '*workflow1 (Copy).json' - ]) - - await comfyPage.executeCommand('Comfy.DuplicateWorkflow') - expect(await tab.getOpenedWorkflowNames()).toEqual([ - 'workflow1.json', - '*workflow1 (Copy).json', - '*workflow1 (Copy) (2).json' - ]) - - await comfyPage.executeCommand('Comfy.DuplicateWorkflow') - expect(await tab.getOpenedWorkflowNames()).toEqual([ - 'workflow1.json', - '*workflow1 (Copy).json', - '*workflow1 (Copy) (2).json', - '*workflow1 (Copy) (3).json' - ]) - }) - - test('Can open workflow after insert', async ({ comfyPage }) => { - await comfyPage.setupWorkflowsDirectory({ - 'workflow1.json': 'single_ksampler.json' - }) - await comfyPage.setup() - - const tab = comfyPage.menu.workflowsTab - await tab.open() - await comfyPage.executeCommand('Comfy.LoadDefaultWorkflow') - const originalNodeCount = (await comfyPage.getNodes()).length - - await tab.insertWorkflow(tab.getPersistedItem('workflow1.json')) - await comfyPage.nextFrame() - expect((await comfyPage.getNodes()).length).toEqual(originalNodeCount + 1) - - await tab.getPersistedItem('workflow1.json').click() - await comfyPage.nextFrame() - expect((await comfyPage.getNodes()).length).toEqual(1) - }) - - test('Can rename nested workflow from opened workflow item', async ({ - comfyPage - }) => { - await comfyPage.setupWorkflowsDirectory({ - foo: { - 'bar.json': 'default.json' - } - }) - await comfyPage.setup() - - const tab = comfyPage.menu.workflowsTab - await tab.open() - // Switch to the parent folder - await tab.getPersistedItem('foo').click() - await comfyPage.page.waitForTimeout(300) - // Switch to the nested workflow - await tab.getPersistedItem('bar').click() - await comfyPage.page.waitForTimeout(300) - - const openedWorkflow = tab.getOpenedItem('foo/bar') - await tab.renameWorkflow(openedWorkflow, 'foo/baz') - expect(await tab.getOpenedWorkflowNames()).toEqual([ - '*Unsaved Workflow.json', - 'foo/baz.json' - ]) - }) - - test('Can save workflow as', async ({ comfyPage }) => { - await comfyPage.executeCommand('Comfy.NewBlankWorkflow') - await comfyPage.menu.topbar.saveWorkflowAs('workflow3.json') - expect( - await comfyPage.menu.workflowsTab.getOpenedWorkflowNames() - ).toEqual(['*Unsaved Workflow.json', 'workflow3.json']) - - await comfyPage.menu.topbar.saveWorkflowAs('workflow4.json') - expect( - await comfyPage.menu.workflowsTab.getOpenedWorkflowNames() - ).toEqual(['*Unsaved Workflow.json', 'workflow3.json', 'workflow4.json']) - }) - - test('Exported workflow does not contain localized slot names', async ({ - comfyPage - }) => { - await comfyPage.loadWorkflow('default') - const exportedWorkflow = await comfyPage.getExportedWorkflow({ - api: false - }) - expect(exportedWorkflow).toBeDefined() - for (const node of exportedWorkflow.nodes) { - for (const slot of node.inputs) { - expect(slot.localized_name).toBeUndefined() - expect(slot.label).toBeUndefined() - } - for (const slot of node.outputs) { - expect(slot.localized_name).toBeUndefined() - expect(slot.label).toBeUndefined() - } - } - }) - - test('Can export same workflow with different locales', async ({ - comfyPage - }) => { - await comfyPage.loadWorkflow('default') - - // Setup download listener before triggering the export - const downloadPromise = comfyPage.page.waitForEvent('download') - await comfyPage.menu.topbar.exportWorkflow('exported_default.json') - - // Wait for the download and get the file content - const download = await downloadPromise - expect(download.suggestedFilename()).toBe('exported_default.json') - - // Get the exported workflow content - const downloadedContent = await comfyPage.getExportedWorkflow({ - api: false - }) - - await comfyPage.setSetting('Comfy.Locale', 'zh') - await comfyPage.setup() - - const downloadedContentZh = await comfyPage.getExportedWorkflow({ - api: false - }) - - // Compare the exported workflow with the original - delete downloadedContent.id - delete downloadedContentZh.id - expect(downloadedContent).toBeDefined() - expect(downloadedContent).toEqual(downloadedContentZh) - }) - - test('Can save workflow as with same name', async ({ comfyPage }) => { - await comfyPage.menu.topbar.saveWorkflow('workflow5.json') - expect( - await comfyPage.menu.workflowsTab.getOpenedWorkflowNames() - ).toEqual(['workflow5.json']) - - await comfyPage.menu.topbar.saveWorkflowAs('workflow5.json') - await comfyPage.confirmDialog.click('overwrite') - expect( - await comfyPage.menu.workflowsTab.getOpenedWorkflowNames() - ).toEqual(['workflow5.json']) - }) - - test('Can save temporary workflow with unmodified name', async ({ - comfyPage - }) => { - expect(await comfyPage.isCurrentWorkflowModified()).toBe(false) - - await comfyPage.menu.topbar.saveWorkflow('Unsaved Workflow') - // Should not trigger the overwrite dialog - expect( - await comfyPage.page.locator('.comfy-modal-content:visible').count() - ).toBe(0) - - expect(await comfyPage.isCurrentWorkflowModified()).toBe(false) - }) - - test('Can overwrite other workflows with save as', async ({ - comfyPage - }) => { - const topbar = comfyPage.menu.topbar - await topbar.saveWorkflow('workflow1.json') - await topbar.saveWorkflowAs('workflow2.json') - await comfyPage.nextFrame() - expect( - await comfyPage.menu.workflowsTab.getOpenedWorkflowNames() - ).toEqual(['workflow1.json', 'workflow2.json']) - expect(await comfyPage.menu.workflowsTab.getActiveWorkflowName()).toEqual( - 'workflow2.json' - ) - - await topbar.saveWorkflowAs('workflow1.json') - await comfyPage.confirmDialog.click('overwrite') - // The old workflow1.json should be deleted and the new one should be saved. - expect( - await comfyPage.menu.workflowsTab.getOpenedWorkflowNames() - ).toEqual(['workflow2.json', 'workflow1.json']) - expect(await comfyPage.menu.workflowsTab.getActiveWorkflowName()).toEqual( - 'workflow1.json' - ) - }) - - test('Does not report warning when switching between opened workflows', async ({ - comfyPage - }) => { - await comfyPage.loadWorkflow('missing_nodes') - await comfyPage.closeDialog() - - // Load blank workflow - await comfyPage.menu.workflowsTab.open() - await comfyPage.executeCommand('Comfy.NewBlankWorkflow') - - // Switch back to the missing_nodes workflow - await comfyPage.menu.workflowsTab.switchToWorkflow('missing_nodes') - - await expect( - comfyPage.page.locator('.comfy-missing-nodes') - ).not.toBeVisible() - }) - - test('Can close saved-workflows from the open workflows section', async ({ - comfyPage - }) => { - await comfyPage.menu.topbar.saveWorkflow( - `tempWorkflow-${test.info().title}` - ) - const closeButton = comfyPage.page.locator( - '.comfyui-workflows-open .close-workflow-button' - ) - await closeButton.click() - expect( - await comfyPage.menu.workflowsTab.getOpenedWorkflowNames() - ).toEqual(['*Unsaved Workflow.json']) - }) - - test('Can close saved workflow with command', async ({ comfyPage }) => { - const tab = comfyPage.menu.workflowsTab - await comfyPage.menu.topbar.saveWorkflow('workflow1.json') - await comfyPage.executeCommand('Workspace.CloseWorkflow') - expect(await tab.getOpenedWorkflowNames()).toEqual([ - '*Unsaved Workflow.json' - ]) - }) - - test('Can delete workflows (confirm disabled)', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.Workflow.ConfirmDelete', false) - - const { topbar, workflowsTab } = comfyPage.menu - - const filename = 'workflow18.json' - await topbar.saveWorkflow(filename) - expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([filename]) - - await workflowsTab.getOpenedItem(filename).click({ button: 'right' }) - await comfyPage.nextFrame() - await comfyPage.clickContextMenuItem('Delete') - - await expect(workflowsTab.getOpenedItem(filename)).not.toBeVisible() - expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([ - '*Unsaved Workflow.json' - ]) - }) - - test('Can delete workflows', async ({ comfyPage }) => { - const { topbar, workflowsTab } = comfyPage.menu - - const filename = 'workflow18.json' - await topbar.saveWorkflow(filename) - expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([filename]) - - await workflowsTab.getOpenedItem(filename).click({ button: 'right' }) - await comfyPage.clickContextMenuItem('Delete') - - await comfyPage.confirmDialog.click('delete') - - await expect(workflowsTab.getOpenedItem(filename)).not.toBeVisible() - expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([ - '*Unsaved Workflow.json' - ]) - }) - - test('Can drop workflow from workflows sidebar', async ({ comfyPage }) => { - await comfyPage.setupWorkflowsDirectory({ - 'workflow1.json': 'default.json' - }) - await comfyPage.setup() - await comfyPage.menu.workflowsTab.open() - - const nodeCount = await comfyPage.getGraphNodesCount() - - // Get the bounding box of the canvas element - const canvasBoundingBox = (await comfyPage.page - .locator('#graph-canvas') - .boundingBox())! - - // Calculate the center position of the canvas - const targetPosition = { - x: canvasBoundingBox.x + canvasBoundingBox.width / 2, - y: canvasBoundingBox.y + canvasBoundingBox.height / 2 - } - - await comfyPage.page.dragAndDrop( - '.comfyui-workflows-browse .node-label:has-text("workflow1.json")', - '#graph-canvas', - { targetPosition } - ) - // Wait for the workflow to be inserted - await comfyPage.page.waitForTimeout(200) - expect(await comfyPage.getGraphNodesCount()).toBe(nodeCount * 2) - }) - }) - test.describe('Workflows topbar tabs', () => { test.beforeEach(async ({ comfyPage }) => { await comfyPage.setSetting( @@ -822,204 +127,3 @@ test.describe('Menu', () => { }) }) }) - -test.describe.skip('Queue sidebar', () => { - test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') - }) - - test('can display tasks', async ({ comfyPage }) => { - await comfyPage.setupHistory().withTask(['example.webp']).setupRoutes() - await comfyPage.menu.queueTab.open() - await comfyPage.menu.queueTab.waitForTasks() - expect(await comfyPage.menu.queueTab.visibleTasks.count()).toBe(1) - }) - - test('can display tasks after closing then opening', async ({ - comfyPage - }) => { - await comfyPage.setupHistory().withTask(['example.webp']).setupRoutes() - await comfyPage.menu.queueTab.open() - await comfyPage.menu.queueTab.close() - await comfyPage.menu.queueTab.open() - await comfyPage.menu.queueTab.waitForTasks() - expect(await comfyPage.menu.queueTab.visibleTasks.count()).toBe(1) - }) - - test.describe('Virtual scroll', () => { - const layouts = [ - { description: 'Five columns layout', width: 95, rows: 3, cols: 5 }, - { description: 'Three columns layout', width: 55, rows: 3, cols: 3 }, - { description: 'Two columns layout', width: 40, rows: 3, cols: 2 } - ] - - test.beforeEach(async ({ comfyPage }) => { - await comfyPage - .setupHistory() - .withTask(['example.webp']) - .repeat(50) - .setupRoutes() - }) - - layouts.forEach(({ description, width, rows, cols }) => { - const preRenderedRows = 1 - const preRenderedTasks = preRenderedRows * cols * 2 - const visibleTasks = rows * cols - const expectRenderLimit = visibleTasks + preRenderedTasks - - test.describe(description, () => { - test.beforeEach(async ({ comfyPage }) => { - await comfyPage.menu.queueTab.setTabWidth(width) - await comfyPage.menu.queueTab.open() - await comfyPage.menu.queueTab.waitForTasks() - }) - - test('should not render items outside of view', async ({ - comfyPage - }) => { - const renderedCount = - await comfyPage.menu.queueTab.visibleTasks.count() - expect(renderedCount).toBeLessThanOrEqual(expectRenderLimit) - }) - - test('should teardown items after scrolling away', async ({ - comfyPage - }) => { - await comfyPage.menu.queueTab.scrollTasks('down') - const renderedCount = - await comfyPage.menu.queueTab.visibleTasks.count() - expect(renderedCount).toBeLessThanOrEqual(expectRenderLimit) - }) - - test('should re-render items after scrolling away then back', async ({ - comfyPage - }) => { - await comfyPage.menu.queueTab.scrollTasks('down') - await comfyPage.menu.queueTab.scrollTasks('up') - const renderedCount = - await comfyPage.menu.queueTab.visibleTasks.count() - expect(renderedCount).toBeLessThanOrEqual(expectRenderLimit) - }) - }) - }) - }) - - test.describe('Expand tasks', () => { - test.beforeEach(async ({ comfyPage }) => { - // 2-item batch and 3-item batch -> 3 additional items when expanded - await comfyPage - .setupHistory() - .withTask(['example.webp', 'example.webp', 'example.webp']) - .withTask(['example.webp', 'example.webp']) - .setupRoutes() - await comfyPage.menu.queueTab.open() - await comfyPage.menu.queueTab.waitForTasks() - }) - - test('can expand tasks with multiple outputs', async ({ comfyPage }) => { - const initialCount = await comfyPage.menu.queueTab.visibleTasks.count() - await comfyPage.menu.queueTab.expandTasks() - expect(await comfyPage.menu.queueTab.visibleTasks.count()).toBe( - initialCount + 3 - ) - }) - - test('can collapse flat tasks', async ({ comfyPage }) => { - const initialCount = await comfyPage.menu.queueTab.visibleTasks.count() - await comfyPage.menu.queueTab.expandTasks() - await comfyPage.menu.queueTab.collapseTasks() - expect(await comfyPage.menu.queueTab.visibleTasks.count()).toBe( - initialCount - ) - }) - }) - - test.describe('Clear tasks', () => { - test.beforeEach(async ({ comfyPage }) => { - await comfyPage - .setupHistory() - .withTask(['example.webp']) - .repeat(6) - .setupRoutes() - await comfyPage.menu.queueTab.open() - }) - - test('can clear all tasks', async ({ comfyPage }) => { - await comfyPage.menu.queueTab.clearTasks() - expect(await comfyPage.menu.queueTab.visibleTasks.count()).toBe(0) - expect( - await comfyPage.menu.queueTab.noResultsPlaceholder.isVisible() - ).toBe(true) - }) - - test('can load new tasks after clearing all', async ({ comfyPage }) => { - await comfyPage.menu.queueTab.clearTasks() - await comfyPage.menu.queueTab.close() - await comfyPage.setupHistory().withTask(['example.webp']).setupRoutes() - await comfyPage.menu.queueTab.open() - await comfyPage.menu.queueTab.waitForTasks() - expect(await comfyPage.menu.queueTab.visibleTasks.count()).toBe(1) - }) - }) - - test.describe('Gallery', () => { - const firstImage = 'example.webp' - const secondImage = 'image32x32.webp' - - test.beforeEach(async ({ comfyPage }) => { - await comfyPage - .setupHistory() - .withTask([secondImage]) - .withTask([firstImage]) - .setupRoutes() - await comfyPage.menu.queueTab.open() - await comfyPage.menu.queueTab.waitForTasks() - await comfyPage.menu.queueTab.openTaskPreview(0) - }) - - test('displays gallery image after opening task preview', async ({ - comfyPage - }) => { - await comfyPage.nextFrame() - expect(comfyPage.menu.queueTab.getGalleryImage(firstImage)).toBeVisible() - }) - - test('maintains active gallery item when new tasks are added', async ({ - comfyPage - }) => { - // Add a new task while the gallery is still open - const newImage = 'image64x64.webp' - comfyPage.setupHistory().withTask([newImage]) - await comfyPage.menu.queueTab.triggerTasksUpdate() - await comfyPage.page.waitForTimeout(500) - const newTask = comfyPage.menu.queueTab.tasks.getByAltText(newImage) - await newTask.waitFor({ state: 'visible' }) - // The active gallery item should still be the initial image - expect(comfyPage.menu.queueTab.getGalleryImage(firstImage)).toBeVisible() - }) - - test.describe('Gallery navigation', () => { - const paths: { - description: string - path: ('Right' | 'Left')[] - end: string - }[] = [ - { description: 'Right', path: ['Right'], end: secondImage }, - { description: 'Left', path: ['Right', 'Left'], end: firstImage }, - { description: 'Left wrap', path: ['Left'], end: secondImage }, - { description: 'Right wrap', path: ['Right', 'Right'], end: firstImage } - ] - - paths.forEach(({ description, path, end }) => { - test(`can navigate gallery ${description}`, async ({ comfyPage }) => { - for (const direction of path) - await comfyPage.page.keyboard.press(`Arrow${direction}`, { - delay: 256 - }) - await comfyPage.nextFrame() - expect(comfyPage.menu.queueTab.getGalleryImage(end)).toBeVisible() - }) - }) - }) - }) -}) diff --git a/browser_tests/tests/sidebar/nodeLibrary.spec.ts b/browser_tests/tests/sidebar/nodeLibrary.spec.ts new file mode 100644 index 0000000000..58f2ae4484 --- /dev/null +++ b/browser_tests/tests/sidebar/nodeLibrary.spec.ts @@ -0,0 +1,339 @@ +import { expect } from '@playwright/test' + +import { comfyPageFixture as test } from '../../fixtures/ComfyPage' + +test.describe('Node library sidebar', () => { + test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', []) + await comfyPage.setSetting('Comfy.NodeLibrary.BookmarksCustomization', {}) + // Open the sidebar + const tab = comfyPage.menu.nodeLibraryTab + await tab.open() + }) + + test('Node preview and drag to canvas', async ({ comfyPage }) => { + const tab = comfyPage.menu.nodeLibraryTab + await tab.getFolder('sampling').click() + + // Hover over a node to display the preview + const nodeSelector = '.p-tree-node-leaf' + await comfyPage.page.hover(nodeSelector) + + // Verify the preview is displayed + const previewVisible = await comfyPage.page.isVisible( + '.node-lib-node-preview' + ) + expect(previewVisible).toBe(true) + + const count = await comfyPage.getGraphNodesCount() + // Drag the node onto the canvas + const canvasSelector = '#graph-canvas' + + // Get the bounding box of the canvas element + const canvasBoundingBox = (await comfyPage.page + .locator(canvasSelector) + .boundingBox())! + + // Calculate the center position of the canvas + const targetPosition = { + x: canvasBoundingBox.x + canvasBoundingBox.width / 2, + y: canvasBoundingBox.y + canvasBoundingBox.height / 2 + } + + await comfyPage.page.dragAndDrop(nodeSelector, canvasSelector, { + targetPosition + }) + + // Verify the node is added to the canvas + expect(await comfyPage.getGraphNodesCount()).toBe(count + 1) + }) + + test('Bookmark node', async ({ comfyPage }) => { + const tab = comfyPage.menu.nodeLibraryTab + await tab.getFolder('sampling').click() + + // Bookmark the node + await tab.getNode('KSampler (Advanced)').locator('.bookmark-button').click() + + // Verify the bookmark is added to the bookmarks tab + expect( + await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2') + ).toEqual(['KSamplerAdvanced']) + // Verify the bookmark node with the same name is added to the tree. + expect(await tab.getNode('KSampler (Advanced)').count()).toBe(2) + + // Hover on the bookmark node to display the preview + await comfyPage.page.hover('.node-lib-bookmark-tree-explorer .tree-leaf') + expect(await comfyPage.page.isVisible('.node-lib-node-preview')).toBe(true) + }) + + test('Ignores unrecognized node', async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo']) + + const tab = comfyPage.menu.nodeLibraryTab + expect(await tab.getFolder('sampling').count()).toBe(1) + expect(await tab.getNode('foo').count()).toBe(0) + }) + + test('Displays empty bookmarks folder', async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/']) + const tab = comfyPage.menu.nodeLibraryTab + expect(await tab.getFolder('foo').count()).toBe(1) + }) + + test('Can add new bookmark folder', async ({ comfyPage }) => { + const tab = comfyPage.menu.nodeLibraryTab + await tab.newFolderButton.click() + const textInput = comfyPage.page.locator('.editable-text input') + await textInput.waitFor({ state: 'visible' }) + await textInput.fill('New Folder') + await textInput.press('Enter') + expect(await tab.getFolder('New Folder').count()).toBe(1) + expect( + await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2') + ).toEqual(['New Folder/']) + }) + + test('Can add nested bookmark folder', async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/']) + const tab = comfyPage.menu.nodeLibraryTab + + await tab.getFolder('foo').click({ button: 'right' }) + await comfyPage.page.getByLabel('New Folder').click() + const textInput = comfyPage.page.locator('.editable-text input') + await textInput.waitFor({ state: 'visible' }) + await textInput.fill('bar') + await textInput.press('Enter') + + expect(await tab.getFolder('bar').count()).toBe(1) + expect( + await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2') + ).toEqual(['foo/', 'foo/bar/']) + }) + + test('Can delete bookmark folder', async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/']) + const tab = comfyPage.menu.nodeLibraryTab + + await tab.getFolder('foo').click({ button: 'right' }) + await comfyPage.page.getByLabel('Delete').click() + + expect( + await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2') + ).toEqual([]) + }) + + test('Can rename bookmark folder', async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/']) + const tab = comfyPage.menu.nodeLibraryTab + + await tab.getFolder('foo').click({ button: 'right' }) + await comfyPage.page + .locator('.p-contextmenu-item-label:has-text("Rename")') + .click() + await comfyPage.page.keyboard.insertText('bar') + await comfyPage.page.keyboard.press('Enter') + + expect( + await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2') + ).toEqual(['bar/']) + }) + + test('Can add bookmark by dragging node to bookmark folder', async ({ + comfyPage + }) => { + await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/']) + const tab = comfyPage.menu.nodeLibraryTab + await tab.getFolder('sampling').click() + await comfyPage.page.dragAndDrop( + tab.nodeSelector('KSampler (Advanced)'), + tab.folderSelector('foo') + ) + expect( + await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2') + ).toEqual(['foo/', 'foo/KSamplerAdvanced']) + }) + + test('Can add bookmark by clicking bookmark button', async ({ + comfyPage + }) => { + const tab = comfyPage.menu.nodeLibraryTab + await tab.getFolder('sampling').click() + await tab.getNode('KSampler (Advanced)').locator('.bookmark-button').click() + expect( + await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2') + ).toEqual(['KSamplerAdvanced']) + }) + + test('Can unbookmark node (Top level bookmark)', async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [ + 'KSamplerAdvanced' + ]) + const tab = comfyPage.menu.nodeLibraryTab + await tab.getNode('KSampler (Advanced)').locator('.bookmark-button').click() + expect( + await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2') + ).toEqual([]) + }) + + test('Can unbookmark node (Library node bookmark)', async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [ + 'KSamplerAdvanced' + ]) + const tab = comfyPage.menu.nodeLibraryTab + await tab.getFolder('sampling').click() + await comfyPage.page + .locator(tab.nodeSelector('KSampler (Advanced)')) + .nth(1) + .locator('.bookmark-button') + .click() + expect( + await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2') + ).toEqual([]) + }) + test('Can customize icon', async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/']) + const tab = comfyPage.menu.nodeLibraryTab + await tab.getFolder('foo').click({ button: 'right' }) + await comfyPage.page.getByLabel('Customize').click() + await comfyPage.page + .locator('.icon-field .p-selectbutton > *:nth-child(2)') + .click() + await comfyPage.page + .locator('.color-field .p-selectbutton > *:nth-child(2)') + .click() + await comfyPage.page.getByLabel('Confirm').click() + await comfyPage.nextFrame() + expect( + await comfyPage.getSetting('Comfy.NodeLibrary.BookmarksCustomization') + ).toEqual({ + 'foo/': { + icon: 'pi-folder', + color: '#007bff' + } + }) + }) + // If color is left as default, it should not be saved + test('Can customize icon (default field)', async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/']) + const tab = comfyPage.menu.nodeLibraryTab + await tab.getFolder('foo').click({ button: 'right' }) + await comfyPage.page.getByLabel('Customize').click() + await comfyPage.page + .locator('.icon-field .p-selectbutton > *:nth-child(2)') + .click() + await comfyPage.page.getByLabel('Confirm').click() + await comfyPage.nextFrame() + expect( + await comfyPage.getSetting('Comfy.NodeLibrary.BookmarksCustomization') + ).toEqual({ + 'foo/': { + icon: 'pi-folder' + } + }) + }) + + test('Can customize bookmark color after interacting with color options', async ({ + comfyPage + }) => { + // Open customization dialog + await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/']) + const tab = comfyPage.menu.nodeLibraryTab + await tab.getFolder('foo').click({ button: 'right' }) + await comfyPage.page.getByLabel('Customize').click() + + // Click a color option multiple times + const customColorOption = comfyPage.page.locator( + '.p-togglebutton-content > .pi-palette' + ) + await customColorOption.click() + await customColorOption.click() + + // Use the color picker + await comfyPage.page + .getByLabel('Customize Folder') + .getByRole('textbox') + .click() + await comfyPage.page.locator('.p-colorpicker-color-background').click() + + // Finalize the customization + await comfyPage.page + .locator('.icon-field .p-selectbutton > *:nth-child(2)') + .click() + await comfyPage.page.getByLabel('Confirm').click() + await comfyPage.nextFrame() + + // Verify the color selection is saved + const setting = await comfyPage.getSetting( + 'Comfy.NodeLibrary.BookmarksCustomization' + ) + await expect(setting).toHaveProperty(['foo/', 'color']) + await expect(setting['foo/'].color).not.toBeNull() + await expect(setting['foo/'].color).not.toBeUndefined() + await expect(setting['foo/'].color).not.toBe('') + }) + + test('Can rename customized bookmark folder', async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/']) + await comfyPage.setSetting('Comfy.NodeLibrary.BookmarksCustomization', { + 'foo/': { + icon: 'pi-folder', + color: '#007bff' + } + }) + const tab = comfyPage.menu.nodeLibraryTab + await tab.getFolder('foo').click({ button: 'right' }) + await comfyPage.page + .locator('.p-contextmenu-item-label:has-text("Rename")') + .click() + await comfyPage.page.keyboard.insertText('bar') + await comfyPage.page.keyboard.press('Enter') + await comfyPage.nextFrame() + expect( + await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2') + ).toEqual(['bar/']) + expect( + await comfyPage.getSetting('Comfy.NodeLibrary.BookmarksCustomization') + ).toEqual({ + 'bar/': { + icon: 'pi-folder', + color: '#007bff' + } + }) + }) + + test('Can delete customized bookmark folder', async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/']) + await comfyPage.setSetting('Comfy.NodeLibrary.BookmarksCustomization', { + 'foo/': { + icon: 'pi-folder', + color: '#007bff' + } + }) + const tab = comfyPage.menu.nodeLibraryTab + await tab.getFolder('foo').click({ button: 'right' }) + await comfyPage.page.getByLabel('Delete').click() + await comfyPage.nextFrame() + expect( + await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2') + ).toEqual([]) + expect( + await comfyPage.getSetting('Comfy.NodeLibrary.BookmarksCustomization') + ).toEqual({}) + }) + + test('Can filter nodes in both trees', async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [ + 'foo/', + 'foo/KSamplerAdvanced', + 'KSampler' + ]) + + const tab = comfyPage.menu.nodeLibraryTab + await tab.nodeLibrarySearchBoxInput.fill('KSampler') + // Node search box is debounced and may take some time to update. + await comfyPage.page.waitForTimeout(1000) + expect(await tab.getNode('KSampler (Advanced)').count()).toBe(2) + }) +}) diff --git a/browser_tests/tests/sidebar/queue.spec.ts b/browser_tests/tests/sidebar/queue.spec.ts new file mode 100644 index 0000000000..2d9dd10bae --- /dev/null +++ b/browser_tests/tests/sidebar/queue.spec.ts @@ -0,0 +1,204 @@ +import { expect } from '@playwright/test' + +import { comfyPageFixture as test } from '../../fixtures/ComfyPage' + +test.describe.skip('Queue sidebar', () => { + test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') + }) + + test('can display tasks', async ({ comfyPage }) => { + await comfyPage.setupHistory().withTask(['example.webp']).setupRoutes() + await comfyPage.menu.queueTab.open() + await comfyPage.menu.queueTab.waitForTasks() + expect(await comfyPage.menu.queueTab.visibleTasks.count()).toBe(1) + }) + + test('can display tasks after closing then opening', async ({ + comfyPage + }) => { + await comfyPage.setupHistory().withTask(['example.webp']).setupRoutes() + await comfyPage.menu.queueTab.open() + await comfyPage.menu.queueTab.close() + await comfyPage.menu.queueTab.open() + await comfyPage.menu.queueTab.waitForTasks() + expect(await comfyPage.menu.queueTab.visibleTasks.count()).toBe(1) + }) + + test.describe('Virtual scroll', () => { + const layouts = [ + { description: 'Five columns layout', width: 95, rows: 3, cols: 5 }, + { description: 'Three columns layout', width: 55, rows: 3, cols: 3 }, + { description: 'Two columns layout', width: 40, rows: 3, cols: 2 } + ] + + test.beforeEach(async ({ comfyPage }) => { + await comfyPage + .setupHistory() + .withTask(['example.webp']) + .repeat(50) + .setupRoutes() + }) + + layouts.forEach(({ description, width, rows, cols }) => { + const preRenderedRows = 1 + const preRenderedTasks = preRenderedRows * cols * 2 + const visibleTasks = rows * cols + const expectRenderLimit = visibleTasks + preRenderedTasks + + test.describe(description, () => { + test.beforeEach(async ({ comfyPage }) => { + await comfyPage.menu.queueTab.setTabWidth(width) + await comfyPage.menu.queueTab.open() + await comfyPage.menu.queueTab.waitForTasks() + }) + + test('should not render items outside of view', async ({ + comfyPage + }) => { + const renderedCount = + await comfyPage.menu.queueTab.visibleTasks.count() + expect(renderedCount).toBeLessThanOrEqual(expectRenderLimit) + }) + + test('should teardown items after scrolling away', async ({ + comfyPage + }) => { + await comfyPage.menu.queueTab.scrollTasks('down') + const renderedCount = + await comfyPage.menu.queueTab.visibleTasks.count() + expect(renderedCount).toBeLessThanOrEqual(expectRenderLimit) + }) + + test('should re-render items after scrolling away then back', async ({ + comfyPage + }) => { + await comfyPage.menu.queueTab.scrollTasks('down') + await comfyPage.menu.queueTab.scrollTasks('up') + const renderedCount = + await comfyPage.menu.queueTab.visibleTasks.count() + expect(renderedCount).toBeLessThanOrEqual(expectRenderLimit) + }) + }) + }) + }) + + test.describe('Expand tasks', () => { + test.beforeEach(async ({ comfyPage }) => { + // 2-item batch and 3-item batch -> 3 additional items when expanded + await comfyPage + .setupHistory() + .withTask(['example.webp', 'example.webp', 'example.webp']) + .withTask(['example.webp', 'example.webp']) + .setupRoutes() + await comfyPage.menu.queueTab.open() + await comfyPage.menu.queueTab.waitForTasks() + }) + + test('can expand tasks with multiple outputs', async ({ comfyPage }) => { + const initialCount = await comfyPage.menu.queueTab.visibleTasks.count() + await comfyPage.menu.queueTab.expandTasks() + expect(await comfyPage.menu.queueTab.visibleTasks.count()).toBe( + initialCount + 3 + ) + }) + + test('can collapse flat tasks', async ({ comfyPage }) => { + const initialCount = await comfyPage.menu.queueTab.visibleTasks.count() + await comfyPage.menu.queueTab.expandTasks() + await comfyPage.menu.queueTab.collapseTasks() + expect(await comfyPage.menu.queueTab.visibleTasks.count()).toBe( + initialCount + ) + }) + }) + + test.describe('Clear tasks', () => { + test.beforeEach(async ({ comfyPage }) => { + await comfyPage + .setupHistory() + .withTask(['example.webp']) + .repeat(6) + .setupRoutes() + await comfyPage.menu.queueTab.open() + }) + + test('can clear all tasks', async ({ comfyPage }) => { + await comfyPage.menu.queueTab.clearTasks() + expect(await comfyPage.menu.queueTab.visibleTasks.count()).toBe(0) + expect( + await comfyPage.menu.queueTab.noResultsPlaceholder.isVisible() + ).toBe(true) + }) + + test('can load new tasks after clearing all', async ({ comfyPage }) => { + await comfyPage.menu.queueTab.clearTasks() + await comfyPage.menu.queueTab.close() + await comfyPage.setupHistory().withTask(['example.webp']).setupRoutes() + await comfyPage.menu.queueTab.open() + await comfyPage.menu.queueTab.waitForTasks() + expect(await comfyPage.menu.queueTab.visibleTasks.count()).toBe(1) + }) + }) + + test.describe('Gallery', () => { + const firstImage = 'example.webp' + const secondImage = 'image32x32.webp' + + test.beforeEach(async ({ comfyPage }) => { + await comfyPage + .setupHistory() + .withTask([secondImage]) + .withTask([firstImage]) + .setupRoutes() + await comfyPage.menu.queueTab.open() + await comfyPage.menu.queueTab.waitForTasks() + await comfyPage.menu.queueTab.openTaskPreview(0) + }) + + test('displays gallery image after opening task preview', async ({ + comfyPage + }) => { + await comfyPage.nextFrame() + expect(comfyPage.menu.queueTab.getGalleryImage(firstImage)).toBeVisible() + }) + + test('maintains active gallery item when new tasks are added', async ({ + comfyPage + }) => { + // Add a new task while the gallery is still open + const newImage = 'image64x64.webp' + comfyPage.setupHistory().withTask([newImage]) + await comfyPage.menu.queueTab.triggerTasksUpdate() + await comfyPage.page.waitForTimeout(500) + const newTask = comfyPage.menu.queueTab.tasks.getByAltText(newImage) + await newTask.waitFor({ state: 'visible' }) + // The active gallery item should still be the initial image + expect(comfyPage.menu.queueTab.getGalleryImage(firstImage)).toBeVisible() + }) + + test.describe('Gallery navigation', () => { + const paths: { + description: string + path: ('Right' | 'Left')[] + end: string + }[] = [ + { description: 'Right', path: ['Right'], end: secondImage }, + { description: 'Left', path: ['Right', 'Left'], end: firstImage }, + { description: 'Left wrap', path: ['Left'], end: secondImage }, + { description: 'Right wrap', path: ['Right', 'Right'], end: firstImage } + ] + + paths.forEach(({ description, path, end }) => { + test(`can navigate gallery ${description}`, async ({ comfyPage }) => { + for (const direction of path) + await comfyPage.page.keyboard.press(`Arrow${direction}`, { + delay: 256 + }) + await comfyPage.nextFrame() + expect(comfyPage.menu.queueTab.getGalleryImage(end)).toBeVisible() + }) + }) + }) + }) +}) diff --git a/browser_tests/tests/sidebar/workflows.spec.ts b/browser_tests/tests/sidebar/workflows.spec.ts new file mode 100644 index 0000000000..0ddcf8dd81 --- /dev/null +++ b/browser_tests/tests/sidebar/workflows.spec.ts @@ -0,0 +1,351 @@ +import { expect } from '@playwright/test' + +import { comfyPageFixture as test } from '../../fixtures/ComfyPage' + +test.describe('Workflows sidebar', () => { + test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.setSetting('Comfy.Workflow.WorkflowTabsPosition', 'Sidebar') + + // Open the sidebar + const tab = comfyPage.menu.workflowsTab + await tab.open() + }) + + test.afterEach(async ({ comfyPage }) => { + await comfyPage.setupWorkflowsDirectory({}) + }) + + test('Can create new blank workflow', async ({ comfyPage }) => { + const tab = comfyPage.menu.workflowsTab + expect(await tab.getOpenedWorkflowNames()).toEqual([ + '*Unsaved Workflow.json' + ]) + + await comfyPage.executeCommand('Comfy.NewBlankWorkflow') + expect(await tab.getOpenedWorkflowNames()).toEqual([ + '*Unsaved Workflow.json', + '*Unsaved Workflow (2).json' + ]) + }) + + test('Can show top level saved workflows', async ({ comfyPage }) => { + await comfyPage.setupWorkflowsDirectory({ + 'workflow1.json': 'default.json', + 'workflow2.json': 'default.json' + }) + await comfyPage.setup() + + const tab = comfyPage.menu.workflowsTab + await tab.open() + expect(await tab.getTopLevelSavedWorkflowNames()).toEqual( + expect.arrayContaining(['workflow1.json', 'workflow2.json']) + ) + }) + + test('Can duplicate workflow', async ({ comfyPage }) => { + const tab = comfyPage.menu.workflowsTab + await comfyPage.menu.topbar.saveWorkflow('workflow1.json') + + expect(await tab.getTopLevelSavedWorkflowNames()).toEqual( + expect.arrayContaining(['workflow1.json']) + ) + + await comfyPage.executeCommand('Comfy.DuplicateWorkflow') + expect(await tab.getOpenedWorkflowNames()).toEqual([ + 'workflow1.json', + '*workflow1 (Copy).json' + ]) + + await comfyPage.executeCommand('Comfy.DuplicateWorkflow') + expect(await tab.getOpenedWorkflowNames()).toEqual([ + 'workflow1.json', + '*workflow1 (Copy).json', + '*workflow1 (Copy) (2).json' + ]) + + await comfyPage.executeCommand('Comfy.DuplicateWorkflow') + expect(await tab.getOpenedWorkflowNames()).toEqual([ + 'workflow1.json', + '*workflow1 (Copy).json', + '*workflow1 (Copy) (2).json', + '*workflow1 (Copy) (3).json' + ]) + }) + + test('Can open workflow after insert', async ({ comfyPage }) => { + await comfyPage.setupWorkflowsDirectory({ + 'workflow1.json': 'single_ksampler.json' + }) + await comfyPage.setup() + + const tab = comfyPage.menu.workflowsTab + await tab.open() + await comfyPage.executeCommand('Comfy.LoadDefaultWorkflow') + const originalNodeCount = (await comfyPage.getNodes()).length + + await tab.insertWorkflow(tab.getPersistedItem('workflow1.json')) + await comfyPage.nextFrame() + expect((await comfyPage.getNodes()).length).toEqual(originalNodeCount + 1) + + await tab.getPersistedItem('workflow1.json').click() + await comfyPage.nextFrame() + expect((await comfyPage.getNodes()).length).toEqual(1) + }) + + test('Can rename nested workflow from opened workflow item', async ({ + comfyPage + }) => { + await comfyPage.setupWorkflowsDirectory({ + foo: { + 'bar.json': 'default.json' + } + }) + await comfyPage.setup() + + const tab = comfyPage.menu.workflowsTab + await tab.open() + // Switch to the parent folder + await tab.getPersistedItem('foo').click() + await comfyPage.page.waitForTimeout(300) + // Switch to the nested workflow + await tab.getPersistedItem('bar').click() + await comfyPage.page.waitForTimeout(300) + + const openedWorkflow = tab.getOpenedItem('foo/bar') + await tab.renameWorkflow(openedWorkflow, 'foo/baz') + expect(await tab.getOpenedWorkflowNames()).toEqual([ + '*Unsaved Workflow.json', + 'foo/baz.json' + ]) + }) + + test('Can save workflow as', async ({ comfyPage }) => { + await comfyPage.executeCommand('Comfy.NewBlankWorkflow') + await comfyPage.menu.topbar.saveWorkflowAs('workflow3.json') + expect(await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()).toEqual([ + '*Unsaved Workflow.json', + 'workflow3.json' + ]) + + await comfyPage.menu.topbar.saveWorkflowAs('workflow4.json') + expect(await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()).toEqual([ + '*Unsaved Workflow.json', + 'workflow3.json', + 'workflow4.json' + ]) + }) + + test('Exported workflow does not contain localized slot names', async ({ + comfyPage + }) => { + await comfyPage.loadWorkflow('default') + const exportedWorkflow = await comfyPage.getExportedWorkflow({ + api: false + }) + expect(exportedWorkflow).toBeDefined() + for (const node of exportedWorkflow.nodes) { + for (const slot of node.inputs) { + expect(slot.localized_name).toBeUndefined() + expect(slot.label).toBeUndefined() + } + for (const slot of node.outputs) { + expect(slot.localized_name).toBeUndefined() + expect(slot.label).toBeUndefined() + } + } + }) + + test('Can export same workflow with different locales', async ({ + comfyPage + }) => { + await comfyPage.loadWorkflow('default') + + // Setup download listener before triggering the export + const downloadPromise = comfyPage.page.waitForEvent('download') + await comfyPage.menu.topbar.exportWorkflow('exported_default.json') + + // Wait for the download and get the file content + const download = await downloadPromise + expect(download.suggestedFilename()).toBe('exported_default.json') + + // Get the exported workflow content + const downloadedContent = await comfyPage.getExportedWorkflow({ + api: false + }) + + await comfyPage.setSetting('Comfy.Locale', 'zh') + await comfyPage.setup() + + const downloadedContentZh = await comfyPage.getExportedWorkflow({ + api: false + }) + + // Compare the exported workflow with the original + delete downloadedContent.id + delete downloadedContentZh.id + expect(downloadedContent).toBeDefined() + expect(downloadedContent).toEqual(downloadedContentZh) + }) + + test('Can save workflow as with same name', async ({ comfyPage }) => { + await comfyPage.menu.topbar.saveWorkflow('workflow5.json') + expect(await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()).toEqual([ + 'workflow5.json' + ]) + + await comfyPage.menu.topbar.saveWorkflowAs('workflow5.json') + await comfyPage.confirmDialog.click('overwrite') + expect(await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()).toEqual([ + 'workflow5.json' + ]) + }) + + test('Can save temporary workflow with unmodified name', async ({ + comfyPage + }) => { + expect(await comfyPage.isCurrentWorkflowModified()).toBe(false) + + await comfyPage.menu.topbar.saveWorkflow('Unsaved Workflow') + // Should not trigger the overwrite dialog + expect( + await comfyPage.page.locator('.comfy-modal-content:visible').count() + ).toBe(0) + + expect(await comfyPage.isCurrentWorkflowModified()).toBe(false) + }) + + test('Can overwrite other workflows with save as', async ({ comfyPage }) => { + const topbar = comfyPage.menu.topbar + await topbar.saveWorkflow('workflow1.json') + await topbar.saveWorkflowAs('workflow2.json') + await comfyPage.nextFrame() + expect(await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()).toEqual([ + 'workflow1.json', + 'workflow2.json' + ]) + expect(await comfyPage.menu.workflowsTab.getActiveWorkflowName()).toEqual( + 'workflow2.json' + ) + + await topbar.saveWorkflowAs('workflow1.json') + await comfyPage.confirmDialog.click('overwrite') + // The old workflow1.json should be deleted and the new one should be saved. + expect(await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()).toEqual([ + 'workflow2.json', + 'workflow1.json' + ]) + expect(await comfyPage.menu.workflowsTab.getActiveWorkflowName()).toEqual( + 'workflow1.json' + ) + }) + + test('Does not report warning when switching between opened workflows', async ({ + comfyPage + }) => { + await comfyPage.loadWorkflow('missing_nodes') + await comfyPage.closeDialog() + + // Load blank workflow + await comfyPage.menu.workflowsTab.open() + await comfyPage.executeCommand('Comfy.NewBlankWorkflow') + + // Switch back to the missing_nodes workflow + await comfyPage.menu.workflowsTab.switchToWorkflow('missing_nodes') + + await expect( + comfyPage.page.locator('.comfy-missing-nodes') + ).not.toBeVisible() + }) + + test('Can close saved-workflows from the open workflows section', async ({ + comfyPage + }) => { + await comfyPage.menu.topbar.saveWorkflow( + `tempWorkflow-${test.info().title}` + ) + const closeButton = comfyPage.page.locator( + '.comfyui-workflows-open .close-workflow-button' + ) + await closeButton.click() + expect(await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()).toEqual([ + '*Unsaved Workflow.json' + ]) + }) + + test('Can close saved workflow with command', async ({ comfyPage }) => { + const tab = comfyPage.menu.workflowsTab + await comfyPage.menu.topbar.saveWorkflow('workflow1.json') + await comfyPage.executeCommand('Workspace.CloseWorkflow') + expect(await tab.getOpenedWorkflowNames()).toEqual([ + '*Unsaved Workflow.json' + ]) + }) + + test('Can delete workflows (confirm disabled)', async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.Workflow.ConfirmDelete', false) + + const { topbar, workflowsTab } = comfyPage.menu + + const filename = 'workflow18.json' + await topbar.saveWorkflow(filename) + expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([filename]) + + await workflowsTab.getOpenedItem(filename).click({ button: 'right' }) + await comfyPage.nextFrame() + await comfyPage.clickContextMenuItem('Delete') + + await expect(workflowsTab.getOpenedItem(filename)).not.toBeVisible() + expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([ + '*Unsaved Workflow.json' + ]) + }) + + test('Can delete workflows', async ({ comfyPage }) => { + const { topbar, workflowsTab } = comfyPage.menu + + const filename = 'workflow18.json' + await topbar.saveWorkflow(filename) + expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([filename]) + + await workflowsTab.getOpenedItem(filename).click({ button: 'right' }) + await comfyPage.clickContextMenuItem('Delete') + + await comfyPage.confirmDialog.click('delete') + + await expect(workflowsTab.getOpenedItem(filename)).not.toBeVisible() + expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([ + '*Unsaved Workflow.json' + ]) + }) + + test('Can drop workflow from workflows sidebar', async ({ comfyPage }) => { + await comfyPage.setupWorkflowsDirectory({ + 'workflow1.json': 'default.json' + }) + await comfyPage.setup() + await comfyPage.menu.workflowsTab.open() + + const nodeCount = await comfyPage.getGraphNodesCount() + + // Get the bounding box of the canvas element + const canvasBoundingBox = (await comfyPage.page + .locator('#graph-canvas') + .boundingBox())! + + // Calculate the center position of the canvas + const targetPosition = { + x: canvasBoundingBox.x + canvasBoundingBox.width / 2, + y: canvasBoundingBox.y + canvasBoundingBox.height / 2 + } + + await comfyPage.page.dragAndDrop( + '.comfyui-workflows-browse .node-label:has-text("workflow1.json")', + '#graph-canvas', + { targetPosition } + ) + // Wait for the workflow to be inserted + await comfyPage.page.waitForTimeout(200) + expect(await comfyPage.getGraphNodesCount()).toBe(nodeCount * 2) + }) +})