mirror of
https://github.com/langgenius/dify.git
synced 2026-05-06 02:18:08 +08:00
Merge branch 'main' into refactor/query-params-nuqs
This commit is contained in:
@ -7,8 +7,12 @@ logs
|
||||
|
||||
# node
|
||||
node_modules
|
||||
dist
|
||||
build
|
||||
coverage
|
||||
.husky
|
||||
.next
|
||||
.pnpm-store
|
||||
|
||||
# vscode
|
||||
.vscode
|
||||
@ -22,3 +26,7 @@ node_modules
|
||||
|
||||
# Jetbrains
|
||||
.idea
|
||||
|
||||
# git
|
||||
.git
|
||||
.gitignore
|
||||
@ -47,6 +47,8 @@ NEXT_PUBLIC_TOP_K_MAX_VALUE=10
|
||||
|
||||
# The maximum number of tokens for segmentation
|
||||
NEXT_PUBLIC_INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH=4000
|
||||
# Used by web/docker/entrypoint.sh to overwrite/export NEXT_PUBLIC_INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH at container startup (Docker only)
|
||||
INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH=4000
|
||||
|
||||
# Maximum loop count in the workflow
|
||||
NEXT_PUBLIC_LOOP_NODE_MAX_COUNT=100
|
||||
|
||||
@ -12,7 +12,8 @@ RUN apk add --no-cache tzdata
|
||||
RUN corepack enable
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
ENV NEXT_PUBLIC_BASE_PATH=""
|
||||
ARG NEXT_PUBLIC_BASE_PATH=""
|
||||
ENV NEXT_PUBLIC_BASE_PATH="$NEXT_PUBLIC_BASE_PATH"
|
||||
|
||||
|
||||
# install packages
|
||||
|
||||
@ -7,19 +7,24 @@ export const jsonObjectWrap = {
|
||||
|
||||
export const jsonConfigPlaceHolder = JSON.stringify(
|
||||
{
|
||||
foo: {
|
||||
type: 'string',
|
||||
},
|
||||
bar: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
sub: {
|
||||
type: 'number',
|
||||
},
|
||||
type: 'object',
|
||||
properties: {
|
||||
foo: {
|
||||
type: 'string',
|
||||
},
|
||||
bar: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
sub: {
|
||||
type: 'number',
|
||||
},
|
||||
},
|
||||
required: [],
|
||||
additionalProperties: true,
|
||||
},
|
||||
required: [],
|
||||
additionalProperties: true,
|
||||
},
|
||||
required: [],
|
||||
additionalProperties: true,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
|
||||
@ -28,7 +28,7 @@ import { checkKeys, getNewVarInWorkflow, replaceSpaceWithUnderscoreInVarNameInpu
|
||||
import ConfigSelect from '../config-select'
|
||||
import ConfigString from '../config-string'
|
||||
import ModalFoot from '../modal-foot'
|
||||
import { jsonConfigPlaceHolder, jsonObjectWrap } from './config'
|
||||
import { jsonConfigPlaceHolder } from './config'
|
||||
import Field from './field'
|
||||
import TypeSelector from './type-select'
|
||||
|
||||
@ -78,13 +78,12 @@ const ConfigModal: FC<IConfigModalProps> = ({
|
||||
const modalRef = useRef<HTMLDivElement>(null)
|
||||
const appDetail = useAppStore(state => state.appDetail)
|
||||
const isBasicApp = appDetail?.mode !== AppModeEnum.ADVANCED_CHAT && appDetail?.mode !== AppModeEnum.WORKFLOW
|
||||
const isSupportJSON = false
|
||||
const jsonSchemaStr = useMemo(() => {
|
||||
const isJsonObject = type === InputVarType.jsonObject
|
||||
if (!isJsonObject || !tempPayload.json_schema)
|
||||
return ''
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(tempPayload.json_schema).properties, null, 2)
|
||||
return JSON.stringify(JSON.parse(tempPayload.json_schema), null, 2)
|
||||
}
|
||||
catch {
|
||||
return ''
|
||||
@ -129,13 +128,14 @@ const ConfigModal: FC<IConfigModalProps> = ({
|
||||
}, [])
|
||||
|
||||
const handleJSONSchemaChange = useCallback((value: string) => {
|
||||
const isEmpty = value == null || value.trim() === ''
|
||||
if (isEmpty) {
|
||||
handlePayloadChange('json_schema')(undefined)
|
||||
return null
|
||||
}
|
||||
try {
|
||||
const v = JSON.parse(value)
|
||||
const res = {
|
||||
...jsonObjectWrap,
|
||||
properties: v,
|
||||
}
|
||||
handlePayloadChange('json_schema')(JSON.stringify(res, null, 2))
|
||||
handlePayloadChange('json_schema')(JSON.stringify(v, null, 2))
|
||||
}
|
||||
catch {
|
||||
return null
|
||||
@ -175,7 +175,7 @@ const ConfigModal: FC<IConfigModalProps> = ({
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...((!isBasicApp && isSupportJSON)
|
||||
...((!isBasicApp)
|
||||
? [{
|
||||
name: t('variableConfig.json', { ns: 'appDebug' }),
|
||||
value: InputVarType.jsonObject,
|
||||
@ -233,7 +233,28 @@ const ConfigModal: FC<IConfigModalProps> = ({
|
||||
|
||||
const checkboxDefaultSelectValue = useMemo(() => getCheckboxDefaultSelectValue(tempPayload.default), [tempPayload.default])
|
||||
|
||||
const isJsonSchemaEmpty = (value: InputVar['json_schema']) => {
|
||||
if (value === null || value === undefined) {
|
||||
return true
|
||||
}
|
||||
if (typeof value !== 'string') {
|
||||
return false
|
||||
}
|
||||
const trimmed = value.trim()
|
||||
return trimmed === ''
|
||||
}
|
||||
|
||||
const handleConfirm = () => {
|
||||
const jsonSchemaValue = tempPayload.json_schema
|
||||
const isSchemaEmpty = isJsonSchemaEmpty(jsonSchemaValue)
|
||||
const normalizedJsonSchema = isSchemaEmpty ? undefined : jsonSchemaValue
|
||||
|
||||
// if the input type is jsonObject and the schema is empty as determined by `isJsonSchemaEmpty`,
|
||||
// remove the `json_schema` field from the payload by setting its value to `undefined`.
|
||||
const payloadToSave = tempPayload.type === InputVarType.jsonObject && isSchemaEmpty
|
||||
? { ...tempPayload, json_schema: undefined }
|
||||
: tempPayload
|
||||
|
||||
const moreInfo = tempPayload.variable === payload?.variable
|
||||
? undefined
|
||||
: {
|
||||
@ -250,7 +271,7 @@ const ConfigModal: FC<IConfigModalProps> = ({
|
||||
return
|
||||
}
|
||||
if (isStringInput || type === InputVarType.number) {
|
||||
onConfirm(tempPayload, moreInfo)
|
||||
onConfirm(payloadToSave, moreInfo)
|
||||
}
|
||||
else if (type === InputVarType.select) {
|
||||
if (options?.length === 0) {
|
||||
@ -270,7 +291,7 @@ const ConfigModal: FC<IConfigModalProps> = ({
|
||||
Toast.notify({ type: 'error', message: t('variableConfig.errorMsg.optionRepeat', { ns: 'appDebug' }) })
|
||||
return
|
||||
}
|
||||
onConfirm(tempPayload, moreInfo)
|
||||
onConfirm(payloadToSave, moreInfo)
|
||||
}
|
||||
else if ([InputVarType.singleFile, InputVarType.multiFiles].includes(type)) {
|
||||
if (tempPayload.allowed_file_types?.length === 0) {
|
||||
@ -283,10 +304,26 @@ const ConfigModal: FC<IConfigModalProps> = ({
|
||||
Toast.notify({ type: 'error', message: errorMessages })
|
||||
return
|
||||
}
|
||||
onConfirm(tempPayload, moreInfo)
|
||||
onConfirm(payloadToSave, moreInfo)
|
||||
}
|
||||
else if (type === InputVarType.jsonObject) {
|
||||
if (!isSchemaEmpty && typeof normalizedJsonSchema === 'string') {
|
||||
try {
|
||||
const schema = JSON.parse(normalizedJsonSchema)
|
||||
if (schema?.type !== 'object') {
|
||||
Toast.notify({ type: 'error', message: t('variableConfig.errorMsg.jsonSchemaMustBeObject', { ns: 'appDebug' }) })
|
||||
return
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Toast.notify({ type: 'error', message: t('variableConfig.errorMsg.jsonSchemaInvalid', { ns: 'appDebug' }) })
|
||||
return
|
||||
}
|
||||
}
|
||||
onConfirm(payloadToSave, moreInfo)
|
||||
}
|
||||
else {
|
||||
onConfirm(tempPayload, moreInfo)
|
||||
onConfirm(payloadToSave, moreInfo)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -5,15 +5,6 @@ import * as React from 'react'
|
||||
import { AgentStrategy } from '@/types/app'
|
||||
import AgentSettingButton from './agent-setting-button'
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string, options?: { ns?: string }) => {
|
||||
const prefix = options?.ns ? `${options.ns}.` : ''
|
||||
return `${prefix}${key}`
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
let latestAgentSettingProps: any
|
||||
vi.mock('./agent/agent-setting', () => ({
|
||||
default: (props: any) => {
|
||||
|
||||
@ -15,15 +15,6 @@ vi.mock('use-context-selector', async (importOriginal) => {
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string, options?: { ns?: string }) => {
|
||||
const prefix = options?.ns ? `${options.ns}.` : ''
|
||||
return `${prefix}${key}`
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
const mockUseFeatures = vi.fn()
|
||||
const mockUseFeaturesStore = vi.fn()
|
||||
vi.mock('@/app/components/base/features/hooks', () => ({
|
||||
|
||||
@ -93,7 +93,6 @@ function createMockProviderContext(overrides: Partial<ProviderContextState> = {}
|
||||
provider: 'openai',
|
||||
label: { en_US: 'OpenAI', zh_Hans: 'OpenAI' },
|
||||
icon_small: { en_US: 'icon', zh_Hans: 'icon' },
|
||||
icon_large: { en_US: 'icon', zh_Hans: 'icon' },
|
||||
status: ModelStatusEnum.active,
|
||||
models: [
|
||||
{
|
||||
@ -711,7 +710,6 @@ describe('DebugWithSingleModel', () => {
|
||||
provider: 'openai',
|
||||
label: { en_US: 'OpenAI', zh_Hans: 'OpenAI' },
|
||||
icon_small: { en_US: 'icon', zh_Hans: 'icon' },
|
||||
icon_large: { en_US: 'icon', zh_Hans: 'icon' },
|
||||
status: ModelStatusEnum.active,
|
||||
models: [
|
||||
{
|
||||
@ -742,7 +740,6 @@ describe('DebugWithSingleModel', () => {
|
||||
provider: 'different-provider',
|
||||
label: { en_US: 'Different Provider', zh_Hans: '不同提供商' },
|
||||
icon_small: { en_US: 'icon', zh_Hans: 'icon' },
|
||||
icon_large: { en_US: 'icon', zh_Hans: 'icon' },
|
||||
status: ModelStatusEnum.active,
|
||||
models: [],
|
||||
},
|
||||
@ -925,7 +922,6 @@ describe('DebugWithSingleModel', () => {
|
||||
provider: 'openai',
|
||||
label: { en_US: 'OpenAI', zh_Hans: 'OpenAI' },
|
||||
icon_small: { en_US: 'icon', zh_Hans: 'icon' },
|
||||
icon_large: { en_US: 'icon', zh_Hans: 'icon' },
|
||||
status: ModelStatusEnum.active,
|
||||
models: [
|
||||
{
|
||||
@ -975,7 +971,6 @@ describe('DebugWithSingleModel', () => {
|
||||
provider: 'openai',
|
||||
label: { en_US: 'OpenAI', zh_Hans: 'OpenAI' },
|
||||
icon_small: { en_US: 'icon', zh_Hans: 'icon' },
|
||||
icon_large: { en_US: 'icon', zh_Hans: 'icon' },
|
||||
status: ModelStatusEnum.active,
|
||||
models: [
|
||||
{
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -1,20 +0,0 @@
|
||||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import type { IconData } from '@/app/components/base/icons/IconBase'
|
||||
import * as React from 'react'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import data from './OpenaiBlue.json'
|
||||
|
||||
const Icon = (
|
||||
{
|
||||
ref,
|
||||
...props
|
||||
}: React.SVGProps<SVGSVGElement> & {
|
||||
ref?: React.RefObject<React.RefObject<HTMLOrSVGElement>>
|
||||
},
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />
|
||||
|
||||
Icon.displayName = 'OpenaiBlue'
|
||||
|
||||
export default Icon
|
||||
File diff suppressed because one or more lines are too long
@ -1,20 +0,0 @@
|
||||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import type { IconData } from '@/app/components/base/icons/IconBase'
|
||||
import * as React from 'react'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import data from './OpenaiTeal.json'
|
||||
|
||||
const Icon = (
|
||||
{
|
||||
ref,
|
||||
...props
|
||||
}: React.SVGProps<SVGSVGElement> & {
|
||||
ref?: React.RefObject<React.RefObject<HTMLOrSVGElement>>
|
||||
},
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />
|
||||
|
||||
Icon.displayName = 'OpenaiTeal'
|
||||
|
||||
export default Icon
|
||||
File diff suppressed because one or more lines are too long
@ -1,20 +0,0 @@
|
||||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import type { IconData } from '@/app/components/base/icons/IconBase'
|
||||
import * as React from 'react'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import data from './OpenaiViolet.json'
|
||||
|
||||
const Icon = (
|
||||
{
|
||||
ref,
|
||||
...props
|
||||
}: React.SVGProps<SVGSVGElement> & {
|
||||
ref?: React.RefObject<React.RefObject<HTMLOrSVGElement>>
|
||||
},
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />
|
||||
|
||||
Icon.displayName = 'OpenaiViolet'
|
||||
|
||||
export default Icon
|
||||
@ -26,12 +26,9 @@ export { default as Localai } from './Localai'
|
||||
export { default as LocalaiText } from './LocalaiText'
|
||||
export { default as Microsoft } from './Microsoft'
|
||||
export { default as OpenaiBlack } from './OpenaiBlack'
|
||||
export { default as OpenaiBlue } from './OpenaiBlue'
|
||||
export { default as OpenaiGreen } from './OpenaiGreen'
|
||||
export { default as OpenaiTeal } from './OpenaiTeal'
|
||||
export { default as OpenaiText } from './OpenaiText'
|
||||
export { default as OpenaiTransparent } from './OpenaiTransparent'
|
||||
export { default as OpenaiViolet } from './OpenaiViolet'
|
||||
export { default as OpenaiYellow } from './OpenaiYellow'
|
||||
export { default as Openllm } from './Openllm'
|
||||
export { default as OpenllmText } from './OpenllmText'
|
||||
|
||||
@ -1,26 +1,14 @@
|
||||
import { cleanup, fireEvent, render } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { createReactI18nextMock } from '@/test/i18n-mock'
|
||||
import InlineDeleteConfirm from './index'
|
||||
|
||||
// Mock react-i18next
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string, defaultValueOrOptions?: string | { ns?: string }) => {
|
||||
const translations: Record<string, string> = {
|
||||
'operation.deleteConfirmTitle': 'Delete?',
|
||||
'operation.yes': 'Yes',
|
||||
'operation.no': 'No',
|
||||
'operation.confirmAction': 'Please confirm your action.',
|
||||
}
|
||||
if (translations[key])
|
||||
return translations[key]
|
||||
// Handle case where second arg is default value string
|
||||
if (typeof defaultValueOrOptions === 'string')
|
||||
return defaultValueOrOptions
|
||||
const prefix = defaultValueOrOptions?.ns ? `${defaultValueOrOptions.ns}.` : ''
|
||||
return `${prefix}${key}`
|
||||
},
|
||||
}),
|
||||
// Mock react-i18next with custom translations for test assertions
|
||||
vi.mock('react-i18next', () => createReactI18nextMock({
|
||||
'operation.deleteConfirmTitle': 'Delete?',
|
||||
'operation.yes': 'Yes',
|
||||
'operation.no': 'No',
|
||||
'operation.confirmAction': 'Please confirm your action.',
|
||||
}))
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { createReactI18nextMock } from '@/test/i18n-mock'
|
||||
import InputWithCopy from './index'
|
||||
|
||||
// Create a mock function that we can track using vi.hoisted
|
||||
@ -10,22 +11,12 @@ vi.mock('copy-to-clipboard', () => ({
|
||||
default: mockCopyToClipboard,
|
||||
}))
|
||||
|
||||
// Mock the i18n hook
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string, options?: { ns?: string }) => {
|
||||
const translations: Record<string, string> = {
|
||||
'operation.copy': 'Copy',
|
||||
'operation.copied': 'Copied',
|
||||
'overview.appInfo.embedded.copy': 'Copy',
|
||||
'overview.appInfo.embedded.copied': 'Copied',
|
||||
}
|
||||
if (translations[key])
|
||||
return translations[key]
|
||||
const prefix = options?.ns ? `${options.ns}.` : ''
|
||||
return `${prefix}${key}`
|
||||
},
|
||||
}),
|
||||
// Mock the i18n hook with custom translations for test assertions
|
||||
vi.mock('react-i18next', () => createReactI18nextMock({
|
||||
'operation.copy': 'Copy',
|
||||
'operation.copied': 'Copied',
|
||||
'overview.appInfo.embedded.copy': 'Copy',
|
||||
'overview.appInfo.embedded.copied': 'Copied',
|
||||
}))
|
||||
|
||||
// Mock es-toolkit/compat debounce
|
||||
|
||||
@ -1,21 +1,12 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { createReactI18nextMock } from '@/test/i18n-mock'
|
||||
import Input, { inputVariants } from './index'
|
||||
|
||||
// Mock the i18n hook
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string, options?: { ns?: string }) => {
|
||||
const translations: Record<string, string> = {
|
||||
'operation.search': 'Search',
|
||||
'placeholder.input': 'Please input',
|
||||
}
|
||||
if (translations[key])
|
||||
return translations[key]
|
||||
const prefix = options?.ns ? `${options.ns}.` : ''
|
||||
return `${prefix}${key}`
|
||||
},
|
||||
}),
|
||||
// Mock the i18n hook with custom translations for test assertions
|
||||
vi.mock('react-i18next', () => createReactI18nextMock({
|
||||
'operation.search': 'Search',
|
||||
'placeholder.input': 'Please input',
|
||||
}))
|
||||
|
||||
describe('Input component', () => {
|
||||
|
||||
248
web/app/components/base/markdown-blocks/think-block.spec.tsx
Normal file
248
web/app/components/base/markdown-blocks/think-block.spec.tsx
Normal file
@ -0,0 +1,248 @@
|
||||
import { act, render, screen } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ChatContextProvider } from '@/app/components/base/chat/chat/context'
|
||||
import ThinkBlock from './think-block'
|
||||
|
||||
// Mock react-i18next
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => {
|
||||
const translations: Record<string, string> = {
|
||||
'chat.thinking': 'Thinking...',
|
||||
'chat.thought': 'Thought',
|
||||
}
|
||||
return translations[key] || key
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
// Helper to wrap component with ChatContextProvider
|
||||
const renderWithContext = (
|
||||
children: React.ReactNode,
|
||||
isResponding: boolean = true,
|
||||
) => {
|
||||
return render(
|
||||
<ChatContextProvider
|
||||
config={undefined}
|
||||
isResponding={isResponding}
|
||||
chatList={[]}
|
||||
showPromptLog={false}
|
||||
questionIcon={undefined}
|
||||
answerIcon={undefined}
|
||||
onSend={undefined}
|
||||
onRegenerate={undefined}
|
||||
onAnnotationEdited={undefined}
|
||||
onAnnotationAdded={undefined}
|
||||
onAnnotationRemoved={undefined}
|
||||
onFeedback={undefined}
|
||||
>
|
||||
{children}
|
||||
</ChatContextProvider>,
|
||||
)
|
||||
}
|
||||
|
||||
describe('ThinkBlock', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.useFakeTimers()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render regular details element when data-think is false', () => {
|
||||
render(
|
||||
<ThinkBlock data-think={false}>
|
||||
<p>Regular content</p>
|
||||
</ThinkBlock>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('Regular content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render think block with thinking state when data-think is true', () => {
|
||||
renderWithContext(
|
||||
<ThinkBlock data-think={true}>
|
||||
<p>Thinking content</p>
|
||||
</ThinkBlock>,
|
||||
true,
|
||||
)
|
||||
|
||||
expect(screen.getByText(/Thinking\.\.\./)).toBeInTheDocument()
|
||||
expect(screen.getByText('Thinking content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render thought state when content has ENDTHINKFLAG', () => {
|
||||
renderWithContext(
|
||||
<ThinkBlock data-think={true}>
|
||||
<p>Completed thinking[ENDTHINKFLAG]</p>
|
||||
</ThinkBlock>,
|
||||
true,
|
||||
)
|
||||
|
||||
expect(screen.getByText(/Thought/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Timer behavior', () => {
|
||||
it('should update elapsed time while thinking', () => {
|
||||
renderWithContext(
|
||||
<ThinkBlock data-think={true}>
|
||||
<p>Thinking...</p>
|
||||
</ThinkBlock>,
|
||||
true,
|
||||
)
|
||||
|
||||
// Initial state should show 0.0s
|
||||
expect(screen.getByText(/\(0\.0s\)/)).toBeInTheDocument()
|
||||
|
||||
// Advance timer by 500ms and run pending timers
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(500)
|
||||
})
|
||||
|
||||
// Should show approximately 0.5s
|
||||
expect(screen.getByText(/\(0\.5s\)/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should stop timer when isResponding becomes false', () => {
|
||||
const { rerender } = render(
|
||||
<ChatContextProvider
|
||||
config={undefined}
|
||||
isResponding={true}
|
||||
chatList={[]}
|
||||
showPromptLog={false}
|
||||
questionIcon={undefined}
|
||||
answerIcon={undefined}
|
||||
onSend={undefined}
|
||||
onRegenerate={undefined}
|
||||
onAnnotationEdited={undefined}
|
||||
onAnnotationAdded={undefined}
|
||||
onAnnotationRemoved={undefined}
|
||||
onFeedback={undefined}
|
||||
>
|
||||
<ThinkBlock data-think={true}>
|
||||
<p>Thinking content</p>
|
||||
</ThinkBlock>
|
||||
</ChatContextProvider>,
|
||||
)
|
||||
|
||||
// Verify initial thinking state
|
||||
expect(screen.getByText(/Thinking\.\.\./)).toBeInTheDocument()
|
||||
|
||||
// Advance timer
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(1000)
|
||||
})
|
||||
|
||||
// Simulate user clicking stop (isResponding becomes false)
|
||||
rerender(
|
||||
<ChatContextProvider
|
||||
config={undefined}
|
||||
isResponding={false}
|
||||
chatList={[]}
|
||||
showPromptLog={false}
|
||||
questionIcon={undefined}
|
||||
answerIcon={undefined}
|
||||
onSend={undefined}
|
||||
onRegenerate={undefined}
|
||||
onAnnotationEdited={undefined}
|
||||
onAnnotationAdded={undefined}
|
||||
onAnnotationRemoved={undefined}
|
||||
onFeedback={undefined}
|
||||
>
|
||||
<ThinkBlock data-think={true}>
|
||||
<p>Thinking content</p>
|
||||
</ThinkBlock>
|
||||
</ChatContextProvider>,
|
||||
)
|
||||
|
||||
// Should now show "Thought" instead of "Thinking..."
|
||||
expect(screen.getByText(/Thought/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should NOT stop timer when isResponding is undefined (outside ChatContextProvider)', () => {
|
||||
// Render without ChatContextProvider
|
||||
render(
|
||||
<ThinkBlock data-think={true}>
|
||||
<p>Content without ENDTHINKFLAG</p>
|
||||
</ThinkBlock>,
|
||||
)
|
||||
|
||||
// Initial state should show "Thinking..."
|
||||
expect(screen.getByText(/Thinking\.\.\./)).toBeInTheDocument()
|
||||
|
||||
// Advance timer
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(2000)
|
||||
})
|
||||
|
||||
// Timer should still be running (showing "Thinking..." not "Thought")
|
||||
expect(screen.getByText(/Thinking\.\.\./)).toBeInTheDocument()
|
||||
expect(screen.getByText(/\(2\.0s\)/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('ENDTHINKFLAG handling', () => {
|
||||
it('should remove ENDTHINKFLAG from displayed content', () => {
|
||||
renderWithContext(
|
||||
<ThinkBlock data-think={true}>
|
||||
<p>Content[ENDTHINKFLAG]</p>
|
||||
</ThinkBlock>,
|
||||
true,
|
||||
)
|
||||
|
||||
expect(screen.getByText('Content')).toBeInTheDocument()
|
||||
expect(screen.queryByText('[ENDTHINKFLAG]')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should detect ENDTHINKFLAG in nested children', () => {
|
||||
renderWithContext(
|
||||
<ThinkBlock data-think={true}>
|
||||
<div>
|
||||
<span>Nested content[ENDTHINKFLAG]</span>
|
||||
</div>
|
||||
</ThinkBlock>,
|
||||
true,
|
||||
)
|
||||
|
||||
// Should show "Thought" since ENDTHINKFLAG is present
|
||||
expect(screen.getByText(/Thought/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should detect ENDTHINKFLAG in array children', () => {
|
||||
renderWithContext(
|
||||
<ThinkBlock data-think={true}>
|
||||
{['Part 1', 'Part 2[ENDTHINKFLAG]']}
|
||||
</ThinkBlock>,
|
||||
true,
|
||||
)
|
||||
|
||||
expect(screen.getByText(/Thought/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge cases', () => {
|
||||
it('should handle empty children', () => {
|
||||
renderWithContext(
|
||||
<ThinkBlock data-think={true}></ThinkBlock>,
|
||||
true,
|
||||
)
|
||||
|
||||
expect(screen.getByText(/Thinking\.\.\./)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle null children gracefully', () => {
|
||||
renderWithContext(
|
||||
<ThinkBlock data-think={true}>
|
||||
{null}
|
||||
</ThinkBlock>,
|
||||
true,
|
||||
)
|
||||
|
||||
expect(screen.getByText(/Thinking\.\.\./)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -59,7 +59,11 @@ const useThinkTimer = (children: any) => {
|
||||
}, [startTime, isComplete])
|
||||
|
||||
useEffect(() => {
|
||||
if (hasEndThink(children) || !isResponding)
|
||||
// Stop timer when:
|
||||
// 1. Content has [ENDTHINKFLAG] marker (normal completion)
|
||||
// 2. isResponding is explicitly false (user clicked stop button)
|
||||
// Note: Don't stop when isResponding is undefined (component used outside ChatContextProvider)
|
||||
if (hasEndThink(children) || isResponding === false)
|
||||
setIsComplete(true)
|
||||
}, [children, isResponding])
|
||||
|
||||
|
||||
@ -3,8 +3,6 @@ import * as React from 'react'
|
||||
import { CategoryEnum } from '.'
|
||||
import Footer from './footer'
|
||||
|
||||
let mockTranslations: Record<string, string> = {}
|
||||
|
||||
vi.mock('next/link', () => ({
|
||||
default: ({ children, href, className, target }: { children: React.ReactNode, href: string, className?: string, target?: string }) => (
|
||||
<a href={href} className={className} target={target} data-testid="pricing-link">
|
||||
@ -13,25 +11,9 @@ vi.mock('next/link', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('react-i18next', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('react-i18next')>()
|
||||
return {
|
||||
...actual,
|
||||
useTranslation: () => ({
|
||||
t: (key: string, options?: { ns?: string }) => {
|
||||
if (mockTranslations[key])
|
||||
return mockTranslations[key]
|
||||
const prefix = options?.ns ? `${options.ns}.` : ''
|
||||
return `${prefix}${key}`
|
||||
},
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
describe('Footer', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockTranslations = {}
|
||||
})
|
||||
|
||||
// Rendering behavior
|
||||
|
||||
@ -18,16 +18,6 @@ const IndexingTypeValues = {
|
||||
// Mock External Dependencies
|
||||
// ==========================================
|
||||
|
||||
// Mock react-i18next (handled by global mock in web/vitest.setup.ts but we override for custom messages)
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string, options?: { ns?: string }) => {
|
||||
const prefix = options?.ns ? `${options.ns}.` : ''
|
||||
return `${prefix}${key}`
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock next/link
|
||||
vi.mock('next/link', () => {
|
||||
return function MockLink({ children, href }: { children: React.ReactNode, href: string }) {
|
||||
|
||||
@ -9,16 +9,6 @@ import Processing from './index'
|
||||
// Mock External Dependencies
|
||||
// ==========================================
|
||||
|
||||
// Mock react-i18next (handled by global mock in web/vitest.setup.ts but we override for custom messages)
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string, options?: { ns?: string }) => {
|
||||
const prefix = options?.ns ? `${options.ns}.` : ''
|
||||
return `${prefix}${key}`
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock useDocLink - returns a function that generates doc URLs
|
||||
// Strips leading slash from path to match actual implementation behavior
|
||||
vi.mock('@/context/i18n', () => ({
|
||||
|
||||
@ -110,7 +110,7 @@ const GotoAnything: FC<Props> = ({
|
||||
isWorkflowPage,
|
||||
isRagPipelinePage,
|
||||
defaultLocale,
|
||||
Object.keys(Actions).sort().join(','),
|
||||
Actions,
|
||||
],
|
||||
queryFn: async () => {
|
||||
const query = searchQueryDebouncedValue.toLowerCase()
|
||||
|
||||
@ -218,7 +218,6 @@ export type ModelProvider = {
|
||||
}
|
||||
icon_small: TypeWithI18N
|
||||
icon_small_dark?: TypeWithI18N
|
||||
icon_large: TypeWithI18N
|
||||
background?: string
|
||||
supported_model_types: ModelTypeEnum[]
|
||||
configurate_methods: ConfigurationMethodEnum[]
|
||||
@ -254,7 +253,6 @@ export type ModelProvider = {
|
||||
|
||||
export type Model = {
|
||||
provider: string
|
||||
icon_large: TypeWithI18N
|
||||
icon_small: TypeWithI18N
|
||||
icon_small_dark?: TypeWithI18N
|
||||
label: TypeWithI18N
|
||||
@ -267,7 +265,6 @@ export type DefaultModelResponse = {
|
||||
model_type: ModelTypeEnum
|
||||
provider: {
|
||||
provider: string
|
||||
icon_large: TypeWithI18N
|
||||
icon_small: TypeWithI18N
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ import type {
|
||||
Model,
|
||||
ModelProvider,
|
||||
} from '../declarations'
|
||||
import { OpenaiBlue, OpenaiTeal, OpenaiViolet, OpenaiYellow } from '@/app/components/base/icons/src/public/llm'
|
||||
import { OpenaiYellow } from '@/app/components/base/icons/src/public/llm'
|
||||
import { Group } from '@/app/components/base/icons/src/vender/other'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { renderI18nObject } from '@/i18n-config'
|
||||
@ -29,12 +29,6 @@ const ModelIcon: FC<ModelIconProps> = ({
|
||||
const language = useLanguage()
|
||||
if (provider?.provider && ['openai', 'langgenius/openai/openai'].includes(provider.provider) && modelName?.startsWith('o'))
|
||||
return <div className="flex items-center justify-center"><OpenaiYellow className={cn('h-5 w-5', className)} /></div>
|
||||
if (provider?.provider && ['openai', 'langgenius/openai/openai'].includes(provider.provider) && modelName?.includes('gpt-4.1'))
|
||||
return <div className="flex items-center justify-center"><OpenaiTeal className={cn('h-5 w-5', className)} /></div>
|
||||
if (provider?.provider && ['openai', 'langgenius/openai/openai'].includes(provider.provider) && modelName?.includes('gpt-4o'))
|
||||
return <div className="flex items-center justify-center"><OpenaiBlue className={cn('h-5 w-5', className)} /></div>
|
||||
if (provider?.provider && ['openai', 'langgenius/openai/openai'].includes(provider.provider) && modelName?.startsWith('gpt-4'))
|
||||
return <div className="flex items-center justify-center"><OpenaiViolet className={cn('h-5 w-5', className)} /></div>
|
||||
|
||||
if (provider?.icon_small) {
|
||||
return (
|
||||
|
||||
@ -21,33 +21,6 @@ import Card from './index'
|
||||
// Mock External Dependencies Only
|
||||
// ================================
|
||||
|
||||
// Mock react-i18next (translation hook)
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock useMixedTranslation hook
|
||||
vi.mock('../marketplace/hooks', () => ({
|
||||
useMixedTranslation: (_locale?: string) => ({
|
||||
t: (key: string, options?: { ns?: string }) => {
|
||||
const fullKey = options?.ns ? `${options.ns}.${key}` : key
|
||||
const translations: Record<string, string> = {
|
||||
'plugin.marketplace.partnerTip': 'Partner plugin',
|
||||
'plugin.marketplace.verifiedTip': 'Verified plugin',
|
||||
'plugin.installModal.installWarning': 'Install warning message',
|
||||
}
|
||||
return translations[fullKey] || key
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock useGetLanguage context
|
||||
vi.mock('@/context/i18n', () => ({
|
||||
useGetLanguage: () => 'en-US',
|
||||
}))
|
||||
|
||||
// Mock useTheme hook
|
||||
vi.mock('@/hooks/use-theme', () => ({
|
||||
default: () => ({ theme: 'light' }),
|
||||
|
||||
@ -64,26 +64,20 @@ vi.mock('@/context/app-context', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string, options?: { ns?: string } & Record<string, unknown>) => {
|
||||
// Build full key with namespace prefix if provided
|
||||
const fullKey = options?.ns ? `${options.ns}.${key}` : key
|
||||
// Handle interpolation params (excluding ns)
|
||||
const { ns: _ns, ...params } = options || {}
|
||||
if (Object.keys(params).length > 0) {
|
||||
return `${fullKey}:${JSON.stringify(params)}`
|
||||
}
|
||||
return fullKey
|
||||
},
|
||||
}),
|
||||
Trans: ({ i18nKey, components }: { i18nKey: string, components?: Record<string, React.ReactNode> }) => (
|
||||
<span data-testid="trans">
|
||||
{i18nKey}
|
||||
{components?.trustSource}
|
||||
</span>
|
||||
),
|
||||
}))
|
||||
vi.mock('react-i18next', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('react-i18next')>()
|
||||
const { createReactI18nextMock } = await import('@/test/i18n-mock')
|
||||
return {
|
||||
...actual,
|
||||
...createReactI18nextMock(),
|
||||
Trans: ({ i18nKey, components }: { i18nKey: string, components?: Record<string, React.ReactNode> }) => (
|
||||
<span data-testid="trans">
|
||||
{i18nKey}
|
||||
{components?.trustSource}
|
||||
</span>
|
||||
),
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('../../../card', () => ({
|
||||
default: ({ payload, titleLeft }: {
|
||||
|
||||
@ -48,21 +48,6 @@ vi.mock('@/service/plugins', () => ({
|
||||
uploadFile: (...args: unknown[]) => mockUploadFile(...args),
|
||||
}))
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string, options?: { ns?: string } & Record<string, unknown>) => {
|
||||
// Build full key with namespace prefix if provided
|
||||
const fullKey = options?.ns ? `${options.ns}.${key}` : key
|
||||
// Handle interpolation params (excluding ns)
|
||||
const { ns: _ns, ...params } = options || {}
|
||||
if (Object.keys(params).length > 0) {
|
||||
return `${fullKey}:${JSON.stringify(params)}`
|
||||
}
|
||||
return fullKey
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../../../card', () => ({
|
||||
default: ({ payload, isLoading, loadingFileName }: {
|
||||
payload: { name: string }
|
||||
|
||||
@ -27,17 +27,17 @@ import {
|
||||
// Mock External Dependencies Only
|
||||
// ================================
|
||||
|
||||
// Mock react-i18next
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock i18next-config
|
||||
vi.mock('@/i18n-config/i18next-config', () => ({
|
||||
default: {
|
||||
getFixedT: (_locale: string) => (key: string) => key,
|
||||
getFixedT: (_locale: string) => (key: string, options?: Record<string, unknown>) => {
|
||||
if (options && options.ns) {
|
||||
return `${options.ns}.${key}`
|
||||
}
|
||||
else {
|
||||
return key
|
||||
}
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
@ -617,8 +617,8 @@ describe('hooks', () => {
|
||||
it('should return translation key when no translation found', () => {
|
||||
const { result } = renderHook(() => useMixedTranslation())
|
||||
|
||||
// The mock returns key as-is
|
||||
expect(result.current.t('category.all', { ns: 'plugin' })).toBe('category.all')
|
||||
// The global mock returns key with namespace prefix
|
||||
expect(result.current.t('category.all', { ns: 'plugin' })).toBe('plugin.category.all')
|
||||
})
|
||||
|
||||
it('should use locale from outer when provided', () => {
|
||||
@ -638,8 +638,8 @@ describe('hooks', () => {
|
||||
|
||||
it('should use getFixedT when localeFromOuter is provided', () => {
|
||||
const { result } = renderHook(() => useMixedTranslation('fr-FR'))
|
||||
// Should still return a function
|
||||
expect(result.current.t('search', { ns: 'plugin' })).toBe('search')
|
||||
// The global mock returns key with namespace prefix
|
||||
expect(result.current.t('search', { ns: 'plugin' })).toBe('plugin.search')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -2756,15 +2756,15 @@ describe('PluginTypeSwitch Component', () => {
|
||||
</MarketplaceContextProvider>,
|
||||
)
|
||||
|
||||
// Note: The mock returns the key without namespace prefix
|
||||
expect(screen.getByText('category.all')).toBeInTheDocument()
|
||||
expect(screen.getByText('category.models')).toBeInTheDocument()
|
||||
expect(screen.getByText('category.tools')).toBeInTheDocument()
|
||||
expect(screen.getByText('category.datasources')).toBeInTheDocument()
|
||||
expect(screen.getByText('category.triggers')).toBeInTheDocument()
|
||||
expect(screen.getByText('category.agents')).toBeInTheDocument()
|
||||
expect(screen.getByText('category.extensions')).toBeInTheDocument()
|
||||
expect(screen.getByText('category.bundles')).toBeInTheDocument()
|
||||
// Note: The global mock returns the key with namespace prefix (plugin.)
|
||||
expect(screen.getByText('plugin.category.all')).toBeInTheDocument()
|
||||
expect(screen.getByText('plugin.category.models')).toBeInTheDocument()
|
||||
expect(screen.getByText('plugin.category.tools')).toBeInTheDocument()
|
||||
expect(screen.getByText('plugin.category.datasources')).toBeInTheDocument()
|
||||
expect(screen.getByText('plugin.category.triggers')).toBeInTheDocument()
|
||||
expect(screen.getByText('plugin.category.agents')).toBeInTheDocument()
|
||||
expect(screen.getByText('plugin.category.extensions')).toBeInTheDocument()
|
||||
expect(screen.getByText('plugin.category.bundles')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should apply className prop', () => {
|
||||
@ -2794,7 +2794,7 @@ describe('PluginTypeSwitch Component', () => {
|
||||
</MarketplaceContextProvider>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByText('category.tools'))
|
||||
fireEvent.click(screen.getByText('plugin.category.tools'))
|
||||
expect(screen.getByTestId('active-type-display')).toHaveTextContent('tool')
|
||||
})
|
||||
|
||||
@ -2816,7 +2816,7 @@ describe('PluginTypeSwitch Component', () => {
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('set-model'))
|
||||
const modelOption = screen.getByText('category.models').closest('div')
|
||||
const modelOption = screen.getByText('plugin.category.models').closest('div')
|
||||
expect(modelOption).toHaveClass('shadow-xs')
|
||||
})
|
||||
})
|
||||
|
||||
@ -219,7 +219,6 @@ const createModelItem = (overrides: Partial<ModelItem> = {}): ModelItem => ({
|
||||
*/
|
||||
const createModel = (overrides: Partial<Model> = {}): Model => ({
|
||||
provider: 'openai',
|
||||
icon_large: { en_US: 'icon-large.png', zh_Hans: 'icon-large.png' },
|
||||
icon_small: { en_US: 'icon-small.png', zh_Hans: 'icon-small.png' },
|
||||
label: { en_US: 'OpenAI', zh_Hans: 'OpenAI' },
|
||||
models: [createModelItem()],
|
||||
|
||||
@ -78,17 +78,6 @@ function createMockLogData(logs: TriggerLogEntity[] = []): { logs: TriggerLogEnt
|
||||
// Mock Setup
|
||||
// ============================================================================
|
||||
|
||||
const mockTranslate = vi.fn((key: string, options?: { ns?: string }) => {
|
||||
// Build full key with namespace prefix if provided
|
||||
const fullKey = options?.ns ? `${options.ns}.${key}` : key
|
||||
return fullKey
|
||||
})
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: mockTranslate,
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock plugin store
|
||||
const mockPluginDetail = createMockPluginDetail()
|
||||
const mockUsePluginStore = vi.fn(() => mockPluginDetail)
|
||||
|
||||
@ -68,17 +68,6 @@ function createMockSubscriptionBuilder(overrides: Partial<TriggerSubscriptionBui
|
||||
// Mock Setup
|
||||
// ============================================================================
|
||||
|
||||
const mockTranslate = vi.fn((key: string, options?: { ns?: string }) => {
|
||||
// Build full key with namespace prefix if provided
|
||||
const fullKey = options?.ns ? `${options.ns}.${key}` : key
|
||||
return fullKey
|
||||
})
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: mockTranslate,
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock plugin store
|
||||
const mockPluginDetail = createMockPluginDetail()
|
||||
const mockUsePluginStore = vi.fn(() => mockPluginDetail)
|
||||
|
||||
@ -12,16 +12,6 @@ import { OAuthEditModal } from './oauth-edit-modal'
|
||||
|
||||
// ==================== Mock Setup ====================
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string, options?: { ns?: string }) => {
|
||||
// Build full key with namespace prefix if provided
|
||||
const fullKey = options?.ns ? `${options.ns}.${key}` : key
|
||||
return fullKey
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
const mockToastNotify = vi.fn()
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
default: { notify: (params: unknown) => mockToastNotify(params) },
|
||||
|
||||
@ -9,28 +9,6 @@ import PluginMutationModal from './index'
|
||||
// Mock External Dependencies Only
|
||||
// ================================
|
||||
|
||||
// Mock react-i18next (translation hook)
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock useMixedTranslation hook
|
||||
vi.mock('../marketplace/hooks', () => ({
|
||||
useMixedTranslation: (_locale?: string) => ({
|
||||
t: (key: string, options?: { ns?: string }) => {
|
||||
const fullKey = options?.ns ? `${options.ns}.${key}` : key
|
||||
return fullKey
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock useGetLanguage context
|
||||
vi.mock('@/context/i18n', () => ({
|
||||
useGetLanguage: () => 'en-US',
|
||||
}))
|
||||
|
||||
// Mock useTheme hook
|
||||
vi.mock('@/hooks/use-theme', () => ({
|
||||
default: () => ({ theme: 'light' }),
|
||||
|
||||
@ -1,10 +1,13 @@
|
||||
'use client'
|
||||
import type { PluginDetail } from '../types'
|
||||
import type { FilterState } from './filter-management'
|
||||
import { useDebounceFn } from 'ahooks'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import PluginDetailPanel from '@/app/components/plugins/plugin-detail-panel'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import { renderI18nObject } from '@/i18n-config'
|
||||
import { useInstalledLatestVersion, useInstalledPluginList, useInvalidateInstalledPluginList } from '@/service/use-plugins'
|
||||
import Loading from '../../base/loading'
|
||||
import { PluginSource } from '../types'
|
||||
@ -13,8 +16,34 @@ import Empty from './empty'
|
||||
import FilterManagement from './filter-management'
|
||||
import List from './list'
|
||||
|
||||
const matchesSearchQuery = (plugin: PluginDetail & { latest_version: string }, query: string, locale: string): boolean => {
|
||||
if (!query)
|
||||
return true
|
||||
const lowerQuery = query.toLowerCase()
|
||||
const { declaration } = plugin
|
||||
// Match plugin_id
|
||||
if (plugin.plugin_id.toLowerCase().includes(lowerQuery))
|
||||
return true
|
||||
// Match plugin name
|
||||
if (plugin.name?.toLowerCase().includes(lowerQuery))
|
||||
return true
|
||||
// Match declaration name
|
||||
if (declaration.name?.toLowerCase().includes(lowerQuery))
|
||||
return true
|
||||
// Match localized label
|
||||
const label = renderI18nObject(declaration.label, locale)
|
||||
if (label?.toLowerCase().includes(lowerQuery))
|
||||
return true
|
||||
// Match localized description
|
||||
const description = renderI18nObject(declaration.description, locale)
|
||||
if (description?.toLowerCase().includes(lowerQuery))
|
||||
return true
|
||||
return false
|
||||
}
|
||||
|
||||
const PluginsPanel = () => {
|
||||
const { t } = useTranslation()
|
||||
const locale = useGetLanguage()
|
||||
const filters = usePluginPageContext(v => v.filters) as FilterState
|
||||
const setFilters = usePluginPageContext(v => v.setFilters)
|
||||
const { data: pluginList, isLoading: isPluginListLoading, isFetching, isLastPage, loadNextPage } = useInstalledPluginList()
|
||||
@ -48,11 +77,11 @@ const PluginsPanel = () => {
|
||||
return (
|
||||
(categories.length === 0 || categories.includes(plugin.declaration.category))
|
||||
&& (tags.length === 0 || tags.some(tag => plugin.declaration.tags.includes(tag)))
|
||||
&& (searchQuery === '' || plugin.plugin_id.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||
&& matchesSearchQuery(plugin, searchQuery, locale)
|
||||
)
|
||||
})
|
||||
return filteredList
|
||||
}, [pluginListWithLatestVersion, filters])
|
||||
}, [pluginListWithLatestVersion, filters, locale])
|
||||
|
||||
const currentPluginDetail = useMemo(() => {
|
||||
const detail = pluginListWithLatestVersion.find(plugin => plugin.plugin_id === currentPluginID)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
1390
web/app/components/rag-pipeline/components/index.spec.tsx
Normal file
1390
web/app/components/rag-pipeline/components/index.spec.tsx
Normal file
File diff suppressed because it is too large
Load Diff
971
web/app/components/rag-pipeline/components/panel/index.spec.tsx
Normal file
971
web/app/components/rag-pipeline/components/panel/index.spec.tsx
Normal file
@ -0,0 +1,971 @@
|
||||
import type { PanelProps } from '@/app/components/workflow/panel'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import RagPipelinePanel from './index'
|
||||
|
||||
// ============================================================================
|
||||
// Mock External Dependencies
|
||||
// ============================================================================
|
||||
|
||||
// Type definitions for dynamic module
|
||||
type DynamicModule = {
|
||||
default?: React.ComponentType<Record<string, unknown>>
|
||||
}
|
||||
|
||||
type PromiseOrModule = Promise<DynamicModule> | DynamicModule
|
||||
|
||||
// Mock next/dynamic to return synchronous components immediately
|
||||
vi.mock('next/dynamic', () => ({
|
||||
default: (loader: () => PromiseOrModule, _options?: Record<string, unknown>) => {
|
||||
let Component: React.ComponentType<Record<string, unknown>> | null = null
|
||||
|
||||
// Try to resolve the loader synchronously for mocked modules
|
||||
try {
|
||||
const result = loader() as PromiseOrModule
|
||||
if (result && typeof (result as Promise<DynamicModule>).then === 'function') {
|
||||
// For async modules, we need to handle them specially
|
||||
// This will work with vi.mock since mocks resolve synchronously
|
||||
(result as Promise<DynamicModule>).then((mod: DynamicModule) => {
|
||||
Component = (mod.default || mod) as React.ComponentType<Record<string, unknown>>
|
||||
})
|
||||
}
|
||||
else if (result) {
|
||||
Component = ((result as DynamicModule).default || result) as React.ComponentType<Record<string, unknown>>
|
||||
}
|
||||
}
|
||||
catch {
|
||||
// If the module can't be resolved, Component stays null
|
||||
}
|
||||
|
||||
// Return a simple wrapper that renders the component or null
|
||||
const DynamicComponent = React.forwardRef((props: Record<string, unknown>, ref: React.Ref<unknown>) => {
|
||||
// For mocked modules, Component should already be set
|
||||
if (Component)
|
||||
return <Component {...props} ref={ref} />
|
||||
|
||||
return null
|
||||
})
|
||||
|
||||
DynamicComponent.displayName = 'DynamicComponent'
|
||||
return DynamicComponent
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock workflow store
|
||||
let mockHistoryWorkflowData: Record<string, unknown> | null = null
|
||||
let mockShowDebugAndPreviewPanel = false
|
||||
let mockShowGlobalVariablePanel = false
|
||||
let mockShowInputFieldPanel = false
|
||||
let mockShowInputFieldPreviewPanel = false
|
||||
let mockInputFieldEditPanelProps: Record<string, unknown> | null = null
|
||||
let mockPipelineId = 'test-pipeline-123'
|
||||
|
||||
type MockStoreState = {
|
||||
historyWorkflowData: Record<string, unknown> | null
|
||||
showDebugAndPreviewPanel: boolean
|
||||
showGlobalVariablePanel: boolean
|
||||
showInputFieldPanel: boolean
|
||||
showInputFieldPreviewPanel: boolean
|
||||
inputFieldEditPanelProps: Record<string, unknown> | null
|
||||
pipelineId: string
|
||||
}
|
||||
|
||||
vi.mock('@/app/components/workflow/store', () => ({
|
||||
useStore: (selector: (state: MockStoreState) => unknown) => {
|
||||
const state: MockStoreState = {
|
||||
historyWorkflowData: mockHistoryWorkflowData,
|
||||
showDebugAndPreviewPanel: mockShowDebugAndPreviewPanel,
|
||||
showGlobalVariablePanel: mockShowGlobalVariablePanel,
|
||||
showInputFieldPanel: mockShowInputFieldPanel,
|
||||
showInputFieldPreviewPanel: mockShowInputFieldPreviewPanel,
|
||||
inputFieldEditPanelProps: mockInputFieldEditPanelProps,
|
||||
pipelineId: mockPipelineId,
|
||||
}
|
||||
return selector(state)
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock Panel component to capture props and render children
|
||||
let capturedPanelProps: PanelProps | null = null
|
||||
vi.mock('@/app/components/workflow/panel', () => ({
|
||||
default: (props: PanelProps) => {
|
||||
capturedPanelProps = props
|
||||
return (
|
||||
<div data-testid="workflow-panel">
|
||||
<div data-testid="panel-left">{props.components?.left}</div>
|
||||
<div data-testid="panel-right">{props.components?.right}</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock Record component
|
||||
vi.mock('@/app/components/workflow/panel/record', () => ({
|
||||
default: () => <div data-testid="record-panel">Record Panel</div>,
|
||||
}))
|
||||
|
||||
// Mock TestRunPanel component
|
||||
vi.mock('@/app/components/rag-pipeline/components/panel/test-run', () => ({
|
||||
default: () => <div data-testid="test-run-panel">Test Run Panel</div>,
|
||||
}))
|
||||
|
||||
// Mock InputFieldPanel component
|
||||
vi.mock('./input-field', () => ({
|
||||
default: () => <div data-testid="input-field-panel">Input Field Panel</div>,
|
||||
}))
|
||||
|
||||
// Mock InputFieldEditorPanel component
|
||||
const mockInputFieldEditorProps = vi.fn()
|
||||
vi.mock('./input-field/editor', () => ({
|
||||
default: (props: Record<string, unknown>) => {
|
||||
mockInputFieldEditorProps(props)
|
||||
return <div data-testid="input-field-editor-panel">Input Field Editor Panel</div>
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock PreviewPanel component
|
||||
vi.mock('./input-field/preview', () => ({
|
||||
default: () => <div data-testid="preview-panel">Preview Panel</div>,
|
||||
}))
|
||||
|
||||
// Mock GlobalVariablePanel component
|
||||
vi.mock('@/app/components/workflow/panel/global-variable-panel', () => ({
|
||||
default: () => <div data-testid="global-variable-panel">Global Variable Panel</div>,
|
||||
}))
|
||||
|
||||
// ============================================================================
|
||||
// Helper Functions
|
||||
// ============================================================================
|
||||
|
||||
type SetupMockOptions = {
|
||||
historyWorkflowData?: Record<string, unknown> | null
|
||||
showDebugAndPreviewPanel?: boolean
|
||||
showGlobalVariablePanel?: boolean
|
||||
showInputFieldPanel?: boolean
|
||||
showInputFieldPreviewPanel?: boolean
|
||||
inputFieldEditPanelProps?: Record<string, unknown> | null
|
||||
pipelineId?: string
|
||||
}
|
||||
|
||||
const setupMocks = (options?: SetupMockOptions) => {
|
||||
mockHistoryWorkflowData = options?.historyWorkflowData ?? null
|
||||
mockShowDebugAndPreviewPanel = options?.showDebugAndPreviewPanel ?? false
|
||||
mockShowGlobalVariablePanel = options?.showGlobalVariablePanel ?? false
|
||||
mockShowInputFieldPanel = options?.showInputFieldPanel ?? false
|
||||
mockShowInputFieldPreviewPanel = options?.showInputFieldPreviewPanel ?? false
|
||||
mockInputFieldEditPanelProps = options?.inputFieldEditPanelProps ?? null
|
||||
mockPipelineId = options?.pipelineId ?? 'test-pipeline-123'
|
||||
capturedPanelProps = null
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// RagPipelinePanel Component Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('RagPipelinePanel', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
setupMocks()
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Rendering Tests
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', async () => {
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('workflow-panel')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should render Panel component with correct structure', async () => {
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('panel-left')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('panel-right')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should pass versionHistoryPanelProps to Panel', async () => {
|
||||
// Arrange
|
||||
setupMocks({ pipelineId: 'my-pipeline-456' })
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(capturedPanelProps?.versionHistoryPanelProps).toBeDefined()
|
||||
expect(capturedPanelProps?.versionHistoryPanelProps?.getVersionListUrl).toBe(
|
||||
'/rag/pipelines/my-pipeline-456/workflows',
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Memoization Tests - versionHistoryPanelProps
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Memoization - versionHistoryPanelProps', () => {
|
||||
it('should compute correct getVersionListUrl based on pipelineId', async () => {
|
||||
// Arrange
|
||||
setupMocks({ pipelineId: 'pipeline-abc' })
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(capturedPanelProps?.versionHistoryPanelProps?.getVersionListUrl).toBe(
|
||||
'/rag/pipelines/pipeline-abc/workflows',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('should compute correct deleteVersionUrl function', async () => {
|
||||
// Arrange
|
||||
setupMocks({ pipelineId: 'pipeline-xyz' })
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
const deleteUrl = capturedPanelProps?.versionHistoryPanelProps?.deleteVersionUrl?.('version-1')
|
||||
expect(deleteUrl).toBe('/rag/pipelines/pipeline-xyz/workflows/version-1')
|
||||
})
|
||||
})
|
||||
|
||||
it('should compute correct updateVersionUrl function', async () => {
|
||||
// Arrange
|
||||
setupMocks({ pipelineId: 'pipeline-def' })
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
const updateUrl = capturedPanelProps?.versionHistoryPanelProps?.updateVersionUrl?.('version-2')
|
||||
expect(updateUrl).toBe('/rag/pipelines/pipeline-def/workflows/version-2')
|
||||
})
|
||||
})
|
||||
|
||||
it('should set latestVersionId to empty string', async () => {
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(capturedPanelProps?.versionHistoryPanelProps?.latestVersionId).toBe('')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Memoization Tests - panelProps
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Memoization - panelProps', () => {
|
||||
it('should pass components.left to Panel', async () => {
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(capturedPanelProps?.components?.left).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
it('should pass components.right to Panel', async () => {
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(capturedPanelProps?.components?.right).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
it('should pass versionHistoryPanelProps to panelProps', async () => {
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(capturedPanelProps?.versionHistoryPanelProps).toBeDefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Component Memoization Tests (React.memo)
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Component Memoization', () => {
|
||||
it('should be wrapped with React.memo', async () => {
|
||||
// The component should not break when re-rendered
|
||||
const { rerender } = render(<RagPipelinePanel />)
|
||||
|
||||
// Act - rerender without prop changes
|
||||
rerender(<RagPipelinePanel />)
|
||||
|
||||
// Assert - component should still render correctly
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('workflow-panel')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// RagPipelinePanelOnRight Component Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('RagPipelinePanelOnRight', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
setupMocks()
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Conditional Rendering - Record Panel
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Record Panel Conditional Rendering', () => {
|
||||
it('should render Record panel when historyWorkflowData exists', async () => {
|
||||
// Arrange
|
||||
setupMocks({ historyWorkflowData: { id: 'history-1' } })
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('record-panel')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should not render Record panel when historyWorkflowData is null', async () => {
|
||||
// Arrange
|
||||
setupMocks({ historyWorkflowData: null })
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('record-panel')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should not render Record panel when historyWorkflowData is undefined', async () => {
|
||||
// Arrange
|
||||
setupMocks({ historyWorkflowData: undefined })
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('record-panel')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Conditional Rendering - TestRun Panel
|
||||
// -------------------------------------------------------------------------
|
||||
describe('TestRun Panel Conditional Rendering', () => {
|
||||
it('should render TestRun panel when showDebugAndPreviewPanel is true', async () => {
|
||||
// Arrange
|
||||
setupMocks({ showDebugAndPreviewPanel: true })
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('test-run-panel')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should not render TestRun panel when showDebugAndPreviewPanel is false', async () => {
|
||||
// Arrange
|
||||
setupMocks({ showDebugAndPreviewPanel: false })
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('test-run-panel')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Conditional Rendering - GlobalVariable Panel
|
||||
// -------------------------------------------------------------------------
|
||||
describe('GlobalVariable Panel Conditional Rendering', () => {
|
||||
it('should render GlobalVariable panel when showGlobalVariablePanel is true', async () => {
|
||||
// Arrange
|
||||
setupMocks({ showGlobalVariablePanel: true })
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('global-variable-panel')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should not render GlobalVariable panel when showGlobalVariablePanel is false', async () => {
|
||||
// Arrange
|
||||
setupMocks({ showGlobalVariablePanel: false })
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('global-variable-panel')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Multiple Panels Rendering
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Multiple Panels Rendering', () => {
|
||||
it('should render all right panels when all conditions are true', async () => {
|
||||
// Arrange
|
||||
setupMocks({
|
||||
historyWorkflowData: { id: 'history-1' },
|
||||
showDebugAndPreviewPanel: true,
|
||||
showGlobalVariablePanel: true,
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('record-panel')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('test-run-panel')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('global-variable-panel')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should render no right panels when all conditions are false', async () => {
|
||||
// Arrange
|
||||
setupMocks({
|
||||
historyWorkflowData: null,
|
||||
showDebugAndPreviewPanel: false,
|
||||
showGlobalVariablePanel: false,
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('record-panel')).not.toBeInTheDocument()
|
||||
expect(screen.queryByTestId('test-run-panel')).not.toBeInTheDocument()
|
||||
expect(screen.queryByTestId('global-variable-panel')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should render only Record and TestRun panels', async () => {
|
||||
// Arrange
|
||||
setupMocks({
|
||||
historyWorkflowData: { id: 'history-1' },
|
||||
showDebugAndPreviewPanel: true,
|
||||
showGlobalVariablePanel: false,
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('record-panel')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('test-run-panel')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('global-variable-panel')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// RagPipelinePanelOnLeft Component Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('RagPipelinePanelOnLeft', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
setupMocks()
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Conditional Rendering - Preview Panel
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Preview Panel Conditional Rendering', () => {
|
||||
it('should render Preview panel when showInputFieldPreviewPanel is true', async () => {
|
||||
// Arrange
|
||||
setupMocks({ showInputFieldPreviewPanel: true })
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('preview-panel')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should not render Preview panel when showInputFieldPreviewPanel is false', async () => {
|
||||
// Arrange
|
||||
setupMocks({ showInputFieldPreviewPanel: false })
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('preview-panel')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Conditional Rendering - InputFieldEditor Panel
|
||||
// -------------------------------------------------------------------------
|
||||
describe('InputFieldEditor Panel Conditional Rendering', () => {
|
||||
it('should render InputFieldEditor panel when inputFieldEditPanelProps is provided', async () => {
|
||||
// Arrange
|
||||
const editProps = {
|
||||
onClose: vi.fn(),
|
||||
onSubmit: vi.fn(),
|
||||
initialData: { variable: 'test' },
|
||||
}
|
||||
setupMocks({ inputFieldEditPanelProps: editProps })
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('input-field-editor-panel')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should not render InputFieldEditor panel when inputFieldEditPanelProps is null', async () => {
|
||||
// Arrange
|
||||
setupMocks({ inputFieldEditPanelProps: null })
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('input-field-editor-panel')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should pass props to InputFieldEditor panel', async () => {
|
||||
// Arrange
|
||||
const editProps = {
|
||||
onClose: vi.fn(),
|
||||
onSubmit: vi.fn(),
|
||||
initialData: { variable: 'test_var', label: 'Test Label' },
|
||||
}
|
||||
setupMocks({ inputFieldEditPanelProps: editProps })
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockInputFieldEditorProps).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
onClose: editProps.onClose,
|
||||
onSubmit: editProps.onSubmit,
|
||||
initialData: editProps.initialData,
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Conditional Rendering - InputField Panel
|
||||
// -------------------------------------------------------------------------
|
||||
describe('InputField Panel Conditional Rendering', () => {
|
||||
it('should render InputField panel when showInputFieldPanel is true', async () => {
|
||||
// Arrange
|
||||
setupMocks({ showInputFieldPanel: true })
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('input-field-panel')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should not render InputField panel when showInputFieldPanel is false', async () => {
|
||||
// Arrange
|
||||
setupMocks({ showInputFieldPanel: false })
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('input-field-panel')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Multiple Panels Rendering
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Multiple Left Panels Rendering', () => {
|
||||
it('should render all left panels when all conditions are true', async () => {
|
||||
// Arrange
|
||||
setupMocks({
|
||||
showInputFieldPreviewPanel: true,
|
||||
inputFieldEditPanelProps: { onClose: vi.fn(), onSubmit: vi.fn() },
|
||||
showInputFieldPanel: true,
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('preview-panel')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('input-field-editor-panel')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('input-field-panel')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should render no left panels when all conditions are false', async () => {
|
||||
// Arrange
|
||||
setupMocks({
|
||||
showInputFieldPreviewPanel: false,
|
||||
inputFieldEditPanelProps: null,
|
||||
showInputFieldPanel: false,
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('preview-panel')).not.toBeInTheDocument()
|
||||
expect(screen.queryByTestId('input-field-editor-panel')).not.toBeInTheDocument()
|
||||
expect(screen.queryByTestId('input-field-panel')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should render only Preview and InputField panels', async () => {
|
||||
// Arrange
|
||||
setupMocks({
|
||||
showInputFieldPreviewPanel: true,
|
||||
inputFieldEditPanelProps: null,
|
||||
showInputFieldPanel: true,
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('preview-panel')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('input-field-editor-panel')).not.toBeInTheDocument()
|
||||
expect(screen.getByTestId('input-field-panel')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// Edge Cases Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
setupMocks()
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Empty/Undefined Values
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Empty/Undefined Values', () => {
|
||||
it('should handle empty pipelineId gracefully', async () => {
|
||||
// Arrange
|
||||
setupMocks({ pipelineId: '' })
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(capturedPanelProps?.versionHistoryPanelProps?.getVersionListUrl).toBe(
|
||||
'/rag/pipelines//workflows',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle special characters in pipelineId', async () => {
|
||||
// Arrange
|
||||
setupMocks({ pipelineId: 'pipeline-with-special_chars.123' })
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(capturedPanelProps?.versionHistoryPanelProps?.getVersionListUrl).toBe(
|
||||
'/rag/pipelines/pipeline-with-special_chars.123/workflows',
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Props Spreading Tests
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Props Spreading', () => {
|
||||
it('should correctly spread inputFieldEditPanelProps to editor component', async () => {
|
||||
// Arrange
|
||||
const customProps = {
|
||||
onClose: vi.fn(),
|
||||
onSubmit: vi.fn(),
|
||||
initialData: {
|
||||
variable: 'custom_var',
|
||||
label: 'Custom Label',
|
||||
type: 'text',
|
||||
},
|
||||
extraProp: 'extra-value',
|
||||
}
|
||||
setupMocks({ inputFieldEditPanelProps: customProps })
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockInputFieldEditorProps).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
extraProp: 'extra-value',
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// State Combinations
|
||||
// -------------------------------------------------------------------------
|
||||
describe('State Combinations', () => {
|
||||
it('should handle all panels visible simultaneously', async () => {
|
||||
// Arrange
|
||||
setupMocks({
|
||||
historyWorkflowData: { id: 'h1' },
|
||||
showDebugAndPreviewPanel: true,
|
||||
showGlobalVariablePanel: true,
|
||||
showInputFieldPreviewPanel: true,
|
||||
inputFieldEditPanelProps: { onClose: vi.fn(), onSubmit: vi.fn() },
|
||||
showInputFieldPanel: true,
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert - All panels should be visible
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('record-panel')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('test-run-panel')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('global-variable-panel')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('preview-panel')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('input-field-editor-panel')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('input-field-panel')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// URL Generator Functions Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('URL Generator Functions', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
setupMocks()
|
||||
})
|
||||
|
||||
it('should return consistent URLs for same versionId', async () => {
|
||||
// Arrange
|
||||
setupMocks({ pipelineId: 'stable-pipeline' })
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
const deleteUrl1 = capturedPanelProps?.versionHistoryPanelProps?.deleteVersionUrl?.('version-x')
|
||||
const deleteUrl2 = capturedPanelProps?.versionHistoryPanelProps?.deleteVersionUrl?.('version-x')
|
||||
expect(deleteUrl1).toBe(deleteUrl2)
|
||||
})
|
||||
})
|
||||
|
||||
it('should return different URLs for different versionIds', async () => {
|
||||
// Arrange
|
||||
setupMocks({ pipelineId: 'stable-pipeline' })
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
const deleteUrl1 = capturedPanelProps?.versionHistoryPanelProps?.deleteVersionUrl?.('version-1')
|
||||
const deleteUrl2 = capturedPanelProps?.versionHistoryPanelProps?.deleteVersionUrl?.('version-2')
|
||||
expect(deleteUrl1).not.toBe(deleteUrl2)
|
||||
expect(deleteUrl1).toBe('/rag/pipelines/stable-pipeline/workflows/version-1')
|
||||
expect(deleteUrl2).toBe('/rag/pipelines/stable-pipeline/workflows/version-2')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// Type Safety Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('Type Safety', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
setupMocks()
|
||||
})
|
||||
|
||||
it('should pass correct PanelProps structure', async () => {
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert - Check structure matches PanelProps
|
||||
await waitFor(() => {
|
||||
expect(capturedPanelProps).toHaveProperty('components')
|
||||
expect(capturedPanelProps).toHaveProperty('versionHistoryPanelProps')
|
||||
expect(capturedPanelProps?.components).toHaveProperty('left')
|
||||
expect(capturedPanelProps?.components).toHaveProperty('right')
|
||||
})
|
||||
})
|
||||
|
||||
it('should pass correct versionHistoryPanelProps structure', async () => {
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(capturedPanelProps?.versionHistoryPanelProps).toHaveProperty('getVersionListUrl')
|
||||
expect(capturedPanelProps?.versionHistoryPanelProps).toHaveProperty('deleteVersionUrl')
|
||||
expect(capturedPanelProps?.versionHistoryPanelProps).toHaveProperty('updateVersionUrl')
|
||||
expect(capturedPanelProps?.versionHistoryPanelProps).toHaveProperty('latestVersionId')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// Performance Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('Performance', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
setupMocks()
|
||||
})
|
||||
|
||||
it('should handle multiple rerenders without issues', async () => {
|
||||
// Arrange
|
||||
const { rerender } = render(<RagPipelinePanel />)
|
||||
|
||||
// Act - Multiple rerenders
|
||||
for (let i = 0; i < 10; i++)
|
||||
rerender(<RagPipelinePanel />)
|
||||
|
||||
// Assert - Component should still work
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('workflow-panel')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// Integration Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('Integration Tests', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
setupMocks()
|
||||
})
|
||||
|
||||
it('should pass correct components to Panel', async () => {
|
||||
// Arrange
|
||||
setupMocks({
|
||||
historyWorkflowData: { id: 'h1' },
|
||||
showInputFieldPanel: true,
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(capturedPanelProps?.components?.left).toBeDefined()
|
||||
expect(capturedPanelProps?.components?.right).toBeDefined()
|
||||
|
||||
// Check that the components are React elements
|
||||
expect(React.isValidElement(capturedPanelProps?.components?.left)).toBe(true)
|
||||
expect(React.isValidElement(capturedPanelProps?.components?.right)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
it('should correctly consume all store selectors', async () => {
|
||||
// Arrange
|
||||
setupMocks({
|
||||
historyWorkflowData: { id: 'test-history' },
|
||||
showDebugAndPreviewPanel: true,
|
||||
showGlobalVariablePanel: true,
|
||||
showInputFieldPanel: true,
|
||||
showInputFieldPreviewPanel: true,
|
||||
inputFieldEditPanelProps: { onClose: vi.fn(), onSubmit: vi.fn() },
|
||||
pipelineId: 'integration-test-pipeline',
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert - All store-dependent rendering should work
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('record-panel')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('test-run-panel')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('global-variable-panel')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('input-field-panel')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('preview-panel')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('input-field-editor-panel')).toBeInTheDocument()
|
||||
expect(capturedPanelProps?.versionHistoryPanelProps?.getVersionListUrl).toBe(
|
||||
'/rag/pipelines/integration-test-pipeline/workflows',
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -49,6 +49,7 @@ const InputFieldEditorPanel = ({
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="input-field-editor-close-btn"
|
||||
className="absolute right-2.5 top-2.5 flex size-8 items-center justify-center"
|
||||
onClick={onClose}
|
||||
>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -53,6 +53,7 @@ const FieldList = ({
|
||||
{LabelRightContent}
|
||||
</div>
|
||||
<ActionButton
|
||||
data-testid="field-list-add-btn"
|
||||
onClick={() => handleOpenInputFieldEditor()}
|
||||
disabled={readonly}
|
||||
className={cn(readonly && 'cursor-not-allowed')}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,937 @@
|
||||
import type { WorkflowRunningData } from '@/app/components/workflow/types'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { WorkflowRunningStatus } from '@/app/components/workflow/types'
|
||||
import { ChunkingMode } from '@/models/datasets'
|
||||
|
||||
import Header from './header'
|
||||
// Import components after mocks
|
||||
import TestRunPanel from './index'
|
||||
|
||||
// ============================================================================
|
||||
// Mocks
|
||||
// ============================================================================
|
||||
|
||||
// Mock workflow store
|
||||
const mockIsPreparingDataSource = vi.fn(() => true)
|
||||
const mockSetIsPreparingDataSource = vi.fn()
|
||||
const mockWorkflowRunningData = vi.fn<() => WorkflowRunningData | undefined>(() => undefined)
|
||||
const mockPipelineId = 'test-pipeline-id'
|
||||
|
||||
vi.mock('@/app/components/workflow/store', () => ({
|
||||
useStore: (selector: (state: Record<string, unknown>) => unknown) => {
|
||||
const state = {
|
||||
isPreparingDataSource: mockIsPreparingDataSource(),
|
||||
workflowRunningData: mockWorkflowRunningData(),
|
||||
pipelineId: mockPipelineId,
|
||||
}
|
||||
return selector(state)
|
||||
},
|
||||
useWorkflowStore: () => ({
|
||||
getState: () => ({
|
||||
isPreparingDataSource: mockIsPreparingDataSource(),
|
||||
setIsPreparingDataSource: mockSetIsPreparingDataSource,
|
||||
}),
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock workflow interactions
|
||||
const mockHandleCancelDebugAndPreviewPanel = vi.fn()
|
||||
vi.mock('@/app/components/workflow/hooks', () => ({
|
||||
useWorkflowInteractions: () => ({
|
||||
handleCancelDebugAndPreviewPanel: mockHandleCancelDebugAndPreviewPanel,
|
||||
}),
|
||||
useWorkflowRun: () => ({
|
||||
handleRun: vi.fn(),
|
||||
}),
|
||||
useToolIcon: () => 'mock-tool-icon',
|
||||
}))
|
||||
|
||||
// Mock data source provider
|
||||
vi.mock('@/app/components/datasets/documents/create-from-pipeline/data-source/store/provider', () => ({
|
||||
default: ({ children }: { children: React.ReactNode }) => <div data-testid="data-source-provider">{children}</div>,
|
||||
}))
|
||||
|
||||
// Mock Preparation component
|
||||
vi.mock('./preparation', () => ({
|
||||
default: () => <div data-testid="preparation-component">Preparation</div>,
|
||||
}))
|
||||
|
||||
// Mock Result component (for TestRunPanel tests only)
|
||||
vi.mock('./result', () => ({
|
||||
default: () => <div data-testid="result-component">Result</div>,
|
||||
}))
|
||||
|
||||
// Mock ResultPanel from workflow
|
||||
vi.mock('@/app/components/workflow/run/result-panel', () => ({
|
||||
default: (props: Record<string, unknown>) => (
|
||||
<div data-testid="result-panel">
|
||||
ResultPanel -
|
||||
{' '}
|
||||
{props.status as string}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock TracingPanel from workflow
|
||||
vi.mock('@/app/components/workflow/run/tracing-panel', () => ({
|
||||
default: (props: { list: unknown[] }) => (
|
||||
<div data-testid="tracing-panel">
|
||||
TracingPanel -
|
||||
{' '}
|
||||
{props.list?.length ?? 0}
|
||||
{' '}
|
||||
items
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock Loading component
|
||||
vi.mock('@/app/components/base/loading', () => ({
|
||||
default: () => <div data-testid="loading">Loading...</div>,
|
||||
}))
|
||||
|
||||
// Mock config
|
||||
vi.mock('@/config', () => ({
|
||||
RAG_PIPELINE_PREVIEW_CHUNK_NUM: 5,
|
||||
}))
|
||||
|
||||
// ============================================================================
|
||||
// Test Data Factories
|
||||
// ============================================================================
|
||||
|
||||
const createMockWorkflowRunningData = (overrides: Partial<WorkflowRunningData> = {}): WorkflowRunningData => ({
|
||||
result: {
|
||||
status: WorkflowRunningStatus.Succeeded,
|
||||
outputs: '{"test": "output"}',
|
||||
outputs_truncated: false,
|
||||
inputs: '{"test": "input"}',
|
||||
inputs_truncated: false,
|
||||
process_data_truncated: false,
|
||||
error: undefined,
|
||||
elapsed_time: 1000,
|
||||
total_tokens: 100,
|
||||
created_at: Date.now(),
|
||||
created_by: 'Test User',
|
||||
total_steps: 5,
|
||||
exceptions_count: 0,
|
||||
},
|
||||
tracing: [],
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const createMockGeneralOutputs = (chunkContents: string[] = ['chunk1', 'chunk2']) => ({
|
||||
chunk_structure: ChunkingMode.text,
|
||||
preview: chunkContents.map(content => ({ content })),
|
||||
})
|
||||
|
||||
const createMockParentChildOutputs = (parentMode: 'paragraph' | 'full-doc' = 'paragraph') => ({
|
||||
chunk_structure: ChunkingMode.parentChild,
|
||||
parent_mode: parentMode,
|
||||
preview: [
|
||||
{ content: 'parent1', child_chunks: ['child1', 'child2'] },
|
||||
{ content: 'parent2', child_chunks: ['child3', 'child4'] },
|
||||
],
|
||||
})
|
||||
|
||||
const createMockQAOutputs = () => ({
|
||||
chunk_structure: ChunkingMode.qa,
|
||||
qa_preview: [
|
||||
{ question: 'Q1', answer: 'A1' },
|
||||
{ question: 'Q2', answer: 'A2' },
|
||||
],
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// TestRunPanel Component Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('TestRunPanel', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockIsPreparingDataSource.mockReturnValue(true)
|
||||
mockWorkflowRunningData.mockReturnValue(undefined)
|
||||
})
|
||||
|
||||
// Basic rendering tests
|
||||
describe('Rendering', () => {
|
||||
it('should render with correct container styles', () => {
|
||||
const { container } = render(<TestRunPanel />)
|
||||
const panelDiv = container.firstChild as HTMLElement
|
||||
|
||||
expect(panelDiv).toHaveClass('relative', 'flex', 'h-full', 'w-[480px]', 'flex-col')
|
||||
})
|
||||
|
||||
it('should render Header component', () => {
|
||||
render(<TestRunPanel />)
|
||||
|
||||
expect(screen.getByText('datasetPipeline.testRun.title')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Conditional rendering based on isPreparingDataSource
|
||||
describe('Conditional Content Rendering', () => {
|
||||
it('should render Preparation inside DataSourceProvider when isPreparingDataSource is true', () => {
|
||||
mockIsPreparingDataSource.mockReturnValue(true)
|
||||
|
||||
render(<TestRunPanel />)
|
||||
|
||||
expect(screen.getByTestId('data-source-provider')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('preparation-component')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('result-component')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render Result when isPreparingDataSource is false', () => {
|
||||
mockIsPreparingDataSource.mockReturnValue(false)
|
||||
|
||||
render(<TestRunPanel />)
|
||||
|
||||
expect(screen.getByTestId('result-component')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('data-source-provider')).not.toBeInTheDocument()
|
||||
expect(screen.queryByTestId('preparation-component')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// Header Component Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('Header', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockIsPreparingDataSource.mockReturnValue(true)
|
||||
})
|
||||
|
||||
// Rendering tests
|
||||
describe('Rendering', () => {
|
||||
it('should render title with correct translation key', () => {
|
||||
render(<Header />)
|
||||
|
||||
expect(screen.getByText('datasetPipeline.testRun.title')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render close button', () => {
|
||||
render(<Header />)
|
||||
|
||||
const closeButton = screen.getByRole('button')
|
||||
expect(closeButton).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should have correct layout classes', () => {
|
||||
const { container } = render(<Header />)
|
||||
const headerDiv = container.firstChild as HTMLElement
|
||||
|
||||
expect(headerDiv).toHaveClass('flex', 'items-center', 'gap-x-2', 'pl-4', 'pr-3', 'pt-4')
|
||||
})
|
||||
})
|
||||
|
||||
// Close button interactions
|
||||
describe('Close Button Interaction', () => {
|
||||
it('should call setIsPreparingDataSource(false) and handleCancelDebugAndPreviewPanel when clicked and isPreparingDataSource is true', () => {
|
||||
mockIsPreparingDataSource.mockReturnValue(true)
|
||||
|
||||
render(<Header />)
|
||||
|
||||
const closeButton = screen.getByRole('button')
|
||||
fireEvent.click(closeButton)
|
||||
|
||||
expect(mockSetIsPreparingDataSource).toHaveBeenCalledWith(false)
|
||||
expect(mockHandleCancelDebugAndPreviewPanel).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should only call handleCancelDebugAndPreviewPanel when isPreparingDataSource is false', () => {
|
||||
mockIsPreparingDataSource.mockReturnValue(false)
|
||||
|
||||
render(<Header />)
|
||||
|
||||
const closeButton = screen.getByRole('button')
|
||||
fireEvent.click(closeButton)
|
||||
|
||||
expect(mockSetIsPreparingDataSource).not.toHaveBeenCalled()
|
||||
expect(mockHandleCancelDebugAndPreviewPanel).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// Result Component Tests (Real Implementation)
|
||||
// ============================================================================
|
||||
|
||||
// Unmock Result for these tests
|
||||
vi.doUnmock('./result')
|
||||
|
||||
describe('Result', () => {
|
||||
// Dynamically import Result to get real implementation
|
||||
let Result: typeof import('./result').default
|
||||
|
||||
beforeAll(async () => {
|
||||
const resultModule = await import('./result')
|
||||
Result = resultModule.default
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockWorkflowRunningData.mockReturnValue(undefined)
|
||||
})
|
||||
|
||||
// Rendering tests
|
||||
describe('Rendering', () => {
|
||||
it('should render with RESULT tab active by default', async () => {
|
||||
render(<Result />)
|
||||
|
||||
await waitFor(() => {
|
||||
const resultTab = screen.getByRole('button', { name: /runLog\.result/i })
|
||||
expect(resultTab).toHaveClass('border-util-colors-blue-brand-blue-brand-600')
|
||||
})
|
||||
})
|
||||
|
||||
it('should render all three tabs', () => {
|
||||
render(<Result />)
|
||||
|
||||
expect(screen.getByRole('button', { name: /runLog\.result/i })).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: /runLog\.detail/i })).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: /runLog\.tracing/i })).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Tab switching tests
|
||||
describe('Tab Switching', () => {
|
||||
it('should switch to DETAIL tab when clicked', async () => {
|
||||
mockWorkflowRunningData.mockReturnValue(createMockWorkflowRunningData())
|
||||
render(<Result />)
|
||||
|
||||
const detailTab = screen.getByRole('button', { name: /runLog\.detail/i })
|
||||
fireEvent.click(detailTab)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('result-panel')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should switch to TRACING tab when clicked', async () => {
|
||||
mockWorkflowRunningData.mockReturnValue(createMockWorkflowRunningData({ tracing: [{ id: '1' }] as unknown as WorkflowRunningData['tracing'] }))
|
||||
render(<Result />)
|
||||
|
||||
const tracingTab = screen.getByRole('button', { name: /runLog\.tracing/i })
|
||||
fireEvent.click(tracingTab)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('tracing-panel')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Loading states
|
||||
describe('Loading States', () => {
|
||||
it('should show loading in DETAIL tab when no result data', async () => {
|
||||
mockWorkflowRunningData.mockReturnValue({
|
||||
result: undefined as unknown as WorkflowRunningData['result'],
|
||||
tracing: [],
|
||||
})
|
||||
render(<Result />)
|
||||
|
||||
const detailTab = screen.getByRole('button', { name: /runLog\.detail/i })
|
||||
fireEvent.click(detailTab)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('loading')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should show loading in TRACING tab when no tracing data', async () => {
|
||||
mockWorkflowRunningData.mockReturnValue(createMockWorkflowRunningData({ tracing: [] }))
|
||||
render(<Result />)
|
||||
|
||||
const tracingTab = screen.getByRole('button', { name: /runLog\.tracing/i })
|
||||
fireEvent.click(tracingTab)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('loading')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// ResultPreview Component Tests
|
||||
// ============================================================================
|
||||
|
||||
// We need to import ResultPreview directly
|
||||
vi.doUnmock('./result/result-preview')
|
||||
|
||||
describe('ResultPreview', () => {
|
||||
let ResultPreview: typeof import('./result/result-preview').default
|
||||
|
||||
beforeAll(async () => {
|
||||
const previewModule = await import('./result/result-preview')
|
||||
ResultPreview = previewModule.default
|
||||
})
|
||||
|
||||
const mockOnSwitchToDetail = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
// Loading state
|
||||
describe('Loading State', () => {
|
||||
it('should show loading spinner when isRunning is true and no outputs', () => {
|
||||
render(
|
||||
<ResultPreview
|
||||
isRunning={true}
|
||||
outputs={undefined}
|
||||
error={undefined}
|
||||
onSwitchToDetail={mockOnSwitchToDetail}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('pipeline.result.resultPreview.loading')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not show loading when outputs are available', () => {
|
||||
render(
|
||||
<ResultPreview
|
||||
isRunning={true}
|
||||
outputs={createMockGeneralOutputs()}
|
||||
error={undefined}
|
||||
onSwitchToDetail={mockOnSwitchToDetail}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.queryByText('pipeline.result.resultPreview.loading')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Error state
|
||||
describe('Error State', () => {
|
||||
it('should show error message when not running and has error', () => {
|
||||
render(
|
||||
<ResultPreview
|
||||
isRunning={false}
|
||||
outputs={undefined}
|
||||
error="Test error message"
|
||||
onSwitchToDetail={mockOnSwitchToDetail}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('pipeline.result.resultPreview.error')).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'pipeline.result.resultPreview.viewDetails' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call onSwitchToDetail when View Details button is clicked', () => {
|
||||
render(
|
||||
<ResultPreview
|
||||
isRunning={false}
|
||||
outputs={undefined}
|
||||
error="Test error message"
|
||||
onSwitchToDetail={mockOnSwitchToDetail}
|
||||
/>,
|
||||
)
|
||||
|
||||
const viewDetailsButton = screen.getByRole('button', { name: 'pipeline.result.resultPreview.viewDetails' })
|
||||
fireEvent.click(viewDetailsButton)
|
||||
|
||||
expect(mockOnSwitchToDetail).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should not show error when still running', () => {
|
||||
render(
|
||||
<ResultPreview
|
||||
isRunning={true}
|
||||
outputs={undefined}
|
||||
error="Test error message"
|
||||
onSwitchToDetail={mockOnSwitchToDetail}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.queryByText('pipeline.result.resultPreview.error')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Success state with outputs
|
||||
describe('Success State with Outputs', () => {
|
||||
it('should render chunk content when outputs are available', () => {
|
||||
render(
|
||||
<ResultPreview
|
||||
isRunning={false}
|
||||
outputs={createMockGeneralOutputs(['test chunk content'])}
|
||||
error={undefined}
|
||||
onSwitchToDetail={mockOnSwitchToDetail}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Check that chunk content is rendered (the real ChunkCardList renders the content)
|
||||
expect(screen.getByText('test chunk content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render multiple chunks when provided', () => {
|
||||
render(
|
||||
<ResultPreview
|
||||
isRunning={false}
|
||||
outputs={createMockGeneralOutputs(['chunk one', 'chunk two'])}
|
||||
error={undefined}
|
||||
onSwitchToDetail={mockOnSwitchToDetail}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('chunk one')).toBeInTheDocument()
|
||||
expect(screen.getByText('chunk two')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show footer tip', () => {
|
||||
render(
|
||||
<ResultPreview
|
||||
isRunning={false}
|
||||
outputs={createMockGeneralOutputs()}
|
||||
error={undefined}
|
||||
onSwitchToDetail={mockOnSwitchToDetail}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText(/pipeline\.result\.resultPreview\.footerTip/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Edge cases
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle empty outputs gracefully', () => {
|
||||
render(
|
||||
<ResultPreview
|
||||
isRunning={false}
|
||||
outputs={null}
|
||||
error={undefined}
|
||||
onSwitchToDetail={mockOnSwitchToDetail}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Should not crash and should not show chunk card list
|
||||
expect(screen.queryByTestId('chunk-card-list')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle undefined outputs', () => {
|
||||
render(
|
||||
<ResultPreview
|
||||
isRunning={false}
|
||||
outputs={undefined}
|
||||
error={undefined}
|
||||
onSwitchToDetail={mockOnSwitchToDetail}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.queryByTestId('chunk-card-list')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// Tabs Component Tests
|
||||
// ============================================================================
|
||||
|
||||
vi.doUnmock('./result/tabs')
|
||||
|
||||
describe('Tabs', () => {
|
||||
let Tabs: typeof import('./result/tabs').default
|
||||
|
||||
beforeAll(async () => {
|
||||
const tabsModule = await import('./result/tabs')
|
||||
Tabs = tabsModule.default
|
||||
})
|
||||
|
||||
const mockSwitchTab = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
// Rendering tests
|
||||
describe('Rendering', () => {
|
||||
it('should render all three tabs', () => {
|
||||
render(
|
||||
<Tabs
|
||||
currentTab="RESULT"
|
||||
workflowRunningData={createMockWorkflowRunningData()}
|
||||
switchTab={mockSwitchTab}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByRole('button', { name: /runLog\.result/i })).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: /runLog\.detail/i })).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: /runLog\.tracing/i })).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Active tab styling
|
||||
describe('Active Tab Styling', () => {
|
||||
it('should highlight RESULT tab when currentTab is RESULT', () => {
|
||||
render(
|
||||
<Tabs
|
||||
currentTab="RESULT"
|
||||
workflowRunningData={createMockWorkflowRunningData()}
|
||||
switchTab={mockSwitchTab}
|
||||
/>,
|
||||
)
|
||||
|
||||
const resultTab = screen.getByRole('button', { name: /runLog\.result/i })
|
||||
expect(resultTab).toHaveClass('border-util-colors-blue-brand-blue-brand-600')
|
||||
})
|
||||
|
||||
it('should highlight DETAIL tab when currentTab is DETAIL', () => {
|
||||
render(
|
||||
<Tabs
|
||||
currentTab="DETAIL"
|
||||
workflowRunningData={createMockWorkflowRunningData()}
|
||||
switchTab={mockSwitchTab}
|
||||
/>,
|
||||
)
|
||||
|
||||
const detailTab = screen.getByRole('button', { name: /runLog\.detail/i })
|
||||
expect(detailTab).toHaveClass('border-util-colors-blue-brand-blue-brand-600')
|
||||
})
|
||||
})
|
||||
|
||||
// Tab click handling
|
||||
describe('Tab Click Handling', () => {
|
||||
it('should call switchTab with RESULT when RESULT tab is clicked', () => {
|
||||
render(
|
||||
<Tabs
|
||||
currentTab="DETAIL"
|
||||
workflowRunningData={createMockWorkflowRunningData()}
|
||||
switchTab={mockSwitchTab}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /runLog\.result/i }))
|
||||
|
||||
expect(mockSwitchTab).toHaveBeenCalledWith('RESULT')
|
||||
})
|
||||
|
||||
it('should call switchTab with DETAIL when DETAIL tab is clicked', () => {
|
||||
render(
|
||||
<Tabs
|
||||
currentTab="RESULT"
|
||||
workflowRunningData={createMockWorkflowRunningData()}
|
||||
switchTab={mockSwitchTab}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /runLog\.detail/i }))
|
||||
|
||||
expect(mockSwitchTab).toHaveBeenCalledWith('DETAIL')
|
||||
})
|
||||
|
||||
it('should call switchTab with TRACING when TRACING tab is clicked', () => {
|
||||
render(
|
||||
<Tabs
|
||||
currentTab="RESULT"
|
||||
workflowRunningData={createMockWorkflowRunningData()}
|
||||
switchTab={mockSwitchTab}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /runLog\.tracing/i }))
|
||||
|
||||
expect(mockSwitchTab).toHaveBeenCalledWith('TRACING')
|
||||
})
|
||||
})
|
||||
|
||||
// Disabled state when no data
|
||||
describe('Disabled State', () => {
|
||||
it('should disable tabs when workflowRunningData is undefined', () => {
|
||||
render(
|
||||
<Tabs
|
||||
currentTab="RESULT"
|
||||
workflowRunningData={undefined}
|
||||
switchTab={mockSwitchTab}
|
||||
/>,
|
||||
)
|
||||
|
||||
const resultTab = screen.getByRole('button', { name: /runLog\.result/i })
|
||||
expect(resultTab).toBeDisabled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// Tab Component Tests
|
||||
// ============================================================================
|
||||
|
||||
vi.doUnmock('./result/tabs/tab')
|
||||
|
||||
describe('Tab', () => {
|
||||
let Tab: typeof import('./result/tabs/tab').default
|
||||
|
||||
beforeAll(async () => {
|
||||
const tabModule = await import('./result/tabs/tab')
|
||||
Tab = tabModule.default
|
||||
})
|
||||
|
||||
const mockOnClick = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
// Rendering tests
|
||||
describe('Rendering', () => {
|
||||
it('should render tab with label', () => {
|
||||
render(
|
||||
<Tab
|
||||
isActive={false}
|
||||
label="Test Tab"
|
||||
value="TEST"
|
||||
workflowRunningData={createMockWorkflowRunningData()}
|
||||
onClick={mockOnClick}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Test Tab' })).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Active state styling
|
||||
describe('Active State', () => {
|
||||
it('should have active styles when isActive is true', () => {
|
||||
render(
|
||||
<Tab
|
||||
isActive={true}
|
||||
label="Active Tab"
|
||||
value="TEST"
|
||||
workflowRunningData={createMockWorkflowRunningData()}
|
||||
onClick={mockOnClick}
|
||||
/>,
|
||||
)
|
||||
|
||||
const tab = screen.getByRole('button')
|
||||
expect(tab).toHaveClass('border-util-colors-blue-brand-blue-brand-600', 'text-text-primary')
|
||||
})
|
||||
|
||||
it('should have inactive styles when isActive is false', () => {
|
||||
render(
|
||||
<Tab
|
||||
isActive={false}
|
||||
label="Inactive Tab"
|
||||
value="TEST"
|
||||
workflowRunningData={createMockWorkflowRunningData()}
|
||||
onClick={mockOnClick}
|
||||
/>,
|
||||
)
|
||||
|
||||
const tab = screen.getByRole('button')
|
||||
expect(tab).toHaveClass('border-transparent', 'text-text-tertiary')
|
||||
})
|
||||
})
|
||||
|
||||
// Click handling
|
||||
describe('Click Handling', () => {
|
||||
it('should call onClick with value when clicked', () => {
|
||||
render(
|
||||
<Tab
|
||||
isActive={false}
|
||||
label="Test Tab"
|
||||
value="MY_VALUE"
|
||||
workflowRunningData={createMockWorkflowRunningData()}
|
||||
onClick={mockOnClick}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
|
||||
expect(mockOnClick).toHaveBeenCalledWith('MY_VALUE')
|
||||
})
|
||||
|
||||
it('should not call onClick when disabled (no workflowRunningData)', () => {
|
||||
render(
|
||||
<Tab
|
||||
isActive={false}
|
||||
label="Test Tab"
|
||||
value="MY_VALUE"
|
||||
workflowRunningData={undefined}
|
||||
onClick={mockOnClick}
|
||||
/>,
|
||||
)
|
||||
|
||||
const tab = screen.getByRole('button')
|
||||
fireEvent.click(tab)
|
||||
|
||||
// The click handler is still called, but button is disabled
|
||||
expect(tab).toBeDisabled()
|
||||
})
|
||||
})
|
||||
|
||||
// Disabled state
|
||||
describe('Disabled State', () => {
|
||||
it('should be disabled when workflowRunningData is undefined', () => {
|
||||
render(
|
||||
<Tab
|
||||
isActive={false}
|
||||
label="Test Tab"
|
||||
value="TEST"
|
||||
workflowRunningData={undefined}
|
||||
onClick={mockOnClick}
|
||||
/>,
|
||||
)
|
||||
|
||||
const tab = screen.getByRole('button')
|
||||
expect(tab).toBeDisabled()
|
||||
expect(tab).toHaveClass('opacity-30')
|
||||
})
|
||||
|
||||
it('should not be disabled when workflowRunningData is provided', () => {
|
||||
render(
|
||||
<Tab
|
||||
isActive={false}
|
||||
label="Test Tab"
|
||||
value="TEST"
|
||||
workflowRunningData={createMockWorkflowRunningData()}
|
||||
onClick={mockOnClick}
|
||||
/>,
|
||||
)
|
||||
|
||||
const tab = screen.getByRole('button')
|
||||
expect(tab).not.toBeDisabled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// formatPreviewChunks Utility Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('formatPreviewChunks', () => {
|
||||
let formatPreviewChunks: typeof import('./result/result-preview/utils').formatPreviewChunks
|
||||
|
||||
beforeAll(async () => {
|
||||
const utilsModule = await import('./result/result-preview/utils')
|
||||
formatPreviewChunks = utilsModule.formatPreviewChunks
|
||||
})
|
||||
|
||||
// Edge cases
|
||||
describe('Edge Cases', () => {
|
||||
it('should return undefined for null outputs', () => {
|
||||
expect(formatPreviewChunks(null)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should return undefined for undefined outputs', () => {
|
||||
expect(formatPreviewChunks(undefined)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should return undefined for unknown chunk structure', () => {
|
||||
const outputs = {
|
||||
chunk_structure: 'unknown_mode',
|
||||
preview: [],
|
||||
}
|
||||
expect(formatPreviewChunks(outputs)).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
// General (text) chunks
|
||||
describe('General Chunks (ChunkingMode.text)', () => {
|
||||
it('should format general chunks correctly', () => {
|
||||
const outputs = createMockGeneralOutputs(['content1', 'content2', 'content3'])
|
||||
const result = formatPreviewChunks(outputs)
|
||||
|
||||
expect(result).toEqual(['content1', 'content2', 'content3'])
|
||||
})
|
||||
|
||||
it('should limit to RAG_PIPELINE_PREVIEW_CHUNK_NUM chunks', () => {
|
||||
const manyChunks = Array.from({ length: 10 }, (_, i) => `chunk${i}`)
|
||||
const outputs = createMockGeneralOutputs(manyChunks)
|
||||
const result = formatPreviewChunks(outputs) as string[]
|
||||
|
||||
// RAG_PIPELINE_PREVIEW_CHUNK_NUM is mocked to 5
|
||||
expect(result).toHaveLength(5)
|
||||
expect(result).toEqual(['chunk0', 'chunk1', 'chunk2', 'chunk3', 'chunk4'])
|
||||
})
|
||||
|
||||
it('should handle empty preview array', () => {
|
||||
const outputs = createMockGeneralOutputs([])
|
||||
const result = formatPreviewChunks(outputs)
|
||||
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
// Parent-child chunks
|
||||
describe('Parent-Child Chunks (ChunkingMode.parentChild)', () => {
|
||||
it('should format paragraph mode parent-child chunks correctly', () => {
|
||||
const outputs = createMockParentChildOutputs('paragraph')
|
||||
const result = formatPreviewChunks(outputs)
|
||||
|
||||
expect(result).toEqual({
|
||||
parent_child_chunks: [
|
||||
{ parent_content: 'parent1', child_contents: ['child1', 'child2'], parent_mode: 'paragraph' },
|
||||
{ parent_content: 'parent2', child_contents: ['child3', 'child4'], parent_mode: 'paragraph' },
|
||||
],
|
||||
parent_mode: 'paragraph',
|
||||
})
|
||||
})
|
||||
|
||||
it('should format full-doc mode parent-child chunks and limit child chunks', () => {
|
||||
const outputs = {
|
||||
chunk_structure: ChunkingMode.parentChild,
|
||||
parent_mode: 'full-doc' as const,
|
||||
preview: [
|
||||
{
|
||||
content: 'full-doc-parent',
|
||||
child_chunks: Array.from({ length: 10 }, (_, i) => `child${i}`),
|
||||
},
|
||||
],
|
||||
}
|
||||
const result = formatPreviewChunks(outputs)
|
||||
|
||||
expect(result).toEqual({
|
||||
parent_child_chunks: [
|
||||
{
|
||||
parent_content: 'full-doc-parent',
|
||||
child_contents: ['child0', 'child1', 'child2', 'child3', 'child4'], // Limited to 5
|
||||
parent_mode: 'full-doc',
|
||||
},
|
||||
],
|
||||
parent_mode: 'full-doc',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// QA chunks
|
||||
describe('QA Chunks (ChunkingMode.qa)', () => {
|
||||
it('should format QA chunks correctly', () => {
|
||||
const outputs = createMockQAOutputs()
|
||||
const result = formatPreviewChunks(outputs)
|
||||
|
||||
expect(result).toEqual({
|
||||
qa_chunks: [
|
||||
{ question: 'Q1', answer: 'A1' },
|
||||
{ question: 'Q2', answer: 'A2' },
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('should limit QA chunks to RAG_PIPELINE_PREVIEW_CHUNK_NUM', () => {
|
||||
const outputs = {
|
||||
chunk_structure: ChunkingMode.qa,
|
||||
qa_preview: Array.from({ length: 10 }, (_, i) => ({
|
||||
question: `Q${i}`,
|
||||
answer: `A${i}`,
|
||||
})),
|
||||
}
|
||||
const result = formatPreviewChunks(outputs) as { qa_chunks: Array<{ question: string, answer: string }> }
|
||||
|
||||
expect(result.qa_chunks).toHaveLength(5)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// Types Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('Types', () => {
|
||||
describe('TestRunStep Enum', () => {
|
||||
it('should have correct enum values', async () => {
|
||||
const { TestRunStep } = await import('./types')
|
||||
|
||||
expect(TestRunStep.dataSource).toBe('dataSource')
|
||||
expect(TestRunStep.documentProcessing).toBe('documentProcessing')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,549 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import Actions from './index'
|
||||
|
||||
// ============================================================================
|
||||
// Actions Component Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('Actions', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Rendering Tests
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', () => {
|
||||
// Arrange
|
||||
const handleNextStep = vi.fn()
|
||||
|
||||
// Act
|
||||
render(<Actions handleNextStep={handleNextStep} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByRole('button')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render button with translated text', () => {
|
||||
// Arrange
|
||||
const handleNextStep = vi.fn()
|
||||
|
||||
// Act
|
||||
render(<Actions handleNextStep={handleNextStep} />)
|
||||
|
||||
// Assert - Translation mock returns key with namespace prefix
|
||||
expect(screen.getByText('datasetCreation.stepOne.button')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render with correct container structure', () => {
|
||||
// Arrange
|
||||
const handleNextStep = vi.fn()
|
||||
|
||||
// Act
|
||||
const { container } = render(<Actions handleNextStep={handleNextStep} />)
|
||||
|
||||
// Assert
|
||||
const wrapper = container.firstChild as HTMLElement
|
||||
expect(wrapper.className).toContain('flex')
|
||||
expect(wrapper.className).toContain('justify-end')
|
||||
expect(wrapper.className).toContain('p-4')
|
||||
expect(wrapper.className).toContain('pt-2')
|
||||
})
|
||||
|
||||
it('should render span with px-0.5 class around text', () => {
|
||||
// Arrange
|
||||
const handleNextStep = vi.fn()
|
||||
|
||||
// Act
|
||||
const { container } = render(<Actions handleNextStep={handleNextStep} />)
|
||||
|
||||
// Assert
|
||||
const span = container.querySelector('span')
|
||||
expect(span).toBeInTheDocument()
|
||||
expect(span?.className).toContain('px-0.5')
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Props Variations Tests
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Props Variations', () => {
|
||||
it('should pass disabled=true to button when disabled prop is true', () => {
|
||||
// Arrange
|
||||
const handleNextStep = vi.fn()
|
||||
|
||||
// Act
|
||||
render(<Actions disabled={true} handleNextStep={handleNextStep} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByRole('button')).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should pass disabled=false to button when disabled prop is false', () => {
|
||||
// Arrange
|
||||
const handleNextStep = vi.fn()
|
||||
|
||||
// Act
|
||||
render(<Actions disabled={false} handleNextStep={handleNextStep} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByRole('button')).not.toBeDisabled()
|
||||
})
|
||||
|
||||
it('should not disable button when disabled prop is undefined', () => {
|
||||
// Arrange
|
||||
const handleNextStep = vi.fn()
|
||||
|
||||
// Act
|
||||
render(<Actions handleNextStep={handleNextStep} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByRole('button')).not.toBeDisabled()
|
||||
})
|
||||
|
||||
it('should handle disabled switching from true to false', () => {
|
||||
// Arrange
|
||||
const handleNextStep = vi.fn()
|
||||
|
||||
// Act
|
||||
const { rerender } = render(
|
||||
<Actions disabled={true} handleNextStep={handleNextStep} />,
|
||||
)
|
||||
|
||||
// Assert - Initially disabled
|
||||
expect(screen.getByRole('button')).toBeDisabled()
|
||||
|
||||
// Act - Rerender with disabled=false
|
||||
rerender(<Actions disabled={false} handleNextStep={handleNextStep} />)
|
||||
|
||||
// Assert - Now enabled
|
||||
expect(screen.getByRole('button')).not.toBeDisabled()
|
||||
})
|
||||
|
||||
it('should handle disabled switching from false to true', () => {
|
||||
// Arrange
|
||||
const handleNextStep = vi.fn()
|
||||
|
||||
// Act
|
||||
const { rerender } = render(
|
||||
<Actions disabled={false} handleNextStep={handleNextStep} />,
|
||||
)
|
||||
|
||||
// Assert - Initially enabled
|
||||
expect(screen.getByRole('button')).not.toBeDisabled()
|
||||
|
||||
// Act - Rerender with disabled=true
|
||||
rerender(<Actions disabled={true} handleNextStep={handleNextStep} />)
|
||||
|
||||
// Assert - Now disabled
|
||||
expect(screen.getByRole('button')).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should handle undefined disabled becoming true', () => {
|
||||
// Arrange
|
||||
const handleNextStep = vi.fn()
|
||||
|
||||
// Act
|
||||
const { rerender } = render(
|
||||
<Actions handleNextStep={handleNextStep} />,
|
||||
)
|
||||
|
||||
// Assert - Initially not disabled (undefined)
|
||||
expect(screen.getByRole('button')).not.toBeDisabled()
|
||||
|
||||
// Act - Rerender with disabled=true
|
||||
rerender(<Actions disabled={true} handleNextStep={handleNextStep} />)
|
||||
|
||||
// Assert - Now disabled
|
||||
expect(screen.getByRole('button')).toBeDisabled()
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// User Interaction Tests
|
||||
// -------------------------------------------------------------------------
|
||||
describe('User Interactions', () => {
|
||||
it('should call handleNextStep when button is clicked', () => {
|
||||
// Arrange
|
||||
const handleNextStep = vi.fn()
|
||||
|
||||
// Act
|
||||
render(<Actions handleNextStep={handleNextStep} />)
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
|
||||
// Assert
|
||||
expect(handleNextStep).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should call handleNextStep exactly once per click', () => {
|
||||
// Arrange
|
||||
const handleNextStep = vi.fn()
|
||||
|
||||
// Act
|
||||
render(<Actions handleNextStep={handleNextStep} />)
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
|
||||
// Assert
|
||||
expect(handleNextStep).toHaveBeenCalled()
|
||||
expect(handleNextStep.mock.calls).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('should call handleNextStep multiple times on multiple clicks', () => {
|
||||
// Arrange
|
||||
const handleNextStep = vi.fn()
|
||||
|
||||
// Act
|
||||
render(<Actions handleNextStep={handleNextStep} />)
|
||||
const button = screen.getByRole('button')
|
||||
fireEvent.click(button)
|
||||
fireEvent.click(button)
|
||||
fireEvent.click(button)
|
||||
|
||||
// Assert
|
||||
expect(handleNextStep).toHaveBeenCalledTimes(3)
|
||||
})
|
||||
|
||||
it('should not call handleNextStep when button is disabled and clicked', () => {
|
||||
// Arrange
|
||||
const handleNextStep = vi.fn()
|
||||
|
||||
// Act
|
||||
render(<Actions disabled={true} handleNextStep={handleNextStep} />)
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
|
||||
// Assert - Disabled button should not trigger onClick
|
||||
expect(handleNextStep).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle rapid clicks when not disabled', () => {
|
||||
// Arrange
|
||||
const handleNextStep = vi.fn()
|
||||
|
||||
// Act
|
||||
render(<Actions handleNextStep={handleNextStep} />)
|
||||
const button = screen.getByRole('button')
|
||||
|
||||
// Simulate rapid clicks
|
||||
for (let i = 0; i < 10; i++)
|
||||
fireEvent.click(button)
|
||||
|
||||
// Assert
|
||||
expect(handleNextStep).toHaveBeenCalledTimes(10)
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Callback Stability Tests
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Callback Stability', () => {
|
||||
it('should use the new handleNextStep when prop changes', () => {
|
||||
// Arrange
|
||||
const handleNextStep1 = vi.fn()
|
||||
const handleNextStep2 = vi.fn()
|
||||
|
||||
// Act
|
||||
const { rerender } = render(
|
||||
<Actions handleNextStep={handleNextStep1} />,
|
||||
)
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
|
||||
rerender(<Actions handleNextStep={handleNextStep2} />)
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
|
||||
// Assert
|
||||
expect(handleNextStep1).toHaveBeenCalledTimes(1)
|
||||
expect(handleNextStep2).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should maintain functionality after rerender with same props', () => {
|
||||
// Arrange
|
||||
const handleNextStep = vi.fn()
|
||||
|
||||
// Act
|
||||
const { rerender } = render(
|
||||
<Actions handleNextStep={handleNextStep} />,
|
||||
)
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
|
||||
rerender(<Actions handleNextStep={handleNextStep} />)
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
|
||||
// Assert
|
||||
expect(handleNextStep).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('should work correctly when handleNextStep changes multiple times', () => {
|
||||
// Arrange
|
||||
const handleNextStep1 = vi.fn()
|
||||
const handleNextStep2 = vi.fn()
|
||||
const handleNextStep3 = vi.fn()
|
||||
|
||||
// Act
|
||||
const { rerender } = render(
|
||||
<Actions handleNextStep={handleNextStep1} />,
|
||||
)
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
|
||||
rerender(<Actions handleNextStep={handleNextStep2} />)
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
|
||||
rerender(<Actions handleNextStep={handleNextStep3} />)
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
|
||||
// Assert
|
||||
expect(handleNextStep1).toHaveBeenCalledTimes(1)
|
||||
expect(handleNextStep2).toHaveBeenCalledTimes(1)
|
||||
expect(handleNextStep3).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Memoization Tests
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Memoization', () => {
|
||||
it('should be wrapped with React.memo', () => {
|
||||
// Arrange
|
||||
const handleNextStep = vi.fn()
|
||||
|
||||
// Act - Verify component is memoized by checking display name pattern
|
||||
const { rerender } = render(
|
||||
<Actions handleNextStep={handleNextStep} />,
|
||||
)
|
||||
|
||||
// Rerender with same props should work without issues
|
||||
rerender(<Actions handleNextStep={handleNextStep} />)
|
||||
|
||||
// Assert - Component should render correctly after rerender
|
||||
expect(screen.getByRole('button')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not break when props remain the same across rerenders', () => {
|
||||
// Arrange
|
||||
const handleNextStep = vi.fn()
|
||||
|
||||
// Act
|
||||
const { rerender } = render(
|
||||
<Actions disabled={false} handleNextStep={handleNextStep} />,
|
||||
)
|
||||
|
||||
// Multiple rerenders with same props
|
||||
for (let i = 0; i < 5; i++) {
|
||||
rerender(<Actions disabled={false} handleNextStep={handleNextStep} />)
|
||||
}
|
||||
|
||||
// Assert - Should still function correctly
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
expect(handleNextStep).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should update correctly when only disabled prop changes', () => {
|
||||
// Arrange
|
||||
const handleNextStep = vi.fn()
|
||||
|
||||
// Act
|
||||
const { rerender } = render(
|
||||
<Actions disabled={false} handleNextStep={handleNextStep} />,
|
||||
)
|
||||
|
||||
// Assert - Initially not disabled
|
||||
expect(screen.getByRole('button')).not.toBeDisabled()
|
||||
|
||||
// Act - Change only disabled prop
|
||||
rerender(<Actions disabled={true} handleNextStep={handleNextStep} />)
|
||||
|
||||
// Assert - Should reflect the new disabled state
|
||||
expect(screen.getByRole('button')).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should update correctly when only handleNextStep prop changes', () => {
|
||||
// Arrange
|
||||
const handleNextStep1 = vi.fn()
|
||||
const handleNextStep2 = vi.fn()
|
||||
|
||||
// Act
|
||||
const { rerender } = render(
|
||||
<Actions disabled={false} handleNextStep={handleNextStep1} />,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
expect(handleNextStep1).toHaveBeenCalledTimes(1)
|
||||
|
||||
// Act - Change only handleNextStep prop
|
||||
rerender(<Actions disabled={false} handleNextStep={handleNextStep2} />)
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
|
||||
// Assert - New callback should be used
|
||||
expect(handleNextStep1).toHaveBeenCalledTimes(1)
|
||||
expect(handleNextStep2).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Edge Cases Tests
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Edge Cases', () => {
|
||||
it('should call handleNextStep even if it has side effects', () => {
|
||||
// Arrange
|
||||
let sideEffectValue = 0
|
||||
const handleNextStep = vi.fn(() => {
|
||||
sideEffectValue = 42
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<Actions handleNextStep={handleNextStep} />)
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
|
||||
// Assert
|
||||
expect(handleNextStep).toHaveBeenCalledTimes(1)
|
||||
expect(sideEffectValue).toBe(42)
|
||||
})
|
||||
|
||||
it('should handle handleNextStep that returns a value', () => {
|
||||
// Arrange
|
||||
const handleNextStep = vi.fn(() => 'return value')
|
||||
|
||||
// Act
|
||||
render(<Actions handleNextStep={handleNextStep} />)
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
|
||||
// Assert
|
||||
expect(handleNextStep).toHaveBeenCalledTimes(1)
|
||||
expect(handleNextStep).toHaveReturnedWith('return value')
|
||||
})
|
||||
|
||||
it('should handle handleNextStep that is async', async () => {
|
||||
// Arrange
|
||||
const handleNextStep = vi.fn().mockResolvedValue(undefined)
|
||||
|
||||
// Act
|
||||
render(<Actions handleNextStep={handleNextStep} />)
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
|
||||
// Assert
|
||||
expect(handleNextStep).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should render correctly with both disabled=true and handleNextStep', () => {
|
||||
// Arrange
|
||||
const handleNextStep = vi.fn()
|
||||
|
||||
// Act
|
||||
render(<Actions disabled={true} handleNextStep={handleNextStep} />)
|
||||
|
||||
// Assert
|
||||
const button = screen.getByRole('button')
|
||||
expect(button).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should handle component unmount gracefully', () => {
|
||||
// Arrange
|
||||
const handleNextStep = vi.fn()
|
||||
|
||||
// Act
|
||||
const { unmount } = render(<Actions handleNextStep={handleNextStep} />)
|
||||
|
||||
// Assert - Unmount should not throw
|
||||
expect(() => unmount()).not.toThrow()
|
||||
})
|
||||
|
||||
it('should handle disabled as boolean-like falsy value', () => {
|
||||
// Arrange
|
||||
const handleNextStep = vi.fn()
|
||||
|
||||
// Act - Test with explicit false
|
||||
render(<Actions disabled={false} handleNextStep={handleNextStep} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByRole('button')).not.toBeDisabled()
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Accessibility Tests
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Accessibility', () => {
|
||||
it('should have button element that can receive focus', () => {
|
||||
// Arrange
|
||||
const handleNextStep = vi.fn()
|
||||
|
||||
// Act
|
||||
render(<Actions handleNextStep={handleNextStep} />)
|
||||
const button = screen.getByRole('button')
|
||||
|
||||
// Assert - Button should be focusable (not disabled by default)
|
||||
expect(button).not.toBeDisabled()
|
||||
})
|
||||
|
||||
it('should indicate disabled state correctly', () => {
|
||||
// Arrange
|
||||
const handleNextStep = vi.fn()
|
||||
|
||||
// Act
|
||||
render(<Actions disabled={true} handleNextStep={handleNextStep} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByRole('button')).toHaveAttribute('disabled')
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Integration Tests
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Integration', () => {
|
||||
it('should work in a typical workflow: enable -> click -> disable', () => {
|
||||
// Arrange
|
||||
const handleNextStep = vi.fn()
|
||||
|
||||
// Act - Start enabled
|
||||
const { rerender } = render(
|
||||
<Actions disabled={false} handleNextStep={handleNextStep} />,
|
||||
)
|
||||
|
||||
// Assert - Can click when enabled
|
||||
expect(screen.getByRole('button')).not.toBeDisabled()
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
expect(handleNextStep).toHaveBeenCalledTimes(1)
|
||||
|
||||
// Act - Disable after click (simulating loading state)
|
||||
rerender(<Actions disabled={true} handleNextStep={handleNextStep} />)
|
||||
|
||||
// Assert - Cannot click when disabled
|
||||
expect(screen.getByRole('button')).toBeDisabled()
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
expect(handleNextStep).toHaveBeenCalledTimes(1) // Still 1, not 2
|
||||
|
||||
// Act - Re-enable
|
||||
rerender(<Actions disabled={false} handleNextStep={handleNextStep} />)
|
||||
|
||||
// Assert - Can click again
|
||||
expect(screen.getByRole('button')).not.toBeDisabled()
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
expect(handleNextStep).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('should maintain consistent rendering across multiple state changes', () => {
|
||||
// Arrange
|
||||
const handleNextStep = vi.fn()
|
||||
|
||||
// Act
|
||||
const { rerender } = render(
|
||||
<Actions disabled={false} handleNextStep={handleNextStep} />,
|
||||
)
|
||||
|
||||
// Toggle disabled state multiple times
|
||||
const states = [true, false, true, false, true]
|
||||
states.forEach((disabled) => {
|
||||
rerender(<Actions disabled={disabled} handleNextStep={handleNextStep} />)
|
||||
if (disabled)
|
||||
expect(screen.getByRole('button')).toBeDisabled()
|
||||
else
|
||||
expect(screen.getByRole('button')).not.toBeDisabled()
|
||||
})
|
||||
|
||||
// Assert - Button should still render correctly
|
||||
expect(screen.getByRole('button')).toBeInTheDocument()
|
||||
expect(screen.getByText('datasetCreation.stepOne.button')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -85,7 +85,11 @@ const PublishAsKnowledgePipelineModal = ({
|
||||
>
|
||||
<div className="title-2xl-semi-bold relative flex items-center p-6 pb-3 pr-14 text-text-primary">
|
||||
{t('common.publishAs', { ns: 'pipeline' })}
|
||||
<div className="absolute right-5 top-5 flex h-8 w-8 cursor-pointer items-center justify-center" onClick={onCancel}>
|
||||
<div
|
||||
data-testid="publish-modal-close-btn"
|
||||
className="absolute right-5 top-5 flex h-8 w-8 cursor-pointer items-center justify-center"
|
||||
onClick={onCancel}
|
||||
>
|
||||
<RiCloseLine className="h-4 w-4 text-text-tertiary" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -49,6 +49,9 @@ const useConfigVision = (model: ModelConfig, {
|
||||
variable_selector: ['sys', 'files'],
|
||||
}
|
||||
}
|
||||
else if (!enabled) {
|
||||
delete draft.configs
|
||||
}
|
||||
})
|
||||
onChange(newPayload)
|
||||
}, [isChatMode, onChange, payload])
|
||||
|
||||
@ -19,6 +19,7 @@ import { useDocLink } from '@/context/i18n'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import { fetchInitValidateStatus, fetchSetupStatus, login, setup } from '@/service/common'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { encryptPassword as encodePassword } from '@/utils/encryption'
|
||||
import Loading from '../components/base/loading'
|
||||
|
||||
const accountFormSchema = z.object({
|
||||
@ -68,7 +69,7 @@ const InstallForm = () => {
|
||||
url: '/login',
|
||||
body: {
|
||||
email: data.email,
|
||||
password: data.password,
|
||||
password: encodePassword(data.password),
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import type { Viewport } from 'next'
|
||||
import { Provider as JotaiProvider } from 'jotai'
|
||||
import { ThemeProvider } from 'next-themes'
|
||||
import { Instrument_Serif } from 'next/font/google'
|
||||
import { NuqsAdapter } from 'nuqs/adapters/next/app'
|
||||
@ -91,27 +92,29 @@ const LocaleLayout = async ({
|
||||
{...datasetMap}
|
||||
>
|
||||
<ReactScanLoader />
|
||||
<ThemeProvider
|
||||
attribute="data-theme"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
enableColorScheme={false}
|
||||
>
|
||||
<NuqsAdapter>
|
||||
<BrowserInitializer>
|
||||
<SentryInitializer>
|
||||
<TanstackQueryInitializer>
|
||||
<I18nServer>
|
||||
<GlobalPublicStoreProvider>
|
||||
{children}
|
||||
</GlobalPublicStoreProvider>
|
||||
</I18nServer>
|
||||
</TanstackQueryInitializer>
|
||||
</SentryInitializer>
|
||||
</BrowserInitializer>
|
||||
</NuqsAdapter>
|
||||
</ThemeProvider>
|
||||
<JotaiProvider>
|
||||
<ThemeProvider
|
||||
attribute="data-theme"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
enableColorScheme={false}
|
||||
>
|
||||
<NuqsAdapter>
|
||||
<BrowserInitializer>
|
||||
<SentryInitializer>
|
||||
<TanstackQueryInitializer>
|
||||
<I18nServer>
|
||||
<GlobalPublicStoreProvider>
|
||||
{children}
|
||||
</GlobalPublicStoreProvider>
|
||||
</I18nServer>
|
||||
</TanstackQueryInitializer>
|
||||
</SentryInitializer>
|
||||
</BrowserInitializer>
|
||||
</NuqsAdapter>
|
||||
</ThemeProvider>
|
||||
</JotaiProvider>
|
||||
<RoutePrefixHandle />
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -1,266 +0,0 @@
|
||||
.main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 6rem;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.description {
|
||||
display: inherit;
|
||||
justify-content: inherit;
|
||||
align-items: inherit;
|
||||
font-size: 0.85rem;
|
||||
max-width: var(--max-width);
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.description a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.description p {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
padding: 1rem;
|
||||
background-color: rgba(var(--callout-rgb), 0.5);
|
||||
border: 1px solid rgba(var(--callout-border-rgb), 0.3);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.code {
|
||||
font-weight: 700;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(33%, auto));
|
||||
width: var(--max-width);
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 1rem 1.2rem;
|
||||
border-radius: var(--border-radius);
|
||||
background: rgba(var(--card-rgb), 0);
|
||||
border: 1px solid rgba(var(--card-border-rgb), 0);
|
||||
transition: background 200ms, border 200ms;
|
||||
}
|
||||
|
||||
.card span {
|
||||
display: inline-block;
|
||||
transition: transform 200ms;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.7rem;
|
||||
}
|
||||
|
||||
.card p {
|
||||
margin: 0;
|
||||
opacity: 0.6;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
max-width: 34ch;
|
||||
}
|
||||
|
||||
.center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
padding: 4rem 0;
|
||||
}
|
||||
|
||||
.center::before {
|
||||
background: var(--secondary-glow);
|
||||
border-radius: 50%;
|
||||
width: 480px;
|
||||
height: 360px;
|
||||
margin-left: -400px;
|
||||
}
|
||||
|
||||
.center::after {
|
||||
background: var(--primary-glow);
|
||||
width: 240px;
|
||||
height: 180px;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.center::before,
|
||||
.center::after {
|
||||
content: '';
|
||||
left: 50%;
|
||||
position: absolute;
|
||||
filter: blur(45px);
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
.logo,
|
||||
.thirteen {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.thirteen {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 75px;
|
||||
height: 75px;
|
||||
padding: 25px 10px;
|
||||
margin-left: 16px;
|
||||
transform: translateZ(0);
|
||||
border-radius: var(--border-radius);
|
||||
overflow: hidden;
|
||||
box-shadow: 0px 2px 8px -1px #0000001a;
|
||||
}
|
||||
|
||||
.thirteen::before,
|
||||
.thirteen::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
/* Conic Gradient Animation */
|
||||
.thirteen::before {
|
||||
animation: 6s rotate linear infinite;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background: var(--tile-border);
|
||||
}
|
||||
|
||||
/* Inner Square */
|
||||
.thirteen::after {
|
||||
inset: 0;
|
||||
padding: 1px;
|
||||
border-radius: var(--border-radius);
|
||||
background: linear-gradient(to bottom right,
|
||||
rgba(var(--tile-start-rgb), 1),
|
||||
rgba(var(--tile-end-rgb), 1));
|
||||
background-clip: content-box;
|
||||
}
|
||||
|
||||
/* Enable hover only on non-touch devices */
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
.card:hover {
|
||||
background: rgba(var(--card-rgb), 0.1);
|
||||
border: 1px solid rgba(var(--card-border-rgb), 0.15);
|
||||
}
|
||||
|
||||
.card:hover span {
|
||||
transform: translateX(4px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion) {
|
||||
.thirteen::before {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.card:hover span {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile and Tablet */
|
||||
@media (max-width: 1023px) {
|
||||
.content {
|
||||
padding: 4rem;
|
||||
}
|
||||
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
margin-bottom: 120px;
|
||||
max-width: 320px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 1rem 2.5rem;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.center {
|
||||
padding: 8rem 0 6rem;
|
||||
}
|
||||
|
||||
.center::before {
|
||||
transform: none;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.description a {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.description p,
|
||||
.description div {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.description p {
|
||||
align-items: center;
|
||||
inset: 0 0 auto;
|
||||
padding: 2rem 1rem 1.4rem;
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25);
|
||||
background: linear-gradient(to bottom,
|
||||
rgba(var(--background-start-rgb), 1),
|
||||
rgba(var(--callout-rgb), 0.5));
|
||||
background-clip: padding-box;
|
||||
backdrop-filter: blur(24px);
|
||||
}
|
||||
|
||||
.description div {
|
||||
align-items: flex-end;
|
||||
pointer-events: none;
|
||||
inset: auto 0 0;
|
||||
padding: 2rem;
|
||||
height: 200px;
|
||||
background: linear-gradient(to bottom,
|
||||
transparent 0%,
|
||||
rgb(var(--background-end-rgb)) 40%);
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.vercelLogo {
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
.logo,
|
||||
.thirteen img {
|
||||
filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
from {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
}
|
||||
158
web/app/signup/components/input-mail.spec.tsx
Normal file
158
web/app/signup/components/input-mail.spec.tsx
Normal file
@ -0,0 +1,158 @@
|
||||
import type { MockedFunction } from 'vitest'
|
||||
import type { SystemFeatures } from '@/types/feature'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { useSendMail } from '@/service/use-common'
|
||||
import { defaultSystemFeatures } from '@/types/feature'
|
||||
import Form from './input-mail'
|
||||
|
||||
const mockSubmitMail = vi.fn()
|
||||
const mockOnSuccess = vi.fn()
|
||||
|
||||
type SystemFeaturesOverrides = Partial<Omit<SystemFeatures, 'branding'>> & {
|
||||
branding?: Partial<SystemFeatures['branding']>
|
||||
}
|
||||
|
||||
const buildSystemFeatures = (overrides: SystemFeaturesOverrides = {}): SystemFeatures => ({
|
||||
...defaultSystemFeatures,
|
||||
...overrides,
|
||||
branding: {
|
||||
...defaultSystemFeatures.branding,
|
||||
...overrides.branding,
|
||||
},
|
||||
})
|
||||
|
||||
vi.mock('next/link', () => ({
|
||||
default: ({ children, href, className, target, rel }: { children: React.ReactNode, href: string, className?: string, target?: string, rel?: string }) => (
|
||||
<a href={href} className={className} target={target} rel={rel}>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/global-public-context', () => ({
|
||||
useGlobalPublicStore: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/i18n', () => ({
|
||||
useLocale: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-common', () => ({
|
||||
useSendMail: vi.fn(),
|
||||
}))
|
||||
|
||||
type UseSendMailResult = ReturnType<typeof useSendMail>
|
||||
|
||||
const mockUseGlobalPublicStore = useGlobalPublicStore as unknown as MockedFunction<typeof useGlobalPublicStore>
|
||||
const mockUseLocale = useLocale as unknown as MockedFunction<typeof useLocale>
|
||||
const mockUseSendMail = useSendMail as unknown as MockedFunction<typeof useSendMail>
|
||||
|
||||
const renderForm = ({
|
||||
brandingEnabled = false,
|
||||
isPending = false,
|
||||
}: {
|
||||
brandingEnabled?: boolean
|
||||
isPending?: boolean
|
||||
} = {}) => {
|
||||
mockUseGlobalPublicStore.mockReturnValue({
|
||||
systemFeatures: buildSystemFeatures({
|
||||
branding: { enabled: brandingEnabled },
|
||||
}),
|
||||
})
|
||||
mockUseLocale.mockReturnValue('en-US')
|
||||
mockUseSendMail.mockReturnValue({
|
||||
mutateAsync: mockSubmitMail,
|
||||
isPending,
|
||||
} as unknown as UseSendMailResult)
|
||||
return render(<Form onSuccess={mockOnSuccess} />)
|
||||
}
|
||||
|
||||
describe('InputMail Form', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockSubmitMail.mockResolvedValue({ result: 'success', data: 'token' })
|
||||
})
|
||||
|
||||
// Rendering baseline UI elements.
|
||||
describe('Rendering', () => {
|
||||
it('should render email input and submit button', () => {
|
||||
renderForm()
|
||||
|
||||
expect(screen.getByLabelText('login.email')).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'login.signup.verifyMail' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('link', { name: 'login.signup.signIn' })).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Prop-driven branding content visibility.
|
||||
describe('Props', () => {
|
||||
it('should show terms links when branding is disabled', () => {
|
||||
renderForm({ brandingEnabled: false })
|
||||
|
||||
expect(screen.getByRole('link', { name: 'login.tos' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('link', { name: 'login.pp' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should hide terms links when branding is enabled', () => {
|
||||
renderForm({ brandingEnabled: true })
|
||||
|
||||
expect(screen.queryByRole('link', { name: 'login.tos' })).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('link', { name: 'login.pp' })).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Submission flow and mutation integration.
|
||||
describe('User Interactions', () => {
|
||||
it('should submit email and call onSuccess when mutation succeeds', async () => {
|
||||
renderForm()
|
||||
const input = screen.getByLabelText('login.email')
|
||||
const button = screen.getByRole('button', { name: 'login.signup.verifyMail' })
|
||||
|
||||
fireEvent.change(input, { target: { value: 'test@example.com' } })
|
||||
fireEvent.click(button)
|
||||
|
||||
expect(mockSubmitMail).toHaveBeenCalledWith({
|
||||
email: 'test@example.com',
|
||||
language: 'en-US',
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockOnSuccess).toHaveBeenCalledWith('test@example.com', 'token')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Validation and failure paths.
|
||||
describe('Edge Cases', () => {
|
||||
it('should block submission when email is invalid', () => {
|
||||
const { container } = renderForm()
|
||||
const form = container.querySelector('form')
|
||||
const input = screen.getByLabelText('login.email')
|
||||
|
||||
fireEvent.change(input, { target: { value: 'invalid-email' } })
|
||||
expect(form).not.toBeNull()
|
||||
fireEvent.submit(form as HTMLFormElement)
|
||||
|
||||
expect(mockSubmitMail).not.toHaveBeenCalled()
|
||||
expect(mockOnSuccess).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not call onSuccess when mutation does not succeed', async () => {
|
||||
mockSubmitMail.mockResolvedValue({ result: 'failed', data: 'token' })
|
||||
renderForm()
|
||||
const input = screen.getByLabelText('login.email')
|
||||
const button = screen.getByRole('button', { name: 'login.signup.verifyMail' })
|
||||
|
||||
fireEvent.change(input, { target: { value: 'test@example.com' } })
|
||||
fireEvent.click(button)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSubmitMail).toHaveBeenCalled()
|
||||
})
|
||||
expect(mockOnSuccess).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,6 +1,5 @@
|
||||
'use client'
|
||||
import type { MailSendResponse } from '@/service/use-common'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import Link from 'next/link'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -27,6 +26,9 @@ export default function Form({
|
||||
const { mutateAsync: submitMail, isPending } = useSendMail()
|
||||
|
||||
const handleSubmit = useCallback(async () => {
|
||||
if (isPending)
|
||||
return
|
||||
|
||||
if (!email) {
|
||||
Toast.notify({ type: 'error', message: t('error.emailEmpty', { ns: 'login' }) })
|
||||
return
|
||||
@ -41,10 +43,14 @@ export default function Form({
|
||||
const res = await submitMail({ email, language: locale })
|
||||
if ((res as MailSendResponse).result === 'success')
|
||||
onSuccess(email, (res as MailSendResponse).data)
|
||||
}, [email, locale, submitMail, t])
|
||||
}, [email, locale, submitMail, t, isPending, onSuccess])
|
||||
|
||||
return (
|
||||
<form onSubmit={noop}>
|
||||
<form onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
handleSubmit()
|
||||
}}
|
||||
>
|
||||
<div className="mb-3">
|
||||
<label htmlFor="email" className="system-md-semibold my-2 text-text-secondary">
|
||||
{t('email', { ns: 'login' })}
|
||||
@ -65,7 +71,7 @@ export default function Form({
|
||||
<Button
|
||||
tabIndex={2}
|
||||
variant="primary"
|
||||
onClick={handleSubmit}
|
||||
type="submit"
|
||||
disabled={isPending || !email}
|
||||
className="w-full"
|
||||
>
|
||||
@ -88,7 +94,7 @@ export default function Form({
|
||||
<>
|
||||
<div className="system-xs-regular mt-3 block w-full text-text-tertiary">
|
||||
{t('tosDesc', { ns: 'login' })}
|
||||
|
||||
|
||||
<Link
|
||||
className="system-xs-medium text-text-secondary hover:underline"
|
||||
target="_blank"
|
||||
@ -97,7 +103,7 @@ export default function Form({
|
||||
>
|
||||
{t('tos', { ns: 'login' })}
|
||||
</Link>
|
||||
&
|
||||
&
|
||||
<Link
|
||||
className="system-xs-medium text-text-secondary hover:underline"
|
||||
target="_blank"
|
||||
|
||||
@ -134,7 +134,7 @@ export const ProviderContextProvider = ({
|
||||
|
||||
const [enableEducationPlan, setEnableEducationPlan] = useState(false)
|
||||
const [isEducationWorkspace, setIsEducationWorkspace] = useState(false)
|
||||
const { data: educationAccountInfo, isLoading: isLoadingEducationAccountInfo, isFetching: isFetchingEducationAccountInfo } = useEducationStatus(!enableEducationPlan)
|
||||
const { data: educationAccountInfo, isLoading: isLoadingEducationAccountInfo, isFetching: isFetchingEducationAccountInfo, isFetchedAfterMount: isEducationDataFetchedAfterMount } = useEducationStatus(!enableEducationPlan)
|
||||
const [isAllowTransferWorkspace, setIsAllowTransferWorkspace] = useState(false)
|
||||
const [isAllowPublishAsCustomKnowledgePipelineTemplate, setIsAllowPublishAsCustomKnowledgePipelineTemplate] = useState(false)
|
||||
|
||||
@ -240,9 +240,9 @@ export const ProviderContextProvider = ({
|
||||
datasetOperatorEnabled,
|
||||
enableEducationPlan,
|
||||
isEducationWorkspace,
|
||||
isEducationAccount: educationAccountInfo?.is_student || false,
|
||||
allowRefreshEducationVerify: educationAccountInfo?.allow_refresh || false,
|
||||
educationAccountExpireAt: educationAccountInfo?.expire_at || null,
|
||||
isEducationAccount: isEducationDataFetchedAfterMount ? (educationAccountInfo?.is_student ?? false) : false,
|
||||
allowRefreshEducationVerify: isEducationDataFetchedAfterMount ? (educationAccountInfo?.allow_refresh ?? false) : false,
|
||||
educationAccountExpireAt: isEducationDataFetchedAfterMount ? (educationAccountInfo?.expire_at ?? null) : null,
|
||||
isLoadingEducationAccountInfo,
|
||||
isFetchingEducationAccountInfo,
|
||||
webappCopyrightEnabled,
|
||||
|
||||
@ -29,11 +29,6 @@ export default {
|
||||
const options = context.options[0] || {}
|
||||
const mode = options.mode || 'any'
|
||||
|
||||
/**
|
||||
* Check if this is a t() function call
|
||||
* @param {import('estree').CallExpression} node
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isTCall(node) {
|
||||
// Direct t() call
|
||||
if (node.callee.type === 'Identifier' && node.callee.name === 't')
|
||||
|
||||
@ -19,26 +19,11 @@ export default {
|
||||
create(context) {
|
||||
const sourceCode = context.sourceCode
|
||||
|
||||
// Track all t() calls to fix
|
||||
/** @type {Array<{ node: import('estree').CallExpression }>} */
|
||||
const tCallsToFix = []
|
||||
|
||||
// Track variables with namespace prefix
|
||||
/** @type {Map<string, { node: import('estree').VariableDeclarator, name: string, oldValue: string, newValue: string, ns: string }>} */
|
||||
const variablesToFix = new Map()
|
||||
|
||||
// Track all namespaces used in the file (from legacy prefix detection)
|
||||
/** @type {Set<string>} */
|
||||
const namespacesUsed = new Set()
|
||||
|
||||
// Track variable values for template literal analysis
|
||||
/** @type {Map<string, string>} */
|
||||
const variableValues = new Map()
|
||||
|
||||
/**
|
||||
* Analyze a template literal and extract namespace info
|
||||
* @param {import('estree').TemplateLiteral} node
|
||||
*/
|
||||
function analyzeTemplateLiteral(node) {
|
||||
const quasis = node.quasis
|
||||
const expressions = node.expressions
|
||||
@ -78,11 +63,6 @@ export default {
|
||||
return { ns: null, canFix: false, fixedQuasis: null, variableToUpdate: null }
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a fixed template literal string
|
||||
* @param {string[]} quasis
|
||||
* @param {import('estree').Expression[]} expressions
|
||||
*/
|
||||
function buildTemplateLiteral(quasis, expressions) {
|
||||
let result = '`'
|
||||
for (let i = 0; i < quasis.length; i++) {
|
||||
@ -95,11 +75,6 @@ export default {
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a t() call already has ns in its second argument
|
||||
* @param {import('estree').CallExpression} node
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function hasNsArgument(node) {
|
||||
if (node.arguments.length < 2)
|
||||
return false
|
||||
|
||||
@ -12,11 +12,6 @@ export default {
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
/**
|
||||
* Check if a t() call has ns in its second argument
|
||||
* @param {import('estree').CallExpression} node
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function hasNsOption(node) {
|
||||
if (node.arguments.length < 2)
|
||||
return false
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
// @ts-check
|
||||
import antfu from '@antfu/eslint-config'
|
||||
import pluginQuery from '@tanstack/eslint-plugin-query'
|
||||
import sonar from 'eslint-plugin-sonarjs'
|
||||
import storybook from 'eslint-plugin-storybook'
|
||||
import tailwind from 'eslint-plugin-tailwindcss'
|
||||
@ -13,6 +14,24 @@ export default antfu(
|
||||
'react/no-forward-ref': 'off',
|
||||
'react/no-use-context': 'off',
|
||||
'react/prefer-namespace-import': 'error',
|
||||
|
||||
// React Compiler rules
|
||||
// Set to warn for gradual adoption
|
||||
'react-hooks/config': 'warn',
|
||||
'react-hooks/error-boundaries': 'warn',
|
||||
'react-hooks/component-hook-factories': 'warn',
|
||||
'react-hooks/gating': 'warn',
|
||||
'react-hooks/globals': 'warn',
|
||||
'react-hooks/immutability': 'warn',
|
||||
'react-hooks/preserve-manual-memoization': 'warn',
|
||||
'react-hooks/purity': 'warn',
|
||||
'react-hooks/refs': 'warn',
|
||||
'react-hooks/set-state-in-effect': 'warn',
|
||||
'react-hooks/set-state-in-render': 'warn',
|
||||
'react-hooks/static-components': 'warn',
|
||||
'react-hooks/unsupported-syntax': 'warn',
|
||||
'react-hooks/use-memo': 'warn',
|
||||
'react-hooks/incompatible-library': 'warn',
|
||||
},
|
||||
},
|
||||
nextjs: true,
|
||||
@ -61,6 +80,7 @@ export default antfu(
|
||||
},
|
||||
},
|
||||
storybook.configs['flat/recommended'],
|
||||
...pluginQuery.configs['flat/recommended'],
|
||||
// sonar
|
||||
{
|
||||
rules: {
|
||||
|
||||
@ -306,6 +306,8 @@
|
||||
"variableConfig.displayName": "اسم العرض",
|
||||
"variableConfig.editModalTitle": "تعديل حقل إدخال",
|
||||
"variableConfig.errorMsg.atLeastOneOption": "خيار واحد على الأقل مطلوب",
|
||||
"variableConfig.errorMsg.jsonSchemaInvalid": "مخطط JSON ليس JSON صالحًا",
|
||||
"variableConfig.errorMsg.jsonSchemaMustBeObject": "يجب أن يكون نوع مخطط JSON \"object\"",
|
||||
"variableConfig.errorMsg.labelNameRequired": "اسم التسمية مطلوب",
|
||||
"variableConfig.errorMsg.optionRepeat": "يوجد خيارات مكررة",
|
||||
"variableConfig.errorMsg.varNameCanBeRepeat": "اسم المتغير لا يمكن تكراره",
|
||||
|
||||
@ -339,6 +339,9 @@
|
||||
"modelProvider.callTimes": "أوقات الاتصال",
|
||||
"modelProvider.card.buyQuota": "شراء حصة",
|
||||
"modelProvider.card.callTimes": "أوقات الاتصال",
|
||||
"modelProvider.card.modelAPI": "النماذج {{modelName}} تستخدم مفتاح واجهة برمجة التطبيقات.",
|
||||
"modelProvider.card.modelNotSupported": "النماذج {{modelName}} غير مثبتة.",
|
||||
"modelProvider.card.modelSupported": "النماذج {{modelName}} تستخدم هذا الحصة.",
|
||||
"modelProvider.card.onTrial": "في التجربة",
|
||||
"modelProvider.card.paid": "مدفوع",
|
||||
"modelProvider.card.priorityUse": "أولوية الاستخدام",
|
||||
@ -394,6 +397,7 @@
|
||||
"modelProvider.quotaTip": "الرموز المجانية المتاحة المتبقية",
|
||||
"modelProvider.rerankModel.key": "نموذج إعادة الترتيب",
|
||||
"modelProvider.rerankModel.tip": "سيعيد نموذج إعادة الترتيب ترتيب قائمة المستندات المرشحة بناءً على المطابقة الدلالية مع استعلام المستخدم، مما يحسن نتائج الترتيب الدلالي",
|
||||
"modelProvider.resetDate": "إعادة الضبط على {{date}}",
|
||||
"modelProvider.searchModel": "نموذج البحث",
|
||||
"modelProvider.selectModel": "اختر نموذجك",
|
||||
"modelProvider.selector.emptySetting": "يرجى الانتقال إلى الإعدادات للتكوين",
|
||||
|
||||
@ -306,6 +306,8 @@
|
||||
"variableConfig.displayName": "Anzeigename",
|
||||
"variableConfig.editModalTitle": "Eingabefeld bearbeiten",
|
||||
"variableConfig.errorMsg.atLeastOneOption": "Mindestens eine Option ist erforderlich",
|
||||
"variableConfig.errorMsg.jsonSchemaInvalid": "JSON-Schema ist kein gültiges JSON",
|
||||
"variableConfig.errorMsg.jsonSchemaMustBeObject": "JSON-Schema muss den Typ \"object\" haben",
|
||||
"variableConfig.errorMsg.labelNameRequired": "Labelname ist erforderlich",
|
||||
"variableConfig.errorMsg.optionRepeat": "Hat Wiederholungsoptionen",
|
||||
"variableConfig.errorMsg.varNameCanBeRepeat": "Variablenname kann nicht wiederholt werden",
|
||||
|
||||
@ -339,6 +339,9 @@
|
||||
"modelProvider.callTimes": "Anrufzeiten",
|
||||
"modelProvider.card.buyQuota": "Kontingent kaufen",
|
||||
"modelProvider.card.callTimes": "Anrufzeiten",
|
||||
"modelProvider.card.modelAPI": "{{modelName}}-Modelle verwenden den API-Schlüssel.",
|
||||
"modelProvider.card.modelNotSupported": "{{modelName}}-Modelle sind nicht installiert.",
|
||||
"modelProvider.card.modelSupported": "{{modelName}}-Modelle verwenden dieses Kontingent.",
|
||||
"modelProvider.card.onTrial": "In Probe",
|
||||
"modelProvider.card.paid": "Bezahlt",
|
||||
"modelProvider.card.priorityUse": "Priorisierte Nutzung",
|
||||
@ -394,6 +397,7 @@
|
||||
"modelProvider.quotaTip": "Verbleibende verfügbare kostenlose Token",
|
||||
"modelProvider.rerankModel.key": "Rerank-Modell",
|
||||
"modelProvider.rerankModel.tip": "Rerank-Modell wird die Kandidatendokumentenliste basierend auf der semantischen Übereinstimmung mit der Benutzeranfrage neu ordnen und die Ergebnisse der semantischen Rangordnung verbessern",
|
||||
"modelProvider.resetDate": "Zurücksetzen bei {{date}}",
|
||||
"modelProvider.searchModel": "Suchmodell",
|
||||
"modelProvider.selectModel": "Wählen Sie Ihr Modell",
|
||||
"modelProvider.selector.emptySetting": "Bitte gehen Sie zu den Einstellungen, um zu konfigurieren",
|
||||
|
||||
@ -306,6 +306,8 @@
|
||||
"variableConfig.displayName": "Display Name",
|
||||
"variableConfig.editModalTitle": "Edit Input Field",
|
||||
"variableConfig.errorMsg.atLeastOneOption": "At least one option is required",
|
||||
"variableConfig.errorMsg.jsonSchemaInvalid": "JSON Schema is not valid JSON",
|
||||
"variableConfig.errorMsg.jsonSchemaMustBeObject": "JSON Schema must have type \"object\"",
|
||||
"variableConfig.errorMsg.labelNameRequired": "Label name is required",
|
||||
"variableConfig.errorMsg.optionRepeat": "Has repeat options",
|
||||
"variableConfig.errorMsg.varNameCanBeRepeat": "Variable name can not be repeated",
|
||||
|
||||
@ -306,6 +306,8 @@
|
||||
"variableConfig.displayName": "Nombre para mostrar",
|
||||
"variableConfig.editModalTitle": "Editar Campo de Entrada",
|
||||
"variableConfig.errorMsg.atLeastOneOption": "Se requiere al menos una opción",
|
||||
"variableConfig.errorMsg.jsonSchemaInvalid": "El esquema JSON no es un JSON válido",
|
||||
"variableConfig.errorMsg.jsonSchemaMustBeObject": "El esquema JSON debe tener el tipo \"object\"",
|
||||
"variableConfig.errorMsg.labelNameRequired": "Nombre de la etiqueta es requerido",
|
||||
"variableConfig.errorMsg.optionRepeat": "Hay opciones repetidas",
|
||||
"variableConfig.errorMsg.varNameCanBeRepeat": "El nombre de la variable no puede repetirse",
|
||||
|
||||
@ -339,6 +339,9 @@
|
||||
"modelProvider.callTimes": "Tiempos de llamada",
|
||||
"modelProvider.card.buyQuota": "Comprar Cuota",
|
||||
"modelProvider.card.callTimes": "Tiempos de llamada",
|
||||
"modelProvider.card.modelAPI": "Los modelos {{modelName}} están usando la clave de API.",
|
||||
"modelProvider.card.modelNotSupported": "Los modelos {{modelName}} no están instalados.",
|
||||
"modelProvider.card.modelSupported": "Los modelos {{modelName}} están utilizando esta cuota.",
|
||||
"modelProvider.card.onTrial": "En prueba",
|
||||
"modelProvider.card.paid": "Pagado",
|
||||
"modelProvider.card.priorityUse": "Uso prioritario",
|
||||
@ -394,6 +397,7 @@
|
||||
"modelProvider.quotaTip": "Tokens gratuitos restantes disponibles",
|
||||
"modelProvider.rerankModel.key": "Modelo de Reordenar",
|
||||
"modelProvider.rerankModel.tip": "El modelo de reordenar reordenará la lista de documentos candidatos basada en la coincidencia semántica con la consulta del usuario, mejorando los resultados de clasificación semántica",
|
||||
"modelProvider.resetDate": "Reiniciar en {{date}}",
|
||||
"modelProvider.searchModel": "Modelo de búsqueda",
|
||||
"modelProvider.selectModel": "Selecciona tu modelo",
|
||||
"modelProvider.selector.emptySetting": "Por favor ve a configuraciones para configurar",
|
||||
|
||||
@ -306,6 +306,8 @@
|
||||
"variableConfig.displayName": "نام نمایشی",
|
||||
"variableConfig.editModalTitle": "ویرایش فیلد ورودی",
|
||||
"variableConfig.errorMsg.atLeastOneOption": "حداقل یک گزینه مورد نیاز است",
|
||||
"variableConfig.errorMsg.jsonSchemaInvalid": "JSON Schema یک JSON معتبر نیست",
|
||||
"variableConfig.errorMsg.jsonSchemaMustBeObject": "نوع JSON Schema باید \"object\" باشد",
|
||||
"variableConfig.errorMsg.labelNameRequired": "نام برچسب الزامی است",
|
||||
"variableConfig.errorMsg.optionRepeat": "دارای گزینه های تکرار",
|
||||
"variableConfig.errorMsg.varNameCanBeRepeat": "نام متغیر را نمی توان تکرار کرد",
|
||||
|
||||
@ -339,6 +339,9 @@
|
||||
"modelProvider.callTimes": "تعداد فراخوانی",
|
||||
"modelProvider.card.buyQuota": "خرید سهمیه",
|
||||
"modelProvider.card.callTimes": "تعداد فراخوانی",
|
||||
"modelProvider.card.modelAPI": "مدلهای {{modelName}} در حال استفاده از کلید API هستند.",
|
||||
"modelProvider.card.modelNotSupported": "مدلهای {{modelName}} نصب نشدهاند.",
|
||||
"modelProvider.card.modelSupported": "مدلهای {{modelName}} از این سهمیه استفاده میکنند.",
|
||||
"modelProvider.card.onTrial": "در حال آزمایش",
|
||||
"modelProvider.card.paid": "پرداخت شده",
|
||||
"modelProvider.card.priorityUse": "استفاده با اولویت",
|
||||
@ -394,6 +397,7 @@
|
||||
"modelProvider.quotaTip": "توکنهای رایگان باقیمانده در دسترس",
|
||||
"modelProvider.rerankModel.key": "مدل رتبهبندی مجدد",
|
||||
"modelProvider.rerankModel.tip": "مدل رتبهبندی مجدد، لیست اسناد کاندید را بر اساس تطابق معنایی با پرسش کاربر مرتب میکند و نتایج رتبهبندی معنایی را بهبود میبخشد",
|
||||
"modelProvider.resetDate": "بازنشانی در {{date}}",
|
||||
"modelProvider.searchModel": "جستجوی مدل",
|
||||
"modelProvider.selectModel": "مدل خود را انتخاب کنید",
|
||||
"modelProvider.selector.emptySetting": "لطفاً به تنظیمات بروید تا پیکربندی کنید",
|
||||
|
||||
@ -306,6 +306,8 @@
|
||||
"variableConfig.displayName": "Nom d’affichage",
|
||||
"variableConfig.editModalTitle": "Edit Input Field",
|
||||
"variableConfig.errorMsg.atLeastOneOption": "At least one option is required",
|
||||
"variableConfig.errorMsg.jsonSchemaInvalid": "Le schéma JSON n’est pas un JSON valide",
|
||||
"variableConfig.errorMsg.jsonSchemaMustBeObject": "Le schéma JSON doit avoir le type \"object\"",
|
||||
"variableConfig.errorMsg.labelNameRequired": "Label name is required",
|
||||
"variableConfig.errorMsg.optionRepeat": "Has repeat options",
|
||||
"variableConfig.errorMsg.varNameCanBeRepeat": "Variable name can not be repeated",
|
||||
|
||||
@ -339,6 +339,9 @@
|
||||
"modelProvider.callTimes": "Temps d'appel",
|
||||
"modelProvider.card.buyQuota": "Acheter Quota",
|
||||
"modelProvider.card.callTimes": "Temps d'appel",
|
||||
"modelProvider.card.modelAPI": "Les modèles {{modelName}} utilisent la clé API.",
|
||||
"modelProvider.card.modelNotSupported": "Les modèles {{modelName}} ne sont pas installés.",
|
||||
"modelProvider.card.modelSupported": "Les modèles {{modelName}} utilisent ce quota.",
|
||||
"modelProvider.card.onTrial": "En Essai",
|
||||
"modelProvider.card.paid": "Payé",
|
||||
"modelProvider.card.priorityUse": "Utilisation prioritaire",
|
||||
@ -394,6 +397,7 @@
|
||||
"modelProvider.quotaTip": "Tokens gratuits restants disponibles",
|
||||
"modelProvider.rerankModel.key": "Modèle de Réorganisation",
|
||||
"modelProvider.rerankModel.tip": "Le modèle de réorganisation réorganisera la liste des documents candidats en fonction de la correspondance sémantique avec la requête de l'utilisateur, améliorant ainsi les résultats du classement sémantique.",
|
||||
"modelProvider.resetDate": "Réinitialiser sur {{date}}",
|
||||
"modelProvider.searchModel": "Modèle de recherche",
|
||||
"modelProvider.selectModel": "Sélectionnez votre modèle",
|
||||
"modelProvider.selector.emptySetting": "Veuillez aller dans les paramètres pour configurer",
|
||||
|
||||
@ -306,6 +306,8 @@
|
||||
"variableConfig.displayName": "प्रदर्शन नाम",
|
||||
"variableConfig.editModalTitle": "इनपुट फ़ील्ड संपादित करें",
|
||||
"variableConfig.errorMsg.atLeastOneOption": "कम से कम एक विकल्प आवश्यक है",
|
||||
"variableConfig.errorMsg.jsonSchemaInvalid": "JSON स्कीमा मान्य JSON नहीं है",
|
||||
"variableConfig.errorMsg.jsonSchemaMustBeObject": "JSON स्कीमा का प्रकार \"object\" होना चाहिए",
|
||||
"variableConfig.errorMsg.labelNameRequired": "लेबल नाम आवश्यक है",
|
||||
"variableConfig.errorMsg.optionRepeat": "विकल्प दोहराए गए हैं",
|
||||
"variableConfig.errorMsg.varNameCanBeRepeat": "वेरिएबल नाम दोहराया नहीं जा सकता",
|
||||
|
||||
@ -339,6 +339,9 @@
|
||||
"modelProvider.callTimes": "कॉल समय",
|
||||
"modelProvider.card.buyQuota": "कोटा खरीदें",
|
||||
"modelProvider.card.callTimes": "कॉल समय",
|
||||
"modelProvider.card.modelAPI": "{{modelName}} मॉडल एपीआई कुंजी का उपयोग कर रहे हैं।",
|
||||
"modelProvider.card.modelNotSupported": "{{modelName}} मॉडल इंस्टॉल नहीं हैं।",
|
||||
"modelProvider.card.modelSupported": "{{modelName}} मॉडल इस कोटा का उपयोग कर रहे हैं।",
|
||||
"modelProvider.card.onTrial": "परीक्षण पर",
|
||||
"modelProvider.card.paid": "भुगतान किया हुआ",
|
||||
"modelProvider.card.priorityUse": "प्राथमिकता उपयोग",
|
||||
@ -394,6 +397,7 @@
|
||||
"modelProvider.quotaTip": "बचे हुए उपलब्ध मुफ्त टोकन",
|
||||
"modelProvider.rerankModel.key": "रीरैंक मॉडल",
|
||||
"modelProvider.rerankModel.tip": "रीरैंक मॉडल उपयोगकर्ता प्रश्न के साथ सांविधिक मेल के आधार पर उम्मीदवार दस्तावेज़ सूची को पुनः क्रमित करेगा, सांविधिक रैंकिंग के परिणामों में सुधार करेगा।",
|
||||
"modelProvider.resetDate": "{{date}} पर रीसेट करें",
|
||||
"modelProvider.searchModel": "खोज मॉडल",
|
||||
"modelProvider.selectModel": "अपने मॉडल का चयन करें",
|
||||
"modelProvider.selector.emptySetting": "कॉन्फ़िगर करने के लिए कृपया सेटिंग्स पर जाएं",
|
||||
|
||||
@ -306,6 +306,8 @@
|
||||
"variableConfig.displayName": "Nama Tampilan",
|
||||
"variableConfig.editModalTitle": "Edit Bidang Input",
|
||||
"variableConfig.errorMsg.atLeastOneOption": "Setidaknya satu opsi diperlukan",
|
||||
"variableConfig.errorMsg.jsonSchemaInvalid": "JSON Schema bukan JSON yang valid",
|
||||
"variableConfig.errorMsg.jsonSchemaMustBeObject": "JSON Schema harus bertipe \"object\"",
|
||||
"variableConfig.errorMsg.labelNameRequired": "Nama label diperlukan",
|
||||
"variableConfig.errorMsg.optionRepeat": "Memiliki opsi pengulangan",
|
||||
"variableConfig.errorMsg.varNameCanBeRepeat": "Nama variabel tidak dapat diulang",
|
||||
|
||||
@ -339,6 +339,9 @@
|
||||
"modelProvider.callTimes": "Waktu panggilan",
|
||||
"modelProvider.card.buyQuota": "Beli Kuota",
|
||||
"modelProvider.card.callTimes": "Waktu panggilan",
|
||||
"modelProvider.card.modelAPI": "Model {{modelName}} sedang menggunakan API Key.",
|
||||
"modelProvider.card.modelNotSupported": "Model {{modelName}} tidak terpasang.",
|
||||
"modelProvider.card.modelSupported": "Model {{modelName}} sedang menggunakan kuota ini.",
|
||||
"modelProvider.card.onTrial": "Sedang Diadili",
|
||||
"modelProvider.card.paid": "Dibayar",
|
||||
"modelProvider.card.priorityUse": "Penggunaan prioritas",
|
||||
@ -394,6 +397,7 @@
|
||||
"modelProvider.quotaTip": "Token gratis yang masih tersedia",
|
||||
"modelProvider.rerankModel.key": "Peringkat ulang Model",
|
||||
"modelProvider.rerankModel.tip": "Model rerank akan menyusun ulang daftar dokumen kandidat berdasarkan kecocokan semantik dengan kueri pengguna, meningkatkan hasil peringkat semantik",
|
||||
"modelProvider.resetDate": "Atur ulang pada {{date}}",
|
||||
"modelProvider.searchModel": "Model pencarian",
|
||||
"modelProvider.selectModel": "Pilih model Anda",
|
||||
"modelProvider.selector.emptySetting": "Silakan buka pengaturan untuk mengonfigurasi",
|
||||
|
||||
@ -306,6 +306,8 @@
|
||||
"variableConfig.displayName": "Nome visualizzato",
|
||||
"variableConfig.editModalTitle": "Modifica Campo Input",
|
||||
"variableConfig.errorMsg.atLeastOneOption": "È richiesta almeno un'opzione",
|
||||
"variableConfig.errorMsg.jsonSchemaInvalid": "Lo schema JSON non è un JSON valido",
|
||||
"variableConfig.errorMsg.jsonSchemaMustBeObject": "Lo schema JSON deve avere tipo \"object\"",
|
||||
"variableConfig.errorMsg.labelNameRequired": "Il nome dell'etichetta è richiesto",
|
||||
"variableConfig.errorMsg.optionRepeat": "Ci sono opzioni ripetute",
|
||||
"variableConfig.errorMsg.varNameCanBeRepeat": "Il nome della variabile non può essere ripetuto",
|
||||
|
||||
@ -339,6 +339,9 @@
|
||||
"modelProvider.callTimes": "Numero di chiamate",
|
||||
"modelProvider.card.buyQuota": "Acquista Quota",
|
||||
"modelProvider.card.callTimes": "Numero di chiamate",
|
||||
"modelProvider.card.modelAPI": "I modelli {{modelName}} stanno utilizzando la chiave API.",
|
||||
"modelProvider.card.modelNotSupported": "I modelli {{modelName}} non sono installati.",
|
||||
"modelProvider.card.modelSupported": "I modelli {{modelName}} stanno utilizzando questa quota.",
|
||||
"modelProvider.card.onTrial": "In Prova",
|
||||
"modelProvider.card.paid": "Pagato",
|
||||
"modelProvider.card.priorityUse": "Uso prioritario",
|
||||
@ -394,6 +397,7 @@
|
||||
"modelProvider.quotaTip": "Token gratuiti rimanenti disponibili",
|
||||
"modelProvider.rerankModel.key": "Modello di Rerank",
|
||||
"modelProvider.rerankModel.tip": "Il modello di rerank riordinerà la lista dei documenti candidati basandosi sulla corrispondenza semantica con la query dell'utente, migliorando i risultati del ranking semantico",
|
||||
"modelProvider.resetDate": "Reimposta su {{date}}",
|
||||
"modelProvider.searchModel": "Modello di ricerca",
|
||||
"modelProvider.selectModel": "Seleziona il tuo modello",
|
||||
"modelProvider.selector.emptySetting": "Per favore vai alle impostazioni per configurare",
|
||||
|
||||
@ -306,6 +306,8 @@
|
||||
"variableConfig.displayName": "表示名",
|
||||
"variableConfig.editModalTitle": "入力フィールドを編集",
|
||||
"variableConfig.errorMsg.atLeastOneOption": "少なくとも 1 つのオプションが必要です",
|
||||
"variableConfig.errorMsg.jsonSchemaInvalid": "JSONスキーマが有効なJSONではありません",
|
||||
"variableConfig.errorMsg.jsonSchemaMustBeObject": "JSONスキーマのtypeは\"object\"である必要があります",
|
||||
"variableConfig.errorMsg.labelNameRequired": "ラベル名は必須です",
|
||||
"variableConfig.errorMsg.optionRepeat": "繰り返しオプションがあります",
|
||||
"variableConfig.errorMsg.varNameCanBeRepeat": "変数名は繰り返すことができません",
|
||||
|
||||
@ -306,6 +306,8 @@
|
||||
"variableConfig.displayName": "표시 이름",
|
||||
"variableConfig.editModalTitle": "입력 필드 편집",
|
||||
"variableConfig.errorMsg.atLeastOneOption": "적어도 하나의 옵션이 필요합니다",
|
||||
"variableConfig.errorMsg.jsonSchemaInvalid": "JSON 스키마가 올바른 JSON이 아닙니다",
|
||||
"variableConfig.errorMsg.jsonSchemaMustBeObject": "JSON 스키마의 type은 \"object\"이어야 합니다",
|
||||
"variableConfig.errorMsg.labelNameRequired": "레이블명은 필수입니다",
|
||||
"variableConfig.errorMsg.optionRepeat": "옵션이 중복되어 있습니다",
|
||||
"variableConfig.errorMsg.varNameCanBeRepeat": "변수명은 중복될 수 없습니다",
|
||||
|
||||
@ -339,6 +339,9 @@
|
||||
"modelProvider.callTimes": "호출 횟수",
|
||||
"modelProvider.card.buyQuota": "Buy Quota",
|
||||
"modelProvider.card.callTimes": "호출 횟수",
|
||||
"modelProvider.card.modelAPI": "{{modelName}} 모델이 API 키를 사용하고 있습니다.",
|
||||
"modelProvider.card.modelNotSupported": "{{modelName}} 모델이 설치되지 않았습니다.",
|
||||
"modelProvider.card.modelSupported": "{{modelName}} 모델이 이 할당량을 사용하고 있습니다.",
|
||||
"modelProvider.card.onTrial": "트라이얼 중",
|
||||
"modelProvider.card.paid": "유료",
|
||||
"modelProvider.card.priorityUse": "우선 사용",
|
||||
@ -394,6 +397,7 @@
|
||||
"modelProvider.quotaTip": "남은 무료 토큰 사용 가능",
|
||||
"modelProvider.rerankModel.key": "재랭크 모델",
|
||||
"modelProvider.rerankModel.tip": "재랭크 모델은 사용자 쿼리와의 의미적 일치를 기반으로 후보 문서 목록을 재배열하여 의미적 순위를 향상시킵니다.",
|
||||
"modelProvider.resetDate": "{{date}}에서 재설정",
|
||||
"modelProvider.searchModel": "검색 모델",
|
||||
"modelProvider.selectModel": "모델 선택",
|
||||
"modelProvider.selector.emptySetting": "설정으로 이동하여 구성하세요",
|
||||
|
||||
@ -306,6 +306,8 @@
|
||||
"variableConfig.displayName": "Nazwa wyświetlana",
|
||||
"variableConfig.editModalTitle": "Edytuj Pole Wejściowe",
|
||||
"variableConfig.errorMsg.atLeastOneOption": "Wymagana jest co najmniej jedna opcja",
|
||||
"variableConfig.errorMsg.jsonSchemaInvalid": "Schemat JSON nie jest prawidłowym JSON-em",
|
||||
"variableConfig.errorMsg.jsonSchemaMustBeObject": "Schemat JSON musi mieć typ \"object\"",
|
||||
"variableConfig.errorMsg.labelNameRequired": "Wymagana nazwa etykiety",
|
||||
"variableConfig.errorMsg.optionRepeat": "Powtarzają się opcje",
|
||||
"variableConfig.errorMsg.varNameCanBeRepeat": "Nazwa zmiennej nie może się powtarzać",
|
||||
|
||||
@ -339,6 +339,9 @@
|
||||
"modelProvider.callTimes": "Czasy wywołań",
|
||||
"modelProvider.card.buyQuota": "Kup limit",
|
||||
"modelProvider.card.callTimes": "Czasy wywołań",
|
||||
"modelProvider.card.modelAPI": "Modele {{modelName}} używają klucza API.",
|
||||
"modelProvider.card.modelNotSupported": "Modele {{modelName}} nie są zainstalowane.",
|
||||
"modelProvider.card.modelSupported": "{{modelName}} modeli korzysta z tej kwoty.",
|
||||
"modelProvider.card.onTrial": "Na próbę",
|
||||
"modelProvider.card.paid": "Płatny",
|
||||
"modelProvider.card.priorityUse": "Używanie z priorytetem",
|
||||
@ -394,6 +397,7 @@
|
||||
"modelProvider.quotaTip": "Pozostałe dostępne darmowe tokeny",
|
||||
"modelProvider.rerankModel.key": "Model ponownego rankingu",
|
||||
"modelProvider.rerankModel.tip": "Model ponownego rankingu zmieni kolejność listy dokumentów kandydatów na podstawie semantycznego dopasowania z zapytaniem użytkownika, poprawiając wyniki rankingu semantycznego",
|
||||
"modelProvider.resetDate": "Reset na {{date}}",
|
||||
"modelProvider.searchModel": "Model wyszukiwania",
|
||||
"modelProvider.selectModel": "Wybierz swój model",
|
||||
"modelProvider.selector.emptySetting": "Przejdź do ustawień, aby skonfigurować",
|
||||
|
||||
@ -306,6 +306,8 @@
|
||||
"variableConfig.displayName": "Nome de exibição",
|
||||
"variableConfig.editModalTitle": "Editar Campo de Entrada",
|
||||
"variableConfig.errorMsg.atLeastOneOption": "Pelo menos uma opção é obrigatória",
|
||||
"variableConfig.errorMsg.jsonSchemaInvalid": "O JSON Schema não é um JSON válido",
|
||||
"variableConfig.errorMsg.jsonSchemaMustBeObject": "O JSON Schema deve ter o tipo \"object\"",
|
||||
"variableConfig.errorMsg.labelNameRequired": "O nome do rótulo é obrigatório",
|
||||
"variableConfig.errorMsg.optionRepeat": "Tem opções repetidas",
|
||||
"variableConfig.errorMsg.varNameCanBeRepeat": "O nome da variável não pode ser repetido",
|
||||
|
||||
@ -339,6 +339,9 @@
|
||||
"modelProvider.callTimes": "Chamadas",
|
||||
"modelProvider.card.buyQuota": "Comprar Quota",
|
||||
"modelProvider.card.callTimes": "Chamadas",
|
||||
"modelProvider.card.modelAPI": "Os modelos {{modelName}} estão usando a Chave de API.",
|
||||
"modelProvider.card.modelNotSupported": "Modelos {{modelName}} não estão instalados.",
|
||||
"modelProvider.card.modelSupported": "Modelos {{modelName}} estão usando esta cota.",
|
||||
"modelProvider.card.onTrial": "Em Teste",
|
||||
"modelProvider.card.paid": "Pago",
|
||||
"modelProvider.card.priorityUse": "Uso prioritário",
|
||||
@ -394,6 +397,7 @@
|
||||
"modelProvider.quotaTip": "Tokens gratuitos disponíveis restantes",
|
||||
"modelProvider.rerankModel.key": "Modelo de Reordenação",
|
||||
"modelProvider.rerankModel.tip": "O modelo de reordenaenação reorganizará a lista de documentos candidatos com base na correspondência semântica com a consulta do usuário, melhorando os resultados da classificação semântica",
|
||||
"modelProvider.resetDate": "Redefinir em {{date}}",
|
||||
"modelProvider.searchModel": "Modelo de pesquisa",
|
||||
"modelProvider.selectModel": "Selecione seu modelo",
|
||||
"modelProvider.selector.emptySetting": "Por favor, vá para configurações para configurar",
|
||||
|
||||
@ -306,6 +306,8 @@
|
||||
"variableConfig.displayName": "Nume afișat",
|
||||
"variableConfig.editModalTitle": "Editați câmpul de intrare",
|
||||
"variableConfig.errorMsg.atLeastOneOption": "Este necesară cel puțin o opțiune",
|
||||
"variableConfig.errorMsg.jsonSchemaInvalid": "Schema JSON nu este un JSON valid",
|
||||
"variableConfig.errorMsg.jsonSchemaMustBeObject": "Schema JSON trebuie să aibă tipul \"object\"",
|
||||
"variableConfig.errorMsg.labelNameRequired": "Numele etichetei este obligatoriu",
|
||||
"variableConfig.errorMsg.optionRepeat": "Există opțiuni repetate",
|
||||
"variableConfig.errorMsg.varNameCanBeRepeat": "Numele variabilei nu poate fi repetat",
|
||||
|
||||
@ -339,6 +339,9 @@
|
||||
"modelProvider.callTimes": "Apeluri",
|
||||
"modelProvider.card.buyQuota": "Cumpără cotă",
|
||||
"modelProvider.card.callTimes": "Apeluri",
|
||||
"modelProvider.card.modelAPI": "Modelele {{modelName}} folosesc cheia API.",
|
||||
"modelProvider.card.modelNotSupported": "Modelele {{modelName}} nu sunt instalate.",
|
||||
"modelProvider.card.modelSupported": "{{modelName}} modele utilizează această cotă.",
|
||||
"modelProvider.card.onTrial": "În probă",
|
||||
"modelProvider.card.paid": "Plătit",
|
||||
"modelProvider.card.priorityUse": "Utilizare prioritară",
|
||||
@ -394,6 +397,7 @@
|
||||
"modelProvider.quotaTip": "Jetoane gratuite disponibile rămase",
|
||||
"modelProvider.rerankModel.key": "Model de reordonare",
|
||||
"modelProvider.rerankModel.tip": "Modelul de reordonare va reordona lista de documente candidate pe baza potrivirii semantice cu interogarea utilizatorului, îmbunătățind rezultatele clasificării semantice",
|
||||
"modelProvider.resetDate": "Resetați la {{date}}",
|
||||
"modelProvider.searchModel": "Model de căutare",
|
||||
"modelProvider.selectModel": "Selectați modelul dvs.",
|
||||
"modelProvider.selector.emptySetting": "Vă rugăm să mergeți la setări pentru a configura",
|
||||
|
||||
@ -306,6 +306,8 @@
|
||||
"variableConfig.displayName": "Отображаемое имя",
|
||||
"variableConfig.editModalTitle": "Редактировать поле ввода",
|
||||
"variableConfig.errorMsg.atLeastOneOption": "Требуется хотя бы один вариант",
|
||||
"variableConfig.errorMsg.jsonSchemaInvalid": "JSON Schema не является корректным JSON",
|
||||
"variableConfig.errorMsg.jsonSchemaMustBeObject": "JSON Schema должна иметь тип \"object\"",
|
||||
"variableConfig.errorMsg.labelNameRequired": "Имя метки обязательно",
|
||||
"variableConfig.errorMsg.optionRepeat": "Есть повторяющиеся варианты",
|
||||
"variableConfig.errorMsg.varNameCanBeRepeat": "Имя переменной не может повторяться",
|
||||
|
||||
@ -339,6 +339,9 @@
|
||||
"modelProvider.callTimes": "Количество вызовов",
|
||||
"modelProvider.card.buyQuota": "Купить квоту",
|
||||
"modelProvider.card.callTimes": "Количество вызовов",
|
||||
"modelProvider.card.modelAPI": "{{modelName}} модели используют ключ API.",
|
||||
"modelProvider.card.modelNotSupported": "Модели {{modelName}} не установлены.",
|
||||
"modelProvider.card.modelSupported": "Эту квоту используют модели {{modelName}}.",
|
||||
"modelProvider.card.onTrial": "Пробная версия",
|
||||
"modelProvider.card.paid": "Платный",
|
||||
"modelProvider.card.priorityUse": "Приоритетное использование",
|
||||
@ -394,6 +397,7 @@
|
||||
"modelProvider.quotaTip": "Оставшиеся доступные бесплатные токены",
|
||||
"modelProvider.rerankModel.key": "Модель повторного ранжирования",
|
||||
"modelProvider.rerankModel.tip": "Модель повторного ранжирования изменит порядок списка документов-кандидатов на основе семантического соответствия запросу пользователя, улучшая результаты семантического ранжирования",
|
||||
"modelProvider.resetDate": "Сброс на {{date}}",
|
||||
"modelProvider.searchModel": "Поиск модели",
|
||||
"modelProvider.selectModel": "Выберите свою модель",
|
||||
"modelProvider.selector.emptySetting": "Пожалуйста, перейдите в настройки для настройки",
|
||||
|
||||
@ -306,6 +306,8 @@
|
||||
"variableConfig.displayName": "Prikazno ime",
|
||||
"variableConfig.editModalTitle": "Uredi vnosno polje",
|
||||
"variableConfig.errorMsg.atLeastOneOption": "Potrebna je vsaj ena možnost",
|
||||
"variableConfig.errorMsg.jsonSchemaInvalid": "Shema JSON ni veljaven JSON",
|
||||
"variableConfig.errorMsg.jsonSchemaMustBeObject": "Shema JSON mora imeti tip \"object\"",
|
||||
"variableConfig.errorMsg.labelNameRequired": "Ime nalepke je obvezno",
|
||||
"variableConfig.errorMsg.optionRepeat": "Ima možnosti ponavljanja",
|
||||
"variableConfig.errorMsg.varNameCanBeRepeat": "Imena spremenljivke ni mogoče ponoviti",
|
||||
|
||||
@ -339,6 +339,9 @@
|
||||
"modelProvider.callTimes": "Število klicev",
|
||||
"modelProvider.card.buyQuota": "Kupi kvoto",
|
||||
"modelProvider.card.callTimes": "Časi klicev",
|
||||
"modelProvider.card.modelAPI": "{{modelName}} modeli uporabljajo API ključ.",
|
||||
"modelProvider.card.modelNotSupported": "{{modelName}} modeli niso nameščeni.",
|
||||
"modelProvider.card.modelSupported": "{{modelName}} modeli uporabljajo to kvoto.",
|
||||
"modelProvider.card.onTrial": "Na preizkusu",
|
||||
"modelProvider.card.paid": "Plačano",
|
||||
"modelProvider.card.priorityUse": "Prednostna uporaba",
|
||||
@ -394,6 +397,7 @@
|
||||
"modelProvider.quotaTip": "Preostali razpoložljivi brezplačni žetoni",
|
||||
"modelProvider.rerankModel.key": "Model za prerazvrstitev",
|
||||
"modelProvider.rerankModel.tip": "Model za prerazvrstitev bo prerazporedil seznam kandidatskih dokumentov na podlagi semantične ujemanja z uporabniško poizvedbo, s čimer se izboljšajo rezultati semantičnega razvrščanja.",
|
||||
"modelProvider.resetDate": "Ponastavi na {{date}}",
|
||||
"modelProvider.searchModel": "Model iskanja",
|
||||
"modelProvider.selectModel": "Izberite svoj model",
|
||||
"modelProvider.selector.emptySetting": "Prosimo, pojdite v nastavitve za konfiguracijo",
|
||||
|
||||
@ -306,6 +306,8 @@
|
||||
"variableConfig.displayName": "ชื่อที่แสดง",
|
||||
"variableConfig.editModalTitle": "แก้ไขฟิลด์อินพุต",
|
||||
"variableConfig.errorMsg.atLeastOneOption": "จําเป็นต้องมีอย่างน้อยหนึ่งตัวเลือก",
|
||||
"variableConfig.errorMsg.jsonSchemaInvalid": "JSON Schema ไม่ใช่ JSON ที่ถูกต้อง",
|
||||
"variableConfig.errorMsg.jsonSchemaMustBeObject": "JSON Schema ต้องมีชนิดเป็น \"object\"",
|
||||
"variableConfig.errorMsg.labelNameRequired": "ต้องมีชื่อฉลาก",
|
||||
"variableConfig.errorMsg.optionRepeat": "มีตัวเลือกการทําซ้ํา",
|
||||
"variableConfig.errorMsg.varNameCanBeRepeat": "ไม่สามารถทําซ้ําชื่อตัวแปรได้",
|
||||
|
||||
@ -339,6 +339,9 @@
|
||||
"modelProvider.callTimes": "เวลาโทร",
|
||||
"modelProvider.card.buyQuota": "ซื้อโควต้า",
|
||||
"modelProvider.card.callTimes": "เวลาโทร",
|
||||
"modelProvider.card.modelAPI": "{{modelName}} โมเดลกำลังใช้คีย์ API",
|
||||
"modelProvider.card.modelNotSupported": "โมเดล {{modelName}} ยังไม่ได้ติดตั้ง",
|
||||
"modelProvider.card.modelSupported": "โมเดล {{modelName}} กำลังใช้โควต้านี้อยู่",
|
||||
"modelProvider.card.onTrial": "ทดลองใช้",
|
||||
"modelProvider.card.paid": "จ่าย",
|
||||
"modelProvider.card.priorityUse": "ลําดับความสําคัญในการใช้งาน",
|
||||
@ -394,6 +397,7 @@
|
||||
"modelProvider.quotaTip": "โทเค็นฟรีที่เหลืออยู่",
|
||||
"modelProvider.rerankModel.key": "จัดอันดับโมเดลใหม่",
|
||||
"modelProvider.rerankModel.tip": "โมเดล Rerank จะจัดลําดับรายการเอกสารผู้สมัครใหม่ตามการจับคู่ความหมายกับการสืบค้นของผู้ใช้ ซึ่งช่วยปรับปรุงผลลัพธ์ของการจัดอันดับความหมาย",
|
||||
"modelProvider.resetDate": "รีเซ็ตเมื่อ {{date}}",
|
||||
"modelProvider.searchModel": "ค้นหารุ่น",
|
||||
"modelProvider.selectModel": "เลือกรุ่นของคุณ",
|
||||
"modelProvider.selector.emptySetting": "โปรดไปที่การตั้งค่าเพื่อกําหนดค่า",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user