mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-24 06:35:10 +00:00
## Summary
Adds Playwright coverage for minimap **Level 1** gaps and **Level 4**
drag-to-pan behavior (`browser_tests/tests/minimap.spec.ts`).
## Changes
- [x] **Level 1.1** — After closing the minimap with the X button,
assert `Comfy.Minimap.Visible` is `false` (in addition to minimap hidden
and toolbar toggle still visible).
- [x] **Level 1.2** — New `@mobile` + `@canvas` suite: on a narrow
viewport (Pixel 5 project), assert default `Comfy.Minimap.Visible` is
`false`, minimap container is absent (`toHaveCount(0)`), and the toolbar
minimap toggle remains visible.
- [x] **Level 1.3** — New test: close via X, re-open via toolbar toggle;
assert minimap and viewport are visible and `Comfy.Minimap.Visible` is
`true`.
- [x] **Level 4.1** — Pointer drag on `minimap-interaction-overlay`
(`pointerdown` / `pointermove` / `pointerup`); use `expect.poll` on main
canvas offset so each move step updates pan; assert meaningful total pan
distance after drag.
- [x] **Level 4.2** — Same drag pattern; assert `.minimap-viewport`
`style.transform` updates mid-drag via `expect.poll` between move steps.
- [x] **Helpers** — `clientPointOnMinimapOverlay`,
`readMainCanvasOffset`, shared `MINIMAP_POINTER_OPTS` (no
`waitForTimeout`).
## Notes
- [x] Desktop minimap tests unchanged in intent; mobile-only cases live
in a separate `test.describe` so `beforeEach` does not force
`Comfy.Minimap.Visible` on small screens.
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Low Risk**
> Low risk: changes are limited to Playwright e2e tests and shared test
utilities, with no production logic modifications. Main risk is
increased test flakiness due to pointer-event simulation and
timing/polling on canvas state.
>
> **Overview**
> Expands minimap Playwright coverage by adding shared helpers
(`minimapUtils.ts`) for overlay-relative pointer coordinates, consistent
pointer event options, and reading the main canvas offset.
>
> Updates `minimap.spec.ts` to assert the `Comfy.Minimap.Visible`
setting toggles correctly when closing/reopening, adds
FitView+minimap-center regression coverage, and replaces mouse-drag with
explicit `pointerdown`/`pointermove`/`pointerup` overlay interactions
that assert progressive canvas panning and viewport `transform` updates.
Adds a new `@mobile` suite verifying the minimap is hidden by default on
small viewports while the toolbar toggle remains visible.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
7fad2a7dd3. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11192-test-extend-minimap-e2e-for-level-1-gaps-and-drag-panning-3416d73d36508194bc81f81a6d4536b8)
by [Unito](https://www.unito.io)
514 lines
16 KiB
TypeScript
514 lines
16 KiB
TypeScript
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'
|
||
import { TestIds } from '@e2e/fixtures/selectors'
|
||
import {
|
||
clientPointOnMinimapOverlay,
|
||
MINIMAP_POINTER_OPTS,
|
||
readMainCanvasOffset
|
||
} from '@e2e/fixtures/utils/minimapUtils'
|
||
|
||
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
|
||
})
|
||
}
|
||
|
||
function getMinimapLocators(comfyPage: ComfyPage) {
|
||
const container = comfyPage.page.getByTestId(TestIds.canvas.minimapContainer)
|
||
return {
|
||
container,
|
||
canvas: comfyPage.page.getByTestId(TestIds.canvas.minimapCanvas),
|
||
viewport: comfyPage.page.getByTestId(TestIds.canvas.minimapViewport),
|
||
toggleButton: comfyPage.page.getByTestId(
|
||
TestIds.canvas.toggleMinimapButton
|
||
),
|
||
closeButton: comfyPage.page.getByTestId(TestIds.canvas.closeMinimapButton)
|
||
}
|
||
}
|
||
|
||
async function clickMinimapAt(
|
||
overlay: Locator,
|
||
page: Page,
|
||
relX: number,
|
||
relY: number
|
||
) {
|
||
const { clientX, clientY } = await clientPointOnMinimapOverlay(
|
||
overlay,
|
||
relX,
|
||
relY
|
||
)
|
||
await page.mouse.click(clientX, clientY)
|
||
}
|
||
|
||
test.describe('Minimap', { tag: '@canvas' }, () => {
|
||
test.beforeEach(async ({ comfyPage }) => {
|
||
await comfyPage.settings.setSetting('Comfy.Minimap.Visible', true)
|
||
await comfyPage.settings.setSetting('Comfy.Graph.CanvasMenu', true)
|
||
await comfyPage.workflow.loadWorkflow('default')
|
||
await comfyPage.page.waitForFunction(() => window.app && window.app.canvas)
|
||
})
|
||
|
||
test('Validate minimap is visible by default', async ({ comfyPage }) => {
|
||
const { container, canvas, viewport } = getMinimapLocators(comfyPage)
|
||
|
||
await expect(container).toBeVisible()
|
||
await expect(canvas).toBeVisible()
|
||
await expect(viewport).toBeVisible()
|
||
|
||
await expect(container).toHaveCSS('position', 'relative')
|
||
|
||
// position and z-index validation moved to the parent container of the minimap
|
||
const minimapMainContainer = comfyPage.page.locator(
|
||
'.minimap-main-container'
|
||
)
|
||
await expect(minimapMainContainer).toHaveCSS('position', 'absolute')
|
||
await expect(minimapMainContainer).toHaveCSS('z-index', '1000')
|
||
})
|
||
|
||
test('Validate minimap toggle button state', async ({ comfyPage }) => {
|
||
const { container, toggleButton } = getMinimapLocators(comfyPage)
|
||
|
||
await expect(toggleButton).toBeVisible()
|
||
await expect(container).toBeVisible()
|
||
})
|
||
|
||
test('Validate minimap can be toggled off and on', async ({ comfyPage }) => {
|
||
const { container, toggleButton } = getMinimapLocators(comfyPage)
|
||
|
||
await expect(container).toBeVisible()
|
||
|
||
await toggleButton.click()
|
||
await comfyPage.nextFrame()
|
||
|
||
await expect(container).toBeHidden()
|
||
|
||
await toggleButton.click()
|
||
await comfyPage.nextFrame()
|
||
|
||
await expect(container).toBeVisible()
|
||
})
|
||
|
||
test('Validate minimap keyboard shortcut Alt+M', async ({ comfyPage }) => {
|
||
const { container } = getMinimapLocators(comfyPage)
|
||
|
||
await expect(container).toBeVisible()
|
||
|
||
await comfyPage.page.keyboard.press('Alt+KeyM')
|
||
await comfyPage.nextFrame()
|
||
|
||
await expect(container).toBeHidden()
|
||
|
||
await comfyPage.page.keyboard.press('Alt+KeyM')
|
||
await comfyPage.nextFrame()
|
||
|
||
await expect(container).toBeVisible()
|
||
})
|
||
|
||
test('Close button hides minimap; toolbar toggle reopens it', async ({
|
||
comfyPage
|
||
}) => {
|
||
const { container, viewport, toggleButton, closeButton } =
|
||
getMinimapLocators(comfyPage)
|
||
|
||
await expect(container).toBeVisible()
|
||
|
||
await closeButton.click()
|
||
await expect(container).toBeHidden()
|
||
|
||
await expect
|
||
.poll(() =>
|
||
comfyPage.settings.getSetting<boolean>('Comfy.Minimap.Visible')
|
||
)
|
||
.toBe(false)
|
||
|
||
await expect(toggleButton).toBeVisible()
|
||
|
||
await toggleButton.click()
|
||
await expect(container).toBeVisible()
|
||
await expect(viewport).toBeVisible()
|
||
|
||
await expect
|
||
.poll(() =>
|
||
comfyPage.settings.getSetting<boolean>('Comfy.Minimap.Visible')
|
||
)
|
||
.toBe(true)
|
||
})
|
||
|
||
test(
|
||
'Panning canvas moves minimap viewport',
|
||
{ tag: '@screenshot' },
|
||
async ({ comfyPage }) => {
|
||
const { container } = getMinimapLocators(comfyPage)
|
||
await expect(container).toBeVisible()
|
||
|
||
await expect(container).toHaveScreenshot('minimap-before-pan.png')
|
||
|
||
await comfyPage.page.evaluate(() => {
|
||
const canvas = window.app!.canvas
|
||
canvas.ds.scale = 3
|
||
canvas.ds.offset[0] = -800
|
||
canvas.ds.offset[1] = -600
|
||
canvas.setDirty(true, true)
|
||
})
|
||
await expect(container).toHaveScreenshot('minimap-after-pan.png')
|
||
}
|
||
)
|
||
|
||
test(
|
||
'Viewport rectangle is visible and positioned within minimap',
|
||
{ tag: '@screenshot' },
|
||
async ({ comfyPage }) => {
|
||
const { container, viewport } = getMinimapLocators(comfyPage)
|
||
await expect(container).toBeVisible()
|
||
await expect(viewport).toBeVisible()
|
||
|
||
const minimapBox = await container.boundingBox()
|
||
const viewportBox = await viewport.boundingBox()
|
||
|
||
expect(minimapBox).toBeTruthy()
|
||
expect(viewportBox).toBeTruthy()
|
||
expect(viewportBox!.width).toBeGreaterThan(0)
|
||
expect(viewportBox!.height).toBeGreaterThan(0)
|
||
|
||
expect(viewportBox!.x + viewportBox!.width).toBeGreaterThan(minimapBox!.x)
|
||
expect(viewportBox!.y + viewportBox!.height).toBeGreaterThan(
|
||
minimapBox!.y
|
||
)
|
||
expect(viewportBox!.x).toBeLessThan(minimapBox!.x + minimapBox!.width)
|
||
expect(viewportBox!.y).toBeLessThan(minimapBox!.y + minimapBox!.height)
|
||
|
||
await expect(container).toHaveScreenshot('minimap-with-viewport.png')
|
||
}
|
||
)
|
||
|
||
test('Clicking on minimap pans the canvas to that position', async ({
|
||
comfyPage
|
||
}) => {
|
||
const { container } = getMinimapLocators(comfyPage)
|
||
await expect(container).toBeVisible()
|
||
|
||
const offsetBefore = await readMainCanvasOffset(comfyPage.page)
|
||
|
||
const minimapBox = await container.boundingBox()
|
||
expect(minimapBox).toBeTruthy()
|
||
|
||
// Click the top-left quadrant — canvas should pan so that region
|
||
// becomes centered, meaning offset increases (moves right/down)
|
||
await comfyPage.page.mouse.click(
|
||
minimapBox!.x + minimapBox!.width * 0.2,
|
||
minimapBox!.y + minimapBox!.height * 0.2
|
||
)
|
||
await comfyPage.nextFrame()
|
||
|
||
await expect
|
||
.poll(() => readMainCanvasOffset(comfyPage.page))
|
||
.not.toEqual(offsetBefore)
|
||
})
|
||
|
||
test('Clicking minimap center after FitView causes minimal canvas movement', async ({
|
||
comfyPage
|
||
}) => {
|
||
const { container, viewport } = getMinimapLocators(comfyPage)
|
||
const overlay = comfyPage.page.getByTestId(
|
||
TestIds.canvas.minimapInteractionOverlay
|
||
)
|
||
await expect(container).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('Dragging on minimap pans the main canvas progressively', async ({
|
||
comfyPage
|
||
}) => {
|
||
const overlay = comfyPage.page.getByTestId(
|
||
TestIds.canvas.minimapInteractionOverlay
|
||
)
|
||
const initial = await readMainCanvasOffset(comfyPage.page)
|
||
|
||
const start = await clientPointOnMinimapOverlay(overlay, 0.48, 0.5)
|
||
await overlay.dispatchEvent('pointerdown', {
|
||
...MINIMAP_POINTER_OPTS,
|
||
clientX: start.clientX,
|
||
clientY: start.clientY
|
||
})
|
||
await comfyPage.nextFrame()
|
||
|
||
const firstMove = await clientPointOnMinimapOverlay(overlay, 0.35, 0.52)
|
||
await overlay.dispatchEvent('pointermove', {
|
||
...MINIMAP_POINTER_OPTS,
|
||
clientX: firstMove.clientX,
|
||
clientY: firstMove.clientY
|
||
})
|
||
await expect
|
||
.poll(() => readMainCanvasOffset(comfyPage.page))
|
||
.not.toStrictEqual(initial)
|
||
const afterFirstMove = await readMainCanvasOffset(comfyPage.page)
|
||
|
||
const secondMove = await clientPointOnMinimapOverlay(overlay, 0.22, 0.55)
|
||
await overlay.dispatchEvent('pointermove', {
|
||
...MINIMAP_POINTER_OPTS,
|
||
clientX: secondMove.clientX,
|
||
clientY: secondMove.clientY
|
||
})
|
||
await expect
|
||
.poll(() => readMainCanvasOffset(comfyPage.page))
|
||
.not.toStrictEqual(afterFirstMove)
|
||
|
||
const upPoint = await clientPointOnMinimapOverlay(overlay, 0.18, 0.58)
|
||
await overlay.dispatchEvent('pointerup', {
|
||
...MINIMAP_POINTER_OPTS,
|
||
clientX: upPoint.clientX,
|
||
clientY: upPoint.clientY
|
||
})
|
||
await comfyPage.nextFrame()
|
||
|
||
const final = await readMainCanvasOffset(comfyPage.page)
|
||
const panDistance =
|
||
Math.abs(final.x - initial.x) + Math.abs(final.y - initial.y)
|
||
expect(
|
||
panDistance,
|
||
'Expected drag from center toward lower-left on minimap to move the canvas meaningfully'
|
||
).toBeGreaterThan(40)
|
||
})
|
||
|
||
test('Minimap viewport transform updates during overlay drag', async ({
|
||
comfyPage
|
||
}) => {
|
||
const { container, viewport } = getMinimapLocators(comfyPage)
|
||
const overlay = comfyPage.page.getByTestId(
|
||
TestIds.canvas.minimapInteractionOverlay
|
||
)
|
||
await expect(container).toBeVisible()
|
||
|
||
const transformBefore = await viewport.evaluate(
|
||
(el: HTMLElement) => el.style.transform
|
||
)
|
||
|
||
const start = await clientPointOnMinimapOverlay(overlay, 0.48, 0.5)
|
||
await overlay.dispatchEvent('pointerdown', {
|
||
...MINIMAP_POINTER_OPTS,
|
||
clientX: start.clientX,
|
||
clientY: start.clientY
|
||
})
|
||
await comfyPage.nextFrame()
|
||
|
||
const mid = await clientPointOnMinimapOverlay(overlay, 0.28, 0.52)
|
||
await overlay.dispatchEvent('pointermove', {
|
||
...MINIMAP_POINTER_OPTS,
|
||
clientX: mid.clientX,
|
||
clientY: mid.clientY
|
||
})
|
||
|
||
await expect
|
||
.poll(() => viewport.evaluate((el: HTMLElement) => el.style.transform))
|
||
.not.toBe(transformBefore)
|
||
|
||
const transformMid = await viewport.evaluate(
|
||
(el: HTMLElement) => el.style.transform
|
||
)
|
||
|
||
const later = await clientPointOnMinimapOverlay(overlay, 0.18, 0.55)
|
||
await overlay.dispatchEvent('pointermove', {
|
||
...MINIMAP_POINTER_OPTS,
|
||
clientX: later.clientX,
|
||
clientY: later.clientY
|
||
})
|
||
|
||
await expect
|
||
.poll(() => viewport.evaluate((el: HTMLElement) => el.style.transform))
|
||
.not.toBe(transformMid)
|
||
|
||
const upPoint = await clientPointOnMinimapOverlay(overlay, 0.18, 0.55)
|
||
await overlay.dispatchEvent('pointerup', {
|
||
...MINIMAP_POINTER_OPTS,
|
||
clientX: upPoint.clientX,
|
||
clientY: upPoint.clientY
|
||
})
|
||
})
|
||
|
||
test('Minimap viewport updates when canvas is zoomed', async ({
|
||
comfyPage
|
||
}) => {
|
||
const { container, viewport } = getMinimapLocators(comfyPage)
|
||
await expect(container).toBeVisible()
|
||
await expect(viewport).toBeVisible()
|
||
|
||
const viewportBefore = await viewport.boundingBox()
|
||
expect(viewportBefore).toBeTruthy()
|
||
|
||
// Zoom in significantly
|
||
await comfyPage.page.evaluate(() => {
|
||
const canvas = window.app!.canvas
|
||
canvas.ds.scale = 3
|
||
canvas.setDirty(true, true)
|
||
})
|
||
await comfyPage.nextFrame()
|
||
|
||
// Viewport rectangle should shrink when zoomed in
|
||
await expect
|
||
.poll(async () => {
|
||
const box = await viewport.boundingBox()
|
||
return box?.width ?? 0
|
||
})
|
||
.toBeLessThan(viewportBefore!.width)
|
||
})
|
||
|
||
test('Minimap canvas is empty after all nodes are deleted', async ({
|
||
comfyPage
|
||
}) => {
|
||
const { canvas } = getMinimapLocators(comfyPage)
|
||
await expect(canvas).toBeVisible()
|
||
|
||
// Minimap should have content before deletion
|
||
await expect.poll(() => hasCanvasContent(canvas)).toBe(true)
|
||
|
||
// Remove all nodes
|
||
await comfyPage.canvas.press('Control+a')
|
||
await comfyPage.canvas.press('Delete')
|
||
await comfyPage.nextFrame()
|
||
|
||
await expect.poll(() => comfyPage.nodeOps.getGraphNodesCount()).toBe(0)
|
||
|
||
// Minimap canvas should be empty — no nodes means nothing to render
|
||
await expect
|
||
.poll(() => hasCanvasContent(canvas), { timeout: 5000 })
|
||
.toBe(false)
|
||
})
|
||
|
||
test('Minimap re-renders after loading a different workflow', async ({
|
||
comfyPage
|
||
}) => {
|
||
const { canvas } = getMinimapLocators(comfyPage)
|
||
await expect(canvas).toBeVisible()
|
||
|
||
// Default workflow has content
|
||
await expect.poll(() => hasCanvasContent(canvas)).toBe(true)
|
||
|
||
// Load a very different workflow
|
||
await comfyPage.workflow.loadWorkflow('large-graph-workflow')
|
||
await comfyPage.nextFrame()
|
||
|
||
// Minimap should still have content (different workflow, still has nodes)
|
||
await expect
|
||
.poll(() => hasCanvasContent(canvas), { timeout: 5000 })
|
||
.toBe(true)
|
||
})
|
||
|
||
test('Minimap viewport position reflects canvas pan state', async ({
|
||
comfyPage
|
||
}) => {
|
||
const { container, viewport } = getMinimapLocators(comfyPage)
|
||
await expect(container).toBeVisible()
|
||
await expect(viewport).toBeVisible()
|
||
|
||
const positionBefore = await viewport.boundingBox()
|
||
expect(positionBefore).toBeTruthy()
|
||
|
||
// Pan the canvas by a large amount to the right and down
|
||
await comfyPage.page.evaluate(() => {
|
||
const canvas = window.app!.canvas
|
||
canvas.ds.offset[0] -= 500
|
||
canvas.ds.offset[1] -= 500
|
||
canvas.setDirty(true, true)
|
||
})
|
||
await comfyPage.nextFrame()
|
||
|
||
// The viewport indicator should have moved within the minimap
|
||
await expect
|
||
.poll(async () => {
|
||
const box = await viewport.boundingBox()
|
||
if (!box || !positionBefore) return false
|
||
return box.x !== positionBefore.x || box.y !== positionBefore.y
|
||
})
|
||
.toBe(true)
|
||
})
|
||
})
|
||
|
||
test.describe('Minimap mobile', { tag: ['@mobile', '@canvas'] }, () => {
|
||
test.beforeEach(async ({ comfyPage }) => {
|
||
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
|
||
await comfyPage.settings.setSetting('Comfy.Graph.CanvasMenu', true)
|
||
await comfyPage.workflow.loadWorkflow('default')
|
||
await comfyPage.page.waitForFunction(() => window.app && window.app.canvas)
|
||
})
|
||
|
||
test('Minimap is hidden by default on small viewports', async ({
|
||
comfyPage
|
||
}) => {
|
||
const innerWidth = await comfyPage.page.evaluate(() => window.innerWidth)
|
||
expect(
|
||
innerWidth,
|
||
'Mobile project should use a viewport narrower than the lg breakpoint'
|
||
).toBeLessThan(1024)
|
||
|
||
await expect
|
||
.poll(() =>
|
||
comfyPage.settings.getSetting<boolean>('Comfy.Minimap.Visible')
|
||
)
|
||
.toBe(false)
|
||
|
||
await expect(
|
||
comfyPage.page.getByTestId(TestIds.canvas.minimapContainer)
|
||
).toHaveCount(0)
|
||
|
||
const toggle = comfyPage.page.getByTestId(
|
||
TestIds.canvas.toggleMinimapButton
|
||
)
|
||
await expect(toggle).toBeVisible()
|
||
})
|
||
})
|