Merge remote-tracking branch 'origin/main' into feat/support-agent-sandbox

# Conflicts:
#	api/uv.lock
#	web/app/components/apps/__tests__/app-card.spec.tsx
#	web/app/components/apps/__tests__/list.spec.tsx
#	web/app/components/datasets/create/__tests__/index.spec.tsx
#	web/app/components/datasets/metadata/metadata-dataset/__tests__/dataset-metadata-drawer.spec.tsx
#	web/app/components/plugins/readme-panel/__tests__/index.spec.tsx
#	web/app/components/rag-pipeline/__tests__/index.spec.tsx
#	web/app/components/rag-pipeline/hooks/__tests__/index.spec.ts
#	web/eslint-suppressions.json
This commit is contained in:
yyh
2026-02-13 15:17:52 +08:00
898 changed files with 58772 additions and 34358 deletions

View File

@ -0,0 +1,67 @@
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { AuthCategory } from '../../types'
import AddApiKeyButton from '../add-api-key-button'
let _mockModalOpen = false
vi.mock('../api-key-modal', () => ({
default: ({ onClose, onUpdate }: { onClose: () => void, onUpdate?: () => void }) => {
_mockModalOpen = true
return (
<div data-testid="api-key-modal">
<button data-testid="modal-close" onClick={onClose}>Close</button>
<button data-testid="modal-update" onClick={onUpdate}>Update</button>
</div>
)
},
}))
const defaultPayload = {
category: AuthCategory.tool,
provider: 'test-provider',
}
describe('AddApiKeyButton', () => {
beforeEach(() => {
vi.clearAllMocks()
_mockModalOpen = false
})
afterEach(() => {
cleanup()
})
it('renders button with default text', () => {
render(<AddApiKeyButton pluginPayload={defaultPayload} />)
expect(screen.getByRole('button')).toBeInTheDocument()
})
it('renders button with custom text', () => {
render(<AddApiKeyButton pluginPayload={defaultPayload} buttonText="Add Key" />)
expect(screen.getByText('Add Key')).toBeInTheDocument()
})
it('opens modal when button is clicked', () => {
render(<AddApiKeyButton pluginPayload={defaultPayload} />)
fireEvent.click(screen.getByRole('button'))
expect(screen.getByTestId('api-key-modal')).toBeInTheDocument()
})
it('respects disabled prop', () => {
render(<AddApiKeyButton pluginPayload={defaultPayload} disabled />)
expect(screen.getByRole('button')).toBeDisabled()
})
it('closes modal when onClose is called', () => {
render(<AddApiKeyButton pluginPayload={defaultPayload} />)
fireEvent.click(screen.getByRole('button'))
expect(screen.getByTestId('api-key-modal')).toBeInTheDocument()
fireEvent.click(screen.getByTestId('modal-close'))
expect(screen.queryByTestId('api-key-modal')).not.toBeInTheDocument()
})
it('applies custom button variant', () => {
render(<AddApiKeyButton pluginPayload={defaultPayload} buttonVariant="primary" />)
expect(screen.getByRole('button')).toBeInTheDocument()
})
})

View File

