mirror of
https://github.com/langgenius/dify.git
synced 2026-04-27 11:06:46 +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:
parent
c3ee83645f
commit
6da802eb2a
@ -1,496 +1,179 @@
|
|||||||
import type { Mock } from 'vitest'
|
import type { AppContextValue } from '@/context/app-context'
|
||||||
|
import type { SystemFeatures } from '@/types/feature'
|
||||||
import { render, screen } from '@testing-library/react'
|
import { render, screen } from '@testing-library/react'
|
||||||
import userEvent from '@testing-library/user-event'
|
import userEvent from '@testing-library/user-event'
|
||||||
import * as React from 'react'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { createMockProviderContextValue } from '@/__mocks__/provider-context'
|
import { createMockProviderContextValue } from '@/__mocks__/provider-context'
|
||||||
import { contactSalesUrl } from '@/app/components/billing/config'
|
import { useToastContext } from '@/app/components/base/toast/context'
|
||||||
|
import { contactSalesUrl, defaultPlan } from '@/app/components/billing/config'
|
||||||
import { Plan } from '@/app/components/billing/type'
|
import { Plan } from '@/app/components/billing/type'
|
||||||
|
import {
|
||||||
|
initialLangGeniusVersionInfo,
|
||||||
|
initialWorkspaceInfo,
|
||||||
|
useAppContext,
|
||||||
|
userProfilePlaceholder,
|
||||||
|
} from '@/context/app-context'
|
||||||
|
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||||
import { useModalContext } from '@/context/modal-context'
|
import { useModalContext } from '@/context/modal-context'
|
||||||
import { useProviderContext } from '@/context/provider-context'
|
import { useProviderContext } from '@/context/provider-context'
|
||||||
|
import { defaultSystemFeatures } from '@/types/feature'
|
||||||
import CustomPage from '../index'
|
import CustomPage from '../index'
|
||||||
|
|
||||||
// Mock external dependencies only
|
|
||||||
vi.mock('@/context/provider-context', () => ({
|
vi.mock('@/context/provider-context', () => ({
|
||||||
useProviderContext: vi.fn(),
|
useProviderContext: vi.fn(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/context/modal-context', () => ({
|
vi.mock('@/context/modal-context', () => ({
|
||||||
useModalContext: vi.fn(),
|
useModalContext: vi.fn(),
|
||||||
}))
|
}))
|
||||||
|
vi.mock('@/context/app-context', async (importOriginal) => {
|
||||||
// Mock the complex CustomWebAppBrand component to avoid dependency issues
|
const actual = await importOriginal<typeof import('@/context/app-context')>()
|
||||||
// This is acceptable because it has complex dependencies (fetch, APIs)
|
return {
|
||||||
vi.mock('@/app/components/custom/custom-web-app-brand', () => ({
|
...actual,
|
||||||
default: () => <div data-testid="custom-web-app-brand">CustomWebAppBrand</div>,
|
useAppContext: vi.fn(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
vi.mock('@/context/global-public-context', () => ({
|
||||||
|
useGlobalPublicStore: vi.fn(),
|
||||||
|
}))
|
||||||
|
vi.mock('@/app/components/base/toast/context', () => ({
|
||||||
|
useToastContext: vi.fn(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
const mockUseProviderContext = vi.mocked(useProviderContext)
|
||||||
|
const mockUseModalContext = vi.mocked(useModalContext)
|
||||||
|
const mockUseAppContext = vi.mocked(useAppContext)
|
||||||
|
const mockUseGlobalPublicStore = vi.mocked(useGlobalPublicStore)
|
||||||
|
const mockUseToastContext = vi.mocked(useToastContext)
|
||||||
|
|
||||||
|
const createProviderContext = ({
|
||||||
|
enableBilling = false,
|
||||||
|
planType = Plan.professional,
|
||||||
|
}: {
|
||||||
|
enableBilling?: boolean
|
||||||
|
planType?: Plan
|
||||||
|
} = {}) => {
|
||||||
|
return createMockProviderContextValue({
|
||||||
|
enableBilling,
|
||||||
|
plan: {
|
||||||
|
...defaultPlan,
|
||||||
|
type: planType,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const createAppContextValue = (): AppContextValue => ({
|
||||||
|
userProfile: userProfilePlaceholder,
|
||||||
|
mutateUserProfile: vi.fn(),
|
||||||
|
currentWorkspace: {
|
||||||
|
...initialWorkspaceInfo,
|
||||||
|
custom_config: {
|
||||||
|
replace_webapp_logo: 'https://example.com/replace.png',
|
||||||
|
remove_webapp_brand: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
isCurrentWorkspaceManager: true,
|
||||||
|
isCurrentWorkspaceOwner: false,
|
||||||
|
isCurrentWorkspaceEditor: false,
|
||||||
|
isCurrentWorkspaceDatasetOperator: false,
|
||||||
|
mutateCurrentWorkspace: vi.fn(),
|
||||||
|
langGeniusVersionInfo: initialLangGeniusVersionInfo,
|
||||||
|
useSelector: vi.fn() as unknown as AppContextValue['useSelector'],
|
||||||
|
isLoadingCurrentWorkspace: false,
|
||||||
|
isValidatingCurrentWorkspace: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const createSystemFeatures = (): SystemFeatures => ({
|
||||||
|
...defaultSystemFeatures,
|
||||||
|
branding: {
|
||||||
|
...defaultSystemFeatures.branding,
|
||||||
|
enabled: true,
|
||||||
|
workspace_logo: 'https://example.com/workspace-logo.png',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
describe('CustomPage', () => {
|
describe('CustomPage', () => {
|
||||||
const mockSetShowPricingModal = vi.fn()
|
const setShowPricingModal = vi.fn()
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
|
|
||||||
// Default mock setup
|
mockUseProviderContext.mockReturnValue(createProviderContext())
|
||||||
;(useModalContext as Mock).mockReturnValue({
|
mockUseModalContext.mockReturnValue({
|
||||||
setShowPricingModal: mockSetShowPricingModal,
|
setShowPricingModal,
|
||||||
})
|
} as unknown as ReturnType<typeof useModalContext>)
|
||||||
|
mockUseAppContext.mockReturnValue(createAppContextValue())
|
||||||
|
mockUseGlobalPublicStore.mockImplementation(selector => selector({
|
||||||
|
systemFeatures: createSystemFeatures(),
|
||||||
|
setSystemFeatures: vi.fn(),
|
||||||
|
}))
|
||||||
|
mockUseToastContext.mockReturnValue({
|
||||||
|
notify: vi.fn(),
|
||||||
|
} as unknown as ReturnType<typeof useToastContext>)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Helper function to render with different provider contexts
|
// Integration coverage for the page and its child custom brand section.
|
||||||
const renderWithContext = (overrides = {}) => {
|
|
||||||
;(useProviderContext as Mock).mockReturnValue(
|
|
||||||
createMockProviderContextValue(overrides),
|
|
||||||
)
|
|
||||||
return render(<CustomPage />)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rendering tests (REQUIRED)
|
|
||||||
describe('Rendering', () => {
|
describe('Rendering', () => {
|
||||||
it('should render without crashing', () => {
|
it('should render the custom brand configuration by default', () => {
|
||||||
// Arrange & Act
|
render(<CustomPage />)
|
||||||
renderWithContext()
|
|
||||||
|
|
||||||
// Assert
|
expect(screen.getByText('custom.webapp.removeBrand')).toBeInTheDocument()
|
||||||
expect(screen.getByTestId('custom-web-app-brand')).toBeInTheDocument()
|
expect(screen.getByText('Chatflow App')).toBeInTheDocument()
|
||||||
})
|
|
||||||
|
|
||||||
it('should always render CustomWebAppBrand component', () => {
|
|
||||||
// Arrange & Act
|
|
||||||
renderWithContext({
|
|
||||||
enableBilling: true,
|
|
||||||
plan: { type: Plan.sandbox },
|
|
||||||
})
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(screen.getByTestId('custom-web-app-brand')).toBeInTheDocument()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should have correct layout structure', () => {
|
|
||||||
// Arrange & Act
|
|
||||||
const { container } = renderWithContext()
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
const mainContainer = container.querySelector('.flex.flex-col')
|
|
||||||
expect(mainContainer).toBeInTheDocument()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// Conditional Rendering - Billing Tip
|
|
||||||
describe('Billing Tip Banner', () => {
|
|
||||||
it('should show billing tip when enableBilling is true and plan is sandbox', () => {
|
|
||||||
// Arrange & Act
|
|
||||||
renderWithContext({
|
|
||||||
enableBilling: true,
|
|
||||||
plan: { type: Plan.sandbox },
|
|
||||||
})
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(screen.getByText('custom.upgradeTip.title')).toBeInTheDocument()
|
|
||||||
expect(screen.getByText('custom.upgradeTip.des')).toBeInTheDocument()
|
|
||||||
expect(screen.getByText('billing.upgradeBtn.encourageShort')).toBeInTheDocument()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should not show billing tip when enableBilling is false', () => {
|
|
||||||
// Arrange & Act
|
|
||||||
renderWithContext({
|
|
||||||
enableBilling: false,
|
|
||||||
plan: { type: Plan.sandbox },
|
|
||||||
})
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(screen.queryByText('custom.upgradeTip.title')).not.toBeInTheDocument()
|
expect(screen.queryByText('custom.upgradeTip.title')).not.toBeInTheDocument()
|
||||||
expect(screen.queryByText('custom.upgradeTip.des')).not.toBeInTheDocument()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should not show billing tip when plan is professional', () => {
|
|
||||||
// Arrange & Act
|
|
||||||
renderWithContext({
|
|
||||||
enableBilling: true,
|
|
||||||
plan: { type: Plan.professional },
|
|
||||||
})
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(screen.queryByText('custom.upgradeTip.title')).not.toBeInTheDocument()
|
|
||||||
expect(screen.queryByText('custom.upgradeTip.des')).not.toBeInTheDocument()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should not show billing tip when plan is team', () => {
|
|
||||||
// Arrange & Act
|
|
||||||
renderWithContext({
|
|
||||||
enableBilling: true,
|
|
||||||
plan: { type: Plan.team },
|
|
||||||
})
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(screen.queryByText('custom.upgradeTip.title')).not.toBeInTheDocument()
|
|
||||||
expect(screen.queryByText('custom.upgradeTip.des')).not.toBeInTheDocument()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should have correct gradient styling for billing tip banner', () => {
|
|
||||||
// Arrange & Act
|
|
||||||
const { container } = renderWithContext({
|
|
||||||
enableBilling: true,
|
|
||||||
plan: { type: Plan.sandbox },
|
|
||||||
})
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
const banner = container.querySelector('.bg-gradient-to-r')
|
|
||||||
expect(banner).toBeInTheDocument()
|
|
||||||
expect(banner).toHaveClass('from-components-input-border-active-prompt-1')
|
|
||||||
expect(banner).toHaveClass('to-components-input-border-active-prompt-2')
|
|
||||||
expect(banner).toHaveClass('p-4')
|
|
||||||
expect(banner).toHaveClass('pl-6')
|
|
||||||
expect(banner).toHaveClass('shadow-lg')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// Conditional Rendering - Contact Sales
|
|
||||||
describe('Contact Sales Section', () => {
|
|
||||||
it('should show contact section when enableBilling is true and plan is professional', () => {
|
|
||||||
// Arrange & Act
|
|
||||||
const { container } = renderWithContext({
|
|
||||||
enableBilling: true,
|
|
||||||
plan: { type: Plan.professional },
|
|
||||||
})
|
|
||||||
|
|
||||||
// Assert - Check that contact section exists with all parts
|
|
||||||
const contactSection = container.querySelector('.absolute.bottom-0')
|
|
||||||
expect(contactSection).toBeInTheDocument()
|
|
||||||
expect(contactSection).toHaveTextContent('custom.customize.prefix')
|
|
||||||
expect(screen.getByText('custom.customize.contactUs')).toBeInTheDocument()
|
|
||||||
expect(contactSection).toHaveTextContent('custom.customize.suffix')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should show contact section when enableBilling is true and plan is team', () => {
|
|
||||||
// Arrange & Act
|
|
||||||
const { container } = renderWithContext({
|
|
||||||
enableBilling: true,
|
|
||||||
plan: { type: Plan.team },
|
|
||||||
})
|
|
||||||
|
|
||||||
// Assert - Check that contact section exists with all parts
|
|
||||||
const contactSection = container.querySelector('.absolute.bottom-0')
|
|
||||||
expect(contactSection).toBeInTheDocument()
|
|
||||||
expect(contactSection).toHaveTextContent('custom.customize.prefix')
|
|
||||||
expect(screen.getByText('custom.customize.contactUs')).toBeInTheDocument()
|
|
||||||
expect(contactSection).toHaveTextContent('custom.customize.suffix')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should not show contact section when enableBilling is false', () => {
|
|
||||||
// Arrange & Act
|
|
||||||
renderWithContext({
|
|
||||||
enableBilling: false,
|
|
||||||
plan: { type: Plan.professional },
|
|
||||||
})
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(screen.queryByText('custom.customize.prefix')).not.toBeInTheDocument()
|
|
||||||
expect(screen.queryByText('custom.customize.contactUs')).not.toBeInTheDocument()
|
expect(screen.queryByText('custom.customize.contactUs')).not.toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should not show contact section when plan is sandbox', () => {
|
it('should show the upgrade banner and open pricing modal for sandbox billing', async () => {
|
||||||
// Arrange & Act
|
|
||||||
renderWithContext({
|
|
||||||
enableBilling: true,
|
|
||||||
plan: { type: Plan.sandbox },
|
|
||||||
})
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(screen.queryByText('custom.customize.prefix')).not.toBeInTheDocument()
|
|
||||||
expect(screen.queryByText('custom.customize.contactUs')).not.toBeInTheDocument()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should render contact link with correct URL', () => {
|
|
||||||
// Arrange & Act
|
|
||||||
renderWithContext({
|
|
||||||
enableBilling: true,
|
|
||||||
plan: { type: Plan.professional },
|
|
||||||
})
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
const link = screen.getByText('custom.customize.contactUs').closest('a')
|
|
||||||
expect(link).toHaveAttribute('href', contactSalesUrl)
|
|
||||||
expect(link).toHaveAttribute('target', '_blank')
|
|
||||||
expect(link).toHaveAttribute('rel', 'noopener noreferrer')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should have correct positioning for contact section', () => {
|
|
||||||
// Arrange & Act
|
|
||||||
const { container } = renderWithContext({
|
|
||||||
enableBilling: true,
|
|
||||||
plan: { type: Plan.professional },
|
|
||||||
})
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
const contactSection = container.querySelector('.absolute.bottom-0')
|
|
||||||
expect(contactSection).toBeInTheDocument()
|
|
||||||
expect(contactSection).toHaveClass('h-[50px]')
|
|
||||||
expect(contactSection).toHaveClass('text-xs')
|
|
||||||
expect(contactSection).toHaveClass('leading-[50px]')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// User Interactions
|
|
||||||
describe('User Interactions', () => {
|
|
||||||
it('should call setShowPricingModal when upgrade button is clicked', async () => {
|
|
||||||
// Arrange
|
|
||||||
const user = userEvent.setup()
|
const user = userEvent.setup()
|
||||||
renderWithContext({
|
mockUseProviderContext.mockReturnValue(createProviderContext({
|
||||||
enableBilling: true,
|
enableBilling: true,
|
||||||
plan: { type: Plan.sandbox },
|
planType: Plan.sandbox,
|
||||||
})
|
}))
|
||||||
|
|
||||||
// Act
|
render(<CustomPage />)
|
||||||
const upgradeButton = screen.getByText('billing.upgradeBtn.encourageShort')
|
|
||||||
await user.click(upgradeButton)
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(mockSetShowPricingModal).toHaveBeenCalledTimes(1)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should call setShowPricingModal without arguments', async () => {
|
|
||||||
// Arrange
|
|
||||||
const user = userEvent.setup()
|
|
||||||
renderWithContext({
|
|
||||||
enableBilling: true,
|
|
||||||
plan: { type: Plan.sandbox },
|
|
||||||
})
|
|
||||||
|
|
||||||
// Act
|
|
||||||
const upgradeButton = screen.getByText('billing.upgradeBtn.encourageShort')
|
|
||||||
await user.click(upgradeButton)
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(mockSetShowPricingModal).toHaveBeenCalledWith()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should handle multiple clicks on upgrade button', async () => {
|
|
||||||
// Arrange
|
|
||||||
const user = userEvent.setup()
|
|
||||||
renderWithContext({
|
|
||||||
enableBilling: true,
|
|
||||||
plan: { type: Plan.sandbox },
|
|
||||||
})
|
|
||||||
|
|
||||||
// Act
|
|
||||||
const upgradeButton = screen.getByText('billing.upgradeBtn.encourageShort')
|
|
||||||
await user.click(upgradeButton)
|
|
||||||
await user.click(upgradeButton)
|
|
||||||
await user.click(upgradeButton)
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(mockSetShowPricingModal).toHaveBeenCalledTimes(3)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should have correct button styling for upgrade button', () => {
|
|
||||||
// Arrange & Act
|
|
||||||
renderWithContext({
|
|
||||||
enableBilling: true,
|
|
||||||
plan: { type: Plan.sandbox },
|
|
||||||
})
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
const upgradeButton = screen.getByText('billing.upgradeBtn.encourageShort')
|
|
||||||
expect(upgradeButton).toHaveClass('cursor-pointer')
|
|
||||||
expect(upgradeButton).toHaveClass('bg-white')
|
|
||||||
expect(upgradeButton).toHaveClass('text-text-accent')
|
|
||||||
expect(upgradeButton).toHaveClass('rounded-3xl')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// Edge Cases (REQUIRED)
|
|
||||||
describe('Edge Cases', () => {
|
|
||||||
it('should handle undefined plan type gracefully', () => {
|
|
||||||
// Arrange & Act
|
|
||||||
expect(() => {
|
|
||||||
renderWithContext({
|
|
||||||
enableBilling: true,
|
|
||||||
plan: { type: undefined },
|
|
||||||
})
|
|
||||||
}).not.toThrow()
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(screen.getByTestId('custom-web-app-brand')).toBeInTheDocument()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should handle plan without type property', () => {
|
|
||||||
// Arrange & Act
|
|
||||||
expect(() => {
|
|
||||||
renderWithContext({
|
|
||||||
enableBilling: true,
|
|
||||||
plan: { type: null },
|
|
||||||
})
|
|
||||||
}).not.toThrow()
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(screen.getByTestId('custom-web-app-brand')).toBeInTheDocument()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should not show any banners when both conditions are false', () => {
|
|
||||||
// Arrange & Act
|
|
||||||
renderWithContext({
|
|
||||||
enableBilling: false,
|
|
||||||
plan: { type: Plan.sandbox },
|
|
||||||
})
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(screen.queryByText('custom.upgradeTip.title')).not.toBeInTheDocument()
|
|
||||||
expect(screen.queryByText('custom.customize.prefix')).not.toBeInTheDocument()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should handle enableBilling undefined', () => {
|
|
||||||
// Arrange & Act
|
|
||||||
expect(() => {
|
|
||||||
renderWithContext({
|
|
||||||
enableBilling: undefined,
|
|
||||||
plan: { type: Plan.sandbox },
|
|
||||||
})
|
|
||||||
}).not.toThrow()
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(screen.queryByText('custom.upgradeTip.title')).not.toBeInTheDocument()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should show only billing tip for sandbox plan, not contact section', () => {
|
|
||||||
// Arrange & Act
|
|
||||||
renderWithContext({
|
|
||||||
enableBilling: true,
|
|
||||||
plan: { type: Plan.sandbox },
|
|
||||||
})
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(screen.getByText('custom.upgradeTip.title')).toBeInTheDocument()
|
expect(screen.getByText('custom.upgradeTip.title')).toBeInTheDocument()
|
||||||
expect(screen.queryByText('custom.customize.contactUs')).not.toBeInTheDocument()
|
expect(screen.queryByText('custom.customize.contactUs')).not.toBeInTheDocument()
|
||||||
|
|
||||||
|
await user.click(screen.getByText('billing.upgradeBtn.encourageShort'))
|
||||||
|
|
||||||
|
expect(setShowPricingModal).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should show only contact section for professional plan, not billing tip', () => {
|
it('should show the contact link for professional workspaces', () => {
|
||||||
// Arrange & Act
|
mockUseProviderContext.mockReturnValue(createProviderContext({
|
||||||
renderWithContext({
|
|
||||||
enableBilling: true,
|
enableBilling: true,
|
||||||
plan: { type: Plan.professional },
|
planType: Plan.professional,
|
||||||
})
|
}))
|
||||||
|
|
||||||
// Assert
|
render(<CustomPage />)
|
||||||
|
|
||||||
|
const contactLink = screen.getByText('custom.customize.contactUs').closest('a')
|
||||||
expect(screen.queryByText('custom.upgradeTip.title')).not.toBeInTheDocument()
|
expect(screen.queryByText('custom.upgradeTip.title')).not.toBeInTheDocument()
|
||||||
expect(screen.getByText('custom.customize.contactUs')).toBeInTheDocument()
|
expect(contactLink).toHaveAttribute('href', contactSalesUrl)
|
||||||
|
expect(contactLink).toHaveAttribute('target', '_blank')
|
||||||
|
expect(contactLink).toHaveAttribute('rel', 'noopener noreferrer')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should show only contact section for team plan, not billing tip', () => {
|
it('should show the contact link for team workspaces', () => {
|
||||||
// Arrange & Act
|
mockUseProviderContext.mockReturnValue(createProviderContext({
|
||||||
renderWithContext({
|
|
||||||
enableBilling: true,
|
enableBilling: true,
|
||||||
plan: { type: Plan.team },
|
planType: Plan.team,
|
||||||
})
|
}))
|
||||||
|
|
||||||
// Assert
|
render(<CustomPage />)
|
||||||
|
|
||||||
|
expect(screen.getByText('custom.customize.contactUs')).toBeInTheDocument()
|
||||||
expect(screen.queryByText('custom.upgradeTip.title')).not.toBeInTheDocument()
|
expect(screen.queryByText('custom.upgradeTip.title')).not.toBeInTheDocument()
|
||||||
expect(screen.getByText('custom.customize.contactUs')).toBeInTheDocument()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should handle empty plan object', () => {
|
it('should hide both billing sections when billing is disabled', () => {
|
||||||
// Arrange & Act
|
mockUseProviderContext.mockReturnValue(createProviderContext({
|
||||||
expect(() => {
|
|
||||||
renderWithContext({
|
|
||||||
enableBilling: true,
|
|
||||||
plan: {},
|
|
||||||
})
|
|
||||||
}).not.toThrow()
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(screen.getByTestId('custom-web-app-brand')).toBeInTheDocument()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// Accessibility Tests
|
|
||||||
describe('Accessibility', () => {
|
|
||||||
it('should have clickable upgrade button', () => {
|
|
||||||
// Arrange & Act
|
|
||||||
renderWithContext({
|
|
||||||
enableBilling: true,
|
|
||||||
plan: { type: Plan.sandbox },
|
|
||||||
})
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
const upgradeButton = screen.getByText('billing.upgradeBtn.encourageShort')
|
|
||||||
expect(upgradeButton).toBeInTheDocument()
|
|
||||||
expect(upgradeButton).toHaveClass('cursor-pointer')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should have proper external link attributes on contact link', () => {
|
|
||||||
// Arrange & Act
|
|
||||||
renderWithContext({
|
|
||||||
enableBilling: true,
|
|
||||||
plan: { type: Plan.professional },
|
|
||||||
})
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
const link = screen.getByText('custom.customize.contactUs').closest('a')
|
|
||||||
expect(link).toHaveAttribute('rel', 'noopener noreferrer')
|
|
||||||
expect(link).toHaveAttribute('target', '_blank')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should have proper text hierarchy in billing tip', () => {
|
|
||||||
// Arrange & Act
|
|
||||||
renderWithContext({
|
|
||||||
enableBilling: true,
|
|
||||||
plan: { type: Plan.sandbox },
|
|
||||||
})
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
const title = screen.getByText('custom.upgradeTip.title')
|
|
||||||
const description = screen.getByText('custom.upgradeTip.des')
|
|
||||||
|
|
||||||
expect(title).toHaveClass('title-xl-semi-bold')
|
|
||||||
expect(description).toHaveClass('system-sm-regular')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should use semantic color classes', () => {
|
|
||||||
// Arrange & Act
|
|
||||||
renderWithContext({
|
|
||||||
enableBilling: true,
|
|
||||||
plan: { type: Plan.sandbox },
|
|
||||||
})
|
|
||||||
|
|
||||||
// Assert - Check that the billing tip has text content (which implies semantic colors)
|
|
||||||
expect(screen.getByText('custom.upgradeTip.title')).toBeInTheDocument()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// Integration Tests
|
|
||||||
describe('Integration', () => {
|
|
||||||
it('should render both CustomWebAppBrand and billing tip together', () => {
|
|
||||||
// Arrange & Act
|
|
||||||
renderWithContext({
|
|
||||||
enableBilling: true,
|
|
||||||
plan: { type: Plan.sandbox },
|
|
||||||
})
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(screen.getByTestId('custom-web-app-brand')).toBeInTheDocument()
|
|
||||||
expect(screen.getByText('custom.upgradeTip.title')).toBeInTheDocument()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should render both CustomWebAppBrand and contact section together', () => {
|
|
||||||
// Arrange & Act
|
|
||||||
renderWithContext({
|
|
||||||
enableBilling: true,
|
|
||||||
plan: { type: Plan.professional },
|
|
||||||
})
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(screen.getByTestId('custom-web-app-brand')).toBeInTheDocument()
|
|
||||||
expect(screen.getByText('custom.customize.contactUs')).toBeInTheDocument()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should render only CustomWebAppBrand when no billing conditions met', () => {
|
|
||||||
// Arrange & Act
|
|
||||||
renderWithContext({
|
|
||||||
enableBilling: false,
|
enableBilling: false,
|
||||||
plan: { type: Plan.sandbox },
|
planType: Plan.sandbox,
|
||||||
})
|
}))
|
||||||
|
|
||||||
|
render(<CustomPage />)
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(screen.getByTestId('custom-web-app-brand')).toBeInTheDocument()
|
|
||||||
expect(screen.queryByText('custom.upgradeTip.title')).not.toBeInTheDocument()
|
expect(screen.queryByText('custom.upgradeTip.title')).not.toBeInTheDocument()
|
||||||
expect(screen.queryByText('custom.customize.contactUs')).not.toBeInTheDocument()
|
expect(screen.queryByText('custom.customize.contactUs')).not.toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,147 +1,158 @@
|
|||||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { getImageUploadErrorMessage, imageUpload } from '@/app/components/base/image-uploader/utils'
|
import useWebAppBrand from '../hooks/use-web-app-brand'
|
||||||
import { useToastContext } from '@/app/components/base/toast/context'
|
|
||||||
import { Plan } from '@/app/components/billing/type'
|
|
||||||
import { useAppContext } from '@/context/app-context'
|
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
|
||||||
import { useProviderContext } from '@/context/provider-context'
|
|
||||||
import { updateCurrentWorkspace } from '@/service/common'
|
|
||||||
import CustomWebAppBrand from '../index'
|
import CustomWebAppBrand from '../index'
|
||||||
|
|
||||||
vi.mock('@/app/components/base/toast/context', () => ({
|
vi.mock('../hooks/use-web-app-brand', () => ({
|
||||||
useToastContext: vi.fn(),
|
default: vi.fn(),
|
||||||
}))
|
|
||||||
vi.mock('@/service/common', () => ({
|
|
||||||
updateCurrentWorkspace: vi.fn(),
|
|
||||||
}))
|
|
||||||
vi.mock('@/context/app-context', () => ({
|
|
||||||
useAppContext: vi.fn(),
|
|
||||||
}))
|
|
||||||
vi.mock('@/context/provider-context', () => ({
|
|
||||||
useProviderContext: vi.fn(),
|
|
||||||
}))
|
|
||||||
vi.mock('@/context/global-public-context', () => ({
|
|
||||||
useGlobalPublicStore: vi.fn(),
|
|
||||||
}))
|
|
||||||
vi.mock('@/app/components/base/image-uploader/utils', () => ({
|
|
||||||
imageUpload: vi.fn(),
|
|
||||||
getImageUploadErrorMessage: vi.fn(),
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const mockNotify = vi.fn()
|
const mockUseWebAppBrand = vi.mocked(useWebAppBrand)
|
||||||
const mockUseToastContext = vi.mocked(useToastContext)
|
|
||||||
const mockUpdateCurrentWorkspace = vi.mocked(updateCurrentWorkspace)
|
|
||||||
const mockUseAppContext = vi.mocked(useAppContext)
|
|
||||||
const mockUseProviderContext = vi.mocked(useProviderContext)
|
|
||||||
const mockUseGlobalPublicStore = vi.mocked(useGlobalPublicStore)
|
|
||||||
const mockImageUpload = vi.mocked(imageUpload)
|
|
||||||
const mockGetImageUploadErrorMessage = vi.mocked(getImageUploadErrorMessage)
|
|
||||||
|
|
||||||
const defaultPlanUsage = {
|
const createHookState = (overrides: Partial<ReturnType<typeof useWebAppBrand>> = {}): ReturnType<typeof useWebAppBrand> => ({
|
||||||
buildApps: 0,
|
fileId: '',
|
||||||
teamMembers: 0,
|
imgKey: 100,
|
||||||
annotatedResponse: 0,
|
uploadProgress: 0,
|
||||||
documentsUploadQuota: 0,
|
uploading: false,
|
||||||
apiRateLimit: 0,
|
webappLogo: 'https://example.com/replace.png',
|
||||||
triggerEvents: 0,
|
webappBrandRemoved: false,
|
||||||
vectorSpace: 0,
|
uploadDisabled: false,
|
||||||
|
workspaceLogo: 'https://example.com/workspace-logo.png',
|
||||||
|
isSandbox: false,
|
||||||
|
isCurrentWorkspaceManager: true,
|
||||||
|
handleApply: vi.fn(),
|
||||||
|
handleCancel: vi.fn(),
|
||||||
|
handleChange: vi.fn(),
|
||||||
|
handleRestore: vi.fn(),
|
||||||
|
handleSwitch: vi.fn(),
|
||||||
|
...overrides,
|
||||||
|
})
|
||||||
|
|
||||||
|
const renderComponent = (overrides: Partial<ReturnType<typeof useWebAppBrand>> = {}) => {
|
||||||
|
const hookState = createHookState(overrides)
|
||||||
|
mockUseWebAppBrand.mockReturnValue(hookState)
|
||||||
|
return {
|
||||||
|
hookState,
|
||||||
|
...render(<CustomWebAppBrand />),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderComponent = () => render(<CustomWebAppBrand />)
|
|
||||||
|
|
||||||
describe('CustomWebAppBrand', () => {
|
describe('CustomWebAppBrand', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
mockUseToastContext.mockReturnValue({ notify: mockNotify } as unknown as ReturnType<typeof useToastContext>)
|
|
||||||
mockUpdateCurrentWorkspace.mockResolvedValue({} as unknown as Awaited<ReturnType<typeof updateCurrentWorkspace>>)
|
|
||||||
mockUseAppContext.mockReturnValue({
|
|
||||||
currentWorkspace: {
|
|
||||||
custom_config: {
|
|
||||||
replace_webapp_logo: 'https://example.com/replace.png',
|
|
||||||
remove_webapp_brand: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mutateCurrentWorkspace: vi.fn(),
|
|
||||||
isCurrentWorkspaceManager: true,
|
|
||||||
} as unknown as ReturnType<typeof useAppContext>)
|
|
||||||
mockUseProviderContext.mockReturnValue({
|
|
||||||
plan: {
|
|
||||||
type: Plan.professional,
|
|
||||||
usage: defaultPlanUsage,
|
|
||||||
total: defaultPlanUsage,
|
|
||||||
reset: {},
|
|
||||||
},
|
|
||||||
enableBilling: false,
|
|
||||||
} as unknown as ReturnType<typeof useProviderContext>)
|
|
||||||
const systemFeaturesState = {
|
|
||||||
branding: {
|
|
||||||
enabled: true,
|
|
||||||
workspace_logo: 'https://example.com/workspace-logo.png',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
mockUseGlobalPublicStore.mockImplementation(selector => selector ? selector({ systemFeatures: systemFeaturesState, setSystemFeatures: vi.fn() } as unknown as ReturnType<typeof useGlobalPublicStore.getState>) : { systemFeatures: systemFeaturesState })
|
|
||||||
mockGetImageUploadErrorMessage.mockReturnValue('upload error')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('disables upload controls when the user cannot manage the workspace', () => {
|
// Integration coverage for the root component with the hook mocked at the boundary.
|
||||||
mockUseAppContext.mockReturnValue({
|
describe('Rendering', () => {
|
||||||
currentWorkspace: {
|
it('should render the upload controls and preview cards with restore action', () => {
|
||||||
custom_config: {
|
renderComponent()
|
||||||
replace_webapp_logo: '',
|
|
||||||
remove_webapp_brand: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mutateCurrentWorkspace: vi.fn(),
|
|
||||||
isCurrentWorkspaceManager: false,
|
|
||||||
} as unknown as ReturnType<typeof useAppContext>)
|
|
||||||
|
|
||||||
const { container } = renderComponent()
|
expect(screen.getByText('custom.webapp.removeBrand')).toBeInTheDocument()
|
||||||
const fileInput = container.querySelector('input[type="file"]') as HTMLInputElement
|
expect(screen.getByRole('button', { name: 'custom.restore' })).toBeInTheDocument()
|
||||||
expect(fileInput).toBeDisabled()
|
expect(screen.getByRole('button', { name: 'custom.change' })).toBeInTheDocument()
|
||||||
})
|
expect(screen.getByText('Chatflow App')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('Workflow App')).toBeInTheDocument()
|
||||||
it('toggles remove brand switch and calls the backend + mutate', async () => {
|
|
||||||
const mutateMock = vi.fn()
|
|
||||||
mockUseAppContext.mockReturnValue({
|
|
||||||
currentWorkspace: {
|
|
||||||
custom_config: {
|
|
||||||
replace_webapp_logo: '',
|
|
||||||
remove_webapp_brand: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mutateCurrentWorkspace: mutateMock,
|
|
||||||
isCurrentWorkspaceManager: true,
|
|
||||||
} as unknown as ReturnType<typeof useAppContext>)
|
|
||||||
|
|
||||||
renderComponent()
|
|
||||||
const switchInput = screen.getByRole('switch')
|
|
||||||
fireEvent.click(switchInput)
|
|
||||||
|
|
||||||
await waitFor(() => expect(mockUpdateCurrentWorkspace).toHaveBeenCalledWith({
|
|
||||||
url: '/workspaces/custom-config',
|
|
||||||
body: { remove_webapp_brand: true },
|
|
||||||
}))
|
|
||||||
await waitFor(() => expect(mutateMock).toHaveBeenCalled())
|
|
||||||
})
|
|
||||||
|
|
||||||
it('shows cancel/apply buttons after successful upload and cancels properly', async () => {
|
|
||||||
mockImageUpload.mockImplementation(({ onProgressCallback, onSuccessCallback }) => {
|
|
||||||
onProgressCallback(50)
|
|
||||||
onSuccessCallback({ id: 'new-logo' })
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const { container } = renderComponent()
|
it('should hide the restore action when uploads are disabled or no logo is configured', () => {
|
||||||
const fileInput = container.querySelector('input[type="file"]') as HTMLInputElement
|
renderComponent({
|
||||||
const testFile = new File(['content'], 'logo.png', { type: 'image/png' })
|
uploadDisabled: true,
|
||||||
fireEvent.change(fileInput, { target: { files: [testFile] } })
|
webappLogo: '',
|
||||||
|
})
|
||||||
|
|
||||||
await waitFor(() => expect(mockImageUpload).toHaveBeenCalled())
|
expect(screen.queryByRole('button', { name: 'custom.restore' })).not.toBeInTheDocument()
|
||||||
await waitFor(() => screen.getByRole('button', { name: 'custom.apply' }))
|
expect(screen.getByRole('button', { name: 'custom.upload' })).toBeDisabled()
|
||||||
|
})
|
||||||
|
|
||||||
const cancelButton = screen.getByRole('button', { name: 'common.operation.cancel' })
|
it('should show the uploading button and failure message when upload state requires it', () => {
|
||||||
fireEvent.click(cancelButton)
|
renderComponent({
|
||||||
|
uploading: true,
|
||||||
|
uploadProgress: -1,
|
||||||
|
})
|
||||||
|
|
||||||
await waitFor(() => expect(screen.queryByRole('button', { name: 'custom.apply' })).toBeNull())
|
expect(screen.getByRole('button', { name: 'custom.uploading' })).toBeDisabled()
|
||||||
|
expect(screen.getByText('custom.uploadedFail')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show apply and cancel actions when a new file is ready', () => {
|
||||||
|
renderComponent({
|
||||||
|
fileId: 'new-logo',
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(screen.getByRole('button', { name: 'custom.apply' })).toBeInTheDocument()
|
||||||
|
expect(screen.getByRole('button', { name: 'common.operation.cancel' })).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should disable the switch when sandbox restrictions are active', () => {
|
||||||
|
renderComponent({
|
||||||
|
isSandbox: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(screen.getByRole('switch')).toHaveAttribute('aria-disabled', 'true')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should default the switch to unchecked when brand removal state is missing', () => {
|
||||||
|
const { container } = renderComponent({
|
||||||
|
webappBrandRemoved: undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(screen.getByRole('switch')).toHaveAttribute('aria-checked', 'false')
|
||||||
|
expect(container.querySelector('.opacity-30')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should dim the upload row when brand removal is enabled', () => {
|
||||||
|
const { container } = renderComponent({
|
||||||
|
webappBrandRemoved: true,
|
||||||
|
uploadDisabled: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(screen.getByRole('switch')).toHaveAttribute('aria-checked', 'true')
|
||||||
|
expect(container.querySelector('.opacity-30')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// User interactions delegated to the hook callbacks.
|
||||||
|
describe('Interactions', () => {
|
||||||
|
it('should delegate switch changes to the hook handler', () => {
|
||||||
|
const { hookState } = renderComponent()
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('switch'))
|
||||||
|
|
||||||
|
expect(hookState.handleSwitch).toHaveBeenCalledWith(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should delegate file input changes and reset the native input value on click', () => {
|
||||||
|
const { container, hookState } = renderComponent()
|
||||||
|
const fileInput = container.querySelector('input[type="file"]') as HTMLInputElement
|
||||||
|
const file = new File(['logo'], 'logo.png', { type: 'image/png' })
|
||||||
|
|
||||||
|
Object.defineProperty(fileInput, 'value', {
|
||||||
|
configurable: true,
|
||||||
|
value: 'stale-selection',
|
||||||
|
writable: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
fireEvent.click(fileInput)
|
||||||
|
fireEvent.change(fileInput, {
|
||||||
|
target: { files: [file] },
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(fileInput.value).toBe('')
|
||||||
|
expect(hookState.handleChange).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should delegate restore, cancel, and apply actions to the hook handlers', () => {
|
||||||
|
const { hookState } = renderComponent({
|
||||||
|
fileId: 'new-logo',
|
||||||
|
})
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'custom.restore' }))
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'common.operation.cancel' }))
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'custom.apply' }))
|
||||||
|
|
||||||
|
expect(hookState.handleRestore).toHaveBeenCalledTimes(1)
|
||||||
|
expect(hookState.handleCancel).toHaveBeenCalledTimes(1)
|
||||||
|
expect(hookState.handleApply).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -0,0 +1,31 @@
|
|||||||
|
import { render, screen } from '@testing-library/react'
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import ChatPreviewCard from '../chat-preview-card'
|
||||||
|
|
||||||
|
describe('ChatPreviewCard', () => {
|
||||||
|
it('should render the chat preview with the powered-by footer', () => {
|
||||||
|
render(
|
||||||
|
<ChatPreviewCard
|
||||||
|
imgKey={8}
|
||||||
|
webappLogo="https://example.com/custom-logo.png"
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(screen.getByText('Chatflow App')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('Hello! How can I assist you today?')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('Talk to Dify')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('POWERED BY')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should hide chat branding footer when brand removal is enabled', () => {
|
||||||
|
render(
|
||||||
|
<ChatPreviewCard
|
||||||
|
imgKey={8}
|
||||||
|
webappBrandRemoved
|
||||||
|
webappLogo="https://example.com/custom-logo.png"
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(screen.queryByText('POWERED BY')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
import { render, screen } from '@testing-library/react'
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import PoweredByBrand from '../powered-by-brand'
|
||||||
|
|
||||||
|
describe('PoweredByBrand', () => {
|
||||||
|
it('should render the workspace logo when available', () => {
|
||||||
|
render(
|
||||||
|
<PoweredByBrand
|
||||||
|
imgKey={1}
|
||||||
|
workspaceLogo="https://example.com/workspace-logo.png"
|
||||||
|
webappLogo="https://example.com/custom-logo.png"
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(screen.getByText('POWERED BY')).toBeInTheDocument()
|
||||||
|
expect(screen.getByAltText('logo')).toHaveAttribute('src', 'https://example.com/workspace-logo.png')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should fall back to the custom web app logo when workspace branding is unavailable', () => {
|
||||||
|
render(
|
||||||
|
<PoweredByBrand
|
||||||
|
imgKey={42}
|
||||||
|
webappLogo="https://example.com/custom-logo.png"
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(screen.getByAltText('logo')).toHaveAttribute('src', 'https://example.com/custom-logo.png?hash=42')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should fall back to the Dify logo when no custom branding exists', () => {
|
||||||
|
render(<PoweredByBrand imgKey={7} />)
|
||||||
|
|
||||||
|
expect(screen.getByAltText('Dify logo')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render nothing when branding is removed', () => {
|
||||||
|
const { container } = render(<PoweredByBrand imgKey={7} webappBrandRemoved />)
|
||||||
|
|
||||||
|
expect(container).toBeEmptyDOMElement()
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
import { render, screen } from '@testing-library/react'
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import WorkflowPreviewCard from '../workflow-preview-card'
|
||||||
|
|
||||||
|
describe('WorkflowPreviewCard', () => {
|
||||||
|
it('should render the workflow preview with execute action and branding footer', () => {
|
||||||
|
render(
|
||||||
|
<WorkflowPreviewCard
|
||||||
|
imgKey={9}
|
||||||
|
workspaceLogo="https://example.com/workspace-logo.png"
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(screen.getByText('Workflow App')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('RUN ONCE')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('RUN BATCH')).toBeInTheDocument()
|
||||||
|
expect(screen.getByRole('button', { name: /Execute/i })).toBeDisabled()
|
||||||
|
expect(screen.getByAltText('logo')).toHaveAttribute('src', 'https://example.com/workspace-logo.png')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should hide workflow branding footer when brand removal is enabled', () => {
|
||||||
|
render(
|
||||||
|
<WorkflowPreviewCard
|
||||||
|
imgKey={9}
|
||||||
|
webappBrandRemoved
|
||||||
|
workspaceLogo="https://example.com/workspace-logo.png"
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(screen.queryByText('POWERED BY')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -0,0 +1,78 @@
|
|||||||
|
import Button from '@/app/components/base/button'
|
||||||
|
import { cn } from '@/utils/classnames'
|
||||||
|
import PoweredByBrand from './powered-by-brand'
|
||||||
|
|
||||||
|
type ChatPreviewCardProps = {
|
||||||
|
webappBrandRemoved?: boolean
|
||||||
|
workspaceLogo?: string
|
||||||
|
webappLogo?: string
|
||||||
|
imgKey: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChatPreviewCard = ({
|
||||||
|
webappBrandRemoved,
|
||||||
|
workspaceLogo,
|
||||||
|
webappLogo,
|
||||||
|
imgKey,
|
||||||
|
}: ChatPreviewCardProps) => {
|
||||||
|
return (
|
||||||
|
<div className="flex h-[320px] grow basis-1/2 overflow-hidden rounded-2xl border-[0.5px] border-components-panel-border-subtle bg-background-default-burn">
|
||||||
|
<div className="flex h-full w-[232px] shrink-0 flex-col p-1 pr-0">
|
||||||
|
<div className="flex items-center gap-3 p-3 pr-2">
|
||||||
|
<div className={cn('inline-flex h-8 w-8 items-center justify-center rounded-lg border border-divider-regular', 'bg-components-icon-bg-blue-light-solid')}>
|
||||||
|
<span className="i-custom-vender-solid-communication-bubble-text-mod h-4 w-4 text-components-avatar-shape-fill-stop-100" />
|
||||||
|
</div>
|
||||||
|
<div className="grow text-text-secondary system-md-semibold">Chatflow App</div>
|
||||||
|
<div className="p-1.5">
|
||||||
|
<span className="i-ri-layout-left-2-line h-4 w-4 text-text-tertiary" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="shrink-0 px-4 py-3">
|
||||||
|
<Button variant="secondary-accent" className="w-full justify-center">
|
||||||
|
<span className="i-ri-edit-box-line mr-1 h-4 w-4" />
|
||||||
|
<div className="p-1 opacity-20">
|
||||||
|
<div className="h-2 w-[94px] rounded-sm bg-text-accent-light-mode-only"></div>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="grow px-3 pt-5">
|
||||||
|
<div className="flex h-8 items-center px-3 py-1">
|
||||||
|
<div className="h-2 w-14 rounded-sm bg-text-quaternary opacity-20"></div>
|
||||||
|
</div>
|
||||||
|
<div className="flex h-8 items-center px-3 py-1">
|
||||||
|
<div className="h-2 w-[168px] rounded-sm bg-text-quaternary opacity-20"></div>
|
||||||
|
</div>
|
||||||
|
<div className="flex h-8 items-center px-3 py-1">
|
||||||
|
<div className="h-2 w-[128px] rounded-sm bg-text-quaternary opacity-20"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex shrink-0 items-center justify-between p-3">
|
||||||
|
<div className="p-1.5">
|
||||||
|
<span className="i-ri-equalizer-2-line h-4 w-4 text-text-tertiary" />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<PoweredByBrand
|
||||||
|
webappBrandRemoved={webappBrandRemoved}
|
||||||
|
workspaceLogo={workspaceLogo}
|
||||||
|
webappLogo={webappLogo}
|
||||||
|
imgKey={imgKey}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-[138px] grow flex-col justify-between p-2 pr-0">
|
||||||
|
<div className="flex grow flex-col justify-between rounded-l-2xl border-[0.5px] border-r-0 border-components-panel-border-subtle bg-chatbot-bg pb-4 pl-[22px] pt-16">
|
||||||
|
<div className="w-[720px] rounded-2xl border border-divider-subtle bg-chat-bubble-bg px-4 py-3">
|
||||||
|
<div className="mb-1 text-text-primary body-md-regular">Hello! How can I assist you today?</div>
|
||||||
|
<Button size="small">
|
||||||
|
<div className="h-2 w-[144px] rounded-sm bg-text-quaternary opacity-20"></div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="flex h-[52px] w-[578px] items-center rounded-xl border border-components-chat-input-border bg-components-panel-bg-blur pl-3.5 text-text-placeholder shadow-md backdrop-blur-sm body-lg-regular">Talk to Dify</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChatPreviewCard
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
import DifyLogo from '@/app/components/base/logo/dify-logo'
|
||||||
|
|
||||||
|
type PoweredByBrandProps = {
|
||||||
|
webappBrandRemoved?: boolean
|
||||||
|
workspaceLogo?: string
|
||||||
|
webappLogo?: string
|
||||||
|
imgKey: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const PoweredByBrand = ({
|
||||||
|
webappBrandRemoved,
|
||||||
|
workspaceLogo,
|
||||||
|
webappLogo,
|
||||||
|
imgKey,
|
||||||
|
}: PoweredByBrandProps) => {
|
||||||
|
if (webappBrandRemoved)
|
||||||
|
return null
|
||||||
|
|
||||||
|
const previewLogo = workspaceLogo || (webappLogo ? `${webappLogo}?hash=${imgKey}` : '')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="text-text-tertiary system-2xs-medium-uppercase">POWERED BY</div>
|
||||||
|
{previewLogo
|
||||||
|
? <img src={previewLogo} alt="logo" className="block h-5 w-auto" />
|
||||||
|
: <DifyLogo size="small" />}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PoweredByBrand
|
||||||
@ -0,0 +1,64 @@
|
|||||||
|
import Button from '@/app/components/base/button'
|
||||||
|
import { cn } from '@/utils/classnames'
|
||||||
|
import PoweredByBrand from './powered-by-brand'
|
||||||
|
|
||||||
|
type WorkflowPreviewCardProps = {
|
||||||
|
webappBrandRemoved?: boolean
|
||||||
|
workspaceLogo?: string
|
||||||
|
webappLogo?: string
|
||||||
|
imgKey: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const WorkflowPreviewCard = ({
|
||||||
|
webappBrandRemoved,
|
||||||
|
workspaceLogo,
|
||||||
|
webappLogo,
|
||||||
|
imgKey,
|
||||||
|
}: WorkflowPreviewCardProps) => {
|
||||||
|
return (
|
||||||
|
<div className="flex h-[320px] grow basis-1/2 flex-col overflow-hidden rounded-2xl border-[0.5px] border-components-panel-border-subtle bg-background-default-burn">
|
||||||
|
<div className="w-full border-b-[0.5px] border-divider-subtle p-4 pb-0">
|
||||||
|
<div className="mb-2 flex items-center gap-3">
|
||||||
|
<div className={cn('inline-flex h-8 w-8 items-center justify-center rounded-lg border border-divider-regular', 'bg-components-icon-bg-indigo-solid')}>
|
||||||
|
<span className="i-ri-exchange-2-fill h-4 w-4 text-components-avatar-shape-fill-stop-100" />
|
||||||
|
</div>
|
||||||
|
<div className="grow text-text-secondary system-md-semibold">Workflow App</div>
|
||||||
|
<div className="p-1.5">
|
||||||
|
<span className="i-ri-layout-left-2-line h-4 w-4 text-text-tertiary" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="flex h-10 shrink-0 items-center border-b-2 border-components-tab-active text-text-primary system-md-semibold-uppercase">RUN ONCE</div>
|
||||||
|
<div className="flex h-10 grow items-center border-b-2 border-transparent text-text-tertiary system-md-semibold-uppercase">RUN BATCH</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grow bg-components-panel-bg">
|
||||||
|
<div className="p-4 pb-1">
|
||||||
|
<div className="mb-1 py-2">
|
||||||
|
<div className="h-2 w-20 rounded-sm bg-text-quaternary opacity-20"></div>
|
||||||
|
</div>
|
||||||
|
<div className="h-16 w-full rounded-lg bg-components-input-bg-normal"></div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between px-4 py-3">
|
||||||
|
<Button size="small">
|
||||||
|
<div className="h-2 w-10 rounded-sm bg-text-quaternary opacity-20"></div>
|
||||||
|
</Button>
|
||||||
|
<Button variant="primary" size="small" disabled>
|
||||||
|
<span className="i-ri-play-large-line mr-1 h-4 w-4" />
|
||||||
|
<span>Execute</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex h-12 shrink-0 items-center gap-1.5 bg-components-panel-bg p-4 pt-3">
|
||||||
|
<PoweredByBrand
|
||||||
|
webappBrandRemoved={webappBrandRemoved}
|
||||||
|
workspaceLogo={workspaceLogo}
|
||||||
|
webappLogo={webappLogo}
|
||||||
|
imgKey={imgKey}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WorkflowPreviewCard
|
||||||
@ -0,0 +1,385 @@
|
|||||||
|
import type { ChangeEvent } from 'react'
|
||||||
|
import type { AppContextValue } from '@/context/app-context'
|
||||||
|
import type { SystemFeatures } from '@/types/feature'
|
||||||
|
import { act, renderHook } from '@testing-library/react'
|
||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import { createMockProviderContextValue } from '@/__mocks__/provider-context'
|
||||||
|
import { getImageUploadErrorMessage, imageUpload } from '@/app/components/base/image-uploader/utils'
|
||||||
|
import { useToastContext } from '@/app/components/base/toast/context'
|
||||||
|
import { defaultPlan } from '@/app/components/billing/config'
|
||||||
|
import { Plan } from '@/app/components/billing/type'
|
||||||
|
import {
|
||||||
|
initialLangGeniusVersionInfo,
|
||||||
|
initialWorkspaceInfo,
|
||||||
|
useAppContext,
|
||||||
|
userProfilePlaceholder,
|
||||||
|
} from '@/context/app-context'
|
||||||
|
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||||
|
import { useProviderContext } from '@/context/provider-context'
|
||||||
|
import { updateCurrentWorkspace } from '@/service/common'
|
||||||
|
import { defaultSystemFeatures } from '@/types/feature'
|
||||||
|
import useWebAppBrand from '../use-web-app-brand'
|
||||||
|
|
||||||
|
vi.mock('@/app/components/base/toast/context', () => ({
|
||||||
|
useToastContext: vi.fn(),
|
||||||
|
}))
|
||||||
|
vi.mock('@/service/common', () => ({
|
||||||
|
updateCurrentWorkspace: vi.fn(),
|
||||||
|
}))
|
||||||
|
vi.mock('@/context/app-context', async (importOriginal) => {
|
||||||
|
const actual = await importOriginal<typeof import('@/context/app-context')>()
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
useAppContext: vi.fn(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
vi.mock('@/context/provider-context', () => ({
|
||||||
|
useProviderContext: vi.fn(),
|
||||||
|
}))
|
||||||
|
vi.mock('@/context/global-public-context', () => ({
|
||||||
|
useGlobalPublicStore: vi.fn(),
|
||||||
|
}))
|
||||||
|
vi.mock('@/app/components/base/image-uploader/utils', () => ({
|
||||||
|
imageUpload: vi.fn(),
|
||||||
|
getImageUploadErrorMessage: vi.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const mockNotify = vi.fn()
|
||||||
|
const mockUseToastContext = vi.mocked(useToastContext)
|
||||||
|
const mockUpdateCurrentWorkspace = vi.mocked(updateCurrentWorkspace)
|
||||||
|
const mockUseAppContext = vi.mocked(useAppContext)
|
||||||
|
const mockUseProviderContext = vi.mocked(useProviderContext)
|
||||||
|
const mockUseGlobalPublicStore = vi.mocked(useGlobalPublicStore)
|
||||||
|
const mockImageUpload = vi.mocked(imageUpload)
|
||||||
|
const mockGetImageUploadErrorMessage = vi.mocked(getImageUploadErrorMessage)
|
||||||
|
|
||||||
|
const createProviderContext = ({
|
||||||
|
enableBilling = false,
|
||||||
|
planType = Plan.professional,
|
||||||
|
}: {
|
||||||
|
enableBilling?: boolean
|
||||||
|
planType?: Plan
|
||||||
|
} = {}) => {
|
||||||
|
return createMockProviderContextValue({
|
||||||
|
enableBilling,
|
||||||
|
plan: {
|
||||||
|
...defaultPlan,
|
||||||
|
type: planType,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const createSystemFeatures = (brandingOverrides: Partial<SystemFeatures['branding']> = {}): SystemFeatures => ({
|
||||||
|
...defaultSystemFeatures,
|
||||||
|
branding: {
|
||||||
|
...defaultSystemFeatures.branding,
|
||||||
|
enabled: true,
|
||||||
|
workspace_logo: 'https://example.com/workspace-logo.png',
|
||||||
|
...brandingOverrides,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const createAppContextValue = (overrides: Partial<AppContextValue> = {}): AppContextValue => {
|
||||||
|
const { currentWorkspace: currentWorkspaceOverride, ...restOverrides } = overrides
|
||||||
|
const workspaceOverrides: Partial<AppContextValue['currentWorkspace']> = currentWorkspaceOverride ?? {}
|
||||||
|
const currentWorkspace = {
|
||||||
|
...initialWorkspaceInfo,
|
||||||
|
...workspaceOverrides,
|
||||||
|
custom_config: {
|
||||||
|
replace_webapp_logo: 'https://example.com/replace.png',
|
||||||
|
remove_webapp_brand: false,
|
||||||
|
...workspaceOverrides.custom_config,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
userProfile: userProfilePlaceholder,
|
||||||
|
mutateUserProfile: vi.fn(),
|
||||||
|
isCurrentWorkspaceManager: true,
|
||||||
|
isCurrentWorkspaceOwner: false,
|
||||||
|
isCurrentWorkspaceEditor: false,
|
||||||
|
isCurrentWorkspaceDatasetOperator: false,
|
||||||
|
mutateCurrentWorkspace: vi.fn(),
|
||||||
|
langGeniusVersionInfo: initialLangGeniusVersionInfo,
|
||||||
|
useSelector: vi.fn() as unknown as AppContextValue['useSelector'],
|
||||||
|
isLoadingCurrentWorkspace: false,
|
||||||
|
isValidatingCurrentWorkspace: false,
|
||||||
|
...restOverrides,
|
||||||
|
currentWorkspace,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('useWebAppBrand', () => {
|
||||||
|
let appContextValue: AppContextValue
|
||||||
|
let systemFeatures: SystemFeatures
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
|
||||||
|
appContextValue = createAppContextValue()
|
||||||
|
systemFeatures = createSystemFeatures()
|
||||||
|
|
||||||
|
mockUseToastContext.mockReturnValue({ notify: mockNotify } as unknown as ReturnType<typeof useToastContext>)
|
||||||
|
mockUpdateCurrentWorkspace.mockResolvedValue(appContextValue.currentWorkspace)
|
||||||
|
mockUseAppContext.mockImplementation(() => appContextValue)
|
||||||
|
mockUseProviderContext.mockReturnValue(createProviderContext())
|
||||||
|
mockUseGlobalPublicStore.mockImplementation(selector => selector({
|
||||||
|
systemFeatures,
|
||||||
|
setSystemFeatures: vi.fn(),
|
||||||
|
}))
|
||||||
|
mockGetImageUploadErrorMessage.mockReturnValue('upload error')
|
||||||
|
})
|
||||||
|
|
||||||
|
// Derived state from context and store inputs.
|
||||||
|
describe('derived state', () => {
|
||||||
|
it('should expose workspace branding and upload availability by default', () => {
|
||||||
|
const { result } = renderHook(() => useWebAppBrand())
|
||||||
|
|
||||||
|
expect(result.current.webappLogo).toBe('https://example.com/replace.png')
|
||||||
|
expect(result.current.workspaceLogo).toBe('https://example.com/workspace-logo.png')
|
||||||
|
expect(result.current.uploadDisabled).toBe(false)
|
||||||
|
expect(result.current.uploading).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should disable uploads in sandbox workspaces and when branding is removed', () => {
|
||||||
|
mockUseProviderContext.mockReturnValue(createProviderContext({
|
||||||
|
enableBilling: true,
|
||||||
|
planType: Plan.sandbox,
|
||||||
|
}))
|
||||||
|
appContextValue = createAppContextValue({
|
||||||
|
currentWorkspace: {
|
||||||
|
...initialWorkspaceInfo,
|
||||||
|
custom_config: {
|
||||||
|
replace_webapp_logo: 'https://example.com/replace.png',
|
||||||
|
remove_webapp_brand: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useWebAppBrand())
|
||||||
|
|
||||||
|
expect(result.current.isSandbox).toBe(true)
|
||||||
|
expect(result.current.webappBrandRemoved).toBe(true)
|
||||||
|
expect(result.current.uploadDisabled).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should fall back to an empty workspace logo when branding is disabled', () => {
|
||||||
|
systemFeatures = createSystemFeatures({
|
||||||
|
enabled: false,
|
||||||
|
workspace_logo: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useWebAppBrand())
|
||||||
|
|
||||||
|
expect(result.current.workspaceLogo).toBe('')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should fall back to an empty custom logo when custom config is missing', () => {
|
||||||
|
appContextValue = {
|
||||||
|
...createAppContextValue(),
|
||||||
|
currentWorkspace: {
|
||||||
|
...initialWorkspaceInfo,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useWebAppBrand())
|
||||||
|
|
||||||
|
expect(result.current.webappLogo).toBe('')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// State transitions driven by user actions.
|
||||||
|
describe('actions', () => {
|
||||||
|
it('should ignore empty file selections', () => {
|
||||||
|
const { result } = renderHook(() => useWebAppBrand())
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.handleChange({
|
||||||
|
target: { files: [] },
|
||||||
|
} as unknown as ChangeEvent<HTMLInputElement>)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(mockImageUpload).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should reject oversized files before upload starts', () => {
|
||||||
|
const { result } = renderHook(() => useWebAppBrand())
|
||||||
|
const oversizedFile = new File(['logo'], 'logo.png', { type: 'image/png' })
|
||||||
|
|
||||||
|
Object.defineProperty(oversizedFile, 'size', {
|
||||||
|
configurable: true,
|
||||||
|
value: 5 * 1024 * 1024 + 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.handleChange({
|
||||||
|
target: { files: [oversizedFile] },
|
||||||
|
} as unknown as ChangeEvent<HTMLInputElement>)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(mockImageUpload).not.toHaveBeenCalled()
|
||||||
|
expect(mockNotify).toHaveBeenCalledWith({
|
||||||
|
type: 'error',
|
||||||
|
message: 'common.imageUploader.uploadFromComputerLimit:{"size":5}',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should update upload state after a successful file upload', () => {
|
||||||
|
mockImageUpload.mockImplementation(({ onProgressCallback, onSuccessCallback }) => {
|
||||||
|
onProgressCallback(100)
|
||||||
|
onSuccessCallback({ id: 'new-logo' })
|
||||||
|
})
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useWebAppBrand())
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.handleChange({
|
||||||
|
target: { files: [new File(['logo'], 'logo.png', { type: 'image/png' })] },
|
||||||
|
} as unknown as ChangeEvent<HTMLInputElement>)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.current.fileId).toBe('new-logo')
|
||||||
|
expect(result.current.uploadProgress).toBe(100)
|
||||||
|
expect(result.current.uploading).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should expose the uploading state while progress is incomplete', () => {
|
||||||
|
mockImageUpload.mockImplementation(({ onProgressCallback }) => {
|
||||||
|
onProgressCallback(50)
|
||||||
|
})
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useWebAppBrand())
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.handleChange({
|
||||||
|
target: { files: [new File(['logo'], 'logo.png', { type: 'image/png' })] },
|
||||||
|
} as unknown as ChangeEvent<HTMLInputElement>)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.current.uploadProgress).toBe(50)
|
||||||
|
expect(result.current.uploading).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should surface upload errors and set the failure state', () => {
|
||||||
|
mockImageUpload.mockImplementation(({ onErrorCallback }) => {
|
||||||
|
onErrorCallback({ response: { code: 'forbidden' } })
|
||||||
|
})
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useWebAppBrand())
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.handleChange({
|
||||||
|
target: { files: [new File(['logo'], 'logo.png', { type: 'image/png' })] },
|
||||||
|
} as unknown as ChangeEvent<HTMLInputElement>)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(mockGetImageUploadErrorMessage).toHaveBeenCalled()
|
||||||
|
expect(mockNotify).toHaveBeenCalledWith({
|
||||||
|
type: 'error',
|
||||||
|
message: 'upload error',
|
||||||
|
})
|
||||||
|
expect(result.current.uploadProgress).toBe(-1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should persist the selected logo and reset transient state on apply', async () => {
|
||||||
|
const mutateCurrentWorkspace = vi.fn()
|
||||||
|
appContextValue = createAppContextValue({
|
||||||
|
mutateCurrentWorkspace,
|
||||||
|
})
|
||||||
|
mockImageUpload.mockImplementation(({ onSuccessCallback }) => {
|
||||||
|
onSuccessCallback({ id: 'new-logo' })
|
||||||
|
})
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useWebAppBrand())
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.handleChange({
|
||||||
|
target: { files: [new File(['logo'], 'logo.png', { type: 'image/png' })] },
|
||||||
|
} as unknown as ChangeEvent<HTMLInputElement>)
|
||||||
|
})
|
||||||
|
|
||||||
|
const previousImgKey = result.current.imgKey
|
||||||
|
const dateNowSpy = vi.spyOn(Date, 'now').mockReturnValue(previousImgKey + 1)
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.handleApply()
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(mockUpdateCurrentWorkspace).toHaveBeenCalledWith({
|
||||||
|
url: '/workspaces/custom-config',
|
||||||
|
body: {
|
||||||
|
remove_webapp_brand: false,
|
||||||
|
replace_webapp_logo: 'new-logo',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expect(mutateCurrentWorkspace).toHaveBeenCalledTimes(1)
|
||||||
|
expect(result.current.fileId).toBe('')
|
||||||
|
expect(result.current.imgKey).toBe(previousImgKey + 1)
|
||||||
|
dateNowSpy.mockRestore()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should restore the default branding configuration', async () => {
|
||||||
|
const mutateCurrentWorkspace = vi.fn()
|
||||||
|
appContextValue = createAppContextValue({
|
||||||
|
mutateCurrentWorkspace,
|
||||||
|
})
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useWebAppBrand())
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.handleRestore()
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(mockUpdateCurrentWorkspace).toHaveBeenCalledWith({
|
||||||
|
url: '/workspaces/custom-config',
|
||||||
|
body: {
|
||||||
|
remove_webapp_brand: false,
|
||||||
|
replace_webapp_logo: '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expect(mutateCurrentWorkspace).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should persist brand removal changes', async () => {
|
||||||
|
const mutateCurrentWorkspace = vi.fn()
|
||||||
|
appContextValue = createAppContextValue({
|
||||||
|
mutateCurrentWorkspace,
|
||||||
|
})
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useWebAppBrand())
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.handleSwitch(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(mockUpdateCurrentWorkspace).toHaveBeenCalledWith({
|
||||||
|
url: '/workspaces/custom-config',
|
||||||
|
body: {
|
||||||
|
remove_webapp_brand: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expect(mutateCurrentWorkspace).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should clear temporary upload state on cancel', () => {
|
||||||
|
mockImageUpload.mockImplementation(({ onSuccessCallback }) => {
|
||||||
|
onSuccessCallback({ id: 'new-logo' })
|
||||||
|
})
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useWebAppBrand())
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.handleChange({
|
||||||
|
target: { files: [new File(['logo'], 'logo.png', { type: 'image/png' })] },
|
||||||
|
} as unknown as ChangeEvent<HTMLInputElement>)
|
||||||
|
})
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.handleCancel()
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.current.fileId).toBe('')
|
||||||
|
expect(result.current.uploadProgress).toBe(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -0,0 +1,121 @@
|
|||||||
|
import type { ChangeEvent } from 'react'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { getImageUploadErrorMessage, imageUpload } from '@/app/components/base/image-uploader/utils'
|
||||||
|
import { useToastContext } from '@/app/components/base/toast/context'
|
||||||
|
import { Plan } from '@/app/components/billing/type'
|
||||||
|
import { useAppContext } from '@/context/app-context'
|
||||||
|
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||||
|
import { useProviderContext } from '@/context/provider-context'
|
||||||
|
import { updateCurrentWorkspace } from '@/service/common'
|
||||||
|
|
||||||
|
const MAX_LOGO_FILE_SIZE = 5 * 1024 * 1024
|
||||||
|
const CUSTOM_CONFIG_URL = '/workspaces/custom-config'
|
||||||
|
const WEB_APP_LOGO_UPLOAD_URL = '/workspaces/custom-config/webapp-logo/upload'
|
||||||
|
|
||||||
|
const useWebAppBrand = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { notify } = useToastContext()
|
||||||
|
const { plan, enableBilling } = useProviderContext()
|
||||||
|
const {
|
||||||
|
currentWorkspace,
|
||||||
|
mutateCurrentWorkspace,
|
||||||
|
isCurrentWorkspaceManager,
|
||||||
|
} = useAppContext()
|
||||||
|
const [fileId, setFileId] = useState('')
|
||||||
|
const [imgKey, setImgKey] = useState(() => Date.now())
|
||||||
|
const [uploadProgress, setUploadProgress] = useState(0)
|
||||||
|
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
|
||||||
|
|
||||||
|
const isSandbox = enableBilling && plan.type === Plan.sandbox
|
||||||
|
const uploading = uploadProgress > 0 && uploadProgress < 100
|
||||||
|
const webappLogo = currentWorkspace.custom_config?.replace_webapp_logo || ''
|
||||||
|
const webappBrandRemoved = currentWorkspace.custom_config?.remove_webapp_brand
|
||||||
|
const uploadDisabled = isSandbox || webappBrandRemoved || !isCurrentWorkspaceManager
|
||||||
|
const workspaceLogo = systemFeatures.branding.enabled ? systemFeatures.branding.workspace_logo : ''
|
||||||
|
|
||||||
|
const persistWorkspaceBrand = async (body: Record<string, unknown>) => {
|
||||||
|
await updateCurrentWorkspace({
|
||||||
|
url: CUSTOM_CONFIG_URL,
|
||||||
|
body,
|
||||||
|
})
|
||||||
|
mutateCurrentWorkspace()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = e.target.files?.[0]
|
||||||
|
|
||||||
|
if (!file)
|
||||||
|
return
|
||||||
|
|
||||||
|
if (file.size > MAX_LOGO_FILE_SIZE) {
|
||||||
|
notify({ type: 'error', message: t('imageUploader.uploadFromComputerLimit', { ns: 'common', size: 5 }) })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
imageUpload({
|
||||||
|
file,
|
||||||
|
onProgressCallback: setUploadProgress,
|
||||||
|
onSuccessCallback: (res) => {
|
||||||
|
setUploadProgress(100)
|
||||||
|
setFileId(res.id)
|
||||||
|
},
|
||||||
|
onErrorCallback: (error) => {
|
||||||
|
const errorMessage = getImageUploadErrorMessage(
|
||||||
|
error,
|
||||||
|
t('imageUploader.uploadFromComputerUploadError', { ns: 'common' }),
|
||||||
|
t,
|
||||||
|
)
|
||||||
|
notify({ type: 'error', message: errorMessage })
|
||||||
|
setUploadProgress(-1)
|
||||||
|
},
|
||||||
|
}, false, WEB_APP_LOGO_UPLOAD_URL)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleApply = async () => {
|
||||||
|
await persistWorkspaceBrand({
|
||||||
|
remove_webapp_brand: webappBrandRemoved,
|
||||||
|
replace_webapp_logo: fileId,
|
||||||
|
})
|
||||||
|
setFileId('')
|
||||||
|
setImgKey(Date.now())
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRestore = async () => {
|
||||||
|
await persistWorkspaceBrand({
|
||||||
|
remove_webapp_brand: false,
|
||||||
|
replace_webapp_logo: '',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSwitch = async (checked: boolean) => {
|
||||||
|
await persistWorkspaceBrand({
|
||||||
|
remove_webapp_brand: checked,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
setFileId('')
|
||||||
|
setUploadProgress(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
fileId,
|
||||||
|
imgKey,
|
||||||
|
uploadProgress,
|
||||||
|
uploading,
|
||||||
|
webappLogo,
|
||||||
|
webappBrandRemoved,
|
||||||
|
uploadDisabled,
|
||||||
|
workspaceLogo,
|
||||||
|
isSandbox,
|
||||||
|
isCurrentWorkspaceManager,
|
||||||
|
handleApply,
|
||||||
|
handleCancel,
|
||||||
|
handleChange,
|
||||||
|
handleRestore,
|
||||||
|
handleSwitch,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useWebAppBrand
|
||||||
@ -1,118 +1,33 @@
|
|||||||
import type { ChangeEvent } from 'react'
|
|
||||||
import {
|
|
||||||
RiEditBoxLine,
|
|
||||||
RiEqualizer2Line,
|
|
||||||
RiExchange2Fill,
|
|
||||||
RiImageAddLine,
|
|
||||||
RiLayoutLeft2Line,
|
|
||||||
RiLoader2Line,
|
|
||||||
RiPlayLargeLine,
|
|
||||||
} from '@remixicon/react'
|
|
||||||
import { useState } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import Button from '@/app/components/base/button'
|
import Button from '@/app/components/base/button'
|
||||||
import Divider from '@/app/components/base/divider'
|
import Divider from '@/app/components/base/divider'
|
||||||
import { BubbleTextMod } from '@/app/components/base/icons/src/vender/solid/communication'
|
|
||||||
import { getImageUploadErrorMessage, imageUpload } from '@/app/components/base/image-uploader/utils'
|
|
||||||
import DifyLogo from '@/app/components/base/logo/dify-logo'
|
|
||||||
import Switch from '@/app/components/base/switch'
|
import Switch from '@/app/components/base/switch'
|
||||||
import { useToastContext } from '@/app/components/base/toast/context'
|
|
||||||
import { Plan } from '@/app/components/billing/type'
|
|
||||||
import { useAppContext } from '@/context/app-context'
|
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
|
||||||
import { useProviderContext } from '@/context/provider-context'
|
|
||||||
import {
|
|
||||||
updateCurrentWorkspace,
|
|
||||||
} from '@/service/common'
|
|
||||||
import { cn } from '@/utils/classnames'
|
import { cn } from '@/utils/classnames'
|
||||||
|
import ChatPreviewCard from './components/chat-preview-card'
|
||||||
|
import WorkflowPreviewCard from './components/workflow-preview-card'
|
||||||
|
import useWebAppBrand from './hooks/use-web-app-brand'
|
||||||
|
|
||||||
const ALLOW_FILE_EXTENSIONS = ['svg', 'png']
|
const ALLOW_FILE_EXTENSIONS = ['svg', 'png']
|
||||||
|
|
||||||
const CustomWebAppBrand = () => {
|
const CustomWebAppBrand = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { notify } = useToastContext()
|
|
||||||
const { plan, enableBilling } = useProviderContext()
|
|
||||||
const {
|
const {
|
||||||
currentWorkspace,
|
fileId,
|
||||||
mutateCurrentWorkspace,
|
imgKey,
|
||||||
|
uploadProgress,
|
||||||
|
uploading,
|
||||||
|
webappLogo,
|
||||||
|
webappBrandRemoved,
|
||||||
|
uploadDisabled,
|
||||||
|
workspaceLogo,
|
||||||
isCurrentWorkspaceManager,
|
isCurrentWorkspaceManager,
|
||||||
} = useAppContext()
|
isSandbox,
|
||||||
const [fileId, setFileId] = useState('')
|
handleApply,
|
||||||
const [imgKey, setImgKey] = useState(() => Date.now())
|
handleCancel,
|
||||||
const [uploadProgress, setUploadProgress] = useState(0)
|
handleChange,
|
||||||
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
|
handleRestore,
|
||||||
const isSandbox = enableBilling && plan.type === Plan.sandbox
|
handleSwitch,
|
||||||
const uploading = uploadProgress > 0 && uploadProgress < 100
|
} = useWebAppBrand()
|
||||||
const webappLogo = currentWorkspace.custom_config?.replace_webapp_logo || ''
|
|
||||||
const webappBrandRemoved = currentWorkspace.custom_config?.remove_webapp_brand
|
|
||||||
const uploadDisabled = isSandbox || webappBrandRemoved || !isCurrentWorkspaceManager
|
|
||||||
|
|
||||||
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const file = e.target.files?.[0]
|
|
||||||
|
|
||||||
if (!file)
|
|
||||||
return
|
|
||||||
|
|
||||||
if (file.size > 5 * 1024 * 1024) {
|
|
||||||
notify({ type: 'error', message: t('imageUploader.uploadFromComputerLimit', { ns: 'common', size: 5 }) })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
imageUpload({
|
|
||||||
file,
|
|
||||||
onProgressCallback: (progress) => {
|
|
||||||
setUploadProgress(progress)
|
|
||||||
},
|
|
||||||
onSuccessCallback: (res) => {
|
|
||||||
setUploadProgress(100)
|
|
||||||
setFileId(res.id)
|
|
||||||
},
|
|
||||||
onErrorCallback: (error?: any) => {
|
|
||||||
const errorMessage = getImageUploadErrorMessage(error, t('imageUploader.uploadFromComputerUploadError', { ns: 'common' }), t as any)
|
|
||||||
notify({ type: 'error', message: errorMessage })
|
|
||||||
setUploadProgress(-1)
|
|
||||||
},
|
|
||||||
}, false, '/workspaces/custom-config/webapp-logo/upload')
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleApply = async () => {
|
|
||||||
await updateCurrentWorkspace({
|
|
||||||
url: '/workspaces/custom-config',
|
|
||||||
body: {
|
|
||||||
remove_webapp_brand: webappBrandRemoved,
|
|
||||||
replace_webapp_logo: fileId,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
mutateCurrentWorkspace()
|
|
||||||
setFileId('')
|
|
||||||
setImgKey(Date.now())
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleRestore = async () => {
|
|
||||||
await updateCurrentWorkspace({
|
|
||||||
url: '/workspaces/custom-config',
|
|
||||||
body: {
|
|
||||||
remove_webapp_brand: false,
|
|
||||||
replace_webapp_logo: '',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
mutateCurrentWorkspace()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSwitch = async (checked: boolean) => {
|
|
||||||
await updateCurrentWorkspace({
|
|
||||||
url: '/workspaces/custom-config',
|
|
||||||
body: {
|
|
||||||
remove_webapp_brand: checked,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
mutateCurrentWorkspace()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
setFileId('')
|
|
||||||
setUploadProgress(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="py-4">
|
<div className="py-4">
|
||||||
@ -149,7 +64,7 @@ const CustomWebAppBrand = () => {
|
|||||||
className="relative mr-2"
|
className="relative mr-2"
|
||||||
disabled={uploadDisabled}
|
disabled={uploadDisabled}
|
||||||
>
|
>
|
||||||
<RiImageAddLine className="mr-1 h-4 w-4" />
|
<span className="i-ri-image-add-line mr-1 h-4 w-4" />
|
||||||
{
|
{
|
||||||
(webappLogo || fileId)
|
(webappLogo || fileId)
|
||||||
? t('change', { ns: 'custom' })
|
? t('change', { ns: 'custom' })
|
||||||
@ -172,7 +87,7 @@ const CustomWebAppBrand = () => {
|
|||||||
className="relative mr-2"
|
className="relative mr-2"
|
||||||
disabled={true}
|
disabled={true}
|
||||||
>
|
>
|
||||||
<RiLoader2Line className="mr-1 h-4 w-4 animate-spin" />
|
<span className="i-ri-loader-2-line mr-1 h-4 w-4 animate-spin" />
|
||||||
{t('uploading', { ns: 'custom' })}
|
{t('uploading', { ns: 'custom' })}
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
@ -208,118 +123,18 @@ const CustomWebAppBrand = () => {
|
|||||||
<Divider bgStyle="gradient" className="grow" />
|
<Divider bgStyle="gradient" className="grow" />
|
||||||
</div>
|
</div>
|
||||||
<div className="relative mb-2 flex items-center gap-3">
|
<div className="relative mb-2 flex items-center gap-3">
|
||||||
{/* chat card */}
|
<ChatPreviewCard
|
||||||
<div className="flex h-[320px] grow basis-1/2 overflow-hidden rounded-2xl border-[0.5px] border-components-panel-border-subtle bg-background-default-burn">
|
webappBrandRemoved={webappBrandRemoved}
|
||||||
<div className="flex h-full w-[232px] shrink-0 flex-col p-1 pr-0">
|
workspaceLogo={workspaceLogo}
|
||||||
<div className="flex items-center gap-3 p-3 pr-2">
|
webappLogo={webappLogo}
|
||||||
<div className={cn('inline-flex h-8 w-8 items-center justify-center rounded-lg border border-divider-regular', 'bg-components-icon-bg-blue-light-solid')}>
|
imgKey={imgKey}
|
||||||
<BubbleTextMod className="h-4 w-4 text-components-avatar-shape-fill-stop-100" />
|
/>
|
||||||
</div>
|
<WorkflowPreviewCard
|
||||||
<div className="grow text-text-secondary system-md-semibold">Chatflow App</div>
|
webappBrandRemoved={webappBrandRemoved}
|
||||||
<div className="p-1.5">
|
workspaceLogo={workspaceLogo}
|
||||||
<RiLayoutLeft2Line className="h-4 w-4 text-text-tertiary" />
|
webappLogo={webappLogo}
|
||||||
</div>
|
imgKey={imgKey}
|
||||||
</div>
|
/>
|
||||||
<div className="shrink-0 px-4 py-3">
|
|
||||||
<Button variant="secondary-accent" className="w-full justify-center">
|
|
||||||
<RiEditBoxLine className="mr-1 h-4 w-4" />
|
|
||||||
<div className="p-1 opacity-20">
|
|
||||||
<div className="h-2 w-[94px] rounded-sm bg-text-accent-light-mode-only"></div>
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="grow px-3 pt-5">
|
|
||||||
<div className="flex h-8 items-center px-3 py-1">
|
|
||||||
<div className="h-2 w-14 rounded-sm bg-text-quaternary opacity-20"></div>
|
|
||||||
</div>
|
|
||||||
<div className="flex h-8 items-center px-3 py-1">
|
|
||||||
<div className="h-2 w-[168px] rounded-sm bg-text-quaternary opacity-20"></div>
|
|
||||||
</div>
|
|
||||||
<div className="flex h-8 items-center px-3 py-1">
|
|
||||||
<div className="h-2 w-[128px] rounded-sm bg-text-quaternary opacity-20"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex shrink-0 items-center justify-between p-3">
|
|
||||||
<div className="p-1.5">
|
|
||||||
<RiEqualizer2Line className="h-4 w-4 text-text-tertiary" />
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-1.5">
|
|
||||||
{!webappBrandRemoved && (
|
|
||||||
<>
|
|
||||||
<div className="text-text-tertiary system-2xs-medium-uppercase">POWERED BY</div>
|
|
||||||
{
|
|
||||||
systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo
|
|
||||||
? <img src={systemFeatures.branding.workspace_logo} alt="logo" className="block h-5 w-auto" />
|
|
||||||
: webappLogo
|
|
||||||
? <img src={`${webappLogo}?hash=${imgKey}`} alt="logo" className="block h-5 w-auto" />
|
|
||||||
: <DifyLogo size="small" />
|
|
||||||
}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex w-[138px] grow flex-col justify-between p-2 pr-0">
|
|
||||||
<div className="flex grow flex-col justify-between rounded-l-2xl border-[0.5px] border-r-0 border-components-panel-border-subtle bg-chatbot-bg pb-4 pl-[22px] pt-16">
|
|
||||||
<div className="w-[720px] rounded-2xl border border-divider-subtle bg-chat-bubble-bg px-4 py-3">
|
|
||||||
<div className="mb-1 text-text-primary body-md-regular">Hello! How can I assist you today?</div>
|
|
||||||
<Button size="small">
|
|
||||||
<div className="h-2 w-[144px] rounded-sm bg-text-quaternary opacity-20"></div>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="flex h-[52px] w-[578px] items-center rounded-xl border border-components-chat-input-border bg-components-panel-bg-blur pl-3.5 text-text-placeholder shadow-md backdrop-blur-sm body-lg-regular">Talk to Dify</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* workflow card */}
|
|
||||||
<div className="flex h-[320px] grow basis-1/2 flex-col overflow-hidden rounded-2xl border-[0.5px] border-components-panel-border-subtle bg-background-default-burn">
|
|
||||||
<div className="w-full border-b-[0.5px] border-divider-subtle p-4 pb-0">
|
|
||||||
<div className="mb-2 flex items-center gap-3">
|
|
||||||
<div className={cn('inline-flex h-8 w-8 items-center justify-center rounded-lg border border-divider-regular', 'bg-components-icon-bg-indigo-solid')}>
|
|
||||||
<RiExchange2Fill className="h-4 w-4 text-components-avatar-shape-fill-stop-100" />
|
|
||||||
</div>
|
|
||||||
<div className="grow text-text-secondary system-md-semibold">Workflow App</div>
|
|
||||||
<div className="p-1.5">
|
|
||||||
<RiLayoutLeft2Line className="h-4 w-4 text-text-tertiary" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="flex h-10 shrink-0 items-center border-b-2 border-components-tab-active text-text-primary system-md-semibold-uppercase">RUN ONCE</div>
|
|
||||||
<div className="flex h-10 grow items-center border-b-2 border-transparent text-text-tertiary system-md-semibold-uppercase">RUN BATCH</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="grow bg-components-panel-bg">
|
|
||||||
<div className="p-4 pb-1">
|
|
||||||
<div className="mb-1 py-2">
|
|
||||||
<div className="h-2 w-20 rounded-sm bg-text-quaternary opacity-20"></div>
|
|
||||||
</div>
|
|
||||||
<div className="h-16 w-full rounded-lg bg-components-input-bg-normal"></div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between px-4 py-3">
|
|
||||||
<Button size="small">
|
|
||||||
<div className="h-2 w-10 rounded-sm bg-text-quaternary opacity-20"></div>
|
|
||||||
</Button>
|
|
||||||
<Button variant="primary" size="small" disabled>
|
|
||||||
<RiPlayLargeLine className="mr-1 h-4 w-4" />
|
|
||||||
<span>Execute</span>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex h-12 shrink-0 items-center gap-1.5 bg-components-panel-bg p-4 pt-3">
|
|
||||||
{!webappBrandRemoved && (
|
|
||||||
<>
|
|
||||||
<div className="text-text-tertiary system-2xs-medium-uppercase">POWERED BY</div>
|
|
||||||
{
|
|
||||||
systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo
|
|
||||||
? <img src={systemFeatures.branding.workspace_logo} alt="logo" className="block h-5 w-auto" />
|
|
||||||
: webappLogo
|
|
||||||
? <img src={`${webappLogo}?hash=${imgKey}`} alt="logo" className="block h-5 w-auto" />
|
|
||||||
: <DifyLogo size="small" />
|
|
||||||
}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { act, render, screen } from '@testing-library/react'
|
|||||||
import { usePathname } from 'next/navigation'
|
import { usePathname } from 'next/navigation'
|
||||||
import { vi } from 'vitest'
|
import { vi } from 'vitest'
|
||||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||||
import HeaderWrapper from './header-wrapper'
|
import HeaderWrapper from '../header-wrapper'
|
||||||
|
|
||||||
vi.mock('next/navigation', () => ({
|
vi.mock('next/navigation', () => ({
|
||||||
usePathname: vi.fn(),
|
usePathname: vi.fn(),
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
import { vi } from 'vitest'
|
import { vi } from 'vitest'
|
||||||
import Header from './index'
|
import Header from '../index'
|
||||||
|
|
||||||
function createMockComponent(testId: string) {
|
function createMockComponent(testId: string) {
|
||||||
return () => <div data-testid={testId} />
|
return () => <div data-testid={testId} />
|
||||||
@ -2,7 +2,7 @@ import { fireEvent, render, screen } from '@testing-library/react'
|
|||||||
import { vi } from 'vitest'
|
import { vi } from 'vitest'
|
||||||
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||||
import { NOTICE_I18N } from '@/i18n-config/language'
|
import { NOTICE_I18N } from '@/i18n-config/language'
|
||||||
import MaintenanceNotice from './maintenance-notice'
|
import MaintenanceNotice from '../maintenance-notice'
|
||||||
|
|
||||||
vi.mock('@/app/components/base/icons/src/vender/line/general', () => ({
|
vi.mock('@/app/components/base/icons/src/vender/line/general', () => ({
|
||||||
X: ({ onClick }: { onClick?: () => void }) => <button type="button" aria-label="close notice" onClick={onClick} />,
|
X: ({ onClick }: { onClick?: () => void }) => <button type="button" aria-label="close notice" onClick={onClick} />,
|
||||||
@ -2,7 +2,7 @@ import type { LangGeniusVersionResponse } from '@/models/common'
|
|||||||
import type { SystemFeatures } from '@/types/feature'
|
import type { SystemFeatures } from '@/types/feature'
|
||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||||
import AccountAbout from './index'
|
import AccountAbout from '../index'
|
||||||
|
|
||||||
vi.mock('@/context/global-public-context', () => ({
|
vi.mock('@/context/global-public-context', () => ({
|
||||||
useGlobalPublicStore: vi.fn(),
|
useGlobalPublicStore: vi.fn(),
|
||||||
@ -8,8 +8,8 @@ import { useModalContext } from '@/context/modal-context'
|
|||||||
import { baseProviderContextValue, useProviderContext } from '@/context/provider-context'
|
import { baseProviderContextValue, useProviderContext } from '@/context/provider-context'
|
||||||
import { getDocDownloadUrl } from '@/service/common'
|
import { getDocDownloadUrl } from '@/service/common'
|
||||||
import { downloadUrl } from '@/utils/download'
|
import { downloadUrl } from '@/utils/download'
|
||||||
import Toast from '../../base/toast'
|
import Toast from '../../../base/toast'
|
||||||
import Compliance from './compliance'
|
import Compliance from '../compliance'
|
||||||
|
|
||||||
vi.mock('@/context/provider-context', async (importOriginal) => {
|
vi.mock('@/context/provider-context', async (importOriginal) => {
|
||||||
const actual = await importOriginal<typeof import('@/context/provider-context')>()
|
const actual = await importOriginal<typeof import('@/context/provider-context')>()
|
||||||
@ -10,13 +10,13 @@ import { useGlobalPublicStore } from '@/context/global-public-context'
|
|||||||
import { useModalContext } from '@/context/modal-context'
|
import { useModalContext } from '@/context/modal-context'
|
||||||
import { useProviderContext } from '@/context/provider-context'
|
import { useProviderContext } from '@/context/provider-context'
|
||||||
import { useLogout } from '@/service/use-common'
|
import { useLogout } from '@/service/use-common'
|
||||||
import AppSelector from './index'
|
import AppSelector from '../index'
|
||||||
|
|
||||||
vi.mock('../account-setting', () => ({
|
vi.mock('../../account-setting', () => ({
|
||||||
default: () => <div data-testid="account-setting">AccountSetting</div>,
|
default: () => <div data-testid="account-setting">AccountSetting</div>,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('../account-about', () => ({
|
vi.mock('../../account-about', () => ({
|
||||||
default: ({ onCancel }: { onCancel: () => void }) => (
|
default: ({ onCancel }: { onCancel: () => void }) => (
|
||||||
<div data-testid="account-about">
|
<div data-testid="account-about">
|
||||||
Version
|
Version
|
||||||
@ -5,7 +5,7 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from '@/app/co
|
|||||||
import { Plan } from '@/app/components/billing/type'
|
import { Plan } from '@/app/components/billing/type'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
import { baseProviderContextValue, useProviderContext } from '@/context/provider-context'
|
import { baseProviderContextValue, useProviderContext } from '@/context/provider-context'
|
||||||
import Support from './support'
|
import Support from '../support'
|
||||||
|
|
||||||
const { mockZendeskKey } = vi.hoisted(() => ({
|
const { mockZendeskKey } = vi.hoisted(() => ({
|
||||||
mockZendeskKey: { value: 'test-key' },
|
mockZendeskKey: { value: 'test-key' },
|
||||||
@ -5,7 +5,7 @@ import { ToastContext } from '@/app/components/base/toast/context'
|
|||||||
import { baseProviderContextValue, useProviderContext } from '@/context/provider-context'
|
import { baseProviderContextValue, useProviderContext } from '@/context/provider-context'
|
||||||
import { useWorkspacesContext } from '@/context/workspace-context'
|
import { useWorkspacesContext } from '@/context/workspace-context'
|
||||||
import { switchWorkspace } from '@/service/common'
|
import { switchWorkspace } from '@/service/common'
|
||||||
import WorkplaceSelector from './index'
|
import WorkplaceSelector from '../index'
|
||||||
|
|
||||||
vi.mock('@/context/workspace-context', () => ({
|
vi.mock('@/context/workspace-context', () => ({
|
||||||
useWorkspacesContext: vi.fn(),
|
useWorkspacesContext: vi.fn(),
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import type { AccountIntegrate } from '@/models/common'
|
import type { AccountIntegrate } from '@/models/common'
|
||||||
import { render, screen } from '@testing-library/react'
|
import { render, screen } from '@testing-library/react'
|
||||||
import { useAccountIntegrates } from '@/service/use-common'
|
import { useAccountIntegrates } from '@/service/use-common'
|
||||||
import IntegrationsPage from './index'
|
import IntegrationsPage from '../index'
|
||||||
|
|
||||||
vi.mock('@/service/use-common', () => ({
|
vi.mock('@/service/use-common', () => ({
|
||||||
useAccountIntegrates: vi.fn(),
|
useAccountIntegrates: vi.fn(),
|
||||||
@ -3,7 +3,7 @@ import {
|
|||||||
ACCOUNT_SETTING_TAB,
|
ACCOUNT_SETTING_TAB,
|
||||||
DEFAULT_ACCOUNT_SETTING_TAB,
|
DEFAULT_ACCOUNT_SETTING_TAB,
|
||||||
isValidAccountSettingTab,
|
isValidAccountSettingTab,
|
||||||
} from './constants'
|
} from '../constants'
|
||||||
|
|
||||||
describe('AccountSetting Constants', () => {
|
describe('AccountSetting Constants', () => {
|
||||||
it('should have correct ACCOUNT_SETTING_MODAL_ACTION', () => {
|
it('should have correct ACCOUNT_SETTING_MODAL_ACTION', () => {
|
||||||
@ -0,0 +1,346 @@
|
|||||||
|
import type { ComponentProps, ReactNode } from 'react'
|
||||||
|
import type { AppContextValue } from '@/context/app-context'
|
||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||||
|
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
|
||||||
|
import userEvent from '@testing-library/user-event'
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
import { useAppContext } from '@/context/app-context'
|
||||||
|
import { baseProviderContextValue, useProviderContext } from '@/context/provider-context'
|
||||||
|
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||||
|
import { ACCOUNT_SETTING_TAB } from '../constants'
|
||||||
|
import AccountSetting from '../index'
|
||||||
|
|
||||||
|
vi.mock('@/context/provider-context', async (importOriginal) => {
|
||||||
|
const actual = await importOriginal<typeof import('@/context/provider-context')>()
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
useProviderContext: vi.fn(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
vi.mock('@/context/app-context', async (importOriginal) => {
|
||||||
|
const actual = await importOriginal<typeof import('@/context/app-context')>()
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
useAppContext: vi.fn(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
vi.mock('next/navigation', () => ({
|
||||||
|
useRouter: vi.fn(() => ({
|
||||||
|
push: vi.fn(),
|
||||||
|
replace: vi.fn(),
|
||||||
|
prefetch: vi.fn(),
|
||||||
|
})),
|
||||||
|
usePathname: vi.fn(() => '/'),
|
||||||
|
useParams: vi.fn(() => ({})),
|
||||||
|
useSearchParams: vi.fn(() => ({ get: vi.fn() })),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/hooks/use-breakpoints', () => ({
|
||||||
|
MediaType: {
|
||||||
|
mobile: 'mobile',
|
||||||
|
tablet: 'tablet',
|
||||||
|
pc: 'pc',
|
||||||
|
},
|
||||||
|
default: vi.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/app/components/billing/billing-page', () => ({
|
||||||
|
default: () => <div data-testid="billing-page">Billing Page</div>,
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/app/components/custom/custom-page', () => ({
|
||||||
|
default: () => <div data-testid="custom-page">Custom Page</div>,
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/app/components/header/account-setting/api-based-extension-page', () => ({
|
||||||
|
default: () => <div data-testid="api-based-extension-page">API Based Extension Page</div>,
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/app/components/header/account-setting/data-source-page-new', () => ({
|
||||||
|
default: () => <div data-testid="data-source-page">Data Source Page</div>,
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/app/components/header/account-setting/language-page', () => ({
|
||||||
|
default: () => <div data-testid="language-page">Language Page</div>,
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/app/components/header/account-setting/members-page', () => ({
|
||||||
|
default: () => <div data-testid="members-page">Members Page</div>,
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/app/components/header/account-setting/model-provider-page', () => ({
|
||||||
|
default: ({ searchText }: { searchText: string }) => (
|
||||||
|
<div data-testid="provider-page">
|
||||||
|
{`provider-search:${searchText}`}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/app/components/header/account-setting/menu-dialog', () => ({
|
||||||
|
default: function MockMenuDialog({
|
||||||
|
children,
|
||||||
|
onClose,
|
||||||
|
show,
|
||||||
|
}: {
|
||||||
|
children: ReactNode
|
||||||
|
onClose: () => void
|
||||||
|
show?: boolean
|
||||||
|
}) {
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === 'Escape')
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('keydown', handleKeyDown)
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keydown', handleKeyDown)
|
||||||
|
}
|
||||||
|
}, [onClose])
|
||||||
|
|
||||||
|
if (!show)
|
||||||
|
return null
|
||||||
|
|
||||||
|
return <div role="dialog">{children}</div>
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
const baseAppContextValue: AppContextValue = {
|
||||||
|
userProfile: {
|
||||||
|
id: '1',
|
||||||
|
name: 'Test User',
|
||||||
|
email: 'test@example.com',
|
||||||
|
avatar: '',
|
||||||
|
avatar_url: '',
|
||||||
|
is_password_set: false,
|
||||||
|
},
|
||||||
|
mutateUserProfile: vi.fn(),
|
||||||
|
currentWorkspace: {
|
||||||
|
id: '1',
|
||||||
|
name: 'Workspace',
|
||||||
|
plan: '',
|
||||||
|
status: '',
|
||||||
|
created_at: 0,
|
||||||
|
role: 'owner',
|
||||||
|
providers: [],
|
||||||
|
trial_credits: 0,
|
||||||
|
trial_credits_used: 0,
|
||||||
|
next_credit_reset_date: 0,
|
||||||
|
},
|
||||||
|
isCurrentWorkspaceManager: true,
|
||||||
|
isCurrentWorkspaceOwner: true,
|
||||||
|
isCurrentWorkspaceEditor: true,
|
||||||
|
isCurrentWorkspaceDatasetOperator: false,
|
||||||
|
mutateCurrentWorkspace: vi.fn(),
|
||||||
|
langGeniusVersionInfo: {
|
||||||
|
current_env: 'testing',
|
||||||
|
current_version: '0.1.0',
|
||||||
|
latest_version: '0.1.0',
|
||||||
|
release_date: '',
|
||||||
|
release_notes: '',
|
||||||
|
version: '0.1.0',
|
||||||
|
can_auto_update: false,
|
||||||
|
},
|
||||||
|
useSelector: vi.fn(),
|
||||||
|
isLoadingCurrentWorkspace: false,
|
||||||
|
isValidatingCurrentWorkspace: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('AccountSetting', () => {
|
||||||
|
const mockOnCancel = vi.fn()
|
||||||
|
const mockOnTabChange = vi.fn()
|
||||||
|
|
||||||
|
const renderAccountSetting = (props: Partial<ComponentProps<typeof AccountSetting>> = {}) => {
|
||||||
|
const queryClient = new QueryClient()
|
||||||
|
const mergedProps: ComponentProps<typeof AccountSetting> = {
|
||||||
|
onCancel: mockOnCancel,
|
||||||
|
...props,
|
||||||
|
}
|
||||||
|
|
||||||
|
const view = render(
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<AccountSetting {...mergedProps} />
|
||||||
|
</QueryClientProvider>,
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
...view,
|
||||||
|
rerenderAccountSetting(nextProps: Partial<ComponentProps<typeof AccountSetting>>) {
|
||||||
|
view.rerender(
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<AccountSetting {...mergedProps} {...nextProps} />
|
||||||
|
</QueryClientProvider>,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
vi.mocked(useProviderContext).mockReturnValue({
|
||||||
|
...baseProviderContextValue,
|
||||||
|
enableBilling: true,
|
||||||
|
enableReplaceWebAppLogo: true,
|
||||||
|
})
|
||||||
|
vi.mocked(useAppContext).mockReturnValue(baseAppContextValue)
|
||||||
|
vi.mocked(useBreakpoints).mockReturnValue(MediaType.pc)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Rendering', () => {
|
||||||
|
it('should render the sidebar with correct menu items', () => {
|
||||||
|
renderAccountSetting()
|
||||||
|
|
||||||
|
expect(screen.getByText('common.userProfile.settings')).toBeInTheDocument()
|
||||||
|
expect(screen.getByTitle('common.settings.provider')).toBeInTheDocument()
|
||||||
|
expect(screen.getByTitle('common.settings.members')).toBeInTheDocument()
|
||||||
|
expect(screen.getByTitle('common.settings.billing')).toBeInTheDocument()
|
||||||
|
expect(screen.getByTitle('common.settings.dataSource')).toBeInTheDocument()
|
||||||
|
expect(screen.getByTitle('common.settings.apiBasedExtension')).toBeInTheDocument()
|
||||||
|
expect(screen.getByTitle('custom.custom')).toBeInTheDocument()
|
||||||
|
expect(screen.getByTitle('common.settings.language')).toBeInTheDocument()
|
||||||
|
expect(screen.getByTestId('members-page')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respect the activeTab prop', () => {
|
||||||
|
renderAccountSetting({ activeTab: ACCOUNT_SETTING_TAB.DATA_SOURCE })
|
||||||
|
|
||||||
|
expect(screen.getByTestId('data-source-page')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should sync the rendered page when activeTab changes', async () => {
|
||||||
|
const { rerenderAccountSetting } = renderAccountSetting({
|
||||||
|
activeTab: ACCOUNT_SETTING_TAB.DATA_SOURCE,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(screen.getByTestId('data-source-page')).toBeInTheDocument()
|
||||||
|
|
||||||
|
rerenderAccountSetting({
|
||||||
|
activeTab: ACCOUNT_SETTING_TAB.CUSTOM,
|
||||||
|
})
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId('custom-page')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should hide sidebar labels on mobile', () => {
|
||||||
|
vi.mocked(useBreakpoints).mockReturnValue(MediaType.mobile)
|
||||||
|
|
||||||
|
renderAccountSetting()
|
||||||
|
|
||||||
|
expect(screen.queryByText('common.settings.provider')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should filter items for dataset operator', () => {
|
||||||
|
vi.mocked(useAppContext).mockReturnValue({
|
||||||
|
...baseAppContextValue,
|
||||||
|
isCurrentWorkspaceDatasetOperator: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
renderAccountSetting()
|
||||||
|
|
||||||
|
expect(screen.queryByTitle('common.settings.provider')).not.toBeInTheDocument()
|
||||||
|
expect(screen.queryByTitle('common.settings.members')).not.toBeInTheDocument()
|
||||||
|
expect(screen.getByTitle('common.settings.language')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should hide billing and custom tabs when disabled', () => {
|
||||||
|
vi.mocked(useProviderContext).mockReturnValue({
|
||||||
|
...baseProviderContextValue,
|
||||||
|
enableBilling: false,
|
||||||
|
enableReplaceWebAppLogo: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
renderAccountSetting()
|
||||||
|
|
||||||
|
expect(screen.queryByTitle('common.settings.billing')).not.toBeInTheDocument()
|
||||||
|
expect(screen.queryByTitle('custom.custom')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Tab Navigation', () => {
|
||||||
|
it('should change active tab when clicking on a menu item', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
|
||||||
|
renderAccountSetting({ onTabChange: mockOnTabChange })
|
||||||
|
|
||||||
|
await user.click(screen.getByTitle('common.settings.provider'))
|
||||||
|
|
||||||
|
expect(mockOnTabChange).toHaveBeenCalledWith(ACCOUNT_SETTING_TAB.PROVIDER)
|
||||||
|
expect(screen.getByTestId('provider-page')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
['common.settings.billing', 'billing-page'],
|
||||||
|
['common.settings.dataSource', 'data-source-page'],
|
||||||
|
['common.settings.apiBasedExtension', 'api-based-extension-page'],
|
||||||
|
['custom.custom', 'custom-page'],
|
||||||
|
['common.settings.language', 'language-page'],
|
||||||
|
['common.settings.members', 'members-page'],
|
||||||
|
])('should render the "%s" page when its sidebar item is selected', async (menuTitle, pageTestId) => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
|
||||||
|
renderAccountSetting()
|
||||||
|
|
||||||
|
await user.click(screen.getByTitle(menuTitle))
|
||||||
|
|
||||||
|
expect(screen.getByTestId(pageTestId)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Interactions', () => {
|
||||||
|
it('should call onCancel when clicking the close button', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
|
||||||
|
renderAccountSetting()
|
||||||
|
|
||||||
|
const closeControls = screen.getByText('ESC').parentElement
|
||||||
|
|
||||||
|
expect(closeControls).not.toBeNull()
|
||||||
|
if (!closeControls)
|
||||||
|
throw new Error('Close controls are missing')
|
||||||
|
|
||||||
|
await user.click(within(closeControls).getByRole('button'))
|
||||||
|
|
||||||
|
expect(mockOnCancel).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should call onCancel when pressing Escape key', () => {
|
||||||
|
renderAccountSetting()
|
||||||
|
|
||||||
|
fireEvent.keyDown(document, { key: 'Escape' })
|
||||||
|
|
||||||
|
expect(mockOnCancel).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should update search value in the provider tab', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
|
||||||
|
renderAccountSetting()
|
||||||
|
|
||||||
|
await user.click(screen.getByTitle('common.settings.provider'))
|
||||||
|
|
||||||
|
const input = screen.getByRole('textbox')
|
||||||
|
await user.type(input, 'test-search')
|
||||||
|
|
||||||
|
expect(input).toHaveValue('test-search')
|
||||||
|
expect(screen.getByText('provider-search:test-search')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle scroll event in panel', () => {
|
||||||
|
renderAccountSetting()
|
||||||
|
|
||||||
|
const scrollContainer = screen.getByRole('dialog').querySelector('.overflow-y-auto')
|
||||||
|
|
||||||
|
expect(scrollContainer).toBeInTheDocument()
|
||||||
|
if (scrollContainer) {
|
||||||
|
fireEvent.scroll(scrollContainer, { target: { scrollTop: 100 } })
|
||||||
|
expect(scrollContainer).toHaveClass('overflow-y-auto')
|
||||||
|
|
||||||
|
fireEvent.scroll(scrollContainer, { target: { scrollTop: 0 } })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
import MenuDialog from './menu-dialog'
|
import MenuDialog from '../menu-dialog'
|
||||||
|
|
||||||
describe('MenuDialog', () => {
|
describe('MenuDialog', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { render, screen } from '@testing-library/react'
|
import { render, screen } from '@testing-library/react'
|
||||||
import Empty from './empty'
|
import Empty from '../empty'
|
||||||
|
|
||||||
describe('Empty State', () => {
|
describe('Empty State', () => {
|
||||||
describe('Rendering', () => {
|
describe('Rendering', () => {
|
||||||
@ -4,7 +4,7 @@ import type { ApiBasedExtension } from '@/models/common'
|
|||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
import { useModalContext } from '@/context/modal-context'
|
import { useModalContext } from '@/context/modal-context'
|
||||||
import { useApiBasedExtensions } from '@/service/use-common'
|
import { useApiBasedExtensions } from '@/service/use-common'
|
||||||
import ApiBasedExtensionPage from './index'
|
import ApiBasedExtensionPage from '../index'
|
||||||
|
|
||||||
vi.mock('@/service/use-common', () => ({
|
vi.mock('@/service/use-common', () => ({
|
||||||
useApiBasedExtensions: vi.fn(),
|
useApiBasedExtensions: vi.fn(),
|
||||||
@ -5,7 +5,7 @@ import { fireEvent, render, screen, waitFor, within } from '@testing-library/rea
|
|||||||
import * as reactI18next from 'react-i18next'
|
import * as reactI18next from 'react-i18next'
|
||||||
import { useModalContext } from '@/context/modal-context'
|
import { useModalContext } from '@/context/modal-context'
|
||||||
import { deleteApiBasedExtension } from '@/service/common'
|
import { deleteApiBasedExtension } from '@/service/common'
|
||||||
import Item from './item'
|
import Item from '../item'
|
||||||
|
|
||||||
// Mock dependencies
|
// Mock dependencies
|
||||||
vi.mock('@/context/modal-context', () => ({
|
vi.mock('@/context/modal-context', () => ({
|
||||||
@ -5,7 +5,7 @@ import * as reactI18next from 'react-i18next'
|
|||||||
import { ToastContext } from '@/app/components/base/toast/context'
|
import { ToastContext } from '@/app/components/base/toast/context'
|
||||||
import { useDocLink } from '@/context/i18n'
|
import { useDocLink } from '@/context/i18n'
|
||||||
import { addApiBasedExtension, updateApiBasedExtension } from '@/service/common'
|
import { addApiBasedExtension, updateApiBasedExtension } from '@/service/common'
|
||||||
import ApiBasedExtensionModal from './modal'
|
import ApiBasedExtensionModal from '../modal'
|
||||||
|
|
||||||
vi.mock('@/context/i18n', () => ({
|
vi.mock('@/context/i18n', () => ({
|
||||||
useDocLink: vi.fn(),
|
useDocLink: vi.fn(),
|
||||||
@ -5,7 +5,7 @@ import { fireEvent, render, screen } from '@testing-library/react'
|
|||||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||||
import { useModalContext } from '@/context/modal-context'
|
import { useModalContext } from '@/context/modal-context'
|
||||||
import { useApiBasedExtensions } from '@/service/use-common'
|
import { useApiBasedExtensions } from '@/service/use-common'
|
||||||
import ApiBasedExtensionSelector from './selector'
|
import ApiBasedExtensionSelector from '../selector'
|
||||||
|
|
||||||
vi.mock('@/context/modal-context', () => ({
|
vi.mock('@/context/modal-context', () => ({
|
||||||
useModalContext: vi.fn(),
|
useModalContext: vi.fn(),
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import type { IItem } from './index'
|
import type { IItem } from '../index'
|
||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
import Collapse from './index'
|
import Collapse from '../index'
|
||||||
|
|
||||||
describe('Collapse', () => {
|
describe('Collapse', () => {
|
||||||
const mockItems: IItem[] = [
|
const mockItems: IItem[] = [
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import type { DataSourceAuth } from './types'
|
import type { DataSourceAuth } from '../types'
|
||||||
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
|
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
|
||||||
import { FormTypeEnum } from '@/app/components/base/form/types'
|
import { FormTypeEnum } from '@/app/components/base/form/types'
|
||||||
import { usePluginAuthAction } from '@/app/components/plugins/plugin-auth'
|
import { usePluginAuthAction } from '@/app/components/plugins/plugin-auth'
|
||||||
@ -8,8 +8,8 @@ import { useRenderI18nObject } from '@/hooks/use-i18n'
|
|||||||
import { openOAuthPopup } from '@/hooks/use-oauth'
|
import { openOAuthPopup } from '@/hooks/use-oauth'
|
||||||
import { useGetDataSourceOAuthUrl, useInvalidDataSourceAuth, useInvalidDataSourceListAuth, useInvalidDefaultDataSourceListAuth } from '@/service/use-datasource'
|
import { useGetDataSourceOAuthUrl, useInvalidDataSourceAuth, useInvalidDataSourceListAuth, useInvalidDefaultDataSourceListAuth } from '@/service/use-datasource'
|
||||||
import { useInvalidDataSourceList } from '@/service/use-pipeline'
|
import { useInvalidDataSourceList } from '@/service/use-pipeline'
|
||||||
import Card from './card'
|
import Card from '../card'
|
||||||
import { useDataSourceAuthUpdate } from './hooks'
|
import { useDataSourceAuthUpdate } from '../hooks'
|
||||||
|
|
||||||
vi.mock('@/app/components/plugins/plugin-auth', () => ({
|
vi.mock('@/app/components/plugins/plugin-auth', () => ({
|
||||||
ApiKeyModal: vi.fn(({ onClose, onUpdate, onRemove, disabled, editValues }: { onClose: () => void, onUpdate: () => void, onRemove: () => void, disabled: boolean, editValues: Record<string, unknown> }) => (
|
ApiKeyModal: vi.fn(({ onClose, onUpdate, onRemove, disabled, editValues }: { onClose: () => void, onUpdate: () => void, onRemove: () => void, disabled: boolean, editValues: Record<string, unknown> }) => (
|
||||||
@ -43,7 +43,7 @@ vi.mock('@/service/use-datasource', () => ({
|
|||||||
useInvalidDefaultDataSourceListAuth: vi.fn(() => vi.fn()),
|
useInvalidDefaultDataSourceListAuth: vi.fn(() => vi.fn()),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('./hooks', () => ({
|
vi.mock('../hooks', () => ({
|
||||||
useDataSourceAuthUpdate: vi.fn(),
|
useDataSourceAuthUpdate: vi.fn(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@ -1,10 +1,10 @@
|
|||||||
import type { DataSourceAuth } from './types'
|
import type { DataSourceAuth } from '../types'
|
||||||
import type { FormSchema } from '@/app/components/base/form/types'
|
import type { FormSchema } from '@/app/components/base/form/types'
|
||||||
import type { AddApiKeyButtonProps, AddOAuthButtonProps, PluginPayload } from '@/app/components/plugins/plugin-auth/types'
|
import type { AddApiKeyButtonProps, AddOAuthButtonProps, PluginPayload } from '@/app/components/plugins/plugin-auth/types'
|
||||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||||
import { FormTypeEnum } from '@/app/components/base/form/types'
|
import { FormTypeEnum } from '@/app/components/base/form/types'
|
||||||
import { AuthCategory } from '@/app/components/plugins/plugin-auth/types'
|
import { AuthCategory } from '@/app/components/plugins/plugin-auth/types'
|
||||||
import Configure from './configure'
|
import Configure from '../configure'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configure Component Tests
|
* Configure Component Tests
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import type { UseQueryResult } from '@tanstack/react-query'
|
import type { UseQueryResult } from '@tanstack/react-query'
|
||||||
import type { DataSourceAuth } from './types'
|
import type { DataSourceAuth } from '../types'
|
||||||
import { render, screen } from '@testing-library/react'
|
import { render, screen } from '@testing-library/react'
|
||||||
import { useTheme } from 'next-themes'
|
import { useTheme } from 'next-themes'
|
||||||
import { usePluginAuthAction } from '@/app/components/plugins/plugin-auth'
|
import { usePluginAuthAction } from '@/app/components/plugins/plugin-auth'
|
||||||
@ -7,8 +7,8 @@ import { useGlobalPublicStore } from '@/context/global-public-context'
|
|||||||
import { useRenderI18nObject } from '@/hooks/use-i18n'
|
import { useRenderI18nObject } from '@/hooks/use-i18n'
|
||||||
import { useGetDataSourceListAuth, useGetDataSourceOAuthUrl } from '@/service/use-datasource'
|
import { useGetDataSourceListAuth, useGetDataSourceOAuthUrl } from '@/service/use-datasource'
|
||||||
import { defaultSystemFeatures } from '@/types/feature'
|
import { defaultSystemFeatures } from '@/types/feature'
|
||||||
import { useDataSourceAuthUpdate, useMarketplaceAllPlugins } from './hooks'
|
import { useDataSourceAuthUpdate, useMarketplaceAllPlugins } from '../hooks'
|
||||||
import DataSourcePage from './index'
|
import DataSourcePage from '../index'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DataSourcePage Component Tests
|
* DataSourcePage Component Tests
|
||||||
@ -33,7 +33,7 @@ vi.mock('@/service/use-datasource', () => ({
|
|||||||
useGetDataSourceOAuthUrl: vi.fn(),
|
useGetDataSourceOAuthUrl: vi.fn(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('./hooks', () => ({
|
vi.mock('../hooks', () => ({
|
||||||
useDataSourceAuthUpdate: vi.fn(),
|
useDataSourceAuthUpdate: vi.fn(),
|
||||||
useMarketplaceAllPlugins: vi.fn(),
|
useMarketplaceAllPlugins: vi.fn(),
|
||||||
}))
|
}))
|
||||||
@ -1,10 +1,10 @@
|
|||||||
import type { DataSourceAuth } from './types'
|
import type { DataSourceAuth } from '../types'
|
||||||
import type { Plugin } from '@/app/components/plugins/types'
|
import type { Plugin } from '@/app/components/plugins/types'
|
||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
import { useTheme } from 'next-themes'
|
import { useTheme } from 'next-themes'
|
||||||
import { PluginCategoryEnum } from '@/app/components/plugins/types'
|
import { PluginCategoryEnum } from '@/app/components/plugins/types'
|
||||||
import { useMarketplaceAllPlugins } from './hooks'
|
import { useMarketplaceAllPlugins } from '../hooks'
|
||||||
import InstallFromMarketplace from './install-from-marketplace'
|
import InstallFromMarketplace from '../install-from-marketplace'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* InstallFromMarketplace Component Tests
|
* InstallFromMarketplace Component Tests
|
||||||
@ -54,7 +54,7 @@ vi.mock('@/app/components/plugins/provider-card', () => ({
|
|||||||
),
|
),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('./hooks', () => ({
|
vi.mock('../hooks', () => ({
|
||||||
useMarketplaceAllPlugins: vi.fn(),
|
useMarketplaceAllPlugins: vi.fn(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import type { DataSourceCredential } from './types'
|
import type { DataSourceCredential } from '../types'
|
||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
import { CredentialTypeEnum } from '@/app/components/plugins/plugin-auth/types'
|
import { CredentialTypeEnum } from '@/app/components/plugins/plugin-auth/types'
|
||||||
import Item from './item'
|
import Item from '../item'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Item Component Tests
|
* Item Component Tests
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import type { DataSourceCredential } from './types'
|
import type { DataSourceCredential } from '../types'
|
||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
import { CredentialTypeEnum } from '@/app/components/plugins/plugin-auth/types'
|
import { CredentialTypeEnum } from '@/app/components/plugins/plugin-auth/types'
|
||||||
import Operator from './operator'
|
import Operator from '../operator'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Operator Component Tests
|
* Operator Component Tests
|
||||||
@ -5,7 +5,7 @@ import {
|
|||||||
useInvalidDefaultDataSourceListAuth,
|
useInvalidDefaultDataSourceListAuth,
|
||||||
} from '@/service/use-datasource'
|
} from '@/service/use-datasource'
|
||||||
import { useInvalidDataSourceList } from '@/service/use-pipeline'
|
import { useInvalidDataSourceList } from '@/service/use-pipeline'
|
||||||
import { useDataSourceAuthUpdate } from './use-data-source-auth-update'
|
import { useDataSourceAuthUpdate } from '../use-data-source-auth-update'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* useDataSourceAuthUpdate Hook Tests
|
* useDataSourceAuthUpdate Hook Tests
|
||||||
@ -5,7 +5,7 @@ import {
|
|||||||
useMarketplacePluginsByCollectionId,
|
useMarketplacePluginsByCollectionId,
|
||||||
} from '@/app/components/plugins/marketplace/hooks'
|
} from '@/app/components/plugins/marketplace/hooks'
|
||||||
import { PluginCategoryEnum } from '@/app/components/plugins/types'
|
import { PluginCategoryEnum } from '@/app/components/plugins/types'
|
||||||
import { useMarketplaceAllPlugins } from './use-marketplace-all-plugins'
|
import { useMarketplaceAllPlugins } from '../use-marketplace-all-plugins'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* useMarketplaceAllPlugins Hook Tests
|
* useMarketplaceAllPlugins Hook Tests
|
||||||
@ -4,7 +4,7 @@ import type { DataSourceNotion as TDataSourceNotion } from '@/models/common'
|
|||||||
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
|
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
import { useDataSourceIntegrates, useInvalidDataSourceIntegrates, useNotionConnection } from '@/service/use-common'
|
import { useDataSourceIntegrates, useInvalidDataSourceIntegrates, useNotionConnection } from '@/service/use-common'
|
||||||
import DataSourceNotion from './index'
|
import DataSourceNotion from '../index'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DataSourceNotion Component Tests
|
* DataSourceNotion Component Tests
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
|
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
|
||||||
import { syncDataSourceNotion, updateDataSourceNotionAction } from '@/service/common'
|
import { syncDataSourceNotion, updateDataSourceNotionAction } from '@/service/common'
|
||||||
import { useInvalidDataSourceIntegrates } from '@/service/use-common'
|
import { useInvalidDataSourceIntegrates } from '@/service/use-common'
|
||||||
import Operate from './index'
|
import Operate from '../index'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Operate Component (Notion) Tests
|
* Operate Component (Notion) Tests
|
||||||
@ -3,7 +3,7 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|||||||
import userEvent from '@testing-library/user-event'
|
import userEvent from '@testing-library/user-event'
|
||||||
|
|
||||||
import { createDataSourceApiKeyBinding } from '@/service/datasets'
|
import { createDataSourceApiKeyBinding } from '@/service/datasets'
|
||||||
import ConfigFirecrawlModal from './config-firecrawl-modal'
|
import ConfigFirecrawlModal from '../config-firecrawl-modal'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ConfigFirecrawlModal Component Tests
|
* ConfigFirecrawlModal Component Tests
|
||||||
@ -3,7 +3,7 @@ import userEvent from '@testing-library/user-event'
|
|||||||
|
|
||||||
import { DataSourceProvider } from '@/models/common'
|
import { DataSourceProvider } from '@/models/common'
|
||||||
import { createDataSourceApiKeyBinding } from '@/service/datasets'
|
import { createDataSourceApiKeyBinding } from '@/service/datasets'
|
||||||
import ConfigJinaReaderModal from './config-jina-reader-modal'
|
import ConfigJinaReaderModal from '../config-jina-reader-modal'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ConfigJinaReaderModal Component Tests
|
* ConfigJinaReaderModal Component Tests
|
||||||
@ -3,7 +3,7 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|||||||
import userEvent from '@testing-library/user-event'
|
import userEvent from '@testing-library/user-event'
|
||||||
|
|
||||||
import { createDataSourceApiKeyBinding } from '@/service/datasets'
|
import { createDataSourceApiKeyBinding } from '@/service/datasets'
|
||||||
import ConfigWatercrawlModal from './config-watercrawl-modal'
|
import ConfigWatercrawlModal from '../config-watercrawl-modal'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ConfigWatercrawlModal Component Tests
|
* ConfigWatercrawlModal Component Tests
|
||||||
@ -5,7 +5,7 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
import { DataSourceProvider } from '@/models/common'
|
import { DataSourceProvider } from '@/models/common'
|
||||||
import { fetchDataSources, removeDataSourceApiKeyBinding } from '@/service/datasets'
|
import { fetchDataSources, removeDataSourceApiKeyBinding } from '@/service/datasets'
|
||||||
import DataSourceWebsite from './index'
|
import DataSourceWebsite from '../index'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DataSourceWebsite Component Tests
|
* DataSourceWebsite Component Tests
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import type { ConfigItemType } from './config-item'
|
import type { ConfigItemType } from '../config-item'
|
||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
import ConfigItem from './config-item'
|
import ConfigItem from '../config-item'
|
||||||
import { DataSourceType } from './types'
|
import { DataSourceType } from '../types'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ConfigItem Component Tests
|
* ConfigItem Component Tests
|
||||||
@ -9,7 +9,7 @@ import { DataSourceType } from './types'
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// Mock Operate component to isolate ConfigItem unit tests.
|
// Mock Operate component to isolate ConfigItem unit tests.
|
||||||
vi.mock('../data-source-notion/operate', () => ({
|
vi.mock('../../data-source-notion/operate', () => ({
|
||||||
default: ({ onAuthAgain, payload }: { onAuthAgain: () => void, payload: { id: string, total: number } }) => (
|
default: ({ onAuthAgain, payload }: { onAuthAgain: () => void, payload: { id: string, total: number } }) => (
|
||||||
<div data-testid="mock-operate">
|
<div data-testid="mock-operate">
|
||||||
<button onClick={onAuthAgain} data-testid="operate-auth-btn">Auth Again</button>
|
<button onClick={onAuthAgain} data-testid="operate-auth-btn">Auth Again</button>
|
||||||
@ -1,15 +1,15 @@
|
|||||||
import type { ConfigItemType } from './config-item'
|
import type { ConfigItemType } from '../config-item'
|
||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
import { DataSourceProvider } from '@/models/common'
|
import { DataSourceProvider } from '@/models/common'
|
||||||
import Panel from './index'
|
import Panel from '../index'
|
||||||
import { DataSourceType } from './types'
|
import { DataSourceType } from '../types'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Panel Component Tests
|
* Panel Component Tests
|
||||||
* Tests layout, conditional rendering, and interactions for data source panels (Notion and Website).
|
* Tests layout, conditional rendering, and interactions for data source panels (Notion and Website).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
vi.mock('../data-source-notion/operate', () => ({
|
vi.mock('../../data-source-notion/operate', () => ({
|
||||||
default: () => <div data-testid="mock-operate" />,
|
default: () => <div data-testid="mock-operate" />,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@ -1,334 +0,0 @@
|
|||||||
import type { AppContextValue } from '@/context/app-context'
|
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
|
||||||
import { useAppContext } from '@/context/app-context'
|
|
||||||
import { baseProviderContextValue, useProviderContext } from '@/context/provider-context'
|
|
||||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
|
||||||
import { ACCOUNT_SETTING_TAB } from './constants'
|
|
||||||
import AccountSetting from './index'
|
|
||||||
|
|
||||||
vi.mock('@/context/provider-context', async (importOriginal) => {
|
|
||||||
const actual = await importOriginal<typeof import('@/context/provider-context')>()
|
|
||||||
return {
|
|
||||||
...actual,
|
|
||||||
useProviderContext: vi.fn(),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
vi.mock('@/context/app-context', async (importOriginal) => {
|
|
||||||
const actual = await importOriginal<typeof import('@/context/app-context')>()
|
|
||||||
return {
|
|
||||||
...actual,
|
|
||||||
useAppContext: vi.fn(),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
vi.mock('next/navigation', () => ({
|
|
||||||
useRouter: vi.fn(() => ({
|
|
||||||
push: vi.fn(),
|
|
||||||
replace: vi.fn(),
|
|
||||||
prefetch: vi.fn(),
|
|
||||||
})),
|
|
||||||
usePathname: vi.fn(() => '/'),
|
|
||||||
useParams: vi.fn(() => ({})),
|
|
||||||
useSearchParams: vi.fn(() => ({ get: vi.fn() })),
|
|
||||||
}))
|
|
||||||
|
|
||||||
vi.mock('@/hooks/use-breakpoints', () => ({
|
|
||||||
MediaType: {
|
|
||||||
mobile: 'mobile',
|
|
||||||
tablet: 'tablet',
|
|
||||||
pc: 'pc',
|
|
||||||
},
|
|
||||||
default: vi.fn(),
|
|
||||||
}))
|
|
||||||
|
|
||||||
vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({
|
|
||||||
useDefaultModel: vi.fn(() => ({ data: null, isLoading: false })),
|
|
||||||
useUpdateDefaultModel: vi.fn(() => ({ trigger: vi.fn() })),
|
|
||||||
useUpdateModelList: vi.fn(() => vi.fn()),
|
|
||||||
useModelList: vi.fn(() => ({ data: [], isLoading: false })),
|
|
||||||
useSystemDefaultModelAndModelList: vi.fn(() => [null, vi.fn()]),
|
|
||||||
}))
|
|
||||||
|
|
||||||
vi.mock('@/service/use-datasource', () => ({
|
|
||||||
useGetDataSourceListAuth: vi.fn(() => ({ data: { result: [] } })),
|
|
||||||
}))
|
|
||||||
|
|
||||||
vi.mock('@/service/use-common', () => ({
|
|
||||||
useApiBasedExtensions: vi.fn(() => ({ data: [], isPending: false })),
|
|
||||||
useMembers: vi.fn(() => ({ data: { accounts: [] }, refetch: vi.fn() })),
|
|
||||||
useProviderContext: vi.fn(),
|
|
||||||
}))
|
|
||||||
|
|
||||||
const baseAppContextValue: AppContextValue = {
|
|
||||||
userProfile: {
|
|
||||||
id: '1',
|
|
||||||
name: 'Test User',
|
|
||||||
email: 'test@example.com',
|
|
||||||
avatar: '',
|
|
||||||
avatar_url: '',
|
|
||||||
is_password_set: false,
|
|
||||||
},
|
|
||||||
mutateUserProfile: vi.fn(),
|
|
||||||
currentWorkspace: {
|
|
||||||
id: '1',
|
|
||||||
name: 'Workspace',
|
|
||||||
plan: '',
|
|
||||||
status: '',
|
|
||||||
created_at: 0,
|
|
||||||
role: 'owner',
|
|
||||||
providers: [],
|
|
||||||
trial_credits: 0,
|
|
||||||
trial_credits_used: 0,
|
|
||||||
next_credit_reset_date: 0,
|
|
||||||
},
|
|
||||||
isCurrentWorkspaceManager: true,
|
|
||||||
isCurrentWorkspaceOwner: true,
|
|
||||||
isCurrentWorkspaceEditor: true,
|
|
||||||
isCurrentWorkspaceDatasetOperator: false,
|
|
||||||
mutateCurrentWorkspace: vi.fn(),
|
|
||||||
langGeniusVersionInfo: {
|
|
||||||
current_env: 'testing',
|
|
||||||
current_version: '0.1.0',
|
|
||||||
latest_version: '0.1.0',
|
|
||||||
release_date: '',
|
|
||||||
release_notes: '',
|
|
||||||
version: '0.1.0',
|
|
||||||
can_auto_update: false,
|
|
||||||
},
|
|
||||||
useSelector: vi.fn(),
|
|
||||||
isLoadingCurrentWorkspace: false,
|
|
||||||
isValidatingCurrentWorkspace: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('AccountSetting', () => {
|
|
||||||
const mockOnCancel = vi.fn()
|
|
||||||
const mockOnTabChange = vi.fn()
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
vi.clearAllMocks()
|
|
||||||
vi.mocked(useProviderContext).mockReturnValue({
|
|
||||||
...baseProviderContextValue,
|
|
||||||
enableBilling: true,
|
|
||||||
enableReplaceWebAppLogo: true,
|
|
||||||
})
|
|
||||||
vi.mocked(useAppContext).mockReturnValue(baseAppContextValue)
|
|
||||||
vi.mocked(useBreakpoints).mockReturnValue(MediaType.pc)
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('Rendering', () => {
|
|
||||||
it('should render the sidebar with correct menu items', () => {
|
|
||||||
// Act
|
|
||||||
render(
|
|
||||||
<QueryClientProvider client={new QueryClient()}>
|
|
||||||
<AccountSetting onCancel={mockOnCancel} />
|
|
||||||
</QueryClientProvider>,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(screen.getByText('common.userProfile.settings')).toBeInTheDocument()
|
|
||||||
expect(screen.getByText('common.settings.provider')).toBeInTheDocument()
|
|
||||||
expect(screen.getAllByText('common.settings.members').length).toBeGreaterThan(0)
|
|
||||||
expect(screen.getByText('common.settings.billing')).toBeInTheDocument()
|
|
||||||
expect(screen.getByText('common.settings.dataSource')).toBeInTheDocument()
|
|
||||||
expect(screen.getByText('common.settings.apiBasedExtension')).toBeInTheDocument()
|
|
||||||
expect(screen.getByText('custom.custom')).toBeInTheDocument()
|
|
||||||
expect(screen.getAllByText('common.settings.language').length).toBeGreaterThan(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respect the activeTab prop', () => {
|
|
||||||
// Act
|
|
||||||
render(
|
|
||||||
<QueryClientProvider client={new QueryClient()}>
|
|
||||||
<AccountSetting onCancel={mockOnCancel} activeTab={ACCOUNT_SETTING_TAB.DATA_SOURCE} />
|
|
||||||
</QueryClientProvider>,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
// Check that the active item title is Data Source
|
|
||||||
const titles = screen.getAllByText('common.settings.dataSource')
|
|
||||||
// One in sidebar, one in header.
|
|
||||||
expect(titles.length).toBeGreaterThan(1)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should hide sidebar labels on mobile', () => {
|
|
||||||
// Arrange
|
|
||||||
vi.mocked(useBreakpoints).mockReturnValue(MediaType.mobile)
|
|
||||||
|
|
||||||
// Act
|
|
||||||
render(
|
|
||||||
<QueryClientProvider client={new QueryClient()}>
|
|
||||||
<AccountSetting onCancel={mockOnCancel} />
|
|
||||||
</QueryClientProvider>,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
// On mobile, the labels should not be rendered as per the implementation
|
|
||||||
expect(screen.queryByText('common.settings.provider')).not.toBeInTheDocument()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should filter items for dataset operator', () => {
|
|
||||||
// Arrange
|
|
||||||
vi.mocked(useAppContext).mockReturnValue({
|
|
||||||
...baseAppContextValue,
|
|
||||||
isCurrentWorkspaceDatasetOperator: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Act
|
|
||||||
render(
|
|
||||||
<QueryClientProvider client={new QueryClient()}>
|
|
||||||
<AccountSetting onCancel={mockOnCancel} />
|
|
||||||
</QueryClientProvider>,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(screen.queryByText('common.settings.provider')).not.toBeInTheDocument()
|
|
||||||
expect(screen.queryByText('common.settings.members')).not.toBeInTheDocument()
|
|
||||||
expect(screen.getByText('common.settings.language')).toBeInTheDocument()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should hide billing and custom tabs when disabled', () => {
|
|
||||||
// Arrange
|
|
||||||
vi.mocked(useProviderContext).mockReturnValue({
|
|
||||||
...baseProviderContextValue,
|
|
||||||
enableBilling: false,
|
|
||||||
enableReplaceWebAppLogo: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Act
|
|
||||||
render(
|
|
||||||
<QueryClientProvider client={new QueryClient()}>
|
|
||||||
<AccountSetting onCancel={mockOnCancel} />
|
|
||||||
</QueryClientProvider>,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(screen.queryByText('common.settings.billing')).not.toBeInTheDocument()
|
|
||||||
expect(screen.queryByText('custom.custom')).not.toBeInTheDocument()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('Tab Navigation', () => {
|
|
||||||
it('should change active tab when clicking on menu item', () => {
|
|
||||||
// Arrange
|
|
||||||
render(
|
|
||||||
<QueryClientProvider client={new QueryClient()}>
|
|
||||||
<AccountSetting onCancel={mockOnCancel} onTabChange={mockOnTabChange} />
|
|
||||||
</QueryClientProvider>,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Act
|
|
||||||
fireEvent.click(screen.getByText('common.settings.provider'))
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(mockOnTabChange).toHaveBeenCalledWith(ACCOUNT_SETTING_TAB.PROVIDER)
|
|
||||||
// Check for content from ModelProviderPage
|
|
||||||
expect(screen.getByText('common.modelProvider.models')).toBeInTheDocument()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should navigate through various tabs and show correct details', () => {
|
|
||||||
// Act & Assert
|
|
||||||
render(
|
|
||||||
<QueryClientProvider client={new QueryClient()}>
|
|
||||||
<AccountSetting onCancel={mockOnCancel} />
|
|
||||||
</QueryClientProvider>,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Billing
|
|
||||||
fireEvent.click(screen.getByText('common.settings.billing'))
|
|
||||||
// Billing Page renders plansCommon.plan if data is loaded, or generic text.
|
|
||||||
// Checking for title in header which is always there
|
|
||||||
expect(screen.getAllByText('common.settings.billing').length).toBeGreaterThan(1)
|
|
||||||
|
|
||||||
// Data Source
|
|
||||||
fireEvent.click(screen.getByText('common.settings.dataSource'))
|
|
||||||
expect(screen.getAllByText('common.settings.dataSource').length).toBeGreaterThan(1)
|
|
||||||
|
|
||||||
// API Based Extension
|
|
||||||
fireEvent.click(screen.getByText('common.settings.apiBasedExtension'))
|
|
||||||
expect(screen.getAllByText('common.settings.apiBasedExtension').length).toBeGreaterThan(1)
|
|
||||||
|
|
||||||
// Custom
|
|
||||||
fireEvent.click(screen.getByText('custom.custom'))
|
|
||||||
// Custom Page uses 'custom.custom' key as well.
|
|
||||||
expect(screen.getAllByText('custom.custom').length).toBeGreaterThan(1)
|
|
||||||
|
|
||||||
// Language
|
|
||||||
fireEvent.click(screen.getAllByText('common.settings.language')[0])
|
|
||||||
expect(screen.getAllByText('common.settings.language').length).toBeGreaterThan(1)
|
|
||||||
|
|
||||||
// Members
|
|
||||||
fireEvent.click(screen.getAllByText('common.settings.members')[0])
|
|
||||||
expect(screen.getAllByText('common.settings.members').length).toBeGreaterThan(1)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('Interactions', () => {
|
|
||||||
it('should call onCancel when clicking close button', () => {
|
|
||||||
// Act
|
|
||||||
render(
|
|
||||||
<QueryClientProvider client={new QueryClient()}>
|
|
||||||
<AccountSetting onCancel={mockOnCancel} />
|
|
||||||
</QueryClientProvider>,
|
|
||||||
)
|
|
||||||
const buttons = screen.getAllByRole('button')
|
|
||||||
fireEvent.click(buttons[0])
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(mockOnCancel).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should call onCancel when pressing Escape key', () => {
|
|
||||||
// Act
|
|
||||||
render(
|
|
||||||
<QueryClientProvider client={new QueryClient()}>
|
|
||||||
<AccountSetting onCancel={mockOnCancel} />
|
|
||||||
</QueryClientProvider>,
|
|
||||||
)
|
|
||||||
fireEvent.keyDown(document, { key: 'Escape' })
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(mockOnCancel).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should update search value in provider tab', () => {
|
|
||||||
// Arrange
|
|
||||||
render(
|
|
||||||
<QueryClientProvider client={new QueryClient()}>
|
|
||||||
<AccountSetting onCancel={mockOnCancel} />
|
|
||||||
</QueryClientProvider>,
|
|
||||||
)
|
|
||||||
fireEvent.click(screen.getByText('common.settings.provider'))
|
|
||||||
|
|
||||||
// Act
|
|
||||||
const input = screen.getByRole('textbox')
|
|
||||||
fireEvent.change(input, { target: { value: 'test-search' } })
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(input).toHaveValue('test-search')
|
|
||||||
expect(screen.getByText('common.modelProvider.models')).toBeInTheDocument()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should handle scroll event in panel', () => {
|
|
||||||
// Act
|
|
||||||
render(
|
|
||||||
<QueryClientProvider client={new QueryClient()}>
|
|
||||||
<AccountSetting onCancel={mockOnCancel} />
|
|
||||||
</QueryClientProvider>,
|
|
||||||
)
|
|
||||||
const scrollContainer = screen.getByRole('dialog').querySelector('.overflow-y-auto')
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(scrollContainer).toBeInTheDocument()
|
|
||||||
if (scrollContainer) {
|
|
||||||
// Scroll down
|
|
||||||
fireEvent.scroll(scrollContainer, { target: { scrollTop: 100 } })
|
|
||||||
expect(scrollContainer).toHaveClass('overflow-y-auto')
|
|
||||||
|
|
||||||
// Scroll back up
|
|
||||||
fireEvent.scroll(scrollContainer, { target: { scrollTop: 0 } })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@ -1,8 +1,8 @@
|
|||||||
import type { ComponentProps } from 'react'
|
import type { ComponentProps } from 'react'
|
||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { ValidatedStatus } from './declarations'
|
import { ValidatedStatus } from '../declarations'
|
||||||
import KeyInput from './KeyInput'
|
import KeyInput from '../KeyInput'
|
||||||
|
|
||||||
type Props = ComponentProps<typeof KeyInput>
|
type Props = ComponentProps<typeof KeyInput>
|
||||||
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { render, screen } from '@testing-library/react'
|
import { render, screen } from '@testing-library/react'
|
||||||
import userEvent from '@testing-library/user-event'
|
import userEvent from '@testing-library/user-event'
|
||||||
import Operate from './Operate'
|
import Operate from '../Operate'
|
||||||
|
|
||||||
describe('Operate', () => {
|
describe('Operate', () => {
|
||||||
it('should render cancel and save when editing is open', () => {
|
it('should render cancel and save when editing is open', () => {
|
||||||
@ -4,7 +4,7 @@ import {
|
|||||||
ValidatedErrorMessage,
|
ValidatedErrorMessage,
|
||||||
ValidatedSuccessIcon,
|
ValidatedSuccessIcon,
|
||||||
ValidatingTip,
|
ValidatingTip,
|
||||||
} from './ValidateStatus'
|
} from '../ValidateStatus'
|
||||||
|
|
||||||
describe('ValidateStatus', () => {
|
describe('ValidateStatus', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
import { ValidatedStatus } from './declarations'
|
import { ValidatedStatus } from '../declarations'
|
||||||
|
|
||||||
describe('declarations', () => {
|
describe('declarations', () => {
|
||||||
describe('ValidatedStatus', () => {
|
describe('ValidatedStatus', () => {
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { act, renderHook } from '@testing-library/react'
|
import { act, renderHook } from '@testing-library/react'
|
||||||
import { ValidatedStatus } from './declarations'
|
import { ValidatedStatus } from '../declarations'
|
||||||
import { useValidate } from './hooks'
|
import { useValidate } from '../hooks'
|
||||||
|
|
||||||
describe('useValidate', () => {
|
describe('useValidate', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import type { ComponentProps } from 'react'
|
import type { ComponentProps } from 'react'
|
||||||
import type { Form } from './declarations'
|
import type { Form } from '../declarations'
|
||||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||||
import KeyValidator from './index'
|
import KeyValidator from '../index'
|
||||||
|
|
||||||
let subscriptionCallback: ((value: string) => void) | null = null
|
let subscriptionCallback: ((value: string) => void) | null = null
|
||||||
const mockEmit = vi.fn((value: string) => {
|
const mockEmit = vi.fn((value: string) => {
|
||||||
@ -22,7 +22,7 @@ vi.mock('@/context/event-emitter', () => ({
|
|||||||
const mockValidate = vi.fn()
|
const mockValidate = vi.fn()
|
||||||
const mockUseValidate = vi.fn()
|
const mockUseValidate = vi.fn()
|
||||||
|
|
||||||
vi.mock('./hooks', () => ({
|
vi.mock('../hooks', () => ({
|
||||||
useValidate: (...args: unknown[]) => mockUseValidate(...args),
|
useValidate: (...args: unknown[]) => mockUseValidate(...args),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@ -4,7 +4,7 @@ import { ToastProvider } from '@/app/components/base/toast'
|
|||||||
import { languages } from '@/i18n-config/language'
|
import { languages } from '@/i18n-config/language'
|
||||||
import { updateUserProfile } from '@/service/common'
|
import { updateUserProfile } from '@/service/common'
|
||||||
import { timezones } from '@/utils/timezone'
|
import { timezones } from '@/utils/timezone'
|
||||||
import LanguagePage from './index'
|
import LanguagePage from '../index'
|
||||||
|
|
||||||
const mockRefresh = vi.fn()
|
const mockRefresh = vi.fn()
|
||||||
const mockMutateUserProfile = vi.fn()
|
const mockMutateUserProfile = vi.fn()
|
||||||
@ -10,7 +10,7 @@ import { useGlobalPublicStore } from '@/context/global-public-context'
|
|||||||
import { useProviderContext } from '@/context/provider-context'
|
import { useProviderContext } from '@/context/provider-context'
|
||||||
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
|
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
|
||||||
import { useMembers } from '@/service/use-common'
|
import { useMembers } from '@/service/use-common'
|
||||||
import MembersPage from './index'
|
import MembersPage from '../index'
|
||||||
|
|
||||||
vi.mock('@/context/app-context')
|
vi.mock('@/context/app-context')
|
||||||
vi.mock('@/context/global-public-context')
|
vi.mock('@/context/global-public-context')
|
||||||
@ -18,7 +18,7 @@ vi.mock('@/context/provider-context')
|
|||||||
vi.mock('@/hooks/use-format-time-from-now')
|
vi.mock('@/hooks/use-format-time-from-now')
|
||||||
vi.mock('@/service/use-common')
|
vi.mock('@/service/use-common')
|
||||||
|
|
||||||
vi.mock('./edit-workspace-modal', () => ({
|
vi.mock('../edit-workspace-modal', () => ({
|
||||||
default: ({ onCancel }: { onCancel: () => void }) => (
|
default: ({ onCancel }: { onCancel: () => void }) => (
|
||||||
<div>
|
<div>
|
||||||
<div>Edit Workspace Modal</div>
|
<div>Edit Workspace Modal</div>
|
||||||
@ -26,12 +26,12 @@ vi.mock('./edit-workspace-modal', () => ({
|
|||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
}))
|
}))
|
||||||
vi.mock('./invite-button', () => ({
|
vi.mock('../invite-button', () => ({
|
||||||
default: ({ onClick, disabled }: { onClick: () => void, disabled: boolean }) => (
|
default: ({ onClick, disabled }: { onClick: () => void, disabled: boolean }) => (
|
||||||
<button onClick={onClick} disabled={disabled}>Invite</button>
|
<button onClick={onClick} disabled={disabled}>Invite</button>
|
||||||
),
|
),
|
||||||
}))
|
}))
|
||||||
vi.mock('./invite-modal', () => ({
|
vi.mock('../invite-modal', () => ({
|
||||||
default: ({ onCancel, onSend }: { onCancel: () => void, onSend: (results: Array<{ email: string, status: 'success', url: string }>) => void }) => (
|
default: ({ onCancel, onSend }: { onCancel: () => void, onSend: (results: Array<{ email: string, status: 'success', url: string }>) => void }) => (
|
||||||
<div>
|
<div>
|
||||||
<div>Invite Modal</div>
|
<div>Invite Modal</div>
|
||||||
@ -40,7 +40,7 @@ vi.mock('./invite-modal', () => ({
|
|||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
}))
|
}))
|
||||||
vi.mock('./invited-modal', () => ({
|
vi.mock('../invited-modal', () => ({
|
||||||
default: ({ onCancel }: { onCancel: () => void }) => (
|
default: ({ onCancel }: { onCancel: () => void }) => (
|
||||||
<div>
|
<div>
|
||||||
<div>Invited Modal</div>
|
<div>Invited Modal</div>
|
||||||
@ -48,13 +48,13 @@ vi.mock('./invited-modal', () => ({
|
|||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
}))
|
}))
|
||||||
vi.mock('./operation', () => ({
|
vi.mock('../operation', () => ({
|
||||||
default: () => <div>Member Operation</div>,
|
default: () => <div>Member Operation</div>,
|
||||||
}))
|
}))
|
||||||
vi.mock('./operation/transfer-ownership', () => ({
|
vi.mock('../operation/transfer-ownership', () => ({
|
||||||
default: ({ onOperate }: { onOperate: () => void }) => <button onClick={onOperate}>Transfer ownership</button>,
|
default: ({ onOperate }: { onOperate: () => void }) => <button onClick={onOperate}>Transfer ownership</button>,
|
||||||
}))
|
}))
|
||||||
vi.mock('./transfer-ownership-modal', () => ({
|
vi.mock('../transfer-ownership-modal', () => ({
|
||||||
default: ({ onClose }: { onClose: () => void }) => (
|
default: ({ onClose }: { onClose: () => void }) => (
|
||||||
<div>
|
<div>
|
||||||
<div>Transfer Ownership Modal</div>
|
<div>Transfer Ownership Modal</div>
|
||||||
@ -5,7 +5,7 @@ import { vi } from 'vitest'
|
|||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||||
import { useWorkspacePermissions } from '@/service/use-workspace'
|
import { useWorkspacePermissions } from '@/service/use-workspace'
|
||||||
import InviteButton from './invite-button'
|
import InviteButton from '../invite-button'
|
||||||
|
|
||||||
vi.mock('@/context/app-context')
|
vi.mock('@/context/app-context')
|
||||||
vi.mock('@/context/global-public-context')
|
vi.mock('@/context/global-public-context')
|
||||||
@ -6,7 +6,7 @@ import { vi } from 'vitest'
|
|||||||
import { ToastContext } from '@/app/components/base/toast/context'
|
import { ToastContext } from '@/app/components/base/toast/context'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
import { updateWorkspaceInfo } from '@/service/common'
|
import { updateWorkspaceInfo } from '@/service/common'
|
||||||
import EditWorkspaceModal from './index'
|
import EditWorkspaceModal from '../index'
|
||||||
|
|
||||||
vi.mock('@/context/app-context')
|
vi.mock('@/context/app-context')
|
||||||
vi.mock('@/service/common')
|
vi.mock('@/service/common')
|
||||||
@ -5,7 +5,7 @@ import { vi } from 'vitest'
|
|||||||
import { ToastContext } from '@/app/components/base/toast/context'
|
import { ToastContext } from '@/app/components/base/toast/context'
|
||||||
import { useProviderContextSelector } from '@/context/provider-context'
|
import { useProviderContextSelector } from '@/context/provider-context'
|
||||||
import { inviteMember } from '@/service/common'
|
import { inviteMember } from '@/service/common'
|
||||||
import InviteModal from './index'
|
import InviteModal from '../index'
|
||||||
|
|
||||||
vi.mock('@/context/provider-context', () => ({
|
vi.mock('@/context/provider-context', () => ({
|
||||||
useProviderContextSelector: vi.fn(),
|
useProviderContextSelector: vi.fn(),
|
||||||
@ -4,7 +4,7 @@ import { useState } from 'react'
|
|||||||
import { vi } from 'vitest'
|
import { vi } from 'vitest'
|
||||||
import { createMockProviderContextValue } from '@/__mocks__/provider-context'
|
import { createMockProviderContextValue } from '@/__mocks__/provider-context'
|
||||||
import { useProviderContext } from '@/context/provider-context'
|
import { useProviderContext } from '@/context/provider-context'
|
||||||
import RoleSelector from './role-selector'
|
import RoleSelector from '../role-selector'
|
||||||
|
|
||||||
vi.mock('@/context/provider-context')
|
vi.mock('@/context/provider-context')
|
||||||
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import type { InvitationResult } from '@/models/common'
|
import type { InvitationResult } from '@/models/common'
|
||||||
import { render, screen } from '@testing-library/react'
|
import { render, screen } from '@testing-library/react'
|
||||||
import InvitedModal from './index'
|
import InvitedModal from '../index'
|
||||||
|
|
||||||
const mockConfigState = vi.hoisted(() => ({ isCeEdition: true }))
|
const mockConfigState = vi.hoisted(() => ({ isCeEdition: true }))
|
||||||
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import { act, fireEvent, render, screen } from '@testing-library/react'
|
import { act, fireEvent, render, screen } from '@testing-library/react'
|
||||||
import userEvent from '@testing-library/user-event'
|
import userEvent from '@testing-library/user-event'
|
||||||
import copy from 'copy-to-clipboard'
|
import copy from 'copy-to-clipboard'
|
||||||
import InvitationLink from './invitation-link'
|
import InvitationLink from '../invitation-link'
|
||||||
|
|
||||||
vi.mock('copy-to-clipboard')
|
vi.mock('copy-to-clipboard')
|
||||||
|
|
||||||
@ -3,7 +3,7 @@ import { render, screen, waitFor } from '@testing-library/react'
|
|||||||
import userEvent from '@testing-library/user-event'
|
import userEvent from '@testing-library/user-event'
|
||||||
import { vi } from 'vitest'
|
import { vi } from 'vitest'
|
||||||
import { ToastContext } from '@/app/components/base/toast/context'
|
import { ToastContext } from '@/app/components/base/toast/context'
|
||||||
import Operation from './index'
|
import Operation from '../index'
|
||||||
|
|
||||||
const mockUpdateMemberRole = vi.fn()
|
const mockUpdateMemberRole = vi.fn()
|
||||||
const mockDeleteMemberOrCancelInvitation = vi.fn()
|
const mockDeleteMemberOrCancelInvitation = vi.fn()
|
||||||
@ -6,7 +6,7 @@ import { vi } from 'vitest'
|
|||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||||
import { useWorkspacePermissions } from '@/service/use-workspace'
|
import { useWorkspacePermissions } from '@/service/use-workspace'
|
||||||
import TransferOwnership from './transfer-ownership'
|
import TransferOwnership from '../transfer-ownership'
|
||||||
|
|
||||||
vi.mock('@/context/app-context')
|
vi.mock('@/context/app-context')
|
||||||
vi.mock('@/context/global-public-context')
|
vi.mock('@/context/global-public-context')
|
||||||
@ -7,13 +7,13 @@ import { ToastContext } from '@/app/components/base/toast/context'
|
|||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
import { ownershipTransfer, sendOwnerEmail, verifyOwnerEmail } from '@/service/common'
|
import { ownershipTransfer, sendOwnerEmail, verifyOwnerEmail } from '@/service/common'
|
||||||
import { useMembers } from '@/service/use-common'
|
import { useMembers } from '@/service/use-common'
|
||||||
import TransferOwnershipModal from './index'
|
import TransferOwnershipModal from '../index'
|
||||||
|
|
||||||
vi.mock('@/context/app-context')
|
vi.mock('@/context/app-context')
|
||||||
vi.mock('@/service/common')
|
vi.mock('@/service/common')
|
||||||
vi.mock('@/service/use-common')
|
vi.mock('@/service/use-common')
|
||||||
|
|
||||||
vi.mock('./member-selector', () => ({
|
vi.mock('../member-selector', () => ({
|
||||||
default: ({ onSelect }: { onSelect: (id: string) => void }) => (
|
default: ({ onSelect }: { onSelect: (id: string) => void }) => (
|
||||||
<button onClick={() => onSelect('new-owner-id')}>Select member</button>
|
<button onClick={() => onSelect('new-owner-id')}>Select member</button>
|
||||||
),
|
),
|
||||||
@ -2,7 +2,7 @@ import { render, screen, waitFor } from '@testing-library/react'
|
|||||||
import userEvent from '@testing-library/user-event'
|
import userEvent from '@testing-library/user-event'
|
||||||
import { vi } from 'vitest'
|
import { vi } from 'vitest'
|
||||||
import { useMembers } from '@/service/use-common'
|
import { useMembers } from '@/service/use-common'
|
||||||
import MemberSelector from './member-selector'
|
import MemberSelector from '../member-selector'
|
||||||
|
|
||||||
vi.mock('@/service/use-common')
|
vi.mock('@/service/use-common')
|
||||||
|
|
||||||
@ -6,7 +6,7 @@ import type {
|
|||||||
DefaultModelResponse,
|
DefaultModelResponse,
|
||||||
Model,
|
Model,
|
||||||
ModelProvider,
|
ModelProvider,
|
||||||
} from './declarations'
|
} from '../declarations'
|
||||||
import { act, renderHook, waitFor } from '@testing-library/react'
|
import { act, renderHook, waitFor } from '@testing-library/react'
|
||||||
import { useLocale } from '@/context/i18n'
|
import { useLocale } from '@/context/i18n'
|
||||||
import { fetchDefaultModal, fetchModelList, fetchModelProviderCredentials } from '@/service/common'
|
import { fetchDefaultModal, fetchModelList, fetchModelProviderCredentials } from '@/service/common'
|
||||||
@ -18,7 +18,7 @@ import {
|
|||||||
ModelStatusEnum,
|
ModelStatusEnum,
|
||||||
ModelTypeEnum,
|
ModelTypeEnum,
|
||||||
PreferredProviderTypeEnum,
|
PreferredProviderTypeEnum,
|
||||||
} from './declarations'
|
} from '../declarations'
|
||||||
import {
|
import {
|
||||||
useAnthropicBuyQuota,
|
useAnthropicBuyQuota,
|
||||||
useCurrentProviderAndModel,
|
useCurrentProviderAndModel,
|
||||||
@ -35,8 +35,8 @@ import {
|
|||||||
useTextGenerationCurrentProviderAndModelAndModelList,
|
useTextGenerationCurrentProviderAndModelAndModelList,
|
||||||
useUpdateModelList,
|
useUpdateModelList,
|
||||||
useUpdateModelProviders,
|
useUpdateModelProviders,
|
||||||
} from './hooks'
|
} from '../hooks'
|
||||||
import { UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST } from './provider-added-card'
|
import { UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST } from '../provider-added-card'
|
||||||
|
|
||||||
// Mock dependencies
|
// Mock dependencies
|
||||||
vi.mock('@tanstack/react-query', () => ({
|
vi.mock('@tanstack/react-query', () => ({
|
||||||
@ -4,8 +4,8 @@ import {
|
|||||||
CurrentSystemQuotaTypeEnum,
|
CurrentSystemQuotaTypeEnum,
|
||||||
CustomConfigurationStatusEnum,
|
CustomConfigurationStatusEnum,
|
||||||
QuotaUnitEnum,
|
QuotaUnitEnum,
|
||||||
} from './declarations'
|
} from '../declarations'
|
||||||
import ModelProviderPage from './index'
|
import ModelProviderPage from '../index'
|
||||||
|
|
||||||
vi.mock('@/context/app-context', () => ({
|
vi.mock('@/context/app-context', () => ({
|
||||||
useAppContext: () => ({
|
useAppContext: () => ({
|
||||||
@ -73,23 +73,23 @@ const mockDefaultModelState: {
|
|||||||
isLoading: false,
|
isLoading: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
vi.mock('./hooks', () => ({
|
vi.mock('../hooks', () => ({
|
||||||
useDefaultModel: () => mockDefaultModelState,
|
useDefaultModel: () => mockDefaultModelState,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('./install-from-marketplace', () => ({
|
vi.mock('../install-from-marketplace', () => ({
|
||||||
default: () => <div data-testid="install-from-marketplace" />,
|
default: () => <div data-testid="install-from-marketplace" />,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('./provider-added-card', () => ({
|
vi.mock('../provider-added-card', () => ({
|
||||||
default: ({ provider }: { provider: { provider: string } }) => <div data-testid="provider-card">{provider.provider}</div>,
|
default: ({ provider }: { provider: { provider: string } }) => <div data-testid="provider-card">{provider.provider}</div>,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('./provider-added-card/quota-panel', () => ({
|
vi.mock('../provider-added-card/quota-panel', () => ({
|
||||||
default: () => <div data-testid="quota-panel" />,
|
default: () => <div data-testid="quota-panel" />,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('./system-model-selector', () => ({
|
vi.mock('../system-model-selector', () => ({
|
||||||
default: () => <div data-testid="system-model-selector" />,
|
default: () => <div data-testid="system-model-selector" />,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@ -1,10 +1,10 @@
|
|||||||
import type { Mock } from 'vitest'
|
import type { Mock } from 'vitest'
|
||||||
import type { ModelProvider } from './declarations'
|
import type { ModelProvider } from '../declarations'
|
||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
|
|
||||||
import { describe, expect, it, vi } from 'vitest'
|
import { describe, expect, it, vi } from 'vitest'
|
||||||
import { useMarketplaceAllPlugins } from './hooks'
|
import { useMarketplaceAllPlugins } from '../hooks'
|
||||||
import InstallFromMarketplace from './install-from-marketplace'
|
import InstallFromMarketplace from '../install-from-marketplace'
|
||||||
|
|
||||||
// Mock dependencies
|
// Mock dependencies
|
||||||
vi.mock('next/link', () => ({
|
vi.mock('next/link', () => ({
|
||||||
@ -39,7 +39,7 @@ vi.mock('@/app/components/plugins/provider-card', () => ({
|
|||||||
default: ({ payload }: { payload: { name: string } }) => <div>{payload.name}</div>,
|
default: ({ payload }: { payload: { name: string } }) => <div>{payload.name}</div>,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('./hooks', () => ({
|
vi.mock('../hooks', () => ({
|
||||||
useMarketplaceAllPlugins: vi.fn(() => ({
|
useMarketplaceAllPlugins: vi.fn(() => ({
|
||||||
plugins: [],
|
plugins: [],
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
@ -6,12 +6,12 @@ import {
|
|||||||
validateModelLoadBalancingCredentials,
|
validateModelLoadBalancingCredentials,
|
||||||
validateModelProvider,
|
validateModelProvider,
|
||||||
} from '@/service/common'
|
} from '@/service/common'
|
||||||
import { ValidatedStatus } from '../key-validator/declarations'
|
import { ValidatedStatus } from '../../key-validator/declarations'
|
||||||
import {
|
import {
|
||||||
ConfigurationMethodEnum,
|
ConfigurationMethodEnum,
|
||||||
FormTypeEnum,
|
FormTypeEnum,
|
||||||
ModelTypeEnum,
|
ModelTypeEnum,
|
||||||
} from './declarations'
|
} from '../declarations'
|
||||||
import {
|
import {
|
||||||
genModelNameFormSchema,
|
genModelNameFormSchema,
|
||||||
genModelTypeFormSchema,
|
genModelTypeFormSchema,
|
||||||
@ -22,7 +22,7 @@ import {
|
|||||||
sizeFormat,
|
sizeFormat,
|
||||||
validateCredentials,
|
validateCredentials,
|
||||||
validateLoadBalancingCredentials,
|
validateLoadBalancingCredentials,
|
||||||
} from './utils'
|
} from '../utils'
|
||||||
|
|
||||||
// Mock service/common functions
|
// Mock service/common functions
|
||||||
vi.mock('@/service/common', () => ({
|
vi.mock('@/service/common', () => ({
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import type { CustomModel, ModelCredential, ModelProvider } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
import type { CustomModel, ModelCredential, ModelProvider } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
import { ConfigurationMethodEnum, ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
import { ConfigurationMethodEnum, ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||||
import AddCredentialInLoadBalancing from './add-credential-in-load-balancing'
|
import AddCredentialInLoadBalancing from '../add-credential-in-load-balancing'
|
||||||
|
|
||||||
vi.mock('@/app/components/header/account-setting/model-provider-page/model-auth', () => ({
|
vi.mock('@/app/components/header/account-setting/model-provider-page/model-auth', () => ({
|
||||||
Authorized: ({
|
Authorized: ({
|
||||||
@ -112,7 +112,7 @@ describe('AddCredentialInLoadBalancing', () => {
|
|||||||
// Must invalidate module cache so the component picks up the new mock
|
// Must invalidate module cache so the component picks up the new mock
|
||||||
vi.resetModules()
|
vi.resetModules()
|
||||||
try {
|
try {
|
||||||
const { default: AddCredentialLB } = await import('./add-credential-in-load-balancing')
|
const { default: AddCredentialLB } = await import('../add-credential-in-load-balancing')
|
||||||
|
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<AddCredentialLB
|
<AddCredentialLB
|
||||||
@ -1,13 +1,13 @@
|
|||||||
import type { ModelProvider } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
import type { ModelProvider } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
import { ConfigurationMethodEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
import { ConfigurationMethodEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||||
import AddCustomModel from './add-custom-model'
|
import AddCustomModel from '../add-custom-model'
|
||||||
|
|
||||||
// Mock hooks
|
// Mock hooks
|
||||||
const mockHandleOpenModalForAddNewCustomModel = vi.fn()
|
const mockHandleOpenModalForAddNewCustomModel = vi.fn()
|
||||||
const mockHandleOpenModalForAddCustomModelToModelList = vi.fn()
|
const mockHandleOpenModalForAddCustomModelToModelList = vi.fn()
|
||||||
|
|
||||||
vi.mock('./hooks/use-auth', () => ({
|
vi.mock('../hooks/use-auth', () => ({
|
||||||
useAuth: (_provider: unknown, _configMethod: unknown, _fixedFields: unknown, options: { mode: string }) => {
|
useAuth: (_provider: unknown, _configMethod: unknown, _fixedFields: unknown, options: { mode: string }) => {
|
||||||
if (options.mode === 'config-custom-model') {
|
if (options.mode === 'config-custom-model') {
|
||||||
return { handleOpenModal: mockHandleOpenModalForAddNewCustomModel }
|
return { handleOpenModal: mockHandleOpenModalForAddNewCustomModel }
|
||||||
@ -20,12 +20,12 @@ vi.mock('./hooks/use-auth', () => ({
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
let mockCanAddedModels: { model: string, model_type: string }[] = []
|
let mockCanAddedModels: { model: string, model_type: string }[] = []
|
||||||
vi.mock('./hooks/use-custom-models', () => ({
|
vi.mock('../hooks/use-custom-models', () => ({
|
||||||
useCanAddedModels: () => mockCanAddedModels,
|
useCanAddedModels: () => mockCanAddedModels,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Mock components
|
// Mock components
|
||||||
vi.mock('../model-icon', () => ({
|
vi.mock('../../model-icon', () => ({
|
||||||
default: () => <div data-testid="model-icon" />,
|
default: () => <div data-testid="model-icon" />,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
import ConfigModel from './config-model'
|
import ConfigModel from '../config-model'
|
||||||
|
|
||||||
// Mock icons
|
// Mock icons
|
||||||
vi.mock('@remixicon/react', () => ({
|
vi.mock('@remixicon/react', () => ({
|
||||||
@ -1,15 +1,15 @@
|
|||||||
import type { ModelProvider } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
import type { ModelProvider } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||||
import { render, screen } from '@testing-library/react'
|
import { render, screen } from '@testing-library/react'
|
||||||
import userEvent from '@testing-library/user-event'
|
import userEvent from '@testing-library/user-event'
|
||||||
import ConfigProvider from './config-provider'
|
import ConfigProvider from '../config-provider'
|
||||||
|
|
||||||
const mockUseCredentialStatus = vi.fn()
|
const mockUseCredentialStatus = vi.fn()
|
||||||
|
|
||||||
vi.mock('./hooks', () => ({
|
vi.mock('../hooks', () => ({
|
||||||
useCredentialStatus: () => mockUseCredentialStatus(),
|
useCredentialStatus: () => mockUseCredentialStatus(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('./authorized', () => ({
|
vi.mock('../authorized', () => ({
|
||||||
default: ({ renderTrigger }: { renderTrigger: () => React.ReactNode }) => (
|
default: ({ renderTrigger }: { renderTrigger: () => React.ReactNode }) => (
|
||||||
<div>
|
<div>
|
||||||
{renderTrigger()}
|
{renderTrigger()}
|
||||||
@ -1,8 +1,8 @@
|
|||||||
import { render, screen } from '@testing-library/react'
|
import { render, screen } from '@testing-library/react'
|
||||||
import userEvent from '@testing-library/user-event'
|
import userEvent from '@testing-library/user-event'
|
||||||
import CredentialSelector from './credential-selector'
|
import CredentialSelector from '../credential-selector'
|
||||||
|
|
||||||
vi.mock('./authorized/credential-item', () => ({
|
vi.mock('../authorized/credential-item', () => ({
|
||||||
default: ({ credential, onItemClick }: { credential: { credential_name: string }, onItemClick?: (c: unknown) => void }) => (
|
default: ({ credential, onItemClick }: { credential: { credential_name: string }, onItemClick?: (c: unknown) => void }) => (
|
||||||
<button type="button" onClick={() => onItemClick?.(credential)}>
|
<button type="button" onClick={() => onItemClick?.(credential)}>
|
||||||
{credential.credential_name}
|
{credential.credential_name}
|
||||||
@ -1,10 +1,10 @@
|
|||||||
import type { ModelProvider } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
import type { ModelProvider } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||||
import { render, screen } from '@testing-library/react'
|
import { render, screen } from '@testing-library/react'
|
||||||
import ManageCustomModelCredentials from './manage-custom-model-credentials'
|
import ManageCustomModelCredentials from '../manage-custom-model-credentials'
|
||||||
|
|
||||||
// Mock hooks
|
// Mock hooks
|
||||||
const mockUseCustomModels = vi.fn()
|
const mockUseCustomModels = vi.fn()
|
||||||
vi.mock('./hooks', () => ({
|
vi.mock('../hooks', () => ({
|
||||||
useCustomModels: () => mockUseCustomModels(),
|
useCustomModels: () => mockUseCustomModels(),
|
||||||
useAuth: () => ({
|
useAuth: () => ({
|
||||||
handleOpenModal: vi.fn(),
|
handleOpenModal: vi.fn(),
|
||||||
@ -12,14 +12,34 @@ vi.mock('./hooks', () => ({
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
// Mock Authorized
|
// Mock Authorized
|
||||||
vi.mock('./authorized', () => ({
|
vi.mock('../authorized', () => ({
|
||||||
default: ({ renderTrigger, items, popupTitle }: { renderTrigger: (o?: boolean) => React.ReactNode, items: Array<{ selectedCredential?: unknown }>, popupTitle: string }) => (
|
default: ({
|
||||||
|
renderTrigger,
|
||||||
|
items,
|
||||||
|
popupTitle,
|
||||||
|
}: {
|
||||||
|
renderTrigger: (o?: boolean) => React.ReactNode
|
||||||
|
items: Array<{
|
||||||
|
model?: { model?: string }
|
||||||
|
selectedCredential?: { credential_id?: string }
|
||||||
|
}>
|
||||||
|
popupTitle: string
|
||||||
|
}) => (
|
||||||
<div data-testid="authorized-mock">
|
<div data-testid="authorized-mock">
|
||||||
<div data-testid="trigger-closed">{renderTrigger()}</div>
|
<div data-testid="trigger-closed">{renderTrigger()}</div>
|
||||||
<div data-testid="trigger-open">{renderTrigger(true)}</div>
|
<div data-testid="trigger-open">{renderTrigger(true)}</div>
|
||||||
<div data-testid="popup-title">{popupTitle}</div>
|
<div data-testid="popup-title">{popupTitle}</div>
|
||||||
<div data-testid="items-count">{items.length}</div>
|
<div data-testid="items-count">{items.length}</div>
|
||||||
<div data-testid="items-selected">{items.map((it, i) => <span key={i} data-testid={`selected-${i}`}>{it.selectedCredential ? 'has-cred' : 'no-cred'}</span>)}</div>
|
<div data-testid="items-selected">
|
||||||
|
{items.map((item, index) => (
|
||||||
|
<span
|
||||||
|
key={item.model?.model ?? item.selectedCredential?.credential_id ?? `missing-${popupTitle}`}
|
||||||
|
data-testid={`selected-${index}`}
|
||||||
|
>
|
||||||
|
{item.selectedCredential ? 'has-cred' : 'no-cred'}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
}))
|
}))
|
||||||
@ -1,10 +1,10 @@
|
|||||||
import type { CustomModel, ModelProvider } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
import type { CustomModel, ModelProvider } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||||
import SwitchCredentialInLoadBalancing from './switch-credential-in-load-balancing'
|
import SwitchCredentialInLoadBalancing from '../switch-credential-in-load-balancing'
|
||||||
|
|
||||||
// Mock components
|
// Mock components
|
||||||
vi.mock('./authorized', () => ({
|
vi.mock('../authorized', () => ({
|
||||||
default: ({ renderTrigger, onItemClick, items }: { renderTrigger: () => React.ReactNode, onItemClick: (c: unknown) => void, items: { credentials: unknown[] }[] }) => (
|
default: ({ renderTrigger, onItemClick, items }: { renderTrigger: () => React.ReactNode, onItemClick: (c: unknown) => void, items: { credentials: unknown[] }[] }) => (
|
||||||
<div data-testid="authorized-mock">
|
<div data-testid="authorized-mock">
|
||||||
<div data-testid="trigger-container" onClick={() => onItemClick(items[0].credentials[0])}>
|
<div data-testid="trigger-container" onClick={() => onItemClick(items[0].credentials[0])}>
|
||||||
@ -1,13 +1,13 @@
|
|||||||
import type { Credential, CustomModelCredential, ModelProvider } from '../../declarations'
|
import type { Credential, CustomModelCredential, ModelProvider } from '../../../declarations'
|
||||||
import { render, screen } from '@testing-library/react'
|
import { render, screen } from '@testing-library/react'
|
||||||
import { ModelTypeEnum } from '../../declarations'
|
import { ModelTypeEnum } from '../../../declarations'
|
||||||
import { AuthorizedItem } from './authorized-item'
|
import { AuthorizedItem } from '../authorized-item'
|
||||||
|
|
||||||
vi.mock('../../model-icon', () => ({
|
vi.mock('../../../model-icon', () => ({
|
||||||
default: ({ modelName }: { modelName: string }) => <div data-testid="model-icon">{modelName}</div>,
|
default: ({ modelName }: { modelName: string }) => <div data-testid="model-icon">{modelName}</div>,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('./credential-item', () => ({
|
vi.mock('../credential-item', () => ({
|
||||||
default: ({ credential, onEdit, onDelete, onItemClick }: {
|
default: ({ credential, onEdit, onDelete, onItemClick }: {
|
||||||
credential: Credential
|
credential: Credential
|
||||||
onEdit?: (credential: Credential) => void
|
onEdit?: (credential: Credential) => void
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import type { Credential } from '../../declarations'
|
import type { Credential } from '../../../declarations'
|
||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
import CredentialItem from './credential-item'
|
import CredentialItem from '../credential-item'
|
||||||
|
|
||||||
vi.mock('@remixicon/react', () => ({
|
vi.mock('@remixicon/react', () => ({
|
||||||
RiCheckLine: () => <div data-testid="check-icon" />,
|
RiCheckLine: () => <div data-testid="check-icon" />,
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import type { Credential, CustomModel, ModelProvider } from '../../declarations'
|
import type { Credential, CustomModel, ModelProvider } from '../../../declarations'
|
||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
import { ConfigurationMethodEnum, ModelTypeEnum } from '../../declarations'
|
import { ConfigurationMethodEnum, ModelTypeEnum } from '../../../declarations'
|
||||||
import Authorized from './index'
|
import Authorized from '../index'
|
||||||
|
|
||||||
const mockHandleOpenModal = vi.fn()
|
const mockHandleOpenModal = vi.fn()
|
||||||
const mockHandleActiveCredential = vi.fn()
|
const mockHandleActiveCredential = vi.fn()
|
||||||
@ -12,7 +12,7 @@ const mockHandleConfirmDelete = vi.fn()
|
|||||||
let mockDeleteCredentialId: string | null = null
|
let mockDeleteCredentialId: string | null = null
|
||||||
let mockDoingAction = false
|
let mockDoingAction = false
|
||||||
|
|
||||||
vi.mock('../hooks', () => ({
|
vi.mock('../../hooks', () => ({
|
||||||
useAuth: () => ({
|
useAuth: () => ({
|
||||||
openConfirmDelete: mockOpenConfirmDelete,
|
openConfirmDelete: mockOpenConfirmDelete,
|
||||||
closeConfirmDelete: mockCloseConfirmDelete,
|
closeConfirmDelete: mockCloseConfirmDelete,
|
||||||
@ -24,7 +24,7 @@ vi.mock('../hooks', () => ({
|
|||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('./authorized-item', () => ({
|
vi.mock('../authorized-item', () => ({
|
||||||
default: ({ credentials, model, onEdit, onDelete, onItemClick }: {
|
default: ({ credentials, model, onEdit, onDelete, onItemClick }: {
|
||||||
credentials: Credential[]
|
credentials: Credential[]
|
||||||
model?: CustomModel
|
model?: CustomModel
|
||||||
@ -1,8 +1,8 @@
|
|||||||
import type { CustomModel } from '../../declarations'
|
import type { CustomModel } from '../../../declarations'
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||||
import { renderHook } from '@testing-library/react'
|
import { renderHook } from '@testing-library/react'
|
||||||
import { ModelTypeEnum } from '../../declarations'
|
import { ModelTypeEnum } from '../../../declarations'
|
||||||
import { useAuthService, useGetCredential } from './use-auth-service'
|
import { useAuthService, useGetCredential } from '../use-auth-service'
|
||||||
|
|
||||||
vi.mock('@/service/use-models', () => ({
|
vi.mock('@/service/use-models', () => ({
|
||||||
useGetProviderCredential: vi.fn(),
|
useGetProviderCredential: vi.fn(),
|
||||||
@ -3,11 +3,11 @@ import type {
|
|||||||
Credential,
|
Credential,
|
||||||
CustomModel,
|
CustomModel,
|
||||||
ModelProvider,
|
ModelProvider,
|
||||||
} from '../../declarations'
|
} from '../../../declarations'
|
||||||
import { act, renderHook } from '@testing-library/react'
|
import { act, renderHook } from '@testing-library/react'
|
||||||
import { ToastContext } from '@/app/components/base/toast/context'
|
import { ToastContext } from '@/app/components/base/toast/context'
|
||||||
import { ConfigurationMethodEnum, ModelModalModeEnum, ModelTypeEnum } from '../../declarations'
|
import { ConfigurationMethodEnum, ModelModalModeEnum, ModelTypeEnum } from '../../../declarations'
|
||||||
import { useAuth } from './use-auth'
|
import { useAuth } from '../use-auth'
|
||||||
|
|
||||||
const mockNotify = vi.fn()
|
const mockNotify = vi.fn()
|
||||||
const mockHandleRefreshModel = vi.fn()
|
const mockHandleRefreshModel = vi.fn()
|
||||||
@ -39,7 +39,7 @@ vi.mock('@/service/use-models', () => ({
|
|||||||
useDeleteModel: () => ({ mutateAsync: mockDeleteModelService }),
|
useDeleteModel: () => ({ mutateAsync: mockDeleteModelService }),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('./use-auth-service', () => ({
|
vi.mock('../use-auth-service', () => ({
|
||||||
useAuthService: () => ({
|
useAuthService: () => ({
|
||||||
getDeleteCredentialService: (isModel: boolean) => (isModel ? mockDeleteModelCredential : mockDeleteProviderCredential),
|
getDeleteCredentialService: (isModel: boolean) => (isModel ? mockDeleteModelCredential : mockDeleteProviderCredential),
|
||||||
getActiveCredentialService: (isModel: boolean) => (isModel ? mockActiveModelCredential : mockActiveProviderCredential),
|
getActiveCredentialService: (isModel: boolean) => (isModel ? mockActiveModelCredential : mockActiveProviderCredential),
|
||||||
@ -1,13 +1,13 @@
|
|||||||
import type { Credential, CustomModelCredential, ModelProvider } from '../../declarations'
|
import type { Credential, CustomModelCredential, ModelProvider } from '../../../declarations'
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||||
import { renderHook } from '@testing-library/react'
|
import { renderHook } from '@testing-library/react'
|
||||||
import { useCredentialData } from './use-credential-data'
|
import { useCredentialData } from '../use-credential-data'
|
||||||
|
|
||||||
vi.mock('./use-auth-service', () => ({
|
vi.mock('../use-auth-service', () => ({
|
||||||
useGetCredential: vi.fn(),
|
useGetCredential: vi.fn(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const { useGetCredential } = await import('./use-auth-service')
|
const { useGetCredential } = await import('../use-auth-service')
|
||||||
|
|
||||||
describe('useCredentialData', () => {
|
describe('useCredentialData', () => {
|
||||||
let queryClient: QueryClient
|
let queryClient: QueryClient
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import type { ModelProvider } from '../../declarations'
|
import type { ModelProvider } from '../../../declarations'
|
||||||
import { renderHook } from '@testing-library/react'
|
import { renderHook } from '@testing-library/react'
|
||||||
import { useCredentialStatus } from './use-credential-status'
|
import { useCredentialStatus } from '../use-credential-status'
|
||||||
|
|
||||||
describe('useCredentialStatus', () => {
|
describe('useCredentialStatus', () => {
|
||||||
it('computes authorized and authRemoved status correctly', () => {
|
it('computes authorized and authRemoved status correctly', () => {
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import type { ModelProvider } from '../../declarations'
|
import type { ModelProvider } from '../../../declarations'
|
||||||
import { renderHook } from '@testing-library/react'
|
import { renderHook } from '@testing-library/react'
|
||||||
import { useCanAddedModels, useCustomModels } from './use-custom-models'
|
import { useCanAddedModels, useCustomModels } from '../use-custom-models'
|
||||||
|
|
||||||
describe('useCustomModels and useCanAddedModels', () => {
|
describe('useCustomModels and useCanAddedModels', () => {
|
||||||
it('extracts custom models from provider correctly', () => {
|
it('extracts custom models from provider correctly', () => {
|
||||||
@ -2,13 +2,13 @@ import type {
|
|||||||
Credential,
|
Credential,
|
||||||
CustomModelCredential,
|
CustomModelCredential,
|
||||||
ModelProvider,
|
ModelProvider,
|
||||||
} from '../../declarations'
|
} from '../../../declarations'
|
||||||
import { renderHook } from '@testing-library/react'
|
import { renderHook } from '@testing-library/react'
|
||||||
import { describe, expect, it, vi } from 'vitest'
|
import { describe, expect, it, vi } from 'vitest'
|
||||||
import { FormTypeEnum } from '@/app/components/base/form/types'
|
import { FormTypeEnum } from '@/app/components/base/form/types'
|
||||||
import { useModelFormSchemas } from './use-model-form-schemas'
|
import { useModelFormSchemas } from '../use-model-form-schemas'
|
||||||
|
|
||||||
vi.mock('../../utils', () => ({
|
vi.mock('../../../utils', () => ({
|
||||||
genModelNameFormSchema: vi.fn(() => ({
|
genModelNameFormSchema: vi.fn(() => ({
|
||||||
type: FormTypeEnum.textInput,
|
type: FormTypeEnum.textInput,
|
||||||
variable: '__model_name',
|
variable: '__model_name',
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { render, screen } from '@testing-library/react'
|
import { render, screen } from '@testing-library/react'
|
||||||
import ModelBadge from './index'
|
import ModelBadge from '../index'
|
||||||
|
|
||||||
describe('ModelBadge', () => {
|
describe('ModelBadge', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@ -1,12 +1,12 @@
|
|||||||
import type { Model } from '../declarations'
|
import type { Model } from '../../declarations'
|
||||||
import { render, screen } from '@testing-library/react'
|
import { render, screen } from '@testing-library/react'
|
||||||
import { Theme } from '@/types/app'
|
import { Theme } from '@/types/app'
|
||||||
import {
|
import {
|
||||||
ConfigurationMethodEnum,
|
ConfigurationMethodEnum,
|
||||||
ModelStatusEnum,
|
ModelStatusEnum,
|
||||||
ModelTypeEnum,
|
ModelTypeEnum,
|
||||||
} from '../declarations'
|
} from '../../declarations'
|
||||||
import ModelIcon from './index'
|
import ModelIcon from '../index'
|
||||||
|
|
||||||
type I18nText = {
|
type I18nText = {
|
||||||
en_US: string
|
en_US: string
|
||||||
@ -20,7 +20,7 @@ vi.mock('@/hooks/use-theme', () => ({
|
|||||||
default: () => ({ theme: mockTheme }),
|
default: () => ({ theme: mockTheme }),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('../hooks', () => ({
|
vi.mock('../../hooks', () => ({
|
||||||
useLanguage: () => mockLanguage,
|
useLanguage: () => mockLanguage,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
import { render } from '@testing-library/react'
|
|
||||||
import Input from './Input'
|
|
||||||
|
|
||||||
it('Input renders correctly as password type with no autocomplete', () => {
|
|
||||||
const { asFragment, getByPlaceholderText } = render(
|
|
||||||
<Input
|
|
||||||
type="password"
|
|
||||||
placeholder="API Key"
|
|
||||||
onChange={vi.fn()}
|
|
||||||
/>,
|
|
||||||
)
|
|
||||||
const input = getByPlaceholderText('API Key')
|
|
||||||
expect(input).toHaveAttribute('type', 'password')
|
|
||||||
expect(input).not.toHaveAttribute('autocomplete')
|
|
||||||
expect(asFragment()).toMatchSnapshot()
|
|
||||||
})
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
||||||
|
|
||||||
exports[`Input renders correctly as password type with no autocomplete 1`] = `
|
|
||||||
<DocumentFragment>
|
|
||||||
<div
|
|
||||||
class="relative"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
class="
|
|
||||||
block h-8 w-full appearance-none rounded-lg border border-transparent bg-components-input-bg-normal px-3 text-sm
|
|
||||||
text-components-input-text-filled caret-primary-600 outline-none
|
|
||||||
placeholder:text-sm placeholder:text-text-tertiary
|
|
||||||
hover:border-components-input-border-hover hover:bg-components-input-bg-hover focus:border-components-input-border-active
|
|
||||||
focus:bg-components-input-bg-active focus:shadow-xs
|
|
||||||
|
|
||||||
|
|
||||||
"
|
|
||||||
placeholder="API Key"
|
|
||||||
tabindex="0"
|
|
||||||
type="password"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</DocumentFragment>
|
|
||||||
`;
|
|
||||||
@ -7,11 +7,11 @@ import type {
|
|||||||
CredentialFormSchemaSelect,
|
CredentialFormSchemaSelect,
|
||||||
CredentialFormSchemaTextInput,
|
CredentialFormSchemaTextInput,
|
||||||
FormValue,
|
FormValue,
|
||||||
} from '../declarations'
|
} from '../../declarations'
|
||||||
import type { NodeOutPutVar } from '@/app/components/workflow/types'
|
import type { NodeOutPutVar } from '@/app/components/workflow/types'
|
||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||||
import { FormTypeEnum } from '../declarations'
|
import { FormTypeEnum } from '../../declarations'
|
||||||
import Form from './Form'
|
import Form from '../Form'
|
||||||
|
|
||||||
type CustomSchema = Omit<CredentialFormSchemaBase, 'type'> & { type: 'custom-type' }
|
type CustomSchema = Omit<CredentialFormSchemaBase, 'type'> & { type: 'custom-type' }
|
||||||
|
|
||||||
@ -23,7 +23,7 @@ const modelSelectorPropsSpy = vi.hoisted(() => vi.fn())
|
|||||||
const toolSelectorPropsSpy = vi.hoisted(() => vi.fn())
|
const toolSelectorPropsSpy = vi.hoisted(() => vi.fn())
|
||||||
|
|
||||||
const mockLanguageRef = { value: 'en_US' }
|
const mockLanguageRef = { value: 'en_US' }
|
||||||
vi.mock('../hooks', () => ({
|
vi.mock('../../hooks', () => ({
|
||||||
useLanguage: () => mockLanguageRef.value,
|
useLanguage: () => mockLanguageRef.value,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@ -84,7 +84,7 @@ vi.mock('@/app/components/workflow/nodes/_base/components/variable/var-reference
|
|||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('../../key-validator/ValidateStatus', () => ({
|
vi.mock('../../../key-validator/ValidateStatus', () => ({
|
||||||
ValidatingTip: () => <div>Validating...</div>,
|
ValidatingTip: () => <div>Validating...</div>,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@ -202,7 +202,7 @@ describe('Form', () => {
|
|||||||
|
|
||||||
// Interaction updates
|
// Interaction updates
|
||||||
describe('Interactions', () => {
|
describe('Interactions', () => {
|
||||||
it('should update values and clear dependent fields when a field changes', () => {
|
it('should update values and clear dependent fields when a field changes', async () => {
|
||||||
const formSchemas: AnyFormSchema[] = [
|
const formSchemas: AnyFormSchema[] = [
|
||||||
createTextSchema({
|
createTextSchema({
|
||||||
variable: 'api_key',
|
variable: 'api_key',
|
||||||
@ -232,8 +232,10 @@ describe('Form', () => {
|
|||||||
|
|
||||||
fireEvent.change(screen.getByPlaceholderText('API Key'), { target: { value: 'new-key' } })
|
fireEvent.change(screen.getByPlaceholderText('API Key'), { target: { value: 'new-key' } })
|
||||||
|
|
||||||
expect(onChange).toHaveBeenCalledWith({ api_key: 'new-key', dependent: 'reset' })
|
await waitFor(() => {
|
||||||
expect(screen.getByText('Validating...')).toBeInTheDocument()
|
expect(onChange).toHaveBeenCalledWith({ api_key: 'new-key', dependent: 'reset' })
|
||||||
|
expect(screen.getByText('Validating...')).toBeInTheDocument()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should render radio options based on show conditions and ignore edit-locked changes', () => {
|
it('should render radio options based on show conditions and ignore edit-locked changes', () => {
|
||||||
@ -447,9 +449,9 @@ describe('Form', () => {
|
|||||||
showOnVariableMap={{}}
|
showOnVariableMap={{}}
|
||||||
isEditMode={false}
|
isEditMode={false}
|
||||||
fieldMoreInfo={() => <div>Extra Info</div>}
|
fieldMoreInfo={() => <div>Extra Info</div>}
|
||||||
override={[[FormTypeEnum.textInput], () => <div>Override Field</div>]}
|
override={[[FormTypeEnum.textInput], () => <div key="override-field">Override Field</div>]}
|
||||||
customRenderField={schema => (
|
customRenderField={schema => (
|
||||||
<div>
|
<div key={schema.variable}>
|
||||||
Custom Render:
|
Custom Render:
|
||||||
{schema.variable}
|
{schema.variable}
|
||||||
</div>
|
</div>
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
import Input from './Input'
|
import Input from '../Input'
|
||||||
|
|
||||||
describe('Input', () => {
|
describe('Input', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@ -19,6 +19,21 @@ describe('Input', () => {
|
|||||||
expect(screen.getByPlaceholderText('API Key')).toHaveValue('hello')
|
expect(screen.getByPlaceholderText('API Key')).toHaveValue('hello')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should render password inputs without autocomplete attributes', () => {
|
||||||
|
render(
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholder="Secret"
|
||||||
|
onChange={vi.fn()}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
const input = screen.getByPlaceholderText('Secret')
|
||||||
|
|
||||||
|
expect(input).toHaveAttribute('type', 'password')
|
||||||
|
expect(input).not.toHaveAttribute('autocomplete')
|
||||||
|
})
|
||||||
|
|
||||||
// User interaction
|
// User interaction
|
||||||
it('should call onChange when the user types', () => {
|
it('should call onChange when the user types', () => {
|
||||||
const onChange = vi.fn()
|
const onChange = vi.fn()
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import type { ComponentProps } from 'react'
|
import type { ComponentProps } from 'react'
|
||||||
import type { Credential, CredentialFormSchema, CustomModel, ModelProvider } from '../declarations'
|
import type { Credential, CredentialFormSchema, CustomModel, ModelProvider } from '../../declarations'
|
||||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import {
|
import {
|
||||||
@ -10,8 +10,8 @@ import {
|
|||||||
ModelTypeEnum,
|
ModelTypeEnum,
|
||||||
PreferredProviderTypeEnum,
|
PreferredProviderTypeEnum,
|
||||||
QuotaUnitEnum,
|
QuotaUnitEnum,
|
||||||
} from '../declarations'
|
} from '../../declarations'
|
||||||
import ModelModal from './index'
|
import ModelModal from '../index'
|
||||||
|
|
||||||
type CredentialData = {
|
type CredentialData = {
|
||||||
credentials: Record<string, unknown>
|
credentials: Record<string, unknown>
|
||||||
@ -45,7 +45,7 @@ const mockHandlers = vi.hoisted(() => ({
|
|||||||
handleActiveCredential: vi.fn(),
|
handleActiveCredential: vi.fn(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('../model-auth/hooks', () => ({
|
vi.mock('../../model-auth/hooks', () => ({
|
||||||
useCredentialData: () => ({
|
useCredentialData: () => ({
|
||||||
isLoading: mockState.isLoading,
|
isLoading: mockState.isLoading,
|
||||||
credentialData: mockState.credentialData,
|
credentialData: mockState.credentialData,
|
||||||
@ -75,7 +75,7 @@ vi.mock('@/hooks/use-i18n', () => ({
|
|||||||
useRenderI18nObject: () => (value: { en_US: string }) => value.en_US,
|
useRenderI18nObject: () => (value: { en_US: string }) => value.en_US,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('../hooks', () => ({
|
vi.mock('../../hooks', () => ({
|
||||||
useLanguage: () => 'en_US',
|
useLanguage: () => 'en_US',
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@ -164,7 +164,7 @@ vi.mock('@/app/components/base/form/form-scenarios/auth', () => ({
|
|||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('../model-auth', () => ({
|
vi.mock('../../model-auth', () => ({
|
||||||
CredentialSelector: ({ onSelect }: { onSelect: (val: unknown) => void }) => (
|
CredentialSelector: ({ onSelect }: { onSelect: (val: unknown) => void }) => (
|
||||||
<button onClick={() => onSelect({ addNewCredential: true })} data-testid="credential-selector">
|
<button onClick={() => onSelect({ addNewCredential: true })} data-testid="credential-selector">
|
||||||
Select Credential
|
Select Credential
|
||||||
@ -1,12 +1,12 @@
|
|||||||
import type { ModelItem } from '../declarations'
|
import type { ModelItem } from '../../declarations'
|
||||||
import { render, screen } from '@testing-library/react'
|
import { render, screen } from '@testing-library/react'
|
||||||
import {
|
import {
|
||||||
ConfigurationMethodEnum,
|
ConfigurationMethodEnum,
|
||||||
ModelFeatureEnum,
|
ModelFeatureEnum,
|
||||||
ModelStatusEnum,
|
ModelStatusEnum,
|
||||||
ModelTypeEnum,
|
ModelTypeEnum,
|
||||||
} from '../declarations'
|
} from '../../declarations'
|
||||||
import ModelName from './index'
|
import ModelName from '../index'
|
||||||
|
|
||||||
let mockLocale = 'en-US'
|
let mockLocale = 'en-US'
|
||||||
|
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import type { MouseEvent } from 'react'
|
import type { MouseEvent } from 'react'
|
||||||
import type { ModelProvider } from '../declarations'
|
import type { ModelProvider } from '../../declarations'
|
||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
import { vi } from 'vitest'
|
import { vi } from 'vitest'
|
||||||
import {
|
import {
|
||||||
@ -7,8 +7,8 @@ import {
|
|||||||
CustomConfigurationStatusEnum,
|
CustomConfigurationStatusEnum,
|
||||||
ModelTypeEnum,
|
ModelTypeEnum,
|
||||||
QuotaUnitEnum,
|
QuotaUnitEnum,
|
||||||
} from '../declarations'
|
} from '../../declarations'
|
||||||
import AgentModelTrigger from './agent-model-trigger'
|
import AgentModelTrigger from '../agent-model-trigger'
|
||||||
|
|
||||||
let modelProviders: ModelProvider[] = []
|
let modelProviders: ModelProvider[] = []
|
||||||
let pluginInfo: { latest_package_identifier: string } | null = null
|
let pluginInfo: { latest_package_identifier: string } | null = null
|
||||||
@ -31,21 +31,21 @@ vi.mock('@/service/use-plugins', () => ({
|
|||||||
usePluginInfo: () => ({ data: pluginInfo, isLoading: pluginLoading }),
|
usePluginInfo: () => ({ data: pluginInfo, isLoading: pluginLoading }),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('../hooks', () => ({
|
vi.mock('../../hooks', () => ({
|
||||||
useModelModalHandler: () => handleOpenModal,
|
useModelModalHandler: () => handleOpenModal,
|
||||||
useUpdateModelList: () => updateModelList,
|
useUpdateModelList: () => updateModelList,
|
||||||
useUpdateModelProviders: () => updateModelProviders,
|
useUpdateModelProviders: () => updateModelProviders,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('../model-icon', () => ({
|
vi.mock('../../model-icon', () => ({
|
||||||
default: () => <div>Icon</div>,
|
default: () => <div>Icon</div>,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('./model-display', () => ({
|
vi.mock('../model-display', () => ({
|
||||||
default: () => <div>ModelDisplay</div>,
|
default: () => <div>ModelDisplay</div>,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('./status-indicators', () => ({
|
vi.mock('../status-indicators', () => ({
|
||||||
default: () => <div>StatusIndicators</div>,
|
default: () => <div>StatusIndicators</div>,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@ -1,8 +1,8 @@
|
|||||||
import type { ComponentProps } from 'react'
|
import type { ComponentProps } from 'react'
|
||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
import { vi } from 'vitest'
|
import { vi } from 'vitest'
|
||||||
import { ConfigurationMethodEnum } from '../declarations'
|
import { ConfigurationMethodEnum } from '../../declarations'
|
||||||
import ConfigurationButton from './configuration-button'
|
import ConfigurationButton from '../configuration-button'
|
||||||
|
|
||||||
describe('ConfigurationButton', () => {
|
describe('ConfigurationButton', () => {
|
||||||
it('should render and handle click', () => {
|
it('should render and handle click', () => {
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
import ModelParameterModal from './index'
|
import ModelParameterModal from '../index'
|
||||||
|
|
||||||
let isAPIKeySet = true
|
let isAPIKeySet = true
|
||||||
let parameterRules: Array<Record<string, unknown>> | undefined = [
|
let parameterRules: Array<Record<string, unknown>> | undefined = [
|
||||||
@ -53,7 +53,7 @@ vi.mock('@/service/use-common', () => ({
|
|||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('../hooks', () => ({
|
vi.mock('../../hooks', () => ({
|
||||||
useTextGenerationCurrentProviderAndModelAndModelList: () => ({
|
useTextGenerationCurrentProviderAndModelAndModelList: () => ({
|
||||||
currentProvider,
|
currentProvider,
|
||||||
currentModel,
|
currentModel,
|
||||||
@ -61,7 +61,7 @@ vi.mock('../hooks', () => ({
|
|||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('./parameter-item', () => ({
|
vi.mock('../parameter-item', () => ({
|
||||||
default: ({ parameterRule, onChange, onSwitch }: {
|
default: ({ parameterRule, onChange, onSwitch }: {
|
||||||
parameterRule: { name: string, label: { en_US: string } }
|
parameterRule: { name: string, label: { en_US: string } }
|
||||||
onChange: (v: number) => void
|
onChange: (v: number) => void
|
||||||
@ -76,7 +76,7 @@ vi.mock('./parameter-item', () => ({
|
|||||||
),
|
),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('../model-selector', () => ({
|
vi.mock('../../model-selector', () => ({
|
||||||
default: ({ onSelect }: { onSelect: (value: { provider: string, model: string }) => void }) => (
|
default: ({ onSelect }: { onSelect: (value: { provider: string, model: string }) => void }) => (
|
||||||
<div data-testid="model-selector">
|
<div data-testid="model-selector">
|
||||||
<button onClick={() => onSelect({ provider: 'openai', model: 'gpt-4.1' })}>Select GPT-4.1</button>
|
<button onClick={() => onSelect({ provider: 'openai', model: 'gpt-4.1' })}>Select GPT-4.1</button>
|
||||||
@ -84,13 +84,13 @@ vi.mock('../model-selector', () => ({
|
|||||||
),
|
),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('./presets-parameter', () => ({
|
vi.mock('../presets-parameter', () => ({
|
||||||
default: ({ onSelect }: { onSelect: (id: number) => void }) => (
|
default: ({ onSelect }: { onSelect: (id: number) => void }) => (
|
||||||
<button onClick={() => onSelect(1)}>Preset 1</button>
|
<button onClick={() => onSelect(1)}>Preset 1</button>
|
||||||
),
|
),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('./trigger', () => ({
|
vi.mock('../trigger', () => ({
|
||||||
default: () => <button>Open Settings</button>,
|
default: () => <button>Open Settings</button>,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@ -1,8 +1,8 @@
|
|||||||
import { render, screen } from '@testing-library/react'
|
import { render, screen } from '@testing-library/react'
|
||||||
import { vi } from 'vitest'
|
import { vi } from 'vitest'
|
||||||
import ModelDisplay from './model-display'
|
import ModelDisplay from '../model-display'
|
||||||
|
|
||||||
vi.mock('../model-name', () => ({
|
vi.mock('../../model-name', () => ({
|
||||||
default: ({ modelItem }: { modelItem: { model: string } }) => <div>{modelItem.model}</div>,
|
default: ({ modelItem }: { modelItem: { model: string } }) => <div>{modelItem.model}</div>,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@ -1,8 +1,8 @@
|
|||||||
import type { ModelParameterRule } from '../declarations'
|
import type { ModelParameterRule } from '../../declarations'
|
||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
import ParameterItem from './parameter-item'
|
import ParameterItem from '../parameter-item'
|
||||||
|
|
||||||
vi.mock('../hooks', () => ({
|
vi.mock('../../hooks', () => ({
|
||||||
useLanguage: () => 'en_US',
|
useLanguage: () => 'en_US',
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
import { vi } from 'vitest'
|
import { vi } from 'vitest'
|
||||||
import PresetsParameter from './presets-parameter'
|
import PresetsParameter from '../presets-parameter'
|
||||||
|
|
||||||
describe('PresetsParameter', () => {
|
describe('PresetsParameter', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import { render, screen } from '@testing-library/react'
|
import { render, screen } from '@testing-library/react'
|
||||||
import userEvent from '@testing-library/user-event'
|
import userEvent from '@testing-library/user-event'
|
||||||
import { vi } from 'vitest'
|
import { vi } from 'vitest'
|
||||||
import StatusIndicators from './status-indicators'
|
import StatusIndicators from '../status-indicators'
|
||||||
|
|
||||||
let installedPlugins = [{ name: 'demo-plugin', plugin_unique_identifier: 'demo@1.0.0' }]
|
let installedPlugins = [{ name: 'demo-plugin', plugin_unique_identifier: 'demo@1.0.0' }]
|
||||||
|
|
||||||
@ -1,9 +1,9 @@
|
|||||||
import type { ComponentProps } from 'react'
|
import type { ComponentProps } from 'react'
|
||||||
import { render, screen } from '@testing-library/react'
|
import { render, screen } from '@testing-library/react'
|
||||||
import userEvent from '@testing-library/user-event'
|
import userEvent from '@testing-library/user-event'
|
||||||
import Trigger from './trigger'
|
import Trigger from '../trigger'
|
||||||
|
|
||||||
vi.mock('../hooks', () => ({
|
vi.mock('../../hooks', () => ({
|
||||||
useLanguage: () => 'en_US',
|
useLanguage: () => 'en_US',
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@ -13,11 +13,11 @@ vi.mock('@/context/provider-context', () => ({
|
|||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('../model-icon', () => ({
|
vi.mock('../../model-icon', () => ({
|
||||||
default: () => <div data-testid="model-icon">Icon</div>,
|
default: () => <div data-testid="model-icon">Icon</div>,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('../model-name', () => ({
|
vi.mock('../../model-name', () => ({
|
||||||
default: ({ modelItem }: { modelItem: { model: string } }) => <div>{modelItem.model}</div>,
|
default: ({ modelItem }: { modelItem: { model: string } }) => <div>{modelItem.model}</div>,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user