mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 06:20:11 +00:00
test: add minimap E2E tests for graph content and click-to-navigate (#10738)
## Summary
Adds Playwright E2E tests verifying that
1. the minimap canvas renders node content
2. clears when the graph is empty
3. correctly navigates the main canvas on click
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10738-test-add-minimap-E2E-tests-for-graph-content-and-click-to-navigate-3336d73d365081eb955ce711b3efc57f)
by [Unito](https://www.unito.io)
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Low Risk**
> Low risk: changes are limited to adding `data-testid` attributes to
the minimap UI and expanding Playwright E2E assertions, with no
production behavior changes expected.
>
> **Overview**
> Strengthens minimap E2E coverage by switching existing assertions from
CSS selectors to new `data-testid`-based selectors and adding helper
utilities for canvas/overlay interactions.
>
> Adds new Playwright tests that verify the minimap canvas renders
content when nodes exist, clears when the graph is emptied, and that
clicking the minimap pans the main canvas (including a
post-`fitViewToSelectionAnimated` tolerance check).
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
06e7542af1. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
---------
Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: Alexander Brown <DrJKL0424@gmail.com>
Co-authored-by: GitHub Action <action@github.com>
This commit is contained in:
@@ -21,6 +21,10 @@ 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',
|
||||
|
||||
@@ -1,8 +1,38 @@
|
||||
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')
|
||||
@@ -13,14 +43,20 @@ test.describe('Minimap', { tag: '@canvas' }, () => {
|
||||
})
|
||||
|
||||
test('Validate minimap is visible by default', async ({ comfyPage }) => {
|
||||
const minimapContainer = comfyPage.page.locator('.litegraph-minimap')
|
||||
const minimapContainer = comfyPage.page.getByTestId(
|
||||
TestIds.canvas.minimapContainer
|
||||
)
|
||||
|
||||
await expect(minimapContainer).toBeVisible()
|
||||
|
||||
const minimapCanvas = minimapContainer.locator('.minimap-canvas')
|
||||
const minimapCanvas = minimapContainer.getByTestId(
|
||||
TestIds.canvas.minimapCanvas
|
||||
)
|
||||
await expect(minimapCanvas).toBeVisible()
|
||||
|
||||
const minimapViewport = minimapContainer.locator('.minimap-viewport')
|
||||
const minimapViewport = minimapContainer.getByTestId(
|
||||
TestIds.canvas.minimapViewport
|
||||
)
|
||||
await expect(minimapViewport).toBeVisible()
|
||||
|
||||
await expect(minimapContainer).toHaveCSS('position', 'relative')
|
||||
@@ -40,12 +76,16 @@ test.describe('Minimap', { tag: '@canvas' }, () => {
|
||||
|
||||
await expect(toggleButton).toBeVisible()
|
||||
|
||||
const minimapContainer = comfyPage.page.locator('.litegraph-minimap')
|
||||
const minimapContainer = comfyPage.page.getByTestId(
|
||||
TestIds.canvas.minimapContainer
|
||||
)
|
||||
await expect(minimapContainer).toBeVisible()
|
||||
})
|
||||
|
||||
test('Validate minimap can be toggled off and on', async ({ comfyPage }) => {
|
||||
const minimapContainer = comfyPage.page.locator('.litegraph-minimap')
|
||||
const minimapContainer = comfyPage.page.getByTestId(
|
||||
TestIds.canvas.minimapContainer
|
||||
)
|
||||
const toggleButton = comfyPage.page.getByTestId(
|
||||
TestIds.canvas.toggleMinimapButton
|
||||
)
|
||||
@@ -60,7 +100,9 @@ test.describe('Minimap', { tag: '@canvas' }, () => {
|
||||
})
|
||||
|
||||
test('Validate minimap keyboard shortcut Alt+M', async ({ comfyPage }) => {
|
||||
const minimapContainer = comfyPage.page.locator('.litegraph-minimap')
|
||||
const minimapContainer = comfyPage.page.getByTestId(
|
||||
TestIds.canvas.minimapContainer
|
||||
)
|
||||
|
||||
await expect(minimapContainer).toBeVisible()
|
||||
|
||||
@@ -72,7 +114,7 @@ test.describe('Minimap', { tag: '@canvas' }, () => {
|
||||
})
|
||||
|
||||
test('Close button hides minimap', async ({ comfyPage }) => {
|
||||
const minimap = comfyPage.page.locator('.litegraph-minimap')
|
||||
const minimap = comfyPage.page.getByTestId(TestIds.canvas.minimapContainer)
|
||||
await expect(minimap).toBeVisible()
|
||||
|
||||
await comfyPage.page.getByTestId(TestIds.canvas.closeMinimapButton).click()
|
||||
@@ -88,7 +130,9 @@ test.describe('Minimap', { tag: '@canvas' }, () => {
|
||||
'Panning canvas moves minimap viewport',
|
||||
{ tag: '@screenshot' },
|
||||
async ({ comfyPage }) => {
|
||||
const minimap = comfyPage.page.locator('.litegraph-minimap')
|
||||
const minimap = comfyPage.page.getByTestId(
|
||||
TestIds.canvas.minimapContainer
|
||||
)
|
||||
await expect(minimap).toBeVisible()
|
||||
|
||||
await expect(minimap).toHaveScreenshot('minimap-before-pan.png')
|
||||
@@ -105,14 +149,135 @@ 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.locator('.litegraph-minimap')
|
||||
const minimap = comfyPage.page.getByTestId(
|
||||
TestIds.canvas.minimapContainer
|
||||
)
|
||||
await expect(minimap).toBeVisible()
|
||||
|
||||
const viewport = minimap.locator('.minimap-viewport')
|
||||
const viewport = minimap.getByTestId(TestIds.canvas.minimapViewport)
|
||||
await expect(viewport).toBeVisible()
|
||||
|
||||
await expect(async () => {
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
<div
|
||||
ref="containerRef"
|
||||
class="litegraph-minimap relative border border-interface-stroke bg-comfy-menu-bg shadow-interface"
|
||||
data-testid="minimap-container"
|
||||
:style="containerStyles"
|
||||
>
|
||||
<Button
|
||||
@@ -58,12 +59,18 @@
|
||||
:width="width"
|
||||
:height="height"
|
||||
class="minimap-canvas"
|
||||
data-testid="minimap-canvas"
|
||||
/>
|
||||
|
||||
<div class="minimap-viewport" :style="viewportStyles" />
|
||||
<div
|
||||
class="minimap-viewport"
|
||||
:style="viewportStyles"
|
||||
data-testid="minimap-viewport"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="absolute inset-0 touch-none"
|
||||
data-testid="minimap-interaction-overlay"
|
||||
@pointerdown="handlePointerDown"
|
||||
@pointermove="handlePointerMove"
|
||||
@pointerup="handlePointerUp"
|
||||
|
||||
Reference in New Issue
Block a user