@ -0,0 +1,102 @@
import { fireEvent, render, screen } from '@testing-library/react'
import * as React from 'react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { AuthCategory } from '../../types'
const mockGetPluginOAuthUrl = vi.fn().mockResolvedValue({ authorization_url: 'https://auth.example.com' })
const mockOpenOAuthPopup = vi.fn()
vi.mock('@/hooks/use-i18n', () => ({
useRenderI18nObject: () => (obj: Record<string, string> | string) => typeof obj === 'string' ? obj : obj.en_US || '',
}))
vi.mock('@/hooks/use-oauth', () => ({
openOAuthPopup: (...args: unknown[]) => mockOpenOAuthPopup(...args),
}))
vi.mock('../../hooks/use-credential', () => ({
useGetPluginOAuthUrlHook: () => ({
mutateAsync: mockGetPluginOAuthUrl,
}),
useGetPluginOAuthClientSchemaHook: () => ({
data: {
schema: [],
is_oauth_custom_client_enabled: false,
is_system_oauth_params_exists: true,
client_params: {},
redirect_uri: 'https://redirect.example.com',
},
isLoading: false,
}),
}))
vi.mock('../oauth-client-settings', () => ({
default: ({ onClose }: { onClose: () => void }) => (
<div data-testid="oauth-settings-modal">
<button data-testid="oauth-settings-close" onClick={onClose}>Close</button>
</div>
),
}))
vi.mock('@/app/components/base/form/types', () => ({
FormTypeEnum: { radio: 'radio' },
}))
vi.mock('@/utils/classnames', () => ({
cn: (...args: unknown[]) => args.filter(Boolean).join(' '),
}))
const basePayload = {
category: AuthCategory.tool,
provider: 'test-provider',
}
describe('AddOAuthButton', () => {
let AddOAuthButton: (typeof import('../add-oauth-button'))['default']
beforeEach(async () => {
vi.clearAllMocks()
const mod = await import('../add-oauth-button')
AddOAuthButton = mod.default
})
it('should render OAuth button when configured (system params exist)', () => {
render(<AddOAuthButton pluginPayload={basePayload} buttonText="Use OAuth" />)
expect(screen.getByText('Use OAuth')).toBeInTheDocument()
})
it('should open OAuth settings modal when settings icon clicked', () => {
render(<AddOAuthButton pluginPayload={basePayload} buttonText="Use OAuth" />)
fireEvent.click(screen.getByTestId('oauth-settings-button'))
expect(screen.getByTestId('oauth-settings-modal')).toBeInTheDocument()
})
it('should close OAuth settings modal', () => {
render(<AddOAuthButton pluginPayload={basePayload} buttonText="Use OAuth" />)
fireEvent.click(screen.getByTestId('oauth-settings-button'))
fireEvent.click(screen.getByTestId('oauth-settings-close'))
expect(screen.queryByTestId('oauth-settings-modal')).not.toBeInTheDocument()
})
it('should trigger OAuth flow on main button click', async () => {
render(<AddOAuthButton pluginPayload={basePayload} buttonText="Use OAuth" />)
const button = screen.getByText('Use OAuth').closest('button')
if (button)
fireEvent.click(button)
expect(mockGetPluginOAuthUrl).toHaveBeenCalled()
})
it('should be disabled when disabled prop is true', () => {
render(<AddOAuthButton pluginPayload={basePayload} buttonText="Use OAuth" disabled />)
const button = screen.getByText('Use OAuth').closest('button')
expect(button).toBeDisabled()
})
})

View File

