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)
})
})
})