From 165480bbacbdf4905f7e9260206fafcc050cf785 Mon Sep 17 00:00:00 2001 From: Laxman Reddy Aileni Date: Wed, 15 Jul 2026 00:41:14 +0000 Subject: [PATCH 1/2] fix: dismiss form item tooltip on click so it cannot block the chat The tooltip for a form item control (e.g. a toggle switch) was shown on mouseover and only hidden on mouseleave. When the control is clicked, toggling it can re-render the prompt input and remove the element before mouseleave fires, leaving the tooltip overlay orphaned on top of the chat and blocking interaction. Hide the tooltip on mousedown/click as well, which also clears any pending show timer. Adds tests covering show-on-hover, dismiss-on-click, and cancelling a pending tooltip. --- .../chat-item/chat-item-form-items.spec.ts | 89 +++++++++++++++++++ .../chat-item/chat-item-form-items.ts | 6 ++ 2 files changed, 95 insertions(+) diff --git a/src/components/__test__/chat-item/chat-item-form-items.spec.ts b/src/components/__test__/chat-item/chat-item-form-items.spec.ts index 326d939d..e49e4885 100644 --- a/src/components/__test__/chat-item/chat-item-form-items.spec.ts +++ b/src/components/__test__/chat-item/chat-item-form-items.spec.ts @@ -1,6 +1,20 @@ import { ChatItemFormItemsWrapper } from '../../chat-item/chat-item-form-items'; import { ChatItem, ChatItemType } from '../../../static'; +// Mock the overlay so we can observe when the tooltip is shown (constructed) and +// hidden (close() called), without depending on real positioning/rendering. +jest.mock('../../overlay', () => ({ + Overlay: jest.fn().mockImplementation(() => ({ + close: jest.fn() + })), + OverlayHorizontalDirection: { + START_TO_RIGHT: 'start-to-right' + }, + OverlayVerticalDirection: { + TO_TOP: 'to-top' + } +})); + describe('ChatItemFormItemsWrapper', () => { it('should render form items wrapper', () => { const wrapper = new ChatItemFormItemsWrapper({ @@ -121,4 +135,79 @@ describe('ChatItemFormItemsWrapper', () => { const textInput = wrapper.render.querySelector('input[type="text"]'); expect(textInput?.hasAttribute('disabled')).toBe(false); }); + + describe('form item tooltip lifecycle', () => { + const buildSwitchWrapper = (): ChatItemFormItemsWrapper => { + const chatItem: ChatItem = { + type: ChatItemType.PROMPT, + formItems: [ + { + id: 'agentic-toggle', + type: 'switch', + title: 'Agentic coding', + value: 'true', + tooltip: 'Turn OFF agentic coding', + alternateTooltip: 'Turn ON agentic coding' + } + ] + }; + return new ChatItemFormItemsWrapper({ tabId: 'test-tab', chatItem }); + }; + + beforeEach(() => { + jest.useFakeTimers(); + const { Overlay } = jest.requireMock('../../overlay'); + (Overlay as jest.Mock).mockClear(); + document.body.innerHTML = ''; + }); + + afterEach(() => { + jest.useRealTimers(); + document.body.innerHTML = ''; + }); + + it('should show the tooltip on hover after the delay', () => { + const wrapper = buildSwitchWrapper(); + document.body.appendChild(wrapper.render); + const switchEl = wrapper.render.querySelector('.mynah-form-input-wrapper') as HTMLElement; + expect(switchEl).not.toBeNull(); + + switchEl.dispatchEvent(new MouseEvent('mouseover', { bubbles: true })); + jest.advanceTimersByTime(350); + + const { Overlay } = jest.requireMock('../../overlay'); + expect(Overlay).toHaveBeenCalledTimes(1); + }); + + it('should dismiss the tooltip when the control is clicked (so it cannot block the chat after toggling)', () => { + const wrapper = buildSwitchWrapper(); + document.body.appendChild(wrapper.render); + const switchEl = wrapper.render.querySelector('.mynah-form-input-wrapper') as HTMLElement; + + switchEl.dispatchEvent(new MouseEvent('mouseover', { bubbles: true })); + jest.advanceTimersByTime(350); + + const { Overlay } = jest.requireMock('../../overlay'); + const overlayInstance = (Overlay as jest.Mock).mock.results[0].value; + + switchEl.dispatchEvent(new MouseEvent('click', { bubbles: true })); + expect(overlayInstance.close).toHaveBeenCalled(); + }); + + it('should cancel a pending tooltip when the control is clicked before it appears', () => { + const wrapper = buildSwitchWrapper(); + document.body.appendChild(wrapper.render); + const switchEl = wrapper.render.querySelector('.mynah-form-input-wrapper') as HTMLElement; + + // Hover starts the show timer, but the user clicks before the delay elapses. + switchEl.dispatchEvent(new MouseEvent('mouseover', { bubbles: true })); + switchEl.dispatchEvent(new MouseEvent('mousedown', { bubbles: true })); + switchEl.dispatchEvent(new MouseEvent('click', { bubbles: true })); + jest.advanceTimersByTime(350); + + // The overlay must never be created, otherwise it would linger over the chat. + const { Overlay } = jest.requireMock('../../overlay'); + expect(Overlay).not.toHaveBeenCalled(); + }); + }); }); diff --git a/src/components/chat-item/chat-item-form-items.ts b/src/components/chat-item/chat-item-form-items.ts index ca23d641..8ec1c969 100644 --- a/src/components/chat-item/chat-item-form-items.ts +++ b/src/components/chat-item/chat-item-form-items.ts @@ -291,6 +291,12 @@ export class ChatItemFormItemsWrapper { this.showTooltip(tooltipToShow, chatOption.render); } }, + // Dismiss the tooltip as soon as the control is activated. Toggling an + // option (e.g. a switch) can re-render the prompt input and remove this + // element before 'mouseleave' fires, which would otherwise leave the + // tooltip overlay orphaned on top of the chat and block interaction. + mousedown: this.hideTooltip, + click: this.hideTooltip, mouseleave: this.hideTooltip } }); From 145e3dc52100418366058bdee2d8caeec9150a92 Mon Sep 17 00:00:00 2001 From: Laxman Reddy Aileni Date: Wed, 15 Jul 2026 00:42:14 +0000 Subject: [PATCH 2/2] chore: coerce evaluate result to boolean in quick-action header flow Wrap the Playwright evaluate result in Boolean() so the value is a genuine boolean in every toolchain, satisfying strict-boolean- expressions. Keeps the repo lint gate green. --- ui-tests/__test__/flows/quick-action-commands-header.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui-tests/__test__/flows/quick-action-commands-header.ts b/ui-tests/__test__/flows/quick-action-commands-header.ts index c90e28b6..a0e6c7ae 100644 --- a/ui-tests/__test__/flows/quick-action-commands-header.ts +++ b/ui-tests/__test__/flows/quick-action-commands-header.ts @@ -137,9 +137,9 @@ export const verifyQuickActionCommandsHeaderStatusVariations = async (page: Page let foundStatusClass = false; for (const statusClass of statusClasses) { - const hasStatus = await headerElement.evaluate((el, className) => + const hasStatus = Boolean(await headerElement.evaluate((el, className) => el.classList.contains(className), statusClass - ); + )); if (hasStatus) { foundStatusClass = true; console.log(`Found status class: ${statusClass}`);