@ -0,0 +1,165 @@
import type { ApiKeyModalProps } from '../api-key-modal'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import * as React from 'react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { AuthCategory } from '../../types'
const mockNotify = vi.fn()
const mockAddPluginCredential = vi.fn().mockResolvedValue({})
const mockUpdatePluginCredential = vi.fn().mockResolvedValue({})
const mockFormValues = { isCheckValidated: true, values: { __name__: 'My Key', api_key: 'sk-123' } }
vi.mock('@/app/components/base/toast', () => ({
useToastContext: () => ({
notify: mockNotify,
}),
}))
vi.mock('../../hooks/use-credential', () => ({
useAddPluginCredentialHook: () => ({
mutateAsync: mockAddPluginCredential,
}),
useGetPluginCredentialSchemaHook: () => ({
data: [
{ name: 'api_key', label: 'API Key', type: 'secret-input', required: true },
],
isLoading: false,
}),
useUpdatePluginCredentialHook: () => ({
mutateAsync: mockUpdatePluginCredential,
}),
}))
vi.mock('../../../readme-panel/entrance', () => ({
ReadmeEntrance: () => <div data-testid="readme-entrance" />,
}))
vi.mock('../../../readme-panel/store', () => ({
ReadmeShowType: { modal: 'modal' },
}))
vi.mock('@/app/components/base/encrypted-bottom', () => ({
EncryptedBottom: () => <div data-testid="encrypted-bottom" />,
}))
vi.mock('@/app/components/base/modal/modal', () => ({
default: ({ children, title, onClose, onConfirm, onExtraButtonClick, showExtraButton, disabled }: {
children: React.ReactNode
title: string
onClose?: () => void
onCancel?: () => void
onConfirm?: () => void
onExtraButtonClick?: () => void
showExtraButton?: boolean
disabled?: boolean
[key: string]: unknown
}) => (
<div data-testid="modal">
<div data-testid="modal-title">{title}</div>
{children}
<button data-testid="modal-confirm" onClick={onConfirm} disabled={disabled}>Confirm</button>
<button data-testid="modal-close" onClick={onClose}>Close</button>
{showExtraButton && <button data-testid="modal-extra" onClick={onExtraButtonClick}>Remove</button>}
</div>
),
}))
vi.mock('@/app/components/base/form/form-scenarios/auth', () => ({
default: React.forwardRef((_props: Record<string, unknown>, ref: React.Ref<unknown>) => {
React.useImperativeHandle(ref, () => ({
getFormValues: () => mockFormValues,
}))
return <div data-testid="auth-form" />
}),
}))
vi.mock('@/app/components/base/form/types', () => ({
FormTypeEnum: { textInput: 'text-input' },
}))
const basePayload = {
category: AuthCategory.tool,
provider: 'test-provider',
}
describe('ApiKeyModal', () => {
let ApiKeyModal: React.FC<ApiKeyModalProps>
beforeEach(async () => {
vi.clearAllMocks()
const mod = await import('../api-key-modal')
ApiKeyModal = mod.default
})
it('should render modal with correct title', () => {
render(<ApiKeyModal pluginPayload={basePayload} />)
expect(screen.getByTestId('modal-title')).toHaveTextContent('plugin.auth.useApiAuth')
})
it('should render auth form when data is loaded', () => {
render(<ApiKeyModal pluginPayload={basePayload} />)
expect(screen.getByTestId('auth-form')).toBeInTheDocument()
})
it('should show remove button when editValues is provided', () => {
render(<ApiKeyModal pluginPayload={basePayload} editValues={{ api_key: 'existing' }} />)
expect(screen.getByTestId('modal-extra')).toBeInTheDocument()
})
it('should not show remove button in add mode', () => {
render(<ApiKeyModal pluginPayload={basePayload} />)
expect(screen.queryByTestId('modal-extra')).not.toBeInTheDocument()
})
it('should call onClose when close button clicked', () => {
const mockOnClose = vi.fn()
render(<ApiKeyModal pluginPayload={basePayload} onClose={mockOnClose} />)
fireEvent.click(screen.getByTestId('modal-close'))
expect(mockOnClose).toHaveBeenCalled()
})
it('should call addPluginCredential on confirm in add mode', async () => {
const mockOnClose = vi.fn()
const mockOnUpdate = vi.fn()
render(<ApiKeyModal pluginPayload={basePayload} onClose={mockOnClose} onUpdate={mockOnUpdate} />)
fireEvent.click(screen.getByTestId('modal-confirm'))
await waitFor(() => {
expect(mockAddPluginCredential).toHaveBeenCalledWith(expect.objectContaining({
type: 'api-key',
name: 'My Key',
}))
})
})
it('should call updatePluginCredential on confirm in edit mode', async () => {
render(<ApiKeyModal pluginPayload={basePayload} editValues={{ api_key: 'existing', __credential_id__: 'cred-1' }} />)
fireEvent.click(screen.getByTestId('modal-confirm'))
await waitFor(() => {
expect(mockUpdatePluginCredential).toHaveBeenCalled()
})
})
it('should call onRemove when remove button clicked', () => {
const mockOnRemove = vi.fn()
render(<ApiKeyModal pluginPayload={basePayload} editValues={{ api_key: 'existing' }} onRemove={mockOnRemove} />)
fireEvent.click(screen.getByTestId('modal-extra'))
expect(mockOnRemove).toHaveBeenCalled()
})
it('should render readme entrance when detail is provided', () => {
const payload = { ...basePayload, detail: { name: 'Test' } as never }
render(<ApiKeyModal pluginPayload={payload} />)
expect(screen.getByTestId('readme-entrance')).toBeInTheDocument()
})
})

View File

