mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-19 22:09:37 +00:00
test: add E2E tests for CanvasModeSelector toolbar component (#10934)
Adds `browser_tests/tests/canvasModeSelector.spec.ts`, covering the canvas toolbar mode-selector component that was introduced with no E2E coverage. Covers: - Trigger button: toolbar visibility, `aria-expanded` state, icon reflects active mode - Popover lifecycle: open on click, close on re-click / item selection / Escape - Mode switching: clicking Hand/Select drives `canvas.state.readOnly`; clicking the active item is a no-op - ARIA state: `aria-checked` and roving `tabindex` track active mode, including state driven by external commands - Keyboard navigation: ArrowDown/Up with wraparound, Escape restores focus to trigger — all using `toBeFocused()` retrying assertions - Focus management: popover auto-focuses the checked item on open - Keybinding integration: `H` / `V` keys update both `canvas.state.readOnly` and the trigger icon - Shortcut hint display: both menu items render non-empty key-sequence hints 22 tests across 7 `describe` groups. All selectors are ARIA-driven. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-10934-test-add-E2E-tests-for-CanvasModeSelector-toolbar-component-33b6d73d3650819cb2cfdca22bf0b9a5) by [Unito](https://www.unito.io)
This commit is contained in:
275
browser_tests/tests/canvasModeSelector.spec.ts
Normal file
275
browser_tests/tests/canvasModeSelector.spec.ts
Normal file
@@ -0,0 +1,275 @@
|
||||
import type { Page } from '@playwright/test'
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
const getLocators = (page: Page) => ({
|
||||
trigger: page.getByRole('button', { name: 'Canvas Mode' }),
|
||||
menu: page.getByRole('menu', { name: 'Canvas Mode' }),
|
||||
selectItem: page.getByRole('menuitemradio', { name: 'Select' }),
|
||||
handItem: page.getByRole('menuitemradio', { name: 'Hand' })
|
||||
})
|
||||
|
||||
const MODES = [
|
||||
{
|
||||
label: 'Select',
|
||||
activateCommand: 'Comfy.Canvas.Unlock',
|
||||
isReadOnly: false,
|
||||
iconPattern: /lucide--mouse-pointer-2/
|
||||
},
|
||||
{
|
||||
label: 'Hand',
|
||||
activateCommand: 'Comfy.Canvas.Lock',
|
||||
isReadOnly: true,
|
||||
iconPattern: /lucide--hand/
|
||||
}
|
||||
]
|
||||
|
||||
test.describe('CanvasModeSelector', { tag: '@canvas' }, () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.Graph.CanvasMenu', true)
|
||||
await comfyPage.command.executeCommand('Comfy.Canvas.Unlock')
|
||||
await comfyPage.nextFrame()
|
||||
})
|
||||
|
||||
test.describe('Trigger button', () => {
|
||||
test('visible in canvas toolbar with ARIA markup', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const { trigger } = getLocators(comfyPage.page)
|
||||
await expect(trigger).toBeVisible()
|
||||
await expect(trigger).toHaveAttribute('aria-expanded', 'false')
|
||||
})
|
||||
|
||||
for (const mode of MODES) {
|
||||
test(`shows ${mode.label}-mode icon on trigger button`, async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.command.executeCommand(mode.activateCommand)
|
||||
await comfyPage.nextFrame()
|
||||
const { trigger } = getLocators(comfyPage.page)
|
||||
const modeIcon = trigger.locator('i[aria-hidden="true"]').first()
|
||||
await expect(modeIcon).toHaveClass(mode.iconPattern)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
test.describe('Popover lifecycle', () => {
|
||||
test('opens when trigger is clicked', async ({ comfyPage }) => {
|
||||
const { trigger, menu } = getLocators(comfyPage.page)
|
||||
await trigger.click()
|
||||
await comfyPage.nextFrame()
|
||||
await expect(menu).toBeVisible()
|
||||
await expect(trigger).toHaveAttribute('aria-expanded', 'true')
|
||||
})
|
||||
|
||||
test('closes when trigger is clicked again', async ({ comfyPage }) => {
|
||||
const { trigger, menu } = getLocators(comfyPage.page)
|
||||
await trigger.click()
|
||||
await comfyPage.nextFrame()
|
||||
await expect(menu).toBeVisible()
|
||||
await trigger.click()
|
||||
await comfyPage.nextFrame()
|
||||
await expect(menu).not.toBeVisible()
|
||||
await expect(trigger).toHaveAttribute('aria-expanded', 'false')
|
||||
})
|
||||
|
||||
test('closes after a mode item is selected', async ({ comfyPage }) => {
|
||||
const { trigger, menu, handItem } = getLocators(comfyPage.page)
|
||||
await trigger.click()
|
||||
await comfyPage.nextFrame()
|
||||
await expect(menu).toBeVisible()
|
||||
await handItem.click()
|
||||
await comfyPage.nextFrame()
|
||||
await expect(menu).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('closes when Escape is pressed', async ({ comfyPage }) => {
|
||||
const { trigger, menu, selectItem } = getLocators(comfyPage.page)
|
||||
await trigger.click()
|
||||
await comfyPage.nextFrame()
|
||||
await expect(menu).toBeVisible()
|
||||
await selectItem.press('Escape')
|
||||
await comfyPage.nextFrame()
|
||||
await expect(menu).not.toBeVisible()
|
||||
await expect(trigger).toHaveAttribute('aria-expanded', 'false')
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Mode switching', () => {
|
||||
for (const mode of MODES) {
|
||||
test(`clicking "${mode.label}" sets canvas readOnly=${mode.isReadOnly}`, async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
if (!mode.isReadOnly) {
|
||||
await comfyPage.command.executeCommand('Comfy.Canvas.Lock')
|
||||
await comfyPage.nextFrame()
|
||||
}
|
||||
const { trigger, menu, selectItem, handItem } = getLocators(
|
||||
comfyPage.page
|
||||
)
|
||||
const item = mode.isReadOnly ? handItem : selectItem
|
||||
await trigger.click()
|
||||
await comfyPage.nextFrame()
|
||||
await expect(menu).toBeVisible()
|
||||
await item.click()
|
||||
await comfyPage.nextFrame()
|
||||
await expect
|
||||
.poll(() => comfyPage.canvasOps.isReadOnly())
|
||||
.toBe(mode.isReadOnly)
|
||||
})
|
||||
}
|
||||
|
||||
test('clicking the currently active item is a no-op', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
expect(
|
||||
await comfyPage.canvasOps.isReadOnly(),
|
||||
'Precondition: canvas starts in Select mode'
|
||||
).toBe(false)
|
||||
const { trigger, menu, selectItem } = getLocators(comfyPage.page)
|
||||
await trigger.click()
|
||||
await comfyPage.nextFrame()
|
||||
await expect(menu).toBeVisible()
|
||||
await selectItem.click()
|
||||
await comfyPage.nextFrame()
|
||||
await expect.poll(() => comfyPage.canvasOps.isReadOnly()).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('ARIA state', () => {
|
||||
test('aria-checked marks Select active on default load', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const { trigger, menu, selectItem, handItem } = getLocators(
|
||||
comfyPage.page
|
||||
)
|
||||
await trigger.click()
|
||||
await comfyPage.nextFrame()
|
||||
await expect(menu).toBeVisible()
|
||||
await expect(selectItem).toHaveAttribute('aria-checked', 'true')
|
||||
await expect(handItem).toHaveAttribute('aria-checked', 'false')
|
||||
})
|
||||
|
||||
for (const mode of MODES) {
|
||||
test(`tabindex=0 is on the active "${mode.label}" item`, async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.command.executeCommand(mode.activateCommand)
|
||||
await comfyPage.nextFrame()
|
||||
const { trigger, menu, selectItem, handItem } = getLocators(
|
||||
comfyPage.page
|
||||
)
|
||||
await trigger.click()
|
||||
await comfyPage.nextFrame()
|
||||
await expect(menu).toBeVisible()
|
||||
|
||||
const activeItem = mode.isReadOnly ? handItem : selectItem
|
||||
const inactiveItem = mode.isReadOnly ? selectItem : handItem
|
||||
|
||||
await expect(activeItem).toHaveAttribute('tabindex', '0')
|
||||
await expect(inactiveItem).toHaveAttribute('tabindex', '-1')
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
test.describe('Keyboard navigation', () => {
|
||||
test('ArrowDown moves focus from Select to Hand', async ({ comfyPage }) => {
|
||||
const { trigger, menu, selectItem, handItem } = getLocators(
|
||||
comfyPage.page
|
||||
)
|
||||
await trigger.click()
|
||||
await comfyPage.nextFrame()
|
||||
await expect(menu).toBeVisible()
|
||||
await selectItem.press('ArrowDown')
|
||||
await expect(handItem).toBeFocused()
|
||||
})
|
||||
|
||||
test('Escape closes popover and restores focus to trigger', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const { trigger, menu, selectItem, handItem } = getLocators(
|
||||
comfyPage.page
|
||||
)
|
||||
await trigger.click()
|
||||
await comfyPage.nextFrame()
|
||||
await expect(menu).toBeVisible()
|
||||
await selectItem.press('ArrowDown')
|
||||
await handItem.press('Escape')
|
||||
await comfyPage.nextFrame()
|
||||
await expect(menu).not.toBeVisible()
|
||||
await expect(trigger).toBeFocused()
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Focus management on open', () => {
|
||||
for (const mode of MODES) {
|
||||
test(`auto-focuses the checked "${mode.label}" item on open`, async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.command.executeCommand(mode.activateCommand)
|
||||
await comfyPage.nextFrame()
|
||||
const { trigger, menu, selectItem, handItem } = getLocators(
|
||||
comfyPage.page
|
||||
)
|
||||
const item = mode.isReadOnly ? handItem : selectItem
|
||||
await trigger.click()
|
||||
await comfyPage.nextFrame()
|
||||
await expect(menu).toBeVisible()
|
||||
await expect(item).toBeFocused()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
test.describe('Keybinding integration', { tag: '@keyboard' }, () => {
|
||||
test("'H' locks canvas and updates trigger icon to Hand", async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
expect(
|
||||
await comfyPage.canvasOps.isReadOnly(),
|
||||
'Precondition: canvas starts unlocked'
|
||||
).toBe(false)
|
||||
await comfyPage.canvas.press('KeyH')
|
||||
await comfyPage.nextFrame()
|
||||
expect(await comfyPage.canvasOps.isReadOnly()).toBe(true)
|
||||
const { trigger } = getLocators(comfyPage.page)
|
||||
const modeIcon = trigger.locator('i[aria-hidden="true"]').first()
|
||||
await expect(modeIcon).toHaveClass(/lucide--hand/)
|
||||
})
|
||||
|
||||
test("'V' unlocks canvas and updates trigger icon to Select", async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.command.executeCommand('Comfy.Canvas.Lock')
|
||||
await comfyPage.nextFrame()
|
||||
expect(
|
||||
await comfyPage.canvasOps.isReadOnly(),
|
||||
'Precondition: canvas starts locked'
|
||||
).toBe(true)
|
||||
await comfyPage.canvas.press('KeyV')
|
||||
await comfyPage.nextFrame()
|
||||
expect(await comfyPage.canvasOps.isReadOnly()).toBe(false)
|
||||
const { trigger } = getLocators(comfyPage.page)
|
||||
const modeIcon = trigger.locator('i[aria-hidden="true"]').first()
|
||||
await expect(modeIcon).toHaveClass(/lucide--mouse-pointer-2/)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Shortcut hint display', () => {
|
||||
test('menu items show non-empty keyboard shortcut text', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const { trigger, menu, selectItem, handItem } = getLocators(
|
||||
comfyPage.page
|
||||
)
|
||||
await trigger.click()
|
||||
await comfyPage.nextFrame()
|
||||
await expect(menu).toBeVisible()
|
||||
const selectHint = selectItem.getByTestId('shortcut-hint')
|
||||
const handHint = handItem.getByTestId('shortcut-hint')
|
||||
|
||||
await expect(selectHint).not.toBeEmpty()
|
||||
await expect(handHint).not.toBeEmpty()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -55,9 +55,11 @@
|
||||
<i class="icon-[lucide--mouse-pointer-2] size-4" aria-hidden="true" />
|
||||
<span>{{ $t('graphCanvasMenu.select') }}</span>
|
||||
</div>
|
||||
<span class="text-[9px] text-text-primary">{{
|
||||
unlockCommandText
|
||||
}}</span>
|
||||
<span
|
||||
class="text-[9px] text-text-primary"
|
||||
data-testid="shortcut-hint"
|
||||
>{{ unlockCommandText }}</span
|
||||
>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@@ -76,7 +78,11 @@
|
||||
<i class="icon-[lucide--hand] size-4" aria-hidden="true" />
|
||||
<span>{{ $t('graphCanvasMenu.hand') }}</span>
|
||||
</div>
|
||||
<span class="text-[9px] text-text-primary">{{ lockCommandText }}</span>
|
||||
<span
|
||||
class="text-[9px] text-text-primary"
|
||||
data-testid="shortcut-hint"
|
||||
>{{ lockCommandText }}</span
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
</Popover>
|
||||
|
||||
Reference in New Issue
Block a user