diff --git a/web/app/components/app/overview/customize/__tests__/index.spec.tsx b/web/app/components/app/overview/customize/__tests__/index.spec.tsx index 6b33cadcf7..5fa2d10a3e 100644 --- a/web/app/components/app/overview/customize/__tests__/index.spec.tsx +++ b/web/app/components/app/overview/customize/__tests__/index.spec.tsx @@ -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') diff --git a/web/app/components/app/overview/customize/index.tsx b/web/app/components/app/overview/customize/index.tsx index 2e1bf93672..3affa339bf 100644 --- a/web/app/components/app/overview/customize/index.tsx +++ b/web/app/components/app/overview/customize/index.tsx @@ -67,7 +67,7 @@ const CustomizeModal: FC = ({
{t(`${prefixCustomize}.way1.step1`, { ns: 'appOverview' })}
{t(`${prefixCustomize}.way1.step1Tip`, { ns: 'appOverview' })}
- @@ -78,7 +78,7 @@ const CustomizeModal: FC = ({
{t(`${prefixCustomize}.way1.step2`, { ns: 'appOverview' })}
{t(`${prefixCustomize}.way1.step2Tip`, { ns: 'appOverview' })}
- @@ -113,6 +113,7 @@ const CustomizeModal: FC = ({

{t(`${prefixCustomize}.way2.name`, { ns: 'appOverview' })}

} /> {renderTrigger(mergedIsOpen)}
} onClick={handleTriggerClick} /> diff --git a/web/app/components/header/account-setting/model-provider-page/model-icon/__tests__/index.spec.tsx b/web/app/components/header/account-setting/model-provider-page/model-icon/__tests__/index.spec.tsx index 24cdc438de..0d6c84c7db 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-icon/__tests__/index.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-icon/__tests__/index.spec.tsx @@ -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() + + 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() + + 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({ diff --git a/web/app/components/header/account-setting/model-provider-page/model-icon/index.tsx b/web/app/components/header/account-setting/model-provider-page/model-icon/index.tsx index 610e7aa066..a558ea0da7 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-icon/index.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-icon/index.tsx @@ -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 = ({ }) => { 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
- if (provider?.icon_small) { + if (iconUrl) { return (
model-icon
@@ -54,7 +52,7 @@ const ModelIcon: FC = ({ )} >
- +
) diff --git a/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/model-selector-trigger.spec.tsx b/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/model-selector-trigger.spec.tsx index d2929b48f6..cd349ece40 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/model-selector-trigger.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/model-selector-trigger.spec.tsx @@ -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() }) }) }) diff --git a/web/app/components/header/account-setting/model-provider-page/provider-icon/__tests__/index.spec.tsx b/web/app/components/header/account-setting/model-provider-page/provider-icon/__tests__/index.spec.tsx index f2871b170a..d7f9bc8322 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-icon/__tests__/index.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-icon/__tests__/index.spec.tsx @@ -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() + + 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() + + 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') diff --git a/web/app/components/header/account-setting/model-provider-page/provider-icon/index.tsx b/web/app/components/header/account-setting/model-provider-page/provider-icon/index.tsx index 2ee75e3fd8..972e82b344 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-icon/index.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-icon/index.tsx @@ -18,6 +18,9 @@ const ProviderIcon: FC = ({ }) => { 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 = ({ return (
- provider-icon + {iconUrl + ? ( + provider-icon + ) + : ( +
+ +
+ )}
{renderI18nObject(provider.label, language)}