Compare commits

...

1 Commits

Author SHA1 Message Date
Kelly Yang
6bf53008e5 test: add mask editor E2E tests for dialog lifecycle 2026-03-29 13:47:39 -07:00
8 changed files with 99 additions and 30 deletions

View File

@@ -101,6 +101,10 @@ export const TestIds = {
errors: {
imageLoadError: 'error-loading-image',
videoLoadError: 'error-loading-video'
},
maskEditor: {
dialog: 'mask-editor-dialog',
uiContainer: 'mask-editor-ui-container'
}
} as const
@@ -127,3 +131,4 @@ export type TestIdValue =
>
| (typeof TestIds.user)[keyof typeof TestIds.user]
| (typeof TestIds.errors)[keyof typeof TestIds.errors]
| (typeof TestIds.maskEditor)[keyof typeof TestIds.maskEditor]

View File

@@ -0,0 +1,35 @@
import { expect } from '@playwright/test'
import type { ComfyPage } from '../fixtures/ComfyPage'
import { TestIds } from '../fixtures/selectors'
export async function loadImageOnNode(comfyPage: ComfyPage) {
await comfyPage.workflow.loadWorkflow('widgets/load_image_widget')
await comfyPage.vueNodes.waitForNodes()
const loadImageNode = (
await comfyPage.nodeOps.getNodeRefsByType('LoadImage')
)[0]
const { x, y } = await loadImageNode.getPosition()
await comfyPage.dragDrop.dragAndDropFile('image64x64.webp', {
dropPosition: { x, y }
})
const imagePreview = comfyPage.page.locator('.image-preview')
await expect(imagePreview).toBeVisible()
await expect(imagePreview.locator('img')).toBeVisible()
await expect(imagePreview).toContainText('x')
return {
imagePreview,
nodeId: String(loadImageNode.id)
}
}
export async function openMaskEditorViaCommand(comfyPage: ComfyPage) {
const { nodeId } = await loadImageOnNode(comfyPage)
await comfyPage.vueNodes.selectNode(nodeId)
await comfyPage.command.executeCommand('Comfy.MaskEditor.OpenMaskEditor')
return comfyPage.page.getByTestId(TestIds.maskEditor.dialog)
}

View File

@@ -1,37 +1,17 @@
import { expect } from '@playwright/test'
import type { ComfyPage } from '../fixtures/ComfyPage'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import { TestIds } from '../fixtures/selectors'
import {
loadImageOnNode,
openMaskEditorViaCommand
} from '../helpers/maskEditorTestUtils'
test.describe('Mask Editor', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
})
async function loadImageOnNode(comfyPage: ComfyPage) {
await comfyPage.workflow.loadWorkflow('widgets/load_image_widget')
await comfyPage.vueNodes.waitForNodes()
const loadImageNode = (
await comfyPage.nodeOps.getNodeRefsByType('LoadImage')
)[0]
const { x, y } = await loadImageNode.getPosition()
await comfyPage.dragDrop.dragAndDropFile('image64x64.webp', {
dropPosition: { x, y }
})
const imagePreview = comfyPage.page.locator('.image-preview')
await expect(imagePreview).toBeVisible()
await expect(imagePreview.locator('img')).toBeVisible()
await expect(imagePreview).toContainText('x')
return {
imagePreview,
nodeId: String(loadImageNode.id)
}
}
test(
'opens mask editor from image preview button',
{ tag: ['@smoke', '@screenshot'] },
@@ -42,7 +22,7 @@ test.describe('Mask Editor', () => {
await imagePreview.getByRole('region').hover()
await comfyPage.page.getByLabel('Edit or mask image').click()
const dialog = comfyPage.page.locator('.mask-editor-dialog')
const dialog = comfyPage.page.getByTestId(TestIds.maskEditor.dialog)
await expect(dialog).toBeVisible()
await expect(
@@ -53,7 +33,9 @@ test.describe('Mask Editor', () => {
await expect(canvasContainer).toBeVisible()
await expect(canvasContainer.locator('canvas')).toHaveCount(4)
await expect(dialog.locator('.maskEditor-ui-container')).toBeVisible()
await expect(
dialog.getByTestId(TestIds.maskEditor.uiContainer)
).toBeVisible()
await expect(dialog.getByText('Save')).toBeVisible()
await expect(dialog.getByText('Cancel')).toBeVisible()
@@ -78,7 +60,7 @@ test.describe('Mask Editor', () => {
await contextMenu.getByText('Open in Mask Editor').click()
const dialog = comfyPage.page.locator('.mask-editor-dialog')
const dialog = comfyPage.page.getByTestId(TestIds.maskEditor.dialog)
await expect(dialog).toBeVisible()
await expect(
dialog.getByRole('heading', { name: 'Mask Editor' })
@@ -89,4 +71,47 @@ test.describe('Mask Editor', () => {
)
}
)
test(
'opens mask editor via command execution',
{ tag: ['@smoke', '@screenshot'] },
async ({ comfyPage }) => {
const dialog = await openMaskEditorViaCommand(comfyPage)
await expect(
dialog.getByTestId(TestIds.maskEditor.uiContainer)
).toBeVisible()
await expect(
dialog.getByRole('heading', { name: 'Mask Editor' })
).toBeVisible()
await expect(dialog).toHaveScreenshot('mask-editor-open-via-command.png')
}
)
test(
'cancel closes mask editor dialog without uploading',
{ tag: ['@smoke', '@screenshot'] },
async ({ comfyPage }) => {
const dialog = await openMaskEditorViaCommand(comfyPage)
await expect(dialog).toBeVisible()
const uploadRequests: string[] = []
await comfyPage.page.route('**/upload/mask', (route) => {
uploadRequests.push('mask')
return route.continue()
})
await comfyPage.page.route('**/upload/image', (route) => {
uploadRequests.push('image')
return route.continue()
})
await expect(dialog).toHaveScreenshot('mask-editor-before-cancel.png')
await dialog.getByRole('button', { name: /cancel/i }).click()
await expect(dialog).not.toBeVisible()
expect(uploadRequests).toHaveLength(0)
await expect(comfyPage.canvas).toHaveScreenshot(
'mask-editor-cancelled-canvas-state.png'
)
}
)
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 321 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 321 KiB

View File

@@ -40,7 +40,10 @@
<LoadingOverlay :loading="!initialized" size="sm" />
<div class="maskEditor-ui-container flex min-h-0 flex-1 flex-col">
<div
class="maskEditor-ui-container flex min-h-0 flex-1 flex-col"
data-testid="mask-editor-ui-container"
>
<div class="flex min-h-0 flex-1 overflow-hidden">
<ToolPanel
v-if="initialized"

View File

@@ -29,7 +29,8 @@ export function useMaskEditor() {
closable: true,
pt: {
root: {
class: 'mask-editor-dialog flex flex-col'
class: 'mask-editor-dialog flex flex-col',
'data-testid': 'mask-editor-dialog'
},
content: {
class: 'flex flex-col min-h-0 flex-1 !p-0'