diff --git a/browser_tests/fixtures/selectors.ts b/browser_tests/fixtures/selectors.ts index 59a64b5355..f78e75645d 100644 --- a/browser_tests/fixtures/selectors.ts +++ b/browser_tests/fixtures/selectors.ts @@ -100,7 +100,8 @@ export const TestIds = { decrement: 'decrement', increment: 'increment', domWidgetTextarea: 'dom-widget-textarea', - subgraphEnterButton: 'subgraph-enter-button' + subgraphEnterButton: 'subgraph-enter-button', + formDropdownMenu: 'form-dropdown-menu' }, builder: { footerNav: 'builder-footer-nav', diff --git a/browser_tests/tests/vueNodes/widgets/load/formDropdownPosition.spec.ts b/browser_tests/tests/vueNodes/widgets/load/formDropdownPosition.spec.ts new file mode 100644 index 0000000000..f0d3db58a2 --- /dev/null +++ b/browser_tests/tests/vueNodes/widgets/load/formDropdownPosition.spec.ts @@ -0,0 +1,124 @@ +import { + comfyExpect as expect, + comfyPageFixture as test +} from '../../../../fixtures/ComfyPage' +import { TestIds } from '../../../../fixtures/selectors' + +test.describe( + 'FormDropdown positioning in Vue nodes', + { tag: ['@widget', '@node'] }, + () => { + test.beforeEach(async ({ comfyPage }) => { + await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.workflow.loadWorkflow('widgets/load_image_widget') + await comfyPage.vueNodes.waitForNodes() + }) + + test('dropdown menu appears directly below the trigger', async ({ + comfyPage + }) => { + const node = comfyPage.vueNodes.getNodeByTitle('Load Image') + await expect(node).toBeVisible() + + const trigger = node.locator( + 'button:has(> span > span), button:has(i.icon-\\[lucide--chevron-down\\])' + ) + await trigger.first().click() + + const menu = comfyPage.page.getByTestId(TestIds.widgets.formDropdownMenu) + await expect(menu).toBeVisible({ timeout: 5000 }) + + const triggerBox = await trigger.first().boundingBox() + const menuBox = await menu.boundingBox() + + expect(triggerBox).toBeTruthy() + expect(menuBox).toBeTruthy() + + // Menu top should be near the trigger bottom (within 20px tolerance for padding) + expect(menuBox!.y).toBeGreaterThanOrEqual( + triggerBox!.y + triggerBox!.height - 5 + ) + expect(menuBox!.y).toBeLessThanOrEqual( + triggerBox!.y + triggerBox!.height + 20 + ) + + // Menu left should be near the trigger left (within 10px tolerance) + expect(menuBox!.x).toBeGreaterThanOrEqual(triggerBox!.x - 10) + expect(menuBox!.x).toBeLessThanOrEqual(triggerBox!.x + 10) + }) + + test('dropdown menu appears correctly at different zoom levels', async ({ + comfyPage + }) => { + for (const zoom of [0.5, 1.5]) { + // Set zoom via canvas + await comfyPage.page.evaluate((scale) => { + const canvas = window.app!.canvas + canvas.ds.scale = scale + canvas.setDirty(true, true) + }, zoom) + await comfyPage.nextFrame() + + const node = comfyPage.vueNodes.getNodeByTitle('Load Image') + await expect(node).toBeVisible() + + const trigger = node.locator( + 'button:has(i.icon-\\[lucide--chevron-down\\])' + ) + await trigger.first().click() + + const menu = comfyPage.page.getByTestId( + TestIds.widgets.formDropdownMenu + ) + await expect(menu).toBeVisible({ timeout: 5000 }) + + const triggerBox = await trigger.first().boundingBox() + const menuBox = await menu.boundingBox() + + expect(triggerBox).toBeTruthy() + expect(menuBox).toBeTruthy() + + // Menu top should still be near trigger bottom regardless of zoom + expect(menuBox!.y).toBeGreaterThanOrEqual( + triggerBox!.y + triggerBox!.height - 5 + ) + expect(menuBox!.y).toBeLessThanOrEqual( + triggerBox!.y + triggerBox!.height + 20 * zoom + ) + + // Close dropdown before next iteration + await comfyPage.page.keyboard.press('Escape') + await expect(menu).not.toBeVisible() + } + }) + + test('dropdown closes on outside click', async ({ comfyPage }) => { + const node = comfyPage.vueNodes.getNodeByTitle('Load Image') + const trigger = node.locator( + 'button:has(i.icon-\\[lucide--chevron-down\\])' + ) + await trigger.first().click() + + const menu = comfyPage.page.getByTestId(TestIds.widgets.formDropdownMenu) + await expect(menu).toBeVisible({ timeout: 5000 }) + + // Click outside the node + await comfyPage.page.mouse.click(10, 10) + await expect(menu).not.toBeVisible() + }) + + test('dropdown closes on Escape key', async ({ comfyPage }) => { + const node = comfyPage.vueNodes.getNodeByTitle('Load Image') + const trigger = node.locator( + 'button:has(i.icon-\\[lucide--chevron-down\\])' + ) + await trigger.first().click() + + const menu = comfyPage.page.getByTestId(TestIds.widgets.formDropdownMenu) + await expect(menu).toBeVisible({ timeout: 5000 }) + + await comfyPage.page.keyboard.press('Escape') + await expect(menu).not.toBeVisible() + }) + } +) diff --git a/src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenu.vue b/src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenu.vue index 1ee7cccc4d..4c3d498445 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenu.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenu.vue @@ -97,6 +97,7 @@ const virtualItems = computed(() =>