Files
ComfyUI_frontend/browser_tests/fixtures/helpers/MaskEditorHelper.ts
Alexander Brown 90b3d8a5c6 test: add mask editor brush adjustment and layer management browser tests (#11368)
*PR Created by the Glary-Bot Agent*

---

## Summary

Adds `browser_tests/tests/maskEditorBrushLayers.spec.ts` covering
untested brush settings interaction and tool/layer management in the
mask editor.

### Coverage gaps filled
- `useBrushDrawing.ts` — brush thickness/opacity/hardness slider
interaction
- `useToolManager.ts` — tool switching with independent mask data, data
preservation across tool switches

### Test cases (5 tests, 2 groups)
| Group | Tests | Behavior |
|---|---|---|
| Brush settings | 3 | Thickness slider changes size, opacity slider
changes opacity, hardness slider changes hardness |
| Layer management | 2 | Different tools produce independent mask data,
switching tools preserves previous mask data |

### References
- Reuses patterns from existing `maskEditor.spec.ts` (`loadImageOnNode`,
`openMaskEditorDialog`, `drawStrokeOnPointerZone`,
`getMaskCanvasPixelData`)
- Follows `browser_tests/AGENTS.md` directory structure
- Follows `browser_tests/FLAKE_PREVENTION_RULES.md` assertion patterns

### Verification
- TypeScript: clean
- ESLint: clean
- oxlint: clean
- oxfmt: formatted

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11368-test-add-mask-editor-brush-adjustment-and-layer-management-browser-tests-3466d73d36508170ae24ebea2b73d60d)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
Co-authored-by: Amp <amp@ampcode.com>
2026-05-04 20:31:15 -04:00

137 lines
4.1 KiB
TypeScript

import type { Locator } from '@playwright/test'
import { expect } from '@playwright/test'
import { comfyPageFixture } from '@e2e/fixtures/ComfyPage'
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
const MASK_CANVAS_INDEX = 2
const RGB_CANVAS_INDEX = 1
export type BrushSliderLabel = 'thickness'
export class MaskEditorHelper {
constructor(private comfyPage: ComfyPage) {}
private get page() {
return this.comfyPage.page
}
async loadImageOnNode() {
await this.comfyPage.workflow.loadWorkflow('widgets/load_image_widget')
const loadImageNode = (
await this.comfyPage.nodeOps.getNodeRefsByType('LoadImage')
)[0]
const { x, y } = await loadImageNode.getPosition()
await this.comfyPage.dragDrop.dragAndDropFile('image64x64.webp', {
dropPosition: { x, y }
})
const imagePreview = this.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)
}
}
async openDialog(): Promise<Locator> {
const { imagePreview } = await this.loadImageOnNode()
await imagePreview.getByRole('region').hover()
await this.page.getByLabel('Edit or mask image').click()
const dialog = this.page.locator('.mask-editor-dialog')
await expect(dialog).toBeVisible()
await expect(
dialog.getByRole('heading', { name: 'Mask Editor' })
).toBeVisible()
const canvasContainer = dialog.locator('#maskEditorCanvasContainer')
await expect(canvasContainer).toBeVisible()
await expect(canvasContainer.locator('canvas')).toHaveCount(4)
return dialog
}
async drawStrokeOnPointerZone(dialog: Locator) {
const pointerZone = dialog.getByTestId('pointer-zone')
await expect(pointerZone).toBeVisible()
const box = await pointerZone.boundingBox()
if (!box) throw new Error('Pointer zone bounding box not found')
const startX = box.x + box.width * 0.3
const startY = box.y + box.height * 0.5
const endX = box.x + box.width * 0.7
const endY = box.y + box.height * 0.5
await this.page.mouse.move(startX, startY)
await this.page.mouse.down()
await this.page.mouse.move(endX, endY, { steps: 10 })
await this.page.mouse.up()
return { startX, startY, endX, endY, box }
}
async drawStrokeAndExpectPixels(dialog: Locator) {
await this.drawStrokeOnPointerZone(dialog)
await expect.poll(() => this.pollMaskPixelCount()).toBeGreaterThan(0)
}
getCanvasPixelData(canvasIndex: number) {
return this.page.evaluate((idx) => {
const canvases = document.querySelectorAll(
'#maskEditorCanvasContainer canvas'
)
const canvas = canvases[idx] as HTMLCanvasElement | undefined
if (!canvas) return null
const ctx = canvas.getContext('2d')
if (!ctx) return null
const data = ctx.getImageData(0, 0, canvas.width, canvas.height)
let nonTransparentPixels = 0
for (let i = 3; i < data.data.length; i += 4) {
if (data.data[i] > 0) nonTransparentPixels++
}
return { nonTransparentPixels, totalPixels: data.data.length / 4 }
}, canvasIndex)
}
pollMaskPixelCount(): Promise<number> {
return this.getCanvasPixelData(MASK_CANVAS_INDEX).then(
(d) => d?.nonTransparentPixels ?? 0
)
}
pollRgbPixelCount(): Promise<number> {
return this.getCanvasPixelData(RGB_CANVAS_INDEX).then(
(d) => d?.nonTransparentPixels ?? 0
)
}
getCanvasSnapshot(canvasIndex: number): Promise<string> {
return this.page.evaluate((idx) => {
const canvas = document.querySelectorAll(
'#maskEditorCanvasContainer canvas'
)[idx] as HTMLCanvasElement | undefined
return canvas?.toDataURL() ?? ''
}, canvasIndex)
}
brushInput(dialog: Locator, label: BrushSliderLabel): Locator {
return dialog.getByTestId(`brush-${label}-input`)
}
}
export const maskEditorTest = comfyPageFixture.extend<{
maskEditor: MaskEditorHelper
}>({
maskEditor: async ({ comfyPage }, use) => {
await use(new MaskEditorHelper(comfyPage))
}
})