@ -1,10 +1,10 @@
import type { ReactNode } from 'react'
import type { PluginPayload } from '../types'
import type { PluginPayload } from '../../types'
import type { FormSchema } from '@/app/components/base/form/types'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { AuthCategory } from '../types'
import { AuthCategory } from '../../types'
// Create a wrapper with QueryClientProvider
const createTestQueryClient = () =>
@ -36,7 +36,7 @@ const mockAddPluginCredential = vi.fn()
const mockUpdatePluginCredential = vi.fn()
const mockGetPluginCredentialSchema = vi.fn()
vi.mock('../hooks/use-credential', () => ({
vi.mock('../../hooks/use-credential', () => ({
useGetPluginOAuthUrlHook: () => ({
mutateAsync: mockGetPluginOAuthUrl,
}),
@ -117,12 +117,12 @@ const createFormSchema = (overrides: Partial<FormSchema> = {}): FormSchema => ({
// ==================== AddApiKeyButton Tests ====================
describe('AddApiKeyButton', () => {
let AddApiKeyButton: typeof import('./add-api-key-button').default
let AddApiKeyButton: typeof import('../add-api-key-button').default
beforeEach(async () => {
vi.clearAllMocks()
mockGetPluginCredentialSchema.mockReturnValue([])
const importedAddApiKeyButton = await import('./add-api-key-button')
const importedAddApiKeyButton = await import('../add-api-key-button')
AddApiKeyButton = importedAddApiKeyButton.default
})
@ -327,7 +327,7 @@ describe('AddApiKeyButton', () => {
describe('Memoization', () => {
it('should be a memoized component', async () => {
const AddApiKeyButtonDefault = (await import('./add-api-key-button')).default
const AddApiKeyButtonDefault = (await import('../add-api-key-button')).default
expect(typeof AddApiKeyButtonDefault).toBe('object')
})
})
@ -335,7 +335,7 @@ describe('AddApiKeyButton', () => {
// ==================== AddOAuthButton Tests ====================
describe('AddOAuthButton', () => {
let AddOAuthButton: typeof import('./add-oauth-button').default
let AddOAuthButton: typeof import('../add-oauth-button').default
beforeEach(async () => {
vi.clearAllMocks()
@ -347,7 +347,7 @@ describe('AddOAuthButton', () => {
redirect_uri: 'https://example.com/callback',
})
mockGetPluginOAuthUrl.mockResolvedValue({ authorization_url: 'https://oauth.example.com/auth' })
const importedAddOAuthButton = await import('./add-oauth-button')
const importedAddOAuthButton = await import('../add-oauth-button')
AddOAuthButton = importedAddOAuthButton.default
})
@ -856,7 +856,7 @@ describe('AddOAuthButton', () => {
// ==================== ApiKeyModal Tests ====================
describe('ApiKeyModal', () => {
let ApiKeyModal: typeof import('./api-key-modal').default
let ApiKeyModal: typeof import('../api-key-modal').default
beforeEach(async () => {
vi.clearAllMocks()
@ -870,7 +870,7 @@ describe('ApiKeyModal', () => {
isCheckValidated: false,
values: {},
})
const importedApiKeyModal = await import('./api-key-modal')
const importedApiKeyModal = await import('../api-key-modal')
ApiKeyModal = importedApiKeyModal.default
})
@ -1272,13 +1272,13 @@ describe('ApiKeyModal', () => {
// ==================== OAuthClientSettings Tests ====================
describe('OAuthClientSettings', () => {
let OAuthClientSettings: typeof import('./oauth-client-settings').default
let OAuthClientSettings: typeof import('../oauth-client-settings').default
beforeEach(async () => {
vi.clearAllMocks()
mockSetPluginOAuthCustomClient.mockResolvedValue({})
mockDeletePluginOAuthCustomClient.mockResolvedValue({})
const importedOAuthClientSettings = await import('./oauth-client-settings')
const importedOAuthClientSettings = await import('../oauth-client-settings')
OAuthClientSettings = importedOAuthClientSettings.default
})
@ -2193,7 +2193,7 @@ describe('OAuthClientSettings', () => {
describe('Memoization', () => {
it('should be a memoized component', async () => {
const OAuthClientSettingsDefault = (await import('./oauth-client-settings')).default
const OAuthClientSettingsDefault = (await import('../oauth-client-settings')).default
expect(typeof OAuthClientSettingsDefault).toBe('object')
})
})
@ -2216,7 +2216,7 @@ describe('Authorize Components Integration', () => {
describe('AddApiKeyButton -> ApiKeyModal Flow', () => {
it('should open ApiKeyModal when AddApiKeyButton is clicked', async () => {
const AddApiKeyButton = (await import('./add-api-key-button')).default
const AddApiKeyButton = (await import('../add-api-key-button')).default
const pluginPayload = createPluginPayload()
render(<AddApiKeyButton pluginPayload={pluginPayload} />, { wrapper: createWrapper() })
@ -2231,7 +2231,7 @@ describe('Authorize Components Integration', () => {
describe('AddOAuthButton -> OAuthClientSettings Flow', () => {
it('should open OAuthClientSettings when setup button is clicked', async () => {
const AddOAuthButton = (await import('./add-oauth-button')).default
const AddOAuthButton = (await import('../add-oauth-button')).default
const pluginPayload = createPluginPayload()
mockGetPluginOAuthClientSchema.mockReturnValue({
schema: [createFormSchema({ name: 'client_id', label: 'Client ID' })],

View File

@ -1,10 +1,10 @@
import type { ReactNode } from 'react'
import type { PluginPayload } from '../types'
import type { PluginPayload } from '../../types'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { AuthCategory } from '../types'
import Authorize from './index'
import { AuthCategory } from '../../types'
import Authorize from '../index'
// Create a wrapper with QueryClientProvider for real component testing
const createTestQueryClient = () =>
@ -29,7 +29,7 @@ const createWrapper = () => {
// Mock API hooks - only mock network-related hooks
const mockGetPluginOAuthClientSchema = vi.fn()
vi.mock('../hooks/use-credential', () => ({
vi.mock('../../hooks/use-credential', () => ({
useGetPluginOAuthUrlHook: () => ({
mutateAsync: vi.fn().mockResolvedValue({ authorization_url: '' }),
}),
@ -96,7 +96,7 @@ describe('Authorize', () => {
it('should render nothing when canOAuth and canApiKey are both false/undefined', () => {
const pluginPayload = createPluginPayload()
const { container } = render(
render(
<Authorize
pluginPayload={pluginPayload}
canOAuth={false}
@ -105,10 +105,7 @@ describe('Authorize', () => {
{ wrapper: createWrapper() },
)
// No buttons should be rendered
expect(screen.queryByRole('button')).not.toBeInTheDocument()
// Container should only have wrapper element
expect(container.querySelector('.flex')).toBeInTheDocument()
})
it('should render only OAuth button when canOAuth is true and canApiKey is false', () => {
@ -225,7 +222,7 @@ describe('Authorize', () => {
// ==================== Props Testing ====================
describe('Props Testing', () => {
describe('theme prop', () => {
it('should render buttons with secondary theme variant when theme is secondary', () => {
it('should render buttons when theme is secondary', () => {
const pluginPayload = createPluginPayload()
render(
@ -239,9 +236,7 @@ describe('Authorize', () => {
)
const buttons = screen.getAllByRole('button')
buttons.forEach((button) => {
expect(button.className).toContain('btn-secondary')
})
expect(buttons).toHaveLength(2)
})
})
@ -327,10 +322,10 @@ describe('Authorize', () => {
expect(screen.getByRole('button')).toBeDisabled()
})
it('should add opacity class when notAllowCustomCredential is true', () => {
it('should disable all buttons when notAllowCustomCredential is true', () => {
const pluginPayload = createPluginPayload()
const { container } = render(
render(
<Authorize
pluginPayload={pluginPayload}
canOAuth={true}
@ -340,8 +335,8 @@ describe('Authorize', () => {
{ wrapper: createWrapper() },
)
const wrappers = container.querySelectorAll('.opacity-50')
expect(wrappers.length).toBe(2) // Both OAuth and API Key wrappers
const buttons = screen.getAllByRole('button')
buttons.forEach(button => expect(button).toBeDisabled())
})
})
})
@ -459,7 +454,7 @@ describe('Authorize', () => {
expect(screen.getAllByRole('button').length).toBe(2)
})
it('should update button variant when theme changes', () => {
it('should change button styling when theme changes', () => {
const pluginPayload = createPluginPayload()
const { rerender } = render(
@ -471,9 +466,7 @@ describe('Authorize', () => {
{ wrapper: createWrapper() },
)
const buttonPrimary = screen.getByRole('button')
// Primary theme with canOAuth=false should have primary variant
expect(buttonPrimary.className).toContain('btn-primary')
const primaryClassName = screen.getByRole('button').className
rerender(
<Authorize
@ -483,7 +476,8 @@ describe('Authorize', () => {
/>,
)
expect(screen.getByRole('button').className).toContain('btn-secondary')
const secondaryClassName = screen.getByRole('button').className
expect(primaryClassName).not.toBe(secondaryClassName)
})
})
@ -568,44 +562,16 @@ describe('Authorize', () => {
// ==================== Component Memoization ====================
describe('Component Memoization', () => {
it('should be a memoized component (exported with memo)', async () => {
const AuthorizeDefault = (await import('./index')).default
const AuthorizeDefault = (await import('../index')).default
expect(AuthorizeDefault).toBeDefined()
// memo wrapped components are React elements with $$typeof
expect(typeof AuthorizeDefault).toBe('object')
})
it('should not re-render wrapper when notAllowCustomCredential stays the same', () => {
const pluginPayload = createPluginPayload()
const onUpdate = vi.fn()
const { rerender, container } = render(
<Authorize
pluginPayload={pluginPayload}
canOAuth={true}
notAllowCustomCredential={false}
onUpdate={onUpdate}
/>,
{ wrapper: createWrapper() },
)
const initialOpacityElements = container.querySelectorAll('.opacity-50').length
rerender(
<Authorize
pluginPayload={pluginPayload}
canOAuth={true}
notAllowCustomCredential={false}
onUpdate={onUpdate}
/>,
)
expect(container.querySelectorAll('.opacity-50').length).toBe(initialOpacityElements)
})
it('should update wrapper when notAllowCustomCredential changes', () => {
it('should reflect notAllowCustomCredential change via button disabled state', () => {
const pluginPayload = createPluginPayload()
const { rerender, container } = render(
const { rerender } = render(
<Authorize
pluginPayload={pluginPayload}
canOAuth={true}
@ -614,7 +580,7 @@ describe('Authorize', () => {
{ wrapper: createWrapper() },
)
expect(container.querySelectorAll('.opacity-50').length).toBe(0)
expect(screen.getByRole('button')).not.toBeDisabled()
rerender(
<Authorize
@ -624,7 +590,7 @@ describe('Authorize', () => {
/>,
)
expect(container.querySelectorAll('.opacity-50').length).toBe(1)
expect(screen.getByRole('button')).toBeDisabled()
})
})

View File

@ -0,0 +1,179 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import * as React from 'react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { AuthCategory } from '../../types'
const mockNotify = vi.fn()
const mockSetPluginOAuthCustomClient = vi.fn().mockResolvedValue({})
const mockDeletePluginOAuthCustomClient = vi.fn().mockResolvedValue({})
const mockInvalidPluginOAuthClientSchema = vi.fn()
const mockFormValues = { isCheckValidated: true, values: { __oauth_client__: 'custom', client_id: 'test-id' } }
vi.mock('@/app/components/base/toast', () => ({
useToastContext: () => ({
notify: mockNotify,
}),
}))
vi.mock('../../hooks/use-credential', () => ({
useSetPluginOAuthCustomClientHook: () => ({
mutateAsync: mockSetPluginOAuthCustomClient,
}),
useDeletePluginOAuthCustomClientHook: () => ({
mutateAsync: mockDeletePluginOAuthCustomClient,
}),
useInvalidPluginOAuthClientSchemaHook: () => mockInvalidPluginOAuthClientSchema,
}))
vi.mock('../../../readme-panel/entrance', () => ({
ReadmeEntrance: () => <div data-testid="readme-entrance" />,
}))
vi.mock('../../../readme-panel/store', () => ({
ReadmeShowType: { modal: 'modal' },
}))
vi.mock('@/app/components/base/modal/modal', () => ({
default: ({ children, title, onClose: _onClose, onConfirm, onCancel, onExtraButtonClick, footerSlot }: {
children: React.ReactNode
title: string
onClose?: () => void
onConfirm?: () => void
onCancel?: () => void
onExtraButtonClick?: () => void
footerSlot?: React.ReactNode
[key: string]: unknown
}) => (
<div data-testid="modal">
<div data-testid="modal-title">{title}</div>
{children}
<button data-testid="modal-confirm" onClick={onConfirm}>Save And Auth</button>
<button data-testid="modal-cancel" onClick={onCancel}>Save Only</button>
<button data-testid="modal-close" onClick={onExtraButtonClick}>Cancel</button>
{!!footerSlot && <div data-testid="footer-slot">{footerSlot}</div>}
</div>
),
}))
vi.mock('@/app/components/base/form/form-scenarios/auth', () => ({
default: React.forwardRef((_props: Record<string, unknown>, ref: React.Ref<unknown>) => {
React.useImperativeHandle(ref, () => ({
getFormValues: () => mockFormValues,
}))
return <div data-testid="auth-form" />
}),
}))
vi.mock('@tanstack/react-form', () => ({
useForm: (config: Record<string, unknown>) => ({
store: { subscribe: vi.fn(), getState: () => ({ values: config.defaultValues || {} }) },
}),
useStore: (_store: unknown, selector: (state: Record<string, unknown>) => unknown) => {
return selector({ values: { __oauth_client__: 'custom' } })
},
}))
const basePayload = {
category: AuthCategory.tool,
provider: 'test-provider',
}
const defaultSchemas = [
{ name: 'client_id', label: 'Client ID', type: 'text-input', required: true },
] as never
describe('OAuthClientSettings', () => {
let OAuthClientSettings: (typeof import('../oauth-client-settings'))['default']
beforeEach(async () => {
vi.clearAllMocks()
const mod = await import('../oauth-client-settings')
OAuthClientSettings = mod.default
})
it('should render modal with correct title', () => {
render(
<OAuthClientSettings
pluginPayload={basePayload}
schemas={defaultSchemas}
/>,
)
expect(screen.getByTestId('modal-title')).toHaveTextContent('plugin.auth.oauthClientSettings')
})
it('should render auth form', () => {
render(
<OAuthClientSettings
pluginPayload={basePayload}
schemas={defaultSchemas}
/>,
)
expect(screen.getByTestId('auth-form')).toBeInTheDocument()
})
it('should call onClose when cancel clicked', () => {
const mockOnClose = vi.fn()
render(
<OAuthClientSettings
pluginPayload={basePayload}
schemas={defaultSchemas}
onClose={mockOnClose}
/>,
)
fireEvent.click(screen.getByTestId('modal-close'))
expect(mockOnClose).toHaveBeenCalled()
})
it('should save settings on save only button click', async () => {
const mockOnClose = vi.fn()
const mockOnUpdate = vi.fn()
render(
<OAuthClientSettings
pluginPayload={basePayload}
schemas={defaultSchemas}
onClose={mockOnClose}
onUpdate={mockOnUpdate}
/>,
)
fireEvent.click(screen.getByTestId('modal-cancel'))
await waitFor(() => {
expect(mockSetPluginOAuthCustomClient).toHaveBeenCalledWith(expect.objectContaining({
enable_oauth_custom_client: true,
}))
})
})
it('should save and authorize on confirm button click', async () => {
const mockOnAuth = vi.fn().mockResolvedValue(undefined)
render(
<OAuthClientSettings
pluginPayload={basePayload}
schemas={defaultSchemas}
onAuth={mockOnAuth}
/>,
)
fireEvent.click(screen.getByTestId('modal-confirm'))
await waitFor(() => {
expect(mockSetPluginOAuthCustomClient).toHaveBeenCalled()
})
})
it('should render readme entrance when detail is provided', () => {
const payload = { ...basePayload, detail: { name: 'Test' } as never }
render(
<OAuthClientSettings
pluginPayload={payload}
schemas={defaultSchemas}
/>,
)
expect(screen.getByTestId('readme-entrance')).toBeInTheDocument()
})
})