refactor(custom): reorganize web app brand module and raise coverage threshold (#33531)

Co-authored-by: CodingOnStar <hanxujiang@dify.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Coding On Star
2026-03-16 18:17:21 +08:00
committed by GitHub
parent c3ee83645f
commit 6da802eb2a
140 changed files with 1782 additions and 1486 deletions

View File

@ -0,0 +1,112 @@
import { act, render, screen } from '@testing-library/react'
import { usePathname } from 'next/navigation'
import { vi } from 'vitest'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import HeaderWrapper from '../header-wrapper'
vi.mock('next/navigation', () => ({
usePathname: vi.fn(),
}))
vi.mock('@/context/event-emitter', () => ({
useEventEmitterContextContext: vi.fn(),
}))
describe('HeaderWrapper', () => {
type CanvasEvent = { type: string, payload: boolean }
let subscriptionCallback: ((event: CanvasEvent) => void) | null = null
const mockUseSubscription = vi.fn<(callback: (event: CanvasEvent) => void) => void>((callback) => {
subscriptionCallback = callback
})
beforeEach(() => {
vi.clearAllMocks()
localStorage.clear()
subscriptionCallback = null
vi.mocked(usePathname).mockReturnValue('/test')
vi.mocked(useEventEmitterContextContext).mockReturnValue({
eventEmitter: { useSubscription: mockUseSubscription },
} as never)
})
it('should render children correctly', () => {
render(
<HeaderWrapper>
<div data-testid="child">Test Child</div>
</HeaderWrapper>,
)
expect(screen.getByTestId('child')).toBeInTheDocument()
expect(screen.getByText('Test Child')).toBeInTheDocument()
})
it('should keep children mounted when workflow maximize events are emitted', () => {
vi.mocked(usePathname).mockReturnValue('/some/path/workflow')
render(
<HeaderWrapper>
<div>Workflow Content</div>
</HeaderWrapper>,
)
act(() => {
subscriptionCallback?.({ type: 'workflow-canvas-maximize', payload: true })
subscriptionCallback?.({ type: 'workflow-canvas-maximize', payload: false })
})
expect(screen.getByText('Workflow Content')).toBeInTheDocument()
})
it('should keep children mounted on pipeline routes when maximize is enabled from storage', () => {
vi.mocked(usePathname).mockReturnValue('/some/path/pipeline')
localStorage.setItem('workflow-canvas-maximize', 'true')
render(
<HeaderWrapper>
<div>Pipeline Content</div>
</HeaderWrapper>,
)
expect(screen.getByText('Pipeline Content')).toBeInTheDocument()
})
it('should keep children mounted on non-canvas routes when maximize is enabled from storage', () => {
vi.mocked(usePathname).mockReturnValue('/apps')
localStorage.setItem('workflow-canvas-maximize', 'true')
render(
<HeaderWrapper>
<div>App Content</div>
</HeaderWrapper>,
)
expect(screen.getByText('App Content')).toBeInTheDocument()
})
it('should keep children mounted when unrelated events are emitted', () => {
vi.mocked(usePathname).mockReturnValue('/some/path/workflow')
render(
<HeaderWrapper>
<div>Workflow Content</div>
</HeaderWrapper>,
)
act(() => {
subscriptionCallback?.({ type: 'other-event', payload: true })
})
expect(screen.getByText('Workflow Content')).toBeInTheDocument()
})
it('should render children when eventEmitter is unavailable', () => {
vi.mocked(useEventEmitterContextContext).mockReturnValue({
eventEmitter: undefined,
} as never)
render(
<HeaderWrapper>
<div>Content Without Emitter</div>
</HeaderWrapper>,
)
expect(screen.getByText('Content Without Emitter')).toBeInTheDocument()
})
})

View File

@ -0,0 +1,251 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { vi } from 'vitest'
import Header from '../index'
function createMockComponent(testId: string) {
return () => <div data-testid={testId} />
}
vi.mock('@/app/components/header/account-dropdown/workplace-selector', () => ({
default: createMockComponent('workplace-selector'),
}))
vi.mock('@/app/components/header/account-dropdown', () => ({
default: createMockComponent('account-dropdown'),
}))
vi.mock('@/app/components/header/app-nav', () => ({
default: createMockComponent('app-nav'),
}))
vi.mock('@/app/components/header/dataset-nav', () => ({
default: createMockComponent('dataset-nav'),
}))
vi.mock('@/app/components/header/env-nav', () => ({
default: createMockComponent('env-nav'),
}))
vi.mock('@/app/components/header/explore-nav', () => ({
default: createMockComponent('explore-nav'),
}))
vi.mock('@/app/components/header/license-env', () => ({
default: createMockComponent('license-nav'),
}))
vi.mock('@/app/components/header/plugins-nav', () => ({
default: createMockComponent('plugins-nav'),
}))
vi.mock('@/app/components/header/tools-nav', () => ({
default: createMockComponent('tools-nav'),
}))
vi.mock('@/app/components/header/plan-badge', () => ({
default: ({ onClick, plan }: { onClick?: () => void, plan?: string }) => (
<button data-testid="plan-badge" onClick={onClick} data-plan={plan} />
),
}))
vi.mock('@/context/workspace-context-provider', () => ({
WorkspaceProvider: ({ children }: { children?: React.ReactNode }) => children,
}))
vi.mock('next/link', () => ({
default: ({ children, href }: { children?: React.ReactNode, href?: string }) => <a href={href}>{children}</a>,
}))
let mockIsWorkspaceEditor = false
let mockIsDatasetOperator = false
let mockMedia = 'desktop'
let mockEnableBilling = false
let mockPlanType = 'sandbox'
let mockBrandingEnabled = false
let mockBrandingTitle: string | null = null
let mockBrandingLogo: string | null = null
const mockSetShowPricingModal = vi.fn()
const mockSetShowAccountSettingModal = vi.fn()
vi.mock('@/context/app-context', () => ({
useAppContext: () => ({
isCurrentWorkspaceEditor: mockIsWorkspaceEditor,
isCurrentWorkspaceDatasetOperator: mockIsDatasetOperator,
}),
}))
vi.mock('@/hooks/use-breakpoints', () => ({
default: () => mockMedia,
MediaType: { mobile: 'mobile', tablet: 'tablet', desktop: 'desktop' },
}))
vi.mock('@/context/provider-context', () => ({
useProviderContext: () => ({
enableBilling: mockEnableBilling,
plan: { type: mockPlanType },
}),
}))
vi.mock('@/context/modal-context', () => ({
useModalContext: () => ({
setShowPricingModal: mockSetShowPricingModal,
setShowAccountSettingModal: mockSetShowAccountSettingModal,
}),
}))
vi.mock('@/context/global-public-context', () => {
type SystemFeatures = { branding: { enabled: boolean, application_title: string | null, workspace_logo: string | null } }
return {
useGlobalPublicStore: (selector: (s: { systemFeatures: SystemFeatures }) => SystemFeatures) =>
selector({
systemFeatures: {
branding: {
enabled: mockBrandingEnabled,
application_title: mockBrandingTitle,
workspace_logo: mockBrandingLogo,
},
},
}),
}
})
describe('Header', () => {
beforeEach(() => {
vi.clearAllMocks()
mockIsWorkspaceEditor = false
mockIsDatasetOperator = false
mockMedia = 'desktop'
mockEnableBilling = false
mockPlanType = 'sandbox'
mockBrandingEnabled = false
mockBrandingTitle = null
mockBrandingLogo = null
})
it('should render header with main nav components', () => {
render(<Header />)
expect(screen.getByRole('img', { name: /dify logo/i })).toBeInTheDocument()
expect(screen.getByTestId('workplace-selector')).toBeInTheDocument()
expect(screen.getByTestId('app-nav')).toBeInTheDocument()
expect(screen.getByTestId('account-dropdown')).toBeInTheDocument()
})
it('should show license nav when billing disabled, plan badge when enabled', () => {
mockEnableBilling = false
const { rerender } = render(<Header />)
expect(screen.getByTestId('license-nav')).toBeInTheDocument()
expect(screen.queryByTestId('plan-badge')).not.toBeInTheDocument()
mockEnableBilling = true
rerender(<Header />)
expect(screen.queryByTestId('license-nav')).not.toBeInTheDocument()
expect(screen.getByTestId('plan-badge')).toBeInTheDocument()
})
it('should hide explore nav when user is dataset operator', () => {
mockIsDatasetOperator = true
render(<Header />)
expect(screen.queryByTestId('explore-nav')).not.toBeInTheDocument()
expect(screen.getByTestId('dataset-nav')).toBeInTheDocument()
})
it('should call pricing modal for free plan, settings modal for paid plan', () => {
mockEnableBilling = true
mockPlanType = 'sandbox'
const { rerender } = render(<Header />)
fireEvent.click(screen.getByTestId('plan-badge'))
expect(mockSetShowPricingModal).toHaveBeenCalledTimes(1)
mockPlanType = 'professional'
rerender(<Header />)
fireEvent.click(screen.getByTestId('plan-badge'))
expect(mockSetShowAccountSettingModal).toHaveBeenCalledTimes(1)
})
it('should render mobile layout without env nav', () => {
mockMedia = 'mobile'
render(<Header />)
expect(screen.getByRole('img', { name: /dify logo/i })).toBeInTheDocument()
expect(screen.queryByTestId('env-nav')).not.toBeInTheDocument()
})
it('should render branded title and logo when branding is enabled', () => {
mockBrandingEnabled = true
mockBrandingTitle = 'Acme Workspace'
mockBrandingLogo = '/logo.png'
render(<Header />)
expect(screen.getByText('Acme Workspace')).toBeInTheDocument()
expect(screen.getByRole('img', { name: /logo/i })).toBeInTheDocument()
expect(screen.queryByRole('img', { name: /dify logo/i })).not.toBeInTheDocument()
})
it('should show default Dify logo when branding is enabled but no workspace_logo', () => {
mockBrandingEnabled = true
mockBrandingTitle = 'Custom Title'
mockBrandingLogo = null
render(<Header />)
expect(screen.getByText('Custom Title')).toBeInTheDocument()
expect(screen.getByRole('img', { name: /dify logo/i })).toBeInTheDocument()
})
it('should show default Dify text when branding enabled but no application_title', () => {
mockBrandingEnabled = true
mockBrandingTitle = null
mockBrandingLogo = null
render(<Header />)
expect(screen.getByText('Dify')).toBeInTheDocument()
})
it('should show dataset nav for editor who is not dataset operator', () => {
mockIsWorkspaceEditor = true
mockIsDatasetOperator = false
render(<Header />)
expect(screen.getByTestId('dataset-nav')).toBeInTheDocument()
expect(screen.getByTestId('explore-nav')).toBeInTheDocument()
expect(screen.getByTestId('app-nav')).toBeInTheDocument()
})
it('should hide dataset nav when neither editor nor dataset operator', () => {
mockIsWorkspaceEditor = false
mockIsDatasetOperator = false
render(<Header />)
expect(screen.queryByTestId('dataset-nav')).not.toBeInTheDocument()
})
it('should render mobile layout with dataset operator nav restrictions', () => {
mockMedia = 'mobile'
mockIsDatasetOperator = true
render(<Header />)
expect(screen.queryByTestId('explore-nav')).not.toBeInTheDocument()
expect(screen.queryByTestId('app-nav')).not.toBeInTheDocument()
expect(screen.queryByTestId('tools-nav')).not.toBeInTheDocument()
expect(screen.getByTestId('dataset-nav')).toBeInTheDocument()
})
it('should render mobile layout with billing enabled', () => {
mockMedia = 'mobile'
mockEnableBilling = true
mockPlanType = 'sandbox'
render(<Header />)
expect(screen.getByTestId('plan-badge')).toBeInTheDocument()
expect(screen.queryByTestId('license-nav')).not.toBeInTheDocument()
})
})

View File

@ -0,0 +1,120 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { vi } from 'vitest'
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { NOTICE_I18N } from '@/i18n-config/language'
import MaintenanceNotice from '../maintenance-notice'
vi.mock('@/app/components/base/icons/src/vender/line/general', () => ({
X: ({ onClick }: { onClick?: () => void }) => <button type="button" aria-label="close notice" onClick={onClick} />,
}))
vi.mock(
'@/app/components/header/account-setting/model-provider-page/hooks',
() => ({
useLanguage: vi.fn(),
}),
)
vi.mock('@/i18n-config/language', async (importOriginal) => {
const actual = (await importOriginal()) as Record<string, unknown>
return {
...actual,
NOTICE_I18N: {
title: {
en_US: 'Notice Title',
zh_Hans: '提示标题',
},
desc: {
en_US: 'Notice Description',
zh_Hans: '提示描述',
},
href: '#',
},
}
})
describe('MaintenanceNotice', () => {
const windowOpenSpy = vi
.spyOn(window, 'open')
.mockImplementation(() => null)
const setNoticeHref = (href: string) => {
NOTICE_I18N.href = href
}
beforeEach(() => {
vi.clearAllMocks()
localStorage.clear()
vi.mocked(useLanguage).mockReturnValue('en_US')
setNoticeHref('#')
})
afterAll(() => {
windowOpenSpy.mockRestore()
})
describe('Rendering', () => {
it('should render localized content correctly (English)', () => {
render(<MaintenanceNotice />)
expect(screen.getByText('Notice Title')).toBeInTheDocument()
expect(screen.getByText('Notice Description')).toBeInTheDocument()
})
it('should render localized content correctly (Chinese)', () => {
vi.mocked(useLanguage).mockReturnValue('zh_Hans')
render(<MaintenanceNotice />)
expect(screen.getByText('提示标题')).toBeInTheDocument()
expect(screen.getByText('提示描述')).toBeInTheDocument()
})
it('should not render when hidden in localStorage', () => {
localStorage.setItem('hide-maintenance-notice', '1')
const { container } = render(<MaintenanceNotice />)
expect(container.firstChild).toBeNull()
})
})
describe('User Interactions', () => {
it('should close the notice when X is clicked', () => {
render(<MaintenanceNotice />)
expect(screen.getByText('Notice Title')).toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: /close notice/i }))
expect(screen.queryByText('Notice Title')).not.toBeInTheDocument()
expect(localStorage.getItem('hide-maintenance-notice')).toBe('1')
})
it('should jump to notice when description is clicked and href is valid', () => {
setNoticeHref('https://dify.ai/notice')
render(<MaintenanceNotice />)
const desc = screen.getByText('Notice Description')
fireEvent.click(desc)
expect(windowOpenSpy).toHaveBeenCalledWith(
'https://dify.ai/notice',
'_blank',
)
})
it('should not jump when href is #', () => {
setNoticeHref('#')
render(<MaintenanceNotice />)
const desc = screen.getByText('Notice Description')
fireEvent.click(desc)
expect(windowOpenSpy).not.toHaveBeenCalled()
})
it('should not jump when href is empty', () => {
setNoticeHref('')
render(<MaintenanceNotice />)
const desc = screen.getByText('Notice Description')
fireEvent.click(desc)
expect(windowOpenSpy).not.toHaveBeenCalled()
})
})
})