fix(web): resolve model provider console warnings (#36422)

This commit is contained in:
yyh 2026-05-20 12:02:01 +08:00 committed by GitHub
parent 7bc5c89e3c
commit 77f1aeb1ac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 105 additions and 41 deletions

View File

@ -21,6 +21,12 @@ describe('CustomizeModal', () => {
vi.clearAllMocks()
})
const getAnchorButton = (name: RegExp) => {
const button = screen.getByRole('button', { name })
expect(button.tagName).toBe('A')
return button as HTMLAnchorElement
}
// Rendering tests - verify component renders correctly with various configurations
describe('Rendering', () => {
it('should render without crashing when isShow is true', async () => {
@ -131,7 +137,7 @@ describe('CustomizeModal', () => {
// Assert - find the GitHub link and verify it contains an SVG icon
await waitFor(() => {
const githubLink = screen.getByRole('link', { name: /step1Operation/i })
const githubLink = getAnchorButton(/step1Operation/i)
expect(githubLink).toBeInTheDocument()
expect(githubLink.querySelector('svg')).toBeInTheDocument()
})
@ -182,7 +188,7 @@ describe('CustomizeModal', () => {
// Assert
await waitFor(() => {
const githubLink = screen.getByRole('link', { name: /step1Operation/i })
const githubLink = getAnchorButton(/step1Operation/i)
expect(githubLink).toHaveAttribute('href', 'https://github.com/langgenius/webapp-conversation')
})
})
@ -196,7 +202,7 @@ describe('CustomizeModal', () => {
// Assert
await waitFor(() => {
const githubLink = screen.getByRole('link', { name: /step1Operation/i })
const githubLink = getAnchorButton(/step1Operation/i)
expect(githubLink).toHaveAttribute('href', 'https://github.com/langgenius/webapp-conversation')
})
})
@ -210,7 +216,7 @@ describe('CustomizeModal', () => {
// Assert
await waitFor(() => {
const githubLink = screen.getByRole('link', { name: /step1Operation/i })
const githubLink = getAnchorButton(/step1Operation/i)
expect(githubLink).toHaveAttribute('href', 'https://github.com/langgenius/webapp-text-generator')
})
})
@ -224,7 +230,7 @@ describe('CustomizeModal', () => {
// Assert
await waitFor(() => {
const githubLink = screen.getByRole('link', { name: /step1Operation/i })
const githubLink = getAnchorButton(/step1Operation/i)
expect(githubLink).toHaveAttribute('href', 'https://github.com/langgenius/webapp-text-generator')
})
})
@ -238,7 +244,7 @@ describe('CustomizeModal', () => {
// Assert
await waitFor(() => {
const githubLink = screen.getByRole('link', { name: /step1Operation/i })
const githubLink = getAnchorButton(/step1Operation/i)
expect(githubLink).toHaveAttribute('href', 'https://github.com/langgenius/webapp-text-generator')
})
})
@ -255,7 +261,7 @@ describe('CustomizeModal', () => {
// Assert
await waitFor(() => {
const githubLink = screen.getByRole('link', { name: /step1Operation/i })
const githubLink = getAnchorButton(/step1Operation/i)
expect(githubLink).toHaveAttribute('target', '_blank')
expect(githubLink).toHaveAttribute('rel', 'noopener noreferrer')
})
@ -270,7 +276,7 @@ describe('CustomizeModal', () => {
// Assert
await waitFor(() => {
const vercelLink = screen.getByRole('link', { name: /step2Operation/i })
const vercelLink = getAnchorButton(/step2Operation/i)
expect(vercelLink).toHaveAttribute('href', 'https://vercel.com/docs/concepts/deployments/git/vercel-for-github')
expect(vercelLink).toHaveAttribute('target', '_blank')
expect(vercelLink).toHaveAttribute('rel', 'noopener noreferrer')
@ -291,7 +297,7 @@ describe('CustomizeModal', () => {
expect(screen.getByText('appOverview.overview.appInfo.customize.way2.operation')).toBeInTheDocument()
})
const way2Link = screen.getByRole('link', { name: /way2\.operation/i })
const way2Link = getAnchorButton(/way2\.operation/i)
expect(way2Link).toHaveAttribute('href', expect.stringContaining('/use-dify/publish/developing-with-apis'))
expect(way2Link).toHaveAttribute('target', '_blank')
expect(way2Link).toHaveAttribute('rel', 'noopener noreferrer')
@ -405,7 +411,7 @@ describe('CustomizeModal', () => {
// Assert - Find GitHub link and verify it contains an SVG icon with expected class
await waitFor(() => {
const githubLink = screen.getByRole('link', { name: /step1Operation/i })
const githubLink = getAnchorButton(/step1Operation/i)
const githubIcon = githubLink.querySelector('svg')
expect(githubIcon).toBeInTheDocument()
expect(githubIcon).toHaveClass('text-text-secondary')

View File

@ -67,7 +67,7 @@ const CustomizeModal: FC<IShareLinkProps> = ({
<div className="flex flex-col">
<div className="text-text-primary">{t(`${prefixCustomize}.way1.step1`, { ns: 'appOverview' })}</div>
<div className="mt-1 mb-2 text-xs text-text-tertiary">{t(`${prefixCustomize}.way1.step1Tip`, { ns: 'appOverview' })}</div>
<Button render={<a href={`https://github.com/langgenius/${isChatApp ? 'webapp-conversation' : 'webapp-text-generator'}`} target="_blank" rel="noopener noreferrer" />}>
<Button nativeButton={false} render={<a href={`https://github.com/langgenius/${isChatApp ? 'webapp-conversation' : 'webapp-text-generator'}`} target="_blank" rel="noopener noreferrer" />}>
<GithubIcon className="mr-2 text-text-secondary" />
{t(`${prefixCustomize}.way1.step1Operation`, { ns: 'appOverview' })}
</Button>
@ -78,7 +78,7 @@ const CustomizeModal: FC<IShareLinkProps> = ({
<div className="flex flex-col">
<div className="text-text-primary">{t(`${prefixCustomize}.way1.step2`, { ns: 'appOverview' })}</div>
<div className="mt-1 mb-2 text-xs text-text-tertiary">{t(`${prefixCustomize}.way1.step2Tip`, { ns: 'appOverview' })}</div>
<Button render={<a href="https://vercel.com/docs/concepts/deployments/git/vercel-for-github" target="_blank" rel="noopener noreferrer" />}>
<Button nativeButton={false} render={<a href="https://vercel.com/docs/concepts/deployments/git/vercel-for-github" target="_blank" rel="noopener noreferrer" />}>
<div className="mr-1.5 border-t-0 border-r-[7px] border-b-12 border-l-[7px] border-solid border-text-primary border-t-transparent border-r-transparent border-l-transparent"></div>
<span>{t(`${prefixCustomize}.way1.step2Operation`, { ns: 'appOverview' })}</span>
</Button>
@ -113,6 +113,7 @@ const CustomizeModal: FC<IShareLinkProps> = ({
</Tag>
<p className="my-2 system-sm-medium text-text-secondary">{t(`${prefixCustomize}.way2.name`, { ns: 'appOverview' })}</p>
<Button
nativeButton={false}
render={<a href={apiDocLink} target="_blank" rel="noopener noreferrer" />}
className="mt-2"
>

View File

@ -101,8 +101,8 @@ describe('Header Component', () => {
})
const buttons = screen.getAllByRole('button')
// Sidebar(1) + NewChat(1) + ResetChat(1) + ViewForm(1) = 4 buttons
expect(buttons).toHaveLength(4)
// Sidebar(1) + Conversation operation(1) + NewChat(1) + ResetChat(1) + ViewForm(1) = 5 buttons
expect(buttons).toHaveLength(5)
})
})
@ -306,8 +306,8 @@ describe('Header Component', () => {
})
const buttons = screen.getAllByRole('button')
// Sidebar(1) + NewChat(1) + ResetChat(1) = 3 buttons
expect(buttons).toHaveLength(3)
// Sidebar(1) + Conversation operation(1) + NewChat(1) + ResetChat(1) = 4 buttons
expect(buttons).toHaveLength(4)
})
it('should render system title if conversation id is missing', () => {

View File

@ -46,12 +46,13 @@ const Operation: FC<Props> = ({
onOpenChange={setOpen}
>
<DropdownMenuTrigger
render={<div />}
className={cn(
'flex cursor-pointer items-center rounded-lg border-none bg-transparent p-1.5 pl-2 text-text-secondary outline-hidden hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid',
open && 'bg-state-base-hover',
)}
>
<div className={cn('flex cursor-pointer items-center rounded-lg p-1.5 pl-2 text-text-secondary hover:bg-state-base-hover', open && 'bg-state-base-hover')}>
<div className="system-md-semibold">{title}</div>
<span aria-hidden className="i-ri-arrow-down-s-line size-4" />
</div>
<span className="system-md-semibold">{title}</span>
<span aria-hidden className="i-ri-arrow-down-s-line size-4" />
</DropdownMenuTrigger>
<DropdownMenuContent
placement={placement}

View File

@ -108,6 +108,7 @@ const AddCustomModel = ({
onOpenChange={setOpen}
>
<PopoverTrigger
nativeButton={false}
render={<div className="inline-block">{renderTrigger(open)}</div>}
/>
<PopoverContent

View File

@ -175,6 +175,7 @@ const Authorized = ({
onOpenChange={setMergedIsOpen}
>
<PopoverTrigger
nativeButton={false}
render={<div className={triggerPopupSameWidth ? 'w-full' : 'inline-block'}>{renderTrigger(mergedIsOpen)}</div>}
onClick={handleTriggerClick}
/>

View File

@ -81,6 +81,30 @@ describe('ModelIcon', () => {
expect(screen.getByRole('img', { name: /model-icon/i })).toHaveAttribute('src', 'dark.png')
})
it('should fall back to the light icon when dark icon is empty', () => {
mockTheme = Theme.dark
const provider = createModel({
icon_small: createI18nText('light.png'),
icon_small_dark: createI18nText(''),
})
render(<ModelIcon provider={provider} />)
expect(screen.getByRole('img', { name: /model-icon/i })).toHaveAttribute('src', 'light.png')
})
it('should render fallback when icon urls are empty', () => {
const provider = createModel({
icon_small: createI18nText(''),
icon_small_dark: createI18nText(''),
})
const { container } = render(<ModelIcon provider={provider} />)
expect(screen.queryByRole('img', { name: /model-icon/i })).not.toBeInTheDocument()
expect(container.querySelector('.i-custom-vender-other-group')).toBeInTheDocument()
})
// Provider override
it('should ignore icon_small for OpenAI models starting with "o"', () => {
const provider = createModel({

View File

@ -5,7 +5,6 @@ import type {
} from '../declarations'
import { cn } from '@langgenius/dify-ui/cn'
import { OpenaiYellow } from '@/app/components/base/icons/src/public/llm'
import { Group } from '@/app/components/base/icons/src/vender/other'
import useTheme from '@/hooks/use-theme'
import { renderI18nObject } from '@/i18n-config'
import { Theme } from '@/types/app'
@ -27,20 +26,19 @@ const ModelIcon: FC<ModelIconProps> = ({
}) => {
const { theme } = useTheme()
const language = useLanguage()
const lightIconUrl = provider?.icon_small ? renderI18nObject(provider.icon_small, language) : ''
const darkIconUrl = provider?.icon_small_dark ? renderI18nObject(provider.icon_small_dark, language) : ''
const iconUrl = theme === Theme.dark ? darkIconUrl || lightIconUrl : lightIconUrl
if (provider?.provider && ['openai', 'langgenius/openai/openai'].includes(provider.provider) && modelName?.startsWith('o'))
return <div className="flex items-center justify-center"><OpenaiYellow className={cn('size-5', className)} /></div>
if (provider?.icon_small) {
if (iconUrl) {
return (
<div className={cn('flex size-5 items-center justify-center', isDeprecated && 'opacity-50', className)}>
<img
alt="model-icon"
src={renderI18nObject(
theme === Theme.dark && provider.icon_small_dark
? provider.icon_small_dark
: provider.icon_small,
language,
)}
src={iconUrl}
className={iconClassName}
/>
</div>
@ -54,7 +52,7 @@ const ModelIcon: FC<ModelIconProps> = ({
)}
>
<div className={cn('flex size-5 items-center justify-center opacity-35', iconClassName)}>
<Group className="size-3 text-text-tertiary" />
<span aria-hidden className="i-custom-vender-other-group size-3 text-text-tertiary" />
</div>
</div>
)

View File

@ -284,7 +284,7 @@ describe('ModelSelectorTrigger', () => {
)
expect(container.querySelector('img[alt="model-icon"]')).not.toBeInTheDocument()
expect(container.querySelector('svg')).toBeInTheDocument()
expect(container.querySelector('.i-custom-vender-other-group')).toBeInTheDocument()
})
})
})

View File

@ -115,6 +115,32 @@ describe('ProviderIcon', () => {
expect(img.src).toBe('https://example.com/dark.png')
})
it('should fall back to light icon when dark icon is empty', () => {
const mockTheme = vi.mocked(useTheme)
mockTheme.mockReturnValue({ theme: Theme.dark, themes: [], setTheme: vi.fn() } as UseThemeReturnType)
const provider = createProvider({
icon_small: { en_US: 'https://example.com/light.png', zh_Hans: 'https://example.com/light.png' },
icon_small_dark: { en_US: '', zh_Hans: '' },
})
render(<ProviderIcon provider={provider} />)
const img = screen.getByAltText('provider-icon') as HTMLImageElement
expect(img.src).toBe('https://example.com/light.png')
})
it('should render fallback icon when provider icon is empty', () => {
const provider = createProvider({
icon_small: { en_US: '', zh_Hans: '' },
icon_small_dark: { en_US: '', zh_Hans: '' },
})
const { container } = render(<ProviderIcon provider={provider} />)
expect(screen.queryByAltText('provider-icon')).not.toBeInTheDocument()
expect(container.querySelector('.i-custom-vender-other-group')).toBeInTheDocument()
})
it('should fall back to localized labels when available', () => {
const mockLang = vi.mocked(useLanguage)
mockLang.mockReturnValue('zh_Hans')

View File

@ -18,6 +18,9 @@ const ProviderIcon: FC<ProviderIconProps> = ({
}) => {
const { theme } = useTheme()
const language = useLanguage()
const lightIconUrl = renderI18nObject(provider.icon_small, language)
const darkIconUrl = provider.icon_small_dark ? renderI18nObject(provider.icon_small_dark, language) : ''
const iconUrl = theme === Theme.dark ? darkIconUrl || lightIconUrl : lightIconUrl
if (provider.provider === 'langgenius/anthropic/anthropic') {
return (
@ -38,16 +41,19 @@ const ProviderIcon: FC<ProviderIconProps> = ({
return (
<div className={cn('inline-flex items-center gap-2', className)}>
<img
alt="provider-icon"
src={renderI18nObject(
theme === Theme.dark && provider.icon_small_dark
? provider.icon_small_dark
: provider.icon_small,
language,
)}
className="size-6"
/>
{iconUrl
? (
<img
alt="provider-icon"
src={iconUrl}
className="size-6"
/>
)
: (
<div className="flex size-6 items-center justify-center rounded-md border-[0.5px] border-components-panel-border-subtle bg-background-default-subtle">
<span aria-hidden className="i-custom-vender-other-group size-4 text-text-tertiary" />
</div>
)}
<div className="system-md-semibold text-text-primary">
{renderI18nObject(provider.label, language)}
</div>