mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-11 14:50:41 +00:00
Compare commits
14 Commits
test/cover
...
test/image
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f698448bba | ||
|
|
d6e58b67fe | ||
|
|
3043b181d7 | ||
|
|
8c9328c1b2 | ||
|
|
577f373cde | ||
|
|
44f88027b6 | ||
|
|
5d07de1913 | ||
|
|
f0ae91de43 | ||
|
|
fb8025c49f | ||
|
|
9bdeaa663c | ||
|
|
337f95c81d | ||
|
|
d887dcb5ed | ||
|
|
40f311faf1 | ||
|
|
afdd0af8b3 |
@@ -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({
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { expect } from '@playwright/test'
|
||||
import type { Locator, Page } from '@playwright/test'
|
||||
|
||||
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
|
||||
import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '@e2e/fixtures/ComfyPage'
|
||||
|
||||
test.describe('Image Compare', () => {
|
||||
test.describe('Image Compare', { tag: '@widget' }, () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
|
||||
await comfyPage.workflow.loadWorkflow('widgets/image_compare_widget')
|
||||
@@ -21,7 +24,12 @@ test.describe('Image Compare', () => {
|
||||
|
||||
async function setImageCompareValue(
|
||||
comfyPage: ComfyPage,
|
||||
value: { beforeImages: string[]; afterImages: string[] }
|
||||
value: {
|
||||
beforeImages: string[]
|
||||
afterImages: string[]
|
||||
beforeAlt?: string
|
||||
afterAlt?: string
|
||||
}
|
||||
) {
|
||||
await comfyPage.page.evaluate(
|
||||
({ value }) => {
|
||||
@@ -37,6 +45,48 @@ test.describe('Image Compare', () => {
|
||||
await comfyPage.nextFrame()
|
||||
}
|
||||
|
||||
async function moveToPercentage(
|
||||
page: Page,
|
||||
containerLocator: Locator,
|
||||
percentage: number
|
||||
) {
|
||||
const box = await containerLocator.boundingBox()
|
||||
if (!box) throw new Error('Container not found')
|
||||
await page.mouse.move(
|
||||
box.x + box.width * (percentage / 100),
|
||||
box.y + box.height / 2
|
||||
)
|
||||
}
|
||||
|
||||
async function waitForImagesLoaded(node: Locator) {
|
||||
await expect
|
||||
.poll(() =>
|
||||
node.evaluate((el) => {
|
||||
const imgs = el.querySelectorAll('img')
|
||||
return (
|
||||
imgs.length > 0 &&
|
||||
Array.from(imgs).every(
|
||||
(img) => img.complete && img.naturalWidth > 0
|
||||
)
|
||||
)
|
||||
})
|
||||
)
|
||||
.toBe(true)
|
||||
}
|
||||
|
||||
async function getClipPathInsetRightPercent(imgLocator: Locator) {
|
||||
return imgLocator.evaluate((el) => {
|
||||
// Accessing raw style avoids cross-browser getComputedStyle normalization issues
|
||||
// Format is uniformly "inset(0 60% 0 0)" per Vue runtime inline style bindings
|
||||
const parts = (el as HTMLElement).style.clipPath.split(' ')
|
||||
return parts.length > 1 ? parseFloat(parts[1]) : -1
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Rendering
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test(
|
||||
'Shows empty state when no images are set',
|
||||
{ tag: '@smoke' },
|
||||
@@ -46,10 +96,14 @@ 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)
|
||||
}
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Slider defaults
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test(
|
||||
'Slider defaults to 50% with both images set',
|
||||
{ tag: ['@smoke', '@screenshot'] },
|
||||
@@ -67,15 +121,444 @@ 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(
|
||||
await handle.evaluate((el) => (el as HTMLElement).style.left)
|
||||
await handle.evaluate((el) => (el as HTMLElement).style.left),
|
||||
'Slider should default to 50% before screenshot'
|
||||
).toBe('50%')
|
||||
await expect(beforeImg).toHaveCSS('clip-path', /50%/)
|
||||
await expect
|
||||
.poll(() => getClipPathInsetRightPercent(beforeImg))
|
||||
.toBeCloseTo(50, 0)
|
||||
|
||||
await waitForImagesLoaded(node)
|
||||
await comfyPage.page.mouse.move(0, 0)
|
||||
await expect(node).toHaveScreenshot('image-compare-default-50.png')
|
||||
}
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Slider interaction
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test(
|
||||
'Mouse hover moves slider position',
|
||||
{ tag: '@smoke' },
|
||||
async ({ comfyPage }) => {
|
||||
const beforeUrl = createTestImageDataUrl('Before', '#c00')
|
||||
const afterUrl = createTestImageDataUrl('After', '#00c')
|
||||
await setImageCompareValue(comfyPage, {
|
||||
beforeImages: [beforeUrl],
|
||||
afterImages: [afterUrl]
|
||||
})
|
||||
|
||||
const node = comfyPage.vueNodes.getNodeLocator('1')
|
||||
const handle = node.getByRole('presentation')
|
||||
const beforeImg = node.locator('img[alt="Before image"]')
|
||||
const afterImg = node.locator('img[alt="After image"]')
|
||||
await expect(afterImg).toBeVisible()
|
||||
|
||||
// Left edge: sliderPosition ≈ 5 → clip-path inset right ≈ 95%
|
||||
await moveToPercentage(comfyPage.page, afterImg, 5)
|
||||
await expect
|
||||
.poll(() => getClipPathInsetRightPercent(beforeImg))
|
||||
.toBeGreaterThan(90)
|
||||
await expect
|
||||
.poll(() =>
|
||||
handle.evaluate((el) => parseFloat((el as HTMLElement).style.left))
|
||||
)
|
||||
.toBeLessThan(10)
|
||||
|
||||
// Right edge: sliderPosition ≈ 95 → clip-path inset right ≈ 5%
|
||||
await moveToPercentage(comfyPage.page, afterImg, 95)
|
||||
await expect
|
||||
.poll(() => getClipPathInsetRightPercent(beforeImg))
|
||||
.toBeLessThan(10)
|
||||
await expect
|
||||
.poll(() =>
|
||||
handle.evaluate((el) => parseFloat((el as HTMLElement).style.left))
|
||||
)
|
||||
.toBeGreaterThan(90)
|
||||
}
|
||||
)
|
||||
|
||||
test('Slider preserves last position when mouse leaves widget', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const beforeUrl = createTestImageDataUrl('Before', '#c00')
|
||||
const afterUrl = createTestImageDataUrl('After', '#00c')
|
||||
await setImageCompareValue(comfyPage, {
|
||||
beforeImages: [beforeUrl],
|
||||
afterImages: [afterUrl]
|
||||
})
|
||||
|
||||
const node = comfyPage.vueNodes.getNodeLocator('1')
|
||||
const handle = node.getByRole('presentation')
|
||||
const afterImg = node.locator('img[alt="After image"]')
|
||||
await expect(afterImg).toBeVisible()
|
||||
|
||||
await moveToPercentage(comfyPage.page, afterImg, 30)
|
||||
// Wait for Vue to commit the slider update
|
||||
await expect
|
||||
.poll(() =>
|
||||
handle.evaluate((el) => parseFloat((el as HTMLElement).style.left))
|
||||
)
|
||||
.toBeCloseTo(30, 0)
|
||||
const positionWhileInside = parseFloat(
|
||||
await handle.evaluate((el) => (el as HTMLElement).style.left)
|
||||
)
|
||||
|
||||
await comfyPage.page.mouse.move(0, 0)
|
||||
|
||||
// Position must not reset to default 50%
|
||||
await expect
|
||||
.poll(() =>
|
||||
handle.evaluate((el) => parseFloat((el as HTMLElement).style.left))
|
||||
)
|
||||
.toBeCloseTo(positionWhileInside, 0)
|
||||
})
|
||||
|
||||
test('Slider clamps to 0% at left edge of container', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const beforeUrl = createTestImageDataUrl('Before', '#c00')
|
||||
const afterUrl = createTestImageDataUrl('After', '#00c')
|
||||
await setImageCompareValue(comfyPage, {
|
||||
beforeImages: [beforeUrl],
|
||||
afterImages: [afterUrl]
|
||||
})
|
||||
|
||||
const node = comfyPage.vueNodes.getNodeLocator('1')
|
||||
const handle = node.getByRole('presentation')
|
||||
const afterImg = node.locator('img[alt="After image"]')
|
||||
await expect(afterImg).toBeVisible()
|
||||
|
||||
const box = await afterImg.boundingBox()
|
||||
if (!box) throw new Error('Container not found')
|
||||
|
||||
// Move to the leftmost pixel (elementX = 0 → sliderPosition = 0)
|
||||
await comfyPage.page.mouse.move(box.x, box.y + box.height / 2)
|
||||
await expect
|
||||
.poll(() => handle.evaluate((el) => (el as HTMLElement).style.left))
|
||||
.toBe('0%')
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Single image modes
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test('Only before image shows without slider when afterImages is empty', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const url = createTestImageDataUrl('Before', '#c00')
|
||||
await setImageCompareValue(comfyPage, {
|
||||
beforeImages: [url],
|
||||
afterImages: []
|
||||
})
|
||||
|
||||
const node = comfyPage.vueNodes.getNodeLocator('1')
|
||||
await expect(node.locator('img')).toHaveCount(1)
|
||||
await expect(node.getByRole('presentation')).toBeHidden()
|
||||
})
|
||||
|
||||
test('Only after image shows without slider when beforeImages is empty', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const url = createTestImageDataUrl('After', '#00c')
|
||||
await setImageCompareValue(comfyPage, {
|
||||
beforeImages: [],
|
||||
afterImages: [url]
|
||||
})
|
||||
|
||||
const node = comfyPage.vueNodes.getNodeLocator('1')
|
||||
await expect(node.locator('img')).toHaveCount(1)
|
||||
await expect(node.getByRole('presentation')).toBeHidden()
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Batch navigation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test(
|
||||
'Batch navigation appears when before side has multiple images',
|
||||
{ tag: '@smoke' },
|
||||
async ({ comfyPage }) => {
|
||||
const url1 = createTestImageDataUrl('A1', '#c00')
|
||||
const url2 = createTestImageDataUrl('A2', '#0c0')
|
||||
const url3 = createTestImageDataUrl('A3', '#00c')
|
||||
const afterUrl = createTestImageDataUrl('B1', '#888')
|
||||
await setImageCompareValue(comfyPage, {
|
||||
beforeImages: [url1, url2, url3],
|
||||
afterImages: [afterUrl]
|
||||
})
|
||||
|
||||
const node = comfyPage.vueNodes.getNodeLocator('1')
|
||||
const beforeBatch = node.getByTestId('before-batch')
|
||||
|
||||
await expect(node.getByTestId('batch-nav')).toBeVisible()
|
||||
await expect(beforeBatch.getByTestId('batch-counter')).toHaveText('1 / 3')
|
||||
// after-batch renders only when afterBatchCount > 1
|
||||
await expect(node.getByTestId('after-batch')).toBeHidden()
|
||||
await expect(beforeBatch.getByTestId('batch-prev')).toBeDisabled()
|
||||
}
|
||||
)
|
||||
|
||||
test('Batch navigation is hidden when both sides have single images', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const url = createTestImageDataUrl('Image', '#c00')
|
||||
await setImageCompareValue(comfyPage, {
|
||||
beforeImages: [url],
|
||||
afterImages: [url]
|
||||
})
|
||||
|
||||
const node = comfyPage.vueNodes.getNodeLocator('1')
|
||||
await expect(node.getByTestId('batch-nav')).toBeHidden()
|
||||
})
|
||||
|
||||
test(
|
||||
'Navigate forward through before images',
|
||||
{ tag: '@smoke' },
|
||||
async ({ comfyPage }) => {
|
||||
const url1 = createTestImageDataUrl('A1', '#c00')
|
||||
const url2 = createTestImageDataUrl('A2', '#0c0')
|
||||
const url3 = createTestImageDataUrl('A3', '#00c')
|
||||
await setImageCompareValue(comfyPage, {
|
||||
beforeImages: [url1, url2, url3],
|
||||
afterImages: [createTestImageDataUrl('B1', '#888')]
|
||||
})
|
||||
|
||||
const node = comfyPage.vueNodes.getNodeLocator('1')
|
||||
const beforeBatch = node.getByTestId('before-batch')
|
||||
const counter = beforeBatch.getByTestId('batch-counter')
|
||||
const nextBtn = beforeBatch.getByTestId('batch-next')
|
||||
const prevBtn = beforeBatch.getByTestId('batch-prev')
|
||||
|
||||
await nextBtn.click()
|
||||
await expect(counter).toHaveText('2 / 3')
|
||||
await expect(node.locator('img[alt="Before image"]')).toHaveAttribute(
|
||||
'src',
|
||||
url2
|
||||
)
|
||||
await expect(prevBtn).toBeEnabled()
|
||||
|
||||
await nextBtn.click()
|
||||
await expect(counter).toHaveText('3 / 3')
|
||||
await expect(nextBtn).toBeDisabled()
|
||||
}
|
||||
)
|
||||
|
||||
test('Navigate backward through before images', async ({ comfyPage }) => {
|
||||
const url1 = createTestImageDataUrl('A1', '#c00')
|
||||
const url2 = createTestImageDataUrl('A2', '#0c0')
|
||||
const url3 = createTestImageDataUrl('A3', '#00c')
|
||||
await setImageCompareValue(comfyPage, {
|
||||
beforeImages: [url1, url2, url3],
|
||||
afterImages: [createTestImageDataUrl('B1', '#888')]
|
||||
})
|
||||
|
||||
const node = comfyPage.vueNodes.getNodeLocator('1')
|
||||
const beforeBatch = node.getByTestId('before-batch')
|
||||
const counter = beforeBatch.getByTestId('batch-counter')
|
||||
const nextBtn = beforeBatch.getByTestId('batch-next')
|
||||
const prevBtn = beforeBatch.getByTestId('batch-prev')
|
||||
|
||||
await nextBtn.click()
|
||||
await nextBtn.click()
|
||||
await expect(counter).toHaveText('3 / 3')
|
||||
|
||||
await prevBtn.click()
|
||||
await expect(counter).toHaveText('2 / 3')
|
||||
await expect(prevBtn).toBeEnabled()
|
||||
await expect(nextBtn).toBeEnabled()
|
||||
})
|
||||
|
||||
test('Before and after batch navigation are independent', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const url1 = createTestImageDataUrl('A1', '#c00')
|
||||
const url2 = createTestImageDataUrl('A2', '#0c0')
|
||||
const url3 = createTestImageDataUrl('A3', '#00c')
|
||||
const urlA = createTestImageDataUrl('B1', '#880')
|
||||
const urlB = createTestImageDataUrl('B2', '#008')
|
||||
await setImageCompareValue(comfyPage, {
|
||||
beforeImages: [url1, url2, url3],
|
||||
afterImages: [urlA, urlB]
|
||||
})
|
||||
|
||||
const node = comfyPage.vueNodes.getNodeLocator('1')
|
||||
const beforeBatch = node.getByTestId('before-batch')
|
||||
const afterBatch = node.getByTestId('after-batch')
|
||||
|
||||
await beforeBatch.getByTestId('batch-next').click()
|
||||
await afterBatch.getByTestId('batch-next').click()
|
||||
|
||||
await expect(beforeBatch.getByTestId('batch-counter')).toHaveText('2 / 3')
|
||||
await expect(afterBatch.getByTestId('batch-counter')).toHaveText('2 / 2')
|
||||
await expect(node.locator('img[alt="Before image"]')).toHaveAttribute(
|
||||
'src',
|
||||
url2
|
||||
)
|
||||
await expect(node.locator('img[alt="After image"]')).toHaveAttribute(
|
||||
'src',
|
||||
urlB
|
||||
)
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Visual regression screenshots
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
for (const { pct, expectedClipMin, expectedClipMax } of [
|
||||
{ pct: 25, expectedClipMin: 70, expectedClipMax: 80 },
|
||||
{ pct: 75, expectedClipMin: 20, expectedClipMax: 30 }
|
||||
]) {
|
||||
test(
|
||||
`Screenshot at ${pct}% slider position`,
|
||||
{ tag: '@screenshot' },
|
||||
async ({ comfyPage }) => {
|
||||
const beforeUrl = createTestImageDataUrl('Before', '#c00')
|
||||
const afterUrl = createTestImageDataUrl('After', '#00c')
|
||||
await setImageCompareValue(comfyPage, {
|
||||
beforeImages: [beforeUrl],
|
||||
afterImages: [afterUrl]
|
||||
})
|
||||
|
||||
const node = comfyPage.vueNodes.getNodeLocator('1')
|
||||
const beforeImg = node.locator('img[alt="Before image"]')
|
||||
const afterImg = node.locator('img[alt="After image"]')
|
||||
await waitForImagesLoaded(node)
|
||||
await moveToPercentage(comfyPage.page, afterImg, pct)
|
||||
await expect
|
||||
.poll(() => getClipPathInsetRightPercent(beforeImg))
|
||||
.toBeGreaterThan(expectedClipMin)
|
||||
await expect
|
||||
.poll(() => getClipPathInsetRightPercent(beforeImg))
|
||||
.toBeLessThan(expectedClipMax)
|
||||
|
||||
await expect(node).toHaveScreenshot(`image-compare-slider-${pct}.png`)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Edge cases
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test('Widget remains stable with broken image URLs', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await setImageCompareValue(comfyPage, {
|
||||
beforeImages: ['https://example.invalid/broken.png'],
|
||||
afterImages: ['https://example.invalid/broken2.png']
|
||||
})
|
||||
|
||||
const node = comfyPage.vueNodes.getNodeLocator('1')
|
||||
await expect(node.locator('img')).toHaveCount(2)
|
||||
await expect(node.getByRole('presentation')).toBeVisible()
|
||||
|
||||
await expect
|
||||
.poll(() =>
|
||||
node.evaluate((el) => {
|
||||
const imgs = el.querySelectorAll('img')
|
||||
let errors = 0
|
||||
imgs.forEach((img) => {
|
||||
if (img.complete && img.naturalWidth === 0 && img.src) errors++
|
||||
})
|
||||
return errors
|
||||
})
|
||||
)
|
||||
.toBe(2)
|
||||
})
|
||||
|
||||
test('Rapid value updates show latest images and reset batch index', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const redUrl = createTestImageDataUrl('Red', '#c00')
|
||||
const green1Url = createTestImageDataUrl('G1', '#0c0')
|
||||
const green2Url = createTestImageDataUrl('G2', '#090')
|
||||
const blueUrl = createTestImageDataUrl('Blue', '#00c')
|
||||
|
||||
await setImageCompareValue(comfyPage, {
|
||||
beforeImages: [redUrl, green1Url],
|
||||
afterImages: [blueUrl]
|
||||
})
|
||||
|
||||
const node = comfyPage.vueNodes.getNodeLocator('1')
|
||||
await node.getByTestId('before-batch').getByTestId('batch-next').click()
|
||||
await expect(
|
||||
node.getByTestId('before-batch').getByTestId('batch-counter')
|
||||
).toHaveText('2 / 2')
|
||||
|
||||
await setImageCompareValue(comfyPage, {
|
||||
beforeImages: [green1Url, green2Url],
|
||||
afterImages: [blueUrl]
|
||||
})
|
||||
|
||||
await expect(node.locator('img[alt="Before image"]')).toHaveAttribute(
|
||||
'src',
|
||||
green1Url
|
||||
)
|
||||
await expect(
|
||||
node.getByTestId('before-batch').getByTestId('batch-counter')
|
||||
).toHaveText('1 / 2')
|
||||
})
|
||||
|
||||
test('Legacy string value shows single image without slider', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const url = createTestImageDataUrl('Legacy', '#c00')
|
||||
await comfyPage.page.evaluate(
|
||||
({ url }) => {
|
||||
const node = window.app!.graph.getNodeById(1)
|
||||
const widget = node?.widgets?.find((w) => w.type === 'imagecompare')
|
||||
if (widget) {
|
||||
widget.value = url
|
||||
widget.callback?.(url)
|
||||
}
|
||||
},
|
||||
{ url }
|
||||
)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const node = comfyPage.vueNodes.getNodeLocator('1')
|
||||
await expect(node.locator('img')).toHaveCount(1)
|
||||
await expect(node.getByRole('presentation')).toBeHidden()
|
||||
})
|
||||
|
||||
test('Custom beforeAlt and afterAlt are used as img alt text', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const beforeUrl = createTestImageDataUrl('Before', '#c00')
|
||||
const afterUrl = createTestImageDataUrl('After', '#00c')
|
||||
await setImageCompareValue(comfyPage, {
|
||||
beforeImages: [beforeUrl],
|
||||
afterImages: [afterUrl],
|
||||
beforeAlt: 'Custom before',
|
||||
afterAlt: 'Custom after'
|
||||
})
|
||||
|
||||
const node = comfyPage.vueNodes.getNodeLocator('1')
|
||||
await expect(node.locator('img[alt="Custom before"]')).toBeVisible()
|
||||
await expect(node.locator('img[alt="Custom after"]')).toBeVisible()
|
||||
})
|
||||
|
||||
test('Large batch sizes show correct counter', async ({ comfyPage }) => {
|
||||
const images = Array.from({ length: 20 }, (_, i) =>
|
||||
createTestImageDataUrl(String(i + 1), '#c00')
|
||||
)
|
||||
await setImageCompareValue(comfyPage, {
|
||||
beforeImages: images,
|
||||
afterImages: images
|
||||
})
|
||||
|
||||
const node = comfyPage.vueNodes.getNodeLocator('1')
|
||||
await expect(
|
||||
node.getByTestId('before-batch').getByTestId('batch-counter')
|
||||
).toHaveText('1 / 20')
|
||||
await expect(
|
||||
node.getByTestId('after-batch').getByTestId('batch-counter')
|
||||
).toHaveText('1 / 20')
|
||||
})
|
||||
})
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user