mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-13 19:20:37 +00:00
Compare commits
7 Commits
codex/clou
...
fix/1.42-d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ddaa49381d | ||
|
|
1a6ecd2304 | ||
|
|
cedeb468eb | ||
|
|
ef4386692f | ||
|
|
48ea044927 | ||
|
|
c5acfb0fd7 | ||
|
|
78daab33f0 |
@@ -84,7 +84,6 @@
|
||||
"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",
|
||||
@@ -117,60 +116,13 @@
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"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": "error",
|
||||
"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"
|
||||
"unicorn/no-empty-file": "error"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
{
|
||||
"last_node_id": 1,
|
||||
"last_link_id": 0,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "ImageCropV2",
|
||||
"pos": [50, 50],
|
||||
"size": [400, 500],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ImageCropV2"
|
||||
},
|
||||
"widgets_values": [
|
||||
{
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"width": 512,
|
||||
"height": 512
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
{
|
||||
"last_node_id": 3,
|
||||
"last_link_id": 2,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "LoadImage",
|
||||
"pos": [50, 50],
|
||||
"size": [315, 314],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [1]
|
||||
},
|
||||
{
|
||||
"name": "MASK",
|
||||
"type": "MASK",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LoadImage"
|
||||
},
|
||||
"widgets_values": ["example.png", "image"]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "ImageCropV2",
|
||||
"pos": [450, 50],
|
||||
"size": [400, 500],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"link": 1
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [2]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ImageCropV2"
|
||||
},
|
||||
"widgets_values": [
|
||||
{
|
||||
"x": 10,
|
||||
"y": 10,
|
||||
"width": 100,
|
||||
"height": 100
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "PreviewImage",
|
||||
"pos": [900, 50],
|
||||
"size": [315, 270],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 2
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"Node name for S&R": "PreviewImage"
|
||||
},
|
||||
"widgets_values": []
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[1, 1, 0, 2, 0, "IMAGE"],
|
||||
[2, 2, 0, 3, 0, "IMAGE"]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -321,7 +321,7 @@ export class ComfyPage {
|
||||
// window.app.extensionManager => GraphView ready
|
||||
window.app && window.app.extensionManager
|
||||
)
|
||||
await this.page.locator('.p-blockui-mask').waitFor({ state: 'hidden' })
|
||||
await this.page.waitForSelector('.p-blockui-mask', { state: 'hidden' })
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
@@ -371,7 +371,7 @@ export class ComfyPage {
|
||||
}
|
||||
|
||||
async closeMenu() {
|
||||
await this.page.locator('button.comfy-close-menu-btn').click()
|
||||
await this.page.click('button.comfy-close-menu-btn')
|
||||
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.getByTestId('node-title').filter({ hasText: title })
|
||||
has: this.page.locator('[data-testid="node-title"]', { hasText: title })
|
||||
})
|
||||
}
|
||||
|
||||
@@ -146,7 +146,7 @@ export class VueNodeHelpers {
|
||||
expectedCount
|
||||
)
|
||||
} else {
|
||||
await this.page.locator('[data-node-id]').first().waitFor()
|
||||
await this.page.waitForSelector('[data-node-id]')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -52,6 +52,6 @@ export class SettingDialog extends BaseDialog {
|
||||
name: 'About'
|
||||
})
|
||||
await aboutButton.click()
|
||||
await this.page.locator('.about-container').waitFor()
|
||||
await this.page.waitForSelector('.about-container')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,9 +301,7 @@ 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
|
||||
.getByRole('button')
|
||||
.and(page.locator('[data-selected]'))
|
||||
this.assetCards = page.locator('[role="button"][data-selected]')
|
||||
this.selectedCards = page.locator('[data-selected="true"]')
|
||||
this.listViewItems = page.locator(
|
||||
'.sidebar-content-container [role="button"][tabindex="0"]'
|
||||
@@ -351,7 +349,7 @@ export class AssetsSidebarTab extends SidebarTab {
|
||||
async dismissToasts() {
|
||||
const closeButtons = this.page.locator('.p-toast-close-button')
|
||||
for (const btn of await closeButtons.all()) {
|
||||
await btn.click().catch(() => {})
|
||||
await btn.click({ force: true }).catch(() => {})
|
||||
}
|
||||
// Wait for all toast elements to fully animate out and detach from DOM
|
||||
await expect(this.page.locator('.p-toast-message'))
|
||||
|
||||
@@ -71,7 +71,7 @@ export class Topbar {
|
||||
async closeWorkflowTab(tabName: string) {
|
||||
const tab = this.getWorkflowTab(tabName)
|
||||
await tab.hover()
|
||||
await tab.locator('.close-button').click()
|
||||
await tab.locator('.close-button').click({ force: true })
|
||||
}
|
||||
|
||||
getSaveDialog(): Locator {
|
||||
|
||||
@@ -23,7 +23,7 @@ export class AppModeHelper {
|
||||
public readonly outputPlaceholder: Locator
|
||||
/** The linear-mode widget list container (visible in app mode). */
|
||||
public readonly linearWidgets: Locator
|
||||
/** The PrimeVue Popover for the image picker (renders with role="dialog"). */
|
||||
/** The Reka UI Popover for the image picker (teleported to body). */
|
||||
public readonly imagePickerPopover: Locator
|
||||
/** The Run button in the app mode footer. */
|
||||
public readonly runButton: Locator
|
||||
@@ -53,11 +53,8 @@ export class AppModeHelper {
|
||||
this.outputPlaceholder = this.page.getByTestId(
|
||||
TestIds.builder.outputPlaceholder
|
||||
)
|
||||
this.linearWidgets = this.page.getByTestId('linear-widgets')
|
||||
this.imagePickerPopover = this.page
|
||||
.getByRole('dialog')
|
||||
.filter({ has: this.page.getByRole('button', { name: 'All' }) })
|
||||
.first()
|
||||
this.linearWidgets = this.page.locator('[data-testid="linear-widgets"]')
|
||||
this.imagePickerPopover = this.page.getByTestId('form-dropdown-content')
|
||||
this.runButton = this.page
|
||||
.getByTestId('linear-run-button')
|
||||
.getByRole('button', { name: /run/i })
|
||||
|
||||
@@ -151,7 +151,6 @@ export class BuilderSelectHelper {
|
||||
const widgetLocator = this.comfyPage.vueNodes
|
||||
.getNodeLocator(String(nodeRef.id))
|
||||
.getByLabel(widgetName, { exact: true })
|
||||
// oxlint-disable-next-line playwright/no-force-option -- Node container has conditional pointer-events:none that blocks actionability
|
||||
await widgetLocator.click({ force: true })
|
||||
await this.comfyPage.nextFrame()
|
||||
}
|
||||
@@ -200,7 +199,6 @@ export class BuilderSelectHelper {
|
||||
const nodeLocator = this.comfyPage.vueNodes.getNodeLocator(
|
||||
String(nodeRef.id)
|
||||
)
|
||||
// oxlint-disable-next-line playwright/no-force-option -- Node container has conditional pointer-events:none that blocks actionability
|
||||
await nodeLocator.click({ force: true })
|
||||
await this.comfyPage.nextFrame()
|
||||
}
|
||||
|
||||
@@ -74,51 +74,6 @@ export class CanvasHelper {
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a canvas-element-relative position to absolute page coordinates.
|
||||
* Use with `page.mouse` APIs when Vue DOM overlays above the canvas would
|
||||
* cause Playwright's actionability check to fail on the canvas locator.
|
||||
*/
|
||||
private async toAbsolute(position: Position): Promise<Position> {
|
||||
const box = await this.canvas.boundingBox()
|
||||
if (!box) throw new Error('Canvas bounding box not available')
|
||||
return { x: box.x + position.x, y: box.y + position.y }
|
||||
}
|
||||
|
||||
/**
|
||||
* Click at canvas-element-relative coordinates using `page.mouse.click()`.
|
||||
* Bypasses Playwright's actionability checks on the canvas locator, which
|
||||
* can fail when Vue-rendered DOM nodes overlay the `<canvas>` element.
|
||||
*/
|
||||
async mouseClickAt(
|
||||
position: Position,
|
||||
options?: {
|
||||
button?: 'left' | 'right' | 'middle'
|
||||
modifiers?: ('Shift' | 'Control' | 'Alt' | 'Meta')[]
|
||||
}
|
||||
): Promise<void> {
|
||||
const abs = await this.toAbsolute(position)
|
||||
const modifiers = options?.modifiers ?? []
|
||||
for (const mod of modifiers) await this.page.keyboard.down(mod)
|
||||
try {
|
||||
await this.page.mouse.click(abs.x, abs.y, {
|
||||
button: options?.button
|
||||
})
|
||||
} finally {
|
||||
for (const mod of modifiers) await this.page.keyboard.up(mod)
|
||||
}
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
/**
|
||||
* Double-click at canvas-element-relative coordinates using `page.mouse`.
|
||||
*/
|
||||
async mouseDblclickAt(position: Position): Promise<void> {
|
||||
const abs = await this.toAbsolute(position)
|
||||
await this.page.mouse.dblclick(abs.x, abs.y)
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async clickEmptySpace(): Promise<void> {
|
||||
await this.canvas.click({ position: DefaultGraphPositions.emptySpaceClick })
|
||||
await this.nextFrame()
|
||||
|
||||
@@ -157,7 +157,7 @@ export class SubgraphHelper {
|
||||
|
||||
// Wait for the appropriate UI element to appear
|
||||
if (action === 'rightClick') {
|
||||
await this.page.locator('.litemenu-entry').first().waitFor({
|
||||
await this.page.waitForSelector('.litemenu-entry', {
|
||||
state: 'visible',
|
||||
timeout: 5000
|
||||
})
|
||||
|
||||
@@ -21,10 +21,6 @@ export const TestIds = {
|
||||
contextMenu: 'canvas-context-menu',
|
||||
toggleMinimapButton: 'toggle-minimap-button',
|
||||
closeMinimapButton: 'close-minimap-button',
|
||||
minimapContainer: 'minimap-container',
|
||||
minimapCanvas: 'minimap-canvas',
|
||||
minimapViewport: 'minimap-viewport',
|
||||
minimapInteractionOverlay: 'minimap-interaction-overlay',
|
||||
toggleLinkVisibilityButton: 'toggle-link-visibility-button',
|
||||
zoomControlsButton: 'zoom-controls-button',
|
||||
zoomInAction: 'zoom-in-action',
|
||||
|
||||
@@ -14,11 +14,10 @@ function makeMatcher<T>(
|
||||
) {
|
||||
await expect(async () => {
|
||||
const value = await getValue(node)
|
||||
if (this.isNot) {
|
||||
expect(value, 'Node is ' + type).not.toBeTruthy()
|
||||
} else {
|
||||
expect(value, 'Node is not ' + type).toBeTruthy()
|
||||
}
|
||||
const assertion = this.isNot
|
||||
? expect(value, 'Node is ' + type).not
|
||||
: expect(value, 'Node is not ' + type)
|
||||
assertion.toBeTruthy()
|
||||
}).toPass({ timeout: 5000, ...options })
|
||||
return {
|
||||
pass: !this.isNot,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { expect } from '@playwright/test'
|
||||
import type { Page } from '@playwright/test'
|
||||
|
||||
import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import { ManageGroupNode } from '@e2e/helpers/manageGroupNode'
|
||||
@@ -355,11 +356,7 @@ export class NodeReference {
|
||||
}
|
||||
async click(
|
||||
position: 'title' | 'collapse',
|
||||
options?: {
|
||||
button?: 'left' | 'right' | 'middle'
|
||||
modifiers?: ('Shift' | 'Control' | 'Alt' | 'Meta')[]
|
||||
moveMouseToEmptyArea?: boolean
|
||||
}
|
||||
options?: Parameters<Page['click']>[1] & { moveMouseToEmptyArea?: boolean }
|
||||
) {
|
||||
let clickPos: Position
|
||||
switch (position) {
|
||||
@@ -380,7 +377,12 @@ export class NodeReference {
|
||||
delete options.moveMouseToEmptyArea
|
||||
}
|
||||
|
||||
await this.comfyPage.canvasOps.mouseClickAt(clickPos, options)
|
||||
await this.comfyPage.canvas.click({
|
||||
...options,
|
||||
position: clickPos,
|
||||
force: true
|
||||
})
|
||||
await this.comfyPage.nextFrame()
|
||||
if (moveMouseToEmptyArea) {
|
||||
await this.comfyPage.canvasOps.moveMouseToEmptyArea()
|
||||
}
|
||||
@@ -497,18 +499,31 @@ export class NodeReference {
|
||||
|
||||
await expect(async () => {
|
||||
// Try just clicking the enter button first
|
||||
await this.comfyPage.canvasOps.mouseClickAt({ x: 250, y: 250 })
|
||||
await this.comfyPage.canvas.click({
|
||||
position: { x: 250, y: 250 },
|
||||
force: true
|
||||
})
|
||||
await this.comfyPage.nextFrame()
|
||||
|
||||
await this.comfyPage.canvasOps.mouseClickAt(subgraphButtonPos)
|
||||
await this.comfyPage.canvas.click({
|
||||
position: subgraphButtonPos,
|
||||
force: true
|
||||
})
|
||||
await this.comfyPage.nextFrame()
|
||||
|
||||
if (await checkIsInSubgraph()) return
|
||||
|
||||
for (const position of clickPositions) {
|
||||
// Clear any selection first
|
||||
await this.comfyPage.canvasOps.mouseClickAt({ x: 250, y: 250 })
|
||||
await this.comfyPage.canvas.click({
|
||||
position: { x: 250, y: 250 },
|
||||
force: true
|
||||
})
|
||||
await this.comfyPage.nextFrame()
|
||||
|
||||
// Double-click to enter subgraph
|
||||
await this.comfyPage.canvasOps.mouseDblclickAt(position)
|
||||
await this.comfyPage.canvas.dblclick({ position, force: true })
|
||||
await this.comfyPage.nextFrame()
|
||||
|
||||
if (await checkIsInSubgraph()) return
|
||||
}
|
||||
|
||||
@@ -15,11 +15,13 @@ export class VueNodeFixture {
|
||||
|
||||
constructor(private readonly locator: Locator) {
|
||||
this.header = locator.locator('[data-testid^="node-header-"]')
|
||||
this.title = locator.getByTestId('node-title')
|
||||
this.titleInput = locator.getByTestId('node-title-input')
|
||||
this.title = locator.locator('[data-testid="node-title"]')
|
||||
this.titleInput = locator.locator('[data-testid="node-title-input"]')
|
||||
this.body = locator.locator('[data-testid^="node-body-"]')
|
||||
this.pinIndicator = locator.getByTestId(TestIds.node.pinIndicator)
|
||||
this.collapseButton = locator.getByTestId('node-collapse-button')
|
||||
this.collapseButton = locator.locator(
|
||||
'[data-testid="node-collapse-button"]'
|
||||
)
|
||||
this.collapseIcon = this.collapseButton.locator('i')
|
||||
this.root = locator
|
||||
}
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
import type { Locator, Page } from '@playwright/test'
|
||||
import type { TestGraphAccess } from '@e2e/types/globals'
|
||||
|
||||
export async function drawStroke(
|
||||
page: Page,
|
||||
canvas: Locator,
|
||||
opts: { startXPct?: number; endXPct?: number; yPct?: number } = {}
|
||||
): Promise<void> {
|
||||
const { startXPct = 0.3, endXPct = 0.7, yPct = 0.5 } = opts
|
||||
const box = await canvas.boundingBox()
|
||||
if (!box) throw new Error('Canvas bounding box not found')
|
||||
await page.mouse.move(
|
||||
box.x + box.width * startXPct,
|
||||
box.y + box.height * yPct
|
||||
)
|
||||
await page.mouse.down()
|
||||
await page.mouse.move(
|
||||
box.x + box.width * endXPct,
|
||||
box.y + box.height * yPct,
|
||||
{ steps: 10 }
|
||||
)
|
||||
await page.mouse.up()
|
||||
}
|
||||
|
||||
export async function hasCanvasContent(canvas: Locator): Promise<boolean> {
|
||||
return canvas.evaluate((el: HTMLCanvasElement) => {
|
||||
const ctx = el.getContext('2d')
|
||||
if (!ctx) return false
|
||||
const { data } = ctx.getImageData(0, 0, el.width, el.height)
|
||||
for (let i = 3; i < data.length; i += 4) {
|
||||
if (data[i] > 0) return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
export async function triggerSerialization(page: Page): Promise<void> {
|
||||
await page.evaluate(async () => {
|
||||
const graph = window.graph as TestGraphAccess | undefined
|
||||
if (!graph) {
|
||||
throw new Error(
|
||||
'Global window.graph is absent. Ensure workflow fixture is loaded.'
|
||||
)
|
||||
}
|
||||
|
||||
const node = graph._nodes_by_id?.['1']
|
||||
if (!node) {
|
||||
throw new Error(
|
||||
'Target node with ID "1" not found in graph._nodes_by_id.'
|
||||
)
|
||||
}
|
||||
|
||||
const widget = node.widgets?.find((w) => w.name === 'mask')
|
||||
if (!widget) {
|
||||
throw new Error('Widget "mask" not found on target node 1.')
|
||||
}
|
||||
|
||||
if (typeof widget.serializeValue !== 'function') {
|
||||
throw new Error(
|
||||
'mask widget on node 1 does not have a serializeValue function.'
|
||||
)
|
||||
}
|
||||
|
||||
await widget.serializeValue(node, 0)
|
||||
})
|
||||
}
|
||||
@@ -137,27 +137,20 @@ test.describe('App mode dropdown clipping', { tag: '@ui' }, () => {
|
||||
const dropdownButton = imageRow.locator('button:has(> span)').first()
|
||||
await dropdownButton.click()
|
||||
|
||||
// The unstyled PrimeVue Popover renders with role="dialog".
|
||||
// Locate the one containing the image grid (filter buttons like "All", "Inputs").
|
||||
// The Reka UI PopoverContent is teleported to <body> via PopoverPortal,
|
||||
// so it's never clipped by the app mode panel's overflow container.
|
||||
const popover = comfyPage.appMode.imagePickerPopover
|
||||
await expect(popover).toBeVisible()
|
||||
|
||||
// Verify popover is outside the linear-widgets container
|
||||
// (PopoverPortal teleports it to <body>, escaping overflow: hidden)
|
||||
await expect
|
||||
.poll(() =>
|
||||
popover.evaluate((el) => {
|
||||
const rect = el.getBoundingClientRect()
|
||||
return (
|
||||
rect.top >= 0 &&
|
||||
rect.left >= 0 &&
|
||||
rect.bottom <= window.innerHeight &&
|
||||
rect.right <= window.innerWidth
|
||||
)
|
||||
const panel = document.querySelector('[data-testid="linear-widgets"]')
|
||||
return panel ? !panel.contains(el) : true
|
||||
})
|
||||
)
|
||||
.toBe(true)
|
||||
|
||||
await expect
|
||||
.poll(() => popover.evaluate(isClippedByAnyAncestor))
|
||||
.toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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).toBeHidden()
|
||||
await expect(comfyPage.appMode.buildAppButton).not.toBeVisible()
|
||||
})
|
||||
|
||||
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).toBeHidden()
|
||||
await expect(comfyPage.appMode.emptyWorkflowText).not.toBeVisible()
|
||||
})
|
||||
|
||||
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).toBeHidden()
|
||||
await expect(comfyPage.appMode.buildAppButton).toBeHidden()
|
||||
await expect(comfyPage.appMode.emptyWorkflowText).not.toBeVisible()
|
||||
await expect(comfyPage.appMode.buildAppButton).not.toBeVisible()
|
||||
})
|
||||
|
||||
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).toBeHidden()
|
||||
await expect(comfyPage.appMode.welcome).not.toBeVisible()
|
||||
})
|
||||
|
||||
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).toBeHidden()
|
||||
await expect(bottomPanel.root).not.toBeVisible()
|
||||
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).toBeHidden()
|
||||
await expect(bottomPanel.root).not.toBeVisible()
|
||||
})
|
||||
|
||||
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"]')
|
||||
).toBeHidden()
|
||||
).not.toBeVisible()
|
||||
})
|
||||
|
||||
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).toBeHidden()
|
||||
await expect(bottomPanel.root).not.toBeVisible()
|
||||
await bottomPanel.keyboardShortcutsButton.click()
|
||||
await expect(bottomPanel.root).toBeVisible()
|
||||
await bottomPanel.keyboardShortcutsButton.click()
|
||||
await expect(bottomPanel.root).toBeHidden()
|
||||
await expect(bottomPanel.root).not.toBeVisible()
|
||||
})
|
||||
|
||||
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).toBeHidden()
|
||||
await expect(bottomPanel.root).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('should display shortcuts in organized columns', async ({
|
||||
@@ -192,7 +192,9 @@ test.describe('Bottom Panel Shortcuts', { tag: '@ui' }, () => {
|
||||
|
||||
await bottomPanel.keyboardShortcutsButton.click()
|
||||
|
||||
await expect(comfyPage.page.getByTestId('shortcuts-columns')).toBeVisible()
|
||||
await expect(
|
||||
comfyPage.page.locator('[data-testid="shortcuts-columns"]')
|
||||
).toBeVisible()
|
||||
|
||||
const subcategoryTitles = bottomPanel.shortcuts.subcategoryTitles
|
||||
await expect.poll(() => subcategoryTitles.count()).toBeGreaterThanOrEqual(2)
|
||||
@@ -203,7 +205,7 @@ test.describe('Bottom Panel Shortcuts', { tag: '@ui' }, () => {
|
||||
}) => {
|
||||
const { bottomPanel } = comfyPage
|
||||
|
||||
await expect(bottomPanel.root).toBeHidden()
|
||||
await expect(bottomPanel.root).not.toBeVisible()
|
||||
|
||||
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).toBeHidden()
|
||||
await expect(appMode.saveAs.successDialog).not.toBeVisible()
|
||||
|
||||
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).toBeHidden()
|
||||
await expect(saveAs.successDialog).not.toBeVisible()
|
||||
}
|
||||
|
||||
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).toBeHidden()
|
||||
await expect(comfyPage.appMode.steps.toolbar).not.toBeVisible()
|
||||
})
|
||||
|
||||
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).toBeHidden()
|
||||
await expect(comfyPage.appMode.steps.toolbar).not.toBeVisible()
|
||||
})
|
||||
|
||||
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).toBeHidden()
|
||||
await expect(saveAs.dialog).not.toBeVisible()
|
||||
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).toBeHidden()
|
||||
await expect(comfyPage.appMode.saveAs.successDialog).not.toBeVisible()
|
||||
|
||||
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).toBeHidden()
|
||||
await expect(comfyPage.appMode.saveAs.successDialog).not.toBeVisible()
|
||||
|
||||
await expect(comfyPage.appMode.steps.toolbar).toBeHidden()
|
||||
await expect(comfyPage.appMode.steps.toolbar).not.toBeVisible()
|
||||
})
|
||||
|
||||
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).toBeHidden()
|
||||
await expect(appMode.saveAs.overwriteDialog).not.toBeVisible()
|
||||
|
||||
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).toBeHidden()
|
||||
await expect(menu).not.toBeVisible()
|
||||
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).toBeHidden()
|
||||
await expect(menu).not.toBeVisible()
|
||||
})
|
||||
|
||||
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).toBeHidden()
|
||||
await expect(menu).not.toBeVisible()
|
||||
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).toBeHidden()
|
||||
await expect(menu).not.toBeVisible()
|
||||
await expect(trigger).toBeFocused()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -38,13 +38,13 @@ test.describe('Default Keybindings', { tag: '@keyboard' }, () => {
|
||||
`.${tabId}-tab-button.side-bar-button-selected`
|
||||
)
|
||||
|
||||
await expect(selectedButton).toBeHidden()
|
||||
await expect(selectedButton).not.toBeVisible()
|
||||
|
||||
await comfyPage.canvas.press(key)
|
||||
await expect(selectedButton).toBeVisible()
|
||||
|
||||
await comfyPage.canvas.press(key)
|
||||
await expect(selectedButton).toBeHidden()
|
||||
await expect(selectedButton).not.toBeVisible()
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -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).toBeHidden()
|
||||
await expect(comfyPage.appMode.linearWidgets).not.toBeVisible()
|
||||
|
||||
// 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).toBeHidden()
|
||||
await expect(minimap).not.toBeVisible()
|
||||
|
||||
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).toBeHidden()
|
||||
await expect(comfyPage.bottomPanel.root).not.toBeVisible()
|
||||
|
||||
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).toBeHidden()
|
||||
await expect(comfyPage.bottomPanel.root).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -11,7 +11,9 @@ 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.getByTestId('settings-dialog')
|
||||
const settingsDialog = comfyPage.page.locator(
|
||||
'[data-testid="settings-dialog"]'
|
||||
)
|
||||
await expect(settingsDialog).toBeVisible()
|
||||
const contentArea = settingsDialog.locator('main')
|
||||
await expect(contentArea).toBeVisible()
|
||||
@@ -24,16 +26,17 @@ test.describe('Settings', () => {
|
||||
await comfyPage.page.keyboard.down('ControlOrMeta')
|
||||
await comfyPage.page.keyboard.press(',')
|
||||
await comfyPage.page.keyboard.up('ControlOrMeta')
|
||||
const settingsLocator = comfyPage.page.getByTestId('settings-dialog')
|
||||
const settingsLocator = comfyPage.page.locator(
|
||||
'[data-testid="settings-dialog"]'
|
||||
)
|
||||
await expect(settingsLocator).toBeVisible()
|
||||
await comfyPage.page.keyboard.press('Escape')
|
||||
await expect(settingsLocator).toBeHidden()
|
||||
await expect(settingsLocator).not.toBeVisible()
|
||||
})
|
||||
|
||||
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'))
|
||||
@@ -46,7 +49,9 @@ test.describe('Settings', () => {
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
|
||||
// Open the keybinding tab
|
||||
const settingsDialog = comfyPage.page.getByTestId('settings-dialog')
|
||||
const settingsDialog = comfyPage.page.locator(
|
||||
'[data-testid="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')).toBeHidden()
|
||||
await expect(dialog.getByText('Test Pack A')).not.toBeVisible()
|
||||
})
|
||||
|
||||
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).toBeHidden()
|
||||
await expect(dialog).not.toBeVisible()
|
||||
})
|
||||
|
||||
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).toBeHidden()
|
||||
await expect(dialog).not.toBeVisible()
|
||||
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).toBeHidden()
|
||||
await expect(dialog).not.toBeVisible()
|
||||
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).toBeHidden()
|
||||
await expect(dialog).not.toBeVisible()
|
||||
})
|
||||
|
||||
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).toBeHidden()
|
||||
await expect(dialog).not.toBeVisible()
|
||||
|
||||
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).toBeHidden()
|
||||
await expect(dialog.root).not.toBeVisible()
|
||||
})
|
||||
|
||||
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).toBeHidden()
|
||||
await expect(dialog.root).not.toBeVisible()
|
||||
})
|
||||
|
||||
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).toBeHidden()
|
||||
await expect(textareaWidget).not.toBeVisible()
|
||||
})
|
||||
|
||||
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).toBeHidden()
|
||||
await expect(lastMultiline).toBeHidden()
|
||||
await expect(firstMultiline).not.toBeVisible()
|
||||
await expect(lastMultiline).not.toBeVisible()
|
||||
})
|
||||
|
||||
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).toBeHidden()
|
||||
await expect(comfyPage.menu.sideToolbar).not.toBeVisible()
|
||||
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')).toBeHidden()
|
||||
await expect(errorDialog.locator('pre')).not.toBeVisible()
|
||||
|
||||
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)
|
||||
).toBeHidden()
|
||||
).not.toBeVisible()
|
||||
})
|
||||
|
||||
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).toBeHidden()
|
||||
await expect(errorOverlay).not.toBeVisible()
|
||||
|
||||
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).toBeHidden()
|
||||
await expect(errorOverlay).not.toBeVisible()
|
||||
|
||||
await comfyPage.keyboard.redo()
|
||||
await expect(errorOverlay).toBeHidden()
|
||||
await expect(errorOverlay).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -156,7 +156,7 @@ test.describe('Error overlay', { tag: '@ui' }, () => {
|
||||
|
||||
await overlay.getByTestId(TestIds.dialogs.errorOverlaySeeErrors).click()
|
||||
|
||||
await expect(overlay).toBeHidden()
|
||||
await expect(overlay).not.toBeVisible()
|
||||
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).toBeHidden()
|
||||
await expect(overlay).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('"Dismiss" closes overlay without opening panel', async ({
|
||||
@@ -181,8 +181,10 @@ test.describe('Error overlay', { tag: '@ui' }, () => {
|
||||
|
||||
await overlay.getByTestId(TestIds.dialogs.errorOverlayDismiss).click()
|
||||
|
||||
await expect(overlay).toBeHidden()
|
||||
await expect(comfyPage.page.getByTestId('properties-panel')).toBeHidden()
|
||||
await expect(overlay).not.toBeVisible()
|
||||
await expect(
|
||||
comfyPage.page.getByTestId('properties-panel')
|
||||
).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('Close button (X) dismisses overlay', async ({ comfyPage }) => {
|
||||
@@ -193,7 +195,7 @@ test.describe('Error overlay', { tag: '@ui' }, () => {
|
||||
|
||||
await overlay.getByRole('button', { name: /close/i }).click()
|
||||
|
||||
await expect(overlay).toBeHidden()
|
||||
await expect(overlay).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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({ timeout: 5000 })
|
||||
}).toPass()
|
||||
|
||||
// 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({ timeout: 5000 })
|
||||
}).toPass()
|
||||
|
||||
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({ timeout: 5000 })
|
||||
}).toPass()
|
||||
})
|
||||
|
||||
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({ timeout: 5000 })
|
||||
}).toPass()
|
||||
|
||||
// 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({ timeout: 5000 })
|
||||
}).toPass()
|
||||
})
|
||||
|
||||
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({ timeout: 5000 })
|
||||
}).toPass()
|
||||
|
||||
// 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({ timeout: 5000 })
|
||||
}).toPass()
|
||||
|
||||
await newPage.close()
|
||||
})
|
||||
|
||||
@@ -14,12 +14,12 @@ test.describe('Focus Mode', { tag: '@ui' }, () => {
|
||||
|
||||
await comfyPage.setFocusMode(true)
|
||||
|
||||
await expect(comfyPage.menu.sideToolbar).toBeHidden()
|
||||
await expect(comfyPage.menu.sideToolbar).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('Focus mode restores UI chrome', async ({ comfyPage }) => {
|
||||
await comfyPage.setFocusMode(true)
|
||||
await expect(comfyPage.menu.sideToolbar).toBeHidden()
|
||||
await expect(comfyPage.menu.sideToolbar).not.toBeVisible()
|
||||
|
||||
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).toBeHidden()
|
||||
await expect(comfyPage.menu.sideToolbar).not.toBeVisible()
|
||||
|
||||
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).toBeHidden()
|
||||
await expect(topMenu).not.toBeVisible()
|
||||
})
|
||||
|
||||
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).toBeHidden()
|
||||
await expect(comfyPage.menu.sideToolbar).not.toBeVisible()
|
||||
|
||||
await comfyPage.setFocusMode(false)
|
||||
await expect(comfyPage.menu.sideToolbar).toBeVisible()
|
||||
|
||||
await comfyPage.setFocusMode(true)
|
||||
await expect(comfyPage.menu.sideToolbar).toBeHidden()
|
||||
await expect(comfyPage.menu.sideToolbar).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -109,6 +109,6 @@ test.describe('Graph', { tag: ['@smoke', '@canvas'] }, () => {
|
||||
expect(r.switchOutputLinkIds).toEqual(
|
||||
expect.arrayContaining([r.cfg85LinkId, r.cfg86LinkId])
|
||||
)
|
||||
}).toPass({ timeout: 5000 })
|
||||
}).toPass()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -94,6 +94,6 @@ test.describe('Graph Canvas Menu', { tag: ['@screenshot', '@canvas'] }, () => {
|
||||
await backdrop.click()
|
||||
|
||||
// Modal should be hidden
|
||||
await expect(zoomModal).toBeHidden()
|
||||
await expect(zoomModal).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -88,10 +88,7 @@ test.describe('Group Node', { tag: '@node' }, () => {
|
||||
.getNode(groupNodeName)
|
||||
.locator('.bookmark-button')
|
||||
.click()
|
||||
await comfyPage.page
|
||||
.locator('.p-tree-node-label.tree-explorer-node-label')
|
||||
.first()
|
||||
.hover()
|
||||
await comfyPage.page.hover('.p-tree-node-label.tree-explorer-node-label')
|
||||
await expect(
|
||||
comfyPage.page.locator('.node-lib-node-preview')
|
||||
).toBeVisible()
|
||||
@@ -102,7 +99,6 @@ test.describe('Group Node', { tag: '@node' }, () => {
|
||||
.click()
|
||||
})
|
||||
})
|
||||
|
||||
test(
|
||||
'Can be added to canvas using search',
|
||||
{ tag: '@screenshot' },
|
||||
@@ -158,7 +154,7 @@ test.describe('Group Node', { tag: '@node' }, () => {
|
||||
await comfyPage.nextFrame()
|
||||
await expect(manage1.selectedNodeTypeSelect).toHaveValue('g1')
|
||||
await manage1.close()
|
||||
await expect(manage1.root).toBeHidden()
|
||||
await expect(manage1.root).not.toBeVisible()
|
||||
|
||||
const manage2 = await group2.manageGroupNode()
|
||||
await expect(manage2.selectedNodeTypeSelect).toHaveValue('g2')
|
||||
@@ -245,7 +241,7 @@ test.describe('Group Node', { tag: '@node' }, () => {
|
||||
await expect.poll(() => comfyPage.nodeOps.getGraphNodesCount()).toBe(1)
|
||||
await expect(
|
||||
comfyPage.page.getByTestId(TestIds.dialogs.errorOverlay)
|
||||
).toBeHidden()
|
||||
).not.toBeVisible()
|
||||
})
|
||||
|
||||
test.describe('Copy and paste', () => {
|
||||
@@ -353,7 +349,6 @@ 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,10 +1,9 @@
|
||||
import type { Locator, Page } from '@playwright/test'
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
|
||||
import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
|
||||
|
||||
test.describe('Image Compare', { tag: '@widget' }, () => {
|
||||
test.describe('Image Compare', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
|
||||
await comfyPage.workflow.loadWorkflow('widgets/image_compare_widget')
|
||||
@@ -22,12 +21,7 @@ test.describe('Image Compare', { tag: '@widget' }, () => {
|
||||
|
||||
async function setImageCompareValue(
|
||||
comfyPage: ComfyPage,
|
||||
value: {
|
||||
beforeImages: string[]
|
||||
afterImages: string[]
|
||||
beforeAlt?: string
|
||||
afterAlt?: string
|
||||
}
|
||||
value: { beforeImages: string[]; afterImages: string[] }
|
||||
) {
|
||||
await comfyPage.page.evaluate(
|
||||
({ value }) => {
|
||||
@@ -43,48 +37,6 @@ test.describe('Image Compare', { tag: '@widget' }, () => {
|
||||
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' },
|
||||
@@ -94,14 +46,10 @@ test.describe('Image Compare', { tag: '@widget' }, () => {
|
||||
|
||||
await expect(node).toContainText('No images to compare')
|
||||
await expect(node.locator('img')).toHaveCount(0)
|
||||
await expect(node.getByRole('presentation')).toHaveCount(0)
|
||||
await expect(node.locator('[role="presentation"]')).toHaveCount(0)
|
||||
}
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Slider defaults
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test(
|
||||
'Slider defaults to 50% with both images set',
|
||||
{ tag: ['@smoke', '@screenshot'] },
|
||||
@@ -119,444 +67,15 @@ test.describe('Image Compare', { tag: '@widget' }, () => {
|
||||
await expect(beforeImg).toBeVisible()
|
||||
await expect(afterImg).toBeVisible()
|
||||
|
||||
const handle = node.getByRole('presentation')
|
||||
const handle = node.locator('[role="presentation"]')
|
||||
await expect(handle).toBeVisible()
|
||||
|
||||
expect(
|
||||
await handle.evaluate((el) => (el as HTMLElement).style.left),
|
||||
'Slider should default to 50% before screenshot'
|
||||
await handle.evaluate((el) => (el as HTMLElement).style.left)
|
||||
).toBe('50%')
|
||||
await expect
|
||||
.poll(() => getClipPathInsetRightPercent(beforeImg))
|
||||
.toBeCloseTo(50, 0)
|
||||
await expect(beforeImg).toHaveCSS('clip-path', /50%/)
|
||||
|
||||
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.
|
Before Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB |
@@ -180,48 +180,6 @@ 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(
|
||||
@@ -1294,7 +1252,7 @@ test.describe('Canvas Navigation', { tag: '@screenshot' }, () => {
|
||||
|
||||
test('Space + left-click drag should pan canvas', async ({ comfyPage }) => {
|
||||
// Click canvas to focus it
|
||||
await comfyPage.canvas.click()
|
||||
await comfyPage.page.click('canvas')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await comfyPage.page.keyboard.down('Space')
|
||||
@@ -1363,7 +1321,7 @@ test.describe('Canvas Navigation', { tag: '@screenshot' }, () => {
|
||||
'panning'
|
||||
)
|
||||
|
||||
await comfyPage.canvas.click()
|
||||
await comfyPage.page.click('canvas')
|
||||
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.getByTestId('docked-job-history-action')
|
||||
comfyPage.page.locator('[data-testid="docked-job-history-action"]')
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
@@ -34,7 +34,9 @@ test.describe('Job History Actions', { tag: '@ui' }, () => {
|
||||
}) => {
|
||||
await openMoreOptionsPopover(comfyPage)
|
||||
|
||||
const action = comfyPage.page.getByTestId('docked-job-history-action')
|
||||
const action = comfyPage.page.locator(
|
||||
'[data-testid="docked-job-history-action"]'
|
||||
)
|
||||
await expect(action).toBeVisible()
|
||||
await expect(action).not.toBeEmpty()
|
||||
})
|
||||
@@ -43,7 +45,7 @@ test.describe('Job History Actions', { tag: '@ui' }, () => {
|
||||
await openMoreOptionsPopover(comfyPage)
|
||||
|
||||
await expect(
|
||||
comfyPage.page.getByTestId('show-run-progress-bar-action')
|
||||
comfyPage.page.locator('[data-testid="show-run-progress-bar-action"]')
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
@@ -51,18 +53,20 @@ test.describe('Job History Actions', { tag: '@ui' }, () => {
|
||||
await openMoreOptionsPopover(comfyPage)
|
||||
|
||||
await expect(
|
||||
comfyPage.page.getByTestId('clear-history-action')
|
||||
comfyPage.page.locator('[data-testid="clear-history-action"]')
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('Clicking docked job history closes popover', async ({ comfyPage }) => {
|
||||
await openMoreOptionsPopover(comfyPage)
|
||||
|
||||
const action = comfyPage.page.getByTestId('docked-job-history-action')
|
||||
const action = comfyPage.page.locator(
|
||||
'[data-testid="docked-job-history-action"]'
|
||||
)
|
||||
await expect(action).toBeVisible()
|
||||
await action.click()
|
||||
|
||||
await expect(action).toBeHidden()
|
||||
await expect(action).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('Clicking show run progress bar toggles setting', async ({
|
||||
@@ -74,7 +78,9 @@ test.describe('Job History Actions', { tag: '@ui' }, () => {
|
||||
|
||||
await openMoreOptionsPopover(comfyPage)
|
||||
|
||||
const action = comfyPage.page.getByTestId('show-run-progress-bar-action')
|
||||
const action = comfyPage.page.locator(
|
||||
'[data-testid="show-run-progress-bar-action"]'
|
||||
)
|
||||
await action.click()
|
||||
|
||||
await expect
|
||||
|
||||
@@ -14,38 +14,48 @@ test.describe('Linear Mode', { tag: '@ui' }, () => {
|
||||
}) => {
|
||||
await comfyPage.appMode.enterAppModeWithInputs([])
|
||||
|
||||
await expect(comfyPage.page.getByTestId('linear-widgets')).toBeVisible()
|
||||
await expect(
|
||||
comfyPage.page.locator('[data-testid="linear-widgets"]')
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('Run button visible in linear mode', async ({ comfyPage }) => {
|
||||
await comfyPage.appMode.enterAppModeWithInputs([])
|
||||
|
||||
await expect(comfyPage.page.getByTestId('linear-run-button')).toBeVisible()
|
||||
await expect(
|
||||
comfyPage.page.locator('[data-testid="linear-run-button"]')
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('Workflow info section visible', async ({ comfyPage }) => {
|
||||
await comfyPage.appMode.enterAppModeWithInputs([])
|
||||
|
||||
await expect(
|
||||
comfyPage.page.getByTestId('linear-workflow-info')
|
||||
comfyPage.page.locator('[data-testid="linear-workflow-info"]')
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('Returns to graph mode', async ({ comfyPage }) => {
|
||||
await comfyPage.appMode.enterAppModeWithInputs([])
|
||||
|
||||
await expect(comfyPage.page.getByTestId('linear-widgets')).toBeVisible()
|
||||
await expect(
|
||||
comfyPage.page.locator('[data-testid="linear-widgets"]')
|
||||
).toBeVisible()
|
||||
|
||||
await comfyPage.appMode.toggleAppMode()
|
||||
|
||||
await expect(comfyPage.canvas).toBeVisible()
|
||||
await expect(comfyPage.page.getByTestId('linear-widgets')).toBeHidden()
|
||||
await expect(
|
||||
comfyPage.page.locator('[data-testid="linear-widgets"]')
|
||||
).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('Canvas not visible in app mode', async ({ comfyPage }) => {
|
||||
await comfyPage.appMode.enterAppModeWithInputs([])
|
||||
|
||||
await expect(comfyPage.page.getByTestId('linear-widgets')).toBeVisible()
|
||||
await expect(comfyPage.canvas).toBeHidden()
|
||||
await expect(
|
||||
comfyPage.page.locator('[data-testid="linear-widgets"]')
|
||||
).toBeVisible()
|
||||
await expect(comfyPage.canvas).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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).toBeHidden()
|
||||
await expect(bottomPanel.root).not.toBeVisible()
|
||||
|
||||
// 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).toBeHidden()
|
||||
await expect(bottomPanel.root).not.toBeVisible()
|
||||
|
||||
// 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).toBeHidden()
|
||||
await expect(menu).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('Displays keybinding next to item', async ({ comfyPage }) => {
|
||||
|
||||
@@ -1,38 +1,8 @@
|
||||
import { expect } from '@playwright/test'
|
||||
import type { Locator, Page } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
|
||||
import { TestIds } from '@e2e/fixtures/selectors'
|
||||
|
||||
function hasCanvasContent(canvas: Locator): Promise<boolean> {
|
||||
return canvas.evaluate((el: HTMLCanvasElement) => {
|
||||
const ctx = el.getContext('2d')
|
||||
if (!ctx) return false
|
||||
const { data } = ctx.getImageData(0, 0, el.width, el.height)
|
||||
for (let i = 3; i < data.length; i += 4) {
|
||||
if (data[i] > 0) return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
async function clickMinimapAt(
|
||||
overlay: Locator,
|
||||
page: Page,
|
||||
relX: number,
|
||||
relY: number
|
||||
) {
|
||||
const box = await overlay.boundingBox()
|
||||
expect(box, 'Minimap interaction overlay not found').toBeTruthy()
|
||||
|
||||
// Click area — avoiding the settings button (top-left, 32×32px)
|
||||
// and close button (top-right, 32×32px)
|
||||
await page.mouse.click(
|
||||
box!.x + box!.width * relX,
|
||||
box!.y + box!.height * relY
|
||||
)
|
||||
}
|
||||
|
||||
test.describe('Minimap', { tag: '@canvas' }, () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
@@ -43,20 +13,14 @@ test.describe('Minimap', { tag: '@canvas' }, () => {
|
||||
})
|
||||
|
||||
test('Validate minimap is visible by default', async ({ comfyPage }) => {
|
||||
const minimapContainer = comfyPage.page.getByTestId(
|
||||
TestIds.canvas.minimapContainer
|
||||
)
|
||||
const minimapContainer = comfyPage.page.locator('.litegraph-minimap')
|
||||
|
||||
await expect(minimapContainer).toBeVisible()
|
||||
|
||||
const minimapCanvas = minimapContainer.getByTestId(
|
||||
TestIds.canvas.minimapCanvas
|
||||
)
|
||||
const minimapCanvas = minimapContainer.locator('.minimap-canvas')
|
||||
await expect(minimapCanvas).toBeVisible()
|
||||
|
||||
const minimapViewport = minimapContainer.getByTestId(
|
||||
TestIds.canvas.minimapViewport
|
||||
)
|
||||
const minimapViewport = minimapContainer.locator('.minimap-viewport')
|
||||
await expect(minimapViewport).toBeVisible()
|
||||
|
||||
await expect(minimapContainer).toHaveCSS('position', 'relative')
|
||||
@@ -76,16 +40,12 @@ test.describe('Minimap', { tag: '@canvas' }, () => {
|
||||
|
||||
await expect(toggleButton).toBeVisible()
|
||||
|
||||
const minimapContainer = comfyPage.page.getByTestId(
|
||||
TestIds.canvas.minimapContainer
|
||||
)
|
||||
const minimapContainer = comfyPage.page.locator('.litegraph-minimap')
|
||||
await expect(minimapContainer).toBeVisible()
|
||||
})
|
||||
|
||||
test('Validate minimap can be toggled off and on', async ({ comfyPage }) => {
|
||||
const minimapContainer = comfyPage.page.getByTestId(
|
||||
TestIds.canvas.minimapContainer
|
||||
)
|
||||
const minimapContainer = comfyPage.page.locator('.litegraph-minimap')
|
||||
const toggleButton = comfyPage.page.getByTestId(
|
||||
TestIds.canvas.toggleMinimapButton
|
||||
)
|
||||
@@ -93,32 +53,30 @@ test.describe('Minimap', { tag: '@canvas' }, () => {
|
||||
await expect(minimapContainer).toBeVisible()
|
||||
|
||||
await toggleButton.click()
|
||||
await expect(minimapContainer).toBeHidden()
|
||||
await expect(minimapContainer).not.toBeVisible()
|
||||
|
||||
await toggleButton.click()
|
||||
await expect(minimapContainer).toBeVisible()
|
||||
})
|
||||
|
||||
test('Validate minimap keyboard shortcut Alt+M', async ({ comfyPage }) => {
|
||||
const minimapContainer = comfyPage.page.getByTestId(
|
||||
TestIds.canvas.minimapContainer
|
||||
)
|
||||
const minimapContainer = comfyPage.page.locator('.litegraph-minimap')
|
||||
|
||||
await expect(minimapContainer).toBeVisible()
|
||||
|
||||
await comfyPage.page.keyboard.press('Alt+KeyM')
|
||||
await expect(minimapContainer).toBeHidden()
|
||||
await expect(minimapContainer).not.toBeVisible()
|
||||
|
||||
await comfyPage.page.keyboard.press('Alt+KeyM')
|
||||
await expect(minimapContainer).toBeVisible()
|
||||
})
|
||||
|
||||
test('Close button hides minimap', async ({ comfyPage }) => {
|
||||
const minimap = comfyPage.page.getByTestId(TestIds.canvas.minimapContainer)
|
||||
const minimap = comfyPage.page.locator('.litegraph-minimap')
|
||||
await expect(minimap).toBeVisible()
|
||||
|
||||
await comfyPage.page.getByTestId(TestIds.canvas.closeMinimapButton).click()
|
||||
await expect(minimap).toBeHidden()
|
||||
await expect(minimap).not.toBeVisible()
|
||||
|
||||
const toggleButton = comfyPage.page.getByTestId(
|
||||
TestIds.canvas.toggleMinimapButton
|
||||
@@ -130,9 +88,7 @@ test.describe('Minimap', { tag: '@canvas' }, () => {
|
||||
'Panning canvas moves minimap viewport',
|
||||
{ tag: '@screenshot' },
|
||||
async ({ comfyPage }) => {
|
||||
const minimap = comfyPage.page.getByTestId(
|
||||
TestIds.canvas.minimapContainer
|
||||
)
|
||||
const minimap = comfyPage.page.locator('.litegraph-minimap')
|
||||
await expect(minimap).toBeVisible()
|
||||
|
||||
await expect(minimap).toHaveScreenshot('minimap-before-pan.png')
|
||||
@@ -149,135 +105,14 @@ test.describe('Minimap', { tag: '@canvas' }, () => {
|
||||
}
|
||||
)
|
||||
|
||||
test('Minimap canvas is non-empty for a workflow with nodes', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const minimapCanvas = comfyPage.page.getByTestId(
|
||||
TestIds.canvas.minimapCanvas
|
||||
)
|
||||
await expect(minimapCanvas).toBeVisible()
|
||||
|
||||
await expect.poll(() => hasCanvasContent(minimapCanvas)).toBe(true)
|
||||
})
|
||||
|
||||
test('Minimap canvas is empty after all nodes are deleted', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const minimapCanvas = comfyPage.page.getByTestId(
|
||||
TestIds.canvas.minimapCanvas
|
||||
)
|
||||
await expect(minimapCanvas).toBeVisible()
|
||||
|
||||
await comfyPage.keyboard.selectAll()
|
||||
await comfyPage.vueNodes.deleteSelected()
|
||||
await expect.poll(() => comfyPage.nodeOps.getGraphNodesCount()).toBe(0)
|
||||
|
||||
await expect.poll(() => hasCanvasContent(minimapCanvas)).toBe(false)
|
||||
})
|
||||
|
||||
test('Clicking minimap corner pans the main canvas', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const minimap = comfyPage.page.getByTestId(TestIds.canvas.minimapContainer)
|
||||
const viewport = minimap.getByTestId(TestIds.canvas.minimapViewport)
|
||||
const overlay = comfyPage.page.getByTestId(
|
||||
TestIds.canvas.minimapInteractionOverlay
|
||||
)
|
||||
await expect(minimap).toBeVisible()
|
||||
|
||||
const before = await comfyPage.page.evaluate(() => ({
|
||||
x: window.app!.canvas.ds.offset[0],
|
||||
y: window.app!.canvas.ds.offset[1]
|
||||
}))
|
||||
|
||||
const transformBefore = await viewport.evaluate(
|
||||
(el: HTMLElement) => el.style.transform
|
||||
)
|
||||
|
||||
await clickMinimapAt(overlay, comfyPage.page, 0.15, 0.85)
|
||||
|
||||
await expect
|
||||
.poll(() =>
|
||||
comfyPage.page.evaluate(() => ({
|
||||
x: window.app!.canvas.ds.offset[0],
|
||||
y: window.app!.canvas.ds.offset[1]
|
||||
}))
|
||||
)
|
||||
.not.toStrictEqual(before)
|
||||
|
||||
await expect
|
||||
.poll(() => viewport.evaluate((el: HTMLElement) => el.style.transform))
|
||||
.not.toBe(transformBefore)
|
||||
})
|
||||
|
||||
test('Clicking minimap center after FitView causes minimal canvas movement', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const minimap = comfyPage.page.getByTestId(TestIds.canvas.minimapContainer)
|
||||
const overlay = comfyPage.page.getByTestId(
|
||||
TestIds.canvas.minimapInteractionOverlay
|
||||
)
|
||||
const viewport = minimap.getByTestId(TestIds.canvas.minimapViewport)
|
||||
await expect(minimap).toBeVisible()
|
||||
|
||||
await comfyPage.page.evaluate(() => {
|
||||
const canvas = window.app!.canvas
|
||||
canvas.ds.offset[0] -= 1000
|
||||
canvas.setDirty(true, true)
|
||||
})
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const transformBefore = await viewport.evaluate(
|
||||
(el: HTMLElement) => el.style.transform
|
||||
)
|
||||
|
||||
await comfyPage.page.evaluate(() => {
|
||||
window.app!.canvas.fitViewToSelectionAnimated({ duration: 1 })
|
||||
})
|
||||
|
||||
await expect
|
||||
.poll(() => viewport.evaluate((el: HTMLElement) => el.style.transform), {
|
||||
timeout: 2000
|
||||
})
|
||||
.not.toBe(transformBefore)
|
||||
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const before = await comfyPage.page.evaluate(() => ({
|
||||
x: window.app!.canvas.ds.offset[0],
|
||||
y: window.app!.canvas.ds.offset[1]
|
||||
}))
|
||||
|
||||
await clickMinimapAt(overlay, comfyPage.page, 0.5, 0.5)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const after = await comfyPage.page.evaluate(() => ({
|
||||
x: window.app!.canvas.ds.offset[0],
|
||||
y: window.app!.canvas.ds.offset[1]
|
||||
}))
|
||||
|
||||
// ~3px overlay error × ~15 canvas/minimap scale ≈ 45, rounded up
|
||||
const TOLERANCE = 50
|
||||
expect(
|
||||
Math.abs(after.x - before.x),
|
||||
`offset.x changed by more than ${TOLERANCE} after clicking minimap center post-FitView`
|
||||
).toBeLessThan(TOLERANCE)
|
||||
expect(
|
||||
Math.abs(after.y - before.y),
|
||||
`offset.y changed by more than ${TOLERANCE} after clicking minimap center post-FitView`
|
||||
).toBeLessThan(TOLERANCE)
|
||||
})
|
||||
|
||||
test(
|
||||
'Viewport rectangle is visible and positioned within minimap',
|
||||
{ tag: '@screenshot' },
|
||||
async ({ comfyPage }) => {
|
||||
const minimap = comfyPage.page.getByTestId(
|
||||
TestIds.canvas.minimapContainer
|
||||
)
|
||||
const minimap = comfyPage.page.locator('.litegraph-minimap')
|
||||
await expect(minimap).toBeVisible()
|
||||
|
||||
const viewport = minimap.getByTestId(TestIds.canvas.minimapViewport)
|
||||
const viewport = minimap.locator('.minimap-viewport')
|
||||
await expect(viewport).toBeVisible()
|
||||
|
||||
await expect(async () => {
|
||||
|
||||
@@ -39,7 +39,9 @@ test.describe(
|
||||
|
||||
await expect(comfyPage.page.locator('.selection-toolbox')).toBeVisible()
|
||||
|
||||
const moreOptionsBtn = comfyPage.page.getByTestId('more-options-button')
|
||||
const moreOptionsBtn = comfyPage.page.locator(
|
||||
'[data-testid="more-options-button"]'
|
||||
)
|
||||
await expect(moreOptionsBtn).toBeVisible()
|
||||
await moreOptionsBtn.click()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
@@ -40,14 +40,13 @@ test.describe('Optional input', { tag: ['@screenshot', '@node'] }, () => {
|
||||
await expect.poll(() => comfyPage.nodeOps.getGraphNodesCount()).toBe(1)
|
||||
await expect(
|
||||
comfyPage.page.getByTestId(TestIds.dialogs.errorOverlay)
|
||||
).toBeHidden()
|
||||
).not.toBeVisible()
|
||||
|
||||
// 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')
|
||||
@@ -63,7 +62,6 @@ 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')
|
||||
@@ -71,12 +69,10 @@ 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'
|
||||
@@ -85,7 +81,6 @@ 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(
|
||||
|
||||
@@ -29,7 +29,7 @@ async function openSelectionToolboxHelp(comfyPage: ComfyPage) {
|
||||
|
||||
const helpButton = comfyPage.selectionToolbox.getByTestId('info-button')
|
||||
await expect(helpButton).toBeVisible()
|
||||
await helpButton.click()
|
||||
await helpButton.click({ force: true })
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
return comfyPage.page.getByTestId('properties-panel')
|
||||
@@ -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')).toBeHidden()
|
||||
await expect(helpPage.locator('.node-help-content')).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -505,7 +505,7 @@ This is English documentation.
|
||||
|
||||
// Should show fallback content (node description)
|
||||
await expect(helpPage).toBeVisible()
|
||||
await expect(helpPage.locator('.p-progressspinner')).toBeHidden()
|
||||
await expect(helpPage.locator('.p-progressspinner')).not.toBeVisible()
|
||||
|
||||
// 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).toBeHidden()
|
||||
await expect(panel.header).not.toBeVisible()
|
||||
|
||||
// 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).toBeHidden()
|
||||
await expect(searchBoxV2.input).not.toBeVisible()
|
||||
|
||||
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).toBeHidden()
|
||||
await expect(searchBoxV2.input).not.toBeVisible()
|
||||
|
||||
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).toBeHidden()
|
||||
await expect(searchBoxV2.input).not.toBeVisible()
|
||||
|
||||
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).toBeHidden()
|
||||
await expect(searchBoxV2.input).not.toBeVisible()
|
||||
|
||||
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).toBeHidden()
|
||||
await expect(searchBoxV2.input).not.toBeVisible()
|
||||
|
||||
await comfyPage.canvasOps.doubleClick()
|
||||
await expect(searchBoxV2.input).toBeVisible()
|
||||
@@ -104,7 +104,9 @@ test.describe('Node search box V2 extended', { tag: '@node' }, () => {
|
||||
.click()
|
||||
|
||||
// Verify filter chip appeared and results changed
|
||||
const filterChip = searchBoxV2.dialog.getByTestId('filter-chip')
|
||||
const filterChip = searchBoxV2.dialog.locator(
|
||||
'[data-testid="filter-chip"]'
|
||||
)
|
||||
await expect(filterChip).toBeVisible()
|
||||
await expect(searchBoxV2.results.first()).toBeVisible()
|
||||
await expect
|
||||
@@ -115,7 +117,7 @@ test.describe('Node search box V2 extended', { tag: '@node' }, () => {
|
||||
await filterChip.getByTestId('chip-delete').click()
|
||||
|
||||
// Filter chip should be removed
|
||||
await expect(filterChip).toBeHidden()
|
||||
await expect(filterChip).not.toBeVisible()
|
||||
await expect(searchBoxV2.results.first()).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,17 +1,9 @@
|
||||
import type { UploadImageResponse } from '@comfyorg/ingest-types'
|
||||
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
|
||||
import {
|
||||
drawStroke,
|
||||
hasCanvasContent,
|
||||
triggerSerialization
|
||||
} from '@e2e/helpers/painter'
|
||||
|
||||
test.describe('Painter', { tag: '@widget' }, () => {
|
||||
test.describe('Painter', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.page.evaluate(() => window.app?.graph?.clear())
|
||||
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
|
||||
await comfyPage.workflow.loadWorkflow('widgets/painter_widget')
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
@@ -28,15 +20,9 @@ test.describe('Painter', { tag: '@widget' }, () => {
|
||||
await expect(painterWidget).toBeVisible()
|
||||
|
||||
await expect(painterWidget.locator('canvas')).toBeVisible()
|
||||
await expect(
|
||||
painterWidget.getByRole('button', { name: 'Brush' })
|
||||
).toBeVisible()
|
||||
await expect(
|
||||
painterWidget.getByRole('button', { name: 'Eraser' })
|
||||
).toBeVisible()
|
||||
await expect(
|
||||
painterWidget.getByTestId('painter-clear-button')
|
||||
).toBeVisible()
|
||||
await expect(painterWidget.getByText('Brush')).toBeVisible()
|
||||
await expect(painterWidget.getByText('Eraser')).toBeVisible()
|
||||
await expect(painterWidget.getByText('Clear')).toBeVisible()
|
||||
await expect(
|
||||
painterWidget.locator('input[type="color"]').first()
|
||||
).toBeVisible()
|
||||
@@ -53,66 +39,22 @@ test.describe('Painter', { tag: '@widget' }, () => {
|
||||
const canvas = node.locator('.widget-expands canvas')
|
||||
await expect(canvas).toBeVisible()
|
||||
|
||||
expect(await hasCanvasContent(canvas), 'canvas should start empty').toBe(
|
||||
false
|
||||
)
|
||||
|
||||
await drawStroke(comfyPage.page, canvas)
|
||||
|
||||
await expect
|
||||
.poll(() => hasCanvasContent(canvas), {
|
||||
message: 'canvas should have content after stroke'
|
||||
})
|
||||
.poll(async () =>
|
||||
canvas.evaluate((el) => {
|
||||
const ctx = (el as HTMLCanvasElement).getContext('2d')
|
||||
if (!ctx) return true
|
||||
const data = ctx.getImageData(
|
||||
0,
|
||||
0,
|
||||
(el as HTMLCanvasElement).width,
|
||||
(el as HTMLCanvasElement).height
|
||||
)
|
||||
return data.data.every((v, i) => (i % 4 === 3 ? v === 0 : true))
|
||||
})
|
||||
)
|
||||
.toBe(true)
|
||||
|
||||
await expect(node).toHaveScreenshot('painter-after-stroke.png')
|
||||
}
|
||||
)
|
||||
|
||||
test.describe('Drawing', () => {
|
||||
test(
|
||||
'Eraser removes drawn content',
|
||||
{ tag: '@smoke' },
|
||||
async ({ comfyPage }) => {
|
||||
const node = comfyPage.vueNodes.getNodeLocator('1')
|
||||
const painterWidget = node.locator('.widget-expands')
|
||||
const canvas = painterWidget.locator('canvas')
|
||||
|
||||
await drawStroke(comfyPage.page, canvas)
|
||||
|
||||
await expect
|
||||
.poll(() => hasCanvasContent(canvas), {
|
||||
message: 'canvas must have content before erasing'
|
||||
})
|
||||
.toBe(true)
|
||||
|
||||
await painterWidget.getByRole('button', { name: 'Eraser' }).click()
|
||||
await drawStroke(comfyPage.page, canvas)
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
canvas.evaluate((el: HTMLCanvasElement) => {
|
||||
const ctx = el.getContext('2d')
|
||||
if (!ctx) return false
|
||||
const cx = Math.floor(el.width / 2)
|
||||
const cy = Math.floor(el.height / 2)
|
||||
const { data } = ctx.getImageData(cx - 5, cy - 5, 10, 10)
|
||||
return data.every((v, i) => i % 4 !== 3 || v === 0)
|
||||
}),
|
||||
{ message: 'erased area should be transparent' }
|
||||
)
|
||||
.toBe(true)
|
||||
}
|
||||
)
|
||||
|
||||
test('Stroke ends cleanly when pointer up fires outside canvas', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const painterWidget = comfyPage.vueNodes
|
||||
.getNodeLocator('1')
|
||||
.locator('.widget-expands')
|
||||
const canvas = painterWidget.locator('canvas')
|
||||
const box = await canvas.boundingBox()
|
||||
if (!box) throw new Error('Canvas bounding box not found')
|
||||
|
||||
@@ -126,250 +68,29 @@ test.describe('Painter', { tag: '@widget' }, () => {
|
||||
box.y + box.height * 0.5,
|
||||
{ steps: 10 }
|
||||
)
|
||||
await comfyPage.page.mouse.move(box.x - 20, box.y + box.height * 0.5)
|
||||
await comfyPage.page.mouse.up()
|
||||
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await expect
|
||||
.poll(() => hasCanvasContent(canvas), {
|
||||
message:
|
||||
'canvas should have content after stroke with pointer up outside'
|
||||
})
|
||||
.poll(async () =>
|
||||
canvas.evaluate((el) => {
|
||||
const ctx = (el as HTMLCanvasElement).getContext('2d')
|
||||
if (!ctx) return false
|
||||
const data = ctx.getImageData(
|
||||
0,
|
||||
0,
|
||||
(el as HTMLCanvasElement).width,
|
||||
(el as HTMLCanvasElement).height
|
||||
)
|
||||
for (let i = 3; i < data.data.length; i += 4) {
|
||||
if (data.data[i] > 0) return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
)
|
||||
.toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Tool selection', () => {
|
||||
test('Tool switching toggles brush-only controls', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const painterWidget = comfyPage.vueNodes
|
||||
.getNodeLocator('1')
|
||||
.locator('.widget-expands')
|
||||
|
||||
await expect(painterWidget.getByTestId('painter-color-row')).toBeVisible()
|
||||
await expect(
|
||||
painterWidget.getByTestId('painter-hardness-row')
|
||||
).toBeVisible()
|
||||
|
||||
await painterWidget.getByRole('button', { name: 'Eraser' }).click()
|
||||
|
||||
await expect(
|
||||
painterWidget.getByTestId('painter-color-row'),
|
||||
'color row should be hidden in eraser mode'
|
||||
).toBeHidden()
|
||||
await expect(
|
||||
painterWidget.getByTestId('painter-hardness-row')
|
||||
).toBeHidden()
|
||||
|
||||
await painterWidget.getByRole('button', { name: 'Brush' }).click()
|
||||
|
||||
await expect(painterWidget.getByTestId('painter-color-row')).toBeVisible()
|
||||
await expect(
|
||||
painterWidget.getByTestId('painter-hardness-row')
|
||||
).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Brush settings', () => {
|
||||
test('Size slider updates the displayed value', async ({ comfyPage }) => {
|
||||
const painterWidget = comfyPage.vueNodes
|
||||
.getNodeLocator('1')
|
||||
.locator('.widget-expands')
|
||||
const sizeRow = painterWidget.getByTestId('painter-size-row')
|
||||
const sizeSlider = sizeRow.getByRole('slider')
|
||||
const sizeDisplay = sizeRow.getByTestId('painter-size-value')
|
||||
|
||||
await expect(sizeDisplay).toHaveText('20')
|
||||
|
||||
await sizeSlider.focus()
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await sizeSlider.press('ArrowRight')
|
||||
}
|
||||
|
||||
await expect(sizeDisplay).toHaveText('30')
|
||||
})
|
||||
|
||||
test('Opacity input clamps out-of-range values', async ({ comfyPage }) => {
|
||||
const painterWidget = comfyPage.vueNodes
|
||||
.getNodeLocator('1')
|
||||
.locator('.widget-expands')
|
||||
const opacityInput = painterWidget
|
||||
.getByTestId('painter-color-row')
|
||||
.locator('input[type="number"]')
|
||||
|
||||
await opacityInput.fill('150')
|
||||
await opacityInput.press('Tab')
|
||||
await expect(opacityInput).toHaveValue('100')
|
||||
|
||||
await opacityInput.fill('-10')
|
||||
await opacityInput.press('Tab')
|
||||
await expect(opacityInput).toHaveValue('0')
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Canvas size controls', () => {
|
||||
test('Width and height sliders visible without connected input', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const painterWidget = comfyPage.vueNodes
|
||||
.getNodeLocator('1')
|
||||
.locator('.widget-expands')
|
||||
|
||||
await expect(painterWidget.getByTestId('painter-width-row')).toBeVisible()
|
||||
await expect(
|
||||
painterWidget.getByTestId('painter-height-row')
|
||||
).toBeVisible()
|
||||
|
||||
await expect(
|
||||
painterWidget.getByTestId('painter-dimension-text')
|
||||
).toBeHidden()
|
||||
})
|
||||
|
||||
test('Width slider resizes the canvas element', async ({ comfyPage }) => {
|
||||
const painterWidget = comfyPage.vueNodes
|
||||
.getNodeLocator('1')
|
||||
.locator('.widget-expands')
|
||||
const canvas = painterWidget.locator('canvas')
|
||||
const widthSlider = painterWidget
|
||||
.getByTestId('painter-width-row')
|
||||
.getByRole('slider')
|
||||
|
||||
const initialWidth = await canvas.evaluate(
|
||||
(el: HTMLCanvasElement) => el.width
|
||||
)
|
||||
expect(initialWidth, 'canvas should start at default width').toBe(512)
|
||||
|
||||
await widthSlider.focus()
|
||||
await widthSlider.press('ArrowRight')
|
||||
|
||||
await expect
|
||||
.poll(() => canvas.evaluate((el: HTMLCanvasElement) => el.width))
|
||||
.toBe(576)
|
||||
})
|
||||
|
||||
test(
|
||||
'Resize preserves existing drawing',
|
||||
{ tag: ['@smoke', '@screenshot'] },
|
||||
async ({ comfyPage }) => {
|
||||
const node = comfyPage.vueNodes.getNodeLocator('1')
|
||||
const painterWidget = node.locator('.widget-expands')
|
||||
const canvas = painterWidget.locator('canvas')
|
||||
const widthSlider = painterWidget
|
||||
.getByTestId('painter-width-row')
|
||||
.getByRole('slider')
|
||||
|
||||
await drawStroke(comfyPage.page, canvas)
|
||||
await expect
|
||||
.poll(() => hasCanvasContent(canvas), {
|
||||
message: 'canvas must have content before resize'
|
||||
})
|
||||
.toBe(true)
|
||||
|
||||
await widthSlider.focus()
|
||||
await widthSlider.press('ArrowRight')
|
||||
|
||||
await expect
|
||||
.poll(() => canvas.evaluate((el: HTMLCanvasElement) => el.width))
|
||||
.toBe(576)
|
||||
|
||||
await expect.poll(() => hasCanvasContent(canvas)).toBe(true)
|
||||
await expect(node).toHaveScreenshot('painter-after-resize.png')
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
test.describe('Clear', () => {
|
||||
test(
|
||||
'Clear removes all drawn content',
|
||||
{ tag: '@smoke' },
|
||||
async ({ comfyPage }) => {
|
||||
const painterWidget = comfyPage.vueNodes
|
||||
.getNodeLocator('1')
|
||||
.locator('.widget-expands')
|
||||
const canvas = painterWidget.locator('canvas')
|
||||
|
||||
await drawStroke(comfyPage.page, canvas)
|
||||
await expect
|
||||
.poll(() => hasCanvasContent(canvas), {
|
||||
message: 'canvas must have content before clear'
|
||||
})
|
||||
.toBe(true)
|
||||
|
||||
const clearButton = painterWidget.getByTestId('painter-clear-button')
|
||||
await clearButton.dispatchEvent('click')
|
||||
|
||||
await expect
|
||||
.poll(() => hasCanvasContent(canvas), {
|
||||
message: 'canvas should be clear after click'
|
||||
})
|
||||
.toBe(false)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
test.describe('Serialization', () => {
|
||||
test('Drawing triggers upload on serialization', async ({ comfyPage }) => {
|
||||
const mockUploadResponse: UploadImageResponse = {
|
||||
name: 'painter-test.png'
|
||||
}
|
||||
let uploadCount = 0
|
||||
|
||||
await comfyPage.page.route('**/upload/image', async (route) => {
|
||||
uploadCount++
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(mockUploadResponse)
|
||||
})
|
||||
})
|
||||
|
||||
const canvas = comfyPage.vueNodes
|
||||
.getNodeLocator('1')
|
||||
.locator('.widget-expands canvas')
|
||||
|
||||
await drawStroke(comfyPage.page, canvas)
|
||||
|
||||
await triggerSerialization(comfyPage.page)
|
||||
|
||||
expect(uploadCount, 'should upload exactly once').toBe(1)
|
||||
})
|
||||
|
||||
test('Empty canvas does not upload on serialization', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
let uploadCount = 0
|
||||
|
||||
await comfyPage.page.route('**/upload/image', async (route) => {
|
||||
uploadCount++
|
||||
const mockResponse: UploadImageResponse = { name: 'painter-test.png' }
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(mockResponse)
|
||||
})
|
||||
})
|
||||
|
||||
await triggerSerialization(comfyPage.page)
|
||||
|
||||
expect(uploadCount, 'empty canvas should not upload').toBe(0)
|
||||
})
|
||||
|
||||
test('Upload failure shows error toast', async ({ comfyPage }) => {
|
||||
await comfyPage.page.route('**/upload/image', async (route) => {
|
||||
await route.fulfill({ status: 500 })
|
||||
})
|
||||
|
||||
const canvas = comfyPage.vueNodes
|
||||
.getNodeLocator('1')
|
||||
.locator('.widget-expands canvas')
|
||||
|
||||
await drawStroke(comfyPage.page, canvas)
|
||||
|
||||
await expect(triggerSerialization(comfyPage.page)).rejects.toThrow()
|
||||
|
||||
await expect(comfyPage.toast.visibleToasts.first()).toBeVisible()
|
||||
})
|
||||
})
|
||||
await expect(node).toHaveScreenshot('painter-after-stroke.png')
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 26 KiB |
@@ -202,7 +202,6 @@ 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).toBeHidden()
|
||||
await expect(errorOverlay).not.toBeVisible()
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ export class PropertiesPanelHelper {
|
||||
async close(): Promise<void> {
|
||||
if (await this.root.isVisible()) {
|
||||
await this.closeButton.click()
|
||||
await expect(this.root).toBeHidden()
|
||||
await expect(this.root).not.toBeVisible()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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).toBeHidden()
|
||||
await expect(panel.errorsTabIcon).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -55,7 +55,7 @@ test.describe('Errors tab - common', { tag: '@ui' }, () => {
|
||||
await errorOverlay
|
||||
.getByTestId(TestIds.dialogs.errorOverlaySeeErrors)
|
||||
.click()
|
||||
await expect(errorOverlay).toBeHidden()
|
||||
await expect(errorOverlay).not.toBeVisible()
|
||||
|
||||
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).toBeHidden()
|
||||
await expect(errorOverlay).not.toBeVisible()
|
||||
}
|
||||
|
||||
test('Should show Find on GitHub and Copy buttons in error card', async ({
|
||||
|
||||
@@ -104,7 +104,6 @@ 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
|
||||
}
|
||||
@@ -126,13 +125,13 @@ test.describe('Errors tab - Missing media', { tag: '@ui' }, () => {
|
||||
await uploadFileViaDropzone(comfyPage)
|
||||
|
||||
await expect(getStatusCard(comfyPage)).toBeVisible()
|
||||
await expect(getDropzone(comfyPage)).toBeHidden()
|
||||
await expect(getDropzone(comfyPage)).not.toBeVisible()
|
||||
|
||||
await comfyPage.page
|
||||
.getByTestId(TestIds.dialogs.missingMediaCancelButton)
|
||||
.click()
|
||||
|
||||
await expect(getStatusCard(comfyPage)).toBeHidden()
|
||||
await expect(getStatusCard(comfyPage)).not.toBeVisible()
|
||||
await expect(getDropzone(comfyPage)).toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -147,7 +146,7 @@ test.describe('Errors tab - Missing media', { tag: '@ui' }, () => {
|
||||
|
||||
await expect(
|
||||
comfyPage.page.getByTestId(TestIds.dialogs.missingMediaGroup)
|
||||
).toBeHidden()
|
||||
).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ test.describe('Errors tab - Missing models', { tag: '@ui' }, () => {
|
||||
const locateButton = comfyPage.page.getByTestId(
|
||||
TestIds.dialogs.missingModelLocate
|
||||
)
|
||||
await expect(locateButton.first()).toBeHidden()
|
||||
await expect(locateButton.first()).not.toBeVisible()
|
||||
|
||||
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')
|
||||
).toBeHidden()
|
||||
).not.toBeVisible()
|
||||
})
|
||||
|
||||
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')).toBeHidden()
|
||||
await expect(panel.getTab('Nodes')).not.toBeVisible()
|
||||
})
|
||||
|
||||
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')).toBeHidden()
|
||||
await expect(panel.getTab('Info')).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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')).toBeHidden()
|
||||
await expect(nodeLocator.getByText('Muted')).toBeHidden()
|
||||
await expect(nodeLocator.getByText('Bypassed')).not.toBeVisible()
|
||||
await expect(nodeLocator.getByText('Muted')).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -114,7 +114,9 @@ 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')).toBeHidden()
|
||||
await expect(
|
||||
nodeLocator.getByTestId('node-pin-indicator')
|
||||
).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -11,7 +11,7 @@ test.describe('Properties panel - Open and close', () => {
|
||||
})
|
||||
|
||||
test('should open via actionbar toggle button', async ({ comfyPage }) => {
|
||||
await expect(panel.root).toBeHidden()
|
||||
await expect(panel.root).not.toBeVisible()
|
||||
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).toBeHidden()
|
||||
await expect(panel.root).not.toBeVisible()
|
||||
})
|
||||
|
||||
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).toBeHidden()
|
||||
await expect(panel.root).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -34,7 +34,7 @@ test.describe('Properties panel - Title editing', () => {
|
||||
'KSampler',
|
||||
'CLIP Text Encode (Prompt)'
|
||||
])
|
||||
await expect(panel.titleEditIcon).toBeHidden()
|
||||
await expect(panel.titleEditIcon).not.toBeVisible()
|
||||
})
|
||||
|
||||
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).toBeHidden()
|
||||
await expect(panel.titleEditIcon).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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')).toBeHidden()
|
||||
await expect(panel.getTab('Info')).not.toBeVisible()
|
||||
})
|
||||
|
||||
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"]')
|
||||
).toBeHidden()
|
||||
).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('Toggling overlay again closes it', async ({ comfyPage }) => {
|
||||
@@ -104,6 +104,8 @@ test.describe('Queue overlay', () => {
|
||||
|
||||
await toggle.click()
|
||||
|
||||
await expect(comfyPage.page.locator('[data-job-id]').first()).toBeHidden()
|
||||
await expect(
|
||||
comfyPage.page.locator('[data-job-id]').first()
|
||||
).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -72,8 +72,8 @@ test.describe('Release Notifications', () => {
|
||||
).toBeVisible()
|
||||
|
||||
// Close help center by dismissable mask
|
||||
await comfyPage.page.locator('.help-center-backdrop').click()
|
||||
await expect(helpMenu).toBeHidden()
|
||||
await comfyPage.page.click('.help-center-backdrop')
|
||||
await expect(helpMenu).not.toBeVisible()
|
||||
})
|
||||
|
||||
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')).toBeHidden()
|
||||
await expect(comfyPage.page.locator('.whats-new-popup')).not.toBeVisible()
|
||||
await expect(
|
||||
comfyPage.page.locator('.release-notification-toast')
|
||||
).toBeHidden()
|
||||
).not.toBeVisible()
|
||||
})
|
||||
|
||||
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).toBeHidden()
|
||||
await expect(whatsNewSection).not.toBeVisible()
|
||||
|
||||
// Should not show any popups or toasts
|
||||
await expect(comfyPage.page.locator('.whats-new-popup')).toBeHidden()
|
||||
await expect(comfyPage.page.locator('.whats-new-popup')).not.toBeVisible()
|
||||
await expect(
|
||||
comfyPage.page.locator('.release-notification-toast')
|
||||
).toBeHidden()
|
||||
).not.toBeVisible()
|
||||
})
|
||||
|
||||
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.locator('.help-center-backdrop').click()
|
||||
await comfyPage.page.click('.help-center-backdrop')
|
||||
|
||||
// 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).toBeHidden()
|
||||
await expect(whatsNewSection).not.toBeVisible()
|
||||
})
|
||||
|
||||
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).toBeHidden()
|
||||
await expect(whatsNewSection).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
.getByRole('button')
|
||||
.locator('[role="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).toBeHidden()
|
||||
await expect(comfyPage.mediaLightbox.root).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('closes gallery when clicking close button', async ({ comfyPage }) => {
|
||||
await runAndOpenGallery(comfyPage)
|
||||
|
||||
await comfyPage.mediaLightbox.closeButton.click()
|
||||
await expect(comfyPage.mediaLightbox.root).toBeHidden()
|
||||
await expect(comfyPage.mediaLightbox.root).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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.locator('.litemenu-entry:has-text("Pin")').click()
|
||||
await comfyPage.page.click('.litemenu-entry:has-text("Pin")')
|
||||
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.locator('.litemenu-entry:has-text("Unpin")').click()
|
||||
await comfyPage.page.click('.litemenu-entry:has-text("Unpin")')
|
||||
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.locator('.litemenu-entry:has-text("Pin")').click()
|
||||
await comfyPage.page.click('.litemenu-entry:has-text("Pin")')
|
||||
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.locator('.litemenu-entry:has-text("Unpin")').click()
|
||||
await comfyPage.page.click('.litemenu-entry:has-text("Unpin")')
|
||||
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.locator('.litemenu-entry:has-text("Pin")').click()
|
||||
await comfyPage.page.click('.litemenu-entry:has-text("Pin")')
|
||||
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.locator('.litemenu-entry:has-text("Unpin")').click()
|
||||
await comfyPage.page.click('.litemenu-entry:has-text("Unpin")')
|
||||
await comfyPage.contextMenu.waitForHidden()
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
|
||||
@@ -9,7 +9,6 @@ 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)'
|
||||
|
||||
@@ -31,7 +30,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).toBeHidden()
|
||||
await expect(comfyPage.selectionToolbox).not.toBeVisible()
|
||||
|
||||
// Select multiple nodes
|
||||
await comfyPage.nodeOps.selectNodes([
|
||||
@@ -87,7 +86,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).toBeHidden()
|
||||
await expect(comfyPage.selectionToolbox).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('shows border only with multiple selections', async ({ comfyPage }) => {
|
||||
@@ -128,7 +127,7 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => {
|
||||
await comfyPage.workflow.loadWorkflow('groups/single_group')
|
||||
|
||||
// Select group + node should show bypass button
|
||||
await comfyPage.canvas.focus()
|
||||
await comfyPage.page.focus('canvas')
|
||||
await comfyPage.page.keyboard.press('Control+A')
|
||||
await expect(
|
||||
comfyPage.page.locator(
|
||||
@@ -142,7 +141,7 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => {
|
||||
comfyPage.page.locator(
|
||||
'.selection-toolbox *[data-testid="bypass-button"]'
|
||||
)
|
||||
).toBeHidden()
|
||||
).not.toBeVisible()
|
||||
})
|
||||
|
||||
test.describe('Color Picker', () => {
|
||||
@@ -170,7 +169,7 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => {
|
||||
await blueColorOption.click()
|
||||
|
||||
// Dropdown should close after selection
|
||||
await expect(colorPickerGroup).toBeHidden()
|
||||
await expect(colorPickerGroup).not.toBeVisible()
|
||||
|
||||
// Node should have the selected color class/style
|
||||
// Note: Exact verification method depends on how color is applied to nodes
|
||||
|
||||
@@ -49,7 +49,7 @@ test.describe('Selection Toolbox - Button Actions', { tag: '@ui' }, () => {
|
||||
|
||||
const deleteButton = comfyPage.page.getByTestId('delete-button')
|
||||
await expect(deleteButton).toBeVisible()
|
||||
await deleteButton.click()
|
||||
await deleteButton.click({ force: true })
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await expect
|
||||
@@ -65,7 +65,7 @@ test.describe('Selection Toolbox - Button Actions', { tag: '@ui' }, () => {
|
||||
|
||||
const infoButton = comfyPage.page.getByTestId('info-button')
|
||||
await expect(infoButton).toBeVisible()
|
||||
await infoButton.click()
|
||||
await infoButton.click({ force: true })
|
||||
await expect(comfyPage.page.getByTestId('properties-panel')).toBeVisible()
|
||||
})
|
||||
|
||||
@@ -98,7 +98,7 @@ test.describe('Selection Toolbox - Button Actions', { tag: '@ui' }, () => {
|
||||
|
||||
const deleteButton = comfyPage.page.getByTestId('delete-button')
|
||||
await expect(deleteButton).toBeVisible()
|
||||
await deleteButton.click()
|
||||
await deleteButton.click({ force: true })
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await expect
|
||||
@@ -120,7 +120,7 @@ test.describe('Selection Toolbox - Button Actions', { tag: '@ui' }, () => {
|
||||
|
||||
const bypassButton = comfyPage.page.getByTestId('bypass-button')
|
||||
await expect(bypassButton).toBeVisible()
|
||||
await bypassButton.click()
|
||||
await bypassButton.click({ force: true })
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await expect.poll(() => nodeRef.isBypassed()).toBe(true)
|
||||
@@ -128,7 +128,7 @@ test.describe('Selection Toolbox - Button Actions', { tag: '@ui' }, () => {
|
||||
BYPASS_CLASS
|
||||
)
|
||||
|
||||
await bypassButton.click()
|
||||
await bypassButton.click({ force: true })
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await expect.poll(() => nodeRef.isBypassed()).toBe(false)
|
||||
@@ -147,7 +147,7 @@ test.describe('Selection Toolbox - Button Actions', { tag: '@ui' }, () => {
|
||||
'convert-to-subgraph-button'
|
||||
)
|
||||
await expect(convertButton).toBeVisible()
|
||||
await convertButton.click()
|
||||
await convertButton.click({ force: true })
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// KSampler should be gone, replaced by a subgraph node
|
||||
@@ -175,7 +175,7 @@ test.describe('Selection Toolbox - Button Actions', { tag: '@ui' }, () => {
|
||||
'convert-to-subgraph-button'
|
||||
)
|
||||
await expect(convertButton).toBeVisible()
|
||||
await convertButton.click()
|
||||
await convertButton.click({ force: true })
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await expect
|
||||
@@ -200,14 +200,11 @@ test.describe('Selection Toolbox - Button Actions', { tag: '@ui' }, () => {
|
||||
await comfyPage.nodeOps.selectNodes(['KSampler', 'Empty Latent Image'])
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await expect(
|
||||
comfyPage.selectionToolbox.getByRole('button', {
|
||||
name: /Frame Nodes/i
|
||||
})
|
||||
).toBeVisible()
|
||||
await comfyPage.selectionToolbox
|
||||
.getByRole('button', { name: /Frame Nodes/i })
|
||||
.click()
|
||||
const frameButton = comfyPage.page.getByRole('button', {
|
||||
name: /Frame Nodes/i
|
||||
})
|
||||
await expect(frameButton).toBeVisible()
|
||||
await frameButton.click({ force: true })
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await expect
|
||||
@@ -226,7 +223,7 @@ test.describe('Selection Toolbox - Button Actions', { tag: '@ui' }, () => {
|
||||
const frameButton = comfyPage.page.getByRole('button', {
|
||||
name: /Frame Nodes/i
|
||||
})
|
||||
await expect(frameButton).toBeHidden()
|
||||
await expect(frameButton).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('execute button visible when output node selected', async ({
|
||||
@@ -256,6 +253,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).toBeHidden()
|
||||
await expect(executeButton).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -47,10 +47,12 @@ test.describe(
|
||||
|
||||
await expect(comfyPage.page.locator('.selection-toolbox')).toBeVisible()
|
||||
|
||||
const moreOptionsBtn = comfyPage.page.getByTestId('more-options-button')
|
||||
const moreOptionsBtn = comfyPage.page.locator(
|
||||
'[data-testid="more-options-button"]'
|
||||
)
|
||||
await expect(moreOptionsBtn).toBeVisible()
|
||||
|
||||
await moreOptionsBtn.click()
|
||||
await comfyPage.page.click('[data-testid="more-options-button"]')
|
||||
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
@@ -62,7 +64,7 @@ test.describe(
|
||||
return
|
||||
}
|
||||
|
||||
await moreOptionsBtn.click()
|
||||
await moreOptionsBtn.click({ force: true })
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const menuOptionsVisibleAfterClick = await comfyPage.page
|
||||
@@ -111,7 +113,7 @@ test.describe(
|
||||
|
||||
await openMoreOptions(comfyPage)
|
||||
await comfyPage.page.getByText('Color', { exact: true }).click()
|
||||
const blueSwatch = comfyPage.page.getByTitle('Blue')
|
||||
const blueSwatch = comfyPage.page.locator('[title="Blue"]')
|
||||
await expect(blueSwatch.first()).toBeVisible()
|
||||
await blueSwatch.first().click()
|
||||
await comfyPage.nextFrame()
|
||||
@@ -126,7 +128,9 @@ test.describe(
|
||||
await comfyPage.nodeOps.getNodeRefsByTitle('KSampler')
|
||||
)[0]
|
||||
await openMoreOptions(comfyPage)
|
||||
await comfyPage.page.getByText('Rename', { exact: true }).click()
|
||||
await comfyPage.page
|
||||
.getByText('Rename', { exact: true })
|
||||
.click({ force: true })
|
||||
const input = comfyPage.page.locator(
|
||||
'.group-title-editor.node-title-editor .editable-text input'
|
||||
)
|
||||
@@ -151,10 +155,14 @@ test.describe(
|
||||
await comfyPage.nextFrame()
|
||||
}
|
||||
|
||||
await comfyPage.canvasOps.mouseClickAt({ x: 0, y: 50 })
|
||||
await comfyPage.page
|
||||
.locator('#graph-canvas')
|
||||
.click({ position: { x: 0, y: 50 }, force: true })
|
||||
|
||||
await comfyPage.nextFrame()
|
||||
await expect(
|
||||
comfyPage.page.getByText('Rename', { exact: true })
|
||||
).toBeHidden()
|
||||
).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('closes More Options menu when clicking the button again (toggle)', async ({
|
||||
@@ -183,7 +191,7 @@ test.describe(
|
||||
|
||||
await expect(
|
||||
comfyPage.page.getByText('Rename', { exact: true })
|
||||
).toBeHidden()
|
||||
).not.toBeVisible()
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
@@ -192,7 +192,6 @@ 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({
|
||||
@@ -746,7 +745,7 @@ test.describe('Assets sidebar - delete confirmation', () => {
|
||||
|
||||
await comfyPage.confirmDialog.delete.click()
|
||||
|
||||
await expect(dialog).toBeHidden()
|
||||
await expect(dialog).not.toBeVisible()
|
||||
await expect(tab.assetCards).toHaveCount(initialCount - 1)
|
||||
|
||||
const successToast = comfyPage.page.locator('.p-toast-message-success')
|
||||
@@ -768,7 +767,7 @@ test.describe('Assets sidebar - delete confirmation', () => {
|
||||
|
||||
await comfyPage.confirmDialog.reject.click()
|
||||
|
||||
await expect(dialog).toBeHidden()
|
||||
await expect(dialog).not.toBeVisible()
|
||||
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')).toBeHidden()
|
||||
await expect(tab.getLeafByLabel('sd_xl_base_1.0')).not.toBeVisible()
|
||||
})
|
||||
|
||||
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.locator(nodeSelector).hover()
|
||||
await comfyPage.page.hover(nodeSelector)
|
||||
|
||||
// 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
|
||||
.locator(nodeSelector)
|
||||
.dragTo(comfyPage.page.locator(canvasSelector), { targetPosition })
|
||||
await comfyPage.page.dragAndDrop(nodeSelector, canvasSelector, {
|
||||
targetPosition
|
||||
})
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Verify the node is added to the canvas
|
||||
@@ -102,9 +102,7 @@ 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
|
||||
.locator('.node-lib-bookmark-tree-explorer .tree-leaf')
|
||||
.hover()
|
||||
await comfyPage.page.hover('.node-lib-bookmark-tree-explorer .tree-leaf')
|
||||
await expect(tab.nodePreview).toBeVisible()
|
||||
})
|
||||
|
||||
@@ -222,7 +220,6 @@ 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
|
||||
@@ -250,7 +247,6 @@ 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)')).toBeHidden()
|
||||
await expect(tab.getNode('KSampler (Advanced)')).not.toBeVisible()
|
||||
|
||||
await tab.searchInput.fill('KSampler')
|
||||
await expect(tab.getNode('KSampler (Advanced)')).toBeVisible()
|
||||
await expect(tab.getNode('CLIPLoader')).toBeHidden()
|
||||
await expect(tab.getNode('CLIPLoader')).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('Drag node to canvas adds it', async ({ comfyPage }) => {
|
||||
|
||||
@@ -27,7 +27,9 @@ test.describe('Workflow sidebar - search', () => {
|
||||
await searchInput.fill('alpha')
|
||||
|
||||
await expect(findWorkflow(comfyPage.page, 'alpha-workflow')).toBeVisible()
|
||||
await expect(findWorkflow(comfyPage.page, 'beta-workflow')).toBeHidden()
|
||||
await expect(
|
||||
findWorkflow(comfyPage.page, 'beta-workflow')
|
||||
).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('Clearing search restores all workflows', async ({ comfyPage }) => {
|
||||
@@ -36,7 +38,9 @@ test.describe('Workflow sidebar - search', () => {
|
||||
|
||||
const searchInput = comfyPage.page.getByPlaceholder('Search Workflow...')
|
||||
await searchInput.fill('alpha')
|
||||
await expect(findWorkflow(comfyPage.page, 'beta-workflow')).toBeHidden()
|
||||
await expect(
|
||||
findWorkflow(comfyPage.page, 'beta-workflow')
|
||||
).not.toBeVisible()
|
||||
|
||||
await searchInput.fill('')
|
||||
|
||||
@@ -51,7 +55,11 @@ test.describe('Workflow sidebar - search', () => {
|
||||
const searchInput = comfyPage.page.getByPlaceholder('Search Workflow...')
|
||||
await searchInput.fill('nonexistent_xyz')
|
||||
|
||||
await expect(findWorkflow(comfyPage.page, 'alpha-workflow')).toBeHidden()
|
||||
await expect(findWorkflow(comfyPage.page, 'beta-workflow')).toBeHidden()
|
||||
await expect(
|
||||
findWorkflow(comfyPage.page, 'alpha-workflow')
|
||||
).not.toBeVisible()
|
||||
await expect(
|
||||
findWorkflow(comfyPage.page, 'beta-workflow')
|
||||
).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -265,7 +265,7 @@ test.describe('Workflows sidebar', () => {
|
||||
|
||||
// Dismiss the error overlay
|
||||
await errorOverlay.getByTestId(TestIds.dialogs.errorOverlayDismiss).click()
|
||||
await expect(errorOverlay).toBeHidden()
|
||||
await expect(errorOverlay).not.toBeVisible()
|
||||
|
||||
// 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)).toBeHidden()
|
||||
await expect(workflowsTab.getOpenedItem(filename)).not.toBeVisible()
|
||||
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)).toBeHidden()
|
||||
await expect(workflowsTab.getOpenedItem(filename)).not.toBeVisible()
|
||||
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: 'domcontentloaded' })
|
||||
await comfyPage.page.reload({ waitUntil: 'networkidle' })
|
||||
await comfyPage.page.waitForFunction(
|
||||
() => window.app && window.app.extensionManager
|
||||
)
|
||||
await comfyPage.page.locator('.p-blockui-mask').waitFor({
|
||||
await comfyPage.page.waitForSelector('.p-blockui-mask', {
|
||||
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.getByTestId('node-body-11')
|
||||
const nodeBody = subgraphVueNode.locator('[data-testid="node-body-11"]')
|
||||
await expect(nodeBody).toBeVisible()
|
||||
|
||||
const widgets = nodeBody.locator('.lg-node-widgets > div')
|
||||
@@ -199,7 +199,12 @@ test.describe(
|
||||
|
||||
const stepsWidget = await ksampler.getWidget(2)
|
||||
const widgetPos = await stepsWidget.getPosition()
|
||||
await comfyPage.canvasOps.mouseClickAt(widgetPos, { button: 'right' })
|
||||
await comfyPage.canvas.click({
|
||||
position: widgetPos,
|
||||
button: 'right',
|
||||
force: true
|
||||
})
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Look for the Promote Widget menu entry
|
||||
const promoteEntry = comfyPage.page
|
||||
@@ -230,7 +235,12 @@ test.describe(
|
||||
const stepsWidget = await ksampler.getWidget(2)
|
||||
const widgetPos = await stepsWidget.getPosition()
|
||||
|
||||
await comfyPage.canvasOps.mouseClickAt(widgetPos, { button: 'right' })
|
||||
await comfyPage.canvas.click({
|
||||
position: widgetPos,
|
||||
button: 'right',
|
||||
force: true
|
||||
})
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const promoteEntry = comfyPage.page
|
||||
.locator('.litemenu-entry')
|
||||
@@ -256,7 +266,12 @@ test.describe(
|
||||
const stepsWidget2 = await ksampler2.getWidget(2)
|
||||
const widgetPos2 = await stepsWidget2.getPosition()
|
||||
|
||||
await comfyPage.canvasOps.mouseClickAt(widgetPos2, { button: 'right' })
|
||||
await comfyPage.canvas.click({
|
||||
position: widgetPos2,
|
||||
button: 'right',
|
||||
force: true
|
||||
})
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const unpromoteEntry = comfyPage.page
|
||||
.locator('.litemenu-entry')
|
||||
@@ -385,7 +400,7 @@ test.describe(
|
||||
|
||||
await comfyPage.command.executeCommand('Comfy.QueuePrompt')
|
||||
|
||||
const nodeBody = subgraphVueNode.getByTestId('node-body-5')
|
||||
const nodeBody = subgraphVueNode.locator('[data-testid="node-body-5"]')
|
||||
await expect(nodeBody).toBeVisible()
|
||||
await expect(
|
||||
nodeBody.locator('.lg-node-widgets > div').first()
|
||||
|
||||
@@ -132,9 +132,7 @@ test.describe('Subgraph Slots', { tag: ['@slow', '@subgraph'] }, () => {
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await expect(comfyPage.page.locator(SELECTORS.promptDialog)).toBeVisible()
|
||||
await comfyPage.page
|
||||
.locator(SELECTORS.promptDialog)
|
||||
.fill(RENAMED_INPUT_NAME)
|
||||
await comfyPage.page.fill(SELECTORS.promptDialog, RENAMED_INPUT_NAME)
|
||||
await comfyPage.page.keyboard.press('Enter')
|
||||
|
||||
await comfyPage.canvas.click({ position: { x: 100, y: 100 } })
|
||||
@@ -155,12 +153,10 @@ test.describe('Subgraph Slots', { tag: ['@slow', '@subgraph'] }, () => {
|
||||
|
||||
await comfyPage.subgraph.doubleClickInputSlot(initialInputLabel!)
|
||||
|
||||
await comfyPage.page.locator(SELECTORS.promptDialog).waitFor({
|
||||
await comfyPage.page.waitForSelector(SELECTORS.promptDialog, {
|
||||
state: 'visible'
|
||||
})
|
||||
await comfyPage.page
|
||||
.locator(SELECTORS.promptDialog)
|
||||
.fill(RENAMED_INPUT_NAME)
|
||||
await comfyPage.page.fill(SELECTORS.promptDialog, RENAMED_INPUT_NAME)
|
||||
await comfyPage.page.keyboard.press('Enter')
|
||||
|
||||
// Force re-render
|
||||
@@ -182,13 +178,11 @@ test.describe('Subgraph Slots', { tag: ['@slow', '@subgraph'] }, () => {
|
||||
|
||||
await comfyPage.subgraph.doubleClickOutputSlot(initialOutputLabel!)
|
||||
|
||||
await comfyPage.page.locator(SELECTORS.promptDialog).waitFor({
|
||||
await comfyPage.page.waitForSelector(SELECTORS.promptDialog, {
|
||||
state: 'visible'
|
||||
})
|
||||
const renamedOutputName = 'renamed_output'
|
||||
await comfyPage.page
|
||||
.locator(SELECTORS.promptDialog)
|
||||
.fill(renamedOutputName)
|
||||
await comfyPage.page.fill(SELECTORS.promptDialog, renamedOutputName)
|
||||
await comfyPage.page.keyboard.press('Enter')
|
||||
|
||||
// Force re-render
|
||||
@@ -215,13 +209,11 @@ test.describe('Subgraph Slots', { tag: ['@slow', '@subgraph'] }, () => {
|
||||
await comfyPage.contextMenu.clickLitegraphMenuItem('Rename Slot')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await comfyPage.page.locator(SELECTORS.promptDialog).waitFor({
|
||||
await comfyPage.page.waitForSelector(SELECTORS.promptDialog, {
|
||||
state: 'visible'
|
||||
})
|
||||
const rightClickRenamedName = 'right_click_renamed'
|
||||
await comfyPage.page
|
||||
.locator(SELECTORS.promptDialog)
|
||||
.fill(rightClickRenamedName)
|
||||
await comfyPage.page.fill(SELECTORS.promptDialog, rightClickRenamedName)
|
||||
await comfyPage.page.keyboard.press('Enter')
|
||||
|
||||
// Force re-render
|
||||
@@ -278,9 +270,7 @@ test.describe('Subgraph Slots', { tag: ['@slow', '@subgraph'] }, () => {
|
||||
|
||||
await expect(comfyPage.page.locator(SELECTORS.promptDialog)).toBeVisible()
|
||||
const labelClickRenamedName = 'label_click_renamed'
|
||||
await comfyPage.page
|
||||
.locator(SELECTORS.promptDialog)
|
||||
.fill(labelClickRenamedName)
|
||||
await comfyPage.page.fill(SELECTORS.promptDialog, labelClickRenamedName)
|
||||
await comfyPage.page.keyboard.press('Enter')
|
||||
|
||||
await comfyPage.canvas.click({ position: { x: 100, y: 100 } })
|
||||
@@ -313,10 +303,8 @@ test.describe('Subgraph Slots', { tag: ['@slow', '@subgraph'] }, () => {
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await expect(comfyPage.page.locator(SELECTORS.promptDialog)).toBeVisible()
|
||||
await comfyPage.page.locator(SELECTORS.promptDialog).fill('')
|
||||
await comfyPage.page
|
||||
.locator(SELECTORS.promptDialog)
|
||||
.fill(RENAMED_SLOT_NAME)
|
||||
await comfyPage.page.fill(SELECTORS.promptDialog, '')
|
||||
await comfyPage.page.fill(SELECTORS.promptDialog, RENAMED_SLOT_NAME)
|
||||
await comfyPage.page.keyboard.press('Enter')
|
||||
|
||||
await expect(comfyPage.page.locator(SELECTORS.promptDialog)).toBeHidden()
|
||||
@@ -344,10 +332,8 @@ test.describe('Subgraph Slots', { tag: ['@slow', '@subgraph'] }, () => {
|
||||
RENAMED_SLOT_NAME
|
||||
)
|
||||
|
||||
await comfyPage.page.locator(SELECTORS.promptDialog).fill('')
|
||||
await comfyPage.page
|
||||
.locator(SELECTORS.promptDialog)
|
||||
.fill(SECOND_RENAMED_NAME)
|
||||
await comfyPage.page.fill(SELECTORS.promptDialog, '')
|
||||
await comfyPage.page.fill(SELECTORS.promptDialog, SECOND_RENAMED_NAME)
|
||||
await comfyPage.page.keyboard.press('Enter')
|
||||
|
||||
await expect(comfyPage.page.locator(SELECTORS.promptDialog)).toBeHidden()
|
||||
@@ -380,10 +366,8 @@ test.describe('Subgraph Slots', { tag: ['@slow', '@subgraph'] }, () => {
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await expect(comfyPage.page.locator(SELECTORS.promptDialog)).toBeVisible()
|
||||
await comfyPage.page.locator(SELECTORS.promptDialog).fill('')
|
||||
await comfyPage.page
|
||||
.locator(SELECTORS.promptDialog)
|
||||
.fill(RENAMED_SLOT_NAME)
|
||||
await comfyPage.page.fill(SELECTORS.promptDialog, '')
|
||||
await comfyPage.page.fill(SELECTORS.promptDialog, RENAMED_SLOT_NAME)
|
||||
await comfyPage.page.keyboard.press('Enter')
|
||||
|
||||
await expect(comfyPage.page.locator(SELECTORS.promptDialog)).toBeHidden()
|
||||
@@ -450,8 +434,8 @@ test.describe('Subgraph Slots', { tag: ['@slow', '@subgraph'] }, () => {
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await expect(comfyPage.page.locator(SELECTORS.promptDialog)).toBeVisible()
|
||||
await comfyPage.page.locator(SELECTORS.promptDialog).fill('')
|
||||
await comfyPage.page.locator(SELECTORS.promptDialog).fill(RENAMED_LABEL)
|
||||
await comfyPage.page.fill(SELECTORS.promptDialog, '')
|
||||
await comfyPage.page.fill(SELECTORS.promptDialog, RENAMED_LABEL)
|
||||
await comfyPage.page.keyboard.press('Enter')
|
||||
await expect(comfyPage.page.locator(SELECTORS.promptDialog)).toBeHidden()
|
||||
|
||||
@@ -549,10 +533,8 @@ test.describe('Subgraph Slots', { tag: ['@slow', '@subgraph'] }, () => {
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await expect(comfyPage.page.locator(SELECTORS.promptDialog)).toBeVisible()
|
||||
await comfyPage.page.locator(SELECTORS.promptDialog).fill('')
|
||||
await comfyPage.page
|
||||
.locator(SELECTORS.promptDialog)
|
||||
.fill('my_custom_prompt')
|
||||
await comfyPage.page.fill(SELECTORS.promptDialog, '')
|
||||
await comfyPage.page.fill(SELECTORS.promptDialog, 'my_custom_prompt')
|
||||
await comfyPage.page.keyboard.press('Enter')
|
||||
await expect(comfyPage.page.locator(SELECTORS.promptDialog)).toBeHidden()
|
||||
|
||||
|
||||
@@ -40,7 +40,6 @@ 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
|
||||
}) => {
|
||||
@@ -186,8 +185,8 @@ test.describe('Templates', { tag: ['@slow', '@workflow'] }, () => {
|
||||
await comfyPage.command.executeCommand('Comfy.BrowseTemplates')
|
||||
await comfyPage.templates.content.waitFor({ state: 'visible' })
|
||||
|
||||
const templateGrid = comfyPage.page.getByTestId(
|
||||
'template-workflows-content'
|
||||
const templateGrid = comfyPage.page.locator(
|
||||
'[data-testid="template-workflows-content"]'
|
||||
)
|
||||
const nav = comfyPage.page.locator('header', { hasText: 'Templates' })
|
||||
|
||||
@@ -303,18 +302,20 @@ test.describe('Templates', { tag: ['@slow', '@workflow'] }, () => {
|
||||
|
||||
// Wait for cards to load
|
||||
await expect(
|
||||
comfyPage.page.getByTestId('template-workflow-short-description')
|
||||
comfyPage.page.locator(
|
||||
'[data-testid="template-workflow-short-description"]'
|
||||
)
|
||||
).toBeVisible()
|
||||
|
||||
// Verify all three cards with different descriptions are visible
|
||||
const shortDescCard = comfyPage.page.getByTestId(
|
||||
'template-workflow-short-description'
|
||||
const shortDescCard = comfyPage.page.locator(
|
||||
'[data-testid="template-workflow-short-description"]'
|
||||
)
|
||||
const mediumDescCard = comfyPage.page.getByTestId(
|
||||
'template-workflow-medium-description'
|
||||
const mediumDescCard = comfyPage.page.locator(
|
||||
'[data-testid="template-workflow-medium-description"]'
|
||||
)
|
||||
const longDescCard = comfyPage.page.getByTestId(
|
||||
'template-workflow-long-description'
|
||||
const longDescCard = comfyPage.page.locator(
|
||||
'[data-testid="template-workflow-long-description"]'
|
||||
)
|
||||
|
||||
await expect(shortDescCard).toBeVisible()
|
||||
@@ -332,8 +333,8 @@ test.describe('Templates', { tag: ['@slow', '@workflow'] }, () => {
|
||||
await expect(longDesc).toContainText('much longer description')
|
||||
|
||||
// Verify grid layout maintains consistency
|
||||
const templateGrid = comfyPage.page.getByTestId(
|
||||
'template-workflows-content'
|
||||
const templateGrid = comfyPage.page.locator(
|
||||
'[data-testid="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
|
||||
.getByRole('menu')
|
||||
.and(comfyPage.page.locator('[data-state="open"]'))
|
||||
const contextMenu = comfyPage.page.locator(
|
||||
'[role="menu"][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
|
||||
.getByRole('menu')
|
||||
.and(comfyPage.page.locator('[data-state="open"]'))
|
||||
const contextMenu = comfyPage.page.locator(
|
||||
'[role="menu"][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).toBeHidden()
|
||||
await expect(dialog.root).not.toBeVisible()
|
||||
})
|
||||
|
||||
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')
|
||||
).toBeHidden()
|
||||
).not.toBeVisible()
|
||||
})
|
||||
|
||||
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')
|
||||
).toBeHidden()
|
||||
).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -94,7 +94,6 @@ async function connectSlots(
|
||||
const fromLoc = slotLocator(page, from.nodeId, from.index, false)
|
||||
const toLoc = slotLocator(page, to.nodeId, to.index, true)
|
||||
await expectVisibleAll(fromLoc, toLoc)
|
||||
// oxlint-disable-next-line playwright/no-force-option -- Slot dot's parent wrapper div intercepts actionability check on inner dot
|
||||
await fromLoc.dragTo(toLoc, { force: true })
|
||||
await nextFrame()
|
||||
}
|
||||
@@ -193,7 +192,6 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => {
|
||||
const inputSlot = slotLocator(comfyPage.page, clipNode.id, 0, true)
|
||||
await expectVisibleAll(outputSlot, inputSlot)
|
||||
|
||||
// oxlint-disable-next-line playwright/no-force-option -- Slot dot's parent wrapper div intercepts actionability check on inner dot
|
||||
await outputSlot.dragTo(inputSlot, { force: true })
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
@@ -220,7 +218,6 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => {
|
||||
const inputSlot = slotLocator(comfyPage.page, samplerNode.id, 3, true)
|
||||
await expectVisibleAll(outputSlot, inputSlot)
|
||||
|
||||
// oxlint-disable-next-line playwright/no-force-option -- Slot dot's parent wrapper div intercepts actionability check on inner dot
|
||||
await outputSlot.dragTo(inputSlot, { force: true })
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
|
||||
@@ -143,7 +143,7 @@ test.describe('Vue Node Context Menu', () => {
|
||||
await openContextMenu(comfyPage, nodeTitle)
|
||||
await clickExactMenuItem(comfyPage, 'Unpin')
|
||||
|
||||
await expect(fixture.pinIndicator).toBeHidden()
|
||||
await expect(fixture.pinIndicator).not.toBeVisible()
|
||||
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).toBeHidden()
|
||||
await expect(fixture.body).not.toBeVisible()
|
||||
|
||||
await openContextMenu(comfyPage, 'KSampler')
|
||||
await clickExactMenuItem(comfyPage, 'Expand Node')
|
||||
@@ -194,7 +194,9 @@ test.describe('Vue Node Context Menu', () => {
|
||||
const subgraphNode = comfyPage.vueNodes.getNodeByTitle('New Subgraph')
|
||||
await expect(subgraphNode).toBeVisible()
|
||||
|
||||
await expect(comfyPage.vueNodes.getNodeByTitle('KSampler')).toBeHidden()
|
||||
await expect(
|
||||
comfyPage.vueNodes.getNodeByTitle('KSampler')
|
||||
).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -307,7 +309,9 @@ test.describe('Vue Node Context Menu', () => {
|
||||
|
||||
const subgraphNode = comfyPage.vueNodes.getNodeByTitle('New Subgraph')
|
||||
await expect(subgraphNode).toBeVisible()
|
||||
await expect(comfyPage.vueNodes.getNodeByTitle('KSampler')).toBeHidden()
|
||||
await expect(
|
||||
comfyPage.vueNodes.getNodeByTitle('KSampler')
|
||||
).not.toBeVisible()
|
||||
|
||||
// Unpack the subgraph
|
||||
await openContextMenu(comfyPage, 'New Subgraph')
|
||||
@@ -316,7 +320,7 @@ test.describe('Vue Node Context Menu', () => {
|
||||
await expect(comfyPage.vueNodes.getNodeByTitle('KSampler')).toBeVisible()
|
||||
await expect(
|
||||
comfyPage.vueNodes.getNodeByTitle('New Subgraph')
|
||||
).toBeHidden()
|
||||
).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('should open properties panel via Edit Subgraph Widgets', async ({
|
||||
@@ -429,7 +433,7 @@ test.describe('Vue Node Context Menu', () => {
|
||||
|
||||
for (const title of nodeTitles) {
|
||||
const fixture = await comfyPage.vueNodes.getFixtureByTitle(title)
|
||||
await expect(fixture.pinIndicator).toBeHidden()
|
||||
await expect(fixture.pinIndicator).not.toBeVisible()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -470,8 +474,8 @@ test.describe('Vue Node Context Menu', () => {
|
||||
await openMultiNodeContextMenu(comfyPage, nodeTitles)
|
||||
await clickExactMenuItem(comfyPage, 'Minimize Node')
|
||||
|
||||
await expect(fixture1.body).toBeHidden()
|
||||
await expect(fixture2.body).toBeHidden()
|
||||
await expect(fixture1.body).not.toBeVisible()
|
||||
await expect(fixture2.body).not.toBeVisible()
|
||||
|
||||
await openMultiNodeContextMenu(comfyPage, nodeTitles)
|
||||
await clickExactMenuItem(comfyPage, 'Expand Node')
|
||||
|
||||
@@ -11,21 +11,16 @@ test.describe('Vue Node Moving', () => {
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
})
|
||||
|
||||
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()
|
||||
const getLoadCheckpointHeaderPos = async (comfyPage: ComfyPage) => {
|
||||
const loadCheckpointHeaderPos = await comfyPage.page
|
||||
.getByText('Load Checkpoint')
|
||||
.boundingBox()
|
||||
if (!box) throw new Error(`${title} header not found`)
|
||||
return box
|
||||
}
|
||||
|
||||
const getLoadCheckpointHeaderPos = async (comfyPage: ComfyPage) =>
|
||||
getHeaderPos(comfyPage, 'Load Checkpoint')
|
||||
if (!loadCheckpointHeaderPos)
|
||||
throw new Error('Load Checkpoint header not found')
|
||||
|
||||
return loadCheckpointHeaderPos
|
||||
}
|
||||
|
||||
const expectPosChanged = async (pos1: Position, pos2: Position) => {
|
||||
const diffX = Math.abs(pos2.x - pos1.x)
|
||||
@@ -34,16 +29,6 @@ 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, {
|
||||
@@ -95,73 +80,6 @@ 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).toBeHidden()
|
||||
await expect(editingTitleInput).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -30,7 +30,7 @@ test.describe('Vue Node Collapse', () => {
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Verify node content is hidden
|
||||
await expect(body).toBeHidden()
|
||||
await expect(body).not.toBeVisible()
|
||||
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).toBeHidden()
|
||||
await expect(pinIndicator).not.toBeVisible()
|
||||
})
|
||||
|
||||
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).toBeHidden()
|
||||
await expect(pinIndicator2).toBeHidden()
|
||||
await expect(pinIndicator1).not.toBeVisible()
|
||||
await expect(pinIndicator2).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('should not allow dragging pinned nodes', async ({ comfyPage }) => {
|
||||
|
||||
@@ -43,8 +43,12 @@ 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 })).toBeHidden()
|
||||
await expect(node.getByLabel('base_shift', { exact: true })).toBeHidden()
|
||||
await expect(
|
||||
node.getByLabel('max_shift', { exact: true })
|
||||
).not.toBeVisible()
|
||||
await expect(
|
||||
node.getByLabel('base_shift', { exact: true })
|
||||
).not.toBeVisible()
|
||||
|
||||
// "Show advanced inputs" button should be present
|
||||
await expect(node.getByText('Show advanced inputs')).toBeVisible()
|
||||
@@ -93,6 +97,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')).toBeHidden()
|
||||
await expect(node.getByText('Show advanced inputs')).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,162 +0,0 @@
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '@e2e/fixtures/ComfyPage'
|
||||
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
|
||||
|
||||
type CropValue = { x: number; y: number; width: number; height: number } | null
|
||||
|
||||
test.describe('Image Crop', { tag: '@widget' }, () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
|
||||
})
|
||||
|
||||
test.describe('without source image', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.workflow.loadWorkflow('widgets/image_crop_widget')
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
})
|
||||
|
||||
test(
|
||||
'Shows empty state when no input image is connected',
|
||||
{ tag: '@smoke' },
|
||||
async ({ comfyPage }) => {
|
||||
const node = comfyPage.vueNodes.getNodeLocator('1')
|
||||
await expect(node).toBeVisible()
|
||||
|
||||
await expect.soft(node.getByTestId('crop-empty-icon')).toBeVisible()
|
||||
await expect.soft(node).toContainText('No input image connected')
|
||||
await expect.soft(node.getByTestId('crop-overlay')).toHaveCount(0)
|
||||
await expect.soft(node.locator('img')).toHaveCount(0)
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
'Renders controls in default state',
|
||||
{ tag: '@smoke' },
|
||||
async ({ comfyPage }) => {
|
||||
const node = comfyPage.vueNodes.getNodeLocator('1')
|
||||
await expect(node).toBeVisible()
|
||||
|
||||
await expect(node.getByText('Ratio')).toBeVisible()
|
||||
await expect(
|
||||
node.locator('button:has(.icon-\\[lucide--lock-open\\])')
|
||||
).toBeVisible()
|
||||
await expect(node.locator('input')).toHaveCount(4)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
test.describe(
|
||||
'with source image after execution',
|
||||
{ tag: ['@widget', '@slow'] },
|
||||
() => {
|
||||
async function getCropValue(comfyPage: ComfyPage): Promise<CropValue> {
|
||||
return comfyPage.page.evaluate(() => {
|
||||
const n = window.app!.graph.getNodeById(2)
|
||||
const w = n?.widgets?.find((w) => w.type === 'imagecrop')
|
||||
const v = w?.value
|
||||
if (v && typeof v === 'object' && 'x' in v) {
|
||||
const crop = v as {
|
||||
x: number
|
||||
y: number
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
return {
|
||||
x: crop.x,
|
||||
y: crop.y,
|
||||
width: crop.width,
|
||||
height: crop.height
|
||||
}
|
||||
}
|
||||
return null
|
||||
})
|
||||
}
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.workflow.loadWorkflow('widgets/image_crop_with_source')
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
await comfyPage.runButton.click()
|
||||
await expect(
|
||||
comfyPage.vueNodes.getNodeLocator('2').locator('img')
|
||||
).toBeVisible({ timeout: 30_000 })
|
||||
})
|
||||
|
||||
test(
|
||||
'Displays source image with crop overlay after execution',
|
||||
{ tag: ['@smoke', '@screenshot'] },
|
||||
async ({ comfyPage }) => {
|
||||
const node = comfyPage.vueNodes.getNodeLocator('2')
|
||||
const img = node.locator('img')
|
||||
|
||||
await expect
|
||||
.poll(() => img.evaluate((el: HTMLImageElement) => el.naturalWidth))
|
||||
.toBeGreaterThan(0)
|
||||
|
||||
await expect(node.getByTestId('crop-overlay')).toBeVisible()
|
||||
await comfyPage.nextFrame()
|
||||
await comfyPage.nextFrame()
|
||||
await expect(node).toHaveScreenshot('image-crop-with-source.png', {
|
||||
maxDiffPixelRatio: 0.05
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
'Drag crop box updates crop position',
|
||||
{ tag: ['@smoke', '@screenshot'] },
|
||||
async ({ comfyPage }) => {
|
||||
const node = comfyPage.vueNodes.getNodeLocator('2')
|
||||
const cropBox = node.getByTestId('crop-overlay')
|
||||
const box = await cropBox.boundingBox()
|
||||
if (!box) throw new Error('Crop box not found')
|
||||
|
||||
const valueBefore = await getCropValue(comfyPage)
|
||||
if (!valueBefore)
|
||||
throw new Error('Widget value missing — check fixture setup')
|
||||
|
||||
const startX = box.x + box.width / 2
|
||||
const startY = box.y + box.height / 2
|
||||
|
||||
const pointerOpts = { bubbles: true, cancelable: true, pointerId: 1 }
|
||||
await cropBox.dispatchEvent('pointerdown', {
|
||||
...pointerOpts,
|
||||
clientX: startX,
|
||||
clientY: startY
|
||||
})
|
||||
await comfyPage.nextFrame()
|
||||
await cropBox.dispatchEvent('pointermove', {
|
||||
...pointerOpts,
|
||||
clientX: startX + 15,
|
||||
clientY: startY + 10
|
||||
})
|
||||
await comfyPage.nextFrame()
|
||||
await cropBox.dispatchEvent('pointermove', {
|
||||
...pointerOpts,
|
||||
clientX: startX + 30,
|
||||
clientY: startY + 20
|
||||
})
|
||||
await cropBox.dispatchEvent('pointerup', {
|
||||
...pointerOpts,
|
||||
clientX: startX + 30,
|
||||
clientY: startY + 20
|
||||
})
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await expect(async () => {
|
||||
const valueAfter = await getCropValue(comfyPage)
|
||||
expect(valueAfter?.x).toBeGreaterThan(valueBefore.x)
|
||||
expect(valueAfter?.y).toBeGreaterThan(valueBefore.y)
|
||||
expect(valueAfter?.width).toBe(valueBefore.width)
|
||||
expect(valueAfter?.height).toBe(valueBefore.height)
|
||||
}).toPass({ timeout: 5000 })
|
||||
|
||||
await expect(node).toHaveScreenshot('image-crop-after-drag.png', {
|
||||
maxDiffPixelRatio: 0.05
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 30 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 30 KiB |
@@ -22,8 +22,10 @@ test.describe('Vue Integer Widget', () => {
|
||||
const initialValue = Number(await controls.input.inputValue())
|
||||
|
||||
// Verify widget is disabled when linked
|
||||
await expect(controls.incrementButton).toBeDisabled()
|
||||
await expect(controls.decrementButton).toBeDisabled()
|
||||
await controls.incrementButton.click({ force: true })
|
||||
await expect(controls.input).toHaveValue(initialValue.toString())
|
||||
|
||||
await controls.decrementButton.click({ force: true })
|
||||
await expect(controls.input).toHaveValue(initialValue.toString())
|
||||
|
||||
await expect(seedWidget).toBeVisible()
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '@e2e/fixtures/ComfyPage'
|
||||
|
||||
test.describe('FormDropdown Position Under CSS Transforms', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
|
||||
await comfyPage.workflow.loadWorkflow('widgets/load_image_widget')
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
})
|
||||
|
||||
function getTriggerButton(
|
||||
comfyPage: Parameters<Parameters<typeof test>[2]>[0]['comfyPage']
|
||||
) {
|
||||
const node = comfyPage.vueNodes.getNodeByTitle('Load Image')
|
||||
return node
|
||||
.locator('button')
|
||||
.filter({
|
||||
has: comfyPage.page.locator('.icon-\\[lucide--chevron-down\\]')
|
||||
})
|
||||
.and(comfyPage.page.locator(':not([data-testid="node-collapse-button"])'))
|
||||
}
|
||||
|
||||
async function clickDropdownTrigger(
|
||||
comfyPage: Parameters<Parameters<typeof test>[2]>[0]['comfyPage']
|
||||
) {
|
||||
await getTriggerButton(comfyPage).click()
|
||||
}
|
||||
|
||||
function getDropdownContent(
|
||||
comfyPage: Parameters<Parameters<typeof test>[2]>[0]['comfyPage']
|
||||
) {
|
||||
return comfyPage.page.getByTestId('form-dropdown-content')
|
||||
}
|
||||
|
||||
test('dropdown menu appears near trigger at default zoom', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await clickDropdownTrigger(comfyPage)
|
||||
|
||||
const menu = getDropdownContent(comfyPage)
|
||||
await expect(menu).toBeVisible()
|
||||
|
||||
const trigger = getTriggerButton(comfyPage)
|
||||
const triggerBox = await trigger.boundingBox()
|
||||
const menuBox = await menu.boundingBox()
|
||||
|
||||
expect(triggerBox).not.toBeNull()
|
||||
expect(menuBox).not.toBeNull()
|
||||
|
||||
// Menu should appear below the trigger, within a reasonable gap
|
||||
// (side-offset is 8px, plus some tolerance for rounding)
|
||||
const gap = menuBox!.y - (triggerBox!.y + triggerBox!.height)
|
||||
expect(gap).toBeGreaterThanOrEqual(-2)
|
||||
expect(gap).toBeLessThanOrEqual(20)
|
||||
|
||||
// Menu should be horizontally aligned with the trigger
|
||||
const horizontalDrift = Math.abs(menuBox!.x - triggerBox!.x)
|
||||
expect(horizontalDrift).toBeLessThan(50)
|
||||
})
|
||||
|
||||
test('dropdown closes on Escape key', async ({ comfyPage }) => {
|
||||
await clickDropdownTrigger(comfyPage)
|
||||
|
||||
const menu = getDropdownContent(comfyPage)
|
||||
await expect(menu).toBeVisible()
|
||||
|
||||
await comfyPage.page.keyboard.press('Escape')
|
||||
await expect(menu).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -17,7 +17,7 @@ test.describe('Vue Upload Widgets', () => {
|
||||
|
||||
await expect(
|
||||
comfyPage.page.getByText('choose file to upload', { exact: true })
|
||||
).toBeHidden()
|
||||
).not.toBeVisible()
|
||||
|
||||
await expect
|
||||
.poll(() =>
|
||||
|
||||
@@ -46,14 +46,13 @@ 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).toBeHidden()
|
||||
await expect(vueContextMenu).not.toBeVisible()
|
||||
await textarea.blur()
|
||||
|
||||
await textarea.click({ button: 'right' })
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user