mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-11 06:50:05 +00:00
Compare commits
7 Commits
test/cover
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3043b181d7 | ||
|
|
8c9328c1b2 | ||
|
|
577f373cde | ||
|
|
44f88027b6 | ||
|
|
5d07de1913 | ||
|
|
f0ae91de43 | ||
|
|
fb8025c49f |
@@ -84,6 +84,7 @@
|
||||
"typescript/no-unsafe-declaration-merging": "off",
|
||||
"typescript/no-unused-vars": "off",
|
||||
"unicorn/no-empty-file": "off",
|
||||
"vitest/require-mock-type-parameters": "off",
|
||||
"unicorn/no-new-array": "off",
|
||||
"unicorn/no-single-promise-in-promise-methods": "off",
|
||||
"unicorn/no-useless-fallback-in-spread": "off",
|
||||
@@ -116,13 +117,60 @@
|
||||
},
|
||||
{
|
||||
"files": ["browser_tests/**/*.ts"],
|
||||
"jsPlugins": ["eslint-plugin-playwright"],
|
||||
"rules": {
|
||||
"typescript/no-explicit-any": "error",
|
||||
"no-async-promise-executor": "error",
|
||||
"no-control-regex": "error",
|
||||
"no-useless-rename": "error",
|
||||
"no-unused-private-class-members": "error",
|
||||
"unicorn/no-empty-file": "error"
|
||||
"unicorn/no-empty-file": "error",
|
||||
"playwright/consistent-spacing-between-blocks": "error",
|
||||
"playwright/expect-expect": [
|
||||
"error",
|
||||
{
|
||||
"assertFunctionNames": [
|
||||
"recordMeasurement",
|
||||
"logMeasurement",
|
||||
"builderSaveAs"
|
||||
],
|
||||
"assertFunctionPatterns": [
|
||||
"^expect",
|
||||
"^assert",
|
||||
"^verify",
|
||||
"^searchAndExpect",
|
||||
"waitForOpen",
|
||||
"waitForClosed",
|
||||
"waitForRequest"
|
||||
]
|
||||
}
|
||||
],
|
||||
"playwright/max-nested-describe": "error",
|
||||
"playwright/no-duplicate-hooks": "error",
|
||||
"playwright/no-element-handle": "error",
|
||||
"playwright/no-eval": "error",
|
||||
"playwright/no-focused-test": "error",
|
||||
"playwright/no-force-option": "off",
|
||||
"playwright/no-networkidle": "error",
|
||||
"playwright/no-page-pause": "error",
|
||||
"playwright/no-skipped-test": "error",
|
||||
"playwright/no-unsafe-references": "error",
|
||||
"playwright/no-unused-locators": "error",
|
||||
"playwright/no-useless-await": "error",
|
||||
"playwright/no-useless-not": "error",
|
||||
"playwright/no-wait-for-navigation": "error",
|
||||
"playwright/no-wait-for-selector": "error",
|
||||
"playwright/no-wait-for-timeout": "error",
|
||||
"playwright/prefer-hooks-on-top": "error",
|
||||
"playwright/prefer-locator": "error",
|
||||
"playwright/prefer-to-have-count": "error",
|
||||
"playwright/prefer-to-have-length": "error",
|
||||
"playwright/prefer-web-first-assertions": "error",
|
||||
"playwright/prefer-native-locators": "error",
|
||||
"playwright/require-to-pass-timeout": "error",
|
||||
"playwright/valid-expect": "error",
|
||||
"playwright/valid-expect-in-promise": "error",
|
||||
"playwright/valid-title": "error"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -321,7 +321,7 @@ export class ComfyPage {
|
||||
// window.app.extensionManager => GraphView ready
|
||||
window.app && window.app.extensionManager
|
||||
)
|
||||
await this.page.waitForSelector('.p-blockui-mask', { state: 'hidden' })
|
||||
await this.page.locator('.p-blockui-mask').waitFor({ state: 'hidden' })
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
@@ -371,7 +371,7 @@ export class ComfyPage {
|
||||
}
|
||||
|
||||
async closeMenu() {
|
||||
await this.page.click('button.comfy-close-menu-btn')
|
||||
await this.page.locator('button.comfy-close-menu-btn').click()
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ export class VueNodeHelpers {
|
||||
*/
|
||||
getNodeByTitle(title: string): Locator {
|
||||
return this.page.locator('[data-node-id]').filter({
|
||||
has: this.page.locator('[data-testid="node-title"]', { hasText: title })
|
||||
has: this.page.getByTestId('node-title').filter({ hasText: title })
|
||||
})
|
||||
}
|
||||
|
||||
@@ -146,7 +146,7 @@ export class VueNodeHelpers {
|
||||
expectedCount
|
||||
)
|
||||
} else {
|
||||
await this.page.waitForSelector('[data-node-id]')
|
||||
await this.page.locator('[data-node-id]').first().waitFor()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -52,6 +52,6 @@ export class SettingDialog extends BaseDialog {
|
||||
name: 'About'
|
||||
})
|
||||
await aboutButton.click()
|
||||
await this.page.waitForSelector('.about-container')
|
||||
await this.page.locator('.about-container').waitFor()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,7 +301,9 @@ export class AssetsSidebarTab extends SidebarTab {
|
||||
this.gridViewOption = page.getByText('Grid view')
|
||||
this.sortNewestFirst = page.getByText('Newest first')
|
||||
this.sortOldestFirst = page.getByText('Oldest first')
|
||||
this.assetCards = page.locator('[role="button"][data-selected]')
|
||||
this.assetCards = page
|
||||
.getByRole('button')
|
||||
.and(page.locator('[data-selected]'))
|
||||
this.selectedCards = page.locator('[data-selected="true"]')
|
||||
this.listViewItems = page.locator(
|
||||
'.sidebar-content-container [role="button"][tabindex="0"]'
|
||||
|
||||
@@ -53,7 +53,7 @@ export class AppModeHelper {
|
||||
this.outputPlaceholder = this.page.getByTestId(
|
||||
TestIds.builder.outputPlaceholder
|
||||
)
|
||||
this.linearWidgets = this.page.locator('[data-testid="linear-widgets"]')
|
||||
this.linearWidgets = this.page.getByTestId('linear-widgets')
|
||||
this.imagePickerPopover = this.page
|
||||
.getByRole('dialog')
|
||||
.filter({ has: this.page.getByRole('button', { name: 'All' }) })
|
||||
|
||||
@@ -157,7 +157,7 @@ export class SubgraphHelper {
|
||||
|
||||
// Wait for the appropriate UI element to appear
|
||||
if (action === 'rightClick') {
|
||||
await this.page.waitForSelector('.litemenu-entry', {
|
||||
await this.page.locator('.litemenu-entry').first().waitFor({
|
||||
state: 'visible',
|
||||
timeout: 5000
|
||||
})
|
||||
|
||||
@@ -14,10 +14,11 @@ function makeMatcher<T>(
|
||||
) {
|
||||
await expect(async () => {
|
||||
const value = await getValue(node)
|
||||
const assertion = this.isNot
|
||||
? expect(value, 'Node is ' + type).not
|
||||
: expect(value, 'Node is not ' + type)
|
||||
assertion.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,
|
||||
|
||||
@@ -15,13 +15,11 @@ export class VueNodeFixture {
|
||||
|
||||
constructor(private readonly locator: Locator) {
|
||||
this.header = locator.locator('[data-testid^="node-header-"]')
|
||||
this.title = locator.locator('[data-testid="node-title"]')
|
||||
this.titleInput = locator.locator('[data-testid="node-title-input"]')
|
||||
this.title = locator.getByTestId('node-title')
|
||||
this.titleInput = locator.getByTestId('node-title-input')
|
||||
this.body = locator.locator('[data-testid^="node-body-"]')
|
||||
this.pinIndicator = locator.getByTestId(TestIds.node.pinIndicator)
|
||||
this.collapseButton = locator.locator(
|
||||
'[data-testid="node-collapse-button"]'
|
||||
)
|
||||
this.collapseButton = locator.getByTestId('node-collapse-button')
|
||||
this.collapseIcon = this.collapseButton.locator('i')
|
||||
this.root = locator
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ test.describe('App mode welcome states', { tag: '@ui' }, () => {
|
||||
|
||||
await expect(comfyPage.appMode.welcome).toBeVisible()
|
||||
await expect(comfyPage.appMode.emptyWorkflowText).toBeVisible()
|
||||
await expect(comfyPage.appMode.buildAppButton).not.toBeVisible()
|
||||
await expect(comfyPage.appMode.buildAppButton).toBeHidden()
|
||||
})
|
||||
|
||||
test('Build app button is visible when no outputs selected', async ({
|
||||
@@ -26,7 +26,7 @@ test.describe('App mode welcome states', { tag: '@ui' }, () => {
|
||||
|
||||
await expect(comfyPage.appMode.welcome).toBeVisible()
|
||||
await expect(comfyPage.appMode.buildAppButton).toBeVisible()
|
||||
await expect(comfyPage.appMode.emptyWorkflowText).not.toBeVisible()
|
||||
await expect(comfyPage.appMode.emptyWorkflowText).toBeHidden()
|
||||
})
|
||||
|
||||
test('Empty workflow and build app are hidden when app has outputs', async ({
|
||||
@@ -35,8 +35,8 @@ test.describe('App mode welcome states', { tag: '@ui' }, () => {
|
||||
await comfyPage.appMode.enterAppModeWithInputs([['3', 'seed']])
|
||||
|
||||
await expect(comfyPage.appMode.linearWidgets).toBeVisible()
|
||||
await expect(comfyPage.appMode.emptyWorkflowText).not.toBeVisible()
|
||||
await expect(comfyPage.appMode.buildAppButton).not.toBeVisible()
|
||||
await expect(comfyPage.appMode.emptyWorkflowText).toBeHidden()
|
||||
await expect(comfyPage.appMode.buildAppButton).toBeHidden()
|
||||
})
|
||||
|
||||
test('Back to workflow returns to graph mode', async ({ comfyPage }) => {
|
||||
@@ -46,7 +46,7 @@ test.describe('App mode welcome states', { tag: '@ui' }, () => {
|
||||
await comfyPage.appMode.backToWorkflowButton.click()
|
||||
|
||||
await expect(comfyPage.canvas).toBeVisible()
|
||||
await expect(comfyPage.appMode.welcome).not.toBeVisible()
|
||||
await expect(comfyPage.appMode.welcome).toBeHidden()
|
||||
})
|
||||
|
||||
test('Load template opens template selector', async ({ comfyPage }) => {
|
||||
|
||||
@@ -11,7 +11,7 @@ test.describe('Bottom Panel Logs', { tag: '@ui' }, () => {
|
||||
test('should open bottom panel via toggle button', async ({ comfyPage }) => {
|
||||
const { bottomPanel } = comfyPage
|
||||
|
||||
await expect(bottomPanel.root).not.toBeVisible()
|
||||
await expect(bottomPanel.root).toBeHidden()
|
||||
await bottomPanel.toggleButton.click()
|
||||
await expect(bottomPanel.root).toBeVisible()
|
||||
})
|
||||
@@ -35,7 +35,7 @@ test.describe('Bottom Panel Logs', { tag: '@ui' }, () => {
|
||||
await expect(bottomPanel.root).toBeVisible()
|
||||
|
||||
await bottomPanel.toggleButton.click()
|
||||
await expect(bottomPanel.root).not.toBeVisible()
|
||||
await expect(bottomPanel.root).toBeHidden()
|
||||
})
|
||||
|
||||
test('should switch between shortcuts and terminal panels', async ({
|
||||
@@ -55,7 +55,7 @@ test.describe('Bottom Panel Logs', { tag: '@ui' }, () => {
|
||||
await expect(logsTab).toBeVisible()
|
||||
await expect(
|
||||
comfyPage.page.locator('[id*="tab_shortcuts-essentials"]')
|
||||
).not.toBeVisible()
|
||||
).toBeHidden()
|
||||
})
|
||||
|
||||
test('should persist Logs tab content in bottom panel', async ({
|
||||
|
||||
@@ -10,11 +10,11 @@ test.describe('Bottom Panel Shortcuts', { tag: '@ui' }, () => {
|
||||
test('should toggle shortcuts panel visibility', async ({ comfyPage }) => {
|
||||
const { bottomPanel } = comfyPage
|
||||
|
||||
await expect(bottomPanel.root).not.toBeVisible()
|
||||
await expect(bottomPanel.root).toBeHidden()
|
||||
await bottomPanel.keyboardShortcutsButton.click()
|
||||
await expect(bottomPanel.root).toBeVisible()
|
||||
await bottomPanel.keyboardShortcutsButton.click()
|
||||
await expect(bottomPanel.root).not.toBeVisible()
|
||||
await expect(bottomPanel.root).toBeHidden()
|
||||
})
|
||||
|
||||
test('should display essentials shortcuts tab', async ({ comfyPage }) => {
|
||||
@@ -182,7 +182,7 @@ test.describe('Bottom Panel Shortcuts', { tag: '@ui' }, () => {
|
||||
await expect(bottomPanel.root).toBeVisible()
|
||||
|
||||
await bottomPanel.keyboardShortcutsButton.click()
|
||||
await expect(bottomPanel.root).not.toBeVisible()
|
||||
await expect(bottomPanel.root).toBeHidden()
|
||||
})
|
||||
|
||||
test('should display shortcuts in organized columns', async ({
|
||||
@@ -192,9 +192,7 @@ test.describe('Bottom Panel Shortcuts', { tag: '@ui' }, () => {
|
||||
|
||||
await bottomPanel.keyboardShortcutsButton.click()
|
||||
|
||||
await expect(
|
||||
comfyPage.page.locator('[data-testid="shortcuts-columns"]')
|
||||
).toBeVisible()
|
||||
await expect(comfyPage.page.getByTestId('shortcuts-columns')).toBeVisible()
|
||||
|
||||
const subcategoryTitles = bottomPanel.shortcuts.subcategoryTitles
|
||||
await expect.poll(() => subcategoryTitles.count()).toBeGreaterThanOrEqual(2)
|
||||
@@ -205,7 +203,7 @@ test.describe('Bottom Panel Shortcuts', { tag: '@ui' }, () => {
|
||||
}) => {
|
||||
const { bottomPanel } = comfyPage
|
||||
|
||||
await expect(bottomPanel.root).not.toBeVisible()
|
||||
await expect(bottomPanel.root).toBeHidden()
|
||||
|
||||
await comfyPage.page.keyboard.press('Control+Shift+KeyK')
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ async function saveCloseAndReopenAsApp(
|
||||
await appMode.steps.goToPreview()
|
||||
await builderSaveAs(appMode, workflowName)
|
||||
await appMode.saveAs.closeButton.click()
|
||||
await expect(appMode.saveAs.successDialog).not.toBeVisible()
|
||||
await expect(appMode.saveAs.successDialog).toBeHidden()
|
||||
|
||||
await appMode.footer.exitBuilder()
|
||||
await openWorkflowFromSidebar(comfyPage, workflowName)
|
||||
|
||||
@@ -31,7 +31,7 @@ async function dismissSuccessDialog(
|
||||
) {
|
||||
const btn = button === 'close' ? saveAs.closeButton : saveAs.dismissButton
|
||||
await btn.click()
|
||||
await expect(saveAs.successDialog).not.toBeVisible()
|
||||
await expect(saveAs.successDialog).toBeHidden()
|
||||
}
|
||||
|
||||
test.describe('Builder save flow', { tag: ['@ui'] }, () => {
|
||||
@@ -113,7 +113,7 @@ test.describe('Builder save flow', { tag: ['@ui'] }, () => {
|
||||
await comfyPage.page.keyboard.press('Escape')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await expect(comfyPage.appMode.steps.toolbar).not.toBeVisible()
|
||||
await expect(comfyPage.appMode.steps.toolbar).toBeHidden()
|
||||
})
|
||||
|
||||
test('Exit builder button exits builder mode', async ({ comfyPage }) => {
|
||||
@@ -121,7 +121,7 @@ test.describe('Builder save flow', { tag: ['@ui'] }, () => {
|
||||
|
||||
await expect(comfyPage.appMode.steps.toolbar).toBeVisible()
|
||||
await comfyPage.appMode.footer.exitBuilder()
|
||||
await expect(comfyPage.appMode.steps.toolbar).not.toBeVisible()
|
||||
await expect(comfyPage.appMode.steps.toolbar).toBeHidden()
|
||||
})
|
||||
|
||||
test('Save button directly saves for previously saved workflow', async ({
|
||||
@@ -141,7 +141,7 @@ test.describe('Builder save flow', { tag: ['@ui'] }, () => {
|
||||
await footer.saveButton.click()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await expect(saveAs.dialog).not.toBeVisible()
|
||||
await expect(saveAs.dialog).toBeHidden()
|
||||
await expect(footer.saveButton).toBeDisabled()
|
||||
})
|
||||
|
||||
@@ -253,7 +253,7 @@ test.describe('Builder save flow', { tag: ['@ui'] }, () => {
|
||||
await builderSaveAs(comfyPage.appMode, `${Date.now()} app-view`, 'App')
|
||||
|
||||
await comfyPage.appMode.saveAs.viewAppButton.click()
|
||||
await expect(comfyPage.appMode.saveAs.successDialog).not.toBeVisible()
|
||||
await expect(comfyPage.appMode.saveAs.successDialog).toBeHidden()
|
||||
|
||||
await expect
|
||||
.poll(() => comfyPage.workflow.getActiveWorkflowActiveAppMode())
|
||||
@@ -271,9 +271,9 @@ test.describe('Builder save flow', { tag: ['@ui'] }, () => {
|
||||
)
|
||||
|
||||
await comfyPage.appMode.saveAs.exitBuilderButton.click()
|
||||
await expect(comfyPage.appMode.saveAs.successDialog).not.toBeVisible()
|
||||
await expect(comfyPage.appMode.saveAs.successDialog).toBeHidden()
|
||||
|
||||
await expect(comfyPage.appMode.steps.toolbar).not.toBeVisible()
|
||||
await expect(comfyPage.appMode.steps.toolbar).toBeHidden()
|
||||
})
|
||||
|
||||
test('save as with different mode does not modify the original workflow', async ({
|
||||
@@ -327,7 +327,7 @@ test.describe('Builder save flow', { tag: ['@ui'] }, () => {
|
||||
|
||||
await expect(appMode.saveAs.overwriteDialog).toBeVisible()
|
||||
await appMode.saveAs.overwriteButton.click()
|
||||
await expect(appMode.saveAs.overwriteDialog).not.toBeVisible()
|
||||
await expect(appMode.saveAs.overwriteDialog).toBeHidden()
|
||||
|
||||
await expect(appMode.saveAs.successMessage).toBeVisible()
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ test.describe('CanvasModeSelector', { tag: '@canvas' }, () => {
|
||||
await expect(menu).toBeVisible()
|
||||
await trigger.click()
|
||||
await comfyPage.nextFrame()
|
||||
await expect(menu).not.toBeVisible()
|
||||
await expect(menu).toBeHidden()
|
||||
await expect(trigger).toHaveAttribute('aria-expanded', 'false')
|
||||
})
|
||||
|
||||
@@ -81,7 +81,7 @@ test.describe('CanvasModeSelector', { tag: '@canvas' }, () => {
|
||||
await expect(menu).toBeVisible()
|
||||
await handItem.click()
|
||||
await comfyPage.nextFrame()
|
||||
await expect(menu).not.toBeVisible()
|
||||
await expect(menu).toBeHidden()
|
||||
})
|
||||
|
||||
test('closes when Escape is pressed', async ({ comfyPage }) => {
|
||||
@@ -91,7 +91,7 @@ test.describe('CanvasModeSelector', { tag: '@canvas' }, () => {
|
||||
await expect(menu).toBeVisible()
|
||||
await selectItem.press('Escape')
|
||||
await comfyPage.nextFrame()
|
||||
await expect(menu).not.toBeVisible()
|
||||
await expect(menu).toBeHidden()
|
||||
await expect(trigger).toHaveAttribute('aria-expanded', 'false')
|
||||
})
|
||||
})
|
||||
@@ -197,7 +197,7 @@ test.describe('CanvasModeSelector', { tag: '@canvas' }, () => {
|
||||
await selectItem.press('ArrowDown')
|
||||
await handItem.press('Escape')
|
||||
await comfyPage.nextFrame()
|
||||
await expect(menu).not.toBeVisible()
|
||||
await expect(menu).toBeHidden()
|
||||
await expect(trigger).toBeFocused()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -38,13 +38,13 @@ test.describe('Default Keybindings', { tag: '@keyboard' }, () => {
|
||||
`.${tabId}-tab-button.side-bar-button-selected`
|
||||
)
|
||||
|
||||
await expect(selectedButton).not.toBeVisible()
|
||||
await expect(selectedButton).toBeHidden()
|
||||
|
||||
await comfyPage.canvas.press(key)
|
||||
await expect(selectedButton).toBeVisible()
|
||||
|
||||
await comfyPage.canvas.press(key)
|
||||
await expect(selectedButton).not.toBeVisible()
|
||||
await expect(selectedButton).toBeHidden()
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -172,7 +172,7 @@ test.describe('Default Keybindings', { tag: '@keyboard' }, () => {
|
||||
|
||||
// Toggle off with Alt+m
|
||||
await comfyPage.page.keyboard.press('Alt+KeyM')
|
||||
await expect(comfyPage.appMode.linearWidgets).not.toBeVisible()
|
||||
await expect(comfyPage.appMode.linearWidgets).toBeHidden()
|
||||
|
||||
// Toggle on again
|
||||
await comfyPage.page.keyboard.press('Alt+KeyM')
|
||||
@@ -189,7 +189,7 @@ test.describe('Default Keybindings', { tag: '@keyboard' }, () => {
|
||||
await expect(minimap).toBeVisible()
|
||||
|
||||
await comfyPage.page.keyboard.press('Alt+Shift+KeyM')
|
||||
await expect(minimap).not.toBeVisible()
|
||||
await expect(minimap).toBeHidden()
|
||||
|
||||
await comfyPage.page.keyboard.press('Alt+Shift+KeyM')
|
||||
await expect(minimap).toBeVisible()
|
||||
@@ -198,13 +198,13 @@ test.describe('Default Keybindings', { tag: '@keyboard' }, () => {
|
||||
test("'Ctrl+`' toggles terminal/logs panel", async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
|
||||
await expect(comfyPage.bottomPanel.root).not.toBeVisible()
|
||||
await expect(comfyPage.bottomPanel.root).toBeHidden()
|
||||
|
||||
await comfyPage.page.keyboard.press('Control+Backquote')
|
||||
await expect(comfyPage.bottomPanel.root).toBeVisible()
|
||||
|
||||
await comfyPage.page.keyboard.press('Control+Backquote')
|
||||
await expect(comfyPage.bottomPanel.root).not.toBeVisible()
|
||||
await expect(comfyPage.bottomPanel.root).toBeHidden()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -11,9 +11,7 @@ test.beforeEach(async ({ comfyPage }) => {
|
||||
test.describe('Settings', () => {
|
||||
test('@mobile Should be visible on mobile', async ({ comfyPage }) => {
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
const settingsDialog = comfyPage.page.locator(
|
||||
'[data-testid="settings-dialog"]'
|
||||
)
|
||||
const settingsDialog = comfyPage.page.getByTestId('settings-dialog')
|
||||
await expect(settingsDialog).toBeVisible()
|
||||
const contentArea = settingsDialog.locator('main')
|
||||
await expect(contentArea).toBeVisible()
|
||||
@@ -26,17 +24,16 @@ test.describe('Settings', () => {
|
||||
await comfyPage.page.keyboard.down('ControlOrMeta')
|
||||
await comfyPage.page.keyboard.press(',')
|
||||
await comfyPage.page.keyboard.up('ControlOrMeta')
|
||||
const settingsLocator = comfyPage.page.locator(
|
||||
'[data-testid="settings-dialog"]'
|
||||
)
|
||||
const settingsLocator = comfyPage.page.getByTestId('settings-dialog')
|
||||
await expect(settingsLocator).toBeVisible()
|
||||
await comfyPage.page.keyboard.press('Escape')
|
||||
await expect(settingsLocator).not.toBeVisible()
|
||||
await expect(settingsLocator).toBeHidden()
|
||||
})
|
||||
|
||||
test('Can change canvas zoom speed setting', async ({ comfyPage }) => {
|
||||
const maxSpeed = 2.5
|
||||
await comfyPage.settings.setSetting('Comfy.Graph.ZoomSpeed', maxSpeed)
|
||||
|
||||
await test.step('Setting should persist', async () => {
|
||||
await expect
|
||||
.poll(() => comfyPage.settings.getSetting('Comfy.Graph.ZoomSpeed'))
|
||||
@@ -49,9 +46,7 @@ test.describe('Settings', () => {
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
|
||||
// Open the keybinding tab
|
||||
const settingsDialog = comfyPage.page.locator(
|
||||
'[data-testid="settings-dialog"]'
|
||||
)
|
||||
const settingsDialog = comfyPage.page.getByTestId('settings-dialog')
|
||||
await expect(settingsDialog).toBeVisible()
|
||||
await settingsDialog
|
||||
.locator('nav [role="button"]', { hasText: 'Keybinding' })
|
||||
|
||||
@@ -307,7 +307,7 @@ test.describe('ManagerDialog', { tag: '@ui' }, () => {
|
||||
await searchInput.fill('Test Pack B')
|
||||
|
||||
await expect(dialog.getByText('Test Pack B')).toBeVisible()
|
||||
await expect(dialog.getByText('Test Pack A')).not.toBeVisible()
|
||||
await expect(dialog.getByText('Test Pack A')).toBeHidden()
|
||||
})
|
||||
|
||||
test('Clicking a pack card opens the info panel', async ({ comfyPage }) => {
|
||||
@@ -360,7 +360,7 @@ test.describe('ManagerDialog', { tag: '@ui' }, () => {
|
||||
await expect(dialog).toBeVisible()
|
||||
|
||||
await comfyPage.page.keyboard.press('Escape')
|
||||
await expect(dialog).not.toBeVisible()
|
||||
await expect(dialog).toBeHidden()
|
||||
})
|
||||
|
||||
test('Empty search shows no results message', async ({ comfyPage }) => {
|
||||
|
||||
@@ -60,7 +60,7 @@ test.describe('Queue Clear History Dialog', { tag: '@ui' }, () => {
|
||||
})
|
||||
|
||||
await dialog.getByRole('button', { name: 'Cancel' }).click()
|
||||
await expect(dialog).not.toBeVisible()
|
||||
await expect(dialog).toBeHidden()
|
||||
expect(clearCalled).toBe(false)
|
||||
|
||||
await comfyPage.page.unroute('**/api/history')
|
||||
@@ -83,7 +83,7 @@ test.describe('Queue Clear History Dialog', { tag: '@ui' }, () => {
|
||||
})
|
||||
|
||||
await dialog.getByLabel('Close').click()
|
||||
await expect(dialog).not.toBeVisible()
|
||||
await expect(dialog).toBeHidden()
|
||||
expect(clearCalled).toBe(false)
|
||||
|
||||
await comfyPage.page.unroute('**/api/history')
|
||||
@@ -106,7 +106,7 @@ test.describe('Queue Clear History Dialog', { tag: '@ui' }, () => {
|
||||
const request = await clearPromise
|
||||
expect(request.postDataJSON()).toEqual({ clear: true })
|
||||
|
||||
await expect(dialog).not.toBeVisible()
|
||||
await expect(dialog).toBeHidden()
|
||||
})
|
||||
|
||||
test('Dialog state resets after close and reopen', async ({ comfyPage }) => {
|
||||
@@ -114,7 +114,7 @@ test.describe('Queue Clear History Dialog', { tag: '@ui' }, () => {
|
||||
const dialog = comfyPage.confirmDialog.root
|
||||
await expect(dialog).toBeVisible()
|
||||
await dialog.getByRole('button', { name: 'Cancel' }).click()
|
||||
await expect(dialog).not.toBeVisible()
|
||||
await expect(dialog).toBeHidden()
|
||||
|
||||
await comfyPage.queuePanel.openClearHistoryDialog()
|
||||
await expect(dialog).toBeVisible()
|
||||
|
||||
@@ -61,7 +61,7 @@ test.describe('Settings dialog', { tag: '@ui' }, () => {
|
||||
await expect(dialog.root).toBeVisible()
|
||||
|
||||
await dialog.close()
|
||||
await expect(dialog.root).not.toBeVisible()
|
||||
await expect(dialog.root).toBeHidden()
|
||||
})
|
||||
|
||||
test('Escape key closes dialog', async ({ comfyPage }) => {
|
||||
@@ -70,7 +70,7 @@ test.describe('Settings dialog', { tag: '@ui' }, () => {
|
||||
await expect(dialog.root).toBeVisible()
|
||||
|
||||
await comfyPage.page.keyboard.press('Escape')
|
||||
await expect(dialog.root).not.toBeVisible()
|
||||
await expect(dialog.root).toBeHidden()
|
||||
})
|
||||
|
||||
test('Search filters settings list', async ({ comfyPage }) => {
|
||||
|
||||
@@ -10,7 +10,7 @@ test.describe('DOM Widget', { tag: '@widget' }, () => {
|
||||
test('Collapsed multiline textarea is not visible', async ({ comfyPage }) => {
|
||||
await comfyPage.workflow.loadWorkflow('widgets/collapsed_multiline')
|
||||
const textareaWidget = comfyPage.page.locator('.comfy-multiline-input')
|
||||
await expect(textareaWidget).not.toBeVisible()
|
||||
await expect(textareaWidget).toBeHidden()
|
||||
})
|
||||
|
||||
test('Multiline textarea correctly collapses', async ({ comfyPage }) => {
|
||||
@@ -25,8 +25,8 @@ test.describe('DOM Widget', { tag: '@widget' }, () => {
|
||||
for (const node of nodes) {
|
||||
await node.click('collapse')
|
||||
}
|
||||
await expect(firstMultiline).not.toBeVisible()
|
||||
await expect(lastMultiline).not.toBeVisible()
|
||||
await expect(firstMultiline).toBeHidden()
|
||||
await expect(lastMultiline).toBeHidden()
|
||||
})
|
||||
|
||||
test(
|
||||
@@ -35,7 +35,7 @@ test.describe('DOM Widget', { tag: '@widget' }, () => {
|
||||
async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
await comfyPage.command.executeCommand('Workspace.ToggleFocusMode')
|
||||
await expect(comfyPage.menu.sideToolbar).not.toBeVisible()
|
||||
await expect(comfyPage.menu.sideToolbar).toBeHidden()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('focus-mode-on.png')
|
||||
}
|
||||
)
|
||||
|
||||
@@ -74,7 +74,7 @@ test.describe('Error dialog', () => {
|
||||
}) => {
|
||||
const errorDialog = await triggerConfigureError(comfyPage)
|
||||
await expect(errorDialog).toBeVisible()
|
||||
await expect(errorDialog.locator('pre')).not.toBeVisible()
|
||||
await expect(errorDialog.locator('pre')).toBeHidden()
|
||||
|
||||
await errorDialog.getByTestId(TestIds.dialogs.errorDialogShowReport).click()
|
||||
|
||||
@@ -83,7 +83,7 @@ test.describe('Error dialog', () => {
|
||||
await expect(reportPre).toHaveText(/\S/)
|
||||
await expect(
|
||||
errorDialog.getByTestId(TestIds.dialogs.errorDialogShowReport)
|
||||
).not.toBeVisible()
|
||||
).toBeHidden()
|
||||
})
|
||||
|
||||
test('Should copy report to clipboard when "Copy to Clipboard" is clicked', async ({
|
||||
|
||||
@@ -100,7 +100,7 @@ test.describe('Error overlay', { tag: '@ui' }, () => {
|
||||
await errorOverlay
|
||||
.getByTestId(TestIds.dialogs.errorOverlayDismiss)
|
||||
.click()
|
||||
await expect(errorOverlay).not.toBeVisible()
|
||||
await expect(errorOverlay).toBeHidden()
|
||||
|
||||
await comfyPage.canvas.click()
|
||||
await comfyPage.nextFrame()
|
||||
@@ -112,10 +112,10 @@ test.describe('Error overlay', { tag: '@ui' }, () => {
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await comfyPage.keyboard.undo()
|
||||
await expect(errorOverlay).not.toBeVisible()
|
||||
await expect(errorOverlay).toBeHidden()
|
||||
|
||||
await comfyPage.keyboard.redo()
|
||||
await expect(errorOverlay).not.toBeVisible()
|
||||
await expect(errorOverlay).toBeHidden()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -156,7 +156,7 @@ test.describe('Error overlay', { tag: '@ui' }, () => {
|
||||
|
||||
await overlay.getByTestId(TestIds.dialogs.errorOverlaySeeErrors).click()
|
||||
|
||||
await expect(overlay).not.toBeVisible()
|
||||
await expect(overlay).toBeHidden()
|
||||
await expect(comfyPage.page.getByTestId('properties-panel')).toBeVisible()
|
||||
})
|
||||
|
||||
@@ -168,7 +168,7 @@ test.describe('Error overlay', { tag: '@ui' }, () => {
|
||||
|
||||
await overlay.getByTestId(TestIds.dialogs.errorOverlaySeeErrors).click()
|
||||
|
||||
await expect(overlay).not.toBeVisible()
|
||||
await expect(overlay).toBeHidden()
|
||||
})
|
||||
|
||||
test('"Dismiss" closes overlay without opening panel', async ({
|
||||
@@ -181,10 +181,8 @@ test.describe('Error overlay', { tag: '@ui' }, () => {
|
||||
|
||||
await overlay.getByTestId(TestIds.dialogs.errorOverlayDismiss).click()
|
||||
|
||||
await expect(overlay).not.toBeVisible()
|
||||
await expect(
|
||||
comfyPage.page.getByTestId('properties-panel')
|
||||
).not.toBeVisible()
|
||||
await expect(overlay).toBeHidden()
|
||||
await expect(comfyPage.page.getByTestId('properties-panel')).toBeHidden()
|
||||
})
|
||||
|
||||
test('Close button (X) dismisses overlay', async ({ comfyPage }) => {
|
||||
@@ -195,7 +193,7 @@ test.describe('Error overlay', { tag: '@ui' }, () => {
|
||||
|
||||
await overlay.getByRole('button', { name: /close/i }).click()
|
||||
|
||||
await expect(overlay).not.toBeVisible()
|
||||
await expect(overlay).toBeHidden()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -69,7 +69,7 @@ test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => {
|
||||
expect(flags?.data).not.toBeNull()
|
||||
expect(flags?.data).toHaveProperty('supports_preview_metadata')
|
||||
expect(typeof flags?.data?.supports_preview_metadata).toBe('boolean')
|
||||
}).toPass()
|
||||
}).toPass({ timeout: 5000 })
|
||||
|
||||
// Verify server sent feature flags back
|
||||
await expect(async () => {
|
||||
@@ -82,7 +82,7 @@ test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => {
|
||||
expect(flags).toHaveProperty('max_upload_size')
|
||||
expect(typeof flags?.max_upload_size).toBe('number')
|
||||
expect(Object.keys(flags ?? {}).length).toBeGreaterThan(0)
|
||||
}).toPass()
|
||||
}).toPass({ timeout: 5000 })
|
||||
|
||||
await newPage.close()
|
||||
})
|
||||
@@ -102,7 +102,7 @@ test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => {
|
||||
expect(typeof flags.supports_preview_metadata).toBe('boolean')
|
||||
expect(flags).toHaveProperty('max_upload_size')
|
||||
expect(typeof flags.max_upload_size).toBe('number')
|
||||
}).toPass()
|
||||
}).toPass({ timeout: 5000 })
|
||||
})
|
||||
|
||||
test('serverSupportsFeature method works with real backend flags', async ({
|
||||
@@ -182,7 +182,7 @@ test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => {
|
||||
)
|
||||
expect(typeof maxUpload).toBe('number')
|
||||
expect(maxUpload as number).toBeGreaterThan(0)
|
||||
}).toPass()
|
||||
}).toPass({ timeout: 5000 })
|
||||
|
||||
// Test getServerFeature with default value for non-existent feature
|
||||
await expect
|
||||
@@ -210,7 +210,7 @@ test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => {
|
||||
expect(typeof features.supports_preview_metadata).toBe('boolean')
|
||||
expect(features).toHaveProperty('max_upload_size')
|
||||
expect(Object.keys(features).length).toBeGreaterThan(0)
|
||||
}).toPass()
|
||||
}).toPass({ timeout: 5000 })
|
||||
})
|
||||
|
||||
test('Client feature flags are immutable', async ({ comfyPage }) => {
|
||||
@@ -348,14 +348,14 @@ test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => {
|
||||
expect(flags).toHaveProperty('supports_preview_metadata')
|
||||
expect(typeof flags?.supports_preview_metadata).toBe('boolean')
|
||||
expect(flags).toHaveProperty('max_upload_size')
|
||||
}).toPass()
|
||||
}).toPass({ timeout: 5000 })
|
||||
|
||||
// Verify feature flags were received and API was initialized
|
||||
await expect(async () => {
|
||||
const readiness = await newPage.evaluate(() => window.__appReadiness)
|
||||
expect(readiness?.featureFlagsReceived).toBe(true)
|
||||
expect(readiness?.apiInitialized).toBe(true)
|
||||
}).toPass()
|
||||
}).toPass({ timeout: 5000 })
|
||||
|
||||
await newPage.close()
|
||||
})
|
||||
|
||||
@@ -14,12 +14,12 @@ test.describe('Focus Mode', { tag: '@ui' }, () => {
|
||||
|
||||
await comfyPage.setFocusMode(true)
|
||||
|
||||
await expect(comfyPage.menu.sideToolbar).not.toBeVisible()
|
||||
await expect(comfyPage.menu.sideToolbar).toBeHidden()
|
||||
})
|
||||
|
||||
test('Focus mode restores UI chrome', async ({ comfyPage }) => {
|
||||
await comfyPage.setFocusMode(true)
|
||||
await expect(comfyPage.menu.sideToolbar).not.toBeVisible()
|
||||
await expect(comfyPage.menu.sideToolbar).toBeHidden()
|
||||
|
||||
await comfyPage.setFocusMode(false)
|
||||
await expect(comfyPage.menu.sideToolbar).toBeVisible()
|
||||
@@ -29,7 +29,7 @@ test.describe('Focus Mode', { tag: '@ui' }, () => {
|
||||
await expect(comfyPage.menu.sideToolbar).toBeVisible()
|
||||
|
||||
await comfyPage.command.executeCommand('Workspace.ToggleFocusMode')
|
||||
await expect(comfyPage.menu.sideToolbar).not.toBeVisible()
|
||||
await expect(comfyPage.menu.sideToolbar).toBeHidden()
|
||||
|
||||
await comfyPage.command.executeCommand('Workspace.ToggleFocusMode')
|
||||
await expect(comfyPage.menu.sideToolbar).toBeVisible()
|
||||
@@ -41,7 +41,7 @@ test.describe('Focus Mode', { tag: '@ui' }, () => {
|
||||
|
||||
await comfyPage.setFocusMode(true)
|
||||
|
||||
await expect(topMenu).not.toBeVisible()
|
||||
await expect(topMenu).toBeHidden()
|
||||
})
|
||||
|
||||
test('Canvas remains visible in focus mode', async ({ comfyPage }) => {
|
||||
@@ -52,12 +52,12 @@ test.describe('Focus Mode', { tag: '@ui' }, () => {
|
||||
|
||||
test('Focus mode can be toggled multiple times', async ({ comfyPage }) => {
|
||||
await comfyPage.setFocusMode(true)
|
||||
await expect(comfyPage.menu.sideToolbar).not.toBeVisible()
|
||||
await expect(comfyPage.menu.sideToolbar).toBeHidden()
|
||||
|
||||
await comfyPage.setFocusMode(false)
|
||||
await expect(comfyPage.menu.sideToolbar).toBeVisible()
|
||||
|
||||
await comfyPage.setFocusMode(true)
|
||||
await expect(comfyPage.menu.sideToolbar).not.toBeVisible()
|
||||
await expect(comfyPage.menu.sideToolbar).toBeHidden()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -109,6 +109,6 @@ test.describe('Graph', { tag: ['@smoke', '@canvas'] }, () => {
|
||||
expect(r.switchOutputLinkIds).toEqual(
|
||||
expect.arrayContaining([r.cfg85LinkId, r.cfg86LinkId])
|
||||
)
|
||||
}).toPass()
|
||||
}).toPass({ timeout: 5000 })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -94,6 +94,6 @@ test.describe('Graph Canvas Menu', { tag: ['@screenshot', '@canvas'] }, () => {
|
||||
await backdrop.click()
|
||||
|
||||
// Modal should be hidden
|
||||
await expect(zoomModal).not.toBeVisible()
|
||||
await expect(zoomModal).toBeHidden()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -88,7 +88,10 @@ test.describe('Group Node', { tag: '@node' }, () => {
|
||||
.getNode(groupNodeName)
|
||||
.locator('.bookmark-button')
|
||||
.click()
|
||||
await comfyPage.page.hover('.p-tree-node-label.tree-explorer-node-label')
|
||||
await comfyPage.page
|
||||
.locator('.p-tree-node-label.tree-explorer-node-label')
|
||||
.first()
|
||||
.hover()
|
||||
await expect(
|
||||
comfyPage.page.locator('.node-lib-node-preview')
|
||||
).toBeVisible()
|
||||
@@ -99,6 +102,7 @@ test.describe('Group Node', { tag: '@node' }, () => {
|
||||
.click()
|
||||
})
|
||||
})
|
||||
|
||||
test(
|
||||
'Can be added to canvas using search',
|
||||
{ tag: '@screenshot' },
|
||||
@@ -154,7 +158,7 @@ test.describe('Group Node', { tag: '@node' }, () => {
|
||||
await comfyPage.nextFrame()
|
||||
await expect(manage1.selectedNodeTypeSelect).toHaveValue('g1')
|
||||
await manage1.close()
|
||||
await expect(manage1.root).not.toBeVisible()
|
||||
await expect(manage1.root).toBeHidden()
|
||||
|
||||
const manage2 = await group2.manageGroupNode()
|
||||
await expect(manage2.selectedNodeTypeSelect).toHaveValue('g2')
|
||||
@@ -241,7 +245,7 @@ test.describe('Group Node', { tag: '@node' }, () => {
|
||||
await expect.poll(() => comfyPage.nodeOps.getGraphNodesCount()).toBe(1)
|
||||
await expect(
|
||||
comfyPage.page.getByTestId(TestIds.dialogs.errorOverlay)
|
||||
).not.toBeVisible()
|
||||
).toBeHidden()
|
||||
})
|
||||
|
||||
test.describe('Copy and paste', () => {
|
||||
@@ -349,6 +353,7 @@ test.describe('Group Node', { tag: '@node' }, () => {
|
||||
await comfyPage.page.keyboard.press('Alt+g')
|
||||
await expect(comfyPage.toast.visibleToasts).toHaveCount(1)
|
||||
})
|
||||
|
||||
test('Convert to group node, selected 1 node', async ({ comfyPage }) => {
|
||||
await expect(comfyPage.toast.visibleToasts).toHaveCount(0)
|
||||
await comfyPage.canvas.click({
|
||||
|
||||
@@ -46,7 +46,7 @@ test.describe('Image Compare', () => {
|
||||
|
||||
await expect(node).toContainText('No images to compare')
|
||||
await expect(node.locator('img')).toHaveCount(0)
|
||||
await expect(node.locator('[role="presentation"]')).toHaveCount(0)
|
||||
await expect(node.getByRole('presentation')).toHaveCount(0)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -67,7 +67,7 @@ test.describe('Image Compare', () => {
|
||||
await expect(beforeImg).toBeVisible()
|
||||
await expect(afterImg).toBeVisible()
|
||||
|
||||
const handle = node.locator('[role="presentation"]')
|
||||
const handle = node.getByRole('presentation')
|
||||
await expect(handle).toBeVisible()
|
||||
|
||||
expect(
|
||||
|
||||
@@ -180,6 +180,48 @@ test.describe('Node Interaction', () => {
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Node Duplication', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
// Pin this suite to the legacy canvas path so Alt+drag exercises
|
||||
// LGraphCanvas, not the Vue node drag handler.
|
||||
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', false)
|
||||
await comfyPage.nextFrame()
|
||||
})
|
||||
|
||||
test('Can duplicate a regular node via Alt+drag', async ({ comfyPage }) => {
|
||||
const before = await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode')
|
||||
expect(
|
||||
before,
|
||||
'Expected exactly 2 CLIPTextEncode nodes in default graph'
|
||||
).toHaveLength(2)
|
||||
|
||||
const target = before[0]
|
||||
const pos = await target.getPosition()
|
||||
const src = { x: pos.x + 16, y: pos.y + 16 }
|
||||
|
||||
await comfyPage.page.mouse.move(src.x, src.y)
|
||||
await comfyPage.page.keyboard.down('Alt')
|
||||
try {
|
||||
await comfyPage.page.mouse.down()
|
||||
await comfyPage.nextFrame()
|
||||
await comfyPage.page.mouse.move(src.x + 120, src.y + 80, { steps: 20 })
|
||||
await comfyPage.page.mouse.up()
|
||||
await comfyPage.nextFrame()
|
||||
} finally {
|
||||
await comfyPage.page.keyboard.up('Alt')
|
||||
}
|
||||
await comfyPage.canvasOps.moveMouseToEmptyArea()
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
async () =>
|
||||
(await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode')).length
|
||||
)
|
||||
.toBe(3)
|
||||
expect(await target.exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Edge Interaction', { tag: '@screenshot' }, () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting(
|
||||
@@ -1252,7 +1294,7 @@ test.describe('Canvas Navigation', { tag: '@screenshot' }, () => {
|
||||
|
||||
test('Space + left-click drag should pan canvas', async ({ comfyPage }) => {
|
||||
// Click canvas to focus it
|
||||
await comfyPage.page.click('canvas')
|
||||
await comfyPage.canvas.click()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await comfyPage.page.keyboard.down('Space')
|
||||
@@ -1321,7 +1363,7 @@ test.describe('Canvas Navigation', { tag: '@screenshot' }, () => {
|
||||
'panning'
|
||||
)
|
||||
|
||||
await comfyPage.page.click('canvas')
|
||||
await comfyPage.canvas.click()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('standard-initial.png')
|
||||
|
||||
@@ -25,7 +25,7 @@ test.describe('Job History Actions', { tag: '@ui' }, () => {
|
||||
await openMoreOptionsPopover(comfyPage)
|
||||
|
||||
await expect(
|
||||
comfyPage.page.locator('[data-testid="docked-job-history-action"]')
|
||||
comfyPage.page.getByTestId('docked-job-history-action')
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
@@ -34,9 +34,7 @@ test.describe('Job History Actions', { tag: '@ui' }, () => {
|
||||
}) => {
|
||||
await openMoreOptionsPopover(comfyPage)
|
||||
|
||||
const action = comfyPage.page.locator(
|
||||
'[data-testid="docked-job-history-action"]'
|
||||
)
|
||||
const action = comfyPage.page.getByTestId('docked-job-history-action')
|
||||
await expect(action).toBeVisible()
|
||||
await expect(action).not.toBeEmpty()
|
||||
})
|
||||
@@ -45,7 +43,7 @@ test.describe('Job History Actions', { tag: '@ui' }, () => {
|
||||
await openMoreOptionsPopover(comfyPage)
|
||||
|
||||
await expect(
|
||||
comfyPage.page.locator('[data-testid="show-run-progress-bar-action"]')
|
||||
comfyPage.page.getByTestId('show-run-progress-bar-action')
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
@@ -53,20 +51,18 @@ test.describe('Job History Actions', { tag: '@ui' }, () => {
|
||||
await openMoreOptionsPopover(comfyPage)
|
||||
|
||||
await expect(
|
||||
comfyPage.page.locator('[data-testid="clear-history-action"]')
|
||||
comfyPage.page.getByTestId('clear-history-action')
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('Clicking docked job history closes popover', async ({ comfyPage }) => {
|
||||
await openMoreOptionsPopover(comfyPage)
|
||||
|
||||
const action = comfyPage.page.locator(
|
||||
'[data-testid="docked-job-history-action"]'
|
||||
)
|
||||
const action = comfyPage.page.getByTestId('docked-job-history-action')
|
||||
await expect(action).toBeVisible()
|
||||
await action.click()
|
||||
|
||||
await expect(action).not.toBeVisible()
|
||||
await expect(action).toBeHidden()
|
||||
})
|
||||
|
||||
test('Clicking show run progress bar toggles setting', async ({
|
||||
@@ -78,9 +74,7 @@ test.describe('Job History Actions', { tag: '@ui' }, () => {
|
||||
|
||||
await openMoreOptionsPopover(comfyPage)
|
||||
|
||||
const action = comfyPage.page.locator(
|
||||
'[data-testid="show-run-progress-bar-action"]'
|
||||
)
|
||||
const action = comfyPage.page.getByTestId('show-run-progress-bar-action')
|
||||
await action.click()
|
||||
|
||||
await expect
|
||||
|
||||
@@ -14,48 +14,38 @@ test.describe('Linear Mode', { tag: '@ui' }, () => {
|
||||
}) => {
|
||||
await comfyPage.appMode.enterAppModeWithInputs([])
|
||||
|
||||
await expect(
|
||||
comfyPage.page.locator('[data-testid="linear-widgets"]')
|
||||
).toBeVisible()
|
||||
await expect(comfyPage.page.getByTestId('linear-widgets')).toBeVisible()
|
||||
})
|
||||
|
||||
test('Run button visible in linear mode', async ({ comfyPage }) => {
|
||||
await comfyPage.appMode.enterAppModeWithInputs([])
|
||||
|
||||
await expect(
|
||||
comfyPage.page.locator('[data-testid="linear-run-button"]')
|
||||
).toBeVisible()
|
||||
await expect(comfyPage.page.getByTestId('linear-run-button')).toBeVisible()
|
||||
})
|
||||
|
||||
test('Workflow info section visible', async ({ comfyPage }) => {
|
||||
await comfyPage.appMode.enterAppModeWithInputs([])
|
||||
|
||||
await expect(
|
||||
comfyPage.page.locator('[data-testid="linear-workflow-info"]')
|
||||
comfyPage.page.getByTestId('linear-workflow-info')
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('Returns to graph mode', async ({ comfyPage }) => {
|
||||
await comfyPage.appMode.enterAppModeWithInputs([])
|
||||
|
||||
await expect(
|
||||
comfyPage.page.locator('[data-testid="linear-widgets"]')
|
||||
).toBeVisible()
|
||||
await expect(comfyPage.page.getByTestId('linear-widgets')).toBeVisible()
|
||||
|
||||
await comfyPage.appMode.toggleAppMode()
|
||||
|
||||
await expect(comfyPage.canvas).toBeVisible()
|
||||
await expect(
|
||||
comfyPage.page.locator('[data-testid="linear-widgets"]')
|
||||
).not.toBeVisible()
|
||||
await expect(comfyPage.page.getByTestId('linear-widgets')).toBeHidden()
|
||||
})
|
||||
|
||||
test('Canvas not visible in app mode', async ({ comfyPage }) => {
|
||||
await comfyPage.appMode.enterAppModeWithInputs([])
|
||||
|
||||
await expect(
|
||||
comfyPage.page.locator('[data-testid="linear-widgets"]')
|
||||
).toBeVisible()
|
||||
await expect(comfyPage.canvas).not.toBeVisible()
|
||||
await expect(comfyPage.page.getByTestId('linear-widgets')).toBeVisible()
|
||||
await expect(comfyPage.canvas).toBeHidden()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -101,7 +101,7 @@ test.describe('Menu', { tag: '@ui' }, () => {
|
||||
|
||||
// Check initial state of bottom panel (it's initially hidden)
|
||||
const { bottomPanel } = comfyPage
|
||||
await expect(bottomPanel.root).not.toBeVisible()
|
||||
await expect(bottomPanel.root).toBeHidden()
|
||||
|
||||
// Checkmark should be invisible initially (panel is hidden)
|
||||
await expect(checkmark).toHaveClass(/invisible/)
|
||||
@@ -126,7 +126,7 @@ test.describe('Menu', { tag: '@ui' }, () => {
|
||||
await expect(viewSubmenu).toBeVisible()
|
||||
|
||||
// Verify bottom panel is hidden again
|
||||
await expect(bottomPanel.root).not.toBeVisible()
|
||||
await expect(bottomPanel.root).toBeHidden()
|
||||
|
||||
// Checkmark should be invisible again (panel is hidden)
|
||||
await expect(checkmark).toHaveClass(/invisible/)
|
||||
@@ -138,7 +138,7 @@ test.describe('Menu', { tag: '@ui' }, () => {
|
||||
.click({ position: { x: viewport.width - 10, y: 10 } })
|
||||
|
||||
// Verify menu is now closed
|
||||
await expect(menu).not.toBeVisible()
|
||||
await expect(menu).toBeHidden()
|
||||
})
|
||||
|
||||
test('Displays keybinding next to item', async ({ comfyPage }) => {
|
||||
|
||||
@@ -53,7 +53,7 @@ test.describe('Minimap', { tag: '@canvas' }, () => {
|
||||
await expect(minimapContainer).toBeVisible()
|
||||
|
||||
await toggleButton.click()
|
||||
await expect(minimapContainer).not.toBeVisible()
|
||||
await expect(minimapContainer).toBeHidden()
|
||||
|
||||
await toggleButton.click()
|
||||
await expect(minimapContainer).toBeVisible()
|
||||
@@ -65,7 +65,7 @@ test.describe('Minimap', { tag: '@canvas' }, () => {
|
||||
await expect(minimapContainer).toBeVisible()
|
||||
|
||||
await comfyPage.page.keyboard.press('Alt+KeyM')
|
||||
await expect(minimapContainer).not.toBeVisible()
|
||||
await expect(minimapContainer).toBeHidden()
|
||||
|
||||
await comfyPage.page.keyboard.press('Alt+KeyM')
|
||||
await expect(minimapContainer).toBeVisible()
|
||||
@@ -76,7 +76,7 @@ test.describe('Minimap', { tag: '@canvas' }, () => {
|
||||
await expect(minimap).toBeVisible()
|
||||
|
||||
await comfyPage.page.getByTestId(TestIds.canvas.closeMinimapButton).click()
|
||||
await expect(minimap).not.toBeVisible()
|
||||
await expect(minimap).toBeHidden()
|
||||
|
||||
const toggleButton = comfyPage.page.getByTestId(
|
||||
TestIds.canvas.toggleMinimapButton
|
||||
|
||||
@@ -39,9 +39,7 @@ test.describe(
|
||||
|
||||
await expect(comfyPage.page.locator('.selection-toolbox')).toBeVisible()
|
||||
|
||||
const moreOptionsBtn = comfyPage.page.locator(
|
||||
'[data-testid="more-options-button"]'
|
||||
)
|
||||
const moreOptionsBtn = comfyPage.page.getByTestId('more-options-button')
|
||||
await expect(moreOptionsBtn).toBeVisible()
|
||||
await moreOptionsBtn.click()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
@@ -40,13 +40,14 @@ test.describe('Optional input', { tag: ['@screenshot', '@node'] }, () => {
|
||||
await expect.poll(() => comfyPage.nodeOps.getGraphNodesCount()).toBe(1)
|
||||
await expect(
|
||||
comfyPage.page.getByTestId(TestIds.dialogs.errorOverlay)
|
||||
).not.toBeVisible()
|
||||
).toBeHidden()
|
||||
|
||||
// If the node's multiline text widget is visible, then it was loaded successfully
|
||||
await expect(comfyPage.page.locator('.comfy-multiline-input')).toHaveCount(
|
||||
1
|
||||
)
|
||||
})
|
||||
|
||||
test('Old workflow with converted input', async ({ comfyPage }) => {
|
||||
await comfyPage.workflow.loadWorkflow('inputs/old_workflow_converted_input')
|
||||
const node = await comfyPage.nodeOps.getNodeRefById('1')
|
||||
@@ -62,6 +63,7 @@ test.describe('Optional input', { tag: ['@screenshot', '@node'] }, () => {
|
||||
expect(vaeInput!.link).toBeNull()
|
||||
expect(convertedInput!.link).not.toBeNull()
|
||||
})
|
||||
|
||||
test('Renamed converted input', async ({ comfyPage }) => {
|
||||
await comfyPage.workflow.loadWorkflow('inputs/renamed_converted_widget')
|
||||
const node = await comfyPage.nodeOps.getNodeRefById('3')
|
||||
@@ -69,10 +71,12 @@ test.describe('Optional input', { tag: ['@screenshot', '@node'] }, () => {
|
||||
const renamedInput = inputs.find((w) => w.name === 'breadth')
|
||||
expect(renamedInput).toBeUndefined()
|
||||
})
|
||||
|
||||
test('slider', async ({ comfyPage }) => {
|
||||
await comfyPage.workflow.loadWorkflow('inputs/simple_slider')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('simple_slider.png')
|
||||
})
|
||||
|
||||
test('unknown converted widget', async ({ comfyPage }) => {
|
||||
await comfyPage.workflow.loadWorkflow(
|
||||
'missing/missing_nodes_converted_widget'
|
||||
@@ -81,6 +85,7 @@ test.describe('Optional input', { tag: ['@screenshot', '@node'] }, () => {
|
||||
'missing_nodes_converted_widget.png'
|
||||
)
|
||||
})
|
||||
|
||||
test('dynamically added input', async ({ comfyPage }) => {
|
||||
await comfyPage.workflow.loadWorkflow('inputs/dynamically_added_input')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
|
||||
@@ -191,7 +191,7 @@ test.describe('Node Help', { tag: ['@slow', '@ui'] }, () => {
|
||||
).toBeVisible()
|
||||
|
||||
// Verify help page is no longer visible
|
||||
await expect(helpPage.locator('.node-help-content')).not.toBeVisible()
|
||||
await expect(helpPage.locator('.node-help-content')).toBeHidden()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -505,7 +505,7 @@ This is English documentation.
|
||||
|
||||
// Should show fallback content (node description)
|
||||
await expect(helpPage).toBeVisible()
|
||||
await expect(helpPage.locator('.p-progressspinner')).not.toBeVisible()
|
||||
await expect(helpPage.locator('.p-progressspinner')).toBeHidden()
|
||||
|
||||
// Should show some content even on error
|
||||
await expect(helpPage).not.toHaveText('')
|
||||
|
||||
@@ -203,7 +203,7 @@ test.describe('Node search box', { tag: '@node' }, () => {
|
||||
await comfyPage.page.keyboard.press('Escape')
|
||||
|
||||
// Verify the filter selection panel is hidden
|
||||
await expect(panel.header).not.toBeVisible()
|
||||
await expect(panel.header).toBeHidden()
|
||||
|
||||
// Verify the node search dialog is still visible
|
||||
await expect(comfyPage.searchBox.input).toBeVisible()
|
||||
|
||||
@@ -29,7 +29,7 @@ test.describe('Node search box V2', { tag: '@node' }, () => {
|
||||
await expect(searchBoxV2.results.first()).toBeVisible()
|
||||
|
||||
await comfyPage.page.keyboard.press('Enter')
|
||||
await expect(searchBoxV2.input).not.toBeVisible()
|
||||
await expect(searchBoxV2.input).toBeHidden()
|
||||
|
||||
await expect
|
||||
.poll(() => comfyPage.nodeOps.getGraphNodesCount())
|
||||
@@ -48,7 +48,7 @@ test.describe('Node search box V2', { tag: '@node' }, () => {
|
||||
|
||||
// Enter should add the first (selected) result
|
||||
await comfyPage.page.keyboard.press('Enter')
|
||||
await expect(searchBoxV2.input).not.toBeVisible()
|
||||
await expect(searchBoxV2.input).toBeHidden()
|
||||
|
||||
await expect
|
||||
.poll(() => comfyPage.nodeOps.getGraphNodesCount())
|
||||
@@ -141,7 +141,7 @@ test.describe('Node search box V2', { tag: '@node' }, () => {
|
||||
|
||||
// Enter selects and adds node
|
||||
await comfyPage.page.keyboard.press('Enter')
|
||||
await expect(searchBoxV2.input).not.toBeVisible()
|
||||
await expect(searchBoxV2.input).toBeHidden()
|
||||
|
||||
await expect
|
||||
.poll(() => comfyPage.nodeOps.getGraphNodesCount())
|
||||
|
||||
@@ -39,7 +39,7 @@ test.describe('Node search box V2 extended', { tag: '@node' }, () => {
|
||||
await expect(searchBoxV2.results.first()).toBeVisible()
|
||||
|
||||
await comfyPage.page.keyboard.press('Escape')
|
||||
await expect(searchBoxV2.input).not.toBeVisible()
|
||||
await expect(searchBoxV2.input).toBeHidden()
|
||||
|
||||
await expect
|
||||
.poll(() => comfyPage.nodeOps.getGraphNodesCount())
|
||||
@@ -56,7 +56,7 @@ test.describe('Node search box V2 extended', { tag: '@node' }, () => {
|
||||
await expect(searchBoxV2.results.first()).toBeVisible()
|
||||
|
||||
await comfyPage.page.keyboard.press('Escape')
|
||||
await expect(searchBoxV2.input).not.toBeVisible()
|
||||
await expect(searchBoxV2.input).toBeHidden()
|
||||
|
||||
await comfyPage.canvasOps.doubleClick()
|
||||
await expect(searchBoxV2.input).toBeVisible()
|
||||
@@ -104,9 +104,7 @@ test.describe('Node search box V2 extended', { tag: '@node' }, () => {
|
||||
.click()
|
||||
|
||||
// Verify filter chip appeared and results changed
|
||||
const filterChip = searchBoxV2.dialog.locator(
|
||||
'[data-testid="filter-chip"]'
|
||||
)
|
||||
const filterChip = searchBoxV2.dialog.getByTestId('filter-chip')
|
||||
await expect(filterChip).toBeVisible()
|
||||
await expect(searchBoxV2.results.first()).toBeVisible()
|
||||
await expect
|
||||
@@ -117,7 +115,7 @@ test.describe('Node search box V2 extended', { tag: '@node' }, () => {
|
||||
await filterChip.getByTestId('chip-delete').click()
|
||||
|
||||
// Filter chip should be removed
|
||||
await expect(filterChip).not.toBeVisible()
|
||||
await expect(filterChip).toBeHidden()
|
||||
await expect(searchBoxV2.results.first()).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -202,6 +202,7 @@ test.describe('Performance', { tag: ['@perf'] }, () => {
|
||||
'domNodes'
|
||||
])
|
||||
})
|
||||
|
||||
test('subgraph DOM widget clipping during node selection', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
|
||||
@@ -13,5 +13,5 @@ export async function openErrorsTabViaSeeErrors(
|
||||
await expect(errorOverlay).toBeVisible()
|
||||
|
||||
await errorOverlay.getByTestId(TestIds.dialogs.errorOverlaySeeErrors).click()
|
||||
await expect(errorOverlay).not.toBeVisible()
|
||||
await expect(errorOverlay).toBeHidden()
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ export class PropertiesPanelHelper {
|
||||
async close(): Promise<void> {
|
||||
if (await this.root.isVisible()) {
|
||||
await this.closeButton.click()
|
||||
await expect(this.root).not.toBeVisible()
|
||||
await expect(this.root).toBeHidden()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ test.describe('Errors tab - common', { tag: '@ui' }, () => {
|
||||
await comfyPage.actionbar.propertiesButton.click()
|
||||
|
||||
const panel = new PropertiesPanelHelper(comfyPage.page)
|
||||
await expect(panel.errorsTabIcon).not.toBeVisible()
|
||||
await expect(panel.errorsTabIcon).toBeHidden()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -55,7 +55,7 @@ test.describe('Errors tab - common', { tag: '@ui' }, () => {
|
||||
await errorOverlay
|
||||
.getByTestId(TestIds.dialogs.errorOverlaySeeErrors)
|
||||
.click()
|
||||
await expect(errorOverlay).not.toBeVisible()
|
||||
await expect(errorOverlay).toBeHidden()
|
||||
|
||||
const runtimePanel = comfyPage.page.getByTestId(
|
||||
TestIds.dialogs.runtimeErrorPanel
|
||||
|
||||
@@ -26,7 +26,7 @@ test.describe('Errors tab - Execution errors', { tag: '@ui' }, () => {
|
||||
await errorOverlay
|
||||
.getByTestId(TestIds.dialogs.errorOverlaySeeErrors)
|
||||
.click()
|
||||
await expect(errorOverlay).not.toBeVisible()
|
||||
await expect(errorOverlay).toBeHidden()
|
||||
}
|
||||
|
||||
test('Should show Find on GitHub and Copy buttons in error card', async ({
|
||||
|
||||
@@ -104,6 +104,7 @@ test.describe('Errors tab - Missing media', { tag: '@ui' }, () => {
|
||||
|
||||
const optionCount = await comfyPage.page.getByRole('option').count()
|
||||
if (optionCount === 0) {
|
||||
// oxlint-disable-next-line playwright/no-skipped-test -- no library options available in CI
|
||||
test.skip()
|
||||
return
|
||||
}
|
||||
@@ -125,13 +126,13 @@ test.describe('Errors tab - Missing media', { tag: '@ui' }, () => {
|
||||
await uploadFileViaDropzone(comfyPage)
|
||||
|
||||
await expect(getStatusCard(comfyPage)).toBeVisible()
|
||||
await expect(getDropzone(comfyPage)).not.toBeVisible()
|
||||
await expect(getDropzone(comfyPage)).toBeHidden()
|
||||
|
||||
await comfyPage.page
|
||||
.getByTestId(TestIds.dialogs.missingMediaCancelButton)
|
||||
.click()
|
||||
|
||||
await expect(getStatusCard(comfyPage)).not.toBeVisible()
|
||||
await expect(getStatusCard(comfyPage)).toBeHidden()
|
||||
await expect(getDropzone(comfyPage)).toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -146,7 +147,7 @@ test.describe('Errors tab - Missing media', { tag: '@ui' }, () => {
|
||||
|
||||
await expect(
|
||||
comfyPage.page.getByTestId(TestIds.dialogs.missingMediaGroup)
|
||||
).not.toBeVisible()
|
||||
).toBeHidden()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ test.describe('Errors tab - Missing models', { tag: '@ui' }, () => {
|
||||
const locateButton = comfyPage.page.getByTestId(
|
||||
TestIds.dialogs.missingModelLocate
|
||||
)
|
||||
await expect(locateButton.first()).not.toBeVisible()
|
||||
await expect(locateButton.first()).toBeHidden()
|
||||
|
||||
const expandButton = comfyPage.page.getByTestId(
|
||||
TestIds.dialogs.missingModelExpand
|
||||
|
||||
@@ -74,7 +74,7 @@ test.describe('Errors tab - Missing nodes', { tag: '@ui' }, () => {
|
||||
.click()
|
||||
await expect(
|
||||
missingNodeCard.getByText('MISSING_NODE_TYPE_IN_SUBGRAPH')
|
||||
).not.toBeVisible()
|
||||
).toBeHidden()
|
||||
})
|
||||
|
||||
test('Locate node button is visible for expanded pack nodes', async ({
|
||||
|
||||
@@ -27,7 +27,7 @@ test.describe('Properties panel - Node selection', () => {
|
||||
})
|
||||
|
||||
test('should not show Nodes tab for single node', async () => {
|
||||
await expect(panel.getTab('Nodes')).not.toBeVisible()
|
||||
await expect(panel.getTab('Nodes')).toBeHidden()
|
||||
})
|
||||
|
||||
test('should display node widgets in Parameters tab', async () => {
|
||||
@@ -65,7 +65,7 @@ test.describe('Properties panel - Node selection', () => {
|
||||
'KSampler',
|
||||
'CLIP Text Encode (Prompt)'
|
||||
])
|
||||
await expect(panel.getTab('Info')).not.toBeVisible()
|
||||
await expect(panel.getTab('Info')).toBeHidden()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -42,8 +42,8 @@ test.describe('Properties panel - Node settings', () => {
|
||||
await expect(nodeLocator.getByText('Bypassed')).toBeVisible()
|
||||
|
||||
await panel.getNodeStateButton('Normal').click()
|
||||
await expect(nodeLocator.getByText('Bypassed')).not.toBeVisible()
|
||||
await expect(nodeLocator.getByText('Muted')).not.toBeVisible()
|
||||
await expect(nodeLocator.getByText('Bypassed')).toBeHidden()
|
||||
await expect(nodeLocator.getByText('Muted')).toBeHidden()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -114,9 +114,7 @@ test.describe('Properties panel - Node settings', () => {
|
||||
await expect(nodeLocator.getByTestId('node-pin-indicator')).toBeVisible()
|
||||
|
||||
await panel.pinnedSwitch.click()
|
||||
await expect(
|
||||
nodeLocator.getByTestId('node-pin-indicator')
|
||||
).not.toBeVisible()
|
||||
await expect(nodeLocator.getByTestId('node-pin-indicator')).toBeHidden()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -11,7 +11,7 @@ test.describe('Properties panel - Open and close', () => {
|
||||
})
|
||||
|
||||
test('should open via actionbar toggle button', async ({ comfyPage }) => {
|
||||
await expect(panel.root).not.toBeVisible()
|
||||
await expect(panel.root).toBeHidden()
|
||||
await comfyPage.actionbar.propertiesButton.click()
|
||||
await expect(panel.root).toBeVisible()
|
||||
})
|
||||
@@ -20,13 +20,13 @@ test.describe('Properties panel - Open and close', () => {
|
||||
await comfyPage.actionbar.propertiesButton.click()
|
||||
await expect(panel.root).toBeVisible()
|
||||
await panel.closeButton.click()
|
||||
await expect(panel.root).not.toBeVisible()
|
||||
await expect(panel.root).toBeHidden()
|
||||
})
|
||||
|
||||
test('should close via close button after opening', async ({ comfyPage }) => {
|
||||
await comfyPage.actionbar.propertiesButton.click()
|
||||
await expect(panel.root).toBeVisible()
|
||||
await panel.close()
|
||||
await expect(panel.root).not.toBeVisible()
|
||||
await expect(panel.root).toBeHidden()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -34,7 +34,7 @@ test.describe('Properties panel - Title editing', () => {
|
||||
'KSampler',
|
||||
'CLIP Text Encode (Prompt)'
|
||||
])
|
||||
await expect(panel.titleEditIcon).not.toBeVisible()
|
||||
await expect(panel.titleEditIcon).toBeHidden()
|
||||
})
|
||||
|
||||
test('should not show pencil icon when nothing is selected', async ({
|
||||
@@ -44,6 +44,6 @@ test.describe('Properties panel - Title editing', () => {
|
||||
window.app!.canvas.deselectAll()
|
||||
})
|
||||
await expect(panel.panelTitle).toContainText('Workflow Overview')
|
||||
await expect(panel.titleEditIcon).not.toBeVisible()
|
||||
await expect(panel.titleEditIcon).toBeHidden()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -23,7 +23,7 @@ test.describe('Properties panel - Workflow Overview', () => {
|
||||
})
|
||||
|
||||
test('should not show Info tab when nothing is selected', async () => {
|
||||
await expect(panel.getTab('Info')).not.toBeVisible()
|
||||
await expect(panel.getTab('Info')).toBeHidden()
|
||||
})
|
||||
|
||||
test('should switch to Nodes tab and list all workflow nodes', async ({
|
||||
|
||||
@@ -93,7 +93,7 @@ test.describe('Queue overlay', () => {
|
||||
).toBeVisible()
|
||||
await expect(
|
||||
comfyPage.page.locator('[data-job-id="job-failed-1"]')
|
||||
).not.toBeVisible()
|
||||
).toBeHidden()
|
||||
})
|
||||
|
||||
test('Toggling overlay again closes it', async ({ comfyPage }) => {
|
||||
@@ -104,8 +104,6 @@ test.describe('Queue overlay', () => {
|
||||
|
||||
await toggle.click()
|
||||
|
||||
await expect(
|
||||
comfyPage.page.locator('[data-job-id]').first()
|
||||
).not.toBeVisible()
|
||||
await expect(comfyPage.page.locator('[data-job-id]').first()).toBeHidden()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -72,8 +72,8 @@ test.describe('Release Notifications', () => {
|
||||
).toBeVisible()
|
||||
|
||||
// Close help center by dismissable mask
|
||||
await comfyPage.page.click('.help-center-backdrop')
|
||||
await expect(helpMenu).not.toBeVisible()
|
||||
await comfyPage.page.locator('.help-center-backdrop').click()
|
||||
await expect(helpMenu).toBeHidden()
|
||||
})
|
||||
|
||||
test('should not show release notifications when mocked (default behavior)', async ({
|
||||
@@ -103,10 +103,10 @@ test.describe('Release Notifications', () => {
|
||||
).toBeVisible()
|
||||
|
||||
// Should not show any popups or toasts
|
||||
await expect(comfyPage.page.locator('.whats-new-popup')).not.toBeVisible()
|
||||
await expect(comfyPage.page.locator('.whats-new-popup')).toBeHidden()
|
||||
await expect(
|
||||
comfyPage.page.locator('.release-notification-toast')
|
||||
).not.toBeVisible()
|
||||
).toBeHidden()
|
||||
})
|
||||
|
||||
test('should handle release API errors gracefully', async ({ comfyPage }) => {
|
||||
@@ -189,13 +189,13 @@ test.describe('Release Notifications', () => {
|
||||
const whatsNewSection = comfyPage.page.getByTestId(
|
||||
TestIds.dialogs.whatsNewSection
|
||||
)
|
||||
await expect(whatsNewSection).not.toBeVisible()
|
||||
await expect(whatsNewSection).toBeHidden()
|
||||
|
||||
// Should not show any popups or toasts
|
||||
await expect(comfyPage.page.locator('.whats-new-popup')).not.toBeVisible()
|
||||
await expect(comfyPage.page.locator('.whats-new-popup')).toBeHidden()
|
||||
await expect(
|
||||
comfyPage.page.locator('.release-notification-toast')
|
||||
).not.toBeVisible()
|
||||
).toBeHidden()
|
||||
})
|
||||
|
||||
test('should not make API calls when notifications are disabled', async ({
|
||||
@@ -325,7 +325,7 @@ test.describe('Release Notifications', () => {
|
||||
await expect(whatsNewSection).toBeVisible()
|
||||
|
||||
// Close help center
|
||||
await comfyPage.page.click('.help-center-backdrop')
|
||||
await comfyPage.page.locator('.help-center-backdrop').click()
|
||||
|
||||
// Disable notifications
|
||||
await comfyPage.settings.setSetting(
|
||||
@@ -337,7 +337,7 @@ test.describe('Release Notifications', () => {
|
||||
await helpCenterButton.click()
|
||||
|
||||
// Verify "What's New?" section is now hidden
|
||||
await expect(whatsNewSection).not.toBeVisible()
|
||||
await expect(whatsNewSection).toBeHidden()
|
||||
})
|
||||
|
||||
test('should handle edge case with empty releases and disabled notifications', async ({
|
||||
@@ -381,6 +381,6 @@ test.describe('Release Notifications', () => {
|
||||
const whatsNewSection = comfyPage.page.getByTestId(
|
||||
TestIds.dialogs.whatsNewSection
|
||||
)
|
||||
await expect(whatsNewSection).not.toBeVisible()
|
||||
await expect(whatsNewSection).toBeHidden()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -31,7 +31,7 @@ test.describe('MediaLightbox', { tag: ['@slow'] }, () => {
|
||||
|
||||
// Wait for any asset card to appear (may contain img or video)
|
||||
const assetCard = comfyPage.page
|
||||
.locator('[role="button"]')
|
||||
.getByRole('button')
|
||||
.filter({ has: comfyPage.page.locator('img, video') })
|
||||
.first()
|
||||
|
||||
@@ -56,13 +56,13 @@ test.describe('MediaLightbox', { tag: ['@slow'] }, () => {
|
||||
await runAndOpenGallery(comfyPage)
|
||||
|
||||
await comfyPage.page.keyboard.press('Escape')
|
||||
await expect(comfyPage.mediaLightbox.root).not.toBeVisible()
|
||||
await expect(comfyPage.mediaLightbox.root).toBeHidden()
|
||||
})
|
||||
|
||||
test('closes gallery when clicking close button', async ({ comfyPage }) => {
|
||||
await runAndOpenGallery(comfyPage)
|
||||
|
||||
await comfyPage.mediaLightbox.closeButton.click()
|
||||
await expect(comfyPage.mediaLightbox.root).not.toBeVisible()
|
||||
await expect(comfyPage.mediaLightbox.root).toBeHidden()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -135,7 +135,7 @@ test.describe('Node Right Click Menu', { tag: ['@screenshot', '@ui'] }, () => {
|
||||
await comfyPage.page.mouse.move(10, 10)
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('right-click-node.png')
|
||||
await comfyPage.page.click('.litemenu-entry:has-text("Pin")')
|
||||
await comfyPage.page.locator('.litemenu-entry:has-text("Pin")').click()
|
||||
await comfyPage.contextMenu.waitForHidden()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
@@ -153,7 +153,7 @@ test.describe('Node Right Click Menu', { tag: ['@screenshot', '@ui'] }, () => {
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'right-click-pinned-node.png'
|
||||
)
|
||||
await comfyPage.page.click('.litemenu-entry:has-text("Unpin")')
|
||||
await comfyPage.page.locator('.litemenu-entry:has-text("Unpin")').click()
|
||||
await comfyPage.contextMenu.waitForHidden()
|
||||
await comfyPage.canvas.click({
|
||||
position: DefaultGraphPositions.emptyLatentWidgetClick,
|
||||
@@ -173,7 +173,7 @@ test.describe('Node Right Click Menu', { tag: ['@screenshot', '@ui'] }, () => {
|
||||
})
|
||||
await comfyPage.page.mouse.move(10, 10)
|
||||
await comfyPage.nextFrame()
|
||||
await comfyPage.page.click('.litemenu-entry:has-text("Pin")')
|
||||
await comfyPage.page.locator('.litemenu-entry:has-text("Pin")').click()
|
||||
await comfyPage.contextMenu.waitForHidden()
|
||||
await comfyPage.canvas.click({
|
||||
position: DefaultGraphPositions.emptyLatentWidgetClick,
|
||||
@@ -181,7 +181,7 @@ test.describe('Node Right Click Menu', { tag: ['@screenshot', '@ui'] }, () => {
|
||||
})
|
||||
await comfyPage.page.mouse.move(10, 10)
|
||||
await comfyPage.nextFrame()
|
||||
await comfyPage.page.click('.litemenu-entry:has-text("Unpin")')
|
||||
await comfyPage.page.locator('.litemenu-entry:has-text("Unpin")').click()
|
||||
await comfyPage.contextMenu.waitForHidden()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
@@ -203,7 +203,7 @@ test.describe('Node Right Click Menu', { tag: ['@screenshot', '@ui'] }, () => {
|
||||
})
|
||||
await comfyPage.page.mouse.move(10, 10)
|
||||
await comfyPage.nextFrame()
|
||||
await comfyPage.page.click('.litemenu-entry:has-text("Pin")')
|
||||
await comfyPage.page.locator('.litemenu-entry:has-text("Pin")').click()
|
||||
await comfyPage.page.keyboard.up('Control')
|
||||
await comfyPage.contextMenu.waitForHidden()
|
||||
await comfyPage.nextFrame()
|
||||
@@ -214,7 +214,7 @@ test.describe('Node Right Click Menu', { tag: ['@screenshot', '@ui'] }, () => {
|
||||
})
|
||||
await comfyPage.page.mouse.move(10, 10)
|
||||
await comfyPage.nextFrame()
|
||||
await comfyPage.page.click('.litemenu-entry:has-text("Unpin")')
|
||||
await comfyPage.page.locator('.litemenu-entry:has-text("Unpin")').click()
|
||||
await comfyPage.contextMenu.waitForHidden()
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
|
||||
@@ -9,6 +9,7 @@ const test = comfyPageFixture
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
const BLUE_COLOR = 'rgb(51, 51, 85)'
|
||||
const RED_COLOR = 'rgb(85, 51, 51)'
|
||||
|
||||
@@ -30,7 +31,7 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => {
|
||||
|
||||
test('shows selection toolbox', async ({ comfyPage }) => {
|
||||
// By default, selection toolbox should be enabled
|
||||
await expect(comfyPage.selectionToolbox).not.toBeVisible()
|
||||
await expect(comfyPage.selectionToolbox).toBeHidden()
|
||||
|
||||
// Select multiple nodes
|
||||
await comfyPage.nodeOps.selectNodes([
|
||||
@@ -86,7 +87,7 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => {
|
||||
await comfyPage.page.mouse.down()
|
||||
await comfyPage.page.mouse.move(nodePos.x + 200, nodePos.y + 200)
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.selectionToolbox).not.toBeVisible()
|
||||
await expect(comfyPage.selectionToolbox).toBeHidden()
|
||||
})
|
||||
|
||||
test('shows border only with multiple selections', async ({ comfyPage }) => {
|
||||
@@ -127,7 +128,7 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => {
|
||||
await comfyPage.workflow.loadWorkflow('groups/single_group')
|
||||
|
||||
// Select group + node should show bypass button
|
||||
await comfyPage.page.focus('canvas')
|
||||
await comfyPage.canvas.focus()
|
||||
await comfyPage.page.keyboard.press('Control+A')
|
||||
await expect(
|
||||
comfyPage.page.locator(
|
||||
@@ -141,7 +142,7 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => {
|
||||
comfyPage.page.locator(
|
||||
'.selection-toolbox *[data-testid="bypass-button"]'
|
||||
)
|
||||
).not.toBeVisible()
|
||||
).toBeHidden()
|
||||
})
|
||||
|
||||
test.describe('Color Picker', () => {
|
||||
@@ -169,7 +170,7 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => {
|
||||
await blueColorOption.click()
|
||||
|
||||
// Dropdown should close after selection
|
||||
await expect(colorPickerGroup).not.toBeVisible()
|
||||
await expect(colorPickerGroup).toBeHidden()
|
||||
|
||||
// Node should have the selected color class/style
|
||||
// Note: Exact verification method depends on how color is applied to nodes
|
||||
|
||||
@@ -204,7 +204,9 @@ test.describe('Selection Toolbox - Button Actions', { tag: '@ui' }, () => {
|
||||
name: /Frame Nodes/i
|
||||
})
|
||||
await expect(frameButton).toBeVisible()
|
||||
await frameButton.click({ force: true })
|
||||
await comfyPage.page
|
||||
.getByRole('button', { name: /Frame Nodes/i })
|
||||
.click({ force: true })
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await expect
|
||||
@@ -223,7 +225,7 @@ test.describe('Selection Toolbox - Button Actions', { tag: '@ui' }, () => {
|
||||
const frameButton = comfyPage.page.getByRole('button', {
|
||||
name: /Frame Nodes/i
|
||||
})
|
||||
await expect(frameButton).not.toBeVisible()
|
||||
await expect(frameButton).toBeHidden()
|
||||
})
|
||||
|
||||
test('execute button visible when output node selected', async ({
|
||||
@@ -253,6 +255,6 @@ test.describe('Selection Toolbox - Button Actions', { tag: '@ui' }, () => {
|
||||
const executeButton = comfyPage.page.getByRole('button', {
|
||||
name: /Execute to selected output nodes/i
|
||||
})
|
||||
await expect(executeButton).not.toBeVisible()
|
||||
await expect(executeButton).toBeHidden()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -47,12 +47,10 @@ test.describe(
|
||||
|
||||
await expect(comfyPage.page.locator('.selection-toolbox')).toBeVisible()
|
||||
|
||||
const moreOptionsBtn = comfyPage.page.locator(
|
||||
'[data-testid="more-options-button"]'
|
||||
)
|
||||
const moreOptionsBtn = comfyPage.page.getByTestId('more-options-button')
|
||||
await expect(moreOptionsBtn).toBeVisible()
|
||||
|
||||
await comfyPage.page.click('[data-testid="more-options-button"]')
|
||||
await moreOptionsBtn.click()
|
||||
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
@@ -113,7 +111,7 @@ test.describe(
|
||||
|
||||
await openMoreOptions(comfyPage)
|
||||
await comfyPage.page.getByText('Color', { exact: true }).click()
|
||||
const blueSwatch = comfyPage.page.locator('[title="Blue"]')
|
||||
const blueSwatch = comfyPage.page.getByTitle('Blue')
|
||||
await expect(blueSwatch.first()).toBeVisible()
|
||||
await blueSwatch.first().click()
|
||||
await comfyPage.nextFrame()
|
||||
@@ -162,7 +160,7 @@ test.describe(
|
||||
await comfyPage.nextFrame()
|
||||
await expect(
|
||||
comfyPage.page.getByText('Rename', { exact: true })
|
||||
).not.toBeVisible()
|
||||
).toBeHidden()
|
||||
})
|
||||
|
||||
test('closes More Options menu when clicking the button again (toggle)', async ({
|
||||
@@ -191,7 +189,7 @@ test.describe(
|
||||
|
||||
await expect(
|
||||
comfyPage.page.getByText('Rename', { exact: true })
|
||||
).not.toBeVisible()
|
||||
).toBeHidden()
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
@@ -192,6 +192,7 @@ test.describe('Assets sidebar - grid view display', () => {
|
||||
// Imported tab should show the mocked files
|
||||
await expect.poll(() => tab.assetCards.count()).toBeGreaterThanOrEqual(1)
|
||||
})
|
||||
|
||||
test('Displays svg outputs', async ({ comfyPage }) => {
|
||||
await comfyPage.assets.mockOutputHistory([
|
||||
createMockJob({
|
||||
@@ -745,7 +746,7 @@ test.describe('Assets sidebar - delete confirmation', () => {
|
||||
|
||||
await comfyPage.confirmDialog.delete.click()
|
||||
|
||||
await expect(dialog).not.toBeVisible()
|
||||
await expect(dialog).toBeHidden()
|
||||
await expect(tab.assetCards).toHaveCount(initialCount - 1)
|
||||
|
||||
const successToast = comfyPage.page.locator('.p-toast-message-success')
|
||||
@@ -767,7 +768,7 @@ test.describe('Assets sidebar - delete confirmation', () => {
|
||||
|
||||
await comfyPage.confirmDialog.reject.click()
|
||||
|
||||
await expect(dialog).not.toBeVisible()
|
||||
await expect(dialog).toBeHidden()
|
||||
await expect(tab.assetCards).toHaveCount(initialCount)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -118,7 +118,7 @@ test.describe('Model library sidebar - search', () => {
|
||||
await expect(tab.getLeafByLabel('dreamshaper_8')).toBeVisible()
|
||||
|
||||
// Other models should not be visible
|
||||
await expect(tab.getLeafByLabel('sd_xl_base_1.0')).not.toBeVisible()
|
||||
await expect(tab.getLeafByLabel('sd_xl_base_1.0')).toBeHidden()
|
||||
})
|
||||
|
||||
test('Clearing search restores folder view', async ({ comfyPage }) => {
|
||||
|
||||
@@ -58,7 +58,7 @@ test.describe('Node library sidebar', () => {
|
||||
|
||||
// Hover over a node to display the preview
|
||||
const nodeSelector = tab.nodeSelector('KSampler (Advanced)')
|
||||
await comfyPage.page.hover(nodeSelector)
|
||||
await comfyPage.page.locator(nodeSelector).hover()
|
||||
|
||||
// Verify the preview is displayed
|
||||
await expect(tab.nodePreview).toBeVisible()
|
||||
@@ -78,9 +78,9 @@ test.describe('Node library sidebar', () => {
|
||||
y: canvasBoundingBox.y + canvasBoundingBox.height / 2
|
||||
}
|
||||
|
||||
await comfyPage.page.dragAndDrop(nodeSelector, canvasSelector, {
|
||||
targetPosition
|
||||
})
|
||||
await comfyPage.page
|
||||
.locator(nodeSelector)
|
||||
.dragTo(comfyPage.page.locator(canvasSelector), { targetPosition })
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Verify the node is added to the canvas
|
||||
@@ -102,7 +102,9 @@ test.describe('Node library sidebar', () => {
|
||||
await expect(tab.getNode('KSampler (Advanced)')).toHaveCount(2)
|
||||
|
||||
// Hover on the bookmark node to display the preview
|
||||
await comfyPage.page.hover('.node-lib-bookmark-tree-explorer .tree-leaf')
|
||||
await comfyPage.page
|
||||
.locator('.node-lib-bookmark-tree-explorer .tree-leaf')
|
||||
.hover()
|
||||
await expect(tab.nodePreview).toBeVisible()
|
||||
})
|
||||
|
||||
@@ -220,6 +222,7 @@ test.describe('Node library sidebar', () => {
|
||||
.click()
|
||||
await expectBookmarks(comfyPage, [])
|
||||
})
|
||||
|
||||
test('Can customize icon', async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting(bookmarksSettingId, ['foo/'])
|
||||
const tab = comfyPage.menu.nodeLibraryTab
|
||||
@@ -247,6 +250,7 @@ test.describe('Node library sidebar', () => {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// If color is left as default, it should not be saved
|
||||
test('Can customize icon (default field)', async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting(bookmarksSettingId, ['foo/'])
|
||||
|
||||
@@ -42,11 +42,11 @@ test.describe('Node library sidebar V2', () => {
|
||||
test('Search filters nodes in All tab', async ({ comfyPage }) => {
|
||||
const tab = comfyPage.menu.nodeLibraryTabV2
|
||||
|
||||
await expect(tab.getNode('KSampler (Advanced)')).not.toBeVisible()
|
||||
await expect(tab.getNode('KSampler (Advanced)')).toBeHidden()
|
||||
|
||||
await tab.searchInput.fill('KSampler')
|
||||
await expect(tab.getNode('KSampler (Advanced)')).toBeVisible()
|
||||
await expect(tab.getNode('CLIPLoader')).not.toBeVisible()
|
||||
await expect(tab.getNode('CLIPLoader')).toBeHidden()
|
||||
})
|
||||
|
||||
test('Drag node to canvas adds it', async ({ comfyPage }) => {
|
||||
|
||||
@@ -27,9 +27,7 @@ test.describe('Workflow sidebar - search', () => {
|
||||
await searchInput.fill('alpha')
|
||||
|
||||
await expect(findWorkflow(comfyPage.page, 'alpha-workflow')).toBeVisible()
|
||||
await expect(
|
||||
findWorkflow(comfyPage.page, 'beta-workflow')
|
||||
).not.toBeVisible()
|
||||
await expect(findWorkflow(comfyPage.page, 'beta-workflow')).toBeHidden()
|
||||
})
|
||||
|
||||
test('Clearing search restores all workflows', async ({ comfyPage }) => {
|
||||
@@ -38,9 +36,7 @@ test.describe('Workflow sidebar - search', () => {
|
||||
|
||||
const searchInput = comfyPage.page.getByPlaceholder('Search Workflow...')
|
||||
await searchInput.fill('alpha')
|
||||
await expect(
|
||||
findWorkflow(comfyPage.page, 'beta-workflow')
|
||||
).not.toBeVisible()
|
||||
await expect(findWorkflow(comfyPage.page, 'beta-workflow')).toBeHidden()
|
||||
|
||||
await searchInput.fill('')
|
||||
|
||||
@@ -55,11 +51,7 @@ test.describe('Workflow sidebar - search', () => {
|
||||
const searchInput = comfyPage.page.getByPlaceholder('Search Workflow...')
|
||||
await searchInput.fill('nonexistent_xyz')
|
||||
|
||||
await expect(
|
||||
findWorkflow(comfyPage.page, 'alpha-workflow')
|
||||
).not.toBeVisible()
|
||||
await expect(
|
||||
findWorkflow(comfyPage.page, 'beta-workflow')
|
||||
).not.toBeVisible()
|
||||
await expect(findWorkflow(comfyPage.page, 'alpha-workflow')).toBeHidden()
|
||||
await expect(findWorkflow(comfyPage.page, 'beta-workflow')).toBeHidden()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -265,7 +265,7 @@ test.describe('Workflows sidebar', () => {
|
||||
|
||||
// Dismiss the error overlay
|
||||
await errorOverlay.getByTestId(TestIds.dialogs.errorOverlayDismiss).click()
|
||||
await expect(errorOverlay).not.toBeVisible()
|
||||
await expect(errorOverlay).toBeHidden()
|
||||
|
||||
// Load blank workflow
|
||||
await comfyPage.menu.workflowsTab.open()
|
||||
@@ -316,7 +316,7 @@ test.describe('Workflows sidebar', () => {
|
||||
await workflowsTab.getOpenedItem(filename).click({ button: 'right' })
|
||||
await comfyPage.nextFrame()
|
||||
await comfyPage.contextMenu.clickMenuItem('Delete')
|
||||
await expect(workflowsTab.getOpenedItem(filename)).not.toBeVisible()
|
||||
await expect(workflowsTab.getOpenedItem(filename)).toBeHidden()
|
||||
await expect
|
||||
.poll(() => workflowsTab.getOpenedWorkflowNames())
|
||||
.toEqual(['*Unsaved Workflow'])
|
||||
@@ -337,7 +337,7 @@ test.describe('Workflows sidebar', () => {
|
||||
|
||||
await comfyPage.confirmDialog.click('delete')
|
||||
|
||||
await expect(workflowsTab.getOpenedItem(filename)).not.toBeVisible()
|
||||
await expect(workflowsTab.getOpenedItem(filename)).toBeHidden()
|
||||
await expect
|
||||
.poll(() => workflowsTab.getOpenedWorkflowNames())
|
||||
.toEqual(['*Unsaved Workflow'])
|
||||
|
||||
@@ -52,11 +52,11 @@ test.describe(
|
||||
await comfyPage.workflow.waitForDraftPersisted()
|
||||
|
||||
// Reload the page (draft auto-loads with hash preserved)
|
||||
await comfyPage.page.reload({ waitUntil: 'networkidle' })
|
||||
await comfyPage.page.reload({ waitUntil: 'domcontentloaded' })
|
||||
await comfyPage.page.waitForFunction(
|
||||
() => window.app && window.app.extensionManager
|
||||
)
|
||||
await comfyPage.page.waitForSelector('.p-blockui-mask', {
|
||||
await comfyPage.page.locator('.p-blockui-mask').waitFor({
|
||||
state: 'hidden'
|
||||
})
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
@@ -131,7 +131,7 @@ test.describe(
|
||||
const enterButton = subgraphVueNode.getByTestId('subgraph-enter-button')
|
||||
await expect(enterButton).toBeVisible()
|
||||
|
||||
const nodeBody = subgraphVueNode.locator('[data-testid="node-body-11"]')
|
||||
const nodeBody = subgraphVueNode.getByTestId('node-body-11')
|
||||
await expect(nodeBody).toBeVisible()
|
||||
|
||||
const widgets = nodeBody.locator('.lg-node-widgets > div')
|
||||
@@ -400,7 +400,7 @@ test.describe(
|
||||
|
||||
await comfyPage.command.executeCommand('Comfy.QueuePrompt')
|
||||
|
||||
const nodeBody = subgraphVueNode.locator('[data-testid="node-body-5"]')
|
||||
const nodeBody = subgraphVueNode.getByTestId('node-body-5')
|
||||
await expect(nodeBody).toBeVisible()
|
||||
await expect(
|
||||
nodeBody.locator('.lg-node-widgets > div').first()
|
||||
|
||||
@@ -132,7 +132,9 @@ test.describe('Subgraph Slots', { tag: ['@slow', '@subgraph'] }, () => {
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await expect(comfyPage.page.locator(SELECTORS.promptDialog)).toBeVisible()
|
||||
await comfyPage.page.fill(SELECTORS.promptDialog, RENAMED_INPUT_NAME)
|
||||
await comfyPage.page
|
||||
.locator(SELECTORS.promptDialog)
|
||||
.fill(RENAMED_INPUT_NAME)
|
||||
await comfyPage.page.keyboard.press('Enter')
|
||||
|
||||
await comfyPage.canvas.click({ position: { x: 100, y: 100 } })
|
||||
@@ -153,10 +155,12 @@ test.describe('Subgraph Slots', { tag: ['@slow', '@subgraph'] }, () => {
|
||||
|
||||
await comfyPage.subgraph.doubleClickInputSlot(initialInputLabel!)
|
||||
|
||||
await comfyPage.page.waitForSelector(SELECTORS.promptDialog, {
|
||||
await comfyPage.page.locator(SELECTORS.promptDialog).waitFor({
|
||||
state: 'visible'
|
||||
})
|
||||
await comfyPage.page.fill(SELECTORS.promptDialog, RENAMED_INPUT_NAME)
|
||||
await comfyPage.page
|
||||
.locator(SELECTORS.promptDialog)
|
||||
.fill(RENAMED_INPUT_NAME)
|
||||
await comfyPage.page.keyboard.press('Enter')
|
||||
|
||||
// Force re-render
|
||||
@@ -178,11 +182,13 @@ test.describe('Subgraph Slots', { tag: ['@slow', '@subgraph'] }, () => {
|
||||
|
||||
await comfyPage.subgraph.doubleClickOutputSlot(initialOutputLabel!)
|
||||
|
||||
await comfyPage.page.waitForSelector(SELECTORS.promptDialog, {
|
||||
await comfyPage.page.locator(SELECTORS.promptDialog).waitFor({
|
||||
state: 'visible'
|
||||
})
|
||||
const renamedOutputName = 'renamed_output'
|
||||
await comfyPage.page.fill(SELECTORS.promptDialog, renamedOutputName)
|
||||
await comfyPage.page
|
||||
.locator(SELECTORS.promptDialog)
|
||||
.fill(renamedOutputName)
|
||||
await comfyPage.page.keyboard.press('Enter')
|
||||
|
||||
// Force re-render
|
||||
@@ -209,11 +215,13 @@ test.describe('Subgraph Slots', { tag: ['@slow', '@subgraph'] }, () => {
|
||||
await comfyPage.contextMenu.clickLitegraphMenuItem('Rename Slot')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await comfyPage.page.waitForSelector(SELECTORS.promptDialog, {
|
||||
await comfyPage.page.locator(SELECTORS.promptDialog).waitFor({
|
||||
state: 'visible'
|
||||
})
|
||||
const rightClickRenamedName = 'right_click_renamed'
|
||||
await comfyPage.page.fill(SELECTORS.promptDialog, rightClickRenamedName)
|
||||
await comfyPage.page
|
||||
.locator(SELECTORS.promptDialog)
|
||||
.fill(rightClickRenamedName)
|
||||
await comfyPage.page.keyboard.press('Enter')
|
||||
|
||||
// Force re-render
|
||||
@@ -270,7 +278,9 @@ test.describe('Subgraph Slots', { tag: ['@slow', '@subgraph'] }, () => {
|
||||
|
||||
await expect(comfyPage.page.locator(SELECTORS.promptDialog)).toBeVisible()
|
||||
const labelClickRenamedName = 'label_click_renamed'
|
||||
await comfyPage.page.fill(SELECTORS.promptDialog, labelClickRenamedName)
|
||||
await comfyPage.page
|
||||
.locator(SELECTORS.promptDialog)
|
||||
.fill(labelClickRenamedName)
|
||||
await comfyPage.page.keyboard.press('Enter')
|
||||
|
||||
await comfyPage.canvas.click({ position: { x: 100, y: 100 } })
|
||||
@@ -303,8 +313,10 @@ test.describe('Subgraph Slots', { tag: ['@slow', '@subgraph'] }, () => {
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await expect(comfyPage.page.locator(SELECTORS.promptDialog)).toBeVisible()
|
||||
await comfyPage.page.fill(SELECTORS.promptDialog, '')
|
||||
await comfyPage.page.fill(SELECTORS.promptDialog, RENAMED_SLOT_NAME)
|
||||
await comfyPage.page.locator(SELECTORS.promptDialog).fill('')
|
||||
await comfyPage.page
|
||||
.locator(SELECTORS.promptDialog)
|
||||
.fill(RENAMED_SLOT_NAME)
|
||||
await comfyPage.page.keyboard.press('Enter')
|
||||
|
||||
await expect(comfyPage.page.locator(SELECTORS.promptDialog)).toBeHidden()
|
||||
@@ -332,8 +344,10 @@ test.describe('Subgraph Slots', { tag: ['@slow', '@subgraph'] }, () => {
|
||||
RENAMED_SLOT_NAME
|
||||
)
|
||||
|
||||
await comfyPage.page.fill(SELECTORS.promptDialog, '')
|
||||
await comfyPage.page.fill(SELECTORS.promptDialog, SECOND_RENAMED_NAME)
|
||||
await comfyPage.page.locator(SELECTORS.promptDialog).fill('')
|
||||
await comfyPage.page
|
||||
.locator(SELECTORS.promptDialog)
|
||||
.fill(SECOND_RENAMED_NAME)
|
||||
await comfyPage.page.keyboard.press('Enter')
|
||||
|
||||
await expect(comfyPage.page.locator(SELECTORS.promptDialog)).toBeHidden()
|
||||
@@ -366,8 +380,10 @@ test.describe('Subgraph Slots', { tag: ['@slow', '@subgraph'] }, () => {
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await expect(comfyPage.page.locator(SELECTORS.promptDialog)).toBeVisible()
|
||||
await comfyPage.page.fill(SELECTORS.promptDialog, '')
|
||||
await comfyPage.page.fill(SELECTORS.promptDialog, RENAMED_SLOT_NAME)
|
||||
await comfyPage.page.locator(SELECTORS.promptDialog).fill('')
|
||||
await comfyPage.page
|
||||
.locator(SELECTORS.promptDialog)
|
||||
.fill(RENAMED_SLOT_NAME)
|
||||
await comfyPage.page.keyboard.press('Enter')
|
||||
|
||||
await expect(comfyPage.page.locator(SELECTORS.promptDialog)).toBeHidden()
|
||||
@@ -434,8 +450,8 @@ test.describe('Subgraph Slots', { tag: ['@slow', '@subgraph'] }, () => {
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await expect(comfyPage.page.locator(SELECTORS.promptDialog)).toBeVisible()
|
||||
await comfyPage.page.fill(SELECTORS.promptDialog, '')
|
||||
await comfyPage.page.fill(SELECTORS.promptDialog, RENAMED_LABEL)
|
||||
await comfyPage.page.locator(SELECTORS.promptDialog).fill('')
|
||||
await comfyPage.page.locator(SELECTORS.promptDialog).fill(RENAMED_LABEL)
|
||||
await comfyPage.page.keyboard.press('Enter')
|
||||
await expect(comfyPage.page.locator(SELECTORS.promptDialog)).toBeHidden()
|
||||
|
||||
@@ -533,8 +549,10 @@ test.describe('Subgraph Slots', { tag: ['@slow', '@subgraph'] }, () => {
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await expect(comfyPage.page.locator(SELECTORS.promptDialog)).toBeVisible()
|
||||
await comfyPage.page.fill(SELECTORS.promptDialog, '')
|
||||
await comfyPage.page.fill(SELECTORS.promptDialog, 'my_custom_prompt')
|
||||
await comfyPage.page.locator(SELECTORS.promptDialog).fill('')
|
||||
await comfyPage.page
|
||||
.locator(SELECTORS.promptDialog)
|
||||
.fill('my_custom_prompt')
|
||||
await comfyPage.page.keyboard.press('Enter')
|
||||
await expect(comfyPage.page.locator(SELECTORS.promptDialog)).toBeHidden()
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ test.describe('Templates', { tag: ['@slow', '@workflow'] }, () => {
|
||||
// page.route(), and change checkTemplateFileExists to use browser-context
|
||||
// fetch (page.request.head bypasses Playwright routing).
|
||||
// https://github.com/Comfy-Org/ComfyUI_frontend/issues/3992
|
||||
// oxlint-disable-next-line playwright/no-skipped-test -- https://github.com/Comfy-Org/ComfyUI_frontend/issues/3992
|
||||
test.skip('should have all required thumbnail media for each template', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
@@ -185,8 +186,8 @@ test.describe('Templates', { tag: ['@slow', '@workflow'] }, () => {
|
||||
await comfyPage.command.executeCommand('Comfy.BrowseTemplates')
|
||||
await comfyPage.templates.content.waitFor({ state: 'visible' })
|
||||
|
||||
const templateGrid = comfyPage.page.locator(
|
||||
'[data-testid="template-workflows-content"]'
|
||||
const templateGrid = comfyPage.page.getByTestId(
|
||||
'template-workflows-content'
|
||||
)
|
||||
const nav = comfyPage.page.locator('header', { hasText: 'Templates' })
|
||||
|
||||
@@ -302,20 +303,18 @@ test.describe('Templates', { tag: ['@slow', '@workflow'] }, () => {
|
||||
|
||||
// Wait for cards to load
|
||||
await expect(
|
||||
comfyPage.page.locator(
|
||||
'[data-testid="template-workflow-short-description"]'
|
||||
)
|
||||
comfyPage.page.getByTestId('template-workflow-short-description')
|
||||
).toBeVisible()
|
||||
|
||||
// Verify all three cards with different descriptions are visible
|
||||
const shortDescCard = comfyPage.page.locator(
|
||||
'[data-testid="template-workflow-short-description"]'
|
||||
const shortDescCard = comfyPage.page.getByTestId(
|
||||
'template-workflow-short-description'
|
||||
)
|
||||
const mediumDescCard = comfyPage.page.locator(
|
||||
'[data-testid="template-workflow-medium-description"]'
|
||||
const mediumDescCard = comfyPage.page.getByTestId(
|
||||
'template-workflow-medium-description'
|
||||
)
|
||||
const longDescCard = comfyPage.page.locator(
|
||||
'[data-testid="template-workflow-long-description"]'
|
||||
const longDescCard = comfyPage.page.getByTestId(
|
||||
'template-workflow-long-description'
|
||||
)
|
||||
|
||||
await expect(shortDescCard).toBeVisible()
|
||||
@@ -333,8 +332,8 @@ test.describe('Templates', { tag: ['@slow', '@workflow'] }, () => {
|
||||
await expect(longDesc).toContainText('much longer description')
|
||||
|
||||
// Verify grid layout maintains consistency
|
||||
const templateGrid = comfyPage.page.locator(
|
||||
'[data-testid="template-workflows-content"]'
|
||||
const templateGrid = comfyPage.page.getByTestId(
|
||||
'template-workflows-content'
|
||||
)
|
||||
await expect(templateGrid).toBeVisible()
|
||||
await expect(templateGrid).toHaveScreenshot(
|
||||
|
||||
@@ -64,9 +64,9 @@ test.describe('Workflow tabs', () => {
|
||||
await topbar.getTab(0).click({ button: 'right' })
|
||||
|
||||
// Reka UI ContextMenuContent gets data-state="open" when active
|
||||
const contextMenu = comfyPage.page.locator(
|
||||
'[role="menu"][data-state="open"]'
|
||||
)
|
||||
const contextMenu = comfyPage.page
|
||||
.getByRole('menu')
|
||||
.and(comfyPage.page.locator('[data-state="open"]'))
|
||||
await expect(contextMenu).toBeVisible()
|
||||
|
||||
await expect(
|
||||
@@ -86,9 +86,9 @@ test.describe('Workflow tabs', () => {
|
||||
await expect.poll(() => topbar.getTabNames()).toHaveLength(2)
|
||||
|
||||
await topbar.getTab(1).click({ button: 'right' })
|
||||
const contextMenu = comfyPage.page.locator(
|
||||
'[role="menu"][data-state="open"]'
|
||||
)
|
||||
const contextMenu = comfyPage.page
|
||||
.getByRole('menu')
|
||||
.and(comfyPage.page.locator('[data-state="open"]'))
|
||||
await expect(contextMenu).toBeVisible()
|
||||
|
||||
await contextMenu
|
||||
|
||||
@@ -112,7 +112,7 @@ test.describe('Settings Search functionality', { tag: '@settings' }, () => {
|
||||
await dialog.open()
|
||||
|
||||
await comfyPage.page.keyboard.press('Escape')
|
||||
await expect(dialog.root).not.toBeVisible()
|
||||
await expect(dialog.root).toBeHidden()
|
||||
})
|
||||
|
||||
test('search box has proper debouncing behavior', async ({ comfyPage }) => {
|
||||
|
||||
@@ -83,7 +83,7 @@ test.describe('Version Mismatch Warnings', { tag: '@slow' }, () => {
|
||||
// Expect no warning toast to be shown
|
||||
await expect(
|
||||
comfyPage.page.getByText('Version Compatibility Warning')
|
||||
).not.toBeVisible()
|
||||
).toBeHidden()
|
||||
})
|
||||
|
||||
test('should persist dismissed state across sessions', async ({
|
||||
@@ -121,6 +121,6 @@ test.describe('Version Mismatch Warnings', { tag: '@slow' }, () => {
|
||||
// The same warning from same versions should not be shown to the user again
|
||||
await expect(
|
||||
comfyPage.page.getByText('Version Compatibility Warning')
|
||||
).not.toBeVisible()
|
||||
).toBeHidden()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -143,7 +143,7 @@ test.describe('Vue Node Context Menu', () => {
|
||||
await openContextMenu(comfyPage, nodeTitle)
|
||||
await clickExactMenuItem(comfyPage, 'Unpin')
|
||||
|
||||
await expect(fixture.pinIndicator).not.toBeVisible()
|
||||
await expect(fixture.pinIndicator).toBeHidden()
|
||||
await expect.poll(() => nodeRef.isPinned()).toBe(false)
|
||||
})
|
||||
|
||||
@@ -178,7 +178,7 @@ test.describe('Vue Node Context Menu', () => {
|
||||
|
||||
await openContextMenu(comfyPage, 'KSampler')
|
||||
await clickExactMenuItem(comfyPage, 'Minimize Node')
|
||||
await expect(fixture.body).not.toBeVisible()
|
||||
await expect(fixture.body).toBeHidden()
|
||||
|
||||
await openContextMenu(comfyPage, 'KSampler')
|
||||
await clickExactMenuItem(comfyPage, 'Expand Node')
|
||||
@@ -194,9 +194,7 @@ test.describe('Vue Node Context Menu', () => {
|
||||
const subgraphNode = comfyPage.vueNodes.getNodeByTitle('New Subgraph')
|
||||
await expect(subgraphNode).toBeVisible()
|
||||
|
||||
await expect(
|
||||
comfyPage.vueNodes.getNodeByTitle('KSampler')
|
||||
).not.toBeVisible()
|
||||
await expect(comfyPage.vueNodes.getNodeByTitle('KSampler')).toBeHidden()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -309,9 +307,7 @@ test.describe('Vue Node Context Menu', () => {
|
||||
|
||||
const subgraphNode = comfyPage.vueNodes.getNodeByTitle('New Subgraph')
|
||||
await expect(subgraphNode).toBeVisible()
|
||||
await expect(
|
||||
comfyPage.vueNodes.getNodeByTitle('KSampler')
|
||||
).not.toBeVisible()
|
||||
await expect(comfyPage.vueNodes.getNodeByTitle('KSampler')).toBeHidden()
|
||||
|
||||
// Unpack the subgraph
|
||||
await openContextMenu(comfyPage, 'New Subgraph')
|
||||
@@ -320,7 +316,7 @@ test.describe('Vue Node Context Menu', () => {
|
||||
await expect(comfyPage.vueNodes.getNodeByTitle('KSampler')).toBeVisible()
|
||||
await expect(
|
||||
comfyPage.vueNodes.getNodeByTitle('New Subgraph')
|
||||
).not.toBeVisible()
|
||||
).toBeHidden()
|
||||
})
|
||||
|
||||
test('should open properties panel via Edit Subgraph Widgets', async ({
|
||||
@@ -433,7 +429,7 @@ test.describe('Vue Node Context Menu', () => {
|
||||
|
||||
for (const title of nodeTitles) {
|
||||
const fixture = await comfyPage.vueNodes.getFixtureByTitle(title)
|
||||
await expect(fixture.pinIndicator).not.toBeVisible()
|
||||
await expect(fixture.pinIndicator).toBeHidden()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -474,8 +470,8 @@ test.describe('Vue Node Context Menu', () => {
|
||||
await openMultiNodeContextMenu(comfyPage, nodeTitles)
|
||||
await clickExactMenuItem(comfyPage, 'Minimize Node')
|
||||
|
||||
await expect(fixture1.body).not.toBeVisible()
|
||||
await expect(fixture2.body).not.toBeVisible()
|
||||
await expect(fixture1.body).toBeHidden()
|
||||
await expect(fixture2.body).toBeHidden()
|
||||
|
||||
await openMultiNodeContextMenu(comfyPage, nodeTitles)
|
||||
await clickExactMenuItem(comfyPage, 'Expand Node')
|
||||
|
||||
@@ -11,17 +11,22 @@ test.describe('Vue Node Moving', () => {
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
})
|
||||
|
||||
const getLoadCheckpointHeaderPos = async (comfyPage: ComfyPage) => {
|
||||
const loadCheckpointHeaderPos = await comfyPage.page
|
||||
.getByText('Load Checkpoint')
|
||||
const getHeaderPos = async (
|
||||
comfyPage: ComfyPage,
|
||||
title: string
|
||||
): Promise<{ x: number; y: number; width: number; height: number }> => {
|
||||
const box = await comfyPage.vueNodes
|
||||
.getNodeByTitle(title)
|
||||
.getByTestId('node-title')
|
||||
.first()
|
||||
.boundingBox()
|
||||
|
||||
if (!loadCheckpointHeaderPos)
|
||||
throw new Error('Load Checkpoint header not found')
|
||||
|
||||
return loadCheckpointHeaderPos
|
||||
if (!box) throw new Error(`${title} header not found`)
|
||||
return box
|
||||
}
|
||||
|
||||
const getLoadCheckpointHeaderPos = async (comfyPage: ComfyPage) =>
|
||||
getHeaderPos(comfyPage, 'Load Checkpoint')
|
||||
|
||||
const expectPosChanged = async (pos1: Position, pos2: Position) => {
|
||||
const diffX = Math.abs(pos2.x - pos1.x)
|
||||
const diffY = Math.abs(pos2.y - pos1.y)
|
||||
@@ -29,6 +34,16 @@ test.describe('Vue Node Moving', () => {
|
||||
expect(diffY).toBeGreaterThan(0)
|
||||
}
|
||||
|
||||
const deltaBetween = (before: Position, after: Position) => ({
|
||||
x: after.x - before.x,
|
||||
y: after.y - before.y
|
||||
})
|
||||
|
||||
const expectSameDelta = (a: Position, b: Position, tol = 2) => {
|
||||
expect(Math.abs(a.x - b.x)).toBeLessThanOrEqual(tol)
|
||||
expect(Math.abs(a.y - b.y)).toBeLessThanOrEqual(tol)
|
||||
}
|
||||
|
||||
test('should allow moving nodes by dragging', async ({ comfyPage }) => {
|
||||
const loadCheckpointHeaderPos = await getLoadCheckpointHeaderPos(comfyPage)
|
||||
await comfyPage.canvasOps.dragAndDrop(loadCheckpointHeaderPos, {
|
||||
@@ -80,6 +95,73 @@ test.describe('Vue Node Moving', () => {
|
||||
await expectPosChanged(headerPos, afterPos)
|
||||
})
|
||||
|
||||
test('should move all selected nodes together when dragging one with Meta held', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const checkpointBefore = await getHeaderPos(comfyPage, 'Load Checkpoint')
|
||||
const ksamplerBefore = await getHeaderPos(comfyPage, 'KSampler')
|
||||
const latentBefore = await getHeaderPos(comfyPage, 'Empty Latent Image')
|
||||
|
||||
const dx = 120
|
||||
const dy = 80
|
||||
|
||||
const clickNodeTitleWithMeta = async (title: string) => {
|
||||
await comfyPage.vueNodes
|
||||
.getNodeByTitle(title)
|
||||
.getByTestId('node-title')
|
||||
.first()
|
||||
.click({ modifiers: ['Meta'] })
|
||||
}
|
||||
|
||||
await comfyPage.page.keyboard.down('Meta')
|
||||
try {
|
||||
await clickNodeTitleWithMeta('Load Checkpoint')
|
||||
await clickNodeTitleWithMeta('KSampler')
|
||||
await clickNodeTitleWithMeta('Empty Latent Image')
|
||||
await expect(comfyPage.vueNodes.selectedNodes).toHaveCount(3)
|
||||
|
||||
// Re-fetch drag source after clicks in case the header reflowed.
|
||||
const dragSrc = await getHeaderPos(comfyPage, 'Load Checkpoint')
|
||||
const centerX = dragSrc.x + dragSrc.width / 2
|
||||
const centerY = dragSrc.y + dragSrc.height / 2
|
||||
|
||||
await comfyPage.page.mouse.move(centerX, centerY)
|
||||
await comfyPage.page.mouse.down()
|
||||
await comfyPage.nextFrame()
|
||||
await comfyPage.page.mouse.move(centerX + dx, centerY + dy, {
|
||||
steps: 20
|
||||
})
|
||||
await comfyPage.page.mouse.up()
|
||||
await comfyPage.nextFrame()
|
||||
} finally {
|
||||
await comfyPage.page.keyboard.up('Meta')
|
||||
await comfyPage.nextFrame()
|
||||
}
|
||||
|
||||
await expect(comfyPage.vueNodes.selectedNodes).toHaveCount(3)
|
||||
|
||||
const checkpointAfter = await getHeaderPos(comfyPage, 'Load Checkpoint')
|
||||
const ksamplerAfter = await getHeaderPos(comfyPage, 'KSampler')
|
||||
const latentAfter = await getHeaderPos(comfyPage, 'Empty Latent Image')
|
||||
|
||||
// All three nodes should have moved together by the same delta.
|
||||
// We don't assert the exact screen delta equals the dragged pixel delta,
|
||||
// because canvas scaling and snap-to-grid can introduce offsets.
|
||||
const checkpointDelta = deltaBetween(checkpointBefore, checkpointAfter)
|
||||
const ksamplerDelta = deltaBetween(ksamplerBefore, ksamplerAfter)
|
||||
const latentDelta = deltaBetween(latentBefore, latentAfter)
|
||||
|
||||
// Confirm an actual drag happened (not zero movement).
|
||||
expect(Math.abs(checkpointDelta.x)).toBeGreaterThan(10)
|
||||
expect(Math.abs(checkpointDelta.y)).toBeGreaterThan(10)
|
||||
|
||||
// Confirm all selected nodes moved by the same delta.
|
||||
expectSameDelta(checkpointDelta, ksamplerDelta)
|
||||
expectSameDelta(checkpointDelta, latentDelta)
|
||||
|
||||
await comfyPage.canvasOps.moveMouseToEmptyArea()
|
||||
})
|
||||
|
||||
test(
|
||||
'@mobile should allow moving nodes by dragging on touch devices',
|
||||
{ tag: '@screenshot' },
|
||||
|
||||
@@ -50,6 +50,6 @@ test.describe('Vue Nodes Renaming', () => {
|
||||
const editingTitleInput = comfyPage.page.getByTestId(
|
||||
TestIds.node.titleInput
|
||||
)
|
||||
await expect(editingTitleInput).not.toBeVisible()
|
||||
await expect(editingTitleInput).toBeHidden()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -30,7 +30,7 @@ test.describe('Vue Node Collapse', () => {
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Verify node content is hidden
|
||||
await expect(body).not.toBeVisible()
|
||||
await expect(body).toBeHidden()
|
||||
await expect
|
||||
.poll(async () => (await vueNode.boundingBox())?.height)
|
||||
.toBeLessThan(expandedBoundingBox.height)
|
||||
|
||||
@@ -24,7 +24,7 @@ test.describe('Vue Node Pin', () => {
|
||||
await expect(pinIndicator).toBeVisible()
|
||||
|
||||
await comfyPage.page.keyboard.press(PIN_HOTKEY)
|
||||
await expect(pinIndicator).not.toBeVisible()
|
||||
await expect(pinIndicator).toBeHidden()
|
||||
})
|
||||
|
||||
test('should allow toggling pin on multiple selected nodes with hotkey', async ({
|
||||
@@ -43,8 +43,8 @@ test.describe('Vue Node Pin', () => {
|
||||
await expect(pinIndicator2).toBeVisible()
|
||||
|
||||
await comfyPage.page.keyboard.press(PIN_HOTKEY)
|
||||
await expect(pinIndicator1).not.toBeVisible()
|
||||
await expect(pinIndicator2).not.toBeVisible()
|
||||
await expect(pinIndicator1).toBeHidden()
|
||||
await expect(pinIndicator2).toBeHidden()
|
||||
})
|
||||
|
||||
test('should not allow dragging pinned nodes', async ({ comfyPage }) => {
|
||||
|
||||
@@ -43,12 +43,8 @@ test.describe('Advanced Widget Visibility', () => {
|
||||
await expect(node.getByLabel('height', { exact: true })).toBeVisible()
|
||||
|
||||
// Advanced widgets should not be rendered
|
||||
await expect(
|
||||
node.getByLabel('max_shift', { exact: true })
|
||||
).not.toBeVisible()
|
||||
await expect(
|
||||
node.getByLabel('base_shift', { exact: true })
|
||||
).not.toBeVisible()
|
||||
await expect(node.getByLabel('max_shift', { exact: true })).toBeHidden()
|
||||
await expect(node.getByLabel('base_shift', { exact: true })).toBeHidden()
|
||||
|
||||
// "Show advanced inputs" button should be present
|
||||
await expect(node.getByText('Show advanced inputs')).toBeVisible()
|
||||
@@ -97,6 +93,6 @@ test.describe('Advanced Widget Visibility', () => {
|
||||
await expect(node.getByLabel('base_shift', { exact: true })).toBeVisible()
|
||||
|
||||
// The toggle button should not be shown when global setting is active
|
||||
await expect(node.getByText('Show advanced inputs')).not.toBeVisible()
|
||||
await expect(node.getByText('Show advanced inputs')).toBeHidden()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -17,7 +17,7 @@ test.describe('Vue Upload Widgets', () => {
|
||||
|
||||
await expect(
|
||||
comfyPage.page.getByText('choose file to upload', { exact: true })
|
||||
).not.toBeVisible()
|
||||
).toBeHidden()
|
||||
|
||||
await expect
|
||||
.poll(() =>
|
||||
|
||||
@@ -46,13 +46,14 @@ test.describe('Vue Multiline String Widget', () => {
|
||||
|
||||
await expect(textarea).toHaveValue('Keep me around')
|
||||
})
|
||||
|
||||
test('should use native context menu when focused', async ({ comfyPage }) => {
|
||||
const textarea = getFirstMultilineStringWidget(comfyPage)
|
||||
const vueContextMenu = comfyPage.page.locator('.p-contextmenu')
|
||||
|
||||
await textarea.focus()
|
||||
await textarea.click({ button: 'right' })
|
||||
await expect(vueContextMenu).not.toBeVisible()
|
||||
await expect(vueContextMenu).toBeHidden()
|
||||
await textarea.blur()
|
||||
|
||||
await textarea.click({ button: 'right' })
|
||||
|
||||
@@ -9,6 +9,7 @@ test.describe('Vue Widget Reactivity', () => {
|
||||
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
})
|
||||
|
||||
test('Should display added widgets', async ({ comfyPage }) => {
|
||||
const loadCheckpointNode = comfyPage.page.locator(
|
||||
'css=[data-testid="node-body-4"] > .lg-node-widgets > div'
|
||||
@@ -32,6 +33,7 @@ test.describe('Vue Widget Reactivity', () => {
|
||||
})
|
||||
await expect(loadCheckpointNode).toHaveCount(4)
|
||||
})
|
||||
|
||||
test('Should hide removed widgets', async ({ comfyPage }) => {
|
||||
const loadCheckpointNode = comfyPage.page.locator(
|
||||
'css=[data-testid="node-body-3"] > .lg-node-widgets > div'
|
||||
|
||||
@@ -272,6 +272,7 @@ test.describe('Image widget', { tag: ['@screenshot', '@widget'] }, () => {
|
||||
// Expect the filename combo value to be updated
|
||||
await expect.poll(() => fileComboWidget.getValue()).toBe('image32x32.webp')
|
||||
})
|
||||
|
||||
test('Displays buttons when viewing single image of batch', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
|
||||
@@ -83,7 +83,7 @@ test.describe('Workflow Tab Thumbnails', { tag: '@workflow' }, () => {
|
||||
1,
|
||||
'Unsaved Workflow (2)'
|
||||
)
|
||||
await expect(thumbnailImg).not.toBeVisible()
|
||||
await expect(thumbnailImg).toBeHidden()
|
||||
})
|
||||
|
||||
async function addNode(comfyPage: ComfyPage, category: string, node: string) {
|
||||
|
||||
@@ -135,6 +135,6 @@ test.describe('Zoom Controls', { tag: '@canvas' }, () => {
|
||||
await zoomButton.click()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await expect(zoomToFit).not.toBeVisible()
|
||||
await expect(zoomToFit).toBeHidden()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -23,7 +23,7 @@ See `docs/testing/*.md` for detailed patterns.
|
||||
|
||||
## Component Testing
|
||||
|
||||
- Use Vue Test Utils for component tests
|
||||
- Use `@testing-library/vue` with `@testing-library/user-event` for component tests (an ESLint rule bans `@vue/test-utils` in new tests)
|
||||
- Follow advice about making components easy to test
|
||||
- Wait for reactivity with `await nextTick()` after state changes
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ Our tests use the following frameworks and libraries:
|
||||
- [Vitest](https://vitest.dev/) - Test runner and assertion library
|
||||
- [@testing-library/vue](https://testing-library.com/docs/vue-testing-library/intro/) - Preferred for user-centric component testing
|
||||
- [@testing-library/user-event](https://testing-library.com/docs/user-event/intro/) - Realistic user interaction simulation
|
||||
- [@vue/test-utils](https://test-utils.vuejs.org/) - Vue component testing utilities (also accepted)
|
||||
- [@vue/test-utils](https://test-utils.vuejs.org/) - Vue component testing utilities (legacy; new tests must use @testing-library/vue)
|
||||
- [Pinia](https://pinia.vuejs.org/cookbook/testing.html) - For store testing
|
||||
|
||||
## Getting Started
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Component Testing Guide
|
||||
|
||||
> **Note**: New component tests must use `@testing-library/vue` with `@testing-library/user-event`. The examples below that use `@vue/test-utils` (`mount`, `wrapper`) are from legacy tests. An ESLint rule enforces this — importing from `@vue/test-utils` in `*.test.ts` files produces a lint error.
|
||||
|
||||
This guide covers patterns and examples for testing Vue components in the ComfyUI Frontend codebase.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
@@ -432,6 +432,23 @@ export default defineConfig([
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['**/*.test.ts'],
|
||||
rules: {
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
paths: [
|
||||
{
|
||||
name: '@vue/test-utils',
|
||||
message:
|
||||
'Use @testing-library/vue with @testing-library/user-event instead.'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
// Browser tests must use comfyPageFixture, not raw @playwright/test test
|
||||
{
|
||||
files: ['browser_tests/tests/**/*.spec.ts'],
|
||||
|
||||
14
package.json
14
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.44.1",
|
||||
"version": "1.44.2",
|
||||
"private": true,
|
||||
"description": "Official front-end implementation of ComfyUI",
|
||||
"homepage": "https://comfy.org",
|
||||
@@ -28,15 +28,15 @@
|
||||
"json-schema": "tsx scripts/generate-json-schema.ts",
|
||||
"knip:no-cache": "knip",
|
||||
"knip": "knip --cache",
|
||||
"lint:fix:no-cache": "oxlint src --type-aware --fix && eslint src --fix",
|
||||
"lint:fix": "oxlint src --type-aware --fix && eslint src --cache --fix",
|
||||
"lint:no-cache": "pnpm exec stylelint '{apps,packages,src}/**/*.{css,vue}' && oxlint src --type-aware && eslint src",
|
||||
"lint:fix:no-cache": "oxlint src browser_tests --type-aware --fix && eslint src --fix",
|
||||
"lint:fix": "oxlint src browser_tests --type-aware --fix && eslint src --cache --fix",
|
||||
"lint:no-cache": "pnpm exec stylelint '{apps,packages,src}/**/*.{css,vue}' && oxlint src browser_tests --type-aware && eslint src",
|
||||
"lint:unstaged:fix": "git diff --name-only HEAD | grep -E '\\.(js|ts|vue|mts)$' | xargs -r eslint --cache --fix",
|
||||
"lint:unstaged": "git diff --name-only HEAD | grep -E '\\.(js|ts|vue|mts)$' | xargs -r eslint --cache",
|
||||
"lint": "pnpm stylelint && oxlint src --type-aware && eslint src --cache",
|
||||
"lint": "pnpm stylelint && oxlint src browser_tests --type-aware && eslint src --cache",
|
||||
"lint:desktop": "nx run @comfyorg/desktop-ui:lint",
|
||||
"locale": "lobe-i18n locale",
|
||||
"oxlint": "oxlint src --type-aware",
|
||||
"oxlint": "oxlint src browser_tests --type-aware",
|
||||
"prepare": "husky || true && git config blame.ignoreRevsFile .git-blame-ignore-revs || true",
|
||||
"preview": "nx preview",
|
||||
"storybook": "nx storybook",
|
||||
@@ -150,7 +150,6 @@
|
||||
"@vitejs/plugin-vue": "catalog:",
|
||||
"@vitest/coverage-v8": "catalog:",
|
||||
"@vitest/ui": "catalog:",
|
||||
"@vue/test-utils": "catalog:",
|
||||
"@webgpu/types": "catalog:",
|
||||
"cross-env": "catalog:",
|
||||
"eslint": "catalog:",
|
||||
@@ -159,6 +158,7 @@
|
||||
"eslint-plugin-better-tailwindcss": "catalog:",
|
||||
"eslint-plugin-import-x": "catalog:",
|
||||
"eslint-plugin-oxlint": "catalog:",
|
||||
"eslint-plugin-playwright": "catalog:",
|
||||
"eslint-plugin-storybook": "catalog:",
|
||||
"eslint-plugin-testing-library": "catalog:",
|
||||
"eslint-plugin-unused-imports": "catalog:",
|
||||
|
||||
29
pnpm-lock.yaml
generated
29
pnpm-lock.yaml
generated
@@ -171,9 +171,6 @@ catalogs:
|
||||
'@vitest/ui':
|
||||
specifier: ^4.0.16
|
||||
version: 4.0.16
|
||||
'@vue/test-utils':
|
||||
specifier: ^2.4.6
|
||||
version: 2.4.6
|
||||
'@vueuse/core':
|
||||
specifier: ^14.2.0
|
||||
version: 14.2.0
|
||||
@@ -222,6 +219,9 @@ catalogs:
|
||||
eslint-plugin-oxlint:
|
||||
specifier: 1.59.0
|
||||
version: 1.59.0
|
||||
eslint-plugin-playwright:
|
||||
specifier: ^2.10.1
|
||||
version: 2.10.1
|
||||
eslint-plugin-storybook:
|
||||
specifier: ^10.2.10
|
||||
version: 10.2.10
|
||||
@@ -690,9 +690,6 @@ importers:
|
||||
'@vitest/ui':
|
||||
specifier: 'catalog:'
|
||||
version: 4.0.16(vitest@4.0.16)
|
||||
'@vue/test-utils':
|
||||
specifier: 'catalog:'
|
||||
version: 2.4.6
|
||||
'@webgpu/types':
|
||||
specifier: 'catalog:'
|
||||
version: 0.1.66
|
||||
@@ -717,6 +714,9 @@ importers:
|
||||
eslint-plugin-oxlint:
|
||||
specifier: 'catalog:'
|
||||
version: 1.59.0(oxlint@1.59.0(oxlint-tsgolint@0.20.0))
|
||||
eslint-plugin-playwright:
|
||||
specifier: 'catalog:'
|
||||
version: 2.10.1(eslint@9.39.1(jiti@2.6.1))
|
||||
eslint-plugin-storybook:
|
||||
specifier: 'catalog:'
|
||||
version: 10.2.10(eslint@9.39.1(jiti@2.6.1))(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)
|
||||
@@ -6144,6 +6144,12 @@ packages:
|
||||
peerDependencies:
|
||||
oxlint: ~1.59.0
|
||||
|
||||
eslint-plugin-playwright@2.10.1:
|
||||
resolution: {integrity: sha512-qea3UxBOb8fTwJ77FMApZKvRye5DOluDHcev0LDJwID3RELeun0JlqzrNIXAB/SXCyB/AesCW/6sZfcT9q3Edg==}
|
||||
engines: {node: '>=16.9.0'}
|
||||
peerDependencies:
|
||||
eslint: '>=8.40.0'
|
||||
|
||||
eslint-plugin-storybook@10.2.10:
|
||||
resolution: {integrity: sha512-aWkoh2rhTaEsMA4yB1iVIcISM5wb0uffp09ZqhwpoD4GAngCs131uq6un+QdnOMc7vXyAnBBfsuhtOj8WwCUgw==}
|
||||
peerDependencies:
|
||||
@@ -6563,6 +6569,10 @@ packages:
|
||||
resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
globals@17.4.0:
|
||||
resolution: {integrity: sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
globalthis@1.0.4:
|
||||
resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -15581,6 +15591,11 @@ snapshots:
|
||||
jsonc-parser: 3.3.1
|
||||
oxlint: 1.59.0(oxlint-tsgolint@0.20.0)
|
||||
|
||||
eslint-plugin-playwright@2.10.1(eslint@9.39.1(jiti@2.6.1)):
|
||||
dependencies:
|
||||
eslint: 9.39.1(jiti@2.6.1)
|
||||
globals: 17.4.0
|
||||
|
||||
eslint-plugin-storybook@10.2.10(eslint@9.39.1(jiti@2.6.1))(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@typescript-eslint/utils': 8.56.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
|
||||
@@ -16092,6 +16107,8 @@ snapshots:
|
||||
|
||||
globals@16.5.0: {}
|
||||
|
||||
globals@17.4.0: {}
|
||||
|
||||
globalthis@1.0.4:
|
||||
dependencies:
|
||||
define-properties: 1.2.1
|
||||
|
||||
@@ -31,12 +31,12 @@ catalog:
|
||||
'@sentry/vite-plugin': ^4.6.0
|
||||
'@sentry/vue': ^10.32.1
|
||||
'@sparkjsdev/spark': ^0.1.10
|
||||
'@tanstack/vue-virtual': ^3.13.12
|
||||
'@storybook/addon-docs': ^10.2.10
|
||||
'@storybook/addon-mcp': 0.1.6
|
||||
'@storybook/vue3': ^10.2.10
|
||||
'@storybook/vue3-vite': ^10.2.10
|
||||
'@tailwindcss/vite': ^4.2.0
|
||||
'@tanstack/vue-virtual': ^3.13.12
|
||||
'@testing-library/jest-dom': ^6.9.1
|
||||
'@testing-library/user-event': ^14.6.1
|
||||
'@testing-library/vue': ^8.1.0
|
||||
@@ -58,7 +58,6 @@ catalog:
|
||||
'@vitejs/plugin-vue': ^6.0.0
|
||||
'@vitest/coverage-v8': ^4.0.16
|
||||
'@vitest/ui': ^4.0.16
|
||||
'@vue/test-utils': ^2.4.6
|
||||
'@vueuse/core': ^14.2.0
|
||||
'@vueuse/integrations': ^14.2.0
|
||||
'@webgpu/types': ^0.1.66
|
||||
@@ -75,6 +74,7 @@ catalog:
|
||||
eslint-plugin-better-tailwindcss: ^4.3.1
|
||||
eslint-plugin-import-x: ^4.16.1
|
||||
eslint-plugin-oxlint: 1.59.0
|
||||
eslint-plugin-playwright: ^2.10.1
|
||||
eslint-plugin-storybook: ^10.2.10
|
||||
eslint-plugin-testing-library: ^7.16.1
|
||||
eslint-plugin-unused-imports: ^4.3.0
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { render, screen } from '@testing-library/vue'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
@@ -6,8 +6,11 @@ import RangeEditor from './RangeEditor.vue'
|
||||
|
||||
const i18n = createI18n({ legacy: false, locale: 'en', messages: { en: {} } })
|
||||
|
||||
function mountEditor(props: InstanceType<typeof RangeEditor>['$props']) {
|
||||
return mount(RangeEditor, {
|
||||
function renderEditor(props: {
|
||||
modelValue: { min: number; max: number; midpoint?: number }
|
||||
[key: string]: unknown
|
||||
}) {
|
||||
return render(RangeEditor, {
|
||||
props,
|
||||
global: { plugins: [i18n] }
|
||||
})
|
||||
@@ -15,20 +18,19 @@ function mountEditor(props: InstanceType<typeof RangeEditor>['$props']) {
|
||||
|
||||
describe('RangeEditor', () => {
|
||||
it('renders with min and max handles', () => {
|
||||
const wrapper = mountEditor({ modelValue: { min: 0.2, max: 0.8 } })
|
||||
renderEditor({ modelValue: { min: 0.2, max: 0.8 } })
|
||||
|
||||
expect(wrapper.find('svg').exists()).toBe(true)
|
||||
expect(wrapper.find('[data-testid="handle-min"]').exists()).toBe(true)
|
||||
expect(wrapper.find('[data-testid="handle-max"]').exists()).toBe(true)
|
||||
expect(screen.getByTestId('handle-min')).toBeDefined()
|
||||
expect(screen.getByTestId('handle-max')).toBeDefined()
|
||||
})
|
||||
|
||||
it('highlights selected range in plain mode', () => {
|
||||
const wrapper = mountEditor({ modelValue: { min: 0.2, max: 0.8 } })
|
||||
renderEditor({ modelValue: { min: 0.2, max: 0.8 } })
|
||||
|
||||
const highlight = wrapper.find('[data-testid="range-highlight"]')
|
||||
expect(highlight.attributes('x')).toBe('0.2')
|
||||
const highlight = screen.getByTestId('range-highlight')
|
||||
expect(highlight.getAttribute('x')).toBe('0.2')
|
||||
expect(
|
||||
Number.parseFloat(highlight.attributes('width') ?? 'NaN')
|
||||
Number.parseFloat(highlight.getAttribute('width') ?? 'NaN')
|
||||
).toBeCloseTo(0.6, 6)
|
||||
})
|
||||
|
||||
@@ -37,37 +39,37 @@ describe('RangeEditor', () => {
|
||||
for (let i = 0; i < 256; i++)
|
||||
histogram[i] = Math.floor(50 + 50 * Math.sin(i / 20))
|
||||
|
||||
const wrapper = mountEditor({
|
||||
renderEditor({
|
||||
modelValue: { min: 0.2, max: 0.8 },
|
||||
display: 'histogram',
|
||||
histogram
|
||||
})
|
||||
|
||||
const left = wrapper.find('[data-testid="range-dim-left"]')
|
||||
const right = wrapper.find('[data-testid="range-dim-right"]')
|
||||
expect(left.attributes('width')).toBe('0.2')
|
||||
expect(right.attributes('x')).toBe('0.8')
|
||||
const left = screen.getByTestId('range-dim-left')
|
||||
const right = screen.getByTestId('range-dim-right')
|
||||
expect(left.getAttribute('width')).toBe('0.2')
|
||||
expect(right.getAttribute('x')).toBe('0.8')
|
||||
})
|
||||
|
||||
it('hides midpoint handle by default', () => {
|
||||
const wrapper = mountEditor({
|
||||
renderEditor({
|
||||
modelValue: { min: 0, max: 1, midpoint: 0.5 }
|
||||
})
|
||||
|
||||
expect(wrapper.find('[data-testid="handle-midpoint"]').exists()).toBe(false)
|
||||
expect(screen.queryByTestId('handle-midpoint')).toBeNull()
|
||||
})
|
||||
|
||||
it('shows midpoint handle when showMidpoint is true', () => {
|
||||
const wrapper = mountEditor({
|
||||
renderEditor({
|
||||
modelValue: { min: 0, max: 1, midpoint: 0.5 },
|
||||
showMidpoint: true
|
||||
})
|
||||
|
||||
expect(wrapper.find('[data-testid="handle-midpoint"]').exists()).toBe(true)
|
||||
expect(screen.getByTestId('handle-midpoint')).toBeDefined()
|
||||
})
|
||||
|
||||
it('renders gradient background when display is gradient', () => {
|
||||
const wrapper = mountEditor({
|
||||
renderEditor({
|
||||
modelValue: { min: 0, max: 1 },
|
||||
display: 'gradient',
|
||||
gradientStops: [
|
||||
@@ -76,8 +78,8 @@ describe('RangeEditor', () => {
|
||||
]
|
||||
})
|
||||
|
||||
expect(wrapper.find('[data-testid="gradient-bg"]').exists()).toBe(true)
|
||||
expect(wrapper.find('linearGradient').exists()).toBe(true)
|
||||
expect(screen.getByTestId('gradient-bg')).toBeDefined()
|
||||
expect(screen.getByTestId('gradient-def')).toBeDefined()
|
||||
})
|
||||
|
||||
it('renders histogram path when display is histogram with data', () => {
|
||||
@@ -85,47 +87,43 @@ describe('RangeEditor', () => {
|
||||
for (let i = 0; i < 256; i++)
|
||||
histogram[i] = Math.floor(50 + 50 * Math.sin(i / 20))
|
||||
|
||||
const wrapper = mountEditor({
|
||||
renderEditor({
|
||||
modelValue: { min: 0, max: 1 },
|
||||
display: 'histogram',
|
||||
histogram
|
||||
})
|
||||
|
||||
expect(wrapper.find('[data-testid="histogram-path"]').exists()).toBe(true)
|
||||
expect(screen.getByTestId('histogram-path')).toBeDefined()
|
||||
})
|
||||
|
||||
it('renders inputs for min and max', () => {
|
||||
const wrapper = mountEditor({ modelValue: { min: 0.2, max: 0.8 } })
|
||||
renderEditor({ modelValue: { min: 0.2, max: 0.8 } })
|
||||
|
||||
const inputs = wrapper.findAll('input')
|
||||
const inputs = screen.getAllByRole('textbox')
|
||||
expect(inputs).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('renders midpoint input when showMidpoint is true', () => {
|
||||
const wrapper = mountEditor({
|
||||
renderEditor({
|
||||
modelValue: { min: 0, max: 1, midpoint: 0.5 },
|
||||
showMidpoint: true
|
||||
})
|
||||
|
||||
const inputs = wrapper.findAll('input')
|
||||
const inputs = screen.getAllByRole('textbox')
|
||||
expect(inputs).toHaveLength(3)
|
||||
})
|
||||
|
||||
it('normalizes handle positions with custom value range', () => {
|
||||
const wrapper = mountEditor({
|
||||
renderEditor({
|
||||
modelValue: { min: 64, max: 192 },
|
||||
valueMin: 0,
|
||||
valueMax: 255
|
||||
})
|
||||
|
||||
const minHandle = wrapper.find('[data-testid="handle-min"]')
|
||||
const maxHandle = wrapper.find('[data-testid="handle-max"]')
|
||||
const minHandle = screen.getByTestId('handle-min')
|
||||
const maxHandle = screen.getByTestId('handle-max')
|
||||
|
||||
expect(
|
||||
Number.parseFloat((minHandle.element as HTMLElement).style.left)
|
||||
).toBeCloseTo(25, 0)
|
||||
expect(
|
||||
Number.parseFloat((maxHandle.element as HTMLElement).style.left)
|
||||
).toBeCloseTo(75, 0)
|
||||
expect(Number.parseFloat(minHandle.style.left)).toBeCloseTo(25, 0)
|
||||
expect(Number.parseFloat(maxHandle.style.left)).toBeCloseTo(75, 0)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -17,7 +17,14 @@
|
||||
"
|
||||
>
|
||||
<defs v-if="display === 'gradient'">
|
||||
<linearGradient :id="gradientId" x1="0" y1="0" x2="1" y2="0">
|
||||
<linearGradient
|
||||
:id="gradientId"
|
||||
data-testid="gradient-def"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="1"
|
||||
y2="0"
|
||||
>
|
||||
<stop
|
||||
v-for="(stop, i) in computedStops"
|
||||
:key="i"
|
||||
|
||||
138
src/composables/useReconnectingNotification.test.ts
Normal file
138
src/composables/useReconnectingNotification.test.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useReconnectingNotification } from '@/composables/useReconnectingNotification'
|
||||
|
||||
const mockToastAdd = vi.fn()
|
||||
const mockToastRemove = vi.fn()
|
||||
|
||||
vi.mock('primevue/usetoast', () => ({
|
||||
useToast: () => ({
|
||||
add: mockToastAdd,
|
||||
remove: mockToastRemove
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key: string) => key
|
||||
})
|
||||
}))
|
||||
|
||||
const settingMocks = vi.hoisted(() => ({
|
||||
disableToast: false
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/settings/settingStore', () => ({
|
||||
useSettingStore: vi.fn(() => ({
|
||||
get: vi.fn((key: string) => {
|
||||
if (key === 'Comfy.Toast.DisableReconnectingToast')
|
||||
return settingMocks.disableToast
|
||||
return undefined
|
||||
})
|
||||
}))
|
||||
}))
|
||||
|
||||
describe('useReconnectingNotification', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
vi.useFakeTimers()
|
||||
vi.clearAllMocks()
|
||||
settingMocks.disableToast = false
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('does not show toast immediately on reconnecting', () => {
|
||||
const { onReconnecting } = useReconnectingNotification()
|
||||
|
||||
onReconnecting()
|
||||
|
||||
expect(mockToastAdd).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('shows error toast after delay', () => {
|
||||
const { onReconnecting } = useReconnectingNotification()
|
||||
|
||||
onReconnecting()
|
||||
vi.advanceTimersByTime(1500)
|
||||
|
||||
expect(mockToastAdd).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
severity: 'error',
|
||||
summary: 'g.reconnecting'
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('suppresses toast when reconnected before delay expires', () => {
|
||||
const { onReconnecting, onReconnected } = useReconnectingNotification()
|
||||
|
||||
onReconnecting()
|
||||
vi.advanceTimersByTime(500)
|
||||
onReconnected()
|
||||
vi.advanceTimersByTime(1500)
|
||||
|
||||
expect(mockToastAdd).not.toHaveBeenCalled()
|
||||
expect(mockToastRemove).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('removes toast and shows success when reconnected after delay', () => {
|
||||
const { onReconnecting, onReconnected } = useReconnectingNotification()
|
||||
|
||||
onReconnecting()
|
||||
vi.advanceTimersByTime(1500)
|
||||
mockToastAdd.mockClear()
|
||||
|
||||
onReconnected()
|
||||
|
||||
expect(mockToastRemove).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
severity: 'error',
|
||||
summary: 'g.reconnecting'
|
||||
})
|
||||
)
|
||||
expect(mockToastAdd).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
severity: 'success',
|
||||
summary: 'g.reconnected',
|
||||
life: 2000
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('does nothing when toast is disabled via setting', () => {
|
||||
settingMocks.disableToast = true
|
||||
const { onReconnecting, onReconnected } = useReconnectingNotification()
|
||||
|
||||
onReconnecting()
|
||||
vi.advanceTimersByTime(1500)
|
||||
onReconnected()
|
||||
|
||||
expect(mockToastAdd).not.toHaveBeenCalled()
|
||||
expect(mockToastRemove).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does nothing when onReconnected is called without prior onReconnecting', () => {
|
||||
const { onReconnected } = useReconnectingNotification()
|
||||
|
||||
onReconnected()
|
||||
|
||||
expect(mockToastAdd).not.toHaveBeenCalled()
|
||||
expect(mockToastRemove).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('handles multiple reconnecting events without duplicating toasts', () => {
|
||||
const { onReconnecting } = useReconnectingNotification()
|
||||
|
||||
onReconnecting()
|
||||
vi.advanceTimersByTime(1500) // first toast fires
|
||||
onReconnecting() // second reconnecting event
|
||||
vi.advanceTimersByTime(1500) // second toast fires
|
||||
|
||||
expect(mockToastAdd).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
52
src/composables/useReconnectingNotification.ts
Normal file
52
src/composables/useReconnectingNotification.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { useTimeoutFn } from '@vueuse/core'
|
||||
import type { ToastMessageOptions } from 'primevue/toast'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
|
||||
const RECONNECT_TOAST_DELAY_MS = 1500
|
||||
|
||||
export function useReconnectingNotification() {
|
||||
const { t } = useI18n()
|
||||
const toast = useToast()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
const reconnectingMessage: ToastMessageOptions = {
|
||||
severity: 'error',
|
||||
summary: t('g.reconnecting')
|
||||
}
|
||||
|
||||
const reconnectingToastShown = ref(false)
|
||||
|
||||
const { start, stop } = useTimeoutFn(
|
||||
() => {
|
||||
toast.add(reconnectingMessage)
|
||||
reconnectingToastShown.value = true
|
||||
},
|
||||
RECONNECT_TOAST_DELAY_MS,
|
||||
{ immediate: false }
|
||||
)
|
||||
|
||||
function onReconnecting() {
|
||||
if (settingStore.get('Comfy.Toast.DisableReconnectingToast')) return
|
||||
start()
|
||||
}
|
||||
|
||||
function onReconnected() {
|
||||
stop()
|
||||
|
||||
if (reconnectingToastShown.value) {
|
||||
toast.remove(reconnectingMessage)
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: t('g.reconnected'),
|
||||
life: 2000
|
||||
})
|
||||
reconnectingToastShown.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return { onReconnecting, onReconnected }
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { render, screen } from '@testing-library/vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
@@ -17,8 +17,8 @@ const i18n = createI18n({
|
||||
}
|
||||
})
|
||||
|
||||
function mountRoleBadge(role: 'owner' | 'member') {
|
||||
return mount(RoleBadge, {
|
||||
function renderRoleBadge(role: 'owner' | 'member') {
|
||||
return render(RoleBadge, {
|
||||
props: { role },
|
||||
global: { plugins: [i18n] }
|
||||
})
|
||||
@@ -26,12 +26,12 @@ function mountRoleBadge(role: 'owner' | 'member') {
|
||||
|
||||
describe('RoleBadge', () => {
|
||||
it('renders the owner label', () => {
|
||||
const wrapper = mountRoleBadge('owner')
|
||||
expect(wrapper.text()).toBe('Owner')
|
||||
renderRoleBadge('owner')
|
||||
expect(screen.getByText('Owner')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders the member label', () => {
|
||||
const wrapper = mountRoleBadge('member')
|
||||
expect(wrapper.text()).toBe('Member')
|
||||
renderRoleBadge('member')
|
||||
expect(screen.getByText('Member')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { render, screen } from '@testing-library/vue'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ref } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import LinearWelcome from './LinearWelcome.vue'
|
||||
|
||||
const hasNodes = ref(false)
|
||||
const hasOutputs = ref(false)
|
||||
const enterBuilder = vi.fn()
|
||||
const { hasNodes, hasOutputs, enterBuilder } = vi.hoisted(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const { ref } = require('vue')
|
||||
return {
|
||||
hasNodes: ref(false),
|
||||
hasOutputs: ref(false),
|
||||
enterBuilder: vi.fn()
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/composables/useAppMode', () => ({
|
||||
useAppMode: () => ({ setMode: vi.fn() })
|
||||
@@ -33,12 +39,12 @@ vi.mock('@/platform/workflow/management/stores/workflowStore', () => ({
|
||||
|
||||
const i18n = createI18n({ legacy: false, locale: 'en', missingWarn: false })
|
||||
|
||||
function mountComponent(
|
||||
function renderComponent(
|
||||
opts: { hasNodes?: boolean; hasOutputs?: boolean } = {}
|
||||
) {
|
||||
hasNodes.value = opts.hasNodes ?? false
|
||||
hasOutputs.value = opts.hasOutputs ?? false
|
||||
return mount(LinearWelcome, {
|
||||
return render(LinearWelcome, {
|
||||
global: { plugins: [i18n] }
|
||||
})
|
||||
}
|
||||
@@ -51,30 +57,27 @@ describe('LinearWelcome', () => {
|
||||
})
|
||||
|
||||
it('shows empty workflow text when there are no nodes', () => {
|
||||
const wrapper = mountComponent({ hasNodes: false })
|
||||
renderComponent({ hasNodes: false })
|
||||
expect(
|
||||
wrapper.find('[data-testid="linear-welcome-empty-workflow"]').exists()
|
||||
).toBe(true)
|
||||
screen.getByTestId('linear-welcome-empty-workflow')
|
||||
).toBeInTheDocument()
|
||||
expect(
|
||||
wrapper.find('[data-testid="linear-welcome-build-app"]').exists()
|
||||
).toBe(false)
|
||||
screen.queryByTestId('linear-welcome-build-app')
|
||||
).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows build app button when there are nodes but no outputs', () => {
|
||||
const wrapper = mountComponent({ hasNodes: true, hasOutputs: false })
|
||||
renderComponent({ hasNodes: true, hasOutputs: false })
|
||||
expect(
|
||||
wrapper.find('[data-testid="linear-welcome-empty-workflow"]').exists()
|
||||
).toBe(false)
|
||||
expect(
|
||||
wrapper.find('[data-testid="linear-welcome-build-app"]').exists()
|
||||
).toBe(true)
|
||||
screen.queryByTestId('linear-welcome-empty-workflow')
|
||||
).not.toBeInTheDocument()
|
||||
expect(screen.getByTestId('linear-welcome-build-app')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('clicking build app button calls enterBuilder', async () => {
|
||||
const wrapper = mountComponent({ hasNodes: true, hasOutputs: false })
|
||||
await wrapper
|
||||
.find('[data-testid="linear-welcome-build-app"]')
|
||||
.trigger('click')
|
||||
const user = userEvent.setup()
|
||||
renderComponent({ hasNodes: true, hasOutputs: false })
|
||||
await user.click(screen.getByTestId('linear-welcome-build-app'))
|
||||
expect(enterBuilder).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
/* eslint-disable testing-library/no-container */
|
||||
/* eslint-disable testing-library/no-node-access */
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { fromAny } from '@total-typescript/shoehorn'
|
||||
import { render } from '@testing-library/vue'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { nextTick } from 'vue'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
@@ -13,7 +11,6 @@ import type {
|
||||
VueNodeData
|
||||
} from '@/composables/graph/useGraphNodeManager'
|
||||
import NodeWidgets from '@/renderer/extensions/vueNodes/components/NodeWidgets.vue'
|
||||
import { usePromotionStore } from '@/stores/promotionStore'
|
||||
import { useWidgetValueStore } from '@/stores/widgetValueStore'
|
||||
|
||||
vi.mock('@/renderer/core/canvas/canvasStore', () => ({
|
||||
@@ -98,35 +95,6 @@ describe('NodeWidgets', () => {
|
||||
})
|
||||
}
|
||||
|
||||
function mountComponent(nodeData?: VueNodeData, setupStores?: () => void) {
|
||||
const pinia = createTestingPinia({ stubActions: false })
|
||||
setActivePinia(pinia)
|
||||
setupStores?.()
|
||||
|
||||
return mount(NodeWidgets, {
|
||||
props: { nodeData },
|
||||
global: {
|
||||
plugins: [pinia],
|
||||
stubs: { InputSlot: true },
|
||||
mocks: { $t: (key: string) => key }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const getBorderStyles = (wrapper: ReturnType<typeof mount>) =>
|
||||
fromAny<{ processedWidgets: unknown[] }, unknown>(
|
||||
wrapper.vm
|
||||
).processedWidgets.map(
|
||||
(entry) =>
|
||||
(
|
||||
entry as {
|
||||
simplified: {
|
||||
borderStyle?: string
|
||||
}
|
||||
}
|
||||
).simplified.borderStyle
|
||||
)
|
||||
|
||||
describe('node-type prop passing', () => {
|
||||
it('passes node type to widget components', () => {
|
||||
const widget = createMockWidget()
|
||||
@@ -155,19 +123,6 @@ describe('NodeWidgets', () => {
|
||||
expect(stub).not.toBeNull()
|
||||
expect(stub!.getAttribute('data-node-type')).toBe('')
|
||||
})
|
||||
|
||||
it.for(['CheckpointLoaderSimple', 'LoraLoader', 'VAELoader', 'KSampler'])(
|
||||
'passes correct node type: %s',
|
||||
(nodeType) => {
|
||||
const widget = createMockWidget()
|
||||
const nodeData = createMockNodeData(nodeType, [widget])
|
||||
const { container } = renderComponent(nodeData)
|
||||
|
||||
const stub = container.querySelector('.widget-stub')
|
||||
expect(stub).not.toBeNull()
|
||||
expect(stub!.getAttribute('data-node-type')).toBe(nodeType)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('deduplicates widgets with identical render identity while keeping distinct promoted sources', () => {
|
||||
@@ -318,54 +273,6 @@ describe('NodeWidgets', () => {
|
||||
expect(container.querySelectorAll('.lg-node-widget')).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('applies promoted border styling to intermediate promoted widgets using host node identity', async () => {
|
||||
const promotedWidget = createMockWidget({
|
||||
name: 'text',
|
||||
type: 'combo',
|
||||
nodeId: 'inner-subgraph:1',
|
||||
storeNodeId: 'inner-subgraph:1',
|
||||
storeName: 'text',
|
||||
slotName: 'text'
|
||||
})
|
||||
const nodeData = createMockNodeData('SubgraphNode', [promotedWidget], '3')
|
||||
const wrapper = mountComponent(nodeData, () => {
|
||||
usePromotionStore().promote('graph-test', '4', {
|
||||
sourceNodeId: '3',
|
||||
sourceWidgetName: 'text',
|
||||
disambiguatingSourceNodeId: '1'
|
||||
})
|
||||
})
|
||||
await nextTick()
|
||||
const borderStyles = getBorderStyles(wrapper)
|
||||
|
||||
expect(borderStyles.some((style) => style?.includes('promoted'))).toBe(true)
|
||||
})
|
||||
|
||||
it('does not apply promoted border styling to outermost widgets', async () => {
|
||||
const promotedWidget = createMockWidget({
|
||||
name: 'text',
|
||||
type: 'combo',
|
||||
nodeId: 'inner-subgraph:1',
|
||||
storeNodeId: 'inner-subgraph:1',
|
||||
storeName: 'text',
|
||||
slotName: 'text'
|
||||
})
|
||||
const nodeData = createMockNodeData('SubgraphNode', [promotedWidget], '4')
|
||||
const wrapper = mountComponent(nodeData, () => {
|
||||
usePromotionStore().promote('graph-test', '4', {
|
||||
sourceNodeId: '3',
|
||||
sourceWidgetName: 'text',
|
||||
disambiguatingSourceNodeId: '1'
|
||||
})
|
||||
})
|
||||
await nextTick()
|
||||
const borderStyles = getBorderStyles(wrapper)
|
||||
|
||||
expect(borderStyles.some((style) => style?.includes('promoted'))).toBe(
|
||||
false
|
||||
)
|
||||
})
|
||||
|
||||
it('hides widgets when merged store options mark them hidden', async () => {
|
||||
const nodeData = createMockNodeData('TestNode', [
|
||||
createMockWidget({
|
||||
|
||||
@@ -80,56 +80,16 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { TooltipOptions } from 'primevue'
|
||||
import { computed, onErrorCaptured, ref, toValue } from 'vue'
|
||||
import type { Component } from 'vue'
|
||||
import { onErrorCaptured, ref } from 'vue'
|
||||
|
||||
import type {
|
||||
SafeWidgetData,
|
||||
VueNodeData,
|
||||
WidgetSlotMetadata
|
||||
} from '@/composables/graph/useGraphNodeManager'
|
||||
import { useAppMode } from '@/composables/useAppMode'
|
||||
import { showNodeOptions } from '@/composables/graph/useMoreOptionsMenu'
|
||||
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import { st } from '@/i18n'
|
||||
import type { IWidgetOptions } from '@/lib/litegraph/src/types/widgets'
|
||||
import { LGraphEventMode } from '@/lib/litegraph/src/types/globalEnums'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import AppInput from '@/renderer/extensions/linearMode/AppInput.vue'
|
||||
import { useNodeTooltips } from '@/renderer/extensions/vueNodes/composables/useNodeTooltips'
|
||||
import { useNodeEventHandlers } from '@/renderer/extensions/vueNodes/composables/useNodeEventHandlers'
|
||||
import { useNodeZIndex } from '@/renderer/extensions/vueNodes/composables/useNodeZIndex'
|
||||
import WidgetDOM from '@/renderer/extensions/vueNodes/widgets/components/WidgetDOM.vue'
|
||||
// Import widget components directly
|
||||
import WidgetLegacy from '@/renderer/extensions/vueNodes/widgets/components/WidgetLegacy.vue'
|
||||
import {
|
||||
getComponent,
|
||||
shouldExpand,
|
||||
shouldRenderAsVue
|
||||
} from '@/renderer/extensions/vueNodes/widgets/registry/widgetRegistry'
|
||||
import { nodeTypeValidForApp } from '@/stores/appModeStore'
|
||||
import type { WidgetState } from '@/stores/widgetValueStore'
|
||||
import {
|
||||
stripGraphPrefix,
|
||||
useWidgetValueStore
|
||||
} from '@/stores/widgetValueStore'
|
||||
import { usePromotionStore } from '@/stores/promotionStore'
|
||||
import { useMissingModelStore } from '@/platform/missingModel/missingModelStore'
|
||||
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
|
||||
import type {
|
||||
LinkedUpstreamInfo,
|
||||
SimplifiedWidget,
|
||||
WidgetValue
|
||||
} from '@/types/simplifiedWidget'
|
||||
import { useProcessedWidgets } from '@/renderer/extensions/vueNodes/composables/useProcessedWidgets'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
import {
|
||||
getExecutionIdFromNodeData,
|
||||
getLocatorIdFromNodeData
|
||||
} from '@/utils/graphTraversalUtil'
|
||||
import { app } from '@/scripts/app'
|
||||
|
||||
import InputSlot from './InputSlot.vue'
|
||||
|
||||
@@ -141,12 +101,7 @@ const { nodeData } = defineProps<NodeWidgetsProps>()
|
||||
|
||||
const { shouldHandleNodePointerEvents, forwardEventToCanvas } =
|
||||
useCanvasInteractions()
|
||||
const { isSelectInputsMode } = useAppMode()
|
||||
const canvasStore = useCanvasStore()
|
||||
const { bringNodeToFront } = useNodeZIndex()
|
||||
const promotionStore = usePromotionStore()
|
||||
const executionErrorStore = useExecutionErrorStore()
|
||||
const missingModelStore = useMissingModelStore()
|
||||
|
||||
function handleWidgetPointerEvent(event: PointerEvent) {
|
||||
if (shouldHandleNodePointerEvents.value) return
|
||||
@@ -160,8 +115,6 @@ function handleBringToFront() {
|
||||
}
|
||||
}
|
||||
|
||||
const { handleNodeRightClick } = useNodeEventHandlers()
|
||||
|
||||
// Error boundary implementation
|
||||
const renderError = ref<string | null>(null)
|
||||
|
||||
@@ -173,314 +126,11 @@ onErrorCaptured((error) => {
|
||||
return false
|
||||
})
|
||||
|
||||
const canSelectInputs = computed(
|
||||
() =>
|
||||
isSelectInputsMode.value &&
|
||||
nodeData?.mode === LGraphEventMode.ALWAYS &&
|
||||
nodeTypeValidForApp(nodeData.type) &&
|
||||
!nodeData.hasErrors
|
||||
)
|
||||
const nodeType = computed(() => nodeData?.type || '')
|
||||
const settingStore = useSettingStore()
|
||||
const showAdvanced = computed(
|
||||
() =>
|
||||
nodeData?.showAdvanced ||
|
||||
settingStore.get('Comfy.Node.AlwaysShowAdvancedWidgets')
|
||||
)
|
||||
const { getWidgetTooltip, createTooltipConfig } = useNodeTooltips(
|
||||
nodeType.value
|
||||
)
|
||||
const widgetValueStore = useWidgetValueStore()
|
||||
|
||||
function createWidgetUpdateHandler(
|
||||
widgetState: WidgetState | undefined,
|
||||
widget: SafeWidgetData,
|
||||
nodeExecId: string,
|
||||
widgetOptions: IWidgetOptions | Record<string, never>
|
||||
): (newValue: WidgetValue) => void {
|
||||
return (newValue: WidgetValue) => {
|
||||
if (widgetState) widgetState.value = newValue
|
||||
widget.callback?.(newValue)
|
||||
const effectiveExecId = widget.sourceExecutionId ?? nodeExecId
|
||||
executionErrorStore.clearWidgetRelatedErrors(
|
||||
effectiveExecId,
|
||||
widget.slotName ?? widget.name,
|
||||
widget.name,
|
||||
newValue,
|
||||
{ min: widgetOptions?.min, max: widgetOptions?.max }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
interface ProcessedWidget {
|
||||
advanced: boolean
|
||||
handleContextMenu: (e: PointerEvent) => void
|
||||
hasLayoutSize: boolean
|
||||
hasError: boolean
|
||||
hidden: boolean
|
||||
id: string
|
||||
name: string
|
||||
renderKey: string
|
||||
simplified: SimplifiedWidget
|
||||
tooltipConfig: TooltipOptions
|
||||
type: string
|
||||
updateHandler: (value: WidgetValue) => void
|
||||
value: WidgetValue
|
||||
vueComponent: Component
|
||||
slotMetadata?: WidgetSlotMetadata
|
||||
}
|
||||
|
||||
function hasWidgetError(
|
||||
widget: SafeWidgetData,
|
||||
nodeExecId: string,
|
||||
nodeErrors: { errors: { extra_info?: { input_name?: string } }[] } | undefined
|
||||
): boolean {
|
||||
const errors = widget.sourceExecutionId
|
||||
? executionErrorStore.lastNodeErrors?.[widget.sourceExecutionId]?.errors
|
||||
: nodeErrors?.errors
|
||||
const inputName = widget.slotName ?? widget.name
|
||||
return (
|
||||
!!errors?.some((e) => e.extra_info?.input_name === inputName) ||
|
||||
missingModelStore.isWidgetMissingModel(
|
||||
widget.sourceExecutionId ?? nodeExecId,
|
||||
widget.name
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
function getWidgetIdentity(
|
||||
widget: SafeWidgetData,
|
||||
nodeId: string | number | undefined,
|
||||
index: number
|
||||
): {
|
||||
dedupeIdentity?: string
|
||||
renderKey: string
|
||||
} {
|
||||
const rawWidgetId = widget.storeNodeId ?? widget.nodeId
|
||||
const storeWidgetName = widget.storeName ?? widget.name
|
||||
const slotNameForIdentity = widget.slotName ?? widget.name
|
||||
const stableIdentityRoot = rawWidgetId
|
||||
? `node:${String(stripGraphPrefix(rawWidgetId))}`
|
||||
: widget.sourceExecutionId
|
||||
? `exec:${widget.sourceExecutionId}`
|
||||
: undefined
|
||||
|
||||
const dedupeIdentity = stableIdentityRoot
|
||||
? `${stableIdentityRoot}:${storeWidgetName}:${slotNameForIdentity}:${widget.type}`
|
||||
: undefined
|
||||
const renderKey =
|
||||
dedupeIdentity ??
|
||||
`transient:${String(nodeId ?? '')}:${storeWidgetName}:${slotNameForIdentity}:${widget.type}:${index}`
|
||||
|
||||
return {
|
||||
dedupeIdentity,
|
||||
renderKey
|
||||
}
|
||||
}
|
||||
|
||||
function isWidgetVisible(options: IWidgetOptions): boolean {
|
||||
const hidden = options.hidden ?? false
|
||||
const advanced = options.advanced ?? false
|
||||
return !hidden && (!advanced || showAdvanced.value)
|
||||
}
|
||||
|
||||
const processedWidgets = computed((): ProcessedWidget[] => {
|
||||
if (!nodeData?.widgets) return []
|
||||
|
||||
// nodeData.id is the local node ID; subgraph nodes need the full execution
|
||||
// path (e.g. "65:63") to match keys in lastNodeErrors.
|
||||
const nodeExecId = app.isGraphReady
|
||||
? getExecutionIdFromNodeData(app.rootGraph, nodeData)
|
||||
: String(nodeData.id ?? '')
|
||||
|
||||
const nodeErrors = executionErrorStore.lastNodeErrors?.[nodeExecId]
|
||||
const graphId = canvasStore.canvas?.graph?.rootGraph.id
|
||||
|
||||
const nodeId = nodeData.id
|
||||
const { widgets } = nodeData
|
||||
const result: ProcessedWidget[] = []
|
||||
const uniqueWidgets: Array<{
|
||||
widget: SafeWidgetData
|
||||
identity: ReturnType<typeof getWidgetIdentity>
|
||||
mergedOptions: IWidgetOptions
|
||||
widgetState: WidgetState | undefined
|
||||
isVisible: boolean
|
||||
}> = []
|
||||
const dedupeIndexByIdentity = new Map<string, number>()
|
||||
|
||||
for (const [index, widget] of widgets.entries()) {
|
||||
if (!shouldRenderAsVue(widget)) continue
|
||||
|
||||
const identity = getWidgetIdentity(widget, nodeId, index)
|
||||
const storeWidgetName = widget.storeName ?? widget.name
|
||||
const bareWidgetId = String(
|
||||
stripGraphPrefix(widget.storeNodeId ?? widget.nodeId ?? nodeId ?? '')
|
||||
)
|
||||
const widgetState = graphId
|
||||
? widgetValueStore.getWidget(graphId, bareWidgetId, storeWidgetName)
|
||||
: undefined
|
||||
const mergedOptions: IWidgetOptions = {
|
||||
...(widget.options ?? {}),
|
||||
...(widgetState?.options ?? {})
|
||||
}
|
||||
const visible = isWidgetVisible(mergedOptions)
|
||||
if (!identity.dedupeIdentity) {
|
||||
uniqueWidgets.push({
|
||||
widget,
|
||||
identity,
|
||||
mergedOptions,
|
||||
widgetState,
|
||||
isVisible: visible
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
const existingIndex = dedupeIndexByIdentity.get(identity.dedupeIdentity)
|
||||
if (existingIndex === undefined) {
|
||||
dedupeIndexByIdentity.set(identity.dedupeIdentity, uniqueWidgets.length)
|
||||
uniqueWidgets.push({
|
||||
widget,
|
||||
identity,
|
||||
mergedOptions,
|
||||
widgetState,
|
||||
isVisible: visible
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
const existingWidget = uniqueWidgets[existingIndex]
|
||||
if (existingWidget && !existingWidget.isVisible && visible) {
|
||||
uniqueWidgets[existingIndex] = {
|
||||
widget,
|
||||
identity,
|
||||
mergedOptions,
|
||||
widgetState,
|
||||
isVisible: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const {
|
||||
widget,
|
||||
mergedOptions,
|
||||
widgetState,
|
||||
identity: { renderKey }
|
||||
} of uniqueWidgets) {
|
||||
const hostNodeId = String(nodeId ?? '')
|
||||
const bareWidgetId = String(
|
||||
stripGraphPrefix(widget.storeNodeId ?? widget.nodeId ?? nodeId ?? '')
|
||||
)
|
||||
const promotionSourceNodeId = widget.storeName
|
||||
? String(bareWidgetId)
|
||||
: undefined
|
||||
|
||||
const vueComponent =
|
||||
getComponent(widget.type) ||
|
||||
(widget.isDOMWidget ? WidgetDOM : WidgetLegacy)
|
||||
|
||||
const { slotMetadata } = widget
|
||||
|
||||
// Get value from store (falls back to undefined if not registered)
|
||||
const value = widgetState?.value as WidgetValue
|
||||
|
||||
// Build options from store state, with disabled override for
|
||||
// slot-linked widgets or widgets with disabled state (e.g. display-only)
|
||||
const isDisabled = slotMetadata?.linked || widgetState?.disabled
|
||||
const widgetOptions = isDisabled
|
||||
? { ...mergedOptions, disabled: true }
|
||||
: mergedOptions
|
||||
|
||||
const borderStyle =
|
||||
graphId &&
|
||||
promotionStore.isPromotedByAny(graphId, {
|
||||
sourceNodeId: hostNodeId,
|
||||
sourceWidgetName: widget.storeName ?? widget.name,
|
||||
disambiguatingSourceNodeId: promotionSourceNodeId
|
||||
})
|
||||
? 'ring ring-component-node-widget-promoted'
|
||||
: mergedOptions.advanced
|
||||
? 'ring ring-component-node-widget-advanced'
|
||||
: undefined
|
||||
|
||||
const linkedUpstream: LinkedUpstreamInfo | undefined =
|
||||
slotMetadata?.linked && slotMetadata.originNodeId
|
||||
? {
|
||||
nodeId: slotMetadata.originNodeId,
|
||||
outputName: slotMetadata.originOutputName
|
||||
}
|
||||
: undefined
|
||||
|
||||
const nodeLocatorId = widget.nodeId
|
||||
? widget.nodeId
|
||||
: nodeData
|
||||
? getLocatorIdFromNodeData(nodeData)
|
||||
: undefined
|
||||
|
||||
const simplified: SimplifiedWidget = {
|
||||
name: widget.name,
|
||||
type: widget.type,
|
||||
value,
|
||||
borderStyle,
|
||||
callback: widget.callback,
|
||||
controlWidget: widget.controlWidget,
|
||||
label: widget.promotedLabel ?? widgetState?.label,
|
||||
linkedUpstream,
|
||||
nodeLocatorId,
|
||||
options: widgetOptions,
|
||||
spec: widget.spec
|
||||
}
|
||||
|
||||
const updateHandler = createWidgetUpdateHandler(
|
||||
widgetState,
|
||||
widget,
|
||||
nodeExecId,
|
||||
widgetOptions
|
||||
)
|
||||
|
||||
const tooltipText = getWidgetTooltip(widget)
|
||||
const tooltipConfig = createTooltipConfig(tooltipText)
|
||||
const handleContextMenu = (e: PointerEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
handleNodeRightClick(e, nodeId)
|
||||
showNodeOptions(
|
||||
e,
|
||||
widget.name,
|
||||
widget.nodeId !== undefined
|
||||
? String(stripGraphPrefix(widget.nodeId))
|
||||
: undefined
|
||||
)
|
||||
}
|
||||
|
||||
result.push({
|
||||
advanced: mergedOptions.advanced ?? false,
|
||||
handleContextMenu,
|
||||
hasLayoutSize: widget.hasLayoutSize ?? false,
|
||||
hasError: hasWidgetError(widget, nodeExecId, nodeErrors),
|
||||
hidden: mergedOptions.hidden ?? false,
|
||||
id: String(bareWidgetId),
|
||||
name: widget.name,
|
||||
renderKey,
|
||||
type: widget.type,
|
||||
vueComponent,
|
||||
simplified,
|
||||
value,
|
||||
updateHandler,
|
||||
tooltipConfig,
|
||||
slotMetadata
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
const gridTemplateRows = computed((): string => {
|
||||
// Use processedWidgets directly since it already has store-based hidden/advanced
|
||||
return toValue(processedWidgets)
|
||||
.filter((w) => !w.hidden && (!w.advanced || showAdvanced.value))
|
||||
.map((w) =>
|
||||
shouldExpand(w.type) || w.hasLayoutSize ? 'auto' : 'min-content'
|
||||
)
|
||||
.join(' ')
|
||||
})
|
||||
const {
|
||||
canSelectInputs,
|
||||
gridTemplateRows,
|
||||
nodeType,
|
||||
processedWidgets,
|
||||
showAdvanced
|
||||
} = useProcessedWidgets(() => nodeData)
|
||||
</script>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user