mirror of
https://github.com/langgenius/dify.git
synced 2026-05-03 17:08:03 +08:00
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:
112
web/app/components/header/__tests__/header-wrapper.spec.tsx
Normal file
112
web/app/components/header/__tests__/header-wrapper.spec.tsx
Normal 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()
|
||||
})
|
||||
})
|
||||
251
web/app/components/header/__tests__/index.spec.tsx
Normal file
251
web/app/components/header/__tests__/index.spec.tsx
Normal 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()
|
||||
})
|
||||
})
|
||||
120
web/app/components/header/__tests__/maintenance-notice.spec.tsx
Normal file
120
web/app/components/header/__tests__/maintenance-notice.spec.tsx
Normal 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()
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user