mirror of
https://github.com/langgenius/dify.git
synced 2026-05-04 01:18:05 +08:00
chore: remove frontend changes
This commit is contained in:
@ -1,7 +1,7 @@
|
||||
import { checkTaskStatus as fetchCheckTaskStatus } from '@/service/plugins'
|
||||
import type { PluginStatus } from '../../types'
|
||||
import { TaskStatus } from '../../types'
|
||||
import { checkTaskStatus as fetchCheckTaskStatus } from '@/service/plugins'
|
||||
import { sleep } from '@/utils'
|
||||
import { TaskStatus } from '../../types'
|
||||
|
||||
const INTERVAL = 10 * 1000 // 10 seconds
|
||||
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Card from '../../card'
|
||||
import Button from '@/app/components/base/button'
|
||||
import type { Plugin, PluginDeclaration, PluginManifestInMarket } from '../../types'
|
||||
import { pluginManifestInMarketToPluginProps, pluginManifestToCardPluginProps } from '../utils'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Badge, { BadgeState } from '@/app/components/base/badge/index'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Card from '../../card'
|
||||
import { pluginManifestInMarketToPluginProps, pluginManifestToCardPluginProps } from '../utils'
|
||||
|
||||
type Props = {
|
||||
payload?: Plugin | PluginDeclaration | PluginManifestInMarket | null
|
||||
@ -30,28 +30,28 @@ const Installed: FC<Props> = ({
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div className='flex flex-col items-start justify-center gap-4 self-stretch px-6 py-3'>
|
||||
<p className='system-md-regular text-text-secondary'>{(isFailed && errMsg) ? errMsg : t(`plugin.installModal.${isFailed ? 'installFailedDesc' : 'installedSuccessfullyDesc'}`)}</p>
|
||||
<div className="flex flex-col items-start justify-center gap-4 self-stretch px-6 py-3">
|
||||
<p className="system-md-regular text-text-secondary">{(isFailed && errMsg) ? errMsg : t(`installModal.${isFailed ? 'installFailedDesc' : 'installedSuccessfullyDesc'}`, { ns: 'plugin' })}</p>
|
||||
{payload && (
|
||||
<div className='flex flex-wrap content-start items-start gap-1 self-stretch rounded-2xl bg-background-section-burn p-2'>
|
||||
<div className="flex flex-wrap content-start items-start gap-1 self-stretch rounded-2xl bg-background-section-burn p-2">
|
||||
<Card
|
||||
className='w-full'
|
||||
className="w-full"
|
||||
payload={isMarketPayload ? pluginManifestInMarketToPluginProps(payload as PluginManifestInMarket) : pluginManifestToCardPluginProps(payload as PluginDeclaration)}
|
||||
installed={!isFailed}
|
||||
installFailed={isFailed}
|
||||
titleLeft={<Badge className='mx-1' size="s" state={BadgeState.Default}>{(payload as PluginDeclaration).version || (payload as PluginManifestInMarket).latest_version}</Badge>}
|
||||
titleLeft={<Badge className="mx-1" size="s" state={BadgeState.Default}>{(payload as PluginDeclaration).version || (payload as PluginManifestInMarket).latest_version}</Badge>}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* Action Buttons */}
|
||||
<div className='flex items-center justify-end gap-2 self-stretch p-6 pt-5'>
|
||||
<div className="flex items-center justify-end gap-2 self-stretch p-6 pt-5">
|
||||
<Button
|
||||
variant='primary'
|
||||
className='min-w-[72px]'
|
||||
variant="primary"
|
||||
className="min-w-[72px]"
|
||||
onClick={handleClose}
|
||||
>
|
||||
{t('common.operation.close')}
|
||||
{t('operation.close', { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@ -1,39 +1,40 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { Group } from '../../../base/icons/src/vender/other'
|
||||
import { LoadingPlaceholder } from '@/app/components/plugins/card/base/placeholder'
|
||||
import Checkbox from '@/app/components/base/checkbox'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Checkbox from '@/app/components/base/checkbox'
|
||||
import { LoadingPlaceholder } from '@/app/components/plugins/card/base/placeholder'
|
||||
import { Group } from '../../../base/icons/src/vender/other'
|
||||
|
||||
const LoadingError: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div className='flex items-center space-x-2'>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
className='shrink-0'
|
||||
className="shrink-0"
|
||||
checked={false}
|
||||
disabled
|
||||
/>
|
||||
<div className='hover-bg-components-panel-on-panel-item-bg relative grow rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg p-4 pb-3 shadow-xs'>
|
||||
<div className="hover-bg-components-panel-on-panel-item-bg relative grow rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg p-4 pb-3 shadow-xs">
|
||||
<div className="flex">
|
||||
<div
|
||||
className='relative flex h-10 w-10 items-center justify-center gap-2 rounded-[10px] border-[0.5px]
|
||||
border-state-destructive-border bg-state-destructive-hover p-1 backdrop-blur-sm'>
|
||||
<div className='flex h-5 w-5 items-center justify-center'>
|
||||
<Group className='text-text-quaternary' />
|
||||
className="relative flex h-10 w-10 items-center justify-center gap-2 rounded-[10px] border-[0.5px]
|
||||
border-state-destructive-border bg-state-destructive-hover p-1 backdrop-blur-sm"
|
||||
>
|
||||
<div className="flex h-5 w-5 items-center justify-center">
|
||||
<Group className="text-text-quaternary" />
|
||||
</div>
|
||||
<div className='absolute bottom-[-4px] right-[-4px] rounded-full border-[2px] border-components-panel-bg bg-state-destructive-solid'>
|
||||
<RiCloseLine className='h-3 w-3 text-text-primary-on-surface' />
|
||||
<div className="absolute bottom-[-4px] right-[-4px] rounded-full border-[2px] border-components-panel-bg bg-state-destructive-solid">
|
||||
<RiCloseLine className="h-3 w-3 text-text-primary-on-surface" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-3 grow">
|
||||
<div className="system-md-semibold flex h-5 items-center text-text-destructive">
|
||||
{t('plugin.installModal.pluginLoadError')}
|
||||
{t('installModal.pluginLoadError', { ns: 'plugin' })}
|
||||
</div>
|
||||
<div className='system-xs-regular mt-0.5 text-text-tertiary'>
|
||||
{t('plugin.installModal.pluginLoadErrorDesc')}
|
||||
<div className="system-xs-regular mt-0.5 text-text-tertiary">
|
||||
{t('installModal.pluginLoadErrorDesc', { ns: 'plugin' })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,19 +1,19 @@
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import Placeholder from '../../card/base/placeholder'
|
||||
import * as React from 'react'
|
||||
import Checkbox from '@/app/components/base/checkbox'
|
||||
import Placeholder from '../../card/base/placeholder'
|
||||
|
||||
const Loading = () => {
|
||||
return (
|
||||
<div className='flex items-center space-x-2'>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
className='shrink-0'
|
||||
className="shrink-0"
|
||||
checked={false}
|
||||
disabled
|
||||
/>
|
||||
<div className='hover-bg-components-panel-on-panel-item-bg relative grow rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg p-4 pb-3 shadow-xs'>
|
||||
<div className="hover-bg-components-panel-on-panel-item-bg relative grow rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg p-4 pb-3 shadow-xs">
|
||||
<Placeholder
|
||||
wrapClassName='w-full'
|
||||
wrapClassName="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import Badge, { BadgeState } from '@/app/components/base/badge/index'
|
||||
import type { VersionProps } from '../../types'
|
||||
import * as React from 'react'
|
||||
import Badge, { BadgeState } from '@/app/components/base/badge/index'
|
||||
|
||||
const Version: FC<VersionProps> = ({
|
||||
hasInstalled,
|
||||
@ -14,19 +14,19 @@ const Version: FC<VersionProps> = ({
|
||||
{
|
||||
!hasInstalled
|
||||
? (
|
||||
<Badge className='mx-1' size="s" state={BadgeState.Default}>{toInstallVersion}</Badge>
|
||||
)
|
||||
<Badge className="mx-1" size="s" state={BadgeState.Default}>{toInstallVersion}</Badge>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
<Badge className='mx-1' size="s" state={BadgeState.Warning}>
|
||||
{`${installedVersion} -> ${toInstallVersion}`}
|
||||
</Badge>
|
||||
{/* <div className='flex px-0.5 justify-center items-center gap-0.5'>
|
||||
<>
|
||||
<Badge className="mx-1" size="s" state={BadgeState.Warning}>
|
||||
{`${installedVersion} -> ${toInstallVersion}`}
|
||||
</Badge>
|
||||
{/* <div className='flex px-0.5 justify-center items-center gap-0.5'>
|
||||
<div className='text-text-warning system-xs-medium'>Used in 3 apps</div>
|
||||
<RiInformation2Line className='w-4 h-4 text-text-tertiary' />
|
||||
</div> */}
|
||||
</>
|
||||
)
|
||||
</>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import Toast, { type IToastProps } from '@/app/components/base/toast'
|
||||
import type { GitHubRepoReleaseResponse } from '../types'
|
||||
import type { IToastProps } from '@/app/components/base/toast'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { GITHUB_ACCESS_TOKEN } from '@/config'
|
||||
import { uploadGitHub } from '@/service/plugins'
|
||||
import { compareVersion, getLatestVersion } from '@/utils/semver'
|
||||
import type { GitHubRepoReleaseResponse } from '../types'
|
||||
import { GITHUB_ACCESS_TOKEN } from '@/config'
|
||||
|
||||
const formatReleases = (releases: any) => {
|
||||
return releases.map((release: any) => ({
|
||||
@ -20,7 +21,8 @@ export const useGitHubReleases = () => {
|
||||
if (!GITHUB_ACCESS_TOKEN) {
|
||||
// Fetch releases without authentication from client
|
||||
const res = await fetch(`https://api.github.com/repos/${owner}/${repo}/releases`)
|
||||
if (!res.ok) throw new Error('Failed to fetch repository releases')
|
||||
if (!res.ok)
|
||||
throw new Error('Failed to fetch repository releases')
|
||||
const data = await res.json()
|
||||
return formatReleases(data)
|
||||
}
|
||||
@ -28,7 +30,8 @@ export const useGitHubReleases = () => {
|
||||
// Fetch releases with authentication from server
|
||||
const res = await fetch(`/repos/${owner}/${repo}/releases`)
|
||||
const bodyJson = await res.json()
|
||||
if (bodyJson.status !== 200) throw new Error(bodyJson.data.message)
|
||||
if (bodyJson.status !== 200)
|
||||
throw new Error(bodyJson.data.message)
|
||||
return formatReleases(bodyJson.data)
|
||||
}
|
||||
}
|
||||
@ -83,7 +86,7 @@ export const useGitHubUpload = () => {
|
||||
repoUrl: string,
|
||||
selectedVersion: string,
|
||||
selectedPackage: string,
|
||||
onSuccess?: (GitHubPackage: { manifest: any; unique_identifier: string }) => void,
|
||||
onSuccess?: (GitHubPackage: { manifest: any, unique_identifier: string }) => void,
|
||||
) => {
|
||||
try {
|
||||
const response = await uploadGitHub(repoUrl, selectedVersion, selectedPackage)
|
||||
@ -91,7 +94,8 @@ export const useGitHubUpload = () => {
|
||||
manifest: response.manifest,
|
||||
unique_identifier: response.unique_identifier,
|
||||
}
|
||||
if (onSuccess) onSuccess(GitHubPackage)
|
||||
if (onSuccess)
|
||||
onSuccess(GitHubPackage)
|
||||
return GitHubPackage
|
||||
}
|
||||
catch (error) {
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import { useCheckInstalled as useDoCheckInstalled } from '@/service/use-plugins'
|
||||
import type { VersionInfo } from '../../types'
|
||||
|
||||
import { useMemo } from 'react'
|
||||
import type { VersionInfo } from '../../types'
|
||||
import { useCheckInstalled as useDoCheckInstalled } from '@/service/use-plugins'
|
||||
|
||||
type Props = {
|
||||
pluginIds: string[],
|
||||
pluginIds: string[]
|
||||
enabled: boolean
|
||||
}
|
||||
const useCheckInstalled = (props: Props) => {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import type { SystemFeatures } from '@/types/feature'
|
||||
import { InstallationScope } from '@/types/feature'
|
||||
import type { Plugin, PluginManifestInMarket } from '../../types'
|
||||
import type { SystemFeatures } from '@/types/feature'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { InstallationScope } from '@/types/feature'
|
||||
|
||||
type PluginProps = (Plugin | PluginManifestInMarket) & { from: 'github' | 'marketplace' | 'package' }
|
||||
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
import { useModelList } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { useInvalidateInstalledPluginList } from '@/service/use-plugins'
|
||||
import { useInvalidateAllBuiltInTools, useInvalidateAllToolProviders, useInvalidateRAGRecommendedPlugins } from '@/service/use-tools'
|
||||
import { useInvalidateStrategyProviders } from '@/service/use-strategy'
|
||||
import type { Plugin, PluginDeclaration, PluginManifestInMarket } from '../../types'
|
||||
import { PluginCategoryEnum } from '../../types'
|
||||
import { useInvalidDataSourceList } from '@/service/use-pipeline'
|
||||
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { useModelList } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { useInvalidDataSourceListAuth } from '@/service/use-datasource'
|
||||
import { useInvalidDataSourceList } from '@/service/use-pipeline'
|
||||
import { useInvalidateInstalledPluginList } from '@/service/use-plugins'
|
||||
import { useInvalidateStrategyProviders } from '@/service/use-strategy'
|
||||
import { useInvalidateAllBuiltInTools, useInvalidateAllToolProviders, useInvalidateRAGRecommendedPlugins } from '@/service/use-tools'
|
||||
import { useInvalidateAllTriggerPlugins } from '@/service/use-triggers'
|
||||
import { PluginCategoryEnum } from '../../types'
|
||||
|
||||
const useRefreshPluginList = () => {
|
||||
const invalidateInstalledPluginList = useInvalidateInstalledPluginList()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,15 +1,16 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { InstallStep } from '../../types'
|
||||
import type { Dependency } from '../../types'
|
||||
import ReadyToInstall from './ready-to-install'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { InstallStep } from '../../types'
|
||||
import useHideLogic from '../hooks/use-hide-logic'
|
||||
import cn from '@/utils/classnames'
|
||||
import ReadyToInstall from './ready-to-install'
|
||||
|
||||
const i18nPrefix = 'plugin.installModal'
|
||||
const i18nPrefix = 'installModal'
|
||||
|
||||
export enum InstallType {
|
||||
fromLocal = 'fromLocal',
|
||||
@ -41,11 +42,11 @@ const InstallBundle: FC<Props> = ({
|
||||
|
||||
const getTitle = useCallback(() => {
|
||||
if (step === InstallStep.uploadFailed)
|
||||
return t(`${i18nPrefix}.uploadFailed`)
|
||||
return t(`${i18nPrefix}.uploadFailed`, { ns: 'plugin' })
|
||||
if (step === InstallStep.installed)
|
||||
return t(`${i18nPrefix}.installComplete`)
|
||||
return t(`${i18nPrefix}.installComplete`, { ns: 'plugin' })
|
||||
|
||||
return t(`${i18nPrefix}.installPlugin`)
|
||||
return t(`${i18nPrefix}.installPlugin`, { ns: 'plugin' })
|
||||
}, [step, t])
|
||||
|
||||
return (
|
||||
@ -55,8 +56,8 @@ const InstallBundle: FC<Props> = ({
|
||||
className={cn(modalClassName, 'shadows-shadow-xl flex min-w-[560px] flex-col items-start rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-0')}
|
||||
closable
|
||||
>
|
||||
<div className='flex items-start gap-2 self-stretch pb-3 pl-6 pr-14 pt-6'>
|
||||
<div className='title-2xl-semi-bold self-stretch text-text-primary'>
|
||||
<div className="flex items-start gap-2 self-stretch pb-3 pl-6 pr-14 pt-6">
|
||||
<div className="title-2xl-semi-bold self-stretch text-text-primary">
|
||||
{getTitle()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect } from 'react'
|
||||
import type { GitHubItemAndMarketPlaceDependency, Plugin } from '../../../types'
|
||||
import { pluginManifestToCardPluginProps } from '../../utils'
|
||||
import type { VersionProps } from '@/app/components/plugins/types'
|
||||
import * as React from 'react'
|
||||
import { useEffect } from 'react'
|
||||
import { useUploadGitHub } from '@/service/use-plugins'
|
||||
import Loading from '../../base/loading'
|
||||
import { pluginManifestToCardPluginProps } from '../../utils'
|
||||
import LoadedItem from './loaded-item'
|
||||
import type { VersionProps } from '@/app/components/plugins/types'
|
||||
|
||||
type Props = {
|
||||
checked: boolean
|
||||
@ -46,7 +47,8 @@ const Item: FC<Props> = ({
|
||||
if (error)
|
||||
onFetchError()
|
||||
}, [error])
|
||||
if (!payload) return <Loading />
|
||||
if (!payload)
|
||||
return <Loading />
|
||||
return (
|
||||
<LoadedItem
|
||||
payload={payload}
|
||||
|
||||
@ -1,13 +1,12 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import type { Plugin } from '../../../types'
|
||||
import Card from '../../../card'
|
||||
import type { Plugin, VersionProps } from '../../../types'
|
||||
import * as React from 'react'
|
||||
import Checkbox from '@/app/components/base/checkbox'
|
||||
import useGetIcon from '../../base/use-get-icon'
|
||||
import { MARKETPLACE_API_PREFIX } from '@/config'
|
||||
import Card from '../../../card'
|
||||
import useGetIcon from '../../base/use-get-icon'
|
||||
import Version from '../../base/version'
|
||||
import type { VersionProps } from '../../../types'
|
||||
import usePluginInstallLimit from '../../hooks/use-install-plugin-limit'
|
||||
|
||||
type Props = {
|
||||
@ -32,15 +31,15 @@ const LoadedItem: FC<Props> = ({
|
||||
}
|
||||
const { canInstall } = usePluginInstallLimit(payload)
|
||||
return (
|
||||
<div className='flex items-center space-x-2'>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
disabled={!canInstall}
|
||||
className='shrink-0'
|
||||
className="shrink-0"
|
||||
checked={checked}
|
||||
onCheck={() => onCheckedChange(payload)}
|
||||
/>
|
||||
<Card
|
||||
className='grow'
|
||||
className="grow"
|
||||
payload={{
|
||||
...payload,
|
||||
icon: isFromMarketPlace ? `${MARKETPLACE_API_PREFIX}/plugins/${payload.org}/${payload.name}/icon` : getIconUrl(payload.icon),
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import type { Plugin } from '../../../types'
|
||||
import type { VersionProps } from '@/app/components/plugins/types'
|
||||
import * as React from 'react'
|
||||
import Loading from '../../base/loading'
|
||||
import LoadedItem from './loaded-item'
|
||||
import type { VersionProps } from '@/app/components/plugins/types'
|
||||
|
||||
type Props = {
|
||||
checked: boolean
|
||||
@ -21,7 +21,8 @@ const MarketPlaceItem: FC<Props> = ({
|
||||
version,
|
||||
versionInfo,
|
||||
}) => {
|
||||
if (!payload) return <Loading />
|
||||
if (!payload)
|
||||
return <Loading />
|
||||
return (
|
||||
<LoadedItem
|
||||
checked={checked}
|
||||
|
||||
@ -1,12 +1,11 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import type { Plugin } from '../../../types'
|
||||
import type { PackageDependency } from '../../../types'
|
||||
import type { PackageDependency, Plugin } from '../../../types'
|
||||
import type { VersionProps } from '@/app/components/plugins/types'
|
||||
import * as React from 'react'
|
||||
import LoadingError from '../../base/loading-error'
|
||||
import { pluginManifestToCardPluginProps } from '../../utils'
|
||||
import LoadedItem from './loaded-item'
|
||||
import LoadingError from '../../base/loading-error'
|
||||
import type { VersionProps } from '@/app/components/plugins/types'
|
||||
|
||||
type Props = {
|
||||
checked: boolean
|
||||
|
||||
@ -1,14 +1,15 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import type { Dependency, InstallStatus, Plugin } from '../../types'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { InstallStep } from '../../types'
|
||||
import Install from './steps/install'
|
||||
import Installed from './steps/installed'
|
||||
import type { Dependency, InstallStatus, Plugin } from '../../types'
|
||||
|
||||
type Props = {
|
||||
step: InstallStep
|
||||
onStepChange: (step: InstallStep) => void,
|
||||
onStepChange: (step: InstallStep) => void
|
||||
onStartToInstall: () => void
|
||||
setIsInstalling: (isInstalling: boolean) => void
|
||||
allPlugins: Dependency[]
|
||||
|
||||
@ -1,16 +1,16 @@
|
||||
'use client'
|
||||
import { useImperativeHandle } from 'react'
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import type { Dependency, GitHubItemAndMarketPlaceDependency, PackageDependency, Plugin, VersionInfo } from '../../../types'
|
||||
import MarketplaceItem from '../item/marketplace-item'
|
||||
import GithubItem from '../item/github-item'
|
||||
import { useFetchPluginsInMarketPlaceByInfo } from '@/service/use-plugins'
|
||||
import useCheckInstalled from '@/app/components/plugins/install-plugin/hooks/use-check-installed'
|
||||
import { produce } from 'immer'
|
||||
import PackageItem from '../item/package-item'
|
||||
import LoadingError from '../../base/loading-error'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useImperativeHandle, useMemo, useState } from 'react'
|
||||
import useCheckInstalled from '@/app/components/plugins/install-plugin/hooks/use-check-installed'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { useFetchPluginsInMarketPlaceByInfo } from '@/service/use-plugins'
|
||||
import LoadingError from '../../base/loading-error'
|
||||
import { pluginInstallLimit } from '../../hooks/use-install-plugin-limit'
|
||||
import GithubItem from '../item/github-item'
|
||||
import MarketplaceItem from '../item/marketplace-item'
|
||||
import PackageItem from '../item/package-item'
|
||||
|
||||
type Props = {
|
||||
allPlugins: Dependency[]
|
||||
@ -229,15 +229,17 @@ const InstallByDSLList = ({
|
||||
}
|
||||
const plugin = plugins[index]
|
||||
if (d.type === 'github') {
|
||||
return (<GithubItem
|
||||
key={index}
|
||||
checked={!!selectedPlugins.find(p => p.plugin_id === plugins[index]?.plugin_id)}
|
||||
onCheckedChange={handleSelect(index)}
|
||||
dependency={d as GitHubItemAndMarketPlaceDependency}
|
||||
onFetchedPayload={handleGitHubPluginFetched(index)}
|
||||
onFetchError={handleGitHubPluginFetchError(index)}
|
||||
versionInfo={getVersionInfo(`${plugin?.org || plugin?.author}/${plugin?.name}`)}
|
||||
/>)
|
||||
return (
|
||||
<GithubItem
|
||||
key={index}
|
||||
checked={!!selectedPlugins.find(p => p.plugin_id === plugins[index]?.plugin_id)}
|
||||
onCheckedChange={handleSelect(index)}
|
||||
dependency={d as GitHubItemAndMarketPlaceDependency}
|
||||
onFetchedPayload={handleGitHubPluginFetched(index)}
|
||||
onFetchError={handleGitHubPluginFetchError(index)}
|
||||
versionInfo={getVersionInfo(`${plugin?.org || plugin?.author}/${plugin?.name}`)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (d.type === 'marketplace') {
|
||||
@ -264,8 +266,7 @@ const InstallByDSLList = ({
|
||||
versionInfo={getVersionInfo(`${plugin?.org || plugin?.author}/${plugin?.name}`)}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,27 +1,26 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { useRef } from 'react'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import {
|
||||
type Dependency,
|
||||
type InstallStatus,
|
||||
type InstallStatusResponse,
|
||||
type Plugin,
|
||||
TaskStatus,
|
||||
type VersionInfo,
|
||||
} from '../../../types'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { RiLoader2Line } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { Dependency, InstallStatus, InstallStatusResponse, Plugin, VersionInfo } from '../../../types'
|
||||
import type { ExposeRefs } from './install-multi'
|
||||
import InstallMulti from './install-multi'
|
||||
import { useInstallOrUpdate, usePluginTaskList } from '@/service/use-plugins'
|
||||
import useRefreshPluginList from '../../hooks/use-refresh-plugin-list'
|
||||
import { RiLoader2Line } from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Checkbox from '@/app/components/base/checkbox'
|
||||
import { useCanInstallPluginFromMarketplace } from '@/app/components/plugins/plugin-page/use-reference-setting'
|
||||
import { useMittContextSelector } from '@/context/mitt-context'
|
||||
import Checkbox from '@/app/components/base/checkbox'
|
||||
import { useInstallOrUpdate, usePluginTaskList } from '@/service/use-plugins'
|
||||
import {
|
||||
|
||||
TaskStatus,
|
||||
|
||||
} from '../../../types'
|
||||
import checkTaskStatus from '../../base/check-task-status'
|
||||
const i18nPrefix = 'plugin.installModal'
|
||||
import useRefreshPluginList from '../../hooks/use-refresh-plugin-list'
|
||||
import InstallMulti from './install-multi'
|
||||
|
||||
const i18nPrefix = 'installModal'
|
||||
|
||||
type Props = {
|
||||
allPlugins: Dependency[]
|
||||
@ -172,11 +171,11 @@ const Install: FC<Props> = ({
|
||||
const { canInstallPluginFromMarketplace } = useCanInstallPluginFromMarketplace()
|
||||
return (
|
||||
<>
|
||||
<div className='flex flex-col items-start justify-center gap-4 self-stretch px-6 py-3'>
|
||||
<div className='system-md-regular text-text-secondary'>
|
||||
<p>{t(`${i18nPrefix}.${selectedPluginsNum > 1 ? 'readyToInstallPackages' : 'readyToInstallPackage'}`, { num: selectedPluginsNum })}</p>
|
||||
<div className="flex flex-col items-start justify-center gap-4 self-stretch px-6 py-3">
|
||||
<div className="system-md-regular text-text-secondary">
|
||||
<p>{t(`${i18nPrefix}.${selectedPluginsNum > 1 ? 'readyToInstallPackages' : 'readyToInstallPackage'}`, { ns: 'plugin', num: selectedPluginsNum })}</p>
|
||||
</div>
|
||||
<div className='w-full space-y-1 rounded-2xl bg-background-section-burn p-2'>
|
||||
<div className="w-full space-y-1 rounded-2xl bg-background-section-burn p-2">
|
||||
<InstallMulti
|
||||
ref={installMultiRef}
|
||||
allPlugins={allPlugins}
|
||||
@ -191,27 +190,29 @@ const Install: FC<Props> = ({
|
||||
</div>
|
||||
{/* Action Buttons */}
|
||||
{!isHideButton && (
|
||||
<div className='flex items-center justify-between gap-2 self-stretch p-6 pt-5'>
|
||||
<div className='px-2'>
|
||||
{canInstall && <div className='flex items-center gap-x-2' onClick={handleClickSelectAll}>
|
||||
<Checkbox checked={isSelectAll} indeterminate={isIndeterminate} />
|
||||
<p className='system-sm-medium cursor-pointer text-text-secondary'>{isSelectAll ? t('common.operation.deSelectAll') : t('common.operation.selectAll')}</p>
|
||||
</div>}
|
||||
<div className="flex items-center justify-between gap-2 self-stretch p-6 pt-5">
|
||||
<div className="px-2">
|
||||
{canInstall && (
|
||||
<div className="flex items-center gap-x-2" onClick={handleClickSelectAll}>
|
||||
<Checkbox checked={isSelectAll} indeterminate={isIndeterminate} />
|
||||
<p className="system-sm-medium cursor-pointer text-text-secondary">{isSelectAll ? t('operation.deSelectAll', { ns: 'common' }) : t('operation.selectAll', { ns: 'common' })}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className='flex items-center justify-end gap-2 self-stretch'>
|
||||
<div className="flex items-center justify-end gap-2 self-stretch">
|
||||
{!canInstall && (
|
||||
<Button variant='secondary' className='min-w-[72px]' onClick={handleCancel}>
|
||||
{t('common.operation.cancel')}
|
||||
<Button variant="secondary" className="min-w-[72px]" onClick={handleCancel}>
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant='primary'
|
||||
className='flex min-w-[72px] space-x-0.5'
|
||||
variant="primary"
|
||||
className="flex min-w-[72px] space-x-0.5"
|
||||
disabled={!canInstall || isInstalling || selectedPlugins.length === 0 || !canInstallPluginFromMarketplace}
|
||||
onClick={handleInstall}
|
||||
>
|
||||
{isInstalling && <RiLoader2Line className='h-4 w-4 animate-spin-slow' />}
|
||||
<span>{t(`${i18nPrefix}.${isInstalling ? 'installing' : 'install'}`)}</span>
|
||||
{isInstalling && <RiLoader2Line className="h-4 w-4 animate-spin-slow" />}
|
||||
<span>{t(`${i18nPrefix}.${isInstalling ? 'installing' : 'install'}`, { ns: 'plugin' })}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import type { InstallStatus, Plugin } from '../../../types'
|
||||
import Card from '@/app/components/plugins/card'
|
||||
import Button from '@/app/components/base/button'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Badge, { BadgeState } from '@/app/components/base/badge/index'
|
||||
import useGetIcon from '../../base/use-get-icon'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Card from '@/app/components/plugins/card'
|
||||
import { MARKETPLACE_API_PREFIX } from '@/config'
|
||||
import useGetIcon from '../../base/use-get-icon'
|
||||
|
||||
type Props = {
|
||||
list: Plugin[]
|
||||
@ -26,21 +26,21 @@ const Installed: FC<Props> = ({
|
||||
const { getIconUrl } = useGetIcon()
|
||||
return (
|
||||
<>
|
||||
<div className='flex flex-col items-start justify-center gap-4 self-stretch px-6 py-3'>
|
||||
<div className="flex flex-col items-start justify-center gap-4 self-stretch px-6 py-3">
|
||||
{/* <p className='text-text-secondary system-md-regular'>{(isFailed && errMsg) ? errMsg : t(`plugin.installModal.${isFailed ? 'installFailedDesc' : 'installedSuccessfullyDesc'}`)}</p> */}
|
||||
<div className='flex flex-wrap content-start items-start gap-1 space-y-1 self-stretch rounded-2xl bg-background-section-burn p-2'>
|
||||
<div className="flex flex-wrap content-start items-start gap-1 space-y-1 self-stretch rounded-2xl bg-background-section-burn p-2">
|
||||
{list.map((plugin, index) => {
|
||||
return (
|
||||
<Card
|
||||
key={plugin.plugin_id}
|
||||
className='w-full'
|
||||
className="w-full"
|
||||
payload={{
|
||||
...plugin,
|
||||
icon: installStatus[index].isFromMarketPlace ? `${MARKETPLACE_API_PREFIX}/plugins/${plugin.org}/${plugin.name}/icon` : getIconUrl(plugin.icon),
|
||||
}}
|
||||
installed={installStatus[index].success}
|
||||
installFailed={!installStatus[index].success}
|
||||
titleLeft={plugin.version ? <Badge className='mx-1' size="s" state={BadgeState.Default}>{plugin.version}</Badge> : null}
|
||||
titleLeft={plugin.version ? <Badge className="mx-1" size="s" state={BadgeState.Default}>{plugin.version}</Badge> : null}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
@ -48,13 +48,13 @@ const Installed: FC<Props> = ({
|
||||
</div>
|
||||
{/* Action Buttons */}
|
||||
{!isHideButton && (
|
||||
<div className='flex items-center justify-end gap-2 self-stretch p-6 pt-5'>
|
||||
<div className="flex items-center justify-end gap-2 self-stretch p-6 pt-5">
|
||||
<Button
|
||||
variant='primary'
|
||||
className='min-w-[72px]'
|
||||
variant="primary"
|
||||
className="min-w-[72px]"
|
||||
onClick={onCancel}
|
||||
>
|
||||
{t('common.operation.close')}
|
||||
{t('operation.close', { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,25 +1,26 @@
|
||||
'use client'
|
||||
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import type { PluginDeclaration, UpdateFromGitHubPayload } from '../../types'
|
||||
import type { Item } from '@/app/components/base/select'
|
||||
import type { InstallState } from '@/app/components/plugins/types'
|
||||
import { useGitHubReleases } from '../hooks'
|
||||
import { convertRepoToUrl, parseGitHubUrl } from '../utils'
|
||||
import type { PluginDeclaration, UpdateFromGitHubPayload } from '../../types'
|
||||
import { InstallStepFromGitHub } from '../../types'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import SetURL from './steps/setURL'
|
||||
import SelectPackage from './steps/selectPackage'
|
||||
import Installed from '../base/installed'
|
||||
import Loaded from './steps/loaded'
|
||||
import useGetIcon from '@/app/components/plugins/install-plugin/base/use-get-icon'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useRefreshPluginList from '../hooks/use-refresh-plugin-list'
|
||||
import cn from '@/utils/classnames'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import useGetIcon from '@/app/components/plugins/install-plugin/base/use-get-icon'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { InstallStepFromGitHub } from '../../types'
|
||||
import Installed from '../base/installed'
|
||||
import { useGitHubReleases } from '../hooks'
|
||||
import useHideLogic from '../hooks/use-hide-logic'
|
||||
import useRefreshPluginList from '../hooks/use-refresh-plugin-list'
|
||||
import { convertRepoToUrl, parseGitHubUrl } from '../utils'
|
||||
import Loaded from './steps/loaded'
|
||||
import SelectPackage from './steps/selectPackage'
|
||||
import SetURL from './steps/setURL'
|
||||
|
||||
const i18nPrefix = 'plugin.installFromGitHub'
|
||||
const i18nPrefix = 'installFromGitHub'
|
||||
|
||||
type InstallFromGitHubProps = {
|
||||
updatePayload?: UpdateFromGitHubPayload
|
||||
@ -60,21 +61,21 @@ const InstallFromGitHub: React.FC<InstallFromGitHubProps> = ({ updatePayload, on
|
||||
|
||||
const packages: Item[] = state.selectedVersion
|
||||
? (state.releases
|
||||
.find(release => release.tag_name === state.selectedVersion)
|
||||
?.assets
|
||||
.map(asset => ({
|
||||
value: asset.name,
|
||||
name: asset.name,
|
||||
})) || [])
|
||||
.find(release => release.tag_name === state.selectedVersion)
|
||||
?.assets
|
||||
.map(asset => ({
|
||||
value: asset.name,
|
||||
name: asset.name,
|
||||
})) || [])
|
||||
: []
|
||||
|
||||
const getTitle = useCallback(() => {
|
||||
if (state.step === InstallStepFromGitHub.installed)
|
||||
return t(`${i18nPrefix}.installedSuccessfully`)
|
||||
return t(`${i18nPrefix}.installedSuccessfully`, { ns: 'plugin' })
|
||||
if (state.step === InstallStepFromGitHub.installFailed)
|
||||
return t(`${i18nPrefix}.installFailed`)
|
||||
return t(`${i18nPrefix}.installFailed`, { ns: 'plugin' })
|
||||
|
||||
return updatePayload ? t(`${i18nPrefix}.updatePlugin`) : t(`${i18nPrefix}.installPlugin`)
|
||||
return updatePayload ? t(`${i18nPrefix}.updatePlugin`, { ns: 'plugin' }) : t(`${i18nPrefix}.installPlugin`, { ns: 'plugin' })
|
||||
}, [state.step, t, updatePayload])
|
||||
|
||||
const handleUrlSubmit = async () => {
|
||||
@ -82,7 +83,7 @@ const InstallFromGitHub: React.FC<InstallFromGitHubProps> = ({ updatePayload, on
|
||||
if (!isValid || !owner || !repo) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('plugin.error.inValidGitHubUrl'),
|
||||
message: t('error.inValidGitHubUrl', { ns: 'plugin' }),
|
||||
})
|
||||
return
|
||||
}
|
||||
@ -98,20 +99,20 @@ const InstallFromGitHub: React.FC<InstallFromGitHubProps> = ({ updatePayload, on
|
||||
else {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('plugin.error.noReleasesFound'),
|
||||
message: t('error.noReleasesFound', { ns: 'plugin' }),
|
||||
})
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('plugin.error.fetchReleasesError'),
|
||||
message: t('error.fetchReleasesError', { ns: 'plugin' }),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleError = (e: any, isInstall: boolean) => {
|
||||
const message = e?.response?.message || t('plugin.installModal.installFailedDesc')
|
||||
const message = e?.response?.message || t('installModal.installFailedDesc', { ns: 'plugin' })
|
||||
setErrorMsg(message)
|
||||
setState(prevState => ({ ...prevState, step: isInstall ? InstallStepFromGitHub.installFailed : InstallStepFromGitHub.uploadFailed }))
|
||||
}
|
||||
@ -172,62 +173,66 @@ const InstallFromGitHub: React.FC<InstallFromGitHubProps> = ({ updatePayload, on
|
||||
border-components-panel-border bg-components-panel-bg p-0`)}
|
||||
closable
|
||||
>
|
||||
<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="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">
|
||||
{getTitle()}
|
||||
</div>
|
||||
<div className='system-xs-regular self-stretch text-text-tertiary'>
|
||||
{!([InstallStepFromGitHub.uploadFailed, InstallStepFromGitHub.installed, InstallStepFromGitHub.installFailed].includes(state.step)) && t('plugin.installFromGitHub.installNote')}
|
||||
<div className="system-xs-regular self-stretch text-text-tertiary">
|
||||
{!([InstallStepFromGitHub.uploadFailed, InstallStepFromGitHub.installed, InstallStepFromGitHub.installFailed].includes(state.step)) && t('installFromGitHub.installNote', { ns: 'plugin' })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{([InstallStepFromGitHub.uploadFailed, InstallStepFromGitHub.installed, InstallStepFromGitHub.installFailed].includes(state.step))
|
||||
? <Installed
|
||||
payload={manifest}
|
||||
isFailed={[InstallStepFromGitHub.uploadFailed, InstallStepFromGitHub.installFailed].includes(state.step)}
|
||||
errMsg={errorMsg}
|
||||
onCancel={onClose}
|
||||
/>
|
||||
: <div className={`flex flex-col items-start justify-center self-stretch px-6 py-3 ${state.step === InstallStepFromGitHub.installed ? 'gap-2' : 'gap-4'}`}>
|
||||
{state.step === InstallStepFromGitHub.setUrl && (
|
||||
<SetURL
|
||||
repoUrl={state.repoUrl}
|
||||
onChange={value => setState(prevState => ({ ...prevState, repoUrl: value }))}
|
||||
onNext={handleUrlSubmit}
|
||||
? (
|
||||
<Installed
|
||||
payload={manifest}
|
||||
isFailed={[InstallStepFromGitHub.uploadFailed, InstallStepFromGitHub.installFailed].includes(state.step)}
|
||||
errMsg={errorMsg}
|
||||
onCancel={onClose}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<div className={`flex flex-col items-start justify-center self-stretch px-6 py-3 ${state.step === InstallStepFromGitHub.installed ? 'gap-2' : 'gap-4'}`}>
|
||||
{state.step === InstallStepFromGitHub.setUrl && (
|
||||
<SetURL
|
||||
repoUrl={state.repoUrl}
|
||||
onChange={value => setState(prevState => ({ ...prevState, repoUrl: value }))}
|
||||
onNext={handleUrlSubmit}
|
||||
onCancel={onClose}
|
||||
/>
|
||||
)}
|
||||
{state.step === InstallStepFromGitHub.selectPackage && (
|
||||
<SelectPackage
|
||||
updatePayload={updatePayload!}
|
||||
repoUrl={state.repoUrl}
|
||||
selectedVersion={state.selectedVersion}
|
||||
versions={versions}
|
||||
onSelectVersion={item => setState(prevState => ({ ...prevState, selectedVersion: item.value as string }))}
|
||||
selectedPackage={state.selectedPackage}
|
||||
packages={packages}
|
||||
onSelectPackage={item => setState(prevState => ({ ...prevState, selectedPackage: item.value as string }))}
|
||||
onUploaded={handleUploaded}
|
||||
onFailed={handleUploadFail}
|
||||
onBack={handleBack}
|
||||
/>
|
||||
)}
|
||||
{state.step === InstallStepFromGitHub.readyToInstall && (
|
||||
<Loaded
|
||||
updatePayload={updatePayload!}
|
||||
uniqueIdentifier={uniqueIdentifier!}
|
||||
payload={manifest as any}
|
||||
repoUrl={state.repoUrl}
|
||||
selectedVersion={state.selectedVersion}
|
||||
selectedPackage={state.selectedPackage}
|
||||
onBack={handleBack}
|
||||
onStartToInstall={handleStartToInstall}
|
||||
onInstalled={handleInstalled}
|
||||
onFailed={handleFailed}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{state.step === InstallStepFromGitHub.selectPackage && (
|
||||
<SelectPackage
|
||||
updatePayload={updatePayload!}
|
||||
repoUrl={state.repoUrl}
|
||||
selectedVersion={state.selectedVersion}
|
||||
versions={versions}
|
||||
onSelectVersion={item => setState(prevState => ({ ...prevState, selectedVersion: item.value as string }))}
|
||||
selectedPackage={state.selectedPackage}
|
||||
packages={packages}
|
||||
onSelectPackage={item => setState(prevState => ({ ...prevState, selectedPackage: item.value as string }))}
|
||||
onUploaded={handleUploaded}
|
||||
onFailed={handleUploadFail}
|
||||
onBack={handleBack}
|
||||
/>
|
||||
)}
|
||||
{state.step === InstallStepFromGitHub.readyToInstall && (
|
||||
<Loaded
|
||||
updatePayload={updatePayload!}
|
||||
uniqueIdentifier={uniqueIdentifier!}
|
||||
payload={manifest as any}
|
||||
repoUrl={state.repoUrl}
|
||||
selectedVersion={state.selectedVersion}
|
||||
selectedPackage={state.selectedPackage}
|
||||
onBack={handleBack}
|
||||
onStartToInstall={handleStartToInstall}
|
||||
onInstalled={handleInstalled}
|
||||
onFailed={handleFailed}
|
||||
/>
|
||||
)}
|
||||
</div>}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
@ -0,0 +1,525 @@
|
||||
import type { Plugin, PluginDeclaration, UpdateFromGitHubPayload } from '../../../types'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { PluginCategoryEnum, TaskStatus } from '../../../types'
|
||||
import Loaded from './loaded'
|
||||
|
||||
// Mock dependencies
|
||||
const mockUseCheckInstalled = vi.fn()
|
||||
vi.mock('@/app/components/plugins/install-plugin/hooks/use-check-installed', () => ({
|
||||
default: (params: { pluginIds: string[], enabled: boolean }) => mockUseCheckInstalled(params),
|
||||
}))
|
||||
|
||||
const mockUpdateFromGitHub = vi.fn()
|
||||
vi.mock('@/service/plugins', () => ({
|
||||
updateFromGitHub: (...args: unknown[]) => mockUpdateFromGitHub(...args),
|
||||
}))
|
||||
|
||||
const mockInstallPackageFromGitHub = vi.fn()
|
||||
const mockHandleRefetch = vi.fn()
|
||||
vi.mock('@/service/use-plugins', () => ({
|
||||
useInstallPackageFromGitHub: () => ({ mutateAsync: mockInstallPackageFromGitHub }),
|
||||
usePluginTaskList: () => ({ handleRefetch: mockHandleRefetch }),
|
||||
}))
|
||||
|
||||
const mockCheck = vi.fn()
|
||||
vi.mock('../../base/check-task-status', () => ({
|
||||
default: () => ({ check: mockCheck }),
|
||||
}))
|
||||
|
||||
// Mock Card component
|
||||
vi.mock('../../../card', () => ({
|
||||
default: ({ payload, titleLeft }: { payload: Plugin, titleLeft?: React.ReactNode }) => (
|
||||
<div data-testid="plugin-card">
|
||||
<span data-testid="card-name">{payload.name}</span>
|
||||
{titleLeft && <span data-testid="title-left">{titleLeft}</span>}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock Version component
|
||||
vi.mock('../../base/version', () => ({
|
||||
default: ({ hasInstalled, installedVersion, toInstallVersion }: {
|
||||
hasInstalled: boolean
|
||||
installedVersion?: string
|
||||
toInstallVersion: string
|
||||
}) => (
|
||||
<span data-testid="version-info">
|
||||
{hasInstalled ? `Update from ${installedVersion} to ${toInstallVersion}` : `Install ${toInstallVersion}`}
|
||||
</span>
|
||||
),
|
||||
}))
|
||||
|
||||
// Factory functions
|
||||
const createMockPayload = (overrides: Partial<PluginDeclaration> = {}): PluginDeclaration => ({
|
||||
plugin_unique_identifier: 'test-uid',
|
||||
version: '1.0.0',
|
||||
author: 'test-author',
|
||||
icon: 'icon.png',
|
||||
name: 'Test Plugin',
|
||||
category: PluginCategoryEnum.tool,
|
||||
label: { 'en-US': 'Test' } as PluginDeclaration['label'],
|
||||
description: { 'en-US': 'Test Description' } as PluginDeclaration['description'],
|
||||
created_at: '2024-01-01',
|
||||
resource: {},
|
||||
plugins: [],
|
||||
verified: true,
|
||||
endpoint: { settings: [], endpoints: [] },
|
||||
model: null,
|
||||
tags: [],
|
||||
agent_strategy: null,
|
||||
meta: { version: '1.0.0' },
|
||||
trigger: {} as PluginDeclaration['trigger'],
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const createMockPluginPayload = (overrides: Partial<Plugin> = {}): Plugin => ({
|
||||
type: 'plugin',
|
||||
org: 'test-org',
|
||||
name: 'Test Plugin',
|
||||
plugin_id: 'test-plugin-id',
|
||||
version: '1.0.0',
|
||||
latest_version: '1.0.0',
|
||||
latest_package_identifier: 'test-pkg',
|
||||
icon: 'icon.png',
|
||||
verified: true,
|
||||
label: { 'en-US': 'Test' },
|
||||
brief: { 'en-US': 'Brief' },
|
||||
description: { 'en-US': 'Description' },
|
||||
introduction: 'Intro',
|
||||
repository: '',
|
||||
category: PluginCategoryEnum.tool,
|
||||
install_count: 100,
|
||||
endpoint: { settings: [] },
|
||||
tags: [],
|
||||
badges: [],
|
||||
verification: { authorized_category: 'langgenius' },
|
||||
from: 'github',
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const createUpdatePayload = (): UpdateFromGitHubPayload => ({
|
||||
originalPackageInfo: {
|
||||
id: 'original-id',
|
||||
repo: 'owner/repo',
|
||||
version: 'v0.9.0',
|
||||
package: 'plugin.zip',
|
||||
releases: [],
|
||||
},
|
||||
})
|
||||
|
||||
describe('Loaded', () => {
|
||||
const defaultProps = {
|
||||
updatePayload: undefined,
|
||||
uniqueIdentifier: 'test-unique-id',
|
||||
payload: createMockPayload() as PluginDeclaration | Plugin,
|
||||
repoUrl: 'https://github.com/owner/repo',
|
||||
selectedVersion: 'v1.0.0',
|
||||
selectedPackage: 'plugin.zip',
|
||||
onBack: vi.fn(),
|
||||
onStartToInstall: vi.fn(),
|
||||
onInstalled: vi.fn(),
|
||||
onFailed: vi.fn(),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockUseCheckInstalled.mockReturnValue({
|
||||
installedInfo: {},
|
||||
isLoading: false,
|
||||
})
|
||||
mockUpdateFromGitHub.mockResolvedValue({ all_installed: true, task_id: 'task-1' })
|
||||
mockInstallPackageFromGitHub.mockResolvedValue({ all_installed: true, task_id: 'task-1' })
|
||||
mockCheck.mockResolvedValue({ status: TaskStatus.success, error: null })
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Rendering Tests
|
||||
// ================================
|
||||
describe('Rendering', () => {
|
||||
it('should render ready to install message', () => {
|
||||
render(<Loaded {...defaultProps} />)
|
||||
|
||||
expect(screen.getByText('plugin.installModal.readyToInstall')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render plugin card', () => {
|
||||
render(<Loaded {...defaultProps} />)
|
||||
|
||||
expect(screen.getByTestId('plugin-card')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render back button when not installing', () => {
|
||||
render(<Loaded {...defaultProps} />)
|
||||
|
||||
expect(screen.getByRole('button', { name: 'plugin.installModal.back' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render install button', () => {
|
||||
render(<Loaded {...defaultProps} />)
|
||||
|
||||
expect(screen.getByRole('button', { name: /plugin.installModal.install/i })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show version info in card title', () => {
|
||||
render(<Loaded {...defaultProps} />)
|
||||
|
||||
expect(screen.getByTestId('version-info')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Props Tests
|
||||
// ================================
|
||||
describe('Props', () => {
|
||||
it('should display plugin name from payload', () => {
|
||||
render(<Loaded {...defaultProps} />)
|
||||
|
||||
expect(screen.getByTestId('card-name')).toHaveTextContent('Test Plugin')
|
||||
})
|
||||
|
||||
it('should pass correct version to Version component', () => {
|
||||
render(<Loaded {...defaultProps} payload={createMockPayload({ version: '2.0.0' })} />)
|
||||
|
||||
expect(screen.getByTestId('version-info')).toHaveTextContent('Install 2.0.0')
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Button State Tests
|
||||
// ================================
|
||||
describe('Button State', () => {
|
||||
it('should disable install button while loading', () => {
|
||||
mockUseCheckInstalled.mockReturnValue({
|
||||
installedInfo: {},
|
||||
isLoading: true,
|
||||
})
|
||||
|
||||
render(<Loaded {...defaultProps} />)
|
||||
|
||||
expect(screen.getByRole('button', { name: /plugin.installModal.install/i })).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should enable install button when not loading', () => {
|
||||
render(<Loaded {...defaultProps} />)
|
||||
|
||||
expect(screen.getByRole('button', { name: /plugin.installModal.install/i })).not.toBeDisabled()
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// User Interactions Tests
|
||||
// ================================
|
||||
describe('User Interactions', () => {
|
||||
it('should call onBack when back button is clicked', () => {
|
||||
const onBack = vi.fn()
|
||||
render(<Loaded {...defaultProps} onBack={onBack} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'plugin.installModal.back' }))
|
||||
|
||||
expect(onBack).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should call onStartToInstall when install starts', async () => {
|
||||
const onStartToInstall = vi.fn()
|
||||
render(<Loaded {...defaultProps} onStartToInstall={onStartToInstall} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /plugin.installModal.install/i }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onStartToInstall).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Installation Flow Tests
|
||||
// ================================
|
||||
describe('Installation Flows', () => {
|
||||
it('should call installPackageFromGitHub for fresh install', async () => {
|
||||
const onInstalled = vi.fn()
|
||||
render(<Loaded {...defaultProps} onInstalled={onInstalled} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /plugin.installModal.install/i }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockInstallPackageFromGitHub).toHaveBeenCalledWith({
|
||||
repoUrl: 'owner/repo',
|
||||
selectedVersion: 'v1.0.0',
|
||||
selectedPackage: 'plugin.zip',
|
||||
uniqueIdentifier: 'test-unique-id',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should call updateFromGitHub when updatePayload is provided', async () => {
|
||||
const updatePayload = createUpdatePayload()
|
||||
render(<Loaded {...defaultProps} updatePayload={updatePayload} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /plugin.installModal.install/i }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdateFromGitHub).toHaveBeenCalledWith(
|
||||
'owner/repo',
|
||||
'v1.0.0',
|
||||
'plugin.zip',
|
||||
'original-id',
|
||||
'test-unique-id',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('should call updateFromGitHub when plugin is already installed', async () => {
|
||||
mockUseCheckInstalled.mockReturnValue({
|
||||
installedInfo: {
|
||||
'test-plugin-id': {
|
||||
installedVersion: '0.9.0',
|
||||
uniqueIdentifier: 'installed-uid',
|
||||
},
|
||||
},
|
||||
isLoading: false,
|
||||
})
|
||||
|
||||
render(<Loaded {...defaultProps} payload={createMockPluginPayload()} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /plugin.installModal.install/i }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdateFromGitHub).toHaveBeenCalledWith(
|
||||
'owner/repo',
|
||||
'v1.0.0',
|
||||
'plugin.zip',
|
||||
'installed-uid',
|
||||
'test-unique-id',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('should call onInstalled when installation completes immediately', async () => {
|
||||
mockInstallPackageFromGitHub.mockResolvedValue({ all_installed: true, task_id: 'task-1' })
|
||||
|
||||
const onInstalled = vi.fn()
|
||||
render(<Loaded {...defaultProps} onInstalled={onInstalled} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /plugin.installModal.install/i }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onInstalled).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it('should check task status when not immediately installed', async () => {
|
||||
mockInstallPackageFromGitHub.mockResolvedValue({ all_installed: false, task_id: 'task-1' })
|
||||
|
||||
render(<Loaded {...defaultProps} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /plugin.installModal.install/i }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockHandleRefetch).toHaveBeenCalled()
|
||||
expect(mockCheck).toHaveBeenCalledWith({
|
||||
taskId: 'task-1',
|
||||
pluginUniqueIdentifier: 'test-unique-id',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should call onInstalled with true when task succeeds', async () => {
|
||||
mockInstallPackageFromGitHub.mockResolvedValue({ all_installed: false, task_id: 'task-1' })
|
||||
mockCheck.mockResolvedValue({ status: TaskStatus.success, error: null })
|
||||
|
||||
const onInstalled = vi.fn()
|
||||
render(<Loaded {...defaultProps} onInstalled={onInstalled} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /plugin.installModal.install/i }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onInstalled).toHaveBeenCalledWith(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Error Handling Tests
|
||||
// ================================
|
||||
describe('Error Handling', () => {
|
||||
it('should call onFailed when task fails', async () => {
|
||||
mockInstallPackageFromGitHub.mockResolvedValue({ all_installed: false, task_id: 'task-1' })
|
||||
mockCheck.mockResolvedValue({ status: TaskStatus.failed, error: 'Installation failed' })
|
||||
|
||||
const onFailed = vi.fn()
|
||||
render(<Loaded {...defaultProps} onFailed={onFailed} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /plugin.installModal.install/i }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onFailed).toHaveBeenCalledWith('Installation failed')
|
||||
})
|
||||
})
|
||||
|
||||
it('should call onFailed with string error', async () => {
|
||||
mockInstallPackageFromGitHub.mockRejectedValue('String error message')
|
||||
|
||||
const onFailed = vi.fn()
|
||||
render(<Loaded {...defaultProps} onFailed={onFailed} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /plugin.installModal.install/i }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onFailed).toHaveBeenCalledWith('String error message')
|
||||
})
|
||||
})
|
||||
|
||||
it('should call onFailed without message for non-string errors', async () => {
|
||||
mockInstallPackageFromGitHub.mockRejectedValue(new Error('Error object'))
|
||||
|
||||
const onFailed = vi.fn()
|
||||
render(<Loaded {...defaultProps} onFailed={onFailed} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /plugin.installModal.install/i }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onFailed).toHaveBeenCalledWith()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Auto-install Effect Tests
|
||||
// ================================
|
||||
describe('Auto-install Effect', () => {
|
||||
it('should call onInstalled when already installed with same identifier', () => {
|
||||
mockUseCheckInstalled.mockReturnValue({
|
||||
installedInfo: {
|
||||
'test-plugin-id': {
|
||||
installedVersion: '1.0.0',
|
||||
uniqueIdentifier: 'test-unique-id',
|
||||
},
|
||||
},
|
||||
isLoading: false,
|
||||
})
|
||||
|
||||
const onInstalled = vi.fn()
|
||||
render(<Loaded {...defaultProps} payload={createMockPluginPayload()} onInstalled={onInstalled} />)
|
||||
|
||||
expect(onInstalled).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not call onInstalled when identifiers differ', () => {
|
||||
mockUseCheckInstalled.mockReturnValue({
|
||||
installedInfo: {
|
||||
'test-plugin-id': {
|
||||
installedVersion: '1.0.0',
|
||||
uniqueIdentifier: 'different-uid',
|
||||
},
|
||||
},
|
||||
isLoading: false,
|
||||
})
|
||||
|
||||
const onInstalled = vi.fn()
|
||||
render(<Loaded {...defaultProps} payload={createMockPluginPayload()} onInstalled={onInstalled} />)
|
||||
|
||||
expect(onInstalled).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Installing State Tests
|
||||
// ================================
|
||||
describe('Installing State', () => {
|
||||
it('should hide back button while installing', async () => {
|
||||
let resolveInstall: (value: { all_installed: boolean, task_id: string }) => void
|
||||
mockInstallPackageFromGitHub.mockImplementation(() => new Promise((resolve) => {
|
||||
resolveInstall = resolve
|
||||
}))
|
||||
|
||||
render(<Loaded {...defaultProps} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /plugin.installModal.install/i }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole('button', { name: 'plugin.installModal.back' })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
resolveInstall!({ all_installed: true, task_id: 'task-1' })
|
||||
})
|
||||
|
||||
it('should show installing text while installing', async () => {
|
||||
let resolveInstall: (value: { all_installed: boolean, task_id: string }) => void
|
||||
mockInstallPackageFromGitHub.mockImplementation(() => new Promise((resolve) => {
|
||||
resolveInstall = resolve
|
||||
}))
|
||||
|
||||
render(<Loaded {...defaultProps} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /plugin.installModal.install/i }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('plugin.installModal.installing')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
resolveInstall!({ all_installed: true, task_id: 'task-1' })
|
||||
})
|
||||
|
||||
it('should not trigger install twice when already installing', async () => {
|
||||
let resolveInstall: (value: { all_installed: boolean, task_id: string }) => void
|
||||
mockInstallPackageFromGitHub.mockImplementation(() => new Promise((resolve) => {
|
||||
resolveInstall = resolve
|
||||
}))
|
||||
|
||||
render(<Loaded {...defaultProps} />)
|
||||
|
||||
const installButton = screen.getByRole('button', { name: /plugin.installModal.install/i })
|
||||
|
||||
// Click twice
|
||||
fireEvent.click(installButton)
|
||||
fireEvent.click(installButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockInstallPackageFromGitHub).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
resolveInstall!({ all_installed: true, task_id: 'task-1' })
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Edge Cases Tests
|
||||
// ================================
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle missing onStartToInstall callback', async () => {
|
||||
render(<Loaded {...defaultProps} onStartToInstall={undefined} />)
|
||||
|
||||
// Should not throw when callback is undefined
|
||||
expect(() => {
|
||||
fireEvent.click(screen.getByRole('button', { name: /plugin.installModal.install/i }))
|
||||
}).not.toThrow()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockInstallPackageFromGitHub).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle plugin without plugin_id', () => {
|
||||
mockUseCheckInstalled.mockReturnValue({
|
||||
installedInfo: {},
|
||||
isLoading: false,
|
||||
})
|
||||
|
||||
render(<Loaded {...defaultProps} payload={createMockPayload()} />)
|
||||
|
||||
expect(mockUseCheckInstalled).toHaveBeenCalledWith({
|
||||
pluginIds: [undefined],
|
||||
enabled: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('should preserve state after component update', () => {
|
||||
const { rerender } = render(<Loaded {...defaultProps} />)
|
||||
|
||||
rerender(<Loaded {...defaultProps} />)
|
||||
|
||||
expect(screen.getByTestId('plugin-card')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,22 +1,22 @@
|
||||
'use client'
|
||||
|
||||
import React, { useEffect } from 'react'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { type Plugin, type PluginDeclaration, TaskStatus, type UpdateFromGitHubPayload } from '../../../types'
|
||||
import Card from '../../../card'
|
||||
import { pluginManifestToCardPluginProps } from '../../utils'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { updateFromGitHub } from '@/service/plugins'
|
||||
import { useInstallPackageFromGitHub } from '@/service/use-plugins'
|
||||
import type { Plugin, PluginDeclaration, UpdateFromGitHubPayload } from '../../../types'
|
||||
import { RiLoader2Line } from '@remixicon/react'
|
||||
import { usePluginTaskList } from '@/service/use-plugins'
|
||||
import checkTaskStatus from '../../base/check-task-status'
|
||||
import * as React from 'react'
|
||||
import { useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import useCheckInstalled from '@/app/components/plugins/install-plugin/hooks/use-check-installed'
|
||||
import { parseGitHubUrl } from '../../utils'
|
||||
import { updateFromGitHub } from '@/service/plugins'
|
||||
import { useInstallPackageFromGitHub, usePluginTaskList } from '@/service/use-plugins'
|
||||
import Card from '../../../card'
|
||||
import { TaskStatus } from '../../../types'
|
||||
import checkTaskStatus from '../../base/check-task-status'
|
||||
import Version from '../../base/version'
|
||||
import { parseGitHubUrl, pluginManifestToCardPluginProps } from '../../utils'
|
||||
|
||||
type LoadedProps = {
|
||||
updatePayload: UpdateFromGitHubPayload
|
||||
updatePayload?: UpdateFromGitHubPayload
|
||||
uniqueIdentifier: string
|
||||
payload: PluginDeclaration | Plugin
|
||||
repoUrl: string
|
||||
@ -28,7 +28,7 @@ type LoadedProps = {
|
||||
onFailed: (message?: string) => void
|
||||
}
|
||||
|
||||
const i18nPrefix = 'plugin.installModal'
|
||||
const i18nPrefix = 'installModal'
|
||||
|
||||
const Loaded: React.FC<LoadedProps> = ({
|
||||
updatePayload,
|
||||
@ -64,7 +64,8 @@ const Loaded: React.FC<LoadedProps> = ({
|
||||
}, [hasInstalled])
|
||||
|
||||
const handleInstall = async () => {
|
||||
if (isInstalling) return
|
||||
if (isInstalling)
|
||||
return
|
||||
setIsInstalling(true)
|
||||
onStartToInstall?.()
|
||||
|
||||
@ -142,34 +143,36 @@ const Loaded: React.FC<LoadedProps> = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='system-md-regular text-text-secondary'>
|
||||
<p>{t(`${i18nPrefix}.readyToInstall`)}</p>
|
||||
<div className="system-md-regular text-text-secondary">
|
||||
<p>{t(`${i18nPrefix}.readyToInstall`, { ns: 'plugin' })}</p>
|
||||
</div>
|
||||
<div className='flex flex-wrap content-start items-start gap-1 self-stretch rounded-2xl bg-background-section-burn p-2'>
|
||||
<div className="flex flex-wrap content-start items-start gap-1 self-stretch rounded-2xl bg-background-section-burn p-2">
|
||||
<Card
|
||||
className='w-full'
|
||||
className="w-full"
|
||||
payload={pluginManifestToCardPluginProps(payload as PluginDeclaration)}
|
||||
titleLeft={!isLoading && <Version
|
||||
hasInstalled={hasInstalled}
|
||||
installedVersion={installedVersion}
|
||||
toInstallVersion={toInstallVersion}
|
||||
/>}
|
||||
titleLeft={!isLoading && (
|
||||
<Version
|
||||
hasInstalled={hasInstalled}
|
||||
installedVersion={installedVersion}
|
||||
toInstallVersion={toInstallVersion}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className='mt-4 flex items-center justify-end gap-2 self-stretch'>
|
||||
<div className="mt-4 flex items-center justify-end gap-2 self-stretch">
|
||||
{!isInstalling && (
|
||||
<Button variant='secondary' className='min-w-[72px]' onClick={onBack}>
|
||||
{t('plugin.installModal.back')}
|
||||
<Button variant="secondary" className="min-w-[72px]" onClick={onBack}>
|
||||
{t('installModal.back', { ns: 'plugin' })}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant='primary'
|
||||
className='flex min-w-[72px] space-x-0.5'
|
||||
variant="primary"
|
||||
className="flex min-w-[72px] space-x-0.5"
|
||||
onClick={handleInstall}
|
||||
disabled={isInstalling || isLoading}
|
||||
>
|
||||
{isInstalling && <RiLoader2Line className='h-4 w-4 animate-spin-slow' />}
|
||||
<span>{t(`${i18nPrefix}.${isInstalling ? 'installing' : 'install'}`)}</span>
|
||||
{isInstalling && <RiLoader2Line className="h-4 w-4 animate-spin-slow" />}
|
||||
<span>{t(`${i18nPrefix}.${isInstalling ? 'installing' : 'install'}`, { ns: 'plugin' })}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@ -0,0 +1,877 @@
|
||||
import type { PluginDeclaration, UpdateFromGitHubPayload } from '../../../types'
|
||||
import type { Item } from '@/app/components/base/select'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { PluginCategoryEnum } from '../../../types'
|
||||
import SelectPackage from './selectPackage'
|
||||
|
||||
// Mock the useGitHubUpload hook
|
||||
const mockHandleUpload = vi.fn()
|
||||
vi.mock('../../hooks', () => ({
|
||||
useGitHubUpload: () => ({ handleUpload: mockHandleUpload }),
|
||||
}))
|
||||
|
||||
// Factory functions
|
||||
const createMockManifest = (): PluginDeclaration => ({
|
||||
plugin_unique_identifier: 'test-uid',
|
||||
version: '1.0.0',
|
||||
author: 'test-author',
|
||||
icon: 'icon.png',
|
||||
name: 'Test Plugin',
|
||||
category: PluginCategoryEnum.tool,
|
||||
label: { 'en-US': 'Test' } as PluginDeclaration['label'],
|
||||
description: { 'en-US': 'Test Description' } as PluginDeclaration['description'],
|
||||
created_at: '2024-01-01',
|
||||
resource: {},
|
||||
plugins: [],
|
||||
verified: true,
|
||||
endpoint: { settings: [], endpoints: [] },
|
||||
model: null,
|
||||
tags: [],
|
||||
agent_strategy: null,
|
||||
meta: { version: '1.0.0' },
|
||||
trigger: {} as PluginDeclaration['trigger'],
|
||||
})
|
||||
|
||||
const createVersions = (): Item[] => [
|
||||
{ value: 'v1.0.0', name: 'v1.0.0' },
|
||||
{ value: 'v0.9.0', name: 'v0.9.0' },
|
||||
]
|
||||
|
||||
const createPackages = (): Item[] => [
|
||||
{ value: 'plugin.zip', name: 'plugin.zip' },
|
||||
{ value: 'plugin.tar.gz', name: 'plugin.tar.gz' },
|
||||
]
|
||||
|
||||
const createUpdatePayload = (): UpdateFromGitHubPayload => ({
|
||||
originalPackageInfo: {
|
||||
id: 'original-id',
|
||||
repo: 'owner/repo',
|
||||
version: 'v0.9.0',
|
||||
package: 'plugin.zip',
|
||||
releases: [],
|
||||
},
|
||||
})
|
||||
|
||||
// Test props type - updatePayload is optional for testing
|
||||
type TestProps = {
|
||||
updatePayload?: UpdateFromGitHubPayload
|
||||
repoUrl?: string
|
||||
selectedVersion?: string
|
||||
versions?: Item[]
|
||||
onSelectVersion?: (item: Item) => void
|
||||
selectedPackage?: string
|
||||
packages?: Item[]
|
||||
onSelectPackage?: (item: Item) => void
|
||||
onUploaded?: (result: { uniqueIdentifier: string, manifest: PluginDeclaration }) => void
|
||||
onFailed?: (errorMsg: string) => void
|
||||
onBack?: () => void
|
||||
}
|
||||
|
||||
describe('SelectPackage', () => {
|
||||
const createDefaultProps = () => ({
|
||||
updatePayload: undefined as UpdateFromGitHubPayload | undefined,
|
||||
repoUrl: 'https://github.com/owner/repo',
|
||||
selectedVersion: '',
|
||||
versions: createVersions(),
|
||||
onSelectVersion: vi.fn() as (item: Item) => void,
|
||||
selectedPackage: '',
|
||||
packages: createPackages(),
|
||||
onSelectPackage: vi.fn() as (item: Item) => void,
|
||||
onUploaded: vi.fn() as (result: { uniqueIdentifier: string, manifest: PluginDeclaration }) => void,
|
||||
onFailed: vi.fn() as (errorMsg: string) => void,
|
||||
onBack: vi.fn() as () => void,
|
||||
})
|
||||
|
||||
// Helper function to render with proper type handling
|
||||
const renderSelectPackage = (overrides: TestProps = {}) => {
|
||||
const props = { ...createDefaultProps(), ...overrides }
|
||||
// Cast to any to bypass strict type checking since component accepts optional updatePayload
|
||||
return render(<SelectPackage {...(props as Parameters<typeof SelectPackage>[0])} />)
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockHandleUpload.mockReset()
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Rendering Tests
|
||||
// ================================
|
||||
describe('Rendering', () => {
|
||||
it('should render version label', () => {
|
||||
renderSelectPackage()
|
||||
|
||||
expect(screen.getByText('plugin.installFromGitHub.selectVersion')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render package label', () => {
|
||||
renderSelectPackage()
|
||||
|
||||
expect(screen.getByText('plugin.installFromGitHub.selectPackage')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render back button when not in edit mode', () => {
|
||||
renderSelectPackage({ updatePayload: undefined })
|
||||
|
||||
expect(screen.getByRole('button', { name: 'plugin.installModal.back' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render back button when in edit mode', () => {
|
||||
renderSelectPackage({ updatePayload: createUpdatePayload() })
|
||||
|
||||
expect(screen.queryByRole('button', { name: 'plugin.installModal.back' })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render next button', () => {
|
||||
renderSelectPackage()
|
||||
|
||||
expect(screen.getByRole('button', { name: 'plugin.installModal.next' })).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Props Tests
|
||||
// ================================
|
||||
describe('Props', () => {
|
||||
it('should pass selectedVersion to PortalSelect', () => {
|
||||
renderSelectPackage({ selectedVersion: 'v1.0.0' })
|
||||
|
||||
// PortalSelect should display the selected version
|
||||
expect(screen.getByText('v1.0.0')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should pass selectedPackage to PortalSelect', () => {
|
||||
renderSelectPackage({ selectedPackage: 'plugin.zip' })
|
||||
|
||||
expect(screen.getByText('plugin.zip')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show installed version badge when updatePayload version differs', () => {
|
||||
renderSelectPackage({
|
||||
updatePayload: createUpdatePayload(),
|
||||
selectedVersion: 'v1.0.0',
|
||||
})
|
||||
|
||||
expect(screen.getByText(/v0\.9\.0\s*->\s*v1\.0\.0/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Button State Tests
|
||||
// ================================
|
||||
describe('Button State', () => {
|
||||
it('should disable next button when no version selected', () => {
|
||||
renderSelectPackage({ selectedVersion: '', selectedPackage: '' })
|
||||
|
||||
expect(screen.getByRole('button', { name: 'plugin.installModal.next' })).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should disable next button when version selected but no package', () => {
|
||||
renderSelectPackage({ selectedVersion: 'v1.0.0', selectedPackage: '' })
|
||||
|
||||
expect(screen.getByRole('button', { name: 'plugin.installModal.next' })).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should enable next button when both version and package selected', () => {
|
||||
renderSelectPackage({ selectedVersion: 'v1.0.0', selectedPackage: 'plugin.zip' })
|
||||
|
||||
expect(screen.getByRole('button', { name: 'plugin.installModal.next' })).not.toBeDisabled()
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// User Interactions Tests
|
||||
// ================================
|
||||
describe('User Interactions', () => {
|
||||
it('should call onBack when back button is clicked', () => {
|
||||
const onBack = vi.fn()
|
||||
renderSelectPackage({ onBack })
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'plugin.installModal.back' }))
|
||||
|
||||
expect(onBack).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should call handleUploadPackage when next button is clicked', async () => {
|
||||
mockHandleUpload.mockImplementation(async (_repo, _version, _package, onSuccess) => {
|
||||
onSuccess({ unique_identifier: 'uid', manifest: createMockManifest() })
|
||||
})
|
||||
|
||||
const onUploaded = vi.fn()
|
||||
renderSelectPackage({
|
||||
selectedVersion: 'v1.0.0',
|
||||
selectedPackage: 'plugin.zip',
|
||||
onUploaded,
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'plugin.installModal.next' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockHandleUpload).toHaveBeenCalledTimes(1)
|
||||
expect(mockHandleUpload).toHaveBeenCalledWith(
|
||||
'owner/repo',
|
||||
'v1.0.0',
|
||||
'plugin.zip',
|
||||
expect.any(Function),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('should not invoke upload when next button is disabled', () => {
|
||||
renderSelectPackage({ selectedVersion: '', selectedPackage: '' })
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'plugin.installModal.next' }))
|
||||
|
||||
expect(mockHandleUpload).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Upload Handling Tests
|
||||
// ================================
|
||||
describe('Upload Handling', () => {
|
||||
it('should call onUploaded with correct data on successful upload', async () => {
|
||||
const mockManifest = createMockManifest()
|
||||
mockHandleUpload.mockImplementation(async (_repo, _version, _package, onSuccess) => {
|
||||
onSuccess({ unique_identifier: 'test-uid', manifest: mockManifest })
|
||||
})
|
||||
|
||||
const onUploaded = vi.fn()
|
||||
renderSelectPackage({
|
||||
selectedVersion: 'v1.0.0',
|
||||
selectedPackage: 'plugin.zip',
|
||||
onUploaded,
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'plugin.installModal.next' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onUploaded).toHaveBeenCalledWith({
|
||||
uniqueIdentifier: 'test-uid',
|
||||
manifest: mockManifest,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should call onFailed with response message on upload error', async () => {
|
||||
mockHandleUpload.mockRejectedValue({ response: { message: 'API Error' } })
|
||||
|
||||
const onFailed = vi.fn()
|
||||
renderSelectPackage({
|
||||
selectedVersion: 'v1.0.0',
|
||||
selectedPackage: 'plugin.zip',
|
||||
onFailed,
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'plugin.installModal.next' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onFailed).toHaveBeenCalledWith('API Error')
|
||||
})
|
||||
})
|
||||
|
||||
it('should call onFailed with default message when no response message', async () => {
|
||||
mockHandleUpload.mockRejectedValue(new Error('Network error'))
|
||||
|
||||
const onFailed = vi.fn()
|
||||
renderSelectPackage({
|
||||
selectedVersion: 'v1.0.0',
|
||||
selectedPackage: 'plugin.zip',
|
||||
onFailed,
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'plugin.installModal.next' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onFailed).toHaveBeenCalledWith('plugin.installFromGitHub.uploadFailed')
|
||||
})
|
||||
})
|
||||
|
||||
it('should not call upload twice when already uploading', async () => {
|
||||
let resolveUpload: (value?: unknown) => void
|
||||
mockHandleUpload.mockImplementation(() => new Promise((resolve) => {
|
||||
resolveUpload = resolve
|
||||
}))
|
||||
|
||||
renderSelectPackage({
|
||||
selectedVersion: 'v1.0.0',
|
||||
selectedPackage: 'plugin.zip',
|
||||
})
|
||||
|
||||
const nextButton = screen.getByRole('button', { name: 'plugin.installModal.next' })
|
||||
|
||||
// Click twice rapidly - this tests the isUploading guard at line 49-50
|
||||
// The first click starts the upload, the second should be ignored
|
||||
fireEvent.click(nextButton)
|
||||
fireEvent.click(nextButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockHandleUpload).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
// Resolve the upload
|
||||
resolveUpload!()
|
||||
})
|
||||
|
||||
it('should disable back button while uploading', async () => {
|
||||
let resolveUpload: (value?: unknown) => void
|
||||
mockHandleUpload.mockImplementation(() => new Promise((resolve) => {
|
||||
resolveUpload = resolve
|
||||
}))
|
||||
|
||||
renderSelectPackage({
|
||||
selectedVersion: 'v1.0.0',
|
||||
selectedPackage: 'plugin.zip',
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'plugin.installModal.next' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: 'plugin.installModal.back' })).toBeDisabled()
|
||||
})
|
||||
|
||||
resolveUpload!()
|
||||
})
|
||||
|
||||
it('should strip github.com prefix from repoUrl', async () => {
|
||||
mockHandleUpload.mockResolvedValue({})
|
||||
|
||||
renderSelectPackage({
|
||||
repoUrl: 'https://github.com/myorg/myrepo',
|
||||
selectedVersion: 'v1.0.0',
|
||||
selectedPackage: 'plugin.zip',
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'plugin.installModal.next' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockHandleUpload).toHaveBeenCalledWith(
|
||||
'myorg/myrepo',
|
||||
expect.any(String),
|
||||
expect.any(String),
|
||||
expect.any(Function),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Edge Cases Tests
|
||||
// ================================
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle empty versions array', () => {
|
||||
renderSelectPackage({ versions: [] })
|
||||
|
||||
expect(screen.getByText('plugin.installFromGitHub.selectVersion')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle empty packages array', () => {
|
||||
renderSelectPackage({ packages: [] })
|
||||
|
||||
expect(screen.getByText('plugin.installFromGitHub.selectPackage')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle updatePayload with installed version', () => {
|
||||
renderSelectPackage({ updatePayload: createUpdatePayload() })
|
||||
|
||||
// Should not show back button in edit mode
|
||||
expect(screen.queryByRole('button', { name: 'plugin.installModal.back' })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should re-enable buttons after upload completes', async () => {
|
||||
mockHandleUpload.mockResolvedValue({})
|
||||
|
||||
renderSelectPackage({
|
||||
selectedVersion: 'v1.0.0',
|
||||
selectedPackage: 'plugin.zip',
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'plugin.installModal.next' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: 'plugin.installModal.back' })).not.toBeDisabled()
|
||||
})
|
||||
})
|
||||
|
||||
it('should re-enable buttons after upload fails', async () => {
|
||||
mockHandleUpload.mockRejectedValue(new Error('Upload failed'))
|
||||
|
||||
renderSelectPackage({
|
||||
selectedVersion: 'v1.0.0',
|
||||
selectedPackage: 'plugin.zip',
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'plugin.installModal.next' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: 'plugin.installModal.back' })).not.toBeDisabled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// PortalSelect Readonly State Tests
|
||||
// ================================
|
||||
describe('PortalSelect Readonly State', () => {
|
||||
it('should make package select readonly when no version selected', () => {
|
||||
renderSelectPackage({ selectedVersion: '' })
|
||||
|
||||
// When no version is selected, package select should be readonly
|
||||
// This is tested by verifying the component renders correctly
|
||||
const trigger = screen.getByText('plugin.installFromGitHub.selectPackagePlaceholder').closest('div')
|
||||
expect(trigger).toHaveClass('cursor-not-allowed')
|
||||
})
|
||||
|
||||
it('should make package select active when version is selected', () => {
|
||||
renderSelectPackage({ selectedVersion: 'v1.0.0' })
|
||||
|
||||
// When version is selected, package select should be active
|
||||
const trigger = screen.getByText('plugin.installFromGitHub.selectPackagePlaceholder').closest('div')
|
||||
expect(trigger).toHaveClass('cursor-pointer')
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// installedValue Props Tests
|
||||
// ================================
|
||||
describe('installedValue Props', () => {
|
||||
it('should pass installedValue when updatePayload is provided', () => {
|
||||
const updatePayload = createUpdatePayload()
|
||||
renderSelectPackage({ updatePayload })
|
||||
|
||||
// The installed version should be passed to PortalSelect
|
||||
// updatePayload.originalPackageInfo.version = 'v0.9.0'
|
||||
expect(screen.getByText('plugin.installFromGitHub.selectVersion')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not pass installedValue when updatePayload is undefined', () => {
|
||||
renderSelectPackage({ updatePayload: undefined })
|
||||
|
||||
// No installed version indicator
|
||||
expect(screen.getByText('plugin.installFromGitHub.selectVersion')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle updatePayload with different version value', () => {
|
||||
const updatePayload = createUpdatePayload()
|
||||
updatePayload.originalPackageInfo.version = 'v2.0.0'
|
||||
renderSelectPackage({ updatePayload })
|
||||
|
||||
// Should render without errors
|
||||
expect(screen.getByText('plugin.installFromGitHub.selectVersion')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show installed badge in version list', () => {
|
||||
const updatePayload = createUpdatePayload()
|
||||
renderSelectPackage({ updatePayload, selectedVersion: '' })
|
||||
|
||||
fireEvent.click(screen.getByText('plugin.installFromGitHub.selectVersionPlaceholder'))
|
||||
|
||||
expect(screen.getByText('INSTALLED')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Next Button Disabled State Combinations
|
||||
// ================================
|
||||
describe('Next Button Disabled State Combinations', () => {
|
||||
it('should disable next button when only version is missing', () => {
|
||||
renderSelectPackage({ selectedVersion: '', selectedPackage: 'plugin.zip' })
|
||||
|
||||
expect(screen.getByRole('button', { name: 'plugin.installModal.next' })).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should disable next button when only package is missing', () => {
|
||||
renderSelectPackage({ selectedVersion: 'v1.0.0', selectedPackage: '' })
|
||||
|
||||
expect(screen.getByRole('button', { name: 'plugin.installModal.next' })).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should disable next button when both are missing', () => {
|
||||
renderSelectPackage({ selectedVersion: '', selectedPackage: '' })
|
||||
|
||||
expect(screen.getByRole('button', { name: 'plugin.installModal.next' })).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should disable next button when uploading even with valid selections', async () => {
|
||||
let resolveUpload: (value?: unknown) => void
|
||||
mockHandleUpload.mockImplementation(() => new Promise((resolve) => {
|
||||
resolveUpload = resolve
|
||||
}))
|
||||
|
||||
renderSelectPackage({
|
||||
selectedVersion: 'v1.0.0',
|
||||
selectedPackage: 'plugin.zip',
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'plugin.installModal.next' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: 'plugin.installModal.next' })).toBeDisabled()
|
||||
})
|
||||
|
||||
resolveUpload!()
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// RepoUrl Format Handling Tests
|
||||
// ================================
|
||||
describe('RepoUrl Format Handling', () => {
|
||||
it('should handle repoUrl without trailing slash', async () => {
|
||||
mockHandleUpload.mockResolvedValue({})
|
||||
|
||||
renderSelectPackage({
|
||||
repoUrl: 'https://github.com/owner/repo',
|
||||
selectedVersion: 'v1.0.0',
|
||||
selectedPackage: 'plugin.zip',
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'plugin.installModal.next' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockHandleUpload).toHaveBeenCalledWith(
|
||||
'owner/repo',
|
||||
'v1.0.0',
|
||||
'plugin.zip',
|
||||
expect.any(Function),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle repoUrl with different org/repo combinations', async () => {
|
||||
mockHandleUpload.mockResolvedValue({})
|
||||
|
||||
renderSelectPackage({
|
||||
repoUrl: 'https://github.com/my-organization/my-plugin-repo',
|
||||
selectedVersion: 'v2.0.0',
|
||||
selectedPackage: 'build.tar.gz',
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'plugin.installModal.next' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockHandleUpload).toHaveBeenCalledWith(
|
||||
'my-organization/my-plugin-repo',
|
||||
'v2.0.0',
|
||||
'build.tar.gz',
|
||||
expect.any(Function),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('should pass through repoUrl without github prefix', async () => {
|
||||
mockHandleUpload.mockResolvedValue({})
|
||||
|
||||
renderSelectPackage({
|
||||
repoUrl: 'plain-org/plain-repo',
|
||||
selectedVersion: 'v1.0.0',
|
||||
selectedPackage: 'plugin.zip',
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'plugin.installModal.next' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockHandleUpload).toHaveBeenCalledWith(
|
||||
'plain-org/plain-repo',
|
||||
'v1.0.0',
|
||||
'plugin.zip',
|
||||
expect.any(Function),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// isEdit Mode Comprehensive Tests
|
||||
// ================================
|
||||
describe('isEdit Mode Comprehensive', () => {
|
||||
it('should set isEdit to true when updatePayload is truthy', () => {
|
||||
const updatePayload = createUpdatePayload()
|
||||
renderSelectPackage({ updatePayload })
|
||||
|
||||
// Back button should not be rendered in edit mode
|
||||
expect(screen.queryByRole('button', { name: 'plugin.installModal.back' })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should set isEdit to false when updatePayload is undefined', () => {
|
||||
renderSelectPackage({ updatePayload: undefined })
|
||||
|
||||
// Back button should be rendered when not in edit mode
|
||||
expect(screen.getByRole('button', { name: 'plugin.installModal.back' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should allow upload in edit mode without back button', async () => {
|
||||
mockHandleUpload.mockImplementation(async (_repo, _version, _package, onSuccess) => {
|
||||
onSuccess({ unique_identifier: 'uid', manifest: createMockManifest() })
|
||||
})
|
||||
|
||||
const onUploaded = vi.fn()
|
||||
renderSelectPackage({
|
||||
updatePayload: createUpdatePayload(),
|
||||
selectedVersion: 'v1.0.0',
|
||||
selectedPackage: 'plugin.zip',
|
||||
onUploaded,
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'plugin.installModal.next' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onUploaded).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Error Response Handling Tests
|
||||
// ================================
|
||||
describe('Error Response Handling', () => {
|
||||
it('should handle error with response.message property', async () => {
|
||||
mockHandleUpload.mockRejectedValue({ response: { message: 'Custom API Error' } })
|
||||
|
||||
const onFailed = vi.fn()
|
||||
renderSelectPackage({
|
||||
selectedVersion: 'v1.0.0',
|
||||
selectedPackage: 'plugin.zip',
|
||||
onFailed,
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'plugin.installModal.next' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onFailed).toHaveBeenCalledWith('Custom API Error')
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle error with empty response object', async () => {
|
||||
mockHandleUpload.mockRejectedValue({ response: {} })
|
||||
|
||||
const onFailed = vi.fn()
|
||||
renderSelectPackage({
|
||||
selectedVersion: 'v1.0.0',
|
||||
selectedPackage: 'plugin.zip',
|
||||
onFailed,
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'plugin.installModal.next' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onFailed).toHaveBeenCalledWith('plugin.installFromGitHub.uploadFailed')
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle error without response property', async () => {
|
||||
mockHandleUpload.mockRejectedValue({ code: 'NETWORK_ERROR' })
|
||||
|
||||
const onFailed = vi.fn()
|
||||
renderSelectPackage({
|
||||
selectedVersion: 'v1.0.0',
|
||||
selectedPackage: 'plugin.zip',
|
||||
onFailed,
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'plugin.installModal.next' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onFailed).toHaveBeenCalledWith('plugin.installFromGitHub.uploadFailed')
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle error with response but no message', async () => {
|
||||
mockHandleUpload.mockRejectedValue({ response: { status: 500 } })
|
||||
|
||||
const onFailed = vi.fn()
|
||||
renderSelectPackage({
|
||||
selectedVersion: 'v1.0.0',
|
||||
selectedPackage: 'plugin.zip',
|
||||
onFailed,
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'plugin.installModal.next' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onFailed).toHaveBeenCalledWith('plugin.installFromGitHub.uploadFailed')
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle string error', async () => {
|
||||
mockHandleUpload.mockRejectedValue('String error message')
|
||||
|
||||
const onFailed = vi.fn()
|
||||
renderSelectPackage({
|
||||
selectedVersion: 'v1.0.0',
|
||||
selectedPackage: 'plugin.zip',
|
||||
onFailed,
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'plugin.installModal.next' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onFailed).toHaveBeenCalledWith('plugin.installFromGitHub.uploadFailed')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Callback Props Tests
|
||||
// ================================
|
||||
describe('Callback Props', () => {
|
||||
it('should pass onSelectVersion to PortalSelect', () => {
|
||||
const onSelectVersion = vi.fn()
|
||||
renderSelectPackage({ onSelectVersion })
|
||||
|
||||
// The callback is passed to PortalSelect, which is a base component
|
||||
// We verify it's rendered correctly
|
||||
expect(screen.getByText('plugin.installFromGitHub.selectVersion')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should pass onSelectPackage to PortalSelect', () => {
|
||||
const onSelectPackage = vi.fn()
|
||||
renderSelectPackage({ onSelectPackage })
|
||||
|
||||
// The callback is passed to PortalSelect, which is a base component
|
||||
expect(screen.getByText('plugin.installFromGitHub.selectPackage')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Upload State Management Tests
|
||||
// ================================
|
||||
describe('Upload State Management', () => {
|
||||
it('should set isUploading to true when upload starts', async () => {
|
||||
let resolveUpload: (value?: unknown) => void
|
||||
mockHandleUpload.mockImplementation(() => new Promise((resolve) => {
|
||||
resolveUpload = resolve
|
||||
}))
|
||||
|
||||
renderSelectPackage({
|
||||
selectedVersion: 'v1.0.0',
|
||||
selectedPackage: 'plugin.zip',
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'plugin.installModal.next' }))
|
||||
|
||||
// Both buttons should be disabled during upload
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: 'plugin.installModal.next' })).toBeDisabled()
|
||||
expect(screen.getByRole('button', { name: 'plugin.installModal.back' })).toBeDisabled()
|
||||
})
|
||||
|
||||
resolveUpload!()
|
||||
})
|
||||
|
||||
it('should set isUploading to false after successful upload', async () => {
|
||||
mockHandleUpload.mockImplementation(async (_repo, _version, _package, onSuccess) => {
|
||||
onSuccess({ unique_identifier: 'uid', manifest: createMockManifest() })
|
||||
})
|
||||
|
||||
renderSelectPackage({
|
||||
selectedVersion: 'v1.0.0',
|
||||
selectedPackage: 'plugin.zip',
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'plugin.installModal.next' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: 'plugin.installModal.next' })).not.toBeDisabled()
|
||||
expect(screen.getByRole('button', { name: 'plugin.installModal.back' })).not.toBeDisabled()
|
||||
})
|
||||
})
|
||||
|
||||
it('should set isUploading to false after failed upload', async () => {
|
||||
mockHandleUpload.mockRejectedValue(new Error('Upload failed'))
|
||||
|
||||
renderSelectPackage({
|
||||
selectedVersion: 'v1.0.0',
|
||||
selectedPackage: 'plugin.zip',
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'plugin.installModal.next' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: 'plugin.installModal.next' })).not.toBeDisabled()
|
||||
expect(screen.getByRole('button', { name: 'plugin.installModal.back' })).not.toBeDisabled()
|
||||
})
|
||||
})
|
||||
|
||||
it('should not allow back button click while uploading', async () => {
|
||||
let resolveUpload: (value?: unknown) => void
|
||||
mockHandleUpload.mockImplementation(() => new Promise((resolve) => {
|
||||
resolveUpload = resolve
|
||||
}))
|
||||
|
||||
const onBack = vi.fn()
|
||||
renderSelectPackage({
|
||||
selectedVersion: 'v1.0.0',
|
||||
selectedPackage: 'plugin.zip',
|
||||
onBack,
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'plugin.installModal.next' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: 'plugin.installModal.back' })).toBeDisabled()
|
||||
})
|
||||
|
||||
// Try to click back button while disabled
|
||||
fireEvent.click(screen.getByRole('button', { name: 'plugin.installModal.back' }))
|
||||
|
||||
// onBack should not be called
|
||||
expect(onBack).not.toHaveBeenCalled()
|
||||
|
||||
resolveUpload!()
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// handleUpload Callback Tests
|
||||
// ================================
|
||||
describe('handleUpload Callback', () => {
|
||||
it('should invoke onSuccess callback with correct data structure', async () => {
|
||||
const mockManifest = createMockManifest()
|
||||
mockHandleUpload.mockImplementation(async (_repo, _version, _package, onSuccess) => {
|
||||
onSuccess({
|
||||
unique_identifier: 'test-unique-identifier',
|
||||
manifest: mockManifest,
|
||||
})
|
||||
})
|
||||
|
||||
const onUploaded = vi.fn()
|
||||
renderSelectPackage({
|
||||
selectedVersion: 'v1.0.0',
|
||||
selectedPackage: 'plugin.zip',
|
||||
onUploaded,
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'plugin.installModal.next' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onUploaded).toHaveBeenCalledWith({
|
||||
uniqueIdentifier: 'test-unique-identifier',
|
||||
manifest: mockManifest,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should pass correct repo, version, and package to handleUpload', async () => {
|
||||
mockHandleUpload.mockResolvedValue({})
|
||||
|
||||
renderSelectPackage({
|
||||
repoUrl: 'https://github.com/test-org/test-repo',
|
||||
selectedVersion: 'v3.0.0',
|
||||
selectedPackage: 'release.zip',
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'plugin.installModal.next' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockHandleUpload).toHaveBeenCalledWith(
|
||||
'test-org/test-repo',
|
||||
'v3.0.0',
|
||||
'release.zip',
|
||||
expect.any(Function),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,14 +1,14 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import type { Item } from '@/app/components/base/select'
|
||||
import { PortalSelect } from '@/app/components/base/select'
|
||||
import Button from '@/app/components/base/button'
|
||||
import type { PluginDeclaration, UpdateFromGitHubPayload } from '../../../types'
|
||||
import type { Item } from '@/app/components/base/select'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { PortalSelect } from '@/app/components/base/select'
|
||||
import { useGitHubUpload } from '../../hooks'
|
||||
|
||||
const i18nPrefix = 'plugin.installFromGitHub'
|
||||
const i18nPrefix = 'installFromGitHub'
|
||||
|
||||
type SelectPackageProps = {
|
||||
updatePayload: UpdateFromGitHubPayload
|
||||
@ -46,7 +46,8 @@ const SelectPackage: React.FC<SelectPackageProps> = ({
|
||||
const { handleUpload } = useGitHubUpload()
|
||||
|
||||
const handleUploadPackage = async () => {
|
||||
if (isUploading) return
|
||||
if (isUploading)
|
||||
return
|
||||
setIsUploading(true)
|
||||
try {
|
||||
const repo = repoUrl.replace('https://github.com/', '')
|
||||
@ -61,7 +62,7 @@ const SelectPackage: React.FC<SelectPackageProps> = ({
|
||||
if (e.response?.message)
|
||||
onFailed(e.response?.message)
|
||||
else
|
||||
onFailed(t(`${i18nPrefix}.uploadFailed`))
|
||||
onFailed(t(`${i18nPrefix}.uploadFailed`, { ns: 'plugin' }))
|
||||
}
|
||||
finally {
|
||||
setIsUploading(false)
|
||||
@ -71,53 +72,54 @@ const SelectPackage: React.FC<SelectPackageProps> = ({
|
||||
return (
|
||||
<>
|
||||
<label
|
||||
htmlFor='version'
|
||||
className='flex flex-col items-start justify-center self-stretch text-text-secondary'
|
||||
htmlFor="version"
|
||||
className="flex flex-col items-start justify-center self-stretch text-text-secondary"
|
||||
>
|
||||
<span className='system-sm-semibold'>{t(`${i18nPrefix}.selectVersion`)}</span>
|
||||
<span className="system-sm-semibold">{t(`${i18nPrefix}.selectVersion`, { ns: 'plugin' })}</span>
|
||||
</label>
|
||||
<PortalSelect
|
||||
value={selectedVersion}
|
||||
onSelect={onSelectVersion}
|
||||
items={versions}
|
||||
installedValue={updatePayload?.originalPackageInfo.version}
|
||||
placeholder={t(`${i18nPrefix}.selectVersionPlaceholder`) || ''}
|
||||
popupClassName='w-[512px] z-[1001]'
|
||||
triggerClassName='text-components-input-text-filled'
|
||||
placeholder={t(`${i18nPrefix}.selectVersionPlaceholder`, { ns: 'plugin' }) || ''}
|
||||
popupClassName="w-[512px] z-[1001]"
|
||||
triggerClassName="text-components-input-text-filled"
|
||||
/>
|
||||
<label
|
||||
htmlFor='package'
|
||||
className='flex flex-col items-start justify-center self-stretch text-text-secondary'
|
||||
htmlFor="package"
|
||||
className="flex flex-col items-start justify-center self-stretch text-text-secondary"
|
||||
>
|
||||
<span className='system-sm-semibold'>{t(`${i18nPrefix}.selectPackage`)}</span>
|
||||
<span className="system-sm-semibold">{t(`${i18nPrefix}.selectPackage`, { ns: 'plugin' })}</span>
|
||||
</label>
|
||||
<PortalSelect
|
||||
value={selectedPackage}
|
||||
onSelect={onSelectPackage}
|
||||
items={packages}
|
||||
readonly={!selectedVersion}
|
||||
placeholder={t(`${i18nPrefix}.selectPackagePlaceholder`) || ''}
|
||||
popupClassName='w-[512px] z-[1001]'
|
||||
triggerClassName='text-components-input-text-filled'
|
||||
placeholder={t(`${i18nPrefix}.selectPackagePlaceholder`, { ns: 'plugin' }) || ''}
|
||||
popupClassName="w-[512px] z-[1001]"
|
||||
triggerClassName="text-components-input-text-filled"
|
||||
/>
|
||||
<div className='mt-4 flex items-center justify-end gap-2 self-stretch'>
|
||||
<div className="mt-4 flex items-center justify-end gap-2 self-stretch">
|
||||
{!isEdit
|
||||
&& <Button
|
||||
variant='secondary'
|
||||
className='min-w-[72px]'
|
||||
onClick={onBack}
|
||||
disabled={isUploading}
|
||||
>
|
||||
{t('plugin.installModal.back')}
|
||||
</Button>
|
||||
}
|
||||
&& (
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="min-w-[72px]"
|
||||
onClick={onBack}
|
||||
disabled={isUploading}
|
||||
>
|
||||
{t('installModal.back', { ns: 'plugin' })}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant='primary'
|
||||
className='min-w-[72px]'
|
||||
variant="primary"
|
||||
className="min-w-[72px]"
|
||||
onClick={handleUploadPackage}
|
||||
disabled={!selectedVersion || !selectedPackage || isUploading}
|
||||
>
|
||||
{t('plugin.installModal.next')}
|
||||
{t('installModal.next', { ns: 'plugin' })}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@ -0,0 +1,180 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import SetURL from './setURL'
|
||||
|
||||
describe('SetURL', () => {
|
||||
const defaultProps = {
|
||||
repoUrl: '',
|
||||
onChange: vi.fn(),
|
||||
onNext: vi.fn(),
|
||||
onCancel: vi.fn(),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Rendering Tests
|
||||
// ================================
|
||||
describe('Rendering', () => {
|
||||
it('should render label with GitHub repo text', () => {
|
||||
render(<SetURL {...defaultProps} />)
|
||||
|
||||
expect(screen.getByText('plugin.installFromGitHub.gitHubRepo')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render input field with correct attributes', () => {
|
||||
render(<SetURL {...defaultProps} />)
|
||||
|
||||
const input = screen.getByRole('textbox')
|
||||
expect(input).toBeInTheDocument()
|
||||
expect(input).toHaveAttribute('type', 'url')
|
||||
expect(input).toHaveAttribute('id', 'repoUrl')
|
||||
expect(input).toHaveAttribute('name', 'repoUrl')
|
||||
expect(input).toHaveAttribute('placeholder', 'Please enter GitHub repo URL')
|
||||
})
|
||||
|
||||
it('should render cancel button', () => {
|
||||
render(<SetURL {...defaultProps} />)
|
||||
|
||||
expect(screen.getByRole('button', { name: 'plugin.installModal.cancel' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render next button', () => {
|
||||
render(<SetURL {...defaultProps} />)
|
||||
|
||||
expect(screen.getByRole('button', { name: 'plugin.installModal.next' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should associate label with input field', () => {
|
||||
render(<SetURL {...defaultProps} />)
|
||||
|
||||
const input = screen.getByLabelText('plugin.installFromGitHub.gitHubRepo')
|
||||
expect(input).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Props Tests
|
||||
// ================================
|
||||
describe('Props', () => {
|
||||
it('should display repoUrl value in input', () => {
|
||||
render(<SetURL {...defaultProps} repoUrl="https://github.com/test/repo" />)
|
||||
|
||||
expect(screen.getByRole('textbox')).toHaveValue('https://github.com/test/repo')
|
||||
})
|
||||
|
||||
it('should display empty string when repoUrl is empty', () => {
|
||||
render(<SetURL {...defaultProps} repoUrl="" />)
|
||||
|
||||
expect(screen.getByRole('textbox')).toHaveValue('')
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// User Interactions Tests
|
||||
// ================================
|
||||
describe('User Interactions', () => {
|
||||
it('should call onChange when input value changes', () => {
|
||||
const onChange = vi.fn()
|
||||
render(<SetURL {...defaultProps} onChange={onChange} />)
|
||||
|
||||
const input = screen.getByRole('textbox')
|
||||
fireEvent.change(input, { target: { value: 'https://github.com/owner/repo' } })
|
||||
|
||||
expect(onChange).toHaveBeenCalledTimes(1)
|
||||
expect(onChange).toHaveBeenCalledWith('https://github.com/owner/repo')
|
||||
})
|
||||
|
||||
it('should call onCancel when cancel button is clicked', () => {
|
||||
const onCancel = vi.fn()
|
||||
render(<SetURL {...defaultProps} onCancel={onCancel} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'plugin.installModal.cancel' }))
|
||||
|
||||
expect(onCancel).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should call onNext when next button is clicked', () => {
|
||||
const onNext = vi.fn()
|
||||
render(<SetURL {...defaultProps} repoUrl="https://github.com/test/repo" onNext={onNext} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'plugin.installModal.next' }))
|
||||
|
||||
expect(onNext).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Button State Tests
|
||||
// ================================
|
||||
describe('Button State', () => {
|
||||
it('should disable next button when repoUrl is empty', () => {
|
||||
render(<SetURL {...defaultProps} repoUrl="" />)
|
||||
|
||||
expect(screen.getByRole('button', { name: 'plugin.installModal.next' })).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should disable next button when repoUrl is only whitespace', () => {
|
||||
render(<SetURL {...defaultProps} repoUrl=" " />)
|
||||
|
||||
expect(screen.getByRole('button', { name: 'plugin.installModal.next' })).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should enable next button when repoUrl has content', () => {
|
||||
render(<SetURL {...defaultProps} repoUrl="https://github.com/test/repo" />)
|
||||
|
||||
expect(screen.getByRole('button', { name: 'plugin.installModal.next' })).not.toBeDisabled()
|
||||
})
|
||||
|
||||
it('should not disable cancel button regardless of repoUrl', () => {
|
||||
render(<SetURL {...defaultProps} repoUrl="" />)
|
||||
|
||||
expect(screen.getByRole('button', { name: 'plugin.installModal.cancel' })).not.toBeDisabled()
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Edge Cases Tests
|
||||
// ================================
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle URL with special characters', () => {
|
||||
const onChange = vi.fn()
|
||||
render(<SetURL {...defaultProps} onChange={onChange} />)
|
||||
|
||||
const input = screen.getByRole('textbox')
|
||||
fireEvent.change(input, { target: { value: 'https://github.com/test-org/repo_name-123' } })
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith('https://github.com/test-org/repo_name-123')
|
||||
})
|
||||
|
||||
it('should handle very long URLs', () => {
|
||||
const longUrl = `https://github.com/${'a'.repeat(100)}/${'b'.repeat(100)}`
|
||||
render(<SetURL {...defaultProps} repoUrl={longUrl} />)
|
||||
|
||||
expect(screen.getByRole('textbox')).toHaveValue(longUrl)
|
||||
})
|
||||
|
||||
it('should handle onChange with empty string', () => {
|
||||
const onChange = vi.fn()
|
||||
render(<SetURL {...defaultProps} repoUrl="some-value" onChange={onChange} />)
|
||||
|
||||
const input = screen.getByRole('textbox')
|
||||
fireEvent.change(input, { target: { value: '' } })
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith('')
|
||||
})
|
||||
|
||||
it('should preserve callback references on rerender', () => {
|
||||
const onNext = vi.fn()
|
||||
const { rerender } = render(<SetURL {...defaultProps} repoUrl="https://github.com/a/b" onNext={onNext} />)
|
||||
|
||||
rerender(<SetURL {...defaultProps} repoUrl="https://github.com/a/b" onNext={onNext} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'plugin.installModal.next' }))
|
||||
|
||||
expect(onNext).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,8 +1,8 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import Button from '@/app/components/base/button'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
|
||||
type SetURLProps = {
|
||||
repoUrl: string
|
||||
@ -16,37 +16,37 @@ const SetURL: React.FC<SetURLProps> = ({ repoUrl, onChange, onNext, onCancel })
|
||||
return (
|
||||
<>
|
||||
<label
|
||||
htmlFor='repoUrl'
|
||||
className='flex flex-col items-start justify-center self-stretch text-text-secondary'
|
||||
htmlFor="repoUrl"
|
||||
className="flex flex-col items-start justify-center self-stretch text-text-secondary"
|
||||
>
|
||||
<span className='system-sm-semibold'>{t('plugin.installFromGitHub.gitHubRepo')}</span>
|
||||
<span className="system-sm-semibold">{t('installFromGitHub.gitHubRepo', { ns: 'plugin' })}</span>
|
||||
</label>
|
||||
<input
|
||||
type='url'
|
||||
id='repoUrl'
|
||||
name='repoUrl'
|
||||
type="url"
|
||||
id="repoUrl"
|
||||
name="repoUrl"
|
||||
value={repoUrl}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
className='shadows-shadow-xs system-sm-regular flex grow items-center gap-[2px]
|
||||
className="shadows-shadow-xs system-sm-regular flex grow items-center gap-[2px]
|
||||
self-stretch overflow-hidden text-ellipsis rounded-lg border border-components-input-border-active
|
||||
bg-components-input-bg-active p-2 text-components-input-text-filled'
|
||||
placeholder='Please enter GitHub repo URL'
|
||||
bg-components-input-bg-active p-2 text-components-input-text-filled"
|
||||
placeholder="Please enter GitHub repo URL"
|
||||
/>
|
||||
<div className='mt-4 flex items-center justify-end gap-2 self-stretch'>
|
||||
<div className="mt-4 flex items-center justify-end gap-2 self-stretch">
|
||||
<Button
|
||||
variant='secondary'
|
||||
className='min-w-[72px]'
|
||||
variant="secondary"
|
||||
className="min-w-[72px]"
|
||||
onClick={onCancel}
|
||||
>
|
||||
{t('plugin.installModal.cancel')}
|
||||
{t('installModal.cancel', { ns: 'plugin' })}
|
||||
</Button>
|
||||
<Button
|
||||
variant='primary'
|
||||
className='min-w-[72px]'
|
||||
variant="primary"
|
||||
className="min-w-[72px]"
|
||||
onClick={onNext}
|
||||
disabled={!repoUrl.trim()}
|
||||
>
|
||||
{t('plugin.installModal.next')}
|
||||
{t('installModal.next', { ns: 'plugin' })}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,18 +1,19 @@
|
||||
'use client'
|
||||
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import type { Dependency, PluginDeclaration } from '../../types'
|
||||
import { InstallStep } from '../../types'
|
||||
import Uploading from './steps/uploading'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import useGetIcon from '@/app/components/plugins/install-plugin/base/use-get-icon'
|
||||
import ReadyToInstallPackage from './ready-to-install'
|
||||
import ReadyToInstallBundle from '../install-bundle/ready-to-install'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { InstallStep } from '../../types'
|
||||
import useHideLogic from '../hooks/use-hide-logic'
|
||||
import cn from '@/utils/classnames'
|
||||
import ReadyToInstallBundle from '../install-bundle/ready-to-install'
|
||||
import ReadyToInstallPackage from './ready-to-install'
|
||||
import Uploading from './steps/uploading'
|
||||
|
||||
const i18nPrefix = 'plugin.installModal'
|
||||
const i18nPrefix = 'installModal'
|
||||
|
||||
type InstallFromLocalPackageProps = {
|
||||
file: File
|
||||
@ -42,15 +43,15 @@ const InstallFromLocalPackage: React.FC<InstallFromLocalPackageProps> = ({
|
||||
|
||||
const getTitle = useCallback(() => {
|
||||
if (step === InstallStep.uploadFailed)
|
||||
return t(`${i18nPrefix}.uploadFailed`)
|
||||
return t(`${i18nPrefix}.uploadFailed`, { ns: 'plugin' })
|
||||
if (isBundle && step === InstallStep.installed)
|
||||
return t(`${i18nPrefix}.installComplete`)
|
||||
return t(`${i18nPrefix}.installComplete`, { ns: 'plugin' })
|
||||
if (step === InstallStep.installed)
|
||||
return t(`${i18nPrefix}.installedSuccessfully`)
|
||||
return t(`${i18nPrefix}.installedSuccessfully`, { ns: 'plugin' })
|
||||
if (step === InstallStep.installFailed)
|
||||
return t(`${i18nPrefix}.installFailed`)
|
||||
return t(`${i18nPrefix}.installFailed`, { ns: 'plugin' })
|
||||
|
||||
return t(`${i18nPrefix}.installPlugin`)
|
||||
return t(`${i18nPrefix}.installPlugin`, { ns: 'plugin' })
|
||||
}, [isBundle, step, t])
|
||||
|
||||
const { getIconUrl } = useGetIcon()
|
||||
@ -91,8 +92,8 @@ const InstallFromLocalPackage: React.FC<InstallFromLocalPackageProps> = ({
|
||||
className={cn(modalClassName, 'shadows-shadow-xl flex min-w-[560px] flex-col items-start rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-0')}
|
||||
closable
|
||||
>
|
||||
<div className='flex items-start gap-2 self-stretch pb-3 pl-6 pr-14 pt-6'>
|
||||
<div className='title-2xl-semi-bold self-stretch text-text-primary'>
|
||||
<div className="flex items-start gap-2 self-stretch pb-3 pl-6 pr-14 pt-6">
|
||||
<div className="title-2xl-semi-bold self-stretch text-text-primary">
|
||||
{getTitle()}
|
||||
</div>
|
||||
</div>
|
||||
@ -106,28 +107,30 @@ const InstallFromLocalPackage: React.FC<InstallFromLocalPackageProps> = ({
|
||||
onFailed={handleUploadFail}
|
||||
/>
|
||||
)}
|
||||
{isBundle ? (
|
||||
<ReadyToInstallBundle
|
||||
step={step}
|
||||
onStepChange={setStep}
|
||||
onStartToInstall={handleStartToInstall}
|
||||
setIsInstalling={setIsInstalling}
|
||||
onClose={onClose}
|
||||
allPlugins={dependencies}
|
||||
/>
|
||||
) : (
|
||||
<ReadyToInstallPackage
|
||||
step={step}
|
||||
onStepChange={setStep}
|
||||
onStartToInstall={handleStartToInstall}
|
||||
setIsInstalling={setIsInstalling}
|
||||
onClose={onClose}
|
||||
uniqueIdentifier={uniqueIdentifier}
|
||||
manifest={manifest}
|
||||
errorMsg={errorMsg}
|
||||
onError={setErrorMsg}
|
||||
/>
|
||||
)}
|
||||
{isBundle
|
||||
? (
|
||||
<ReadyToInstallBundle
|
||||
step={step}
|
||||
onStepChange={setStep}
|
||||
onStartToInstall={handleStartToInstall}
|
||||
setIsInstalling={setIsInstalling}
|
||||
onClose={onClose}
|
||||
allPlugins={dependencies}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<ReadyToInstallPackage
|
||||
step={step}
|
||||
onStepChange={setStep}
|
||||
onStartToInstall={handleStartToInstall}
|
||||
setIsInstalling={setIsInstalling}
|
||||
onClose={onClose}
|
||||
uniqueIdentifier={uniqueIdentifier}
|
||||
manifest={manifest}
|
||||
errorMsg={errorMsg}
|
||||
onError={setErrorMsg}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
@ -0,0 +1,471 @@
|
||||
import type { PluginDeclaration } from '../../types'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { InstallStep, PluginCategoryEnum } from '../../types'
|
||||
import ReadyToInstall from './ready-to-install'
|
||||
|
||||
// Factory function for test data
|
||||
const createMockManifest = (overrides: Partial<PluginDeclaration> = {}): PluginDeclaration => ({
|
||||
plugin_unique_identifier: 'test-plugin-uid',
|
||||
version: '1.0.0',
|
||||
author: 'test-author',
|
||||
icon: 'test-icon.png',
|
||||
name: 'Test Plugin',
|
||||
category: PluginCategoryEnum.tool,
|
||||
label: { 'en-US': 'Test Plugin' } as PluginDeclaration['label'],
|
||||
description: { 'en-US': 'A test plugin' } as PluginDeclaration['description'],
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
resource: {},
|
||||
plugins: [],
|
||||
verified: true,
|
||||
endpoint: { settings: [], endpoints: [] },
|
||||
model: null,
|
||||
tags: [],
|
||||
agent_strategy: null,
|
||||
meta: { version: '1.0.0' },
|
||||
trigger: {} as PluginDeclaration['trigger'],
|
||||
...overrides,
|
||||
})
|
||||
|
||||
// Mock external dependencies
|
||||
const mockRefreshPluginList = vi.fn()
|
||||
vi.mock('../hooks/use-refresh-plugin-list', () => ({
|
||||
default: () => ({
|
||||
refreshPluginList: mockRefreshPluginList,
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock Install component
|
||||
let _installOnInstalled: ((notRefresh?: boolean) => void) | null = null
|
||||
let _installOnFailed: ((message?: string) => void) | null = null
|
||||
let _installOnCancel: (() => void) | null = null
|
||||
let _installOnStartToInstall: (() => void) | null = null
|
||||
|
||||
vi.mock('./steps/install', () => ({
|
||||
default: ({
|
||||
uniqueIdentifier,
|
||||
payload,
|
||||
onCancel,
|
||||
onStartToInstall,
|
||||
onInstalled,
|
||||
onFailed,
|
||||
}: {
|
||||
uniqueIdentifier: string
|
||||
payload: PluginDeclaration
|
||||
onCancel: () => void
|
||||
onStartToInstall?: () => void
|
||||
onInstalled: (notRefresh?: boolean) => void
|
||||
onFailed: (message?: string) => void
|
||||
}) => {
|
||||
_installOnInstalled = onInstalled
|
||||
_installOnFailed = onFailed
|
||||
_installOnCancel = onCancel
|
||||
_installOnStartToInstall = onStartToInstall ?? null
|
||||
return (
|
||||
<div data-testid="install-step">
|
||||
<span data-testid="install-uid">{uniqueIdentifier}</span>
|
||||
<span data-testid="install-payload-name">{payload.name}</span>
|
||||
<button data-testid="install-cancel-btn" onClick={onCancel}>Cancel</button>
|
||||
<button data-testid="install-start-btn" onClick={() => onStartToInstall?.()}>
|
||||
Start Install
|
||||
</button>
|
||||
<button data-testid="install-installed-btn" onClick={() => onInstalled()}>
|
||||
Installed
|
||||
</button>
|
||||
<button data-testid="install-installed-no-refresh-btn" onClick={() => onInstalled(true)}>
|
||||
Installed (No Refresh)
|
||||
</button>
|
||||
<button data-testid="install-failed-btn" onClick={() => onFailed()}>
|
||||
Failed
|
||||
</button>
|
||||
<button data-testid="install-failed-msg-btn" onClick={() => onFailed('Error message')}>
|
||||
Failed with Message
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock Installed component
|
||||
vi.mock('../base/installed', () => ({
|
||||
default: ({
|
||||
payload,
|
||||
isFailed,
|
||||
errMsg,
|
||||
onCancel,
|
||||
}: {
|
||||
payload: PluginDeclaration | null
|
||||
isFailed: boolean
|
||||
errMsg: string | null
|
||||
onCancel: () => void
|
||||
}) => (
|
||||
<div data-testid="installed-step">
|
||||
<span data-testid="installed-payload-name">{payload?.name || 'null'}</span>
|
||||
<span data-testid="installed-is-failed">{isFailed ? 'true' : 'false'}</span>
|
||||
<span data-testid="installed-err-msg">{errMsg || 'null'}</span>
|
||||
<button data-testid="installed-cancel-btn" onClick={onCancel}>Close</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
describe('ReadyToInstall', () => {
|
||||
const defaultProps = {
|
||||
step: InstallStep.readyToInstall,
|
||||
onStepChange: vi.fn(),
|
||||
onStartToInstall: vi.fn(),
|
||||
setIsInstalling: vi.fn(),
|
||||
onClose: vi.fn(),
|
||||
uniqueIdentifier: 'test-unique-identifier',
|
||||
manifest: createMockManifest(),
|
||||
errorMsg: null as string | null,
|
||||
onError: vi.fn(),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
_installOnInstalled = null
|
||||
_installOnFailed = null
|
||||
_installOnCancel = null
|
||||
_installOnStartToInstall = null
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Rendering Tests
|
||||
// ================================
|
||||
describe('Rendering', () => {
|
||||
it('should render Install component when step is readyToInstall', () => {
|
||||
render(<ReadyToInstall {...defaultProps} step={InstallStep.readyToInstall} />)
|
||||
|
||||
expect(screen.getByTestId('install-step')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('installed-step')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render Installed component when step is uploadFailed', () => {
|
||||
render(<ReadyToInstall {...defaultProps} step={InstallStep.uploadFailed} />)
|
||||
|
||||
expect(screen.queryByTestId('install-step')).not.toBeInTheDocument()
|
||||
expect(screen.getByTestId('installed-step')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render Installed component when step is installed', () => {
|
||||
render(<ReadyToInstall {...defaultProps} step={InstallStep.installed} />)
|
||||
|
||||
expect(screen.queryByTestId('install-step')).not.toBeInTheDocument()
|
||||
expect(screen.getByTestId('installed-step')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render Installed component when step is installFailed', () => {
|
||||
render(<ReadyToInstall {...defaultProps} step={InstallStep.installFailed} />)
|
||||
|
||||
expect(screen.queryByTestId('install-step')).not.toBeInTheDocument()
|
||||
expect(screen.getByTestId('installed-step')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Props Passing Tests
|
||||
// ================================
|
||||
describe('Props Passing', () => {
|
||||
it('should pass uniqueIdentifier to Install component', () => {
|
||||
render(<ReadyToInstall {...defaultProps} uniqueIdentifier="custom-uid" />)
|
||||
|
||||
expect(screen.getByTestId('install-uid')).toHaveTextContent('custom-uid')
|
||||
})
|
||||
|
||||
it('should pass manifest to Install component', () => {
|
||||
const manifest = createMockManifest({ name: 'Custom Plugin' })
|
||||
render(<ReadyToInstall {...defaultProps} manifest={manifest} />)
|
||||
|
||||
expect(screen.getByTestId('install-payload-name')).toHaveTextContent('Custom Plugin')
|
||||
})
|
||||
|
||||
it('should pass manifest to Installed component', () => {
|
||||
const manifest = createMockManifest({ name: 'Installed Plugin' })
|
||||
render(<ReadyToInstall {...defaultProps} step={InstallStep.installed} manifest={manifest} />)
|
||||
|
||||
expect(screen.getByTestId('installed-payload-name')).toHaveTextContent('Installed Plugin')
|
||||
})
|
||||
|
||||
it('should pass errorMsg to Installed component', () => {
|
||||
render(
|
||||
<ReadyToInstall
|
||||
{...defaultProps}
|
||||
step={InstallStep.installFailed}
|
||||
errorMsg="Some error"
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('installed-err-msg')).toHaveTextContent('Some error')
|
||||
})
|
||||
|
||||
it('should pass isFailed=true for uploadFailed step', () => {
|
||||
render(<ReadyToInstall {...defaultProps} step={InstallStep.uploadFailed} />)
|
||||
|
||||
expect(screen.getByTestId('installed-is-failed')).toHaveTextContent('true')
|
||||
})
|
||||
|
||||
it('should pass isFailed=true for installFailed step', () => {
|
||||
render(<ReadyToInstall {...defaultProps} step={InstallStep.installFailed} />)
|
||||
|
||||
expect(screen.getByTestId('installed-is-failed')).toHaveTextContent('true')
|
||||
})
|
||||
|
||||
it('should pass isFailed=false for installed step', () => {
|
||||
render(<ReadyToInstall {...defaultProps} step={InstallStep.installed} />)
|
||||
|
||||
expect(screen.getByTestId('installed-is-failed')).toHaveTextContent('false')
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// handleInstalled Callback Tests
|
||||
// ================================
|
||||
describe('handleInstalled Callback', () => {
|
||||
it('should call onStepChange with installed when handleInstalled is triggered', () => {
|
||||
const onStepChange = vi.fn()
|
||||
render(<ReadyToInstall {...defaultProps} onStepChange={onStepChange} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('install-installed-btn'))
|
||||
|
||||
expect(onStepChange).toHaveBeenCalledWith(InstallStep.installed)
|
||||
})
|
||||
|
||||
it('should call refreshPluginList when handleInstalled is triggered without notRefresh', () => {
|
||||
const manifest = createMockManifest()
|
||||
render(<ReadyToInstall {...defaultProps} manifest={manifest} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('install-installed-btn'))
|
||||
|
||||
expect(mockRefreshPluginList).toHaveBeenCalledWith(manifest)
|
||||
})
|
||||
|
||||
it('should not call refreshPluginList when handleInstalled is triggered with notRefresh=true', () => {
|
||||
render(<ReadyToInstall {...defaultProps} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('install-installed-no-refresh-btn'))
|
||||
|
||||
expect(mockRefreshPluginList).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should call setIsInstalling(false) when handleInstalled is triggered', () => {
|
||||
const setIsInstalling = vi.fn()
|
||||
render(<ReadyToInstall {...defaultProps} setIsInstalling={setIsInstalling} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('install-installed-btn'))
|
||||
|
||||
expect(setIsInstalling).toHaveBeenCalledWith(false)
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// handleFailed Callback Tests
|
||||
// ================================
|
||||
describe('handleFailed Callback', () => {
|
||||
it('should call onStepChange with installFailed when handleFailed is triggered', () => {
|
||||
const onStepChange = vi.fn()
|
||||
render(<ReadyToInstall {...defaultProps} onStepChange={onStepChange} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('install-failed-btn'))
|
||||
|
||||
expect(onStepChange).toHaveBeenCalledWith(InstallStep.installFailed)
|
||||
})
|
||||
|
||||
it('should call setIsInstalling(false) when handleFailed is triggered', () => {
|
||||
const setIsInstalling = vi.fn()
|
||||
render(<ReadyToInstall {...defaultProps} setIsInstalling={setIsInstalling} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('install-failed-btn'))
|
||||
|
||||
expect(setIsInstalling).toHaveBeenCalledWith(false)
|
||||
})
|
||||
|
||||
it('should call onError when handleFailed is triggered with error message', () => {
|
||||
const onError = vi.fn()
|
||||
render(<ReadyToInstall {...defaultProps} onError={onError} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('install-failed-msg-btn'))
|
||||
|
||||
expect(onError).toHaveBeenCalledWith('Error message')
|
||||
})
|
||||
|
||||
it('should not call onError when handleFailed is triggered without error message', () => {
|
||||
const onError = vi.fn()
|
||||
render(<ReadyToInstall {...defaultProps} onError={onError} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('install-failed-btn'))
|
||||
|
||||
expect(onError).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// onClose Callback Tests
|
||||
// ================================
|
||||
describe('onClose Callback', () => {
|
||||
it('should call onClose when cancel is clicked in Install component', () => {
|
||||
const onClose = vi.fn()
|
||||
render(<ReadyToInstall {...defaultProps} onClose={onClose} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('install-cancel-btn'))
|
||||
|
||||
expect(onClose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should call onClose when cancel is clicked in Installed component', () => {
|
||||
const onClose = vi.fn()
|
||||
render(<ReadyToInstall {...defaultProps} step={InstallStep.installed} onClose={onClose} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('installed-cancel-btn'))
|
||||
|
||||
expect(onClose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// onStartToInstall Callback Tests
|
||||
// ================================
|
||||
describe('onStartToInstall Callback', () => {
|
||||
it('should pass onStartToInstall to Install component', () => {
|
||||
const onStartToInstall = vi.fn()
|
||||
render(<ReadyToInstall {...defaultProps} onStartToInstall={onStartToInstall} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('install-start-btn'))
|
||||
|
||||
expect(onStartToInstall).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Step Transitions Tests
|
||||
// ================================
|
||||
describe('Step Transitions', () => {
|
||||
it('should handle transition from readyToInstall to installed', () => {
|
||||
const onStepChange = vi.fn()
|
||||
const { rerender } = render(
|
||||
<ReadyToInstall {...defaultProps} step={InstallStep.readyToInstall} onStepChange={onStepChange} />,
|
||||
)
|
||||
|
||||
// Initially shows Install component
|
||||
expect(screen.getByTestId('install-step')).toBeInTheDocument()
|
||||
|
||||
// Simulate successful installation
|
||||
fireEvent.click(screen.getByTestId('install-installed-btn'))
|
||||
|
||||
expect(onStepChange).toHaveBeenCalledWith(InstallStep.installed)
|
||||
|
||||
// Rerender with new step
|
||||
rerender(<ReadyToInstall {...defaultProps} step={InstallStep.installed} onStepChange={onStepChange} />)
|
||||
|
||||
// Now shows Installed component
|
||||
expect(screen.getByTestId('installed-step')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle transition from readyToInstall to installFailed', () => {
|
||||
const onStepChange = vi.fn()
|
||||
const { rerender } = render(
|
||||
<ReadyToInstall {...defaultProps} step={InstallStep.readyToInstall} onStepChange={onStepChange} />,
|
||||
)
|
||||
|
||||
// Initially shows Install component
|
||||
expect(screen.getByTestId('install-step')).toBeInTheDocument()
|
||||
|
||||
// Simulate failed installation
|
||||
fireEvent.click(screen.getByTestId('install-failed-btn'))
|
||||
|
||||
expect(onStepChange).toHaveBeenCalledWith(InstallStep.installFailed)
|
||||
|
||||
// Rerender with new step
|
||||
rerender(<ReadyToInstall {...defaultProps} step={InstallStep.installFailed} onStepChange={onStepChange} />)
|
||||
|
||||
// Now shows Installed component with failed state
|
||||
expect(screen.getByTestId('installed-step')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('installed-is-failed')).toHaveTextContent('true')
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Edge Cases Tests
|
||||
// ================================
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle null manifest', () => {
|
||||
render(<ReadyToInstall {...defaultProps} step={InstallStep.installed} manifest={null} />)
|
||||
|
||||
expect(screen.getByTestId('installed-payload-name')).toHaveTextContent('null')
|
||||
})
|
||||
|
||||
it('should handle null errorMsg', () => {
|
||||
render(<ReadyToInstall {...defaultProps} step={InstallStep.installFailed} errorMsg={null} />)
|
||||
|
||||
expect(screen.getByTestId('installed-err-msg')).toHaveTextContent('null')
|
||||
})
|
||||
|
||||
it('should handle empty string errorMsg', () => {
|
||||
render(<ReadyToInstall {...defaultProps} step={InstallStep.installFailed} errorMsg="" />)
|
||||
|
||||
expect(screen.getByTestId('installed-err-msg')).toHaveTextContent('null')
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Callback Stability Tests
|
||||
// ================================
|
||||
describe('Callback Stability', () => {
|
||||
it('should maintain stable handleInstalled callback across re-renders', () => {
|
||||
const onStepChange = vi.fn()
|
||||
const setIsInstalling = vi.fn()
|
||||
const { rerender } = render(
|
||||
<ReadyToInstall
|
||||
{...defaultProps}
|
||||
onStepChange={onStepChange}
|
||||
setIsInstalling={setIsInstalling}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Rerender with same props
|
||||
rerender(
|
||||
<ReadyToInstall
|
||||
{...defaultProps}
|
||||
onStepChange={onStepChange}
|
||||
setIsInstalling={setIsInstalling}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Callback should still work
|
||||
fireEvent.click(screen.getByTestId('install-installed-btn'))
|
||||
|
||||
expect(onStepChange).toHaveBeenCalledWith(InstallStep.installed)
|
||||
expect(setIsInstalling).toHaveBeenCalledWith(false)
|
||||
})
|
||||
|
||||
it('should maintain stable handleFailed callback across re-renders', () => {
|
||||
const onStepChange = vi.fn()
|
||||
const setIsInstalling = vi.fn()
|
||||
const onError = vi.fn()
|
||||
const { rerender } = render(
|
||||
<ReadyToInstall
|
||||
{...defaultProps}
|
||||
onStepChange={onStepChange}
|
||||
setIsInstalling={setIsInstalling}
|
||||
onError={onError}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Rerender with same props
|
||||
rerender(
|
||||
<ReadyToInstall
|
||||
{...defaultProps}
|
||||
onStepChange={onStepChange}
|
||||
setIsInstalling={setIsInstalling}
|
||||
onError={onError}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Callback should still work
|
||||
fireEvent.click(screen.getByTestId('install-failed-msg-btn'))
|
||||
|
||||
expect(onStepChange).toHaveBeenCalledWith(InstallStep.installFailed)
|
||||
expect(setIsInstalling).toHaveBeenCalledWith(false)
|
||||
expect(onError).toHaveBeenCalledWith('Error message')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,22 +1,23 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import type { PluginDeclaration } from '../../types'
|
||||
import * as React from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import { InstallStep } from '../../types'
|
||||
import Install from './steps/install'
|
||||
import Installed from '../base/installed'
|
||||
import useRefreshPluginList from '../hooks/use-refresh-plugin-list'
|
||||
import Install from './steps/install'
|
||||
|
||||
type Props = {
|
||||
step: InstallStep
|
||||
onStepChange: (step: InstallStep) => void,
|
||||
onStepChange: (step: InstallStep) => void
|
||||
onStartToInstall: () => void
|
||||
setIsInstalling: (isInstalling: boolean) => void
|
||||
onClose: () => void
|
||||
uniqueIdentifier: string | null,
|
||||
manifest: PluginDeclaration | null,
|
||||
errorMsg: string | null,
|
||||
onError: (errorMsg: string) => void,
|
||||
uniqueIdentifier: string | null
|
||||
manifest: PluginDeclaration | null
|
||||
errorMsg: string | null
|
||||
onError: (errorMsg: string) => void
|
||||
}
|
||||
|
||||
const ReadyToInstall: FC<Props> = ({
|
||||
|
||||
@ -0,0 +1,626 @@
|
||||
import type { PluginDeclaration } from '../../../types'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { PluginCategoryEnum, TaskStatus } from '../../../types'
|
||||
import Install from './install'
|
||||
|
||||
// Factory function for test data
|
||||
const createMockManifest = (overrides: Partial<PluginDeclaration> = {}): PluginDeclaration => ({
|
||||
plugin_unique_identifier: 'test-plugin-uid',
|
||||
version: '1.0.0',
|
||||
author: 'test-author',
|
||||
icon: 'test-icon.png',
|
||||
name: 'Test Plugin',
|
||||
category: PluginCategoryEnum.tool,
|
||||
label: { 'en-US': 'Test Plugin' } as PluginDeclaration['label'],
|
||||
description: { 'en-US': 'A test plugin' } as PluginDeclaration['description'],
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
resource: {},
|
||||
plugins: [],
|
||||
verified: true,
|
||||
endpoint: { settings: [], endpoints: [] },
|
||||
model: null,
|
||||
tags: [],
|
||||
agent_strategy: null,
|
||||
meta: { version: '1.0.0', minimum_dify_version: '0.8.0' },
|
||||
trigger: {} as PluginDeclaration['trigger'],
|
||||
...overrides,
|
||||
})
|
||||
|
||||
// Mock external dependencies
|
||||
const mockUseCheckInstalled = vi.fn()
|
||||
vi.mock('@/app/components/plugins/install-plugin/hooks/use-check-installed', () => ({
|
||||
default: () => mockUseCheckInstalled(),
|
||||
}))
|
||||
|
||||
const mockInstallPackageFromLocal = vi.fn()
|
||||
vi.mock('@/service/use-plugins', () => ({
|
||||
useInstallPackageFromLocal: () => ({
|
||||
mutateAsync: mockInstallPackageFromLocal,
|
||||
}),
|
||||
usePluginTaskList: () => ({
|
||||
handleRefetch: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
const mockUninstallPlugin = vi.fn()
|
||||
vi.mock('@/service/plugins', () => ({
|
||||
uninstallPlugin: (...args: unknown[]) => mockUninstallPlugin(...args),
|
||||
}))
|
||||
|
||||
const mockCheck = vi.fn()
|
||||
const mockStop = vi.fn()
|
||||
vi.mock('../../base/check-task-status', () => ({
|
||||
default: () => ({
|
||||
check: mockCheck,
|
||||
stop: mockStop,
|
||||
}),
|
||||
}))
|
||||
|
||||
const mockLangGeniusVersionInfo = { current_version: '1.0.0' }
|
||||
vi.mock('@/context/app-context', () => ({
|
||||
useAppContext: () => ({
|
||||
langGeniusVersionInfo: mockLangGeniusVersionInfo,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string, options?: { ns?: string } & Record<string, unknown>) => {
|
||||
// Build full key with namespace prefix if provided
|
||||
const fullKey = options?.ns ? `${options.ns}.${key}` : key
|
||||
// Handle interpolation params (excluding ns)
|
||||
const { ns: _ns, ...params } = options || {}
|
||||
if (Object.keys(params).length > 0) {
|
||||
return `${fullKey}:${JSON.stringify(params)}`
|
||||
}
|
||||
return fullKey
|
||||
},
|
||||
}),
|
||||
Trans: ({ i18nKey, components }: { i18nKey: string, components?: Record<string, React.ReactNode> }) => (
|
||||
<span data-testid="trans">
|
||||
{i18nKey}
|
||||
{components?.trustSource}
|
||||
</span>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../../../card', () => ({
|
||||
default: ({ payload, titleLeft }: {
|
||||
payload: Record<string, unknown>
|
||||
titleLeft?: React.ReactNode
|
||||
}) => (
|
||||
<div data-testid="card">
|
||||
<span data-testid="card-name">{payload?.name as string}</span>
|
||||
<div data-testid="card-title-left">{titleLeft}</div>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../../base/version', () => ({
|
||||
default: ({ hasInstalled, installedVersion, toInstallVersion }: {
|
||||
hasInstalled: boolean
|
||||
installedVersion?: string
|
||||
toInstallVersion: string
|
||||
}) => (
|
||||
<div data-testid="version">
|
||||
<span data-testid="version-has-installed">{hasInstalled ? 'true' : 'false'}</span>
|
||||
<span data-testid="version-installed">{installedVersion || 'null'}</span>
|
||||
<span data-testid="version-to-install">{toInstallVersion}</span>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../../utils', () => ({
|
||||
pluginManifestToCardPluginProps: (manifest: PluginDeclaration) => ({
|
||||
name: manifest.name,
|
||||
author: manifest.author,
|
||||
version: manifest.version,
|
||||
}),
|
||||
}))
|
||||
|
||||
describe('Install', () => {
|
||||
const defaultProps = {
|
||||
uniqueIdentifier: 'test-unique-identifier',
|
||||
payload: createMockManifest(),
|
||||
onCancel: vi.fn(),
|
||||
onStartToInstall: vi.fn(),
|
||||
onInstalled: vi.fn(),
|
||||
onFailed: vi.fn(),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockUseCheckInstalled.mockReturnValue({
|
||||
installedInfo: null,
|
||||
isLoading: false,
|
||||
})
|
||||
mockInstallPackageFromLocal.mockReset()
|
||||
mockUninstallPlugin.mockReset()
|
||||
mockCheck.mockReset()
|
||||
mockStop.mockReset()
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Rendering Tests
|
||||
// ================================
|
||||
describe('Rendering', () => {
|
||||
it('should render ready to install message', () => {
|
||||
render(<Install {...defaultProps} />)
|
||||
|
||||
expect(screen.getByText('plugin.installModal.readyToInstall')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render trust source message', () => {
|
||||
render(<Install {...defaultProps} />)
|
||||
|
||||
expect(screen.getByTestId('trans')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render plugin card', () => {
|
||||
render(<Install {...defaultProps} />)
|
||||
|
||||
expect(screen.getByTestId('card')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('card-name')).toHaveTextContent('Test Plugin')
|
||||
})
|
||||
|
||||
it('should render cancel button', () => {
|
||||
render(<Install {...defaultProps} />)
|
||||
|
||||
expect(screen.getByRole('button', { name: 'common.operation.cancel' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render install button', () => {
|
||||
render(<Install {...defaultProps} />)
|
||||
|
||||
expect(screen.getByRole('button', { name: 'plugin.installModal.install' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show version component when not loading', () => {
|
||||
mockUseCheckInstalled.mockReturnValue({
|
||||
installedInfo: null,
|
||||
isLoading: false,
|
||||
})
|
||||
|
||||
render(<Install {...defaultProps} />)
|
||||
|
||||
expect(screen.getByTestId('version')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not show version component when loading', () => {
|
||||
mockUseCheckInstalled.mockReturnValue({
|
||||
installedInfo: null,
|
||||
isLoading: true,
|
||||
})
|
||||
|
||||
render(<Install {...defaultProps} />)
|
||||
|
||||
expect(screen.queryByTestId('version')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Version Display Tests
|
||||
// ================================
|
||||
describe('Version Display', () => {
|
||||
it('should display toInstallVersion from payload', () => {
|
||||
const payload = createMockManifest({ version: '2.0.0' })
|
||||
mockUseCheckInstalled.mockReturnValue({
|
||||
installedInfo: null,
|
||||
isLoading: false,
|
||||
})
|
||||
|
||||
render(<Install {...defaultProps} payload={payload} />)
|
||||
|
||||
expect(screen.getByTestId('version-to-install')).toHaveTextContent('2.0.0')
|
||||
})
|
||||
|
||||
it('should display hasInstalled=false when not installed', () => {
|
||||
mockUseCheckInstalled.mockReturnValue({
|
||||
installedInfo: null,
|
||||
isLoading: false,
|
||||
})
|
||||
|
||||
render(<Install {...defaultProps} />)
|
||||
|
||||
expect(screen.getByTestId('version-has-installed')).toHaveTextContent('false')
|
||||
})
|
||||
|
||||
it('should display hasInstalled=true when already installed', () => {
|
||||
mockUseCheckInstalled.mockReturnValue({
|
||||
installedInfo: {
|
||||
'test-author/Test Plugin': {
|
||||
installedVersion: '0.9.0',
|
||||
installedId: 'installed-id',
|
||||
uniqueIdentifier: 'old-uid',
|
||||
},
|
||||
},
|
||||
isLoading: false,
|
||||
})
|
||||
|
||||
render(<Install {...defaultProps} />)
|
||||
|
||||
expect(screen.getByTestId('version-has-installed')).toHaveTextContent('true')
|
||||
expect(screen.getByTestId('version-installed')).toHaveTextContent('0.9.0')
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Install Button State Tests
|
||||
// ================================
|
||||
describe('Install Button State', () => {
|
||||
it('should disable install button when loading', () => {
|
||||
mockUseCheckInstalled.mockReturnValue({
|
||||
installedInfo: null,
|
||||
isLoading: true,
|
||||
})
|
||||
|
||||
render(<Install {...defaultProps} />)
|
||||
|
||||
expect(screen.getByRole('button', { name: 'plugin.installModal.install' })).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should enable install button when not loading', () => {
|
||||
mockUseCheckInstalled.mockReturnValue({
|
||||
installedInfo: null,
|
||||
isLoading: false,
|
||||
})
|
||||
|
||||
render(<Install {...defaultProps} />)
|
||||
|
||||
expect(screen.getByRole('button', { name: 'plugin.installModal.install' })).not.toBeDisabled()
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Cancel Button Tests
|
||||
// ================================
|
||||
describe('Cancel Button', () => {
|
||||
it('should call onCancel and stop when cancel button is clicked', () => {
|
||||
const onCancel = vi.fn()
|
||||
render(<Install {...defaultProps} onCancel={onCancel} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.cancel' }))
|
||||
|
||||
expect(mockStop).toHaveBeenCalled()
|
||||
expect(onCancel).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should hide cancel button when installing', async () => {
|
||||
mockInstallPackageFromLocal.mockImplementation(() => new Promise(() => {}))
|
||||
|
||||
render(<Install {...defaultProps} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'plugin.installModal.install' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole('button', { name: 'common.operation.cancel' })).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Installation Flow Tests
|
||||
// ================================
|
||||
describe('Installation Flow', () => {
|
||||
it('should call onStartToInstall when install button is clicked', async () => {
|
||||
mockInstallPackageFromLocal.mockResolvedValue({
|
||||
all_installed: true,
|
||||
task_id: 'task-123',
|
||||
})
|
||||
|
||||
const onStartToInstall = vi.fn()
|
||||
render(<Install {...defaultProps} onStartToInstall={onStartToInstall} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'plugin.installModal.install' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onStartToInstall).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
it('should call onInstalled when all_installed is true', async () => {
|
||||
mockInstallPackageFromLocal.mockResolvedValue({
|
||||
all_installed: true,
|
||||
task_id: 'task-123',
|
||||
})
|
||||
|
||||
const onInstalled = vi.fn()
|
||||
render(<Install {...defaultProps} onInstalled={onInstalled} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'plugin.installModal.install' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onInstalled).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it('should check task status when all_installed is false', async () => {
|
||||
mockInstallPackageFromLocal.mockResolvedValue({
|
||||
all_installed: false,
|
||||
task_id: 'task-123',
|
||||
})
|
||||
mockCheck.mockResolvedValue({ status: TaskStatus.success, error: null })
|
||||
|
||||
const onInstalled = vi.fn()
|
||||
render(<Install {...defaultProps} onInstalled={onInstalled} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'plugin.installModal.install' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockCheck).toHaveBeenCalledWith({
|
||||
taskId: 'task-123',
|
||||
pluginUniqueIdentifier: 'test-unique-identifier',
|
||||
})
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onInstalled).toHaveBeenCalledWith(true)
|
||||
})
|
||||
})
|
||||
|
||||
it('should call onFailed when task status is failed', async () => {
|
||||
mockInstallPackageFromLocal.mockResolvedValue({
|
||||
all_installed: false,
|
||||
task_id: 'task-123',
|
||||
})
|
||||
mockCheck.mockResolvedValue({ status: TaskStatus.failed, error: 'Task failed error' })
|
||||
|
||||
const onFailed = vi.fn()
|
||||
render(<Install {...defaultProps} onFailed={onFailed} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'plugin.installModal.install' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onFailed).toHaveBeenCalledWith('Task failed error')
|
||||
})
|
||||
})
|
||||
|
||||
it('should uninstall existing plugin before installing new version', async () => {
|
||||
mockUseCheckInstalled.mockReturnValue({
|
||||
installedInfo: {
|
||||
'test-author/Test Plugin': {
|
||||
installedVersion: '0.9.0',
|
||||
installedId: 'installed-id-to-uninstall',
|
||||
uniqueIdentifier: 'old-uid',
|
||||
},
|
||||
},
|
||||
isLoading: false,
|
||||
})
|
||||
mockUninstallPlugin.mockResolvedValue({})
|
||||
mockInstallPackageFromLocal.mockResolvedValue({
|
||||
all_installed: true,
|
||||
task_id: 'task-123',
|
||||
})
|
||||
|
||||
render(<Install {...defaultProps} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'plugin.installModal.install' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUninstallPlugin).toHaveBeenCalledWith('installed-id-to-uninstall')
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockInstallPackageFromLocal).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Error Handling Tests
|
||||
// ================================
|
||||
describe('Error Handling', () => {
|
||||
it('should call onFailed with error string', async () => {
|
||||
mockInstallPackageFromLocal.mockRejectedValue('Installation error string')
|
||||
|
||||
const onFailed = vi.fn()
|
||||
render(<Install {...defaultProps} onFailed={onFailed} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'plugin.installModal.install' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onFailed).toHaveBeenCalledWith('Installation error string')
|
||||
})
|
||||
})
|
||||
|
||||
it('should call onFailed without message when error is not string', async () => {
|
||||
mockInstallPackageFromLocal.mockRejectedValue({ code: 'ERROR' })
|
||||
|
||||
const onFailed = vi.fn()
|
||||
render(<Install {...defaultProps} onFailed={onFailed} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'plugin.installModal.install' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onFailed).toHaveBeenCalledWith()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Auto Install Behavior Tests
|
||||
// ================================
|
||||
describe('Auto Install Behavior', () => {
|
||||
it('should call onInstalled when already installed with same uniqueIdentifier', async () => {
|
||||
mockUseCheckInstalled.mockReturnValue({
|
||||
installedInfo: {
|
||||
'test-author/Test Plugin': {
|
||||
installedVersion: '1.0.0',
|
||||
installedId: 'installed-id',
|
||||
uniqueIdentifier: 'test-unique-identifier',
|
||||
},
|
||||
},
|
||||
isLoading: false,
|
||||
})
|
||||
|
||||
const onInstalled = vi.fn()
|
||||
render(<Install {...defaultProps} onInstalled={onInstalled} />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onInstalled).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it('should not auto-call onInstalled when uniqueIdentifier differs', () => {
|
||||
mockUseCheckInstalled.mockReturnValue({
|
||||
installedInfo: {
|
||||
'test-author/Test Plugin': {
|
||||
installedVersion: '1.0.0',
|
||||
installedId: 'installed-id',
|
||||
uniqueIdentifier: 'different-uid',
|
||||
},
|
||||
},
|
||||
isLoading: false,
|
||||
})
|
||||
|
||||
const onInstalled = vi.fn()
|
||||
render(<Install {...defaultProps} onInstalled={onInstalled} />)
|
||||
|
||||
// Should not be called immediately
|
||||
expect(onInstalled).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Dify Version Compatibility Tests
|
||||
// ================================
|
||||
describe('Dify Version Compatibility', () => {
|
||||
it('should not show warning when dify version is compatible', () => {
|
||||
mockLangGeniusVersionInfo.current_version = '1.0.0'
|
||||
const payload = createMockManifest({ meta: { version: '1.0.0', minimum_dify_version: '0.8.0' } })
|
||||
|
||||
render(<Install {...defaultProps} payload={payload} />)
|
||||
|
||||
expect(screen.queryByText(/plugin.difyVersionNotCompatible/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show warning when dify version is incompatible', () => {
|
||||
mockLangGeniusVersionInfo.current_version = '1.0.0'
|
||||
const payload = createMockManifest({ meta: { version: '1.0.0', minimum_dify_version: '2.0.0' } })
|
||||
|
||||
render(<Install {...defaultProps} payload={payload} />)
|
||||
|
||||
expect(screen.getByText(/plugin.difyVersionNotCompatible/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should be compatible when minimum_dify_version is undefined', () => {
|
||||
mockLangGeniusVersionInfo.current_version = '1.0.0'
|
||||
const payload = createMockManifest({ meta: { version: '1.0.0' } })
|
||||
|
||||
render(<Install {...defaultProps} payload={payload} />)
|
||||
|
||||
expect(screen.queryByText(/plugin.difyVersionNotCompatible/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should be compatible when current_version is empty', () => {
|
||||
mockLangGeniusVersionInfo.current_version = ''
|
||||
const payload = createMockManifest({ meta: { version: '1.0.0', minimum_dify_version: '2.0.0' } })
|
||||
|
||||
render(<Install {...defaultProps} payload={payload} />)
|
||||
|
||||
// When current_version is empty, should be compatible (no warning)
|
||||
expect(screen.queryByText(/plugin.difyVersionNotCompatible/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should be compatible when current_version is undefined', () => {
|
||||
mockLangGeniusVersionInfo.current_version = undefined as unknown as string
|
||||
const payload = createMockManifest({ meta: { version: '1.0.0', minimum_dify_version: '2.0.0' } })
|
||||
|
||||
render(<Install {...defaultProps} payload={payload} />)
|
||||
|
||||
// When current_version is undefined, should be compatible (no warning)
|
||||
expect(screen.queryByText(/plugin.difyVersionNotCompatible/)).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Installing State Tests
|
||||
// ================================
|
||||
describe('Installing State', () => {
|
||||
it('should show installing text when installing', async () => {
|
||||
mockInstallPackageFromLocal.mockImplementation(() => new Promise(() => {}))
|
||||
|
||||
render(<Install {...defaultProps} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'plugin.installModal.install' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('plugin.installModal.installing')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should disable install button when installing', async () => {
|
||||
mockInstallPackageFromLocal.mockImplementation(() => new Promise(() => {}))
|
||||
|
||||
render(<Install {...defaultProps} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'plugin.installModal.install' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: /plugin.installModal.installing/ })).toBeDisabled()
|
||||
})
|
||||
})
|
||||
|
||||
it('should show loading spinner when installing', async () => {
|
||||
mockInstallPackageFromLocal.mockImplementation(() => new Promise(() => {}))
|
||||
|
||||
render(<Install {...defaultProps} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'plugin.installModal.install' }))
|
||||
|
||||
await waitFor(() => {
|
||||
const spinner = document.querySelector('.animate-spin-slow')
|
||||
expect(spinner).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should not trigger install twice when already installing', async () => {
|
||||
mockInstallPackageFromLocal.mockImplementation(() => new Promise(() => {}))
|
||||
|
||||
render(<Install {...defaultProps} />)
|
||||
|
||||
const installButton = screen.getByRole('button', { name: 'plugin.installModal.install' })
|
||||
|
||||
// Click install
|
||||
fireEvent.click(installButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockInstallPackageFromLocal).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
// Try to click again (button should be disabled but let's verify the guard works)
|
||||
fireEvent.click(screen.getByRole('button', { name: /plugin.installModal.installing/ }))
|
||||
|
||||
// Should still only be called once due to isInstalling guard
|
||||
expect(mockInstallPackageFromLocal).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Callback Props Tests
|
||||
// ================================
|
||||
describe('Callback Props', () => {
|
||||
it('should work without onStartToInstall callback', async () => {
|
||||
mockInstallPackageFromLocal.mockResolvedValue({
|
||||
all_installed: true,
|
||||
task_id: 'task-123',
|
||||
})
|
||||
|
||||
const onInstalled = vi.fn()
|
||||
render(
|
||||
<Install
|
||||
{...defaultProps}
|
||||
onStartToInstall={undefined}
|
||||
onInstalled={onInstalled}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'plugin.installModal.install' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onInstalled).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,21 +1,23 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect, useMemo } from 'react'
|
||||
import { type PluginDeclaration, TaskStatus } from '../../../types'
|
||||
import Card from '../../../card'
|
||||
import { pluginManifestToCardPluginProps } from '../../utils'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import type { PluginDeclaration } from '../../../types'
|
||||
import { RiLoader2Line } from '@remixicon/react'
|
||||
import checkTaskStatus from '../../base/check-task-status'
|
||||
import { useInstallPackageFromLocal, usePluginTaskList } from '@/service/use-plugins'
|
||||
import useCheckInstalled from '@/app/components/plugins/install-plugin/hooks/use-check-installed'
|
||||
import { uninstallPlugin } from '@/service/plugins'
|
||||
import Version from '../../base/version'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import * as React from 'react'
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { gte } from 'semver'
|
||||
import Button from '@/app/components/base/button'
|
||||
import useCheckInstalled from '@/app/components/plugins/install-plugin/hooks/use-check-installed'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { uninstallPlugin } from '@/service/plugins'
|
||||
import { useInstallPackageFromLocal, usePluginTaskList } from '@/service/use-plugins'
|
||||
import Card from '../../../card'
|
||||
import { TaskStatus } from '../../../types'
|
||||
import checkTaskStatus from '../../base/check-task-status'
|
||||
import Version from '../../base/version'
|
||||
import { pluginManifestToCardPluginProps } from '../../utils'
|
||||
|
||||
const i18nPrefix = 'plugin.installModal'
|
||||
const i18nPrefix = 'installModal'
|
||||
|
||||
type Props = {
|
||||
uniqueIdentifier: string
|
||||
@ -65,7 +67,8 @@ const Installed: FC<Props> = ({
|
||||
|
||||
const { handleRefetch } = usePluginTaskList(payload.category)
|
||||
const handleInstall = async () => {
|
||||
if (isInstalling) return
|
||||
if (isInstalling)
|
||||
return
|
||||
setIsInstalling(true)
|
||||
onStartToInstall?.()
|
||||
|
||||
@ -113,48 +116,50 @@ const Installed: FC<Props> = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='flex flex-col items-start justify-center gap-4 self-stretch px-6 py-3'>
|
||||
<div className='system-md-regular text-text-secondary'>
|
||||
<p>{t(`${i18nPrefix}.readyToInstall`)}</p>
|
||||
<div className="flex flex-col items-start justify-center gap-4 self-stretch px-6 py-3">
|
||||
<div className="system-md-regular text-text-secondary">
|
||||
<p>{t(`${i18nPrefix}.readyToInstall`, { ns: 'plugin' })}</p>
|
||||
<p>
|
||||
<Trans
|
||||
i18nKey={`${i18nPrefix}.fromTrustSource`}
|
||||
components={{ trustSource: <span className='system-md-semibold' /> }}
|
||||
components={{ trustSource: <span className="system-md-semibold" /> }}
|
||||
/>
|
||||
</p>
|
||||
{!isDifyVersionCompatible && (
|
||||
<p className='system-md-regular flex items-center gap-1 text-text-warning'>
|
||||
{t('plugin.difyVersionNotCompatible', { minimalDifyVersion: payload.meta.minimum_dify_version })}
|
||||
<p className="system-md-regular flex items-center gap-1 text-text-warning">
|
||||
{t('difyVersionNotCompatible', { ns: 'plugin', minimalDifyVersion: payload.meta.minimum_dify_version })}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className='flex flex-wrap content-start items-start gap-1 self-stretch rounded-2xl bg-background-section-burn p-2'>
|
||||
<div className="flex flex-wrap content-start items-start gap-1 self-stretch rounded-2xl bg-background-section-burn p-2">
|
||||
<Card
|
||||
className='w-full'
|
||||
className="w-full"
|
||||
payload={pluginManifestToCardPluginProps(payload)}
|
||||
titleLeft={!isLoading && <Version
|
||||
hasInstalled={hasInstalled}
|
||||
installedVersion={installedVersion}
|
||||
toInstallVersion={toInstallVersion}
|
||||
/>}
|
||||
titleLeft={!isLoading && (
|
||||
<Version
|
||||
hasInstalled={hasInstalled}
|
||||
installedVersion={installedVersion}
|
||||
toInstallVersion={toInstallVersion}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* Action Buttons */}
|
||||
<div className='flex items-center justify-end gap-2 self-stretch p-6 pt-5'>
|
||||
<div className="flex items-center justify-end gap-2 self-stretch p-6 pt-5">
|
||||
{!isInstalling && (
|
||||
<Button variant='secondary' className='min-w-[72px]' onClick={handleCancel}>
|
||||
{t('common.operation.cancel')}
|
||||
<Button variant="secondary" className="min-w-[72px]" onClick={handleCancel}>
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant='primary'
|
||||
className='flex min-w-[72px] space-x-0.5'
|
||||
variant="primary"
|
||||
className="flex min-w-[72px] space-x-0.5"
|
||||
disabled={isInstalling || isLoading}
|
||||
onClick={handleInstall}
|
||||
>
|
||||
{isInstalling && <RiLoader2Line className='h-4 w-4 animate-spin-slow' />}
|
||||
<span>{t(`${i18nPrefix}.${isInstalling ? 'installing' : 'install'}`)}</span>
|
||||
{isInstalling && <RiLoader2Line className="h-4 w-4 animate-spin-slow" />}
|
||||
<span>{t(`${i18nPrefix}.${isInstalling ? 'installing' : 'install'}`, { ns: 'plugin' })}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@ -0,0 +1,356 @@
|
||||
import type { Dependency, PluginDeclaration } from '../../../types'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { PluginCategoryEnum } from '../../../types'
|
||||
import Uploading from './uploading'
|
||||
|
||||
// Factory function for test data
|
||||
const createMockManifest = (overrides: Partial<PluginDeclaration> = {}): PluginDeclaration => ({
|
||||
plugin_unique_identifier: 'test-plugin-uid',
|
||||
version: '1.0.0',
|
||||
author: 'test-author',
|
||||
icon: 'test-icon.png',
|
||||
name: 'Test Plugin',
|
||||
category: PluginCategoryEnum.tool,
|
||||
label: { 'en-US': 'Test Plugin' } as PluginDeclaration['label'],
|
||||
description: { 'en-US': 'A test plugin' } as PluginDeclaration['description'],
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
resource: {},
|
||||
plugins: [],
|
||||
verified: true,
|
||||
endpoint: { settings: [], endpoints: [] },
|
||||
model: null,
|
||||
tags: [],
|
||||
agent_strategy: null,
|
||||
meta: { version: '1.0.0' },
|
||||
trigger: {} as PluginDeclaration['trigger'],
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const createMockDependencies = (): Dependency[] => [
|
||||
{
|
||||
type: 'package',
|
||||
value: {
|
||||
unique_identifier: 'dep-1',
|
||||
manifest: createMockManifest({ name: 'Dep Plugin 1' }),
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const createMockFile = (name: string = 'test-plugin.difypkg'): File => {
|
||||
return new File(['test content'], name, { type: 'application/octet-stream' })
|
||||
}
|
||||
|
||||
// Mock external dependencies
|
||||
const mockUploadFile = vi.fn()
|
||||
vi.mock('@/service/plugins', () => ({
|
||||
uploadFile: (...args: unknown[]) => mockUploadFile(...args),
|
||||
}))
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string, options?: { ns?: string } & Record<string, unknown>) => {
|
||||
// Build full key with namespace prefix if provided
|
||||
const fullKey = options?.ns ? `${options.ns}.${key}` : key
|
||||
// Handle interpolation params (excluding ns)
|
||||
const { ns: _ns, ...params } = options || {}
|
||||
if (Object.keys(params).length > 0) {
|
||||
return `${fullKey}:${JSON.stringify(params)}`
|
||||
}
|
||||
return fullKey
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../../../card', () => ({
|
||||
default: ({ payload, isLoading, loadingFileName }: {
|
||||
payload: { name: string }
|
||||
isLoading?: boolean
|
||||
loadingFileName?: string
|
||||
}) => (
|
||||
<div data-testid="card">
|
||||
<span data-testid="card-name">{payload?.name}</span>
|
||||
<span data-testid="card-is-loading">{isLoading ? 'true' : 'false'}</span>
|
||||
<span data-testid="card-loading-filename">{loadingFileName || 'null'}</span>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
describe('Uploading', () => {
|
||||
const defaultProps = {
|
||||
isBundle: false,
|
||||
file: createMockFile(),
|
||||
onCancel: vi.fn(),
|
||||
onPackageUploaded: vi.fn(),
|
||||
onBundleUploaded: vi.fn(),
|
||||
onFailed: vi.fn(),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockUploadFile.mockReset()
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Rendering Tests
|
||||
// ================================
|
||||
describe('Rendering', () => {
|
||||
it('should render uploading message with file name', () => {
|
||||
render(<Uploading {...defaultProps} />)
|
||||
|
||||
expect(screen.getByText(/plugin.installModal.uploadingPackage/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render loading spinner', () => {
|
||||
render(<Uploading {...defaultProps} />)
|
||||
|
||||
// The spinner has animate-spin-slow class
|
||||
const spinner = document.querySelector('.animate-spin-slow')
|
||||
expect(spinner).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render card with loading state', () => {
|
||||
render(<Uploading {...defaultProps} />)
|
||||
|
||||
expect(screen.getByTestId('card-is-loading')).toHaveTextContent('true')
|
||||
})
|
||||
|
||||
it('should render card with file name', () => {
|
||||
const file = createMockFile('my-plugin.difypkg')
|
||||
render(<Uploading {...defaultProps} file={file} />)
|
||||
|
||||
expect(screen.getByTestId('card-name')).toHaveTextContent('my-plugin.difypkg')
|
||||
expect(screen.getByTestId('card-loading-filename')).toHaveTextContent('my-plugin.difypkg')
|
||||
})
|
||||
|
||||
it('should render cancel button', () => {
|
||||
render(<Uploading {...defaultProps} />)
|
||||
|
||||
expect(screen.getByRole('button', { name: 'common.operation.cancel' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render disabled install button', () => {
|
||||
render(<Uploading {...defaultProps} />)
|
||||
|
||||
const installButton = screen.getByRole('button', { name: 'plugin.installModal.install' })
|
||||
expect(installButton).toBeDisabled()
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Upload Behavior Tests
|
||||
// ================================
|
||||
describe('Upload Behavior', () => {
|
||||
it('should call uploadFile on mount', async () => {
|
||||
mockUploadFile.mockResolvedValue({})
|
||||
|
||||
render(<Uploading {...defaultProps} />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUploadFile).toHaveBeenCalledWith(defaultProps.file, false)
|
||||
})
|
||||
})
|
||||
|
||||
it('should call uploadFile with isBundle=true for bundle files', async () => {
|
||||
mockUploadFile.mockResolvedValue({})
|
||||
|
||||
render(<Uploading {...defaultProps} isBundle />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUploadFile).toHaveBeenCalledWith(defaultProps.file, true)
|
||||
})
|
||||
})
|
||||
|
||||
it('should call onFailed when upload fails with error message', async () => {
|
||||
const errorMessage = 'Upload failed: file too large'
|
||||
mockUploadFile.mockRejectedValue({
|
||||
response: { message: errorMessage },
|
||||
})
|
||||
|
||||
const onFailed = vi.fn()
|
||||
render(<Uploading {...defaultProps} onFailed={onFailed} />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onFailed).toHaveBeenCalledWith(errorMessage)
|
||||
})
|
||||
})
|
||||
|
||||
// NOTE: The uploadFile API has an unconventional contract where it always rejects.
|
||||
// Success vs failure is determined by whether response.message exists:
|
||||
// - If response.message exists → treated as failure (calls onFailed)
|
||||
// - If response.message is absent → treated as success (calls onPackageUploaded/onBundleUploaded)
|
||||
// This explains why we use mockRejectedValue for "success" scenarios below.
|
||||
|
||||
it('should call onPackageUploaded when upload rejects without error message (success case)', async () => {
|
||||
const mockResult = {
|
||||
unique_identifier: 'test-uid',
|
||||
manifest: createMockManifest(),
|
||||
}
|
||||
mockUploadFile.mockRejectedValue({
|
||||
response: mockResult,
|
||||
})
|
||||
|
||||
const onPackageUploaded = vi.fn()
|
||||
render(
|
||||
<Uploading
|
||||
{...defaultProps}
|
||||
isBundle={false}
|
||||
onPackageUploaded={onPackageUploaded}
|
||||
/>,
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onPackageUploaded).toHaveBeenCalledWith({
|
||||
uniqueIdentifier: mockResult.unique_identifier,
|
||||
manifest: mockResult.manifest,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should call onBundleUploaded when upload rejects without error message (success case)', async () => {
|
||||
const mockDependencies = createMockDependencies()
|
||||
mockUploadFile.mockRejectedValue({
|
||||
response: mockDependencies,
|
||||
})
|
||||
|
||||
const onBundleUploaded = vi.fn()
|
||||
render(
|
||||
<Uploading
|
||||
{...defaultProps}
|
||||
isBundle
|
||||
onBundleUploaded={onBundleUploaded}
|
||||
/>,
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onBundleUploaded).toHaveBeenCalledWith(mockDependencies)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Cancel Button Tests
|
||||
// ================================
|
||||
describe('Cancel Button', () => {
|
||||
it('should call onCancel when cancel button is clicked', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onCancel = vi.fn()
|
||||
render(<Uploading {...defaultProps} onCancel={onCancel} />)
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.cancel' }))
|
||||
|
||||
expect(onCancel).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// File Name Display Tests
|
||||
// ================================
|
||||
describe('File Name Display', () => {
|
||||
it('should display correct file name for package file', () => {
|
||||
const file = createMockFile('custom-plugin.difypkg')
|
||||
render(<Uploading {...defaultProps} file={file} />)
|
||||
|
||||
expect(screen.getByTestId('card-name')).toHaveTextContent('custom-plugin.difypkg')
|
||||
})
|
||||
|
||||
it('should display correct file name for bundle file', () => {
|
||||
const file = createMockFile('custom-bundle.difybndl')
|
||||
render(<Uploading {...defaultProps} file={file} isBundle />)
|
||||
|
||||
expect(screen.getByTestId('card-name')).toHaveTextContent('custom-bundle.difybndl')
|
||||
})
|
||||
|
||||
it('should display file name in uploading message', () => {
|
||||
const file = createMockFile('special-plugin.difypkg')
|
||||
render(<Uploading {...defaultProps} file={file} />)
|
||||
|
||||
// The message includes the file name as a parameter
|
||||
expect(screen.getByText(/plugin\.installModal\.uploadingPackage/)).toHaveTextContent('special-plugin.difypkg')
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Edge Cases Tests
|
||||
// ================================
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle empty response gracefully', async () => {
|
||||
mockUploadFile.mockRejectedValue({
|
||||
response: {},
|
||||
})
|
||||
|
||||
const onPackageUploaded = vi.fn()
|
||||
render(<Uploading {...defaultProps} onPackageUploaded={onPackageUploaded} />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onPackageUploaded).toHaveBeenCalledWith({
|
||||
uniqueIdentifier: undefined,
|
||||
manifest: undefined,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle response with only unique_identifier', async () => {
|
||||
mockUploadFile.mockRejectedValue({
|
||||
response: { unique_identifier: 'only-uid' },
|
||||
})
|
||||
|
||||
const onPackageUploaded = vi.fn()
|
||||
render(<Uploading {...defaultProps} onPackageUploaded={onPackageUploaded} />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onPackageUploaded).toHaveBeenCalledWith({
|
||||
uniqueIdentifier: 'only-uid',
|
||||
manifest: undefined,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle file with special characters in name', () => {
|
||||
const file = createMockFile('my plugin (v1.0).difypkg')
|
||||
render(<Uploading {...defaultProps} file={file} />)
|
||||
|
||||
expect(screen.getByTestId('card-name')).toHaveTextContent('my plugin (v1.0).difypkg')
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Props Variations Tests
|
||||
// ================================
|
||||
describe('Props Variations', () => {
|
||||
it('should work with different file types', () => {
|
||||
const files = [
|
||||
createMockFile('plugin-a.difypkg'),
|
||||
createMockFile('plugin-b.zip'),
|
||||
createMockFile('bundle.difybndl'),
|
||||
]
|
||||
|
||||
files.forEach((file) => {
|
||||
const { unmount } = render(<Uploading {...defaultProps} file={file} />)
|
||||
expect(screen.getByTestId('card-name')).toHaveTextContent(file.name)
|
||||
unmount()
|
||||
})
|
||||
})
|
||||
|
||||
it('should pass isBundle=false to uploadFile for package files', async () => {
|
||||
mockUploadFile.mockResolvedValue({})
|
||||
|
||||
render(<Uploading {...defaultProps} isBundle={false} />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUploadFile).toHaveBeenCalledWith(expect.anything(), false)
|
||||
})
|
||||
})
|
||||
|
||||
it('should pass isBundle=true to uploadFile for bundle files', async () => {
|
||||
mockUploadFile.mockResolvedValue({})
|
||||
|
||||
render(<Uploading {...defaultProps} isBundle />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUploadFile).toHaveBeenCalledWith(expect.anything(), true)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,13 +1,14 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { RiLoader2Line } from '@remixicon/react'
|
||||
import Card from '../../../card'
|
||||
import type { Dependency, PluginDeclaration } from '../../../types'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { RiLoader2Line } from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { uploadFile } from '@/service/plugins'
|
||||
const i18nPrefix = 'plugin.installModal'
|
||||
import Card from '../../../card'
|
||||
|
||||
const i18nPrefix = 'installModal'
|
||||
|
||||
type Props = {
|
||||
isBundle: boolean
|
||||
@ -58,18 +59,19 @@ const Uploading: FC<Props> = ({
|
||||
}, [])
|
||||
return (
|
||||
<>
|
||||
<div className='flex flex-col items-start justify-center gap-4 self-stretch px-6 py-3'>
|
||||
<div className='flex items-center gap-1 self-stretch'>
|
||||
<RiLoader2Line className='h-4 w-4 animate-spin-slow text-text-accent' />
|
||||
<div className='system-md-regular text-text-secondary'>
|
||||
<div className="flex flex-col items-start justify-center gap-4 self-stretch px-6 py-3">
|
||||
<div className="flex items-center gap-1 self-stretch">
|
||||
<RiLoader2Line className="h-4 w-4 animate-spin-slow text-text-accent" />
|
||||
<div className="system-md-regular text-text-secondary">
|
||||
{t(`${i18nPrefix}.uploadingPackage`, {
|
||||
ns: 'plugin',
|
||||
packageName: fileName,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-wrap content-start items-start gap-1 self-stretch rounded-2xl bg-background-section-burn p-2'>
|
||||
<div className="flex flex-wrap content-start items-start gap-1 self-stretch rounded-2xl bg-background-section-burn p-2">
|
||||
<Card
|
||||
className='w-full'
|
||||
className="w-full"
|
||||
payload={{ name: fileName } as any}
|
||||
isLoading
|
||||
loadingFileName={fileName}
|
||||
@ -79,16 +81,16 @@ const Uploading: FC<Props> = ({
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className='flex items-center justify-end gap-2 self-stretch p-6 pt-5'>
|
||||
<Button variant='secondary' className='min-w-[72px]' onClick={onCancel}>
|
||||
{t('common.operation.cancel')}
|
||||
<div className="flex items-center justify-end gap-2 self-stretch p-6 pt-5">
|
||||
<Button variant="secondary" className="min-w-[72px]" onClick={onCancel}>
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</Button>
|
||||
<Button
|
||||
variant='primary'
|
||||
className='min-w-[72px]'
|
||||
variant="primary"
|
||||
className="min-w-[72px]"
|
||||
disabled
|
||||
>
|
||||
{t(`${i18nPrefix}.install`)}
|
||||
{t(`${i18nPrefix}.install`, { ns: 'plugin' })}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@ -0,0 +1,928 @@
|
||||
import type { Dependency, Plugin, PluginManifestInMarket } from '../../types'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { InstallStep, PluginCategoryEnum } from '../../types'
|
||||
import InstallFromMarketplace from './index'
|
||||
|
||||
// Factory functions for test data
|
||||
// Use type casting to avoid strict locale requirements in tests
|
||||
const createMockManifest = (overrides: Partial<PluginManifestInMarket> = {}): PluginManifestInMarket => ({
|
||||
plugin_unique_identifier: 'test-unique-identifier',
|
||||
name: 'Test Plugin',
|
||||
org: 'test-org',
|
||||
icon: 'test-icon.png',
|
||||
label: { en_US: 'Test Plugin' } as PluginManifestInMarket['label'],
|
||||
category: PluginCategoryEnum.tool,
|
||||
version: '1.0.0',
|
||||
latest_version: '1.0.0',
|
||||
brief: { en_US: 'A test plugin' } as PluginManifestInMarket['brief'],
|
||||
introduction: 'Introduction text',
|
||||
verified: true,
|
||||
install_count: 100,
|
||||
badges: [],
|
||||
verification: { authorized_category: 'community' },
|
||||
from: 'marketplace',
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const createMockPlugin = (overrides: Partial<Plugin> = {}): Plugin => ({
|
||||
type: 'plugin',
|
||||
org: 'test-org',
|
||||
name: 'Test Plugin',
|
||||
plugin_id: 'test-plugin-id',
|
||||
version: '1.0.0',
|
||||
latest_version: '1.0.0',
|
||||
latest_package_identifier: 'test-package-id',
|
||||
icon: 'test-icon.png',
|
||||
verified: true,
|
||||
label: { en_US: 'Test Plugin' },
|
||||
brief: { en_US: 'A test plugin' },
|
||||
description: { en_US: 'A test plugin description' },
|
||||
introduction: 'Introduction text',
|
||||
repository: 'https://github.com/test/plugin',
|
||||
category: PluginCategoryEnum.tool,
|
||||
install_count: 100,
|
||||
endpoint: { settings: [] },
|
||||
tags: [],
|
||||
badges: [],
|
||||
verification: { authorized_category: 'community' },
|
||||
from: 'marketplace',
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const createMockDependencies = (): Dependency[] => [
|
||||
{
|
||||
type: 'github',
|
||||
value: {
|
||||
repo: 'test/plugin1',
|
||||
version: 'v1.0.0',
|
||||
package: 'plugin1.zip',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'marketplace',
|
||||
value: {
|
||||
plugin_unique_identifier: 'plugin-2-uid',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
// Mock external dependencies
|
||||
const mockRefreshPluginList = vi.fn()
|
||||
vi.mock('../hooks/use-refresh-plugin-list', () => ({
|
||||
default: () => ({ refreshPluginList: mockRefreshPluginList }),
|
||||
}))
|
||||
|
||||
let mockHideLogicState = {
|
||||
modalClassName: 'test-modal-class',
|
||||
foldAnimInto: vi.fn(),
|
||||
setIsInstalling: vi.fn(),
|
||||
handleStartToInstall: vi.fn(),
|
||||
}
|
||||
vi.mock('../hooks/use-hide-logic', () => ({
|
||||
default: () => mockHideLogicState,
|
||||
}))
|
||||
|
||||
// Mock child components
|
||||
vi.mock('./steps/install', () => ({
|
||||
default: ({
|
||||
uniqueIdentifier,
|
||||
payload,
|
||||
onCancel,
|
||||
onInstalled,
|
||||
onFailed,
|
||||
onStartToInstall,
|
||||
}: {
|
||||
uniqueIdentifier: string
|
||||
payload: PluginManifestInMarket | Plugin
|
||||
onCancel: () => void
|
||||
onInstalled: (notRefresh?: boolean) => void
|
||||
onFailed: (message?: string) => void
|
||||
onStartToInstall: () => void
|
||||
}) => (
|
||||
<div data-testid="install-step">
|
||||
<span data-testid="unique-identifier">{uniqueIdentifier}</span>
|
||||
<span data-testid="payload-name">{payload?.name}</span>
|
||||
<button data-testid="cancel-btn" onClick={onCancel}>Cancel</button>
|
||||
<button data-testid="start-install-btn" onClick={onStartToInstall}>Start Install</button>
|
||||
<button data-testid="install-success-btn" onClick={() => onInstalled()}>Install Success</button>
|
||||
<button data-testid="install-success-no-refresh-btn" onClick={() => onInstalled(true)}>Install Success No Refresh</button>
|
||||
<button data-testid="install-fail-btn" onClick={() => onFailed('Installation failed')}>Install Fail</button>
|
||||
<button data-testid="install-fail-no-msg-btn" onClick={() => onFailed()}>Install Fail No Msg</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../install-bundle/ready-to-install', () => ({
|
||||
default: ({
|
||||
step,
|
||||
onStepChange,
|
||||
onStartToInstall,
|
||||
setIsInstalling,
|
||||
onClose,
|
||||
allPlugins,
|
||||
isFromMarketPlace,
|
||||
}: {
|
||||
step: InstallStep
|
||||
onStepChange: (step: InstallStep) => void
|
||||
onStartToInstall: () => void
|
||||
setIsInstalling: (isInstalling: boolean) => void
|
||||
onClose: () => void
|
||||
allPlugins: Dependency[]
|
||||
isFromMarketPlace?: boolean
|
||||
}) => (
|
||||
<div data-testid="bundle-step">
|
||||
<span data-testid="bundle-step-value">{step}</span>
|
||||
<span data-testid="bundle-plugins-count">{allPlugins?.length || 0}</span>
|
||||
<span data-testid="is-from-marketplace">{isFromMarketPlace ? 'true' : 'false'}</span>
|
||||
<button data-testid="bundle-cancel-btn" onClick={onClose}>Cancel</button>
|
||||
<button data-testid="bundle-start-install-btn" onClick={onStartToInstall}>Start Install</button>
|
||||
<button data-testid="bundle-set-installing-true" onClick={() => setIsInstalling(true)}>Set Installing True</button>
|
||||
<button data-testid="bundle-set-installing-false" onClick={() => setIsInstalling(false)}>Set Installing False</button>
|
||||
<button data-testid="bundle-change-to-installed" onClick={() => onStepChange(InstallStep.installed)}>Change to Installed</button>
|
||||
<button data-testid="bundle-change-to-failed" onClick={() => onStepChange(InstallStep.installFailed)}>Change to Failed</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../base/installed', () => ({
|
||||
default: ({
|
||||
payload,
|
||||
isMarketPayload,
|
||||
isFailed,
|
||||
errMsg,
|
||||
onCancel,
|
||||
}: {
|
||||
payload: PluginManifestInMarket | Plugin | null
|
||||
isMarketPayload?: boolean
|
||||
isFailed: boolean
|
||||
errMsg?: string | null
|
||||
onCancel: () => void
|
||||
}) => (
|
||||
<div data-testid="installed-step">
|
||||
<span data-testid="installed-payload">{payload?.name || 'no-payload'}</span>
|
||||
<span data-testid="is-market-payload">{isMarketPayload ? 'true' : 'false'}</span>
|
||||
<span data-testid="is-failed">{isFailed ? 'true' : 'false'}</span>
|
||||
<span data-testid="error-msg">{errMsg || 'no-error'}</span>
|
||||
<button data-testid="installed-close-btn" onClick={onCancel}>Close</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
describe('InstallFromMarketplace', () => {
|
||||
const defaultProps = {
|
||||
uniqueIdentifier: 'test-unique-identifier',
|
||||
manifest: createMockManifest(),
|
||||
onSuccess: vi.fn(),
|
||||
onClose: vi.fn(),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockHideLogicState = {
|
||||
modalClassName: 'test-modal-class',
|
||||
foldAnimInto: vi.fn(),
|
||||
setIsInstalling: vi.fn(),
|
||||
handleStartToInstall: vi.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Rendering Tests
|
||||
// ================================
|
||||
describe('Rendering', () => {
|
||||
it('should render modal with correct initial state for single plugin', () => {
|
||||
render(<InstallFromMarketplace {...defaultProps} />)
|
||||
|
||||
expect(screen.getByTestId('install-step')).toBeInTheDocument()
|
||||
expect(screen.getByText('plugin.installModal.installPlugin')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render with bundle step when isBundle is true', () => {
|
||||
const dependencies = createMockDependencies()
|
||||
render(
|
||||
<InstallFromMarketplace
|
||||
{...defaultProps}
|
||||
isBundle={true}
|
||||
dependencies={dependencies}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('bundle-step')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('bundle-plugins-count')).toHaveTextContent('2')
|
||||
})
|
||||
|
||||
it('should pass isFromMarketPlace as true to bundle component', () => {
|
||||
const dependencies = createMockDependencies()
|
||||
render(
|
||||
<InstallFromMarketplace
|
||||
{...defaultProps}
|
||||
isBundle={true}
|
||||
dependencies={dependencies}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('is-from-marketplace')).toHaveTextContent('true')
|
||||
})
|
||||
|
||||
it('should pass correct props to Install component', () => {
|
||||
render(<InstallFromMarketplace {...defaultProps} />)
|
||||
|
||||
expect(screen.getByTestId('unique-identifier')).toHaveTextContent('test-unique-identifier')
|
||||
expect(screen.getByTestId('payload-name')).toHaveTextContent('Test Plugin')
|
||||
})
|
||||
|
||||
it('should apply modal className from useHideLogic', () => {
|
||||
expect(mockHideLogicState.modalClassName).toBe('test-modal-class')
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Title Display Tests
|
||||
// ================================
|
||||
describe('Title Display', () => {
|
||||
it('should show install title in readyToInstall step', () => {
|
||||
render(<InstallFromMarketplace {...defaultProps} />)
|
||||
|
||||
expect(screen.getByText('plugin.installModal.installPlugin')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show success title when installation completes for single plugin', async () => {
|
||||
render(<InstallFromMarketplace {...defaultProps} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('install-success-btn'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('plugin.installModal.installedSuccessfully')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should show bundle complete title when bundle installation completes', async () => {
|
||||
const dependencies = createMockDependencies()
|
||||
render(
|
||||
<InstallFromMarketplace
|
||||
{...defaultProps}
|
||||
isBundle={true}
|
||||
dependencies={dependencies}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('bundle-change-to-installed'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('plugin.installModal.installComplete')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should show failed title when installation fails', async () => {
|
||||
render(<InstallFromMarketplace {...defaultProps} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('install-fail-btn'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('plugin.installModal.installFailed')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// State Management Tests
|
||||
// ================================
|
||||
describe('State Management', () => {
|
||||
it('should transition from readyToInstall to installed on success', async () => {
|
||||
render(<InstallFromMarketplace {...defaultProps} />)
|
||||
|
||||
expect(screen.getByTestId('install-step')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByTestId('install-success-btn'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('installed-step')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('is-failed')).toHaveTextContent('false')
|
||||
})
|
||||
})
|
||||
|
||||
it('should transition from readyToInstall to installFailed on failure', async () => {
|
||||
render(<InstallFromMarketplace {...defaultProps} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('install-fail-btn'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('installed-step')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('is-failed')).toHaveTextContent('true')
|
||||
expect(screen.getByTestId('error-msg')).toHaveTextContent('Installation failed')
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle failure without error message', async () => {
|
||||
render(<InstallFromMarketplace {...defaultProps} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('install-fail-no-msg-btn'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('installed-step')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('is-failed')).toHaveTextContent('true')
|
||||
expect(screen.getByTestId('error-msg')).toHaveTextContent('no-error')
|
||||
})
|
||||
})
|
||||
|
||||
it('should update step via onStepChange in bundle mode', async () => {
|
||||
const dependencies = createMockDependencies()
|
||||
render(
|
||||
<InstallFromMarketplace
|
||||
{...defaultProps}
|
||||
isBundle={true}
|
||||
dependencies={dependencies}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('bundle-change-to-installed'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('plugin.installModal.installComplete')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Callback Stability Tests (Memoization)
|
||||
// ================================
|
||||
describe('Callback Stability', () => {
|
||||
it('should maintain stable getTitle callback across rerenders', () => {
|
||||
const { rerender } = render(<InstallFromMarketplace {...defaultProps} />)
|
||||
|
||||
expect(screen.getByText('plugin.installModal.installPlugin')).toBeInTheDocument()
|
||||
|
||||
rerender(<InstallFromMarketplace {...defaultProps} />)
|
||||
|
||||
expect(screen.getByText('plugin.installModal.installPlugin')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should maintain stable handleInstalled callback', async () => {
|
||||
const { rerender } = render(<InstallFromMarketplace {...defaultProps} />)
|
||||
|
||||
rerender(<InstallFromMarketplace {...defaultProps} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('install-success-btn'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('installed-step')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should maintain stable handleFailed callback', async () => {
|
||||
const { rerender } = render(<InstallFromMarketplace {...defaultProps} />)
|
||||
|
||||
rerender(<InstallFromMarketplace {...defaultProps} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('install-fail-btn'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('installed-step')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('is-failed')).toHaveTextContent('true')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// User Interactions Tests
|
||||
// ================================
|
||||
describe('User Interactions', () => {
|
||||
it('should call onClose when cancel is clicked', () => {
|
||||
render(<InstallFromMarketplace {...defaultProps} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('cancel-btn'))
|
||||
|
||||
expect(defaultProps.onClose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should call foldAnimInto when modal close is triggered', () => {
|
||||
render(<InstallFromMarketplace {...defaultProps} />)
|
||||
|
||||
expect(mockHideLogicState.foldAnimInto).toBeDefined()
|
||||
})
|
||||
|
||||
it('should call handleStartToInstall when start install is triggered', () => {
|
||||
render(<InstallFromMarketplace {...defaultProps} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('start-install-btn'))
|
||||
|
||||
expect(mockHideLogicState.handleStartToInstall).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should call onSuccess when close button is clicked in installed step', async () => {
|
||||
render(<InstallFromMarketplace {...defaultProps} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('install-success-btn'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('installed-step')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByTestId('installed-close-btn'))
|
||||
|
||||
expect(defaultProps.onSuccess).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should call onClose in bundle mode cancel', () => {
|
||||
const dependencies = createMockDependencies()
|
||||
render(
|
||||
<InstallFromMarketplace
|
||||
{...defaultProps}
|
||||
isBundle={true}
|
||||
dependencies={dependencies}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('bundle-cancel-btn'))
|
||||
|
||||
expect(defaultProps.onClose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Refresh Plugin List Tests
|
||||
// ================================
|
||||
describe('Refresh Plugin List', () => {
|
||||
it('should call refreshPluginList when installation completes without notRefresh flag', async () => {
|
||||
render(<InstallFromMarketplace {...defaultProps} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('install-success-btn'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockRefreshPluginList).toHaveBeenCalledWith(defaultProps.manifest)
|
||||
})
|
||||
})
|
||||
|
||||
it('should not call refreshPluginList when notRefresh flag is true', async () => {
|
||||
render(<InstallFromMarketplace {...defaultProps} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('install-success-no-refresh-btn'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockRefreshPluginList).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// setIsInstalling Tests
|
||||
// ================================
|
||||
describe('setIsInstalling Behavior', () => {
|
||||
it('should call setIsInstalling(false) when installation completes', async () => {
|
||||
render(<InstallFromMarketplace {...defaultProps} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('install-success-btn'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockHideLogicState.setIsInstalling).toHaveBeenCalledWith(false)
|
||||
})
|
||||
})
|
||||
|
||||
it('should call setIsInstalling(false) when installation fails', async () => {
|
||||
render(<InstallFromMarketplace {...defaultProps} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('install-fail-btn'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockHideLogicState.setIsInstalling).toHaveBeenCalledWith(false)
|
||||
})
|
||||
})
|
||||
|
||||
it('should pass setIsInstalling to bundle component', () => {
|
||||
const dependencies = createMockDependencies()
|
||||
render(
|
||||
<InstallFromMarketplace
|
||||
{...defaultProps}
|
||||
isBundle={true}
|
||||
dependencies={dependencies}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('bundle-set-installing-true'))
|
||||
expect(mockHideLogicState.setIsInstalling).toHaveBeenCalledWith(true)
|
||||
|
||||
fireEvent.click(screen.getByTestId('bundle-set-installing-false'))
|
||||
expect(mockHideLogicState.setIsInstalling).toHaveBeenCalledWith(false)
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Installed Component Props Tests
|
||||
// ================================
|
||||
describe('Installed Component Props', () => {
|
||||
it('should pass isMarketPayload as true to Installed component', async () => {
|
||||
render(<InstallFromMarketplace {...defaultProps} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('install-success-btn'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('is-market-payload')).toHaveTextContent('true')
|
||||
})
|
||||
})
|
||||
|
||||
it('should pass correct payload to Installed component', async () => {
|
||||
render(<InstallFromMarketplace {...defaultProps} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('install-success-btn'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('installed-payload')).toHaveTextContent('Test Plugin')
|
||||
})
|
||||
})
|
||||
|
||||
it('should pass isFailed as true when installation fails', async () => {
|
||||
render(<InstallFromMarketplace {...defaultProps} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('install-fail-btn'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('is-failed')).toHaveTextContent('true')
|
||||
})
|
||||
})
|
||||
|
||||
it('should pass error message to Installed component on failure', async () => {
|
||||
render(<InstallFromMarketplace {...defaultProps} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('install-fail-btn'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('error-msg')).toHaveTextContent('Installation failed')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Prop Variations Tests
|
||||
// ================================
|
||||
describe('Prop Variations', () => {
|
||||
it('should work with Plugin type manifest', () => {
|
||||
const plugin = createMockPlugin()
|
||||
render(
|
||||
<InstallFromMarketplace
|
||||
{...defaultProps}
|
||||
manifest={plugin}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('payload-name')).toHaveTextContent('Test Plugin')
|
||||
})
|
||||
|
||||
it('should work with PluginManifestInMarket type manifest', () => {
|
||||
const manifest = createMockManifest({ name: 'Market Plugin' })
|
||||
render(
|
||||
<InstallFromMarketplace
|
||||
{...defaultProps}
|
||||
manifest={manifest}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('payload-name')).toHaveTextContent('Market Plugin')
|
||||
})
|
||||
|
||||
it('should handle different uniqueIdentifier values', () => {
|
||||
render(
|
||||
<InstallFromMarketplace
|
||||
{...defaultProps}
|
||||
uniqueIdentifier="custom-unique-id-123"
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('unique-identifier')).toHaveTextContent('custom-unique-id-123')
|
||||
})
|
||||
|
||||
it('should work without isBundle prop (default to single plugin)', () => {
|
||||
render(<InstallFromMarketplace {...defaultProps} />)
|
||||
|
||||
expect(screen.getByTestId('install-step')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('bundle-step')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should work with isBundle=false', () => {
|
||||
render(
|
||||
<InstallFromMarketplace
|
||||
{...defaultProps}
|
||||
isBundle={false}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('install-step')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('bundle-step')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should work with empty dependencies array in bundle mode', () => {
|
||||
render(
|
||||
<InstallFromMarketplace
|
||||
{...defaultProps}
|
||||
isBundle={true}
|
||||
dependencies={[]}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('bundle-step')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('bundle-plugins-count')).toHaveTextContent('0')
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Edge Cases Tests
|
||||
// ================================
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle manifest with minimal required fields', () => {
|
||||
const minimalManifest = createMockManifest({
|
||||
name: 'Minimal',
|
||||
version: '0.0.1',
|
||||
})
|
||||
render(
|
||||
<InstallFromMarketplace
|
||||
{...defaultProps}
|
||||
manifest={minimalManifest}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('payload-name')).toHaveTextContent('Minimal')
|
||||
})
|
||||
|
||||
it('should handle multiple rapid state transitions', async () => {
|
||||
render(<InstallFromMarketplace {...defaultProps} />)
|
||||
|
||||
// Trigger installation completion
|
||||
fireEvent.click(screen.getByTestId('install-success-btn'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('installed-step')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Should stay in installed state
|
||||
expect(screen.getByTestId('is-failed')).toHaveTextContent('false')
|
||||
})
|
||||
|
||||
it('should handle bundle mode step changes', async () => {
|
||||
const dependencies = createMockDependencies()
|
||||
render(
|
||||
<InstallFromMarketplace
|
||||
{...defaultProps}
|
||||
isBundle={true}
|
||||
dependencies={dependencies}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Change to installed step
|
||||
fireEvent.click(screen.getByTestId('bundle-change-to-installed'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('plugin.installModal.installComplete')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle bundle mode failure step change', async () => {
|
||||
const dependencies = createMockDependencies()
|
||||
render(
|
||||
<InstallFromMarketplace
|
||||
{...defaultProps}
|
||||
isBundle={true}
|
||||
dependencies={dependencies}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('bundle-change-to-failed'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('plugin.installModal.installFailed')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should not render Install component in terminal steps', async () => {
|
||||
render(<InstallFromMarketplace {...defaultProps} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('install-success-btn'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('install-step')).not.toBeInTheDocument()
|
||||
expect(screen.getByTestId('installed-step')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should render Installed component for success state with isFailed false', async () => {
|
||||
render(<InstallFromMarketplace {...defaultProps} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('install-success-btn'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('installed-step')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('is-failed')).toHaveTextContent('false')
|
||||
})
|
||||
})
|
||||
|
||||
it('should render Installed component for failure state with isFailed true', async () => {
|
||||
render(<InstallFromMarketplace {...defaultProps} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('install-fail-btn'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('installed-step')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('is-failed')).toHaveTextContent('true')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Terminal Steps Rendering Tests
|
||||
// ================================
|
||||
describe('Terminal Steps Rendering', () => {
|
||||
it('should render Installed component when step is installed', async () => {
|
||||
render(<InstallFromMarketplace {...defaultProps} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('install-success-btn'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('installed-step')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should render Installed component when step is installFailed', async () => {
|
||||
render(<InstallFromMarketplace {...defaultProps} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('install-fail-btn'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('installed-step')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('is-failed')).toHaveTextContent('true')
|
||||
})
|
||||
})
|
||||
|
||||
it('should not render Install component when in terminal step', async () => {
|
||||
render(<InstallFromMarketplace {...defaultProps} />)
|
||||
|
||||
// Initially Install is shown
|
||||
expect(screen.getByTestId('install-step')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByTestId('install-success-btn'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('install-step')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Data Flow Tests
|
||||
// ================================
|
||||
describe('Data Flow', () => {
|
||||
it('should pass uniqueIdentifier to Install component', () => {
|
||||
render(<InstallFromMarketplace {...defaultProps} uniqueIdentifier="flow-test-id" />)
|
||||
|
||||
expect(screen.getByTestId('unique-identifier')).toHaveTextContent('flow-test-id')
|
||||
})
|
||||
|
||||
it('should pass manifest payload to Install component', () => {
|
||||
const customManifest = createMockManifest({ name: 'Flow Test Plugin' })
|
||||
render(<InstallFromMarketplace {...defaultProps} manifest={customManifest} />)
|
||||
|
||||
expect(screen.getByTestId('payload-name')).toHaveTextContent('Flow Test Plugin')
|
||||
})
|
||||
|
||||
it('should pass dependencies to bundle component', () => {
|
||||
const dependencies = createMockDependencies()
|
||||
render(
|
||||
<InstallFromMarketplace
|
||||
{...defaultProps}
|
||||
isBundle={true}
|
||||
dependencies={dependencies}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('bundle-plugins-count')).toHaveTextContent('2')
|
||||
})
|
||||
|
||||
it('should pass current step to bundle component', () => {
|
||||
const dependencies = createMockDependencies()
|
||||
render(
|
||||
<InstallFromMarketplace
|
||||
{...defaultProps}
|
||||
isBundle={true}
|
||||
dependencies={dependencies}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('bundle-step-value')).toHaveTextContent(InstallStep.readyToInstall)
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Manifest Category Variations Tests
|
||||
// ================================
|
||||
describe('Manifest Category Variations', () => {
|
||||
it('should handle tool category manifest', () => {
|
||||
const manifest = createMockManifest({ category: PluginCategoryEnum.tool })
|
||||
render(<InstallFromMarketplace {...defaultProps} manifest={manifest} />)
|
||||
|
||||
expect(screen.getByTestId('install-step')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle model category manifest', () => {
|
||||
const manifest = createMockManifest({ category: PluginCategoryEnum.model })
|
||||
render(<InstallFromMarketplace {...defaultProps} manifest={manifest} />)
|
||||
|
||||
expect(screen.getByTestId('install-step')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle extension category manifest', () => {
|
||||
const manifest = createMockManifest({ category: PluginCategoryEnum.extension })
|
||||
render(<InstallFromMarketplace {...defaultProps} manifest={manifest} />)
|
||||
|
||||
expect(screen.getByTestId('install-step')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Hook Integration Tests
|
||||
// ================================
|
||||
describe('Hook Integration', () => {
|
||||
it('should use handleStartToInstall from useHideLogic', () => {
|
||||
render(<InstallFromMarketplace {...defaultProps} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('start-install-btn'))
|
||||
|
||||
expect(mockHideLogicState.handleStartToInstall).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should use setIsInstalling from useHideLogic in handleInstalled', async () => {
|
||||
render(<InstallFromMarketplace {...defaultProps} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('install-success-btn'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockHideLogicState.setIsInstalling).toHaveBeenCalledWith(false)
|
||||
})
|
||||
})
|
||||
|
||||
it('should use setIsInstalling from useHideLogic in handleFailed', async () => {
|
||||
render(<InstallFromMarketplace {...defaultProps} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('install-fail-btn'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockHideLogicState.setIsInstalling).toHaveBeenCalledWith(false)
|
||||
})
|
||||
})
|
||||
|
||||
it('should use refreshPluginList from useRefreshPluginList', async () => {
|
||||
render(<InstallFromMarketplace {...defaultProps} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('install-success-btn'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockRefreshPluginList).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// getTitle Memoization Tests
|
||||
// ================================
|
||||
describe('getTitle Memoization', () => {
|
||||
it('should return installPlugin title for readyToInstall step', () => {
|
||||
render(<InstallFromMarketplace {...defaultProps} />)
|
||||
|
||||
expect(screen.getByText('plugin.installModal.installPlugin')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should return installedSuccessfully for non-bundle installed step', async () => {
|
||||
render(<InstallFromMarketplace {...defaultProps} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('install-success-btn'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('plugin.installModal.installedSuccessfully')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should return installComplete for bundle installed step', async () => {
|
||||
const dependencies = createMockDependencies()
|
||||
render(
|
||||
<InstallFromMarketplace
|
||||
{...defaultProps}
|
||||
isBundle={true}
|
||||
dependencies={dependencies}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('bundle-change-to-installed'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('plugin.installModal.installComplete')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should return installFailed for installFailed step', async () => {
|
||||
render(<InstallFromMarketplace {...defaultProps} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('install-fail-btn'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('plugin.installModal.installFailed')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,18 +1,19 @@
|
||||
'use client'
|
||||
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import type { Dependency, Plugin, PluginManifestInMarket } from '../../types'
|
||||
import { InstallStep } from '../../types'
|
||||
import Install from './steps/install'
|
||||
import Installed from '../base/installed'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { InstallStep } from '../../types'
|
||||
import Installed from '../base/installed'
|
||||
import useHideLogic from '../hooks/use-hide-logic'
|
||||
import useRefreshPluginList from '../hooks/use-refresh-plugin-list'
|
||||
import ReadyToInstallBundle from '../install-bundle/ready-to-install'
|
||||
import cn from '@/utils/classnames'
|
||||
import useHideLogic from '../hooks/use-hide-logic'
|
||||
import Install from './steps/install'
|
||||
|
||||
const i18nPrefix = 'plugin.installModal'
|
||||
const i18nPrefix = 'installModal'
|
||||
|
||||
type InstallFromMarketplaceProps = {
|
||||
uniqueIdentifier: string
|
||||
@ -46,12 +47,12 @@ const InstallFromMarketplace: React.FC<InstallFromMarketplaceProps> = ({
|
||||
|
||||
const getTitle = useCallback(() => {
|
||||
if (isBundle && step === InstallStep.installed)
|
||||
return t(`${i18nPrefix}.installComplete`)
|
||||
return t(`${i18nPrefix}.installComplete`, { ns: 'plugin' })
|
||||
if (step === InstallStep.installed)
|
||||
return t(`${i18nPrefix}.installedSuccessfully`)
|
||||
return t(`${i18nPrefix}.installedSuccessfully`, { ns: 'plugin' })
|
||||
if (step === InstallStep.installFailed)
|
||||
return t(`${i18nPrefix}.installFailed`)
|
||||
return t(`${i18nPrefix}.installPlugin`)
|
||||
return t(`${i18nPrefix}.installFailed`, { ns: 'plugin' })
|
||||
return t(`${i18nPrefix}.installPlugin`, { ns: 'plugin' })
|
||||
}, [isBundle, step, t])
|
||||
|
||||
const handleInstalled = useCallback((notRefresh?: boolean) => {
|
||||
@ -72,53 +73,57 @@ const InstallFromMarketplace: React.FC<InstallFromMarketplaceProps> = ({
|
||||
<Modal
|
||||
isShow={true}
|
||||
onClose={foldAnimInto}
|
||||
wrapperClassName='z-[9999]'
|
||||
wrapperClassName="z-[9999]"
|
||||
className={cn(modalClassName, 'shadows-shadow-xl flex min-w-[560px] flex-col items-start rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-0')}
|
||||
closable
|
||||
>
|
||||
<div className='flex items-start gap-2 self-stretch pb-3 pl-6 pr-14 pt-6'>
|
||||
<div className='title-2xl-semi-bold self-stretch text-text-primary'>
|
||||
<div className="flex items-start gap-2 self-stretch pb-3 pl-6 pr-14 pt-6">
|
||||
<div className="title-2xl-semi-bold self-stretch text-text-primary">
|
||||
{getTitle()}
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
isBundle ? (
|
||||
<ReadyToInstallBundle
|
||||
step={step}
|
||||
onStepChange={setStep}
|
||||
onStartToInstall={handleStartToInstall}
|
||||
setIsInstalling={setIsInstalling}
|
||||
onClose={onClose}
|
||||
allPlugins={dependencies!}
|
||||
isFromMarketPlace
|
||||
/>
|
||||
) : (<>
|
||||
{
|
||||
step === InstallStep.readyToInstall && (
|
||||
<Install
|
||||
uniqueIdentifier={uniqueIdentifier}
|
||||
payload={manifest!}
|
||||
onCancel={onClose}
|
||||
onInstalled={handleInstalled}
|
||||
onFailed={handleFailed}
|
||||
isBundle
|
||||
? (
|
||||
<ReadyToInstallBundle
|
||||
step={step}
|
||||
onStepChange={setStep}
|
||||
onStartToInstall={handleStartToInstall}
|
||||
/>
|
||||
)}
|
||||
{
|
||||
[InstallStep.installed, InstallStep.installFailed].includes(step) && (
|
||||
<Installed
|
||||
payload={manifest!}
|
||||
isMarketPayload
|
||||
isFailed={step === InstallStep.installFailed}
|
||||
errMsg={errorMsg}
|
||||
onCancel={onSuccess}
|
||||
setIsInstalling={setIsInstalling}
|
||||
onClose={onClose}
|
||||
allPlugins={dependencies!}
|
||||
isFromMarketPlace
|
||||
/>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
{
|
||||
step === InstallStep.readyToInstall && (
|
||||
<Install
|
||||
uniqueIdentifier={uniqueIdentifier}
|
||||
payload={manifest!}
|
||||
onCancel={onClose}
|
||||
onInstalled={handleInstalled}
|
||||
onFailed={handleFailed}
|
||||
onStartToInstall={handleStartToInstall}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
[InstallStep.installed, InstallStep.installFailed].includes(step) && (
|
||||
<Installed
|
||||
payload={manifest!}
|
||||
isMarketPayload
|
||||
isFailed={step === InstallStep.installFailed}
|
||||
errMsg={errorMsg}
|
||||
onCancel={onSuccess}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
</Modal >
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,729 @@
|
||||
import type { Plugin, PluginManifestInMarket } from '../../../types'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { act } from 'react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { PluginCategoryEnum, TaskStatus } from '../../../types'
|
||||
import Install from './install'
|
||||
|
||||
// Factory functions for test data
|
||||
const createMockManifest = (overrides: Partial<PluginManifestInMarket> = {}): PluginManifestInMarket => ({
|
||||
plugin_unique_identifier: 'test-unique-identifier',
|
||||
name: 'Test Plugin',
|
||||
org: 'test-org',
|
||||
icon: 'test-icon.png',
|
||||
label: { en_US: 'Test Plugin' } as PluginManifestInMarket['label'],
|
||||
category: PluginCategoryEnum.tool,
|
||||
version: '1.0.0',
|
||||
latest_version: '1.0.0',
|
||||
brief: { en_US: 'A test plugin' } as PluginManifestInMarket['brief'],
|
||||
introduction: 'Introduction text',
|
||||
verified: true,
|
||||
install_count: 100,
|
||||
badges: [],
|
||||
verification: { authorized_category: 'community' },
|
||||
from: 'marketplace',
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const createMockPlugin = (overrides: Partial<Plugin> = {}): Plugin => ({
|
||||
type: 'plugin',
|
||||
org: 'test-org',
|
||||
name: 'Test Plugin',
|
||||
plugin_id: 'test-plugin-id',
|
||||
version: '1.0.0',
|
||||
latest_version: '1.0.0',
|
||||
latest_package_identifier: 'test-package-id',
|
||||
icon: 'test-icon.png',
|
||||
verified: true,
|
||||
label: { en_US: 'Test Plugin' },
|
||||
brief: { en_US: 'A test plugin' },
|
||||
description: { en_US: 'A test plugin description' },
|
||||
introduction: 'Introduction text',
|
||||
repository: 'https://github.com/test/plugin',
|
||||
category: PluginCategoryEnum.tool,
|
||||
install_count: 100,
|
||||
endpoint: { settings: [] },
|
||||
tags: [],
|
||||
badges: [],
|
||||
verification: { authorized_category: 'community' },
|
||||
from: 'marketplace',
|
||||
...overrides,
|
||||
})
|
||||
|
||||
// Mock variables for controlling test behavior
|
||||
let mockInstalledInfo: Record<string, { installedId: string, installedVersion: string, uniqueIdentifier: string }> | undefined
|
||||
let mockIsLoading = false
|
||||
const mockInstallPackageFromMarketPlace = vi.fn()
|
||||
const mockUpdatePackageFromMarketPlace = vi.fn()
|
||||
const mockCheckTaskStatus = vi.fn()
|
||||
const mockStopTaskStatus = vi.fn()
|
||||
const mockHandleRefetch = vi.fn()
|
||||
let mockPluginDeclaration: { manifest: { meta: { minimum_dify_version: string } } } | undefined
|
||||
let mockCanInstall = true
|
||||
let mockLangGeniusVersionInfo = { current_version: '1.0.0' }
|
||||
|
||||
// Mock useCheckInstalled
|
||||
vi.mock('@/app/components/plugins/install-plugin/hooks/use-check-installed', () => ({
|
||||
default: ({ pluginIds }: { pluginIds: string[], enabled: boolean }) => ({
|
||||
installedInfo: mockInstalledInfo,
|
||||
isLoading: mockIsLoading,
|
||||
error: null,
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock service hooks
|
||||
vi.mock('@/service/use-plugins', () => ({
|
||||
useInstallPackageFromMarketPlace: () => ({
|
||||
mutateAsync: mockInstallPackageFromMarketPlace,
|
||||
}),
|
||||
useUpdatePackageFromMarketPlace: () => ({
|
||||
mutateAsync: mockUpdatePackageFromMarketPlace,
|
||||
}),
|
||||
usePluginDeclarationFromMarketPlace: () => ({
|
||||
data: mockPluginDeclaration,
|
||||
}),
|
||||
usePluginTaskList: () => ({
|
||||
handleRefetch: mockHandleRefetch,
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock checkTaskStatus
|
||||
vi.mock('../../base/check-task-status', () => ({
|
||||
default: () => ({
|
||||
check: mockCheckTaskStatus,
|
||||
stop: mockStopTaskStatus,
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock useAppContext
|
||||
vi.mock('@/context/app-context', () => ({
|
||||
useAppContext: () => ({
|
||||
langGeniusVersionInfo: mockLangGeniusVersionInfo,
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock useInstallPluginLimit
|
||||
vi.mock('../../hooks/use-install-plugin-limit', () => ({
|
||||
default: () => ({ canInstall: mockCanInstall }),
|
||||
}))
|
||||
|
||||
// Mock Card component
|
||||
vi.mock('../../../card', () => ({
|
||||
default: ({ payload, titleLeft, className, limitedInstall }: {
|
||||
payload: any
|
||||
titleLeft?: React.ReactNode
|
||||
className?: string
|
||||
limitedInstall?: boolean
|
||||
}) => (
|
||||
<div data-testid="plugin-card">
|
||||
<span data-testid="card-payload-name">{payload?.name}</span>
|
||||
<span data-testid="card-limited-install">{limitedInstall ? 'true' : 'false'}</span>
|
||||
{titleLeft && <div data-testid="card-title-left">{titleLeft}</div>}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock Version component
|
||||
vi.mock('../../base/version', () => ({
|
||||
default: ({ hasInstalled, installedVersion, toInstallVersion }: {
|
||||
hasInstalled: boolean
|
||||
installedVersion?: string
|
||||
toInstallVersion: string
|
||||
}) => (
|
||||
<div data-testid="version-component">
|
||||
<span data-testid="has-installed">{hasInstalled ? 'true' : 'false'}</span>
|
||||
<span data-testid="installed-version">{installedVersion || 'none'}</span>
|
||||
<span data-testid="to-install-version">{toInstallVersion}</span>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock utils
|
||||
vi.mock('../../utils', () => ({
|
||||
pluginManifestInMarketToPluginProps: (payload: PluginManifestInMarket) => ({
|
||||
name: payload.name,
|
||||
icon: payload.icon,
|
||||
category: payload.category,
|
||||
}),
|
||||
}))
|
||||
|
||||
describe('Install Component (steps/install.tsx)', () => {
|
||||
const defaultProps = {
|
||||
uniqueIdentifier: 'test-unique-identifier',
|
||||
payload: createMockManifest(),
|
||||
onCancel: vi.fn(),
|
||||
onStartToInstall: vi.fn(),
|
||||
onInstalled: vi.fn(),
|
||||
onFailed: vi.fn(),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockInstalledInfo = undefined
|
||||
mockIsLoading = false
|
||||
mockPluginDeclaration = undefined
|
||||
mockCanInstall = true
|
||||
mockLangGeniusVersionInfo = { current_version: '1.0.0' }
|
||||
mockInstallPackageFromMarketPlace.mockResolvedValue({
|
||||
all_installed: false,
|
||||
task_id: 'task-123',
|
||||
})
|
||||
mockUpdatePackageFromMarketPlace.mockResolvedValue({
|
||||
all_installed: false,
|
||||
task_id: 'task-456',
|
||||
})
|
||||
mockCheckTaskStatus.mockResolvedValue({
|
||||
status: TaskStatus.success,
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Rendering Tests
|
||||
// ================================
|
||||
describe('Rendering', () => {
|
||||
it('should render ready to install text', () => {
|
||||
render(<Install {...defaultProps} />)
|
||||
|
||||
expect(screen.getByText('plugin.installModal.readyToInstall')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render plugin card with correct payload', () => {
|
||||
render(<Install {...defaultProps} />)
|
||||
|
||||
expect(screen.getByTestId('plugin-card')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('card-payload-name')).toHaveTextContent('Test Plugin')
|
||||
})
|
||||
|
||||
it('should render cancel button when not installing', () => {
|
||||
render(<Install {...defaultProps} />)
|
||||
|
||||
expect(screen.getByText('common.operation.cancel')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render install button', () => {
|
||||
render(<Install {...defaultProps} />)
|
||||
|
||||
expect(screen.getByText('plugin.installModal.install')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render version component while loading', () => {
|
||||
mockIsLoading = true
|
||||
render(<Install {...defaultProps} />)
|
||||
|
||||
expect(screen.queryByTestId('version-component')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render version component when not loading', () => {
|
||||
mockIsLoading = false
|
||||
render(<Install {...defaultProps} />)
|
||||
|
||||
expect(screen.getByTestId('version-component')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Version Display Tests
|
||||
// ================================
|
||||
describe('Version Display', () => {
|
||||
it('should show hasInstalled as false when not installed', () => {
|
||||
mockInstalledInfo = undefined
|
||||
render(<Install {...defaultProps} />)
|
||||
|
||||
expect(screen.getByTestId('has-installed')).toHaveTextContent('false')
|
||||
})
|
||||
|
||||
it('should show hasInstalled as true when already installed', () => {
|
||||
mockInstalledInfo = {
|
||||
'test-plugin-id': {
|
||||
installedId: 'install-id',
|
||||
installedVersion: '0.9.0',
|
||||
uniqueIdentifier: 'old-unique-id',
|
||||
},
|
||||
}
|
||||
const plugin = createMockPlugin()
|
||||
render(<Install {...defaultProps} payload={plugin} />)
|
||||
|
||||
expect(screen.getByTestId('has-installed')).toHaveTextContent('true')
|
||||
expect(screen.getByTestId('installed-version')).toHaveTextContent('0.9.0')
|
||||
})
|
||||
|
||||
it('should show correct toInstallVersion from payload.version', () => {
|
||||
const manifest = createMockManifest({ version: '2.0.0' })
|
||||
render(<Install {...defaultProps} payload={manifest} />)
|
||||
|
||||
expect(screen.getByTestId('to-install-version')).toHaveTextContent('2.0.0')
|
||||
})
|
||||
|
||||
it('should fallback to latest_version when version is undefined', () => {
|
||||
const manifest = createMockManifest({ version: undefined as any, latest_version: '3.0.0' })
|
||||
render(<Install {...defaultProps} payload={manifest} />)
|
||||
|
||||
expect(screen.getByTestId('to-install-version')).toHaveTextContent('3.0.0')
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Version Compatibility Tests
|
||||
// ================================
|
||||
describe('Version Compatibility', () => {
|
||||
it('should not show warning when no plugin declaration', () => {
|
||||
mockPluginDeclaration = undefined
|
||||
render(<Install {...defaultProps} />)
|
||||
|
||||
expect(screen.queryByText(/difyVersionNotCompatible/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not show warning when dify version is compatible', () => {
|
||||
mockLangGeniusVersionInfo = { current_version: '2.0.0' }
|
||||
mockPluginDeclaration = {
|
||||
manifest: { meta: { minimum_dify_version: '1.0.0' } },
|
||||
}
|
||||
render(<Install {...defaultProps} />)
|
||||
|
||||
expect(screen.queryByText(/difyVersionNotCompatible/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show warning when dify version is incompatible', () => {
|
||||
mockLangGeniusVersionInfo = { current_version: '1.0.0' }
|
||||
mockPluginDeclaration = {
|
||||
manifest: { meta: { minimum_dify_version: '2.0.0' } },
|
||||
}
|
||||
render(<Install {...defaultProps} />)
|
||||
|
||||
expect(screen.getByText(/plugin.difyVersionNotCompatible/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Install Limit Tests
|
||||
// ================================
|
||||
describe('Install Limit', () => {
|
||||
it('should pass limitedInstall=false to Card when canInstall is true', () => {
|
||||
mockCanInstall = true
|
||||
render(<Install {...defaultProps} />)
|
||||
|
||||
expect(screen.getByTestId('card-limited-install')).toHaveTextContent('false')
|
||||
})
|
||||
|
||||
it('should pass limitedInstall=true to Card when canInstall is false', () => {
|
||||
mockCanInstall = false
|
||||
render(<Install {...defaultProps} />)
|
||||
|
||||
expect(screen.getByTestId('card-limited-install')).toHaveTextContent('true')
|
||||
})
|
||||
|
||||
it('should disable install button when canInstall is false', () => {
|
||||
mockCanInstall = false
|
||||
render(<Install {...defaultProps} />)
|
||||
|
||||
const installBtn = screen.getByText('plugin.installModal.install').closest('button')
|
||||
expect(installBtn).toBeDisabled()
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Button States Tests
|
||||
// ================================
|
||||
describe('Button States', () => {
|
||||
it('should disable install button when loading', () => {
|
||||
mockIsLoading = true
|
||||
render(<Install {...defaultProps} />)
|
||||
|
||||
const installBtn = screen.getByText('plugin.installModal.install').closest('button')
|
||||
expect(installBtn).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should enable install button when not loading and canInstall', () => {
|
||||
mockIsLoading = false
|
||||
mockCanInstall = true
|
||||
render(<Install {...defaultProps} />)
|
||||
|
||||
const installBtn = screen.getByText('plugin.installModal.install').closest('button')
|
||||
expect(installBtn).not.toBeDisabled()
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Cancel Button Tests
|
||||
// ================================
|
||||
describe('Cancel Button', () => {
|
||||
it('should call onCancel and stop when cancel is clicked', () => {
|
||||
render(<Install {...defaultProps} />)
|
||||
|
||||
fireEvent.click(screen.getByText('common.operation.cancel'))
|
||||
|
||||
expect(mockStopTaskStatus).toHaveBeenCalled()
|
||||
expect(defaultProps.onCancel).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// New Installation Flow Tests
|
||||
// ================================
|
||||
describe('New Installation Flow', () => {
|
||||
it('should call onStartToInstall when install button is clicked', async () => {
|
||||
render(<Install {...defaultProps} />)
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('plugin.installModal.install'))
|
||||
})
|
||||
|
||||
expect(defaultProps.onStartToInstall).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should call installPackageFromMarketPlace for new installation', async () => {
|
||||
mockInstalledInfo = undefined
|
||||
render(<Install {...defaultProps} />)
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('plugin.installModal.install'))
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockInstallPackageFromMarketPlace).toHaveBeenCalledWith('test-unique-identifier')
|
||||
})
|
||||
})
|
||||
|
||||
it('should call onInstalled immediately when all_installed is true', async () => {
|
||||
mockInstallPackageFromMarketPlace.mockResolvedValue({
|
||||
all_installed: true,
|
||||
task_id: 'task-123',
|
||||
})
|
||||
render(<Install {...defaultProps} />)
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('plugin.installModal.install'))
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(defaultProps.onInstalled).toHaveBeenCalled()
|
||||
expect(mockCheckTaskStatus).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it('should check task status when all_installed is false', async () => {
|
||||
mockInstallPackageFromMarketPlace.mockResolvedValue({
|
||||
all_installed: false,
|
||||
task_id: 'task-123',
|
||||
})
|
||||
render(<Install {...defaultProps} />)
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('plugin.installModal.install'))
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockHandleRefetch).toHaveBeenCalled()
|
||||
expect(mockCheckTaskStatus).toHaveBeenCalledWith({
|
||||
taskId: 'task-123',
|
||||
pluginUniqueIdentifier: 'test-unique-identifier',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should call onInstalled with true when task succeeds', async () => {
|
||||
mockCheckTaskStatus.mockResolvedValue({ status: TaskStatus.success })
|
||||
render(<Install {...defaultProps} />)
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('plugin.installModal.install'))
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(defaultProps.onInstalled).toHaveBeenCalledWith(true)
|
||||
})
|
||||
})
|
||||
|
||||
it('should call onFailed when task fails', async () => {
|
||||
mockCheckTaskStatus.mockResolvedValue({
|
||||
status: TaskStatus.failed,
|
||||
error: 'Task failed error',
|
||||
})
|
||||
render(<Install {...defaultProps} />)
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('plugin.installModal.install'))
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(defaultProps.onFailed).toHaveBeenCalledWith('Task failed error')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Update Installation Flow Tests
|
||||
// ================================
|
||||
describe('Update Installation Flow', () => {
|
||||
beforeEach(() => {
|
||||
mockInstalledInfo = {
|
||||
'test-plugin-id': {
|
||||
installedId: 'install-id',
|
||||
installedVersion: '0.9.0',
|
||||
uniqueIdentifier: 'old-unique-id',
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
it('should call updatePackageFromMarketPlace for update installation', async () => {
|
||||
const plugin = createMockPlugin()
|
||||
render(<Install {...defaultProps} payload={plugin} />)
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('plugin.installModal.install'))
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdatePackageFromMarketPlace).toHaveBeenCalledWith({
|
||||
original_plugin_unique_identifier: 'old-unique-id',
|
||||
new_plugin_unique_identifier: 'test-unique-identifier',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should not call installPackageFromMarketPlace when updating', async () => {
|
||||
const plugin = createMockPlugin()
|
||||
render(<Install {...defaultProps} payload={plugin} />)
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('plugin.installModal.install'))
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockInstallPackageFromMarketPlace).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Auto-Install on Already Installed Tests
|
||||
// ================================
|
||||
describe('Auto-Install on Already Installed', () => {
|
||||
it('should call onInstalled when already installed with same uniqueIdentifier', async () => {
|
||||
mockInstalledInfo = {
|
||||
'test-plugin-id': {
|
||||
installedId: 'install-id',
|
||||
installedVersion: '1.0.0',
|
||||
uniqueIdentifier: 'test-unique-identifier',
|
||||
},
|
||||
}
|
||||
const plugin = createMockPlugin()
|
||||
render(<Install {...defaultProps} payload={plugin} />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(defaultProps.onInstalled).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it('should not auto-install when uniqueIdentifier differs', async () => {
|
||||
mockInstalledInfo = {
|
||||
'test-plugin-id': {
|
||||
installedId: 'install-id',
|
||||
installedVersion: '1.0.0',
|
||||
uniqueIdentifier: 'different-unique-id',
|
||||
},
|
||||
}
|
||||
const plugin = createMockPlugin()
|
||||
render(<Install {...defaultProps} payload={plugin} />)
|
||||
|
||||
// Wait a bit to ensure onInstalled is not called
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
expect(defaultProps.onInstalled).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Error Handling Tests
|
||||
// ================================
|
||||
describe('Error Handling', () => {
|
||||
it('should call onFailed with string error', async () => {
|
||||
mockInstallPackageFromMarketPlace.mockRejectedValue('String error message')
|
||||
render(<Install {...defaultProps} />)
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('plugin.installModal.install'))
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(defaultProps.onFailed).toHaveBeenCalledWith('String error message')
|
||||
})
|
||||
})
|
||||
|
||||
it('should call onFailed without message for non-string error', async () => {
|
||||
mockInstallPackageFromMarketPlace.mockRejectedValue(new Error('Error object'))
|
||||
render(<Install {...defaultProps} />)
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('plugin.installModal.install'))
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(defaultProps.onFailed).toHaveBeenCalledWith()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Installing State Tests
|
||||
// ================================
|
||||
describe('Installing State', () => {
|
||||
it('should hide cancel button while installing', async () => {
|
||||
// Make the install take some time
|
||||
mockInstallPackageFromMarketPlace.mockImplementation(() => new Promise(() => {}))
|
||||
render(<Install {...defaultProps} />)
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('plugin.installModal.install'))
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('common.operation.cancel')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should show installing text while installing', async () => {
|
||||
mockInstallPackageFromMarketPlace.mockImplementation(() => new Promise(() => {}))
|
||||
render(<Install {...defaultProps} />)
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('plugin.installModal.install'))
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('plugin.installModal.installing')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should disable install button while installing', async () => {
|
||||
mockInstallPackageFromMarketPlace.mockImplementation(() => new Promise(() => {}))
|
||||
render(<Install {...defaultProps} />)
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('plugin.installModal.install'))
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
const installBtn = screen.getByText('plugin.installModal.installing').closest('button')
|
||||
expect(installBtn).toBeDisabled()
|
||||
})
|
||||
})
|
||||
|
||||
it('should not trigger multiple installs when clicking rapidly', async () => {
|
||||
mockInstallPackageFromMarketPlace.mockImplementation(() => new Promise(() => {}))
|
||||
render(<Install {...defaultProps} />)
|
||||
|
||||
const installBtn = screen.getByText('plugin.installModal.install').closest('button')!
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(installBtn)
|
||||
})
|
||||
|
||||
// Wait for the button to be disabled
|
||||
await waitFor(() => {
|
||||
expect(installBtn).toBeDisabled()
|
||||
})
|
||||
|
||||
// Try clicking again - should not trigger another install
|
||||
await act(async () => {
|
||||
fireEvent.click(installBtn)
|
||||
fireEvent.click(installBtn)
|
||||
})
|
||||
|
||||
expect(mockInstallPackageFromMarketPlace).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Prop Variations Tests
|
||||
// ================================
|
||||
describe('Prop Variations', () => {
|
||||
it('should work with PluginManifestInMarket payload', () => {
|
||||
const manifest = createMockManifest({ name: 'Manifest Plugin' })
|
||||
render(<Install {...defaultProps} payload={manifest} />)
|
||||
|
||||
expect(screen.getByTestId('card-payload-name')).toHaveTextContent('Manifest Plugin')
|
||||
})
|
||||
|
||||
it('should work with Plugin payload', () => {
|
||||
const plugin = createMockPlugin({ name: 'Plugin Type' })
|
||||
render(<Install {...defaultProps} payload={plugin} />)
|
||||
|
||||
expect(screen.getByTestId('card-payload-name')).toHaveTextContent('Plugin Type')
|
||||
})
|
||||
|
||||
it('should work without onStartToInstall callback', async () => {
|
||||
const propsWithoutCallback = {
|
||||
...defaultProps,
|
||||
onStartToInstall: undefined,
|
||||
}
|
||||
render(<Install {...propsWithoutCallback} />)
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('plugin.installModal.install'))
|
||||
})
|
||||
|
||||
// Should not throw and should proceed with installation
|
||||
await waitFor(() => {
|
||||
expect(mockInstallPackageFromMarketPlace).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle different uniqueIdentifier values', async () => {
|
||||
render(<Install {...defaultProps} uniqueIdentifier="custom-id-123" />)
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('plugin.installModal.install'))
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockInstallPackageFromMarketPlace).toHaveBeenCalledWith('custom-id-123')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Edge Cases Tests
|
||||
// ================================
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle empty plugin_id gracefully', () => {
|
||||
const manifest = createMockManifest()
|
||||
// Manifest doesn't have plugin_id, so installedInfo won't match
|
||||
render(<Install {...defaultProps} payload={manifest} />)
|
||||
|
||||
expect(screen.getByTestId('has-installed')).toHaveTextContent('false')
|
||||
})
|
||||
|
||||
it('should handle undefined installedInfo', () => {
|
||||
mockInstalledInfo = undefined
|
||||
render(<Install {...defaultProps} />)
|
||||
|
||||
expect(screen.getByTestId('has-installed')).toHaveTextContent('false')
|
||||
})
|
||||
|
||||
it('should handle null current_version in langGeniusVersionInfo', () => {
|
||||
mockLangGeniusVersionInfo = { current_version: null as any }
|
||||
mockPluginDeclaration = {
|
||||
manifest: { meta: { minimum_dify_version: '1.0.0' } },
|
||||
}
|
||||
render(<Install {...defaultProps} />)
|
||||
|
||||
// Should not show warning when current_version is null (defaults to compatible)
|
||||
expect(screen.queryByText(/difyVersionNotCompatible/)).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Component Memoization Tests
|
||||
// ================================
|
||||
describe('Component Memoization', () => {
|
||||
it('should maintain stable component across rerenders with same props', () => {
|
||||
const { rerender } = render(<Install {...defaultProps} />)
|
||||
|
||||
expect(screen.getByTestId('plugin-card')).toBeInTheDocument()
|
||||
|
||||
rerender(<Install {...defaultProps} />)
|
||||
|
||||
expect(screen.getByTestId('plugin-card')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,23 +1,24 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect, useMemo } from 'react'
|
||||
// import { RiInformation2Line } from '@remixicon/react'
|
||||
import { type Plugin, type PluginManifestInMarket, TaskStatus } from '../../../types'
|
||||
import Card from '../../../card'
|
||||
import { pluginManifestInMarketToPluginProps } from '../../utils'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { Plugin, PluginManifestInMarket } from '../../../types'
|
||||
import { RiLoader2Line } from '@remixicon/react'
|
||||
import { useInstallPackageFromMarketPlace, usePluginDeclarationFromMarketPlace, useUpdatePackageFromMarketPlace } from '@/service/use-plugins'
|
||||
import checkTaskStatus from '../../base/check-task-status'
|
||||
import useCheckInstalled from '@/app/components/plugins/install-plugin/hooks/use-check-installed'
|
||||
import Version from '../../base/version'
|
||||
import { usePluginTaskList } from '@/service/use-plugins'
|
||||
import * as React from 'react'
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { gte } from 'semver'
|
||||
import Button from '@/app/components/base/button'
|
||||
import useCheckInstalled from '@/app/components/plugins/install-plugin/hooks/use-check-installed'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useInstallPackageFromMarketPlace, usePluginDeclarationFromMarketPlace, usePluginTaskList, useUpdatePackageFromMarketPlace } from '@/service/use-plugins'
|
||||
import Card from '../../../card'
|
||||
// import { RiInformation2Line } from '@remixicon/react'
|
||||
import { TaskStatus } from '../../../types'
|
||||
import checkTaskStatus from '../../base/check-task-status'
|
||||
import Version from '../../base/version'
|
||||
import useInstallPluginLimit from '../../hooks/use-install-plugin-limit'
|
||||
import { pluginManifestInMarketToPluginProps } from '../../utils'
|
||||
|
||||
const i18nPrefix = 'plugin.installModal'
|
||||
const i18nPrefix = 'installModal'
|
||||
|
||||
type Props = {
|
||||
uniqueIdentifier: string
|
||||
@ -67,7 +68,8 @@ const Installed: FC<Props> = ({
|
||||
}
|
||||
|
||||
const handleInstall = async () => {
|
||||
if (isInstalling) return
|
||||
if (isInstalling)
|
||||
return
|
||||
onStartToInstall?.()
|
||||
setIsInstalling(true)
|
||||
try {
|
||||
@ -122,50 +124,53 @@ const Installed: FC<Props> = ({
|
||||
const { langGeniusVersionInfo } = useAppContext()
|
||||
const { data: pluginDeclaration } = usePluginDeclarationFromMarketPlace(uniqueIdentifier)
|
||||
const isDifyVersionCompatible = useMemo(() => {
|
||||
if (!pluginDeclaration || !langGeniusVersionInfo.current_version) return true
|
||||
if (!pluginDeclaration || !langGeniusVersionInfo.current_version)
|
||||
return true
|
||||
return gte(langGeniusVersionInfo.current_version, pluginDeclaration?.manifest.meta.minimum_dify_version ?? '0.0.0')
|
||||
}, [langGeniusVersionInfo.current_version, pluginDeclaration])
|
||||
|
||||
const { canInstall } = useInstallPluginLimit({ ...payload, from: 'marketplace' })
|
||||
return (
|
||||
<>
|
||||
<div className='flex flex-col items-start justify-center gap-4 self-stretch px-6 py-3'>
|
||||
<div className='system-md-regular text-text-secondary'>
|
||||
<p>{t(`${i18nPrefix}.readyToInstall`)}</p>
|
||||
<div className="flex flex-col items-start justify-center gap-4 self-stretch px-6 py-3">
|
||||
<div className="system-md-regular text-text-secondary">
|
||||
<p>{t(`${i18nPrefix}.readyToInstall`, { ns: 'plugin' })}</p>
|
||||
{!isDifyVersionCompatible && (
|
||||
<p className='system-md-regular text-text-warning'>
|
||||
{t('plugin.difyVersionNotCompatible', { minimalDifyVersion: pluginDeclaration?.manifest.meta.minimum_dify_version })}
|
||||
<p className="system-md-regular text-text-warning">
|
||||
{t('difyVersionNotCompatible', { ns: 'plugin', minimalDifyVersion: pluginDeclaration?.manifest.meta.minimum_dify_version })}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className='flex flex-wrap content-start items-start gap-1 self-stretch rounded-2xl bg-background-section-burn p-2'>
|
||||
<div className="flex flex-wrap content-start items-start gap-1 self-stretch rounded-2xl bg-background-section-burn p-2">
|
||||
<Card
|
||||
className='w-full'
|
||||
className="w-full"
|
||||
payload={pluginManifestInMarketToPluginProps(payload as PluginManifestInMarket)}
|
||||
titleLeft={!isLoading && <Version
|
||||
hasInstalled={hasInstalled}
|
||||
installedVersion={installedVersion}
|
||||
toInstallVersion={toInstallVersion}
|
||||
/>}
|
||||
titleLeft={!isLoading && (
|
||||
<Version
|
||||
hasInstalled={hasInstalled}
|
||||
installedVersion={installedVersion}
|
||||
toInstallVersion={toInstallVersion}
|
||||
/>
|
||||
)}
|
||||
limitedInstall={!canInstall}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* Action Buttons */}
|
||||
<div className='flex items-center justify-end gap-2 self-stretch p-6 pt-5'>
|
||||
<div className="flex items-center justify-end gap-2 self-stretch p-6 pt-5">
|
||||
{!isInstalling && (
|
||||
<Button variant='secondary' className='min-w-[72px]' onClick={handleCancel}>
|
||||
{t('common.operation.cancel')}
|
||||
<Button variant="secondary" className="min-w-[72px]" onClick={handleCancel}>
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant='primary'
|
||||
className='flex min-w-[72px] space-x-0.5'
|
||||
variant="primary"
|
||||
className="flex min-w-[72px] space-x-0.5"
|
||||
disabled={isInstalling || isLoading || !canInstall}
|
||||
onClick={handleInstall}
|
||||
>
|
||||
{isInstalling && <RiLoader2Line className='h-4 w-4 animate-spin-slow' />}
|
||||
<span>{t(`${i18nPrefix}.${isInstalling ? 'installing' : 'install'}`)}</span>
|
||||
{isInstalling && <RiLoader2Line className="h-4 w-4 animate-spin-slow" />}
|
||||
<span>{t(`${i18nPrefix}.${isInstalling ? 'installing' : 'install'}`, { ns: 'plugin' })}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type { Plugin, PluginDeclaration, PluginManifestInMarket } from '../types'
|
||||
import type { GitHubUrlInfo } from '@/app/components/plugins/types'
|
||||
import { isEmpty } from 'lodash-es'
|
||||
import { isEmpty } from 'es-toolkit/compat'
|
||||
|
||||
export const pluginManifestToCardPluginProps = (pluginManifest: PluginDeclaration): Plugin => {
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user