From ff5a09856e5f80394ce078d505fc51b3169b33c7 Mon Sep 17 00:00:00 2001 From: Laxman Reddy Aileni Date: Tue, 14 Jul 2026 23:51:27 +0000 Subject: [PATCH 1/3] fix: render model selector tooltip as markdown instead of raw HTML The select component built its hover tooltip using raw HTML tags ( and
), but tooltip content is rendered through the markdown parser (via CardBody), which escapes raw HTML. As a result the literal tags were shown to users instead of a formatted label and description. Use markdown syntax (bold + line break) so the selected option's label and description render as intended. Adds unit tests covering the rendered tooltip content. --- .../components/form-items/select.spec.ts | 98 +++++++++++++++++++ src/components/form-items/select.ts | 6 +- 2 files changed, 102 insertions(+), 2 deletions(-) diff --git a/src/__test__/components/form-items/select.spec.ts b/src/__test__/components/form-items/select.spec.ts index f7fa0eec3..934622ba0 100644 --- a/src/__test__/components/form-items/select.spec.ts +++ b/src/__test__/components/form-items/select.spec.ts @@ -6,6 +6,21 @@ import { Select, SelectInternal, SelectProps } from '../../../components/form-items/select'; import { MynahIcons } from '../../../components/icon'; import { DomBuilder } from '../../../helper/dom'; +import { configureMarked } from '../../../helper/marked'; + +// Mock the overlay component so we can capture the tooltip content that gets rendered, +// without depending on the real Overlay's positioning logic. +jest.mock('../../../components/overlay', () => ({ + Overlay: jest.fn().mockImplementation(() => ({ + close: jest.fn() + })), + OverlayHorizontalDirection: { + START_TO_RIGHT: 'start-to-right' + }, + OverlayVerticalDirection: { + TO_TOP: 'to-top' + } +})); describe('Select Component', () => { let select: SelectInternal; @@ -361,4 +376,87 @@ describe('Select Component', () => { expect(select.getValue()).toBe('option2'); }); }); + + describe('Tooltip content', () => { + // Options that carry a description mirror the model-selector use case, where the + // hover tooltip shows the selected model's label and description. + const optionsWithDescription = [ + { value: 'model1', label: 'Claude Sonnet 4', description: 'Hybrid reasoning and coding for regular use' }, + { value: 'model2', label: 'Claude Haiku', description: 'Fast responses for lightweight tasks' } + ]; + + beforeEach(() => { + // The tooltip is rendered through the markdown parser, which (in production) escapes + // raw HTML. Configure marked here so the test reflects real rendering behavior. + configureMarked(); + jest.useFakeTimers(); + const { Overlay } = jest.requireMock('../../../components/overlay'); + (Overlay as jest.Mock).mockClear(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + const openTooltip = (): HTMLElement => { + document.body.appendChild(select.render); + const container = document.body.querySelector('.mynah-form-input-container') as HTMLElement; + container.dispatchEvent(new MouseEvent('mouseenter')); + jest.advanceTimersByTime(350); + + const { Overlay } = jest.requireMock('../../../components/overlay'); + expect(Overlay).toHaveBeenCalledTimes(1); + const overlayProps = (Overlay as jest.Mock).mock.calls[0][0]; + // The overlay renders a Card whose body is the parsed tooltip content. + return overlayProps.children[0] as HTMLElement; + }; + + it('should render the tooltip as markdown, not raw HTML tags', () => { + select = new SelectInternal({ + options: optionsWithDescription, + value: 'model1' + }); + + const tooltipContent = openTooltip(); + + // The label must render as a real bold element, not literal "" text. + const strongElement = tooltipContent.querySelector('strong'); + expect(strongElement).not.toBeNull(); + expect(strongElement?.textContent).toBe('Claude Sonnet 4'); + + // Regression guard: the raw HTML tags must never leak into the visible text. + expect(tooltipContent.textContent).not.toContain(''); + expect(tooltipContent.textContent).not.toContain('
'); + + // Both label and description should be present in the rendered tooltip. + expect(tooltipContent.textContent).toContain('Claude Sonnet 4'); + expect(tooltipContent.textContent).toContain('Hybrid reasoning and coding for regular use'); + }); + + it('should reflect the currently selected option in the tooltip', () => { + select = new SelectInternal({ + options: optionsWithDescription, + value: 'model1' + }); + select.setValue('model2'); + + const tooltipContent = openTooltip(); + + expect(tooltipContent.querySelector('strong')?.textContent).toBe('Claude Haiku'); + expect(tooltipContent.textContent).toContain('Fast responses for lightweight tasks'); + expect(tooltipContent.textContent).not.toContain(''); + }); + + it('should fall back to the base tooltip when the option has no description', () => { + select = new SelectInternal({ + options: testOptions, + value: 'option1', + tooltip: 'Base tooltip text' + }); + + const tooltipContent = openTooltip(); + + expect(tooltipContent.textContent).toContain('Base tooltip text'); + }); + }); }); diff --git a/src/components/form-items/select.ts b/src/components/form-items/select.ts index 31c7b2d60..cdda65b57 100644 --- a/src/components/form-items/select.ts +++ b/src/components/form-items/select.ts @@ -149,9 +149,11 @@ export class SelectInternal { const currentValue = this.selectElement.value; const selectedOption = this.props.options?.find(option => option.value === currentValue); - // If there's a selected option, show label and description; otherwise use the base tooltip + // If there's a selected option, show label and description; otherwise use the base tooltip. + // The tooltip is rendered through the markdown parser (via CardBody), which escapes raw HTML, + // so use markdown syntax (bold + line break) instead of HTML tags to avoid showing literal markup. if (selectedOption?.description != null) { - return `${selectedOption.label}
${selectedOption.description}`; + return `**${selectedOption.label}**\n${selectedOption.description}`; } return this.props.tooltip ?? ''; }; From 5584c1dc5323486a29017a4d336c779057f0a1f4 Mon Sep 17 00:00:00 2001 From: Laxman Reddy Aileni Date: Wed, 15 Jul 2026 00:01:06 +0000 Subject: [PATCH 2/3] chore: satisfy strict-boolean-expressions in quick-action header flow Use an explicit boolean comparison for the value returned by the Playwright evaluate call, which is typed as any. Fixes an eslint error that blocked the pre-push lint hook. --- ui-tests/__test__/flows/quick-action-commands-header.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-tests/__test__/flows/quick-action-commands-header.ts b/ui-tests/__test__/flows/quick-action-commands-header.ts index c90e28b6f..ae6907d9d 100644 --- a/ui-tests/__test__/flows/quick-action-commands-header.ts +++ b/ui-tests/__test__/flows/quick-action-commands-header.ts @@ -140,7 +140,7 @@ export const verifyQuickActionCommandsHeaderStatusVariations = async (page: Page const hasStatus = await headerElement.evaluate((el, className) => el.classList.contains(className), statusClass ); - if (hasStatus) { + if (hasStatus === true) { foundStatusClass = true; console.log(`Found status class: ${statusClass}`); break; From 03e05e64a61532c787fd2067bb26db871eb27bd7 Mon Sep 17 00:00:00 2001 From: Laxman Reddy Aileni Date: Wed, 15 Jul 2026 00:10:14 +0000 Subject: [PATCH 3/3] 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. This satisfies strict-boolean- expressions without introducing an unnecessary boolean-literal comparison, keeping the lint gate green across environments. --- ui-tests/__test__/flows/quick-action-commands-header.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ui-tests/__test__/flows/quick-action-commands-header.ts b/ui-tests/__test__/flows/quick-action-commands-header.ts index ae6907d9d..a0e6c7ae8 100644 --- a/ui-tests/__test__/flows/quick-action-commands-header.ts +++ b/ui-tests/__test__/flows/quick-action-commands-header.ts @@ -137,10 +137,10 @@ 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 === true) { + )); + if (hasStatus) { foundStatusClass = true; console.log(`Found status class: ${statusClass}`); break;