Merge branch 'main' into chore/workflow-toast-overlay-migration
# Conflicts: # web/app/components/workflow/panel/debug-and-preview/__tests__/hooks/opening-statement.spec.ts
This commit is contained in:
@@ -12,8 +12,16 @@ vi.mock('@/config', () => ({
|
||||
}))
|
||||
|
||||
const mockToastNotify = vi.fn()
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
default: { notify: (...args: unknown[]) => mockToastNotify(...args) },
|
||||
vi.mock('@/app/components/base/ui/toast', () => ({
|
||||
toast: Object.assign((message: string, options?: { type?: string }) => mockToastNotify({ type: options?.type, message }), {
|
||||
success: (message: string) => mockToastNotify({ type: 'success', message }),
|
||||
error: (message: string) => mockToastNotify({ type: 'error', message }),
|
||||
warning: (message: string) => mockToastNotify({ type: 'warning', message }),
|
||||
info: (message: string) => mockToastNotify({ type: 'info', message }),
|
||||
dismiss: vi.fn(),
|
||||
update: vi.fn(),
|
||||
promise: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
const mockUploadGitHub = vi.fn()
|
||||
|
||||
@@ -3,8 +3,16 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { useGitHubReleases, useGitHubUpload } from '../hooks'
|
||||
|
||||
const mockNotify = vi.fn()
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
default: { notify: (...args: unknown[]) => mockNotify(...args) },
|
||||
vi.mock('@/app/components/base/ui/toast', () => ({
|
||||
toast: Object.assign((...args: unknown[]) => mockNotify(...args), {
|
||||
success: (...args: unknown[]) => mockNotify(...args),
|
||||
error: (...args: unknown[]) => mockNotify(...args),
|
||||
warning: (...args: unknown[]) => mockNotify(...args),
|
||||
info: (...args: unknown[]) => mockNotify(...args),
|
||||
dismiss: vi.fn(),
|
||||
update: vi.fn(),
|
||||
promise: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/config', () => ({
|
||||
@@ -56,9 +64,7 @@ describe('install-plugin/hooks', () => {
|
||||
const releases = await result.current.fetchReleases('owner', 'repo')
|
||||
|
||||
expect(releases).toEqual([])
|
||||
expect(mockNotify).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ type: 'error' }),
|
||||
)
|
||||
expect(mockNotify).toHaveBeenCalledWith('Failed to fetch repository releases')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -130,9 +136,7 @@ describe('install-plugin/hooks', () => {
|
||||
await expect(
|
||||
result.current.handleUpload('url', 'v1', 'pkg'),
|
||||
).rejects.toThrow('Upload failed')
|
||||
expect(mockNotify).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ type: 'error', message: 'Error uploading package' }),
|
||||
)
|
||||
expect(mockNotify).toHaveBeenCalledWith('Error uploading package')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { GitHubRepoReleaseResponse } from '../types'
|
||||
import type { IToastProps } from '@/app/components/base/toast'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { GITHUB_ACCESS_TOKEN } from '@/config'
|
||||
import { uploadGitHub } from '@/service/plugins'
|
||||
import { compareVersion, getLatestVersion } from '@/utils/semver'
|
||||
@@ -37,16 +36,10 @@ export const useGitHubReleases = () => {
|
||||
}
|
||||
catch (error) {
|
||||
if (error instanceof Error) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: error.message,
|
||||
})
|
||||
toast.error(error.message)
|
||||
}
|
||||
else {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: 'Failed to fetch repository releases',
|
||||
})
|
||||
toast.error('Failed to fetch repository releases')
|
||||
}
|
||||
return []
|
||||
}
|
||||
@@ -54,7 +47,7 @@ export const useGitHubReleases = () => {
|
||||
|
||||
const checkForUpdates = (fetchedReleases: GitHubRepoReleaseResponse[], currentVersion: string) => {
|
||||
let needUpdate = false
|
||||
const toastProps: IToastProps = {
|
||||
const toastProps: { type?: 'success' | 'error' | 'info' | 'warning', message: string } = {
|
||||
type: 'info',
|
||||
message: 'No new version available',
|
||||
}
|
||||
@@ -99,10 +92,7 @@ export const useGitHubUpload = () => {
|
||||
return GitHubPackage
|
||||
}
|
||||
catch (error) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: 'Error uploading package',
|
||||
})
|
||||
toast.error('Error uploading package')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,10 +57,16 @@ const createUpdatePayload = (overrides: Partial<UpdateFromGitHubPayload> = {}):
|
||||
|
||||
// Mock external dependencies
|
||||
const mockNotify = vi.fn()
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
default: {
|
||||
notify: (props: { type: string, message: string }) => mockNotify(props),
|
||||
},
|
||||
vi.mock('@/app/components/base/ui/toast', () => ({
|
||||
toast: Object.assign((props: { type: string, message: string }) => mockNotify(props), {
|
||||
success: (message: string) => mockNotify({ type: 'success', message }),
|
||||
error: (message: string) => mockNotify({ type: 'error', message }),
|
||||
warning: (message: string) => mockNotify({ type: 'warning', message }),
|
||||
info: (message: string) => mockNotify({ type: 'info', message }),
|
||||
dismiss: vi.fn(),
|
||||
update: vi.fn(),
|
||||
promise: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
const mockGetIconUrl = vi.fn()
|
||||
|
||||
@@ -7,7 +7,7 @@ import * as React from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import useGetIcon from '@/app/components/plugins/install-plugin/base/use-get-icon'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { InstallStepFromGitHub } from '../../types'
|
||||
@@ -81,10 +81,7 @@ const InstallFromGitHub: React.FC<InstallFromGitHubProps> = ({ updatePayload, on
|
||||
const handleUrlSubmit = async () => {
|
||||
const { isValid, owner, repo } = parseGitHubUrl(state.repoUrl)
|
||||
if (!isValid || !owner || !repo) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('error.inValidGitHubUrl', { ns: 'plugin' }),
|
||||
})
|
||||
toast.error(t('error.inValidGitHubUrl', { ns: 'plugin' }))
|
||||
return
|
||||
}
|
||||
try {
|
||||
@@ -97,17 +94,11 @@ const InstallFromGitHub: React.FC<InstallFromGitHubProps> = ({ updatePayload, on
|
||||
}))
|
||||
}
|
||||
else {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('error.noReleasesFound', { ns: 'plugin' }),
|
||||
})
|
||||
toast.error(t('error.noReleasesFound', { ns: 'plugin' }))
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('error.fetchReleasesError', { ns: 'plugin' }),
|
||||
})
|
||||
toast.error(t('error.fetchReleasesError', { ns: 'plugin' }))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,10 +166,10 @@ const InstallFromGitHub: React.FC<InstallFromGitHubProps> = ({ updatePayload, on
|
||||
>
|
||||
<div className="flex items-start gap-2 self-stretch pb-3 pl-6 pr-14 pt-6">
|
||||
<div className="flex grow flex-col items-start gap-1">
|
||||
<div className="title-2xl-semi-bold self-stretch text-text-primary">
|
||||
<div className="self-stretch text-text-primary title-2xl-semi-bold">
|
||||
{getTitle()}
|
||||
</div>
|
||||
<div className="system-xs-regular self-stretch text-text-tertiary">
|
||||
<div className="self-stretch text-text-tertiary system-xs-regular">
|
||||
{!([InstallStepFromGitHub.uploadFailed, InstallStepFromGitHub.installed, InstallStepFromGitHub.installFailed].includes(state.step)) && t('installFromGitHub.installNote', { ns: 'plugin' })}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,10 +2,25 @@ import type { PluginDetail } from '../../types'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import * as amplitude from '@/app/components/base/amplitude'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { PluginSource } from '../../types'
|
||||
import DetailHeader from '../detail-header'
|
||||
|
||||
const { mockToast } = vi.hoisted(() => ({
|
||||
mockToast: Object.assign(vi.fn(), {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warning: vi.fn(),
|
||||
info: vi.fn(),
|
||||
dismiss: vi.fn(),
|
||||
update: vi.fn(),
|
||||
promise: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/ui/toast', () => ({
|
||||
toast: mockToast,
|
||||
}))
|
||||
|
||||
const {
|
||||
mockSetShowUpdatePluginModal,
|
||||
mockRefreshModelProviders,
|
||||
@@ -272,7 +287,7 @@ describe('DetailHeader', () => {
|
||||
vi.clearAllMocks()
|
||||
mockAutoUpgradeInfo = null
|
||||
mockEnableMarketplace = true
|
||||
vi.spyOn(Toast, 'notify').mockImplementation(() => ({ clear: vi.fn() }))
|
||||
vi.clearAllMocks()
|
||||
vi.spyOn(amplitude, 'trackEvent').mockImplementation(() => {})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { EndpointListItem, PluginDetail } from '../../types'
|
||||
import { act, fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import EndpointCard from '../endpoint-card'
|
||||
|
||||
const mockHandleChange = vi.fn()
|
||||
@@ -9,6 +8,22 @@ const mockEnableEndpoint = vi.fn()
|
||||
const mockDisableEndpoint = vi.fn()
|
||||
const mockDeleteEndpoint = vi.fn()
|
||||
const mockUpdateEndpoint = vi.fn()
|
||||
const mockToastNotify = vi.fn()
|
||||
|
||||
vi.mock('@/app/components/base/ui/toast', () => ({
|
||||
toast: Object.assign(
|
||||
(message: string, options?: { type?: string }) => mockToastNotify({ type: options?.type, message }),
|
||||
{
|
||||
success: (message: string) => mockToastNotify({ type: 'success', message }),
|
||||
error: (message: string) => mockToastNotify({ type: 'error', message }),
|
||||
warning: (message: string) => mockToastNotify({ type: 'warning', message }),
|
||||
info: (message: string) => mockToastNotify({ type: 'info', message }),
|
||||
dismiss: vi.fn(),
|
||||
update: vi.fn(),
|
||||
promise: vi.fn(),
|
||||
},
|
||||
),
|
||||
}))
|
||||
|
||||
// Flags to control whether operations should fail
|
||||
const failureFlags = {
|
||||
@@ -127,8 +142,6 @@ describe('EndpointCard', () => {
|
||||
failureFlags.disable = false
|
||||
failureFlags.delete = false
|
||||
failureFlags.update = false
|
||||
// Mock Toast.notify to prevent toast elements from accumulating in DOM
|
||||
vi.spyOn(Toast, 'notify').mockImplementation(() => ({ clear: vi.fn() }))
|
||||
// Polyfill document.execCommand for copy-to-clipboard in jsdom
|
||||
if (typeof document.execCommand !== 'function') {
|
||||
document.execCommand = vi.fn().mockReturnValue(true)
|
||||
|
||||
@@ -2,9 +2,25 @@ import type { FormSchema } from '../../../base/form/types'
|
||||
import type { PluginDetail } from '../../types'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import EndpointModal from '../endpoint-modal'
|
||||
|
||||
const mockToastNotify = vi.fn()
|
||||
|
||||
vi.mock('@/app/components/base/ui/toast', () => ({
|
||||
toast: Object.assign(
|
||||
(message: string, options?: { type?: string }) => mockToastNotify({ type: options?.type, message }),
|
||||
{
|
||||
success: (message: string) => mockToastNotify({ type: 'success', message }),
|
||||
error: (message: string) => mockToastNotify({ type: 'error', message }),
|
||||
warning: (message: string) => mockToastNotify({ type: 'warning', message }),
|
||||
info: (message: string) => mockToastNotify({ type: 'info', message }),
|
||||
dismiss: vi.fn(),
|
||||
update: vi.fn(),
|
||||
promise: vi.fn(),
|
||||
},
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/use-i18n', () => ({
|
||||
useRenderI18nObject: () => (obj: Record<string, string> | string) =>
|
||||
typeof obj === 'string' ? obj : obj?.en_US || '',
|
||||
@@ -69,11 +85,9 @@ const mockPluginDetail: PluginDetail = {
|
||||
describe('EndpointModal', () => {
|
||||
const mockOnCancel = vi.fn()
|
||||
const mockOnSaved = vi.fn()
|
||||
let mockToastNotify: ReturnType<typeof vi.spyOn>
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockToastNotify = vi.spyOn(Toast, 'notify').mockImplementation(() => ({ clear: vi.fn() }))
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { ModalStates, VersionTarget } from '../use-detail-header-state'
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import * as amplitude from '@/app/components/base/amplitude'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { PluginSource } from '../../../../types'
|
||||
import { usePluginOperations } from '../use-plugin-operations'
|
||||
|
||||
@@ -20,6 +19,7 @@ const {
|
||||
mockUninstallPlugin,
|
||||
mockFetchReleases,
|
||||
mockCheckForUpdates,
|
||||
mockToastNotify,
|
||||
} = vi.hoisted(() => {
|
||||
return {
|
||||
mockSetShowUpdatePluginModal: vi.fn(),
|
||||
@@ -29,9 +29,25 @@ const {
|
||||
mockUninstallPlugin: vi.fn(() => Promise.resolve({ success: true })),
|
||||
mockFetchReleases: vi.fn(() => Promise.resolve([{ tag_name: 'v2.0.0' }])),
|
||||
mockCheckForUpdates: vi.fn(() => ({ needUpdate: true, toastProps: { type: 'success', message: 'Update available' } })),
|
||||
mockToastNotify: vi.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/app/components/base/ui/toast', () => ({
|
||||
toast: Object.assign(
|
||||
(message: string, options?: { type?: string }) => mockToastNotify({ type: options?.type, message }),
|
||||
{
|
||||
success: (message: string) => mockToastNotify({ type: 'success', message }),
|
||||
error: (message: string) => mockToastNotify({ type: 'error', message }),
|
||||
warning: (message: string) => mockToastNotify({ type: 'warning', message }),
|
||||
info: (message: string) => mockToastNotify({ type: 'info', message }),
|
||||
dismiss: vi.fn(),
|
||||
update: vi.fn(),
|
||||
promise: vi.fn(),
|
||||
},
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/modal-context', () => ({
|
||||
useModalContext: () => ({
|
||||
setShowUpdatePluginModal: mockSetShowUpdatePluginModal,
|
||||
@@ -124,7 +140,6 @@ describe('usePluginOperations', () => {
|
||||
modalStates = createModalStatesMock()
|
||||
versionPicker = createVersionPickerMock()
|
||||
mockOnUpdate = vi.fn()
|
||||
vi.spyOn(Toast, 'notify').mockImplementation(() => ({ clear: vi.fn() }))
|
||||
vi.spyOn(amplitude, 'trackEvent').mockImplementation(() => {})
|
||||
})
|
||||
|
||||
@@ -233,7 +248,7 @@ describe('usePluginOperations', () => {
|
||||
})
|
||||
|
||||
expect(mockCheckForUpdates).toHaveBeenCalled()
|
||||
expect(Toast.notify).toHaveBeenCalled()
|
||||
expect(mockToastNotify).toHaveBeenCalledWith({ type: 'success', message: 'Update available' })
|
||||
})
|
||||
|
||||
it('should show update plugin modal when update is needed', async () => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { ModalStates, VersionTarget } from './use-detail-header-state'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { trackEvent } from '@/app/components/base/amplitude'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { uninstallPlugin } from '@/service/plugins'
|
||||
@@ -60,10 +60,7 @@ export const usePluginOperations = ({
|
||||
}
|
||||
|
||||
if (!meta?.repo || !meta?.version || !meta?.package) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: 'Missing plugin metadata for GitHub update',
|
||||
})
|
||||
toast.error('Missing plugin metadata for GitHub update')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -74,7 +71,7 @@ export const usePluginOperations = ({
|
||||
return
|
||||
|
||||
const { needUpdate, toastProps } = checkForUpdates(fetchedReleases, meta.version)
|
||||
Toast.notify(toastProps)
|
||||
toast(toastProps.message, { type: toastProps.type })
|
||||
|
||||
if (needUpdate) {
|
||||
setShowUpdatePluginModal({
|
||||
@@ -122,10 +119,7 @@ export const usePluginOperations = ({
|
||||
|
||||
if (res.success) {
|
||||
modalStates.hideDeleteConfirm()
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('action.deleteSuccess', { ns: 'plugin' }),
|
||||
})
|
||||
toast.success(t('action.deleteSuccess', { ns: 'plugin' }))
|
||||
handlePluginUpdated(true)
|
||||
|
||||
if (PluginCategoryEnum.model.includes(category))
|
||||
|
||||
@@ -9,8 +9,8 @@ import ActionButton from '@/app/components/base/action-button'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import { CopyCheck } from '@/app/components/base/icons/src/vender/line/files'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import { addDefaultValue, toolCredentialToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
|
||||
import {
|
||||
@@ -47,7 +47,7 @@ const EndpointCard = ({
|
||||
await handleChange()
|
||||
},
|
||||
onError: () => {
|
||||
Toast.notify({ type: 'error', message: t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }) })
|
||||
toast.error(t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }))
|
||||
setActive(false)
|
||||
},
|
||||
})
|
||||
@@ -57,7 +57,7 @@ const EndpointCard = ({
|
||||
hideDisableConfirm()
|
||||
},
|
||||
onError: () => {
|
||||
Toast.notify({ type: 'error', message: t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }) })
|
||||
toast.error(t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }))
|
||||
setActive(false)
|
||||
},
|
||||
})
|
||||
@@ -83,7 +83,7 @@ const EndpointCard = ({
|
||||
hideDeleteConfirm()
|
||||
},
|
||||
onError: () => {
|
||||
Toast.notify({ type: 'error', message: t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }) })
|
||||
toast.error(t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }))
|
||||
},
|
||||
})
|
||||
|
||||
@@ -108,7 +108,7 @@ const EndpointCard = ({
|
||||
hideEndpointModalConfirm()
|
||||
},
|
||||
onError: () => {
|
||||
Toast.notify({ type: 'error', message: t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }) })
|
||||
toast.error(t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }))
|
||||
},
|
||||
})
|
||||
const handleUpdate = (state: Record<string, any>) => updateEndpoint({
|
||||
@@ -139,7 +139,7 @@ const EndpointCard = ({
|
||||
<div className="rounded-xl bg-background-section-burn p-0.5">
|
||||
<div className="group rounded-[10px] border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg p-2.5 pl-3">
|
||||
<div className="flex items-center">
|
||||
<div className="system-md-semibold mb-1 flex h-6 grow items-center gap-1 text-text-secondary">
|
||||
<div className="mb-1 flex h-6 grow items-center gap-1 text-text-secondary system-md-semibold">
|
||||
<RiLoginCircleLine className="h-4 w-4" />
|
||||
<div>{data.name}</div>
|
||||
</div>
|
||||
@@ -154,8 +154,8 @@ const EndpointCard = ({
|
||||
</div>
|
||||
{data.declaration.endpoints.filter(endpoint => !endpoint.hidden).map((endpoint, index) => (
|
||||
<div key={index} className="flex h-6 items-center">
|
||||
<div className="system-xs-regular w-12 shrink-0 text-text-tertiary">{endpoint.method}</div>
|
||||
<div className="group/item system-xs-regular flex grow items-center truncate text-text-secondary">
|
||||
<div className="w-12 shrink-0 text-text-tertiary system-xs-regular">{endpoint.method}</div>
|
||||
<div className="group/item flex grow items-center truncate text-text-secondary system-xs-regular">
|
||||
<div title={`${data.url}${endpoint.path}`} className="truncate">{`${data.url}${endpoint.path}`}</div>
|
||||
<Tooltip popupContent={t(`operation.${isCopied ? 'copied' : 'copy'}`, { ns: 'common' })} position="top">
|
||||
<ActionButton className="ml-2 hidden shrink-0 group-hover/item:flex" onClick={() => handleCopy(`${data.url}${endpoint.path}`)}>
|
||||
@@ -168,13 +168,13 @@ const EndpointCard = ({
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-2 pl-3">
|
||||
{active && (
|
||||
<div className="system-xs-semibold-uppercase flex items-center gap-1 text-util-colors-green-green-600">
|
||||
<div className="flex items-center gap-1 text-util-colors-green-green-600 system-xs-semibold-uppercase">
|
||||
<Indicator color="green" />
|
||||
{t('detailPanel.serviceOk', { ns: 'plugin' })}
|
||||
</div>
|
||||
)}
|
||||
{!active && (
|
||||
<div className="system-xs-semibold-uppercase flex items-center gap-1 text-text-tertiary">
|
||||
<div className="flex items-center gap-1 text-text-tertiary system-xs-semibold-uppercase">
|
||||
<Indicator color="gray" />
|
||||
{t('detailPanel.disabled', { ns: 'plugin' })}
|
||||
</div>
|
||||
|
||||
@@ -9,8 +9,8 @@ import * as React from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { toolCredentialToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
import {
|
||||
@@ -50,7 +50,7 @@ const EndpointList = ({ detail }: Props) => {
|
||||
hideEndpointModal()
|
||||
},
|
||||
onError: () => {
|
||||
Toast.notify({ type: 'error', message: t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }) })
|
||||
toast.error(t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }))
|
||||
},
|
||||
})
|
||||
|
||||
@@ -64,7 +64,7 @@ const EndpointList = ({ detail }: Props) => {
|
||||
|
||||
return (
|
||||
<div className={cn('border-divider-subtle px-4 py-2', showTopBorder && 'border-t')}>
|
||||
<div className="system-sm-semibold-uppercase mb-1 flex h-6 items-center justify-between text-text-secondary">
|
||||
<div className="mb-1 flex h-6 items-center justify-between text-text-secondary system-sm-semibold-uppercase">
|
||||
<div className="flex items-center gap-0.5">
|
||||
{t('detailPanel.endpoints', { ns: 'plugin' })}
|
||||
<Tooltip
|
||||
@@ -75,13 +75,13 @@ const EndpointList = ({ detail }: Props) => {
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg border-[0.5px] border-components-panel-border-subtle bg-background-default-subtle">
|
||||
<RiApps2AddLine className="h-4 w-4 text-text-tertiary" />
|
||||
</div>
|
||||
<div className="system-xs-regular text-text-tertiary">{t('detailPanel.endpointsTip', { ns: 'plugin' })}</div>
|
||||
<div className="text-text-tertiary system-xs-regular">{t('detailPanel.endpointsTip', { ns: 'plugin' })}</div>
|
||||
<a
|
||||
href={docLink('/develop-plugin/getting-started/getting-started-dify-plugin')}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="system-xs-regular inline-flex cursor-pointer items-center gap-1 text-text-accent">
|
||||
<div className="inline-flex cursor-pointer items-center gap-1 text-text-accent system-xs-regular">
|
||||
<RiBookOpenLine className="h-3 w-3" />
|
||||
{t('detailPanel.endpointsDocLink', { ns: 'plugin' })}
|
||||
</div>
|
||||
@@ -95,7 +95,7 @@ const EndpointList = ({ detail }: Props) => {
|
||||
</ActionButton>
|
||||
</div>
|
||||
{data.endpoints.length === 0 && (
|
||||
<div className="system-xs-regular mb-1 flex justify-center rounded-[10px] bg-background-section p-3 text-text-tertiary">{t('detailPanel.endpointsEmpty', { ns: 'plugin' })}</div>
|
||||
<div className="mb-1 flex justify-center rounded-[10px] bg-background-section p-3 text-text-tertiary system-xs-regular">{t('detailPanel.endpointsEmpty', { ns: 'plugin' })}</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-2">
|
||||
{data.endpoints.map((item, index) => (
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Drawer from '@/app/components/base/drawer'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import Form from '@/app/components/header/account-setting/model-provider-page/model-modal/Form'
|
||||
import { useRenderI18nObject } from '@/hooks/use-i18n'
|
||||
import { cn } from '@/utils/classnames'
|
||||
@@ -48,7 +48,10 @@ const EndpointModal: FC<Props> = ({
|
||||
const handleSave = () => {
|
||||
for (const field of formSchemas) {
|
||||
if (field.required && !tempCredential[field.name]) {
|
||||
Toast.notify({ type: 'error', message: t('errorMsg.fieldRequired', { ns: 'common', field: typeof field.label === 'string' ? field.label : getValueFromI18nObject(field.label as Record<string, string>) }) })
|
||||
toast.error(t('errorMsg.fieldRequired', {
|
||||
ns: 'common',
|
||||
field: typeof field.label === 'string' ? field.label : getValueFromI18nObject(field.label as Record<string, string>),
|
||||
}))
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -83,12 +86,12 @@ const EndpointModal: FC<Props> = ({
|
||||
<>
|
||||
<div className="p-4 pb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="system-xl-semibold text-text-primary">{t('detailPanel.endpointModalTitle', { ns: 'plugin' })}</div>
|
||||
<div className="text-text-primary system-xl-semibold">{t('detailPanel.endpointModalTitle', { ns: 'plugin' })}</div>
|
||||
<ActionButton onClick={onCancel}>
|
||||
<RiCloseLine className="h-4 w-4" />
|
||||
</ActionButton>
|
||||
</div>
|
||||
<div className="system-xs-regular mt-0.5 text-text-tertiary">{t('detailPanel.endpointModalDesc', { ns: 'plugin' })}</div>
|
||||
<div className="mt-0.5 text-text-tertiary system-xs-regular">{t('detailPanel.endpointModalDesc', { ns: 'plugin' })}</div>
|
||||
<ReadmeEntrance pluginDetail={pluginDetail} className="px-0 pt-3" />
|
||||
</div>
|
||||
<div className="grow overflow-y-auto">
|
||||
@@ -109,7 +112,7 @@ const EndpointModal: FC<Props> = ({
|
||||
href={item.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="body-xs-regular inline-flex items-center text-text-accent-secondary"
|
||||
className="inline-flex items-center text-text-accent-secondary body-xs-regular"
|
||||
>
|
||||
{t('howToGet', { ns: 'tools' })}
|
||||
<RiArrowRightUpLine className="ml-1 h-3 w-3" />
|
||||
|
||||
@@ -1,14 +1,29 @@
|
||||
import type { Model, ModelItem } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
// Import component after mocks
|
||||
import Toast from '@/app/components/base/toast'
|
||||
|
||||
import { ConfigurationMethodEnum, ModelStatusEnum, ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
|
||||
// Import component after mocks
|
||||
import ModelParameterModal from '../index'
|
||||
|
||||
// ==================== Mock Setup ====================
|
||||
|
||||
const mockToastNotify = vi.fn()
|
||||
vi.mock('@/app/components/base/ui/toast', () => ({
|
||||
toast: Object.assign(
|
||||
(message: string, options?: { type?: string }) => mockToastNotify({ type: options?.type, message }),
|
||||
{
|
||||
success: (message: string) => mockToastNotify({ type: 'success', message }),
|
||||
error: (message: string) => mockToastNotify({ type: 'error', message }),
|
||||
warning: (message: string) => mockToastNotify({ type: 'warning', message }),
|
||||
info: (message: string) => mockToastNotify({ type: 'info', message }),
|
||||
dismiss: vi.fn(),
|
||||
update: vi.fn(),
|
||||
promise: vi.fn(),
|
||||
},
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock provider context
|
||||
const mockProviderContextValue = {
|
||||
isAPIKeySet: true,
|
||||
@@ -53,8 +68,6 @@ vi.mock('@/utils/completion-params', () => ({
|
||||
fetchAndMergeValidCompletionParams: (...args: unknown[]) => mockFetchAndMergeValidCompletionParams(...args),
|
||||
}))
|
||||
|
||||
const mockToastNotify = vi.spyOn(Toast, 'notify')
|
||||
|
||||
// Mock child components
|
||||
vi.mock('@/app/components/header/account-setting/model-provider-page/model-selector', () => ({
|
||||
default: ({ defaultModel, modelList, scopeFeatures, onSelect }: {
|
||||
@@ -244,7 +257,6 @@ const setupModelLists = (config: {
|
||||
describe('ModelParameterModal', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockToastNotify.mockReturnValue({})
|
||||
mockProviderContextValue.isAPIKeySet = true
|
||||
mockProviderContextValue.modelProviders = []
|
||||
setupModelLists()
|
||||
@@ -865,9 +877,7 @@ describe('ModelParameterModal', () => {
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(Toast.notify).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ type: 'warning' }),
|
||||
)
|
||||
expect(mockToastNotify).toHaveBeenCalledWith(expect.objectContaining({ type: 'warning' }))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -892,9 +902,7 @@ describe('ModelParameterModal', () => {
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(Toast.notify).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ type: 'error' }),
|
||||
)
|
||||
expect(mockToastNotify).toHaveBeenCalledWith(expect.objectContaining({ type: 'error' }))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -10,12 +10,12 @@ import type {
|
||||
import type { TriggerProps } from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal/trigger'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/app/components/base/ui/popover'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { ModelStatusEnum, ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import {
|
||||
useModelList,
|
||||
@@ -134,14 +134,11 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
|
||||
|
||||
const keys = Object.keys(removedDetails || {})
|
||||
if (keys.length) {
|
||||
Toast.notify({
|
||||
type: 'warning',
|
||||
message: `${t('modelProvider.parametersInvalidRemoved', { ns: 'common' })}: ${keys.map(k => `${k} (${removedDetails[k]})`).join(', ')}`,
|
||||
})
|
||||
toast.warning(`${t('modelProvider.parametersInvalidRemoved', { ns: 'common' })}: ${keys.map(k => `${k} (${removedDetails[k]})`).join(', ')}`)
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Toast.notify({ type: 'error', message: t('error', { ns: 'common' }) })
|
||||
toast.error(t('error', { ns: 'common' }))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,26 @@
|
||||
import type { TriggerLogEntity } from '@/app/components/workflow/block-selector/types'
|
||||
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import LogViewer from '../log-viewer'
|
||||
|
||||
const mockToastNotify = vi.fn()
|
||||
const mockWriteText = vi.fn()
|
||||
|
||||
vi.mock('@/app/components/base/ui/toast', () => ({
|
||||
toast: Object.assign(
|
||||
(message: string, options?: { type?: string }) => mockToastNotify({ type: options?.type, message }),
|
||||
{
|
||||
success: (message: string) => mockToastNotify({ type: 'success', message }),
|
||||
error: (message: string) => mockToastNotify({ type: 'error', message }),
|
||||
warning: (message: string) => mockToastNotify({ type: 'warning', message }),
|
||||
info: (message: string) => mockToastNotify({ type: 'info', message }),
|
||||
dismiss: vi.fn(),
|
||||
update: vi.fn(),
|
||||
promise: vi.fn(),
|
||||
},
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/nodes/_base/components/editor/code-editor', () => ({
|
||||
default: ({ value }: { value: unknown }) => (
|
||||
<div data-testid="code-editor">{JSON.stringify(value)}</div>
|
||||
@@ -57,10 +71,6 @@ beforeEach(() => {
|
||||
},
|
||||
configurable: true,
|
||||
})
|
||||
vi.spyOn(Toast, 'notify').mockImplementation((args) => {
|
||||
mockToastNotify(args)
|
||||
return { clear: vi.fn() }
|
||||
})
|
||||
})
|
||||
|
||||
describe('LogViewer', () => {
|
||||
|
||||
@@ -26,10 +26,16 @@ vi.mock('@/service/use-triggers', () => ({
|
||||
useDeleteTriggerSubscription: () => ({ mutate: vi.fn(), isPending: false }),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
default: {
|
||||
notify: vi.fn(),
|
||||
},
|
||||
vi.mock('@/app/components/base/ui/toast', () => ({
|
||||
toast: Object.assign(vi.fn(), {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warning: vi.fn(),
|
||||
info: vi.fn(),
|
||||
dismiss: vi.fn(),
|
||||
update: vi.fn(),
|
||||
promise: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
const createSubscription = (overrides: Partial<TriggerSubscription> = {}): TriggerSubscription => ({
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { TriggerSubscription } from '@/app/components/workflow/block-selector/types'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { TriggerCredentialTypeEnum } from '@/app/components/workflow/block-selector/types'
|
||||
import { SubscriptionSelectorView } from '../selector-view'
|
||||
|
||||
@@ -26,6 +25,18 @@ vi.mock('@/service/use-triggers', () => ({
|
||||
useDeleteTriggerSubscription: () => ({ mutate: mockDelete, isPending: false }),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/ui/toast', () => ({
|
||||
toast: Object.assign(vi.fn(), {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warning: vi.fn(),
|
||||
info: vi.fn(),
|
||||
dismiss: vi.fn(),
|
||||
update: vi.fn(),
|
||||
promise: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
const createSubscription = (overrides: Partial<TriggerSubscription> = {}): TriggerSubscription => ({
|
||||
id: 'sub-1',
|
||||
name: 'Subscription One',
|
||||
@@ -42,7 +53,6 @@ const createSubscription = (overrides: Partial<TriggerSubscription> = {}): Trigg
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockSubscriptions = [createSubscription()]
|
||||
vi.spyOn(Toast, 'notify').mockImplementation(() => ({ clear: vi.fn() }))
|
||||
})
|
||||
|
||||
describe('SubscriptionSelectorView', () => {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { TriggerSubscription } from '@/app/components/workflow/block-selector/types'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { TriggerCredentialTypeEnum } from '@/app/components/workflow/block-selector/types'
|
||||
import SubscriptionCard from '../subscription-card'
|
||||
|
||||
@@ -30,6 +29,18 @@ vi.mock('@/service/use-triggers', () => ({
|
||||
useDeleteTriggerSubscription: () => ({ mutate: vi.fn(), isPending: false }),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/ui/toast', () => ({
|
||||
toast: Object.assign(vi.fn(), {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warning: vi.fn(),
|
||||
info: vi.fn(),
|
||||
dismiss: vi.fn(),
|
||||
update: vi.fn(),
|
||||
promise: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
const createSubscription = (overrides: Partial<TriggerSubscription> = {}): TriggerSubscription => ({
|
||||
id: 'sub-1',
|
||||
name: 'Subscription One',
|
||||
@@ -45,7 +56,6 @@ const createSubscription = (overrides: Partial<TriggerSubscription> = {}): Trigg
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.spyOn(Toast, 'notify').mockImplementation(() => ({ clear: vi.fn() }))
|
||||
})
|
||||
|
||||
describe('SubscriptionCard', () => {
|
||||
|
||||
@@ -122,10 +122,16 @@ vi.mock('@/utils/urlValidation', () => ({
|
||||
}))
|
||||
|
||||
const mockToastNotify = vi.fn()
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
default: {
|
||||
notify: (params: unknown) => mockToastNotify(params),
|
||||
},
|
||||
vi.mock('@/app/components/base/ui/toast', () => ({
|
||||
toast: Object.assign((params: unknown) => mockToastNotify(params), {
|
||||
success: (message: unknown) => mockToastNotify({ type: 'success', message }),
|
||||
error: (message: unknown) => mockToastNotify({ type: 'error', message }),
|
||||
warning: (message: unknown) => mockToastNotify({ type: 'warning', message }),
|
||||
info: (message: unknown) => mockToastNotify({ type: 'info', message }),
|
||||
dismiss: vi.fn(),
|
||||
update: vi.fn(),
|
||||
promise: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/modal/modal', () => ({
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { SimpleDetail } from '../../../store'
|
||||
import type { TriggerOAuthConfig, TriggerProviderApiEntity, TriggerSubscription, TriggerSubscriptionBuilder } from '@/app/components/workflow/block-selector/types'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { SupportedCreationMethods } from '@/app/components/plugins/types'
|
||||
import { TriggerCredentialTypeEnum } from '@/app/components/workflow/block-selector/types'
|
||||
import { CreateButtonType, CreateSubscriptionButton, DEFAULT_METHOD } from '../index'
|
||||
@@ -33,10 +34,16 @@ vi.mock('@/app/components/base/portal-to-follow-elem', () => ({
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
default: {
|
||||
notify: vi.fn(),
|
||||
},
|
||||
vi.mock('@/app/components/base/ui/toast', () => ({
|
||||
toast: Object.assign(vi.fn(), {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warning: vi.fn(),
|
||||
info: vi.fn(),
|
||||
dismiss: vi.fn(),
|
||||
update: vi.fn(),
|
||||
promise: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
let mockStoreDetail: SimpleDetail | undefined
|
||||
@@ -908,8 +915,6 @@ describe('CreateSubscriptionButton', () => {
|
||||
|
||||
it('should handle OAuth initiation error', async () => {
|
||||
// Arrange
|
||||
const Toast = await import('@/app/components/base/toast')
|
||||
|
||||
mockInitiateOAuth.mockImplementation((_provider: string, callbacks: { onError: () => void }) => {
|
||||
callbacks.onError()
|
||||
})
|
||||
@@ -932,9 +937,7 @@ describe('CreateSubscriptionButton', () => {
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(Toast.default.notify).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ type: 'error' }),
|
||||
)
|
||||
expect(toast.error).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -86,10 +86,19 @@ vi.mock('@/hooks/use-oauth', () => ({
|
||||
}))
|
||||
|
||||
const mockToastNotify = vi.fn()
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
default: {
|
||||
notify: (params: unknown) => mockToastNotify(params),
|
||||
},
|
||||
vi.mock('@/app/components/base/ui/toast', () => ({
|
||||
toast: Object.assign(
|
||||
(message: string, options?: { type?: string }) => mockToastNotify({ type: options?.type, message }),
|
||||
{
|
||||
success: (message: string) => mockToastNotify({ type: 'success', message }),
|
||||
error: (message: string) => mockToastNotify({ type: 'error', message }),
|
||||
warning: (message: string) => mockToastNotify({ type: 'warning', message }),
|
||||
info: (message: string) => mockToastNotify({ type: 'info', message }),
|
||||
dismiss: vi.fn(),
|
||||
update: vi.fn(),
|
||||
promise: vi.fn(),
|
||||
},
|
||||
),
|
||||
}))
|
||||
|
||||
const mockClipboardWriteText = vi.fn()
|
||||
|
||||
@@ -77,10 +77,19 @@ vi.mock('@/hooks/use-oauth', () => ({
|
||||
}))
|
||||
|
||||
const mockToastNotify = vi.fn()
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
default: {
|
||||
notify: (params: unknown) => mockToastNotify(params),
|
||||
},
|
||||
vi.mock('@/app/components/base/ui/toast', () => ({
|
||||
toast: Object.assign(
|
||||
(message: string, options?: { type?: string }) => mockToastNotify({ type: options?.type, message }),
|
||||
{
|
||||
success: (message: string) => mockToastNotify({ type: 'success', message }),
|
||||
error: (message: string) => mockToastNotify({ type: 'error', message }),
|
||||
warning: (message: string) => mockToastNotify({ type: 'warning', message }),
|
||||
info: (message: string) => mockToastNotify({ type: 'info', message }),
|
||||
dismiss: vi.fn(),
|
||||
update: vi.fn(),
|
||||
promise: vi.fn(),
|
||||
},
|
||||
),
|
||||
}))
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { BuildTriggerSubscriptionPayload } from '@/service/use-triggers'
|
||||
import { debounce } from 'es-toolkit/compat'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { SupportedCreationMethods } from '@/app/components/plugins/types'
|
||||
import { TriggerCredentialTypeEnum } from '@/app/components/workflow/block-selector/types'
|
||||
import {
|
||||
@@ -154,10 +154,7 @@ export const useCommonModalState = ({
|
||||
onError: async (error: unknown) => {
|
||||
const errorMessage = await parsePluginErrorMessage(error) || t('modal.errors.updateFailed', { ns: 'pluginTrigger' })
|
||||
console.error('Failed to update subscription builder:', error)
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: errorMessage,
|
||||
})
|
||||
toast.error(errorMessage)
|
||||
},
|
||||
},
|
||||
)
|
||||
@@ -178,10 +175,7 @@ export const useCommonModalState = ({
|
||||
}
|
||||
catch (error) {
|
||||
console.error('createBuilder error:', error)
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('modal.errors.createFailed', { ns: 'pluginTrigger' }),
|
||||
})
|
||||
toast.error(t('modal.errors.createFailed', { ns: 'pluginTrigger' }))
|
||||
}
|
||||
}
|
||||
if (!isInitializedRef.current && !subscriptionBuilder && detail?.provider)
|
||||
@@ -239,10 +233,7 @@ export const useCommonModalState = ({
|
||||
const handleVerify = useCallback(() => {
|
||||
// Guard against uninitialized state
|
||||
if (!detail?.provider || !subscriptionBuilder?.id) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: 'Subscription builder not initialized',
|
||||
})
|
||||
toast.error('Subscription builder not initialized')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -250,10 +241,7 @@ export const useCommonModalState = ({
|
||||
const credentials = apiKeyCredentialsFormValues.values
|
||||
|
||||
if (!Object.keys(credentials).length) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: 'Please fill in all required credentials',
|
||||
})
|
||||
toast.error('Please fill in all required credentials')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -270,10 +258,7 @@ export const useCommonModalState = ({
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('modal.apiKey.verify.success', { ns: 'pluginTrigger' }),
|
||||
})
|
||||
toast.success(t('modal.apiKey.verify.success', { ns: 'pluginTrigger' }))
|
||||
setCurrentStep(ApiKeyStep.Configuration)
|
||||
},
|
||||
onError: async (error: unknown) => {
|
||||
@@ -290,10 +275,7 @@ export const useCommonModalState = ({
|
||||
// Handle create
|
||||
const handleCreate = useCallback(() => {
|
||||
if (!subscriptionBuilder) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: 'Subscription builder not found',
|
||||
})
|
||||
toast.error('Subscription builder not found')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -327,19 +309,13 @@ export const useCommonModalState = ({
|
||||
params,
|
||||
{
|
||||
onSuccess: () => {
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('subscription.createSuccess', { ns: 'pluginTrigger' }),
|
||||
})
|
||||
toast.success(t('subscription.createSuccess', { ns: 'pluginTrigger' }))
|
||||
onClose()
|
||||
refetch?.()
|
||||
},
|
||||
onError: async (error: unknown) => {
|
||||
const errorMessage = await parsePluginErrorMessage(error) || t('subscription.createFailed', { ns: 'pluginTrigger' })
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: errorMessage,
|
||||
})
|
||||
toast.error(errorMessage)
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { TriggerOAuthClientParams, TriggerOAuthConfig, TriggerSubscriptionB
|
||||
import type { ConfigureTriggerOAuthPayload } from '@/service/use-triggers'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { openOAuthPopup } from '@/hooks/use-oauth'
|
||||
import {
|
||||
useConfigureTriggerOAuth,
|
||||
@@ -118,20 +118,14 @@ export const useOAuthClientState = ({
|
||||
openOAuthPopup(response.authorization_url, (callbackData) => {
|
||||
if (!callbackData)
|
||||
return
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('modal.oauth.authorization.authSuccess', { ns: 'pluginTrigger' }),
|
||||
})
|
||||
toast.success(t('modal.oauth.authorization.authSuccess', { ns: 'pluginTrigger' }))
|
||||
onClose()
|
||||
showOAuthCreateModal(response.subscription_builder)
|
||||
})
|
||||
},
|
||||
onError: () => {
|
||||
setAuthorizationStatus(AuthorizationStatusEnum.Failed)
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('modal.oauth.authorization.authFailed', { ns: 'pluginTrigger' }),
|
||||
})
|
||||
toast.error(t('modal.oauth.authorization.authFailed', { ns: 'pluginTrigger' }))
|
||||
},
|
||||
})
|
||||
}, [providerName, initiateOAuth, onClose, showOAuthCreateModal, t])
|
||||
@@ -141,16 +135,10 @@ export const useOAuthClientState = ({
|
||||
deleteOAuth(providerName, {
|
||||
onSuccess: () => {
|
||||
onClose()
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('modal.oauth.remove.success', { ns: 'pluginTrigger' }),
|
||||
})
|
||||
toast.success(t('modal.oauth.remove.success', { ns: 'pluginTrigger' }))
|
||||
},
|
||||
onError: (error: unknown) => {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: getErrorMessage(error, t('modal.oauth.remove.failed', { ns: 'pluginTrigger' })),
|
||||
})
|
||||
toast.error(getErrorMessage(error, t('modal.oauth.remove.failed', { ns: 'pluginTrigger' })))
|
||||
},
|
||||
})
|
||||
}, [providerName, deleteOAuth, onClose, t])
|
||||
@@ -187,10 +175,7 @@ export const useOAuthClientState = ({
|
||||
return
|
||||
}
|
||||
onClose()
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('modal.oauth.save.success', { ns: 'pluginTrigger' }),
|
||||
})
|
||||
toast.success(t('modal.oauth.save.success', { ns: 'pluginTrigger' }))
|
||||
},
|
||||
})
|
||||
}, [clientType, providerName, oauthClientSchema, oauthConfig?.params, configureOAuth, handleAuthorization, onClose, t])
|
||||
|
||||
@@ -8,8 +8,8 @@ import { ActionButton, ActionButtonState } from '@/app/components/base/action-bu
|
||||
import Badge from '@/app/components/base/badge'
|
||||
import { Button } from '@/app/components/base/button'
|
||||
import CustomSelect from '@/app/components/base/select/custom'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { openOAuthPopup } from '@/hooks/use-oauth'
|
||||
import { useInitiateTriggerOAuth, useTriggerOAuthConfig, useTriggerProviderInfo } from '@/service/use-triggers'
|
||||
import { cn } from '@/utils/classnames'
|
||||
@@ -107,19 +107,13 @@ export const CreateSubscriptionButton = ({ buttonType = CreateButtonType.FULL_BU
|
||||
onSuccess: (response) => {
|
||||
openOAuthPopup(response.authorization_url, (callbackData) => {
|
||||
if (callbackData) {
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('modal.oauth.authorization.authSuccess', { ns: 'pluginTrigger' }),
|
||||
})
|
||||
toast.success(t('modal.oauth.authorization.authSuccess', { ns: 'pluginTrigger' }))
|
||||
setSelectedCreateInfo({ type: SupportedCreationMethods.OAUTH, builder: response.subscription_builder })
|
||||
}
|
||||
})
|
||||
},
|
||||
onError: () => {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('modal.oauth.authorization.authFailed', { ns: 'pluginTrigger' }),
|
||||
})
|
||||
toast.error(t('modal.oauth.authorization.authFailed', { ns: 'pluginTrigger' }))
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { BaseForm } from '@/app/components/base/form/components/base'
|
||||
import Modal from '@/app/components/base/modal/modal'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import OptionCard from '@/app/components/workflow/nodes/_base/components/option-card'
|
||||
import { usePluginStore } from '../../store'
|
||||
import { ClientTypeEnum, useOAuthClientState } from './hooks/use-oauth-client-state'
|
||||
@@ -48,10 +48,7 @@ export const OAuthClientSettingsModal = ({ oauthConfig, onClose, showOAuthCreate
|
||||
|
||||
const handleCopyRedirectUri = () => {
|
||||
navigator.clipboard.writeText(oauthConfig?.redirect_uri || '')
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('actionMsg.copySuccessfully', { ns: 'common' }),
|
||||
})
|
||||
toast.success(t('actionMsg.copySuccessfully', { ns: 'common' }))
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -47,13 +47,19 @@ vi.mock('@/service/use-triggers', () => ({
|
||||
useTriggerPluginDynamicOptions: () => ({ data: [], isLoading: false }),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/toast', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/app/components/base/toast')>()
|
||||
vi.mock('@/app/components/base/ui/toast', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/app/components/base/ui/toast')>()
|
||||
return {
|
||||
...actual,
|
||||
default: {
|
||||
notify: (args: { type: string, message: string }) => mockToast(args),
|
||||
},
|
||||
toast: Object.assign((args: { type: string, message: string }) => mockToast(args), {
|
||||
success: (message: string) => mockToast({ type: 'success', message }),
|
||||
error: (message: string) => mockToast({ type: 'error', message }),
|
||||
warning: (message: string) => mockToast({ type: 'warning', message }),
|
||||
info: (message: string) => mockToast({ type: 'info', message }),
|
||||
dismiss: vi.fn(),
|
||||
update: vi.fn(),
|
||||
promise: vi.fn(),
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -13,8 +13,16 @@ import { OAuthEditModal } from '../oauth-edit-modal'
|
||||
// ==================== Mock Setup ====================
|
||||
|
||||
const mockToastNotify = vi.fn()
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
default: { notify: (params: unknown) => mockToastNotify(params) },
|
||||
vi.mock('@/app/components/base/ui/toast', () => ({
|
||||
toast: Object.assign((message: string, options?: { type?: string }) => mockToastNotify({ type: options?.type, message }), {
|
||||
success: (message: string) => mockToastNotify({ type: 'success', message }),
|
||||
error: (message: string) => mockToastNotify({ type: 'error', message }),
|
||||
warning: (message: string) => mockToastNotify({ type: 'warning', message }),
|
||||
info: (message: string) => mockToastNotify({ type: 'info', message }),
|
||||
dismiss: vi.fn(),
|
||||
update: vi.fn(),
|
||||
promise: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
const mockParsePluginErrorMessage = vi.fn()
|
||||
|
||||
@@ -30,13 +30,19 @@ vi.mock('@/service/use-triggers', () => ({
|
||||
useTriggerPluginDynamicOptions: () => ({ data: [], isLoading: false }),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/toast', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/app/components/base/toast')>()
|
||||
vi.mock('@/app/components/base/ui/toast', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/app/components/base/ui/toast')>()
|
||||
return {
|
||||
...actual,
|
||||
default: {
|
||||
notify: (args: { type: string, message: string }) => mockToast(args),
|
||||
},
|
||||
toast: Object.assign((args: { type: string, message: string }) => mockToast(args), {
|
||||
success: (message: string) => mockToast({ type: 'success', message }),
|
||||
error: (message: string) => mockToast({ type: 'error', message }),
|
||||
warning: (message: string) => mockToast({ type: 'warning', message }),
|
||||
info: (message: string) => mockToast({ type: 'info', message }),
|
||||
dismiss: vi.fn(),
|
||||
update: vi.fn(),
|
||||
promise: vi.fn(),
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -30,13 +30,19 @@ vi.mock('@/service/use-triggers', () => ({
|
||||
useTriggerPluginDynamicOptions: () => ({ data: [], isLoading: false }),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/toast', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/app/components/base/toast')>()
|
||||
vi.mock('@/app/components/base/ui/toast', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/app/components/base/ui/toast')>()
|
||||
return {
|
||||
...actual,
|
||||
default: {
|
||||
notify: (args: { type: string, message: string }) => mockToast(args),
|
||||
},
|
||||
toast: Object.assign((args: { type: string, message: string }) => mockToast(args), {
|
||||
success: (message: string) => mockToast({ type: 'success', message }),
|
||||
error: (message: string) => mockToast({ type: 'error', message }),
|
||||
warning: (message: string) => mockToast({ type: 'warning', message }),
|
||||
info: (message: string) => mockToast({ type: 'info', message }),
|
||||
dismiss: vi.fn(),
|
||||
update: vi.fn(),
|
||||
promise: vi.fn(),
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import { EncryptedBottom } from '@/app/components/base/encrypted-bottom'
|
||||
import { BaseForm } from '@/app/components/base/form/components/base'
|
||||
import { FormTypeEnum } from '@/app/components/base/form/types'
|
||||
import Modal from '@/app/components/base/modal/modal'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { ReadmeEntrance } from '@/app/components/plugins/readme-panel/entrance'
|
||||
import { useUpdateTriggerSubscription, useVerifyTriggerSubscription } from '@/service/use-triggers'
|
||||
import { parsePluginErrorMessage } from '@/utils/error-parser'
|
||||
@@ -65,7 +65,7 @@ const StatusStep = ({ isActive, text, onClick, clickable }: {
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={`system-2xs-semibold-uppercase flex items-center gap-1 ${isActive
|
||||
className={`flex items-center gap-1 system-2xs-semibold-uppercase ${isActive
|
||||
? 'text-state-accent-solid'
|
||||
: 'text-text-tertiary'} ${clickable ? 'cursor-pointer hover:text-text-secondary' : ''}`}
|
||||
onClick={clickable ? onClick : undefined}
|
||||
@@ -143,20 +143,14 @@ export const ApiKeyEditModal = ({ onClose, subscription, pluginDetail }: Props)
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('modal.apiKey.verify.success', { ns: 'pluginTrigger' }),
|
||||
})
|
||||
toast.success(t('modal.apiKey.verify.success', { ns: 'pluginTrigger' }))
|
||||
// Only save credentials if any field was modified (not all hidden)
|
||||
setVerifiedCredentials(areAllCredentialsHidden(credentials) ? null : credentials)
|
||||
setCurrentStep(EditStep.EditConfiguration)
|
||||
},
|
||||
onError: async (error: unknown) => {
|
||||
const errorMessage = await parsePluginErrorMessage(error) || t('modal.apiKey.verify.error', { ns: 'pluginTrigger' })
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: errorMessage,
|
||||
})
|
||||
toast.error(errorMessage)
|
||||
},
|
||||
},
|
||||
)
|
||||
@@ -192,19 +186,13 @@ export const ApiKeyEditModal = ({ onClose, subscription, pluginDetail }: Props)
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('subscription.list.item.actions.edit.success', { ns: 'pluginTrigger' }),
|
||||
})
|
||||
toast.success(t('subscription.list.item.actions.edit.success', { ns: 'pluginTrigger' }))
|
||||
refetch?.()
|
||||
onClose()
|
||||
},
|
||||
onError: async (error: unknown) => {
|
||||
const errorMessage = await parsePluginErrorMessage(error) || t('subscription.list.item.actions.edit.error', { ns: 'pluginTrigger' })
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: errorMessage,
|
||||
})
|
||||
toast.error(errorMessage)
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import { BaseForm } from '@/app/components/base/form/components/base'
|
||||
import { FormTypeEnum } from '@/app/components/base/form/types'
|
||||
import Modal from '@/app/components/base/modal/modal'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { ReadmeEntrance } from '@/app/components/plugins/readme-panel/entrance'
|
||||
import { useUpdateTriggerSubscription } from '@/service/use-triggers'
|
||||
import { ReadmeShowType } from '../../../readme-panel/store'
|
||||
@@ -94,18 +94,12 @@ export const ManualEditModal = ({ onClose, subscription, pluginDetail }: Props)
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('subscription.list.item.actions.edit.success', { ns: 'pluginTrigger' }),
|
||||
})
|
||||
toast.success(t('subscription.list.item.actions.edit.success', { ns: 'pluginTrigger' }))
|
||||
refetch?.()
|
||||
onClose()
|
||||
},
|
||||
onError: (error: unknown) => {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: getErrorMessage(error, t('subscription.list.item.actions.edit.error', { ns: 'pluginTrigger' })),
|
||||
})
|
||||
toast.error(getErrorMessage(error, t('subscription.list.item.actions.edit.error', { ns: 'pluginTrigger' })))
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import { BaseForm } from '@/app/components/base/form/components/base'
|
||||
import { FormTypeEnum } from '@/app/components/base/form/types'
|
||||
import Modal from '@/app/components/base/modal/modal'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { ReadmeEntrance } from '@/app/components/plugins/readme-panel/entrance'
|
||||
import { useUpdateTriggerSubscription } from '@/service/use-triggers'
|
||||
import { ReadmeShowType } from '../../../readme-panel/store'
|
||||
@@ -94,18 +94,12 @@ export const OAuthEditModal = ({ onClose, subscription, pluginDetail }: Props) =
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('subscription.list.item.actions.edit.success', { ns: 'pluginTrigger' }),
|
||||
})
|
||||
toast.success(t('subscription.list.item.actions.edit.success', { ns: 'pluginTrigger' }))
|
||||
refetch?.()
|
||||
onClose()
|
||||
},
|
||||
onError: (error: unknown) => {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: getErrorMessage(error, t('subscription.list.item.actions.edit.error', { ns: 'pluginTrigger' })),
|
||||
})
|
||||
toast.error(getErrorMessage(error, t('subscription.list.item.actions.edit.error', { ns: 'pluginTrigger' })))
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
@@ -11,7 +11,7 @@ import dayjs from 'dayjs'
|
||||
import * as React from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
|
||||
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
|
||||
import { cn } from '@/utils/classnames'
|
||||
@@ -89,10 +89,7 @@ const LogViewer = ({ logs, className }: Props) => {
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
navigator.clipboard.writeText(String(parsedData))
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('actionMsg.copySuccessfully', { ns: 'common' }),
|
||||
})
|
||||
toast.success(t('actionMsg.copySuccessfully', { ns: 'common' }))
|
||||
}}
|
||||
className="rounded-md p-0.5 hover:bg-components-panel-border"
|
||||
>
|
||||
|
||||
@@ -298,8 +298,16 @@ vi.mock('@/app/components/header/account-setting/model-provider-page/model-modal
|
||||
|
||||
// Mock Toast - need to track notify calls for assertions
|
||||
const mockToastNotify = vi.fn()
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
default: { notify: (...args: unknown[]) => mockToastNotify(...args) },
|
||||
vi.mock('@/app/components/base/ui/toast', () => ({
|
||||
toast: Object.assign((message: string, options?: { type?: string }) => mockToastNotify({ type: options?.type, message }), {
|
||||
success: (message: string) => mockToastNotify({ type: 'success', message }),
|
||||
error: (message: string) => mockToastNotify({ type: 'error', message }),
|
||||
warning: (message: string) => mockToastNotify({ type: 'warning', message }),
|
||||
info: (message: string) => mockToastNotify({ type: 'info', message }),
|
||||
dismiss: vi.fn(),
|
||||
update: vi.fn(),
|
||||
promise: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
// ==================== Test Utilities ====================
|
||||
@@ -1943,7 +1951,7 @@ describe('ToolCredentialsForm Component', () => {
|
||||
const saveBtn = screen.getByText(/save/i)
|
||||
fireEvent.click(saveBtn)
|
||||
|
||||
// Toast.notify should have been called with error (lines 49-50)
|
||||
// notifyToast should have been called with error (lines 49-50)
|
||||
expect(mockToastNotify).toHaveBeenCalledWith(expect.objectContaining({ type: 'error' }))
|
||||
// onSaved should not be called because validation fails
|
||||
expect(onSaved).not.toHaveBeenCalled()
|
||||
|
||||
@@ -10,12 +10,16 @@ vi.mock('@/utils/classnames', () => ({
|
||||
cn: (...args: unknown[]) => args.filter(Boolean).join(' '),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
default: { notify: vi.fn() },
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/toast/context', () => ({
|
||||
useToastContext: () => ({ notify: vi.fn() }),
|
||||
vi.mock('@/app/components/base/ui/toast', () => ({
|
||||
toast: Object.assign(vi.fn(), {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warning: vi.fn(),
|
||||
info: vi.fn(),
|
||||
dismiss: vi.fn(),
|
||||
update: vi.fn(),
|
||||
promise: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
const mockFormSchemas = [
|
||||
|
||||
@@ -10,7 +10,7 @@ import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import Form from '@/app/components/header/account-setting/model-provider-page/model-modal/Form'
|
||||
import { addDefaultValue, toolCredentialToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
|
||||
import { useRenderI18nObject } from '@/hooks/use-i18n'
|
||||
@@ -49,7 +49,10 @@ const ToolCredentialForm: FC<Props> = ({
|
||||
return
|
||||
for (const field of credentialSchema) {
|
||||
if (field.required && !tempCredential[field.name]) {
|
||||
Toast.notify({ type: 'error', message: t('errorMsg.fieldRequired', { ns: 'common', field: getValueFromI18nObject(field.label) }) })
|
||||
toast.error(t('errorMsg.fieldRequired', {
|
||||
ns: 'common',
|
||||
field: getValueFromI18nObject(field.label),
|
||||
}))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { MetaData, PluginCategoryEnum } from '../../types'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
|
||||
// ==================== Imports (after mocks) ====================
|
||||
|
||||
@@ -17,12 +16,29 @@ const {
|
||||
mockCheckForUpdates,
|
||||
mockSetShowUpdatePluginModal,
|
||||
mockInvalidateInstalledPluginList,
|
||||
mockToastNotify,
|
||||
} = vi.hoisted(() => ({
|
||||
mockUninstallPlugin: vi.fn(),
|
||||
mockFetchReleases: vi.fn(),
|
||||
mockCheckForUpdates: vi.fn(),
|
||||
mockSetShowUpdatePluginModal: vi.fn(),
|
||||
mockInvalidateInstalledPluginList: vi.fn(),
|
||||
mockToastNotify: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/ui/toast', () => ({
|
||||
toast: Object.assign(
|
||||
(message: string, options?: { type?: string }) => mockToastNotify({ type: options?.type, message }),
|
||||
{
|
||||
success: (message: string) => mockToastNotify({ type: 'success', message }),
|
||||
error: (message: string) => mockToastNotify({ type: 'error', message }),
|
||||
warning: (message: string) => mockToastNotify({ type: 'warning', message }),
|
||||
info: (message: string) => mockToastNotify({ type: 'info', message }),
|
||||
dismiss: vi.fn(),
|
||||
update: vi.fn(),
|
||||
promise: vi.fn(),
|
||||
},
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock uninstall plugin service
|
||||
@@ -140,13 +156,8 @@ const getActionButtons = () => screen.getAllByRole('button')
|
||||
const queryActionButtons = () => screen.queryAllByRole('button')
|
||||
|
||||
describe('Action Component', () => {
|
||||
// Spy on Toast.notify - real component but we track calls
|
||||
let toastNotifySpy: ReturnType<typeof vi.spyOn>
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
// Spy on Toast.notify and mock implementation to avoid DOM side effects
|
||||
toastNotifySpy = vi.spyOn(Toast, 'notify').mockImplementation(() => ({ clear: vi.fn() }))
|
||||
mockUninstallPlugin.mockResolvedValue({ success: true })
|
||||
mockFetchReleases.mockResolvedValue([])
|
||||
mockCheckForUpdates.mockReturnValue({
|
||||
@@ -155,10 +166,6 @@ describe('Action Component', () => {
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
toastNotifySpy.mockRestore()
|
||||
})
|
||||
|
||||
// ==================== Rendering Tests ====================
|
||||
describe('Rendering', () => {
|
||||
it('should render delete button when isShowDelete is true', () => {
|
||||
@@ -563,9 +570,9 @@ describe('Action Component', () => {
|
||||
render(<Action {...props} />)
|
||||
fireEvent.click(getActionButtons()[0])
|
||||
|
||||
// Assert - Toast.notify is called with the toast props
|
||||
// Assert - toast is called with the translated payload
|
||||
await waitFor(() => {
|
||||
expect(toastNotifySpy).toHaveBeenCalledWith({ type: 'success', message: 'Already up to date' })
|
||||
expect(mockToastNotify).toHaveBeenCalledWith({ type: 'success', message: 'Already up to date' })
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useBoolean } from 'ahooks'
|
||||
import * as React from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { uninstallPlugin } from '@/service/plugins'
|
||||
import { useInvalidateInstalledPluginList } from '@/service/use-plugins'
|
||||
@@ -65,7 +65,7 @@ const Action: FC<Props> = ({
|
||||
if (fetchedReleases.length === 0)
|
||||
return
|
||||
const { needUpdate, toastProps } = checkForUpdates(fetchedReleases, meta!.version)
|
||||
Toast.notify(toastProps)
|
||||
toast(toastProps.message, { type: toastProps.type })
|
||||
if (needUpdate) {
|
||||
setShowUpdatePluginModal({
|
||||
onSaveCallback: () => {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,199 @@
|
||||
/* eslint-disable ts/no-explicit-any */
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import { useChat } from '../../hooks'
|
||||
|
||||
const mockHandleRun = vi.fn()
|
||||
const mockNotify = vi.fn()
|
||||
const mockFetchInspectVars = vi.fn()
|
||||
const mockInvalidAllLastRun = vi.fn()
|
||||
const mockSetIterTimes = vi.fn()
|
||||
const mockSetLoopTimes = vi.fn()
|
||||
const mockSubmitHumanInputForm = vi.fn()
|
||||
const mockSseGet = vi.fn()
|
||||
const mockGetNodes = vi.fn((): any[] => [])
|
||||
|
||||
let mockWorkflowRunningData: any = null
|
||||
|
||||
vi.mock('@/service/base', () => ({
|
||||
sseGet: (...args: any[]) => mockSseGet(...args),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-workflow', () => ({
|
||||
useInvalidAllLastRun: () => mockInvalidAllLastRun,
|
||||
}))
|
||||
|
||||
vi.mock('@/service/workflow', () => ({
|
||||
submitHumanInputForm: (...args: any[]) => mockSubmitHumanInputForm(...args),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/ui/toast', () => ({
|
||||
toast: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warning: vi.fn(),
|
||||
info: (...args: any[]) => mockNotify(...args),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('reactflow', () => ({
|
||||
useStoreApi: () => ({
|
||||
getState: () => ({
|
||||
getNodes: mockGetNodes,
|
||||
}),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../../../../hooks', () => ({
|
||||
useWorkflowRun: () => ({ handleRun: mockHandleRun }),
|
||||
useSetWorkflowVarsWithValue: () => ({ fetchInspectVars: mockFetchInspectVars }),
|
||||
}))
|
||||
|
||||
vi.mock('../../../../hooks-store', () => ({
|
||||
useHooksStore: () => null,
|
||||
}))
|
||||
|
||||
vi.mock('../../../../store', () => ({
|
||||
useWorkflowStore: () => ({
|
||||
getState: () => ({
|
||||
setIterTimes: mockSetIterTimes,
|
||||
setLoopTimes: mockSetLoopTimes,
|
||||
inputs: {},
|
||||
workflowRunningData: mockWorkflowRunningData,
|
||||
}),
|
||||
}),
|
||||
useStore: () => vi.fn(),
|
||||
}))
|
||||
|
||||
const resetMocksAndWorkflowState = () => {
|
||||
vi.clearAllMocks()
|
||||
mockWorkflowRunningData = null
|
||||
}
|
||||
|
||||
describe('useChat – handleSend', () => {
|
||||
beforeEach(() => {
|
||||
resetMocksAndWorkflowState()
|
||||
mockHandleRun.mockReset()
|
||||
})
|
||||
|
||||
it('should call handleRun with processed params', () => {
|
||||
const { result } = renderHook(() => useChat({}))
|
||||
|
||||
act(() => {
|
||||
result.current.handleSend({ query: 'hello', inputs: {} }, {})
|
||||
})
|
||||
|
||||
expect(mockHandleRun).toHaveBeenCalledTimes(1)
|
||||
const [bodyParams] = mockHandleRun.mock.calls[0]
|
||||
expect(bodyParams.query).toBe('hello')
|
||||
})
|
||||
|
||||
it('should show notification and return false when already responding', () => {
|
||||
mockHandleRun.mockImplementation(() => {})
|
||||
|
||||
const { result } = renderHook(() => useChat({}))
|
||||
|
||||
act(() => {
|
||||
result.current.handleSend({ query: 'first' }, {})
|
||||
})
|
||||
|
||||
act(() => {
|
||||
const returned = result.current.handleSend({ query: 'second' }, {})
|
||||
expect(returned).toBe(false)
|
||||
})
|
||||
|
||||
expect(mockNotify).toHaveBeenCalledWith('appDebug.errorMessage.waitForResponse')
|
||||
})
|
||||
|
||||
it('should set isResponding to true after sending', () => {
|
||||
const { result } = renderHook(() => useChat({}))
|
||||
act(() => {
|
||||
result.current.handleSend({ query: 'hello' }, {})
|
||||
})
|
||||
expect(result.current.isResponding).toBe(true)
|
||||
})
|
||||
|
||||
it('should add placeholder question and answer to chatList', () => {
|
||||
const { result } = renderHook(() => useChat({}))
|
||||
|
||||
act(() => {
|
||||
result.current.handleSend({ query: 'test question' }, {})
|
||||
})
|
||||
|
||||
const questionItem = result.current.chatList.find(item => item.content === 'test question')
|
||||
expect(questionItem).toBeDefined()
|
||||
expect(questionItem!.isAnswer).toBe(false)
|
||||
|
||||
const answerPlaceholder = result.current.chatList.find(
|
||||
item => item.isAnswer && !item.isOpeningStatement && item.content === '',
|
||||
)
|
||||
expect(answerPlaceholder).toBeDefined()
|
||||
})
|
||||
|
||||
it('should strip url from local_file transfer method files', () => {
|
||||
const { result } = renderHook(() => useChat({}))
|
||||
|
||||
act(() => {
|
||||
result.current.handleSend(
|
||||
{
|
||||
query: 'hello',
|
||||
files: [
|
||||
{
|
||||
id: 'f1',
|
||||
name: 'test.png',
|
||||
size: 1024,
|
||||
type: 'image/png',
|
||||
progress: 100,
|
||||
transferMethod: 'local_file',
|
||||
supportFileType: 'image',
|
||||
url: 'blob://local',
|
||||
uploadedId: 'up1',
|
||||
},
|
||||
{
|
||||
id: 'f2',
|
||||
name: 'remote.png',
|
||||
size: 2048,
|
||||
type: 'image/png',
|
||||
progress: 100,
|
||||
transferMethod: 'remote_url',
|
||||
supportFileType: 'image',
|
||||
url: 'https://example.com/img.png',
|
||||
uploadedId: '',
|
||||
},
|
||||
] as any,
|
||||
},
|
||||
{},
|
||||
)
|
||||
})
|
||||
|
||||
expect(mockHandleRun).toHaveBeenCalledTimes(1)
|
||||
const [bodyParams] = mockHandleRun.mock.calls[0]
|
||||
const localFile = bodyParams.files.find((f: any) => f.transfer_method === 'local_file')
|
||||
const remoteFile = bodyParams.files.find((f: any) => f.transfer_method === 'remote_url')
|
||||
expect(localFile.url).toBe('')
|
||||
expect(remoteFile.url).toBe('https://example.com/img.png')
|
||||
})
|
||||
|
||||
it('should abort previous workflowEventsAbortController before sending', () => {
|
||||
const mockAbort = vi.fn()
|
||||
mockHandleRun.mockImplementation((_params: any, callbacks: any) => {
|
||||
callbacks.getAbortController({ abort: mockAbort } as any)
|
||||
callbacks.onCompleted(false)
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useChat({}))
|
||||
|
||||
act(() => {
|
||||
result.current.handleSend({ query: 'first' }, {})
|
||||
})
|
||||
|
||||
mockHandleRun.mockImplementation((_params: any, callbacks: any) => {
|
||||
callbacks.getAbortController({ abort: vi.fn() } as any)
|
||||
})
|
||||
|
||||
act(() => {
|
||||
result.current.handleSend({ query: 'second' }, {})
|
||||
})
|
||||
|
||||
expect(mockAbort).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,204 @@
|
||||
/* eslint-disable ts/no-explicit-any */
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import { useChat } from '../../hooks'
|
||||
|
||||
const mockHandleRun = vi.fn()
|
||||
const mockNotify = vi.fn()
|
||||
const mockFetchInspectVars = vi.fn()
|
||||
const mockInvalidAllLastRun = vi.fn()
|
||||
const mockSetIterTimes = vi.fn()
|
||||
const mockSetLoopTimes = vi.fn()
|
||||
const mockSubmitHumanInputForm = vi.fn()
|
||||
const mockSseGet = vi.fn()
|
||||
const mockStopChat = vi.fn()
|
||||
const mockGetNodes = vi.fn((): any[] => [])
|
||||
|
||||
let mockWorkflowRunningData: any = null
|
||||
|
||||
vi.mock('@/service/base', () => ({
|
||||
sseGet: (...args: any[]) => mockSseGet(...args),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-workflow', () => ({
|
||||
useInvalidAllLastRun: () => mockInvalidAllLastRun,
|
||||
}))
|
||||
|
||||
vi.mock('@/service/workflow', () => ({
|
||||
submitHumanInputForm: (...args: any[]) => mockSubmitHumanInputForm(...args),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/ui/toast', () => ({
|
||||
toast: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warning: vi.fn(),
|
||||
info: (...args: any[]) => mockNotify(...args),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('reactflow', () => ({
|
||||
useStoreApi: () => ({
|
||||
getState: () => ({
|
||||
getNodes: mockGetNodes,
|
||||
}),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../../../../hooks', () => ({
|
||||
useWorkflowRun: () => ({ handleRun: mockHandleRun }),
|
||||
useSetWorkflowVarsWithValue: () => ({ fetchInspectVars: mockFetchInspectVars }),
|
||||
}))
|
||||
|
||||
vi.mock('../../../../hooks-store', () => ({
|
||||
useHooksStore: () => null,
|
||||
}))
|
||||
|
||||
vi.mock('../../../../store', () => ({
|
||||
useWorkflowStore: () => ({
|
||||
getState: () => ({
|
||||
setIterTimes: mockSetIterTimes,
|
||||
setLoopTimes: mockSetLoopTimes,
|
||||
inputs: {},
|
||||
workflowRunningData: mockWorkflowRunningData,
|
||||
}),
|
||||
}),
|
||||
useStore: () => vi.fn(),
|
||||
}))
|
||||
|
||||
const resetMocksAndWorkflowState = () => {
|
||||
vi.clearAllMocks()
|
||||
mockWorkflowRunningData = null
|
||||
}
|
||||
|
||||
describe('useChat – handleStop', () => {
|
||||
beforeEach(() => {
|
||||
resetMocksAndWorkflowState()
|
||||
})
|
||||
|
||||
it('should set isResponding to false', () => {
|
||||
const { result } = renderHook(() => useChat({}))
|
||||
act(() => {
|
||||
result.current.handleStop()
|
||||
})
|
||||
expect(result.current.isResponding).toBe(false)
|
||||
})
|
||||
|
||||
it('should not call stopChat when taskId is empty even if stopChat is provided', () => {
|
||||
const { result } = renderHook(() => useChat({}, undefined, undefined, mockStopChat))
|
||||
act(() => {
|
||||
result.current.handleStop()
|
||||
})
|
||||
expect(mockStopChat).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should reset iter/loop times to defaults', () => {
|
||||
const { result } = renderHook(() => useChat({}))
|
||||
act(() => {
|
||||
result.current.handleStop()
|
||||
})
|
||||
expect(mockSetIterTimes).toHaveBeenCalledWith(1)
|
||||
expect(mockSetLoopTimes).toHaveBeenCalledWith(1)
|
||||
})
|
||||
|
||||
it('should abort workflowEventsAbortController when set', () => {
|
||||
const mockWfAbort = vi.fn()
|
||||
mockHandleRun.mockImplementation((_params: any, callbacks: any) => {
|
||||
callbacks.getAbortController({ abort: mockWfAbort } as any)
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useChat({}))
|
||||
|
||||
act(() => {
|
||||
result.current.handleSend({ query: 'test' }, {})
|
||||
})
|
||||
|
||||
act(() => {
|
||||
result.current.handleStop()
|
||||
})
|
||||
|
||||
expect(mockWfAbort).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should abort suggestedQuestionsAbortController when set', async () => {
|
||||
const mockSqAbort = vi.fn()
|
||||
let capturedCb: any
|
||||
|
||||
mockHandleRun.mockImplementation((_params: any, callbacks: any) => {
|
||||
capturedCb = callbacks
|
||||
})
|
||||
|
||||
const mockGetSuggested = vi.fn().mockImplementation((_id: string, getAbortCtrl: any) => {
|
||||
getAbortCtrl({ abort: mockSqAbort } as any)
|
||||
return Promise.resolve({ data: ['s'] })
|
||||
})
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useChat({ suggested_questions_after_answer: { enabled: true } }),
|
||||
)
|
||||
|
||||
act(() => {
|
||||
result.current.handleSend({ query: 'test' }, {
|
||||
onGetSuggestedQuestions: mockGetSuggested,
|
||||
})
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
await capturedCb.onCompleted(false)
|
||||
})
|
||||
|
||||
act(() => {
|
||||
result.current.handleStop()
|
||||
})
|
||||
|
||||
expect(mockSqAbort).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should call stopChat with taskId when both are available', () => {
|
||||
mockHandleRun.mockImplementation((_params: any, callbacks: any) => {
|
||||
callbacks.onData('msg', true, {
|
||||
conversationId: 'c1',
|
||||
messageId: 'msg-1',
|
||||
taskId: 'task-stop',
|
||||
})
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useChat({}, undefined, undefined, mockStopChat))
|
||||
|
||||
act(() => {
|
||||
result.current.handleSend({ query: 'test' }, {})
|
||||
})
|
||||
|
||||
act(() => {
|
||||
result.current.handleStop()
|
||||
})
|
||||
|
||||
expect(mockStopChat).toHaveBeenCalledWith('task-stop')
|
||||
})
|
||||
})
|
||||
|
||||
describe('useChat – handleRestart', () => {
|
||||
beforeEach(() => {
|
||||
resetMocksAndWorkflowState()
|
||||
})
|
||||
|
||||
it('should clear suggestedQuestions and set isResponding to false', () => {
|
||||
const config = { opening_statement: 'Hello' }
|
||||
const { result } = renderHook(() => useChat(config))
|
||||
|
||||
act(() => {
|
||||
result.current.handleRestart()
|
||||
})
|
||||
|
||||
expect(result.current.suggestedQuestions).toEqual([])
|
||||
expect(result.current.isResponding).toBe(false)
|
||||
})
|
||||
|
||||
it('should reset iter/loop times to defaults', () => {
|
||||
const { result } = renderHook(() => useChat({}))
|
||||
act(() => {
|
||||
result.current.handleRestart()
|
||||
})
|
||||
expect(mockSetIterTimes).toHaveBeenCalledWith(1)
|
||||
expect(mockSetLoopTimes).toHaveBeenCalledWith(1)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,385 @@
|
||||
/* eslint-disable ts/no-explicit-any */
|
||||
import type { ChatItemInTree } from '@/app/components/base/chat/types'
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import { useChat } from '../../hooks'
|
||||
|
||||
const mockHandleRun = vi.fn()
|
||||
const mockNotify = vi.fn()
|
||||
const mockFetchInspectVars = vi.fn()
|
||||
const mockInvalidAllLastRun = vi.fn()
|
||||
const mockSetIterTimes = vi.fn()
|
||||
const mockSetLoopTimes = vi.fn()
|
||||
const mockSubmitHumanInputForm = vi.fn()
|
||||
const mockSseGet = vi.fn()
|
||||
const mockGetNodes = vi.fn((): any[] => [])
|
||||
|
||||
let mockWorkflowRunningData: any = null
|
||||
|
||||
vi.mock('@/service/base', () => ({
|
||||
sseGet: (...args: any[]) => mockSseGet(...args),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-workflow', () => ({
|
||||
useInvalidAllLastRun: () => mockInvalidAllLastRun,
|
||||
}))
|
||||
|
||||
vi.mock('@/service/workflow', () => ({
|
||||
submitHumanInputForm: (...args: any[]) => mockSubmitHumanInputForm(...args),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/ui/toast', () => ({
|
||||
toast: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warning: vi.fn(),
|
||||
info: (...args: any[]) => mockNotify(...args),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('reactflow', () => ({
|
||||
useStoreApi: () => ({
|
||||
getState: () => ({
|
||||
getNodes: mockGetNodes,
|
||||
}),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../../../../hooks', () => ({
|
||||
useWorkflowRun: () => ({ handleRun: mockHandleRun }),
|
||||
useSetWorkflowVarsWithValue: () => ({ fetchInspectVars: mockFetchInspectVars }),
|
||||
}))
|
||||
|
||||
vi.mock('../../../../hooks-store', () => ({
|
||||
useHooksStore: () => null,
|
||||
}))
|
||||
|
||||
vi.mock('../../../../store', () => ({
|
||||
useWorkflowStore: () => ({
|
||||
getState: () => ({
|
||||
setIterTimes: mockSetIterTimes,
|
||||
setLoopTimes: mockSetLoopTimes,
|
||||
inputs: {},
|
||||
workflowRunningData: mockWorkflowRunningData,
|
||||
}),
|
||||
}),
|
||||
useStore: () => vi.fn(),
|
||||
}))
|
||||
|
||||
const resetMocksAndWorkflowState = () => {
|
||||
vi.clearAllMocks()
|
||||
mockWorkflowRunningData = null
|
||||
}
|
||||
|
||||
describe('useChat – handleSwitchSibling', () => {
|
||||
beforeEach(() => {
|
||||
resetMocksAndWorkflowState()
|
||||
mockHandleRun.mockReset()
|
||||
mockSseGet.mockReset()
|
||||
})
|
||||
|
||||
it('should call handleResume when target has workflow_run_id and pending humanInputFormData', async () => {
|
||||
let sendCallbacks: any
|
||||
mockHandleRun.mockImplementation((_params: any, callbacks: any) => {
|
||||
sendCallbacks = callbacks
|
||||
})
|
||||
mockSseGet.mockImplementation(() => {})
|
||||
|
||||
const { result } = renderHook(() => useChat({}))
|
||||
|
||||
act(() => {
|
||||
result.current.handleSend({ query: 'test' }, {})
|
||||
})
|
||||
|
||||
act(() => {
|
||||
sendCallbacks.onWorkflowStarted({
|
||||
workflow_run_id: 'wfr-switch',
|
||||
task_id: 'task-1',
|
||||
conversation_id: null,
|
||||
message_id: 'msg-switch',
|
||||
})
|
||||
})
|
||||
|
||||
act(() => {
|
||||
sendCallbacks.onHumanInputRequired({
|
||||
data: { node_id: 'human-n', form_token: 'ft-1' },
|
||||
})
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
await sendCallbacks.onCompleted(false)
|
||||
})
|
||||
|
||||
act(() => {
|
||||
result.current.handleSwitchSibling('msg-switch', {})
|
||||
})
|
||||
|
||||
expect(mockSseGet).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not call handleResume when target has no humanInputFormDataList', async () => {
|
||||
let sendCallbacks: any
|
||||
mockHandleRun.mockImplementation((_params: any, callbacks: any) => {
|
||||
sendCallbacks = callbacks
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useChat({}))
|
||||
|
||||
act(() => {
|
||||
result.current.handleSend({ query: 'test' }, {})
|
||||
})
|
||||
|
||||
act(() => {
|
||||
sendCallbacks.onWorkflowStarted({
|
||||
workflow_run_id: 'wfr-switch',
|
||||
task_id: 'task-1',
|
||||
conversation_id: null,
|
||||
message_id: 'msg-switch',
|
||||
})
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
await sendCallbacks.onCompleted(false)
|
||||
})
|
||||
|
||||
act(() => {
|
||||
result.current.handleSwitchSibling('msg-switch', {})
|
||||
})
|
||||
|
||||
expect(mockSseGet).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should return undefined from findMessageInTree when not found', () => {
|
||||
const { result } = renderHook(() => useChat({}))
|
||||
|
||||
act(() => {
|
||||
result.current.handleSwitchSibling('nonexistent-id', {})
|
||||
})
|
||||
|
||||
expect(mockSseGet).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should search children recursively in findMessageInTree', async () => {
|
||||
let sendCallbacks: any
|
||||
mockHandleRun.mockImplementation((_params: any, callbacks: any) => {
|
||||
sendCallbacks = callbacks
|
||||
})
|
||||
mockSseGet.mockImplementation(() => {})
|
||||
|
||||
const { result } = renderHook(() => useChat({}))
|
||||
|
||||
act(() => {
|
||||
result.current.handleSend({ query: 'parent' }, {})
|
||||
})
|
||||
|
||||
act(() => {
|
||||
sendCallbacks.onWorkflowStarted({
|
||||
workflow_run_id: 'wfr-1',
|
||||
task_id: 'task-1',
|
||||
conversation_id: null,
|
||||
message_id: 'msg-parent',
|
||||
})
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
await sendCallbacks.onCompleted(false)
|
||||
})
|
||||
|
||||
act(() => {
|
||||
result.current.handleSend({
|
||||
query: 'child',
|
||||
parent_message_id: 'msg-parent',
|
||||
}, {})
|
||||
})
|
||||
|
||||
act(() => {
|
||||
sendCallbacks.onWorkflowStarted({
|
||||
workflow_run_id: 'wfr-2',
|
||||
task_id: 'task-2',
|
||||
conversation_id: null,
|
||||
message_id: 'msg-child',
|
||||
})
|
||||
})
|
||||
|
||||
act(() => {
|
||||
sendCallbacks.onHumanInputRequired({
|
||||
data: { node_id: 'h-child', form_token: 'ft-c' },
|
||||
})
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
await sendCallbacks.onCompleted(false)
|
||||
})
|
||||
|
||||
act(() => {
|
||||
result.current.handleSwitchSibling('msg-child', {})
|
||||
})
|
||||
|
||||
expect(mockSseGet).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('useChat – handleSubmitHumanInputForm', () => {
|
||||
beforeEach(() => {
|
||||
resetMocksAndWorkflowState()
|
||||
mockSubmitHumanInputForm.mockResolvedValue({})
|
||||
})
|
||||
|
||||
it('should call submitHumanInputForm with token and data', async () => {
|
||||
const { result } = renderHook(() => useChat({}))
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleSubmitHumanInputForm('token-123', { field: 'value' })
|
||||
})
|
||||
|
||||
expect(mockSubmitHumanInputForm).toHaveBeenCalledWith('token-123', { field: 'value' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('useChat – getHumanInputNodeData', () => {
|
||||
beforeEach(() => {
|
||||
resetMocksAndWorkflowState()
|
||||
mockGetNodes.mockReturnValue([])
|
||||
})
|
||||
|
||||
it('should return the custom node matching the given nodeID', () => {
|
||||
const mockNode = { id: 'node-1', type: 'custom', data: { title: 'Human Input' } }
|
||||
mockGetNodes.mockReturnValue([
|
||||
mockNode,
|
||||
{ id: 'node-2', type: 'custom', data: { title: 'Other' } },
|
||||
])
|
||||
|
||||
const { result } = renderHook(() => useChat({}))
|
||||
const node = result.current.getHumanInputNodeData('node-1')
|
||||
expect(node).toEqual(mockNode)
|
||||
})
|
||||
|
||||
it('should return undefined when no matching node', () => {
|
||||
mockGetNodes.mockReturnValue([{ id: 'node-2', type: 'custom', data: {} }])
|
||||
|
||||
const { result } = renderHook(() => useChat({}))
|
||||
const node = result.current.getHumanInputNodeData('nonexistent')
|
||||
expect(node).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should filter out non-custom nodes', () => {
|
||||
mockGetNodes.mockReturnValue([
|
||||
{ id: 'node-1', type: 'default', data: {} },
|
||||
{ id: 'node-1', type: 'custom', data: { found: true } },
|
||||
])
|
||||
|
||||
const { result } = renderHook(() => useChat({}))
|
||||
const node = result.current.getHumanInputNodeData('node-1')
|
||||
expect(node).toEqual({ id: 'node-1', type: 'custom', data: { found: true } })
|
||||
})
|
||||
})
|
||||
|
||||
describe('useChat – conversationId and setTargetMessageId', () => {
|
||||
beforeEach(() => {
|
||||
resetMocksAndWorkflowState()
|
||||
})
|
||||
|
||||
it('should initially be an empty string', () => {
|
||||
const { result } = renderHook(() => useChat({}))
|
||||
expect(result.current.conversationId).toBe('')
|
||||
})
|
||||
|
||||
it('setTargetMessageId should change chatList thread path', () => {
|
||||
const prevChatTree: ChatItemInTree[] = [
|
||||
{
|
||||
id: 'q1',
|
||||
content: 'question 1',
|
||||
isAnswer: false,
|
||||
children: [
|
||||
{
|
||||
id: 'a1',
|
||||
content: 'answer 1',
|
||||
isAnswer: true,
|
||||
children: [
|
||||
{
|
||||
id: 'q2-branch-a',
|
||||
content: 'branch A question',
|
||||
isAnswer: false,
|
||||
children: [
|
||||
{ id: 'a2-branch-a', content: 'branch A answer', isAnswer: true, children: [] },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'q2-branch-b',
|
||||
content: 'branch B question',
|
||||
isAnswer: false,
|
||||
children: [
|
||||
{ id: 'a2-branch-b', content: 'branch B answer', isAnswer: true, children: [] },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const { result } = renderHook(() => useChat({}, undefined, prevChatTree))
|
||||
|
||||
const defaultList = result.current.chatList
|
||||
expect(defaultList.some(item => item.id === 'a1')).toBe(true)
|
||||
|
||||
act(() => {
|
||||
result.current.setTargetMessageId('a2-branch-a')
|
||||
})
|
||||
|
||||
const listA = result.current.chatList
|
||||
expect(listA.some(item => item.id === 'a2-branch-a')).toBe(true)
|
||||
expect(listA.some(item => item.id === 'a2-branch-b')).toBe(false)
|
||||
|
||||
act(() => {
|
||||
result.current.setTargetMessageId('a2-branch-b')
|
||||
})
|
||||
|
||||
const listB = result.current.chatList
|
||||
expect(listB.some(item => item.id === 'a2-branch-b')).toBe(true)
|
||||
expect(listB.some(item => item.id === 'a2-branch-a')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('useChat – updateCurrentQAOnTree with parent_message_id', () => {
|
||||
let capturedCallbacks: any
|
||||
|
||||
beforeEach(() => {
|
||||
resetMocksAndWorkflowState()
|
||||
mockHandleRun.mockReset()
|
||||
mockHandleRun.mockImplementation((_params: any, callbacks: any) => {
|
||||
capturedCallbacks = callbacks
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle follow-up message with parent_message_id', async () => {
|
||||
const { result } = renderHook(() => useChat({}))
|
||||
|
||||
act(() => {
|
||||
result.current.handleSend({ query: 'first' }, {})
|
||||
})
|
||||
|
||||
const firstCallbacks = capturedCallbacks
|
||||
|
||||
act(() => {
|
||||
firstCallbacks.onData('answer1', true, {
|
||||
conversationId: 'c1',
|
||||
messageId: 'msg-1',
|
||||
taskId: 't1',
|
||||
})
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
await firstCallbacks.onCompleted(false)
|
||||
})
|
||||
|
||||
act(() => {
|
||||
result.current.handleSend({
|
||||
query: 'follow up',
|
||||
parent_message_id: 'msg-1',
|
||||
}, {})
|
||||
})
|
||||
|
||||
expect(mockHandleRun).toHaveBeenCalledTimes(2)
|
||||
expect(result.current.chatList.length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
@@ -1,17 +1,30 @@
|
||||
/* eslint-disable ts/no-explicit-any */
|
||||
import type { ChatItemInTree } from '@/app/components/base/chat/types'
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { useChat } from '../hooks'
|
||||
import { useChat } from '../../hooks'
|
||||
|
||||
const mockHandleRun = vi.fn()
|
||||
const mockNotify = vi.fn()
|
||||
const mockFetchInspectVars = vi.fn()
|
||||
const mockInvalidAllLastRun = vi.fn()
|
||||
const mockSetIterTimes = vi.fn()
|
||||
const mockSetLoopTimes = vi.fn()
|
||||
const mockSubmitHumanInputForm = vi.fn()
|
||||
const mockSseGet = vi.fn()
|
||||
const mockGetNodes = vi.fn((): any[] => [])
|
||||
|
||||
let mockWorkflowRunningData: any = null
|
||||
|
||||
vi.mock('@/service/base', () => ({
|
||||
sseGet: vi.fn(),
|
||||
sseGet: (...args: any[]) => mockSseGet(...args),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-workflow', () => ({
|
||||
useInvalidAllLastRun: () => vi.fn(),
|
||||
useInvalidAllLastRun: () => mockInvalidAllLastRun,
|
||||
}))
|
||||
|
||||
vi.mock('@/service/workflow', () => ({
|
||||
submitHumanInputForm: vi.fn(),
|
||||
submitHumanInputForm: (...args: any[]) => mockSubmitHumanInputForm(...args),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/ui/toast', () => ({
|
||||
@@ -19,37 +32,47 @@ vi.mock('@/app/components/base/ui/toast', () => ({
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warning: vi.fn(),
|
||||
info: vi.fn(),
|
||||
info: (...args: any[]) => mockNotify(...args),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('reactflow', () => ({
|
||||
useStoreApi: () => ({ getState: () => ({}) }),
|
||||
useStoreApi: () => ({
|
||||
getState: () => ({
|
||||
getNodes: mockGetNodes,
|
||||
}),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../../../hooks', () => ({
|
||||
useWorkflowRun: () => ({ handleRun: vi.fn() }),
|
||||
useSetWorkflowVarsWithValue: () => ({ fetchInspectVars: vi.fn() }),
|
||||
vi.mock('../../../../hooks', () => ({
|
||||
useWorkflowRun: () => ({ handleRun: mockHandleRun }),
|
||||
useSetWorkflowVarsWithValue: () => ({ fetchInspectVars: mockFetchInspectVars }),
|
||||
}))
|
||||
|
||||
vi.mock('../../../hooks-store', () => ({
|
||||
vi.mock('../../../../hooks-store', () => ({
|
||||
useHooksStore: () => null,
|
||||
}))
|
||||
|
||||
vi.mock('../../../store', () => ({
|
||||
vi.mock('../../../../store', () => ({
|
||||
useWorkflowStore: () => ({
|
||||
getState: () => ({
|
||||
setIterTimes: vi.fn(),
|
||||
setLoopTimes: vi.fn(),
|
||||
setIterTimes: mockSetIterTimes,
|
||||
setLoopTimes: mockSetLoopTimes,
|
||||
inputs: {},
|
||||
workflowRunningData: mockWorkflowRunningData,
|
||||
}),
|
||||
}),
|
||||
useStore: () => vi.fn(),
|
||||
}))
|
||||
|
||||
const resetMocksAndWorkflowState = () => {
|
||||
vi.clearAllMocks()
|
||||
mockWorkflowRunningData = null
|
||||
}
|
||||
|
||||
describe('workflow debug useChat – opening statement stability', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
resetMocksAndWorkflowState()
|
||||
})
|
||||
|
||||
it('should return empty chatList when config has no opening_statement', () => {
|
||||
@@ -64,7 +87,6 @@ describe('workflow debug useChat – opening statement stability', () => {
|
||||
|
||||
it('should use stable id "opening-statement" instead of Date.now()', () => {
|
||||
const config = { opening_statement: 'Welcome!' }
|
||||
|
||||
const { result } = renderHook(() => useChat(config))
|
||||
expect(result.current.chatList[0].id).toBe('opening-statement')
|
||||
})
|
||||
@@ -137,4 +159,21 @@ describe('workflow debug useChat – opening statement stability', () => {
|
||||
const openerAfter = result.current.chatList[0]
|
||||
expect(openerAfter).toBe(openerBefore)
|
||||
})
|
||||
|
||||
it('should include suggestedQuestions in opening statement when config has them', () => {
|
||||
const config = {
|
||||
opening_statement: 'Welcome!',
|
||||
suggested_questions: ['How are you?', 'What can you do?'],
|
||||
}
|
||||
const { result } = renderHook(() => useChat(config))
|
||||
const opener = result.current.chatList[0]
|
||||
expect(opener.suggestedQuestions).toEqual(['How are you?', 'What can you do?'])
|
||||
})
|
||||
|
||||
it('should not include suggestedQuestions when config has none', () => {
|
||||
const config = { opening_statement: 'Welcome!' }
|
||||
const { result } = renderHook(() => useChat(config))
|
||||
const opener = result.current.chatList[0]
|
||||
expect(opener.suggestedQuestions).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,919 @@
|
||||
/* eslint-disable ts/no-explicit-any */
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import { useChat } from '../../hooks'
|
||||
|
||||
const mockHandleRun = vi.fn()
|
||||
const mockNotify = vi.fn()
|
||||
const mockFetchInspectVars = vi.fn()
|
||||
const mockInvalidAllLastRun = vi.fn()
|
||||
const mockSetIterTimes = vi.fn()
|
||||
const mockSetLoopTimes = vi.fn()
|
||||
const mockSubmitHumanInputForm = vi.fn()
|
||||
const mockSseGet = vi.fn()
|
||||
const mockGetNodes = vi.fn((): any[] => [])
|
||||
|
||||
let mockWorkflowRunningData: any = null
|
||||
|
||||
vi.mock('@/service/base', () => ({
|
||||
sseGet: (...args: any[]) => mockSseGet(...args),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-workflow', () => ({
|
||||
useInvalidAllLastRun: () => mockInvalidAllLastRun,
|
||||
}))
|
||||
|
||||
vi.mock('@/service/workflow', () => ({
|
||||
submitHumanInputForm: (...args: any[]) => mockSubmitHumanInputForm(...args),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/ui/toast', () => ({
|
||||
toast: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warning: vi.fn(),
|
||||
info: (...args: any[]) => mockNotify(...args),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('reactflow', () => ({
|
||||
useStoreApi: () => ({
|
||||
getState: () => ({
|
||||
getNodes: mockGetNodes,
|
||||
}),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../../../../hooks', () => ({
|
||||
useWorkflowRun: () => ({ handleRun: mockHandleRun }),
|
||||
useSetWorkflowVarsWithValue: () => ({ fetchInspectVars: mockFetchInspectVars }),
|
||||
}))
|
||||
|
||||
vi.mock('../../../../hooks-store', () => ({
|
||||
useHooksStore: () => null,
|
||||
}))
|
||||
|
||||
vi.mock('../../../../store', () => ({
|
||||
useWorkflowStore: () => ({
|
||||
getState: () => ({
|
||||
setIterTimes: mockSetIterTimes,
|
||||
setLoopTimes: mockSetLoopTimes,
|
||||
inputs: {},
|
||||
workflowRunningData: mockWorkflowRunningData,
|
||||
}),
|
||||
}),
|
||||
useStore: () => vi.fn(),
|
||||
}))
|
||||
|
||||
const resetMocksAndWorkflowState = () => {
|
||||
vi.clearAllMocks()
|
||||
mockWorkflowRunningData = null
|
||||
}
|
||||
|
||||
describe('useChat – handleSend SSE callbacks', () => {
|
||||
let capturedCallbacks: any
|
||||
|
||||
beforeEach(() => {
|
||||
resetMocksAndWorkflowState()
|
||||
mockHandleRun.mockReset()
|
||||
mockHandleRun.mockImplementation((_params: any, callbacks: any) => {
|
||||
capturedCallbacks = callbacks
|
||||
})
|
||||
})
|
||||
|
||||
function setupAndSend(config: any = {}) {
|
||||
const hook = renderHook(() => useChat(config))
|
||||
act(() => {
|
||||
hook.result.current.handleSend({ query: 'test' }, {
|
||||
onGetSuggestedQuestions: vi.fn().mockResolvedValue({ data: ['q1'] }),
|
||||
})
|
||||
})
|
||||
return hook
|
||||
}
|
||||
|
||||
function startWorkflow(overrides: Record<string, any> = {}) {
|
||||
act(() => {
|
||||
capturedCallbacks.onWorkflowStarted({
|
||||
workflow_run_id: 'wfr-1',
|
||||
task_id: 'task-1',
|
||||
conversation_id: null,
|
||||
message_id: null,
|
||||
...overrides,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function startNode(nodeId: string, traceId: string, extra: Record<string, any> = {}) {
|
||||
act(() => {
|
||||
capturedCallbacks.onNodeStarted({
|
||||
data: { node_id: nodeId, id: traceId, ...extra },
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
describe('onData', () => {
|
||||
it('should append message content', () => {
|
||||
const { result } = setupAndSend()
|
||||
|
||||
act(() => {
|
||||
capturedCallbacks.onData('Hello', true, {
|
||||
conversationId: 'conv-1',
|
||||
messageId: 'msg-1',
|
||||
taskId: 'task-1',
|
||||
})
|
||||
})
|
||||
|
||||
const answer = result.current.chatList.find(item => item.isAnswer && !item.isOpeningStatement)
|
||||
expect(answer!.content).toContain('Hello')
|
||||
})
|
||||
|
||||
it('should set response id from messageId on first call', () => {
|
||||
const { result } = setupAndSend()
|
||||
|
||||
act(() => {
|
||||
capturedCallbacks.onData('Hi', true, {
|
||||
conversationId: 'conv-1',
|
||||
messageId: 'msg-123',
|
||||
taskId: 'task-1',
|
||||
})
|
||||
})
|
||||
|
||||
const answer = result.current.chatList.find(item => item.id === 'msg-123')
|
||||
expect(answer).toBeDefined()
|
||||
})
|
||||
|
||||
it('should set conversationId on first message with newConversationId', () => {
|
||||
const { result } = setupAndSend()
|
||||
|
||||
act(() => {
|
||||
capturedCallbacks.onData('Hi', true, {
|
||||
conversationId: 'new-conv-id',
|
||||
messageId: 'msg-1',
|
||||
taskId: 'task-1',
|
||||
})
|
||||
})
|
||||
|
||||
expect(result.current.conversationId).toBe('new-conv-id')
|
||||
})
|
||||
|
||||
it('should not set conversationId when isFirstMessage is false', () => {
|
||||
const { result } = setupAndSend()
|
||||
|
||||
act(() => {
|
||||
capturedCallbacks.onData('Hi', false, {
|
||||
conversationId: 'conv-should-not-set',
|
||||
messageId: 'msg-1',
|
||||
taskId: 'task-1',
|
||||
})
|
||||
})
|
||||
|
||||
expect(result.current.conversationId).toBe('')
|
||||
})
|
||||
|
||||
it('should not update hasSetResponseId when messageId is empty', () => {
|
||||
const { result } = setupAndSend()
|
||||
|
||||
act(() => {
|
||||
capturedCallbacks.onData('msg1', true, {
|
||||
conversationId: '',
|
||||
messageId: '',
|
||||
taskId: 'task-1',
|
||||
})
|
||||
})
|
||||
|
||||
act(() => {
|
||||
capturedCallbacks.onData('msg2', false, {
|
||||
conversationId: '',
|
||||
messageId: 'late-id',
|
||||
taskId: 'task-1',
|
||||
})
|
||||
})
|
||||
|
||||
const answer = result.current.chatList.find(item => item.id === 'late-id')
|
||||
expect(answer).toBeDefined()
|
||||
})
|
||||
|
||||
it('should only set hasSetResponseId once', () => {
|
||||
const { result } = setupAndSend()
|
||||
|
||||
act(() => {
|
||||
capturedCallbacks.onData('msg1', true, {
|
||||
conversationId: 'c1',
|
||||
messageId: 'msg-first',
|
||||
taskId: 'task-1',
|
||||
})
|
||||
})
|
||||
|
||||
act(() => {
|
||||
capturedCallbacks.onData('msg2', false, {
|
||||
conversationId: 'c1',
|
||||
messageId: 'msg-second',
|
||||
taskId: 'task-1',
|
||||
})
|
||||
})
|
||||
|
||||
const question = result.current.chatList.find(item => !item.isAnswer)
|
||||
expect(question!.id).toBe('question-msg-first')
|
||||
})
|
||||
})
|
||||
|
||||
describe('onCompleted', () => {
|
||||
it('should set isResponding to false', async () => {
|
||||
const { result } = setupAndSend()
|
||||
await act(async () => {
|
||||
await capturedCallbacks.onCompleted(false)
|
||||
})
|
||||
expect(result.current.isResponding).toBe(false)
|
||||
})
|
||||
|
||||
it('should call fetchInspectVars and invalidAllLastRun when not paused', async () => {
|
||||
setupAndSend()
|
||||
await act(async () => {
|
||||
await capturedCallbacks.onCompleted(false)
|
||||
})
|
||||
expect(mockFetchInspectVars).toHaveBeenCalledWith({})
|
||||
expect(mockInvalidAllLastRun).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not call fetchInspectVars when workflow is paused', async () => {
|
||||
mockWorkflowRunningData = { result: { status: 'paused' } }
|
||||
setupAndSend()
|
||||
await act(async () => {
|
||||
await capturedCallbacks.onCompleted(false)
|
||||
})
|
||||
expect(mockFetchInspectVars).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should set error content on response item when hasError with errorMessage', async () => {
|
||||
const { result } = setupAndSend()
|
||||
|
||||
act(() => {
|
||||
capturedCallbacks.onData('partial', true, {
|
||||
conversationId: 'c1',
|
||||
messageId: 'msg-err',
|
||||
taskId: 't1',
|
||||
})
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
await capturedCallbacks.onCompleted(true, 'Something went wrong')
|
||||
})
|
||||
|
||||
const answer = result.current.chatList.find(item => item.id === 'msg-err')
|
||||
expect(answer!.content).toBe('Something went wrong')
|
||||
expect(answer!.isError).toBe(true)
|
||||
})
|
||||
|
||||
it('should not set error content when hasError is true but errorMessage is empty', async () => {
|
||||
const { result } = setupAndSend()
|
||||
await act(async () => {
|
||||
await capturedCallbacks.onCompleted(true)
|
||||
})
|
||||
expect(result.current.isResponding).toBe(false)
|
||||
})
|
||||
|
||||
it('should fetch suggested questions when enabled and invoke abort controller callback', async () => {
|
||||
const mockGetSuggested = vi.fn().mockImplementation((_id: string, getAbortCtrl: any) => {
|
||||
getAbortCtrl(new AbortController())
|
||||
return Promise.resolve({ data: ['suggestion1'] })
|
||||
})
|
||||
const hook = renderHook(() =>
|
||||
useChat({ suggested_questions_after_answer: { enabled: true } }),
|
||||
)
|
||||
|
||||
mockHandleRun.mockImplementation((_params: any, callbacks: any) => {
|
||||
capturedCallbacks = callbacks
|
||||
})
|
||||
|
||||
act(() => {
|
||||
hook.result.current.handleSend({ query: 'test' }, {
|
||||
onGetSuggestedQuestions: mockGetSuggested,
|
||||
})
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
await capturedCallbacks.onCompleted(false)
|
||||
})
|
||||
|
||||
expect(mockGetSuggested).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should set suggestedQuestions to empty array when fetch fails', async () => {
|
||||
const mockGetSuggested = vi.fn().mockRejectedValue(new Error('fail'))
|
||||
const hook = renderHook(() =>
|
||||
useChat({ suggested_questions_after_answer: { enabled: true } }),
|
||||
)
|
||||
|
||||
mockHandleRun.mockImplementation((_params: any, callbacks: any) => {
|
||||
capturedCallbacks = callbacks
|
||||
})
|
||||
|
||||
act(() => {
|
||||
hook.result.current.handleSend({ query: 'test' }, {
|
||||
onGetSuggestedQuestions: mockGetSuggested,
|
||||
})
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
await capturedCallbacks.onCompleted(false)
|
||||
})
|
||||
|
||||
expect(hook.result.current.suggestedQuestions).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('onError', () => {
|
||||
it('should set isResponding to false', () => {
|
||||
const { result } = setupAndSend()
|
||||
act(() => {
|
||||
capturedCallbacks.onError()
|
||||
})
|
||||
expect(result.current.isResponding).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('onMessageEnd', () => {
|
||||
it('should update citation and files', () => {
|
||||
const { result } = setupAndSend()
|
||||
|
||||
act(() => {
|
||||
capturedCallbacks.onData('response', true, {
|
||||
conversationId: 'c1',
|
||||
messageId: 'msg-1',
|
||||
taskId: 't1',
|
||||
})
|
||||
})
|
||||
|
||||
act(() => {
|
||||
capturedCallbacks.onMessageEnd({
|
||||
metadata: { retriever_resources: [{ id: 'r1' }] },
|
||||
files: [],
|
||||
})
|
||||
})
|
||||
|
||||
const answer = result.current.chatList.find(item => item.id === 'msg-1')
|
||||
expect(answer!.citation).toEqual([{ id: 'r1' }])
|
||||
})
|
||||
|
||||
it('should default citation to empty array when no retriever_resources', () => {
|
||||
const { result } = setupAndSend()
|
||||
|
||||
act(() => {
|
||||
capturedCallbacks.onData('response', true, {
|
||||
conversationId: 'c1',
|
||||
messageId: 'msg-1',
|
||||
taskId: 't1',
|
||||
})
|
||||
})
|
||||
|
||||
act(() => {
|
||||
capturedCallbacks.onMessageEnd({ metadata: {}, files: [] })
|
||||
})
|
||||
|
||||
const answer = result.current.chatList.find(item => item.id === 'msg-1')
|
||||
expect(answer!.citation).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('onMessageReplace', () => {
|
||||
it('should replace answer content on responseItem', () => {
|
||||
const { result } = setupAndSend()
|
||||
|
||||
act(() => {
|
||||
capturedCallbacks.onData('old', true, {
|
||||
conversationId: 'c1',
|
||||
messageId: 'msg-1',
|
||||
taskId: 't1',
|
||||
})
|
||||
})
|
||||
|
||||
act(() => {
|
||||
capturedCallbacks.onMessageReplace({ answer: 'replaced' })
|
||||
})
|
||||
|
||||
act(() => {
|
||||
capturedCallbacks.onMessageEnd({ metadata: {}, files: [] })
|
||||
})
|
||||
|
||||
const answer = result.current.chatList.find(item => item.id === 'msg-1')
|
||||
expect(answer!.content).toBe('replaced')
|
||||
})
|
||||
})
|
||||
|
||||
describe('onWorkflowStarted', () => {
|
||||
it('should create workflow process with Running status', () => {
|
||||
const { result } = setupAndSend()
|
||||
|
||||
act(() => {
|
||||
capturedCallbacks.onWorkflowStarted({
|
||||
workflow_run_id: 'wfr-1',
|
||||
task_id: 'task-1',
|
||||
conversation_id: 'conv-1',
|
||||
message_id: 'msg-1',
|
||||
})
|
||||
})
|
||||
|
||||
const answer = result.current.chatList.find(item => item.isAnswer && !item.isOpeningStatement)
|
||||
expect(answer!.workflowProcess!.status).toBe('running')
|
||||
expect(answer!.workflowProcess!.tracing).toEqual([])
|
||||
})
|
||||
|
||||
it('should set conversationId when provided', () => {
|
||||
const { result } = setupAndSend()
|
||||
|
||||
act(() => {
|
||||
capturedCallbacks.onWorkflowStarted({
|
||||
workflow_run_id: 'wfr-1',
|
||||
task_id: 'task-1',
|
||||
conversation_id: 'from-workflow',
|
||||
message_id: null,
|
||||
})
|
||||
})
|
||||
|
||||
expect(result.current.conversationId).toBe('from-workflow')
|
||||
})
|
||||
|
||||
it('should not override existing conversationId when conversation_id is null', () => {
|
||||
const { result } = setupAndSend()
|
||||
startWorkflow()
|
||||
expect(result.current.conversationId).toBe('')
|
||||
})
|
||||
|
||||
it('should resume existing workflow process when tracing exists', () => {
|
||||
const { result } = setupAndSend()
|
||||
startWorkflow()
|
||||
startNode('n1', 'trace-1')
|
||||
startWorkflow({ workflow_run_id: 'wfr-2', task_id: 'task-2' })
|
||||
|
||||
const answer = result.current.chatList.find(item => item.isAnswer && !item.isOpeningStatement)
|
||||
expect(answer!.workflowProcess!.status).toBe('running')
|
||||
expect(answer!.workflowProcess!.tracing.length).toBe(1)
|
||||
})
|
||||
|
||||
it('should replace placeholder answer id with real message_id from server', () => {
|
||||
const { result } = setupAndSend()
|
||||
|
||||
act(() => {
|
||||
capturedCallbacks.onWorkflowStarted({
|
||||
workflow_run_id: 'wfr-1',
|
||||
task_id: 'task-1',
|
||||
conversation_id: null,
|
||||
message_id: 'wf-msg-id',
|
||||
})
|
||||
})
|
||||
|
||||
const answer = result.current.chatList.find(item => item.id === 'wf-msg-id')
|
||||
expect(answer).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('onWorkflowFinished', () => {
|
||||
it('should update workflow process status', () => {
|
||||
const { result } = setupAndSend()
|
||||
startWorkflow()
|
||||
|
||||
act(() => {
|
||||
capturedCallbacks.onWorkflowFinished({ data: { status: 'succeeded' } })
|
||||
})
|
||||
|
||||
const answer = result.current.chatList.find(item => item.isAnswer && !item.isOpeningStatement)
|
||||
expect(answer!.workflowProcess!.status).toBe('succeeded')
|
||||
})
|
||||
})
|
||||
|
||||
describe('onIterationStart / onIterationFinish', () => {
|
||||
it('should push tracing entry on start', () => {
|
||||
const { result } = setupAndSend()
|
||||
startWorkflow()
|
||||
|
||||
act(() => {
|
||||
capturedCallbacks.onIterationStart({
|
||||
data: { id: 'iter-1', node_id: 'n-iter' },
|
||||
})
|
||||
})
|
||||
|
||||
const answer = result.current.chatList.find(item => item.isAnswer && !item.isOpeningStatement)
|
||||
expect(answer!.workflowProcess!.tracing).toHaveLength(1)
|
||||
const trace = answer!.workflowProcess!.tracing[0]
|
||||
expect(trace.id).toBe('iter-1')
|
||||
expect(trace.node_id).toBe('n-iter')
|
||||
expect(trace.status).toBe('running')
|
||||
})
|
||||
|
||||
it('should update matching tracing on finish', () => {
|
||||
const { result } = setupAndSend()
|
||||
startWorkflow()
|
||||
|
||||
act(() => {
|
||||
capturedCallbacks.onIterationStart({
|
||||
data: { id: 'iter-1', node_id: 'n-iter' },
|
||||
})
|
||||
})
|
||||
|
||||
act(() => {
|
||||
capturedCallbacks.onIterationFinish({
|
||||
data: { id: 'iter-1', node_id: 'n-iter', output: 'done' },
|
||||
})
|
||||
})
|
||||
|
||||
const answer = result.current.chatList.find(item => item.isAnswer && !item.isOpeningStatement)
|
||||
const trace = answer!.workflowProcess!.tracing.find((t: any) => t.id === 'iter-1')
|
||||
expect(trace).toBeDefined()
|
||||
expect(trace!.node_id).toBe('n-iter')
|
||||
expect((trace as any).output).toBe('done')
|
||||
})
|
||||
|
||||
it('should not update tracing on finish when id does not match', () => {
|
||||
const { result } = setupAndSend()
|
||||
startWorkflow()
|
||||
|
||||
act(() => {
|
||||
capturedCallbacks.onIterationStart({
|
||||
data: { id: 'iter-1', node_id: 'n-iter' },
|
||||
})
|
||||
})
|
||||
|
||||
act(() => {
|
||||
capturedCallbacks.onIterationFinish({
|
||||
data: { id: 'iter-nonexistent', node_id: 'n-other' },
|
||||
})
|
||||
})
|
||||
|
||||
const answer = result.current.chatList.find(item => item.isAnswer && !item.isOpeningStatement)
|
||||
expect(answer!.workflowProcess!.tracing).toHaveLength(1)
|
||||
expect((answer!.workflowProcess!.tracing[0] as any).output).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('onLoopStart / onLoopFinish', () => {
|
||||
it('should push tracing entry on start', () => {
|
||||
const { result } = setupAndSend()
|
||||
startWorkflow()
|
||||
|
||||
act(() => {
|
||||
capturedCallbacks.onLoopStart({
|
||||
data: { id: 'loop-1', node_id: 'n-loop' },
|
||||
})
|
||||
})
|
||||
|
||||
const answer = result.current.chatList.find(item => item.isAnswer && !item.isOpeningStatement)
|
||||
expect(answer!.workflowProcess!.tracing).toHaveLength(1)
|
||||
const trace = answer!.workflowProcess!.tracing[0]
|
||||
expect(trace.id).toBe('loop-1')
|
||||
expect(trace.node_id).toBe('n-loop')
|
||||
expect(trace.status).toBe('running')
|
||||
})
|
||||
|
||||
it('should update matching tracing on finish', () => {
|
||||
const { result } = setupAndSend()
|
||||
startWorkflow()
|
||||
|
||||
act(() => {
|
||||
capturedCallbacks.onLoopStart({
|
||||
data: { id: 'loop-1', node_id: 'n-loop' },
|
||||
})
|
||||
})
|
||||
|
||||
act(() => {
|
||||
capturedCallbacks.onLoopFinish({
|
||||
data: { id: 'loop-1', node_id: 'n-loop', output: 'done' },
|
||||
})
|
||||
})
|
||||
|
||||
const answer = result.current.chatList.find(item => item.isAnswer && !item.isOpeningStatement)
|
||||
expect(answer!.workflowProcess!.tracing).toHaveLength(1)
|
||||
const trace = answer!.workflowProcess!.tracing[0]
|
||||
expect(trace.id).toBe('loop-1')
|
||||
expect(trace.node_id).toBe('n-loop')
|
||||
expect((trace as any).output).toBe('done')
|
||||
})
|
||||
|
||||
it('should not update tracing on finish when id does not match', () => {
|
||||
const { result } = setupAndSend()
|
||||
startWorkflow()
|
||||
|
||||
act(() => {
|
||||
capturedCallbacks.onLoopStart({
|
||||
data: { id: 'loop-1', node_id: 'n-loop' },
|
||||
})
|
||||
})
|
||||
|
||||
act(() => {
|
||||
capturedCallbacks.onLoopFinish({
|
||||
data: { id: 'loop-nonexistent', node_id: 'n-other' },
|
||||
})
|
||||
})
|
||||
|
||||
const answer = result.current.chatList.find(item => item.isAnswer && !item.isOpeningStatement)
|
||||
expect(answer!.workflowProcess!.tracing).toHaveLength(1)
|
||||
expect((answer!.workflowProcess!.tracing[0] as any).output).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('onNodeStarted / onNodeRetry / onNodeFinished', () => {
|
||||
it('should add new tracing entry', () => {
|
||||
const { result } = setupAndSend()
|
||||
startWorkflow()
|
||||
startNode('node-1', 'trace-1')
|
||||
|
||||
const answer = result.current.chatList.find(item => item.isAnswer && !item.isOpeningStatement)
|
||||
expect(answer!.workflowProcess!.tracing).toHaveLength(1)
|
||||
const trace = answer!.workflowProcess!.tracing[0]
|
||||
expect(trace.id).toBe('trace-1')
|
||||
expect(trace.node_id).toBe('node-1')
|
||||
expect(trace.status).toBe('running')
|
||||
})
|
||||
|
||||
it('should update existing tracing entry with same node_id', () => {
|
||||
const { result } = setupAndSend()
|
||||
startWorkflow()
|
||||
startNode('node-1', 'trace-1')
|
||||
startNode('node-1', 'trace-1-v2')
|
||||
|
||||
const answer = result.current.chatList.find(item => item.isAnswer && !item.isOpeningStatement)
|
||||
expect(answer!.workflowProcess!.tracing).toHaveLength(1)
|
||||
const trace = answer!.workflowProcess!.tracing[0]
|
||||
expect(trace.id).toBe('trace-1-v2')
|
||||
expect(trace.node_id).toBe('node-1')
|
||||
expect(trace.status).toBe('running')
|
||||
})
|
||||
|
||||
it('should push retry data to tracing', () => {
|
||||
const { result } = setupAndSend()
|
||||
startWorkflow()
|
||||
|
||||
act(() => {
|
||||
capturedCallbacks.onNodeRetry({
|
||||
data: { node_id: 'node-1', id: 'retry-1', retry_index: 1 },
|
||||
})
|
||||
})
|
||||
|
||||
const answer = result.current.chatList.find(item => item.isAnswer && !item.isOpeningStatement)
|
||||
expect(answer!.workflowProcess!.tracing).toHaveLength(1)
|
||||
const trace = answer!.workflowProcess!.tracing[0]
|
||||
expect(trace.id).toBe('retry-1')
|
||||
expect(trace.node_id).toBe('node-1')
|
||||
expect((trace as any).retry_index).toBe(1)
|
||||
})
|
||||
|
||||
it('should update tracing entry on finish by id', () => {
|
||||
const { result } = setupAndSend()
|
||||
startWorkflow()
|
||||
startNode('node-1', 'trace-1')
|
||||
|
||||
act(() => {
|
||||
capturedCallbacks.onNodeFinished({
|
||||
data: { node_id: 'node-1', id: 'trace-1', status: 'succeeded', outputs: { text: 'done' } },
|
||||
})
|
||||
})
|
||||
|
||||
const answer = result.current.chatList.find(item => item.isAnswer && !item.isOpeningStatement)
|
||||
expect(answer!.workflowProcess!.tracing).toHaveLength(1)
|
||||
const trace = answer!.workflowProcess!.tracing[0]
|
||||
expect(trace.id).toBe('trace-1')
|
||||
expect(trace.status).toBe('succeeded')
|
||||
expect((trace as any).outputs).toEqual({ text: 'done' })
|
||||
})
|
||||
|
||||
it('should not update tracing on finish when id does not match', () => {
|
||||
const { result } = setupAndSend()
|
||||
startWorkflow()
|
||||
startNode('node-1', 'trace-1')
|
||||
|
||||
act(() => {
|
||||
capturedCallbacks.onNodeFinished({
|
||||
data: { node_id: 'node-x', id: 'trace-x', status: 'succeeded' },
|
||||
})
|
||||
})
|
||||
|
||||
const answer = result.current.chatList.find(item => item.isAnswer && !item.isOpeningStatement)
|
||||
expect(answer!.workflowProcess!.tracing).toHaveLength(1)
|
||||
const trace = answer!.workflowProcess!.tracing[0]
|
||||
expect(trace.id).toBe('trace-1')
|
||||
expect(trace.status).toBe('running')
|
||||
})
|
||||
})
|
||||
|
||||
describe('onAgentLog', () => {
|
||||
function setupWithNode() {
|
||||
const hook = setupAndSend()
|
||||
startWorkflow()
|
||||
return hook
|
||||
}
|
||||
|
||||
it('should create execution_metadata.agent_log when no execution_metadata exists', () => {
|
||||
const { result } = setupWithNode()
|
||||
startNode('agent-node', 'trace-agent')
|
||||
|
||||
act(() => {
|
||||
capturedCallbacks.onAgentLog({
|
||||
data: { node_id: 'agent-node', message_id: 'log-1', content: 'init' },
|
||||
})
|
||||
})
|
||||
|
||||
const answer = result.current.chatList.find(item => item.isAnswer && !item.isOpeningStatement)
|
||||
const agentTrace = answer!.workflowProcess!.tracing.find((t: any) => t.node_id === 'agent-node')
|
||||
expect(agentTrace!.execution_metadata!.agent_log).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('should create agent_log array when execution_metadata exists but no agent_log', () => {
|
||||
const { result } = setupWithNode()
|
||||
startNode('agent-node', 'trace-agent', { execution_metadata: { parallel_id: 'p1' } })
|
||||
|
||||
act(() => {
|
||||
capturedCallbacks.onAgentLog({
|
||||
data: { node_id: 'agent-node', message_id: 'log-1', content: 'step1' },
|
||||
})
|
||||
})
|
||||
|
||||
const answer = result.current.chatList.find(item => item.isAnswer && !item.isOpeningStatement)
|
||||
const agentTrace = answer!.workflowProcess!.tracing.find((t: any) => t.node_id === 'agent-node')
|
||||
expect(agentTrace!.execution_metadata!.agent_log).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('should update existing agent_log entry by message_id', () => {
|
||||
const { result } = setupWithNode()
|
||||
startNode('agent-node', 'trace-agent', {
|
||||
execution_metadata: { agent_log: [{ message_id: 'log-1', content: 'v1' }] },
|
||||
})
|
||||
|
||||
act(() => {
|
||||
capturedCallbacks.onAgentLog({
|
||||
data: { node_id: 'agent-node', message_id: 'log-1', content: 'v2' },
|
||||
})
|
||||
})
|
||||
|
||||
const answer = result.current.chatList.find(item => item.isAnswer && !item.isOpeningStatement)
|
||||
const agentTrace = answer!.workflowProcess!.tracing.find((t: any) => t.node_id === 'agent-node')
|
||||
expect(agentTrace!.execution_metadata!.agent_log).toHaveLength(1)
|
||||
expect((agentTrace!.execution_metadata!.agent_log as any[])[0].content).toBe('v2')
|
||||
})
|
||||
|
||||
it('should push new agent_log entry when message_id does not match', () => {
|
||||
const { result } = setupWithNode()
|
||||
startNode('agent-node', 'trace-agent', {
|
||||
execution_metadata: { agent_log: [{ message_id: 'log-1', content: 'v1' }] },
|
||||
})
|
||||
|
||||
act(() => {
|
||||
capturedCallbacks.onAgentLog({
|
||||
data: { node_id: 'agent-node', message_id: 'log-2', content: 'new' },
|
||||
})
|
||||
})
|
||||
|
||||
const answer = result.current.chatList.find(item => item.isAnswer && !item.isOpeningStatement)
|
||||
const agentTrace = answer!.workflowProcess!.tracing.find((t: any) => t.node_id === 'agent-node')
|
||||
expect(agentTrace!.execution_metadata!.agent_log).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('should not crash when node_id is not found in tracing', () => {
|
||||
setupWithNode()
|
||||
|
||||
act(() => {
|
||||
capturedCallbacks.onAgentLog({
|
||||
data: { node_id: 'nonexistent-node', message_id: 'log-1', content: 'noop' },
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('onHumanInputRequired', () => {
|
||||
it('should add form data to humanInputFormDataList', () => {
|
||||
const { result } = setupAndSend()
|
||||
startWorkflow()
|
||||
startNode('human-node', 'trace-human')
|
||||
|
||||
act(() => {
|
||||
capturedCallbacks.onHumanInputRequired({
|
||||
data: { node_id: 'human-node', form_token: 'token-1' },
|
||||
})
|
||||
})
|
||||
|
||||
const answer = result.current.chatList.find(item => item.isAnswer && !item.isOpeningStatement)
|
||||
expect(answer!.humanInputFormDataList).toHaveLength(1)
|
||||
expect(answer!.humanInputFormDataList![0].node_id).toBe('human-node')
|
||||
expect((answer!.humanInputFormDataList![0] as any).form_token).toBe('token-1')
|
||||
})
|
||||
|
||||
it('should update existing form for same node_id', () => {
|
||||
const { result } = setupAndSend()
|
||||
startWorkflow()
|
||||
startNode('human-node', 'trace-human')
|
||||
|
||||
act(() => {
|
||||
capturedCallbacks.onHumanInputRequired({
|
||||
data: { node_id: 'human-node', form_token: 'token-1' },
|
||||
})
|
||||
})
|
||||
|
||||
act(() => {
|
||||
capturedCallbacks.onHumanInputRequired({
|
||||
data: { node_id: 'human-node', form_token: 'token-2' },
|
||||
})
|
||||
})
|
||||
|
||||
const answer = result.current.chatList.find(item => item.isAnswer && !item.isOpeningStatement)
|
||||
expect(answer!.humanInputFormDataList).toHaveLength(1)
|
||||
expect((answer!.humanInputFormDataList![0] as any).form_token).toBe('token-2')
|
||||
})
|
||||
|
||||
it('should push new form data for different node_id', () => {
|
||||
const { result } = setupAndSend()
|
||||
startWorkflow()
|
||||
|
||||
act(() => {
|
||||
capturedCallbacks.onHumanInputRequired({
|
||||
data: { node_id: 'human-node-1', form_token: 'token-1' },
|
||||
})
|
||||
})
|
||||
|
||||
act(() => {
|
||||
capturedCallbacks.onHumanInputRequired({
|
||||
data: { node_id: 'human-node-2', form_token: 'token-2' },
|
||||
})
|
||||
})
|
||||
|
||||
const answer = result.current.chatList.find(item => item.isAnswer && !item.isOpeningStatement)
|
||||
expect(answer!.humanInputFormDataList).toHaveLength(2)
|
||||
expect(answer!.humanInputFormDataList![0].node_id).toBe('human-node-1')
|
||||
expect(answer!.humanInputFormDataList![1].node_id).toBe('human-node-2')
|
||||
})
|
||||
|
||||
it('should set tracing node status to Paused when tracing index found', () => {
|
||||
const { result } = setupAndSend()
|
||||
startWorkflow()
|
||||
startNode('human-node', 'trace-human')
|
||||
|
||||
act(() => {
|
||||
capturedCallbacks.onHumanInputRequired({
|
||||
data: { node_id: 'human-node', form_token: 'token-1' },
|
||||
})
|
||||
})
|
||||
|
||||
const answer = result.current.chatList.find(item => item.isAnswer && !item.isOpeningStatement)
|
||||
const trace = answer!.workflowProcess!.tracing.find((t: any) => t.node_id === 'human-node')
|
||||
expect(trace!.status).toBe('paused')
|
||||
})
|
||||
})
|
||||
|
||||
describe('onHumanInputFormFilled', () => {
|
||||
it('should remove form and add to filled list', () => {
|
||||
const { result } = setupAndSend()
|
||||
startWorkflow()
|
||||
|
||||
act(() => {
|
||||
capturedCallbacks.onHumanInputRequired({
|
||||
data: { node_id: 'human-node', form_token: 'token-1' },
|
||||
})
|
||||
})
|
||||
|
||||
act(() => {
|
||||
capturedCallbacks.onHumanInputFormFilled({
|
||||
data: { node_id: 'human-node', form_data: { answer: 'yes' } },
|
||||
})
|
||||
})
|
||||
|
||||
const answer = result.current.chatList.find(item => item.isAnswer && !item.isOpeningStatement)
|
||||
expect(answer!.humanInputFormDataList).toHaveLength(0)
|
||||
expect(answer!.humanInputFilledFormDataList).toHaveLength(1)
|
||||
expect(answer!.humanInputFilledFormDataList![0].node_id).toBe('human-node')
|
||||
expect((answer!.humanInputFilledFormDataList![0] as any).form_data).toEqual({ answer: 'yes' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('onHumanInputFormTimeout', () => {
|
||||
it('should update expiration_time on form data', () => {
|
||||
const { result } = setupAndSend()
|
||||
startWorkflow()
|
||||
|
||||
act(() => {
|
||||
capturedCallbacks.onHumanInputRequired({
|
||||
data: { node_id: 'human-node', form_token: 'token-1' },
|
||||
})
|
||||
})
|
||||
|
||||
act(() => {
|
||||
capturedCallbacks.onHumanInputFormTimeout({
|
||||
data: { node_id: 'human-node', expiration_time: '2025-01-01T00:00:00Z' },
|
||||
})
|
||||
})
|
||||
|
||||
const answer = result.current.chatList.find(item => item.isAnswer && !item.isOpeningStatement)
|
||||
const form = answer!.humanInputFormDataList!.find((f: any) => f.node_id === 'human-node')
|
||||
expect(form!.expiration_time).toBe('2025-01-01T00:00:00Z')
|
||||
})
|
||||
})
|
||||
|
||||
describe('onWorkflowPaused', () => {
|
||||
it('should set status to Paused', () => {
|
||||
const { result } = setupAndSend()
|
||||
startWorkflow()
|
||||
|
||||
act(() => {
|
||||
capturedCallbacks.onWorkflowPaused({ data: {} })
|
||||
})
|
||||
|
||||
const answer = result.current.chatList.find(item => item.isAnswer && !item.isOpeningStatement)
|
||||
expect(answer!.workflowProcess!.status).toBe('paused')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -5062,9 +5062,6 @@
|
||||
}
|
||||
},
|
||||
"app/components/plugins/install-plugin/hooks.ts": {
|
||||
"no-restricted-imports": {
|
||||
"count": 2
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 4
|
||||
}
|
||||
@@ -5100,9 +5097,6 @@
|
||||
},
|
||||
"app/components/plugins/install-plugin/install-from-github/index.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 3
|
||||
},
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 2
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
@@ -5367,17 +5361,9 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"app/components/plugins/plugin-detail-panel/detail-header/hooks/use-plugin-operations.ts": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"app/components/plugins/plugin-detail-panel/endpoint-card.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 3
|
||||
},
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 5
|
||||
"count": 2
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 2
|
||||
@@ -5385,22 +5371,13 @@
|
||||
},
|
||||
"app/components/plugins/plugin-detail-panel/endpoint-list.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 2
|
||||
},
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 4
|
||||
"count": 1
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"app/components/plugins/plugin-detail-panel/endpoint-modal.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 3
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 7
|
||||
}
|
||||
@@ -5414,9 +5391,6 @@
|
||||
}
|
||||
},
|
||||
"app/components/plugins/plugin-detail-panel/model-selector/index.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 3
|
||||
}
|
||||
@@ -5471,27 +5445,21 @@
|
||||
"app/components/plugins/plugin-detail-panel/subscription-list/create/hooks/use-common-modal-state.ts": {
|
||||
"erasable-syntax-only/enums": {
|
||||
"count": 1
|
||||
},
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"app/components/plugins/plugin-detail-panel/subscription-list/create/hooks/use-oauth-client-state.ts": {
|
||||
"erasable-syntax-only/enums": {
|
||||
"count": 2
|
||||
},
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"app/components/plugins/plugin-detail-panel/subscription-list/create/index.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 4
|
||||
"count": 3
|
||||
}
|
||||
},
|
||||
"app/components/plugins/plugin-detail-panel/subscription-list/create/oauth-client.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 2
|
||||
"count": 1
|
||||
},
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 3
|
||||
@@ -5507,20 +5475,17 @@
|
||||
"count": 1
|
||||
},
|
||||
"no-restricted-imports": {
|
||||
"count": 2
|
||||
},
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"app/components/plugins/plugin-detail-panel/subscription-list/edit/manual-edit-modal.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 2
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"app/components/plugins/plugin-detail-panel/subscription-list/edit/oauth-edit-modal.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 2
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"app/components/plugins/plugin-detail-panel/subscription-list/index.tsx": {
|
||||
@@ -5540,9 +5505,6 @@
|
||||
"erasable-syntax-only/enums": {
|
||||
"count": 1
|
||||
},
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 5
|
||||
},
|
||||
@@ -5600,11 +5562,6 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"app/components/plugins/plugin-detail-panel/tool-selector/components/tool-credentials-form.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"app/components/plugins/plugin-detail-panel/tool-selector/components/tool-item.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 2
|
||||
@@ -5643,7 +5600,7 @@
|
||||
},
|
||||
"app/components/plugins/plugin-item/action.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 3
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"app/components/plugins/plugin-item/index.tsx": {
|
||||
|
||||
Reference in New Issue
Block a user