Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions src/components/__test__/chat-item/chat-item-form-items.spec.ts
Original file line number Diff line number Diff line change
@@ -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({
Expand Down Expand Up @@ -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();
});
});
});
6 changes: 6 additions & 0 deletions src/components/chat-item/chat-item-form-items.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
});
Expand Down
4 changes: 2 additions & 2 deletions ui-tests/__test__/flows/quick-action-commands-header.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
Expand Down
Loading