From 14e7f0ec1b57b7ccd1112ab20187db45258aa70a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 02:15:49 +0000 Subject: [PATCH 1/3] Initial plan From d144e156127b0fa49409d4f874e1006e6bb01472 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 02:27:16 +0000 Subject: [PATCH 2/3] Add unit tests for primer_react_action_menu_display_in_viewport_inside_portal feature flag Co-authored-by: francinelucca <40550942+francinelucca@users.noreply.github.com> --- .../react/src/ActionMenu/ActionMenu.test.tsx | 190 ++++++++++++++++++ 1 file changed, 190 insertions(+) diff --git a/packages/react/src/ActionMenu/ActionMenu.test.tsx b/packages/react/src/ActionMenu/ActionMenu.test.tsx index 0be609c68e8..562b7d0856a 100644 --- a/packages/react/src/ActionMenu/ActionMenu.test.tsx +++ b/packages/react/src/ActionMenu/ActionMenu.test.tsx @@ -12,6 +12,8 @@ import {SearchIcon, KebabHorizontalIcon} from '@primer/octicons-react' import type {JSX} from 'react' import {implementsClassName} from '../utils/testing' +import {FeatureFlags} from '../FeatureFlags' +import Portal from '../Portal' function Example(): JSX.Element { return ( @@ -812,4 +814,192 @@ describe('ActionMenu', () => { expect(mockOnKeyDown).toHaveBeenCalledTimes(1) }) }) + + describe('feature flag: primer_react_action_menu_display_in_viewport_inside_portal', () => { + it('should enable displayInViewport when flag is enabled and ActionMenu is inside a portal', async () => { + const mockOnPositionChange = vi.fn() + + // When the ActionMenu is wrapped in a Portal, it's inside a portal context. + // With the flag enabled, displayInViewport should be automatically enabled. + const component = HTMLRender( + + + + Toggle Menu + + + New file + + + + + , + ) + + const user = userEvent.setup() + const button = component.getByRole('button') + await user.click(button) + + await waitFor(() => { + expect(component.queryByRole('menu')).toBeInTheDocument() + }) + + // Verify the menu rendered and positioning was applied + expect(mockOnPositionChange).toHaveBeenCalled() + }) + + it('should not enable displayInViewport when flag is enabled but ActionMenu is NOT inside a portal', async () => { + const mockOnPositionChange = vi.fn() + + // Without being wrapped in a Portal, the ActionMenu is not in a portal context. + // Even with the flag enabled, displayInViewport should remain at its default (false). + const component = HTMLRender( + + + Toggle Menu + + + New file + + + + , + ) + + const user = userEvent.setup() + const button = component.getByRole('button') + await user.click(button) + + await waitFor(() => { + expect(component.queryByRole('menu')).toBeInTheDocument() + }) + + // Verify the menu rendered correctly without displayInViewport + expect(mockOnPositionChange).toHaveBeenCalled() + }) + + it('should not enable displayInViewport when flag is disabled, even inside a portal', async () => { + const mockOnPositionChange = vi.fn() + + // Even when inside a Portal, with the flag disabled, displayInViewport + // should remain at its default (false). + const component = HTMLRender( + + + + Toggle Menu + + + New file + + + + + , + ) + + const user = userEvent.setup() + const button = component.getByRole('button') + await user.click(button) + + await waitFor(() => { + expect(component.queryByRole('menu')).toBeInTheDocument() + }) + + // Verify the menu rendered with default displayInViewport behavior + expect(mockOnPositionChange).toHaveBeenCalled() + }) + + it('should not enable displayInViewport when flag is disabled and outside portal', async () => { + const mockOnPositionChange = vi.fn() + + // Default scenario: flag disabled and not in a portal context. + // displayInViewport should remain at its default (false). + const component = HTMLRender( + + + Toggle Menu + + + New file + + + + , + ) + + const user = userEvent.setup() + const button = component.getByRole('button') + await user.click(button) + + await waitFor(() => { + expect(component.queryByRole('menu')).toBeInTheDocument() + }) + + // Verify default behavior + expect(mockOnPositionChange).toHaveBeenCalled() + }) + + it('should respect explicit displayInViewport prop over feature flag logic', async () => { + const mockOnPositionChange = vi.fn() + + // Test that an explicit displayInViewport=false prop overrides the automatic + // detection, even when the flag is enabled and the ActionMenu is inside a portal. + const component = HTMLRender( + + + + Toggle Menu + + + New file + + + + + , + ) + + const user = userEvent.setup() + const button = component.getByRole('button') + await user.click(button) + + await waitFor(() => { + expect(component.queryByRole('menu')).toBeInTheDocument() + }) + + // Verify explicit prop was respected + expect(mockOnPositionChange).toHaveBeenCalled() + }) + + it('should respect explicit displayInViewport=true prop even when flag is disabled', async () => { + const mockOnPositionChange = vi.fn() + + // Test that an explicit displayInViewport=true prop works regardless of + // the flag state or portal context. + const component = HTMLRender( + + + Toggle Menu + + + New file + + + + , + ) + + const user = userEvent.setup() + const button = component.getByRole('button') + await user.click(button) + + await waitFor(() => { + expect(component.queryByRole('menu')).toBeInTheDocument() + }) + + // Verify explicit prop was respected + expect(mockOnPositionChange).toHaveBeenCalled() + }) + }) }) From 2faf94e09e101713aa02255c252702c321050b06 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 03:09:06 +0000 Subject: [PATCH 3/3] Mock getAnchoredPosition to verify displayInViewport settings in tests Co-authored-by: francinelucca <40550942+francinelucca@users.noreply.github.com> --- .../react/src/ActionMenu/ActionMenu.test.tsx | 122 +++++++++++++----- 1 file changed, 88 insertions(+), 34 deletions(-) diff --git a/packages/react/src/ActionMenu/ActionMenu.test.tsx b/packages/react/src/ActionMenu/ActionMenu.test.tsx index 562b7d0856a..0c93dc0b7a9 100644 --- a/packages/react/src/ActionMenu/ActionMenu.test.tsx +++ b/packages/react/src/ActionMenu/ActionMenu.test.tsx @@ -1,4 +1,4 @@ -import {describe, expect, it, vi} from 'vitest' +import {describe, expect, it, vi, beforeEach} from 'vitest' import {render as HTMLRender, waitFor, act, within} from '@testing-library/react' import userEvent from '@testing-library/user-event' import type React from 'react' @@ -9,12 +9,35 @@ import {Tooltip as TooltipV2} from '../TooltipV2/Tooltip' import {SingleSelect} from '../ActionMenu/ActionMenu.features.stories' import {MixedSelection} from '../ActionMenu/ActionMenu.examples.stories' import {SearchIcon, KebabHorizontalIcon} from '@primer/octicons-react' +import {getAnchoredPosition} from '@primer/behaviors' +import type {AnchorPosition} from '@primer/behaviors' import type {JSX} from 'react' import {implementsClassName} from '../utils/testing' import {FeatureFlags} from '../FeatureFlags' import Portal from '../Portal' +// Mock getAnchoredPosition for feature flag tests +vi.mock('@primer/behaviors', async () => { + const actual = await vi.importActual('@primer/behaviors') + return { + ...actual, + getAnchoredPosition: vi.fn( + ( + _floatingElement: Element, + _anchorElement: Element | DOMRect, + _settings?: Partial<{displayInViewport?: boolean}>, + ) => + ({ + top: 100, + left: 100, + anchorSide: 'outside-bottom', + anchorAlign: 'start', + }) as AnchorPosition, + ), + } +}) + function Example(): JSX.Element { return ( @@ -816,9 +839,14 @@ describe('ActionMenu', () => { }) describe('feature flag: primer_react_action_menu_display_in_viewport_inside_portal', () => { - it('should enable displayInViewport when flag is enabled and ActionMenu is inside a portal', async () => { - const mockOnPositionChange = vi.fn() + const mockGetAnchoredPosition = vi.mocked(getAnchoredPosition) + + beforeEach(() => { + // Reset mock before each test + mockGetAnchoredPosition.mockClear() + }) + it('should enable displayInViewport when flag is enabled and ActionMenu is inside a portal', async () => { // When the ActionMenu is wrapped in a Portal, it's inside a portal context. // With the flag enabled, displayInViewport should be automatically enabled. const component = HTMLRender( @@ -826,7 +854,7 @@ describe('ActionMenu', () => { Toggle Menu - + New file @@ -844,20 +872,24 @@ describe('ActionMenu', () => { expect(component.queryByRole('menu')).toBeInTheDocument() }) - // Verify the menu rendered and positioning was applied - expect(mockOnPositionChange).toHaveBeenCalled() + // Verify getAnchoredPosition was called with displayInViewport: true + await waitFor(() => { + expect(mockGetAnchoredPosition).toHaveBeenCalled() + }) + + const calls = mockGetAnchoredPosition.mock.calls + const lastCall = calls[calls.length - 1] + expect(lastCall[2]?.displayInViewport).toBe(true) }) it('should not enable displayInViewport when flag is enabled but ActionMenu is NOT inside a portal', async () => { - const mockOnPositionChange = vi.fn() - // Without being wrapped in a Portal, the ActionMenu is not in a portal context. - // Even with the flag enabled, displayInViewport should remain at its default (false). + // Even with the flag enabled, displayInViewport should remain at its default (false/undefined). const component = HTMLRender( Toggle Menu - + New file @@ -874,21 +906,25 @@ describe('ActionMenu', () => { expect(component.queryByRole('menu')).toBeInTheDocument() }) - // Verify the menu rendered correctly without displayInViewport - expect(mockOnPositionChange).toHaveBeenCalled() + // Verify getAnchoredPosition was called without displayInViewport enabled + await waitFor(() => { + expect(mockGetAnchoredPosition).toHaveBeenCalled() + }) + + const calls = mockGetAnchoredPosition.mock.calls + const lastCall = calls[calls.length - 1] + expect(lastCall[2]?.displayInViewport).not.toBe(true) }) it('should not enable displayInViewport when flag is disabled, even inside a portal', async () => { - const mockOnPositionChange = vi.fn() - // Even when inside a Portal, with the flag disabled, displayInViewport - // should remain at its default (false). + // should remain at its default (false/undefined). const component = HTMLRender( Toggle Menu - + New file @@ -906,20 +942,24 @@ describe('ActionMenu', () => { expect(component.queryByRole('menu')).toBeInTheDocument() }) - // Verify the menu rendered with default displayInViewport behavior - expect(mockOnPositionChange).toHaveBeenCalled() + // Verify getAnchoredPosition was called without displayInViewport enabled + await waitFor(() => { + expect(mockGetAnchoredPosition).toHaveBeenCalled() + }) + + const calls = mockGetAnchoredPosition.mock.calls + const lastCall = calls[calls.length - 1] + expect(lastCall[2]?.displayInViewport).not.toBe(true) }) it('should not enable displayInViewport when flag is disabled and outside portal', async () => { - const mockOnPositionChange = vi.fn() - // Default scenario: flag disabled and not in a portal context. - // displayInViewport should remain at its default (false). + // displayInViewport should remain at its default (false/undefined). const component = HTMLRender( Toggle Menu - + New file @@ -936,13 +976,17 @@ describe('ActionMenu', () => { expect(component.queryByRole('menu')).toBeInTheDocument() }) - // Verify default behavior - expect(mockOnPositionChange).toHaveBeenCalled() + // Verify getAnchoredPosition was called without displayInViewport enabled + await waitFor(() => { + expect(mockGetAnchoredPosition).toHaveBeenCalled() + }) + + const calls = mockGetAnchoredPosition.mock.calls + const lastCall = calls[calls.length - 1] + expect(lastCall[2]?.displayInViewport).not.toBe(true) }) it('should respect explicit displayInViewport prop over feature flag logic', async () => { - const mockOnPositionChange = vi.fn() - // Test that an explicit displayInViewport=false prop overrides the automatic // detection, even when the flag is enabled and the ActionMenu is inside a portal. const component = HTMLRender( @@ -950,7 +994,7 @@ describe('ActionMenu', () => { Toggle Menu - + New file @@ -968,20 +1012,24 @@ describe('ActionMenu', () => { expect(component.queryByRole('menu')).toBeInTheDocument() }) - // Verify explicit prop was respected - expect(mockOnPositionChange).toHaveBeenCalled() + // Verify getAnchoredPosition was called with displayInViewport: false (explicit override) + await waitFor(() => { + expect(mockGetAnchoredPosition).toHaveBeenCalled() + }) + + const calls = mockGetAnchoredPosition.mock.calls + const lastCall = calls[calls.length - 1] + expect(lastCall[2]?.displayInViewport).toBe(false) }) it('should respect explicit displayInViewport=true prop even when flag is disabled', async () => { - const mockOnPositionChange = vi.fn() - // Test that an explicit displayInViewport=true prop works regardless of // the flag state or portal context. const component = HTMLRender( Toggle Menu - + New file @@ -998,8 +1046,14 @@ describe('ActionMenu', () => { expect(component.queryByRole('menu')).toBeInTheDocument() }) - // Verify explicit prop was respected - expect(mockOnPositionChange).toHaveBeenCalled() + // Verify getAnchoredPosition was called with displayInViewport: true (explicit override) + await waitFor(() => { + expect(mockGetAnchoredPosition).toHaveBeenCalled() + }) + + const calls = mockGetAnchoredPosition.mock.calls + const lastCall = calls[calls.length - 1] + expect(lastCall[2]?.displayInViewport).toBe(true) }) }) })