import React, { act } from 'react'; import { createRoot } from 'react-dom/client'; import { ProviderModelBadges } from '@renderer/components/runtime/ProviderModelBadges'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; function render(element: React.ReactElement): HTMLDivElement { const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); act(() => { root.render(element); }); return host; } describe('ProviderModelBadges', () => { beforeEach(() => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); }); afterEach(() => { document.body.innerHTML = ''; }); it('does not render stale availability chips for OpenCode models', () => { const host = render( ); expect(host.textContent).toContain('gpt-oss'); expect(host.textContent).not.toContain('Check failed'); }); it('keeps availability chips for providers that still support explicit badge checks', () => { const host = render( ); expect(host.textContent).toContain('Check failed'); }); it('renders catalog badges from verbose provider metadata', () => { const host = render( ); expect(host.textContent).toContain('big-pickle'); expect(host.textContent).toContain('Free'); }); it('renders paid and free OpenCode models together without marking every model free', () => { const host = render( ); expect(host.textContent).toContain('big-pickle'); expect(host.textContent).toContain('GPT-5.4'); expect(host.textContent?.match(/Free/g)).toHaveLength(1); }); it('uses the OpenCode catalog when provider models are summary-only', () => { const host = render( ); expect(host.textContent).toContain('big-pickle'); expect(host.textContent).toContain('GPT-5.4'); expect(host.textContent).not.toContain('hidden-model'); }); it('renders OpenCode free badges from metadata when badgeLabel is absent', () => { const host = render( ); expect(host.textContent).toContain('gpt-oss'); expect(host.textContent).toContain('Free'); }); it('does not duplicate a catalog badge that matches the displayed model label', () => { const host = render( ); expect(host.textContent?.match(/Opus 4\.6/g)).toHaveLength(1); }); it('collapses long model lists and expands them inline without an internal scroll area', () => { const models = Array.from( { length: 18 }, (_, index) => `model-${String(index + 1).padStart(2, '0')}` ); const host = render( ); expect(host.textContent).toContain('model-15'); expect(host.textContent).not.toContain('model-16'); expect(host.textContent).toContain('+3 more'); const moreButton = Array.from(host.querySelectorAll('button')).find((button) => button.textContent?.includes('+3 more') ); expect(moreButton).toBeTruthy(); act(() => { moreButton?.click(); }); expect(host.textContent).toContain('model-18'); expect(host.textContent).toContain('Hide'); const list = host.firstElementChild?.firstElementChild as HTMLElement | null; expect(list?.style.maxHeight).toBe(''); expect(list?.style.overflowY).toBe(''); const hideButton = Array.from(host.querySelectorAll('button')).find((button) => button.textContent?.includes('Hide') ); expect(hideButton).toBeTruthy(); act(() => { hideButton?.click(); }); expect(host.textContent).not.toContain('model-16'); expect(host.textContent).toContain('+3 more'); }); it('limits collapsed model badges by rendered rows when requested', () => { const originalOffsetTop = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'offsetTop'); Object.defineProperty(HTMLElement.prototype, 'offsetTop', { configurable: true, get() { const siblings = Array.from(this.parentElement?.children ?? []); const index = Math.max(0, siblings.indexOf(this)); return Math.floor(index / 3) * 20; }, }); try { const models = Array.from( { length: 18 }, (_, index) => `model-${String(index + 1).padStart(2, '0')}` ); const host = render( ); expect(host.textContent).toContain('model-05'); expect(host.textContent).not.toContain('model-06'); expect(host.textContent).toContain('+13 more'); } finally { if (originalOffsetTop) { Object.defineProperty(HTMLElement.prototype, 'offsetTop', originalOffsetTop); } else { delete (HTMLElement.prototype as { offsetTop?: number }).offsetTop; } } }); });