mirror of https://github.com/langgenius/dify.git
2137 lines
68 KiB
TypeScript
2137 lines
68 KiB
TypeScript
import type { GitHubRepoReleaseResponse, PluginDeclaration, PluginManifestInMarket, UpdateFromGitHubPayload } from '../../types'
|
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { PluginCategoryEnum } from '../../types'
|
|
import { convertRepoToUrl, parseGitHubUrl, pluginManifestInMarketToPluginProps, pluginManifestToCardPluginProps } from '../utils'
|
|
import InstallFromGitHub from './index'
|
|
|
|
// Factory functions for test data (defined before mocks that use them)
|
|
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 createMockReleases = (): GitHubRepoReleaseResponse[] => [
|
|
{
|
|
tag_name: 'v1.0.0',
|
|
assets: [
|
|
{ id: 1, name: 'plugin-v1.0.0.zip', browser_download_url: 'https://github.com/test/repo/releases/download/v1.0.0/plugin-v1.0.0.zip' },
|
|
{ id: 2, name: 'plugin-v1.0.0.tar.gz', browser_download_url: 'https://github.com/test/repo/releases/download/v1.0.0/plugin-v1.0.0.tar.gz' },
|
|
],
|
|
},
|
|
{
|
|
tag_name: 'v0.9.0',
|
|
assets: [
|
|
{ id: 3, name: 'plugin-v0.9.0.zip', browser_download_url: 'https://github.com/test/repo/releases/download/v0.9.0/plugin-v0.9.0.zip' },
|
|
],
|
|
},
|
|
]
|
|
|
|
const createUpdatePayload = (overrides: Partial<UpdateFromGitHubPayload> = {}): UpdateFromGitHubPayload => ({
|
|
originalPackageInfo: {
|
|
id: 'original-id',
|
|
repo: 'owner/repo',
|
|
version: 'v0.9.0',
|
|
package: 'plugin-v0.9.0.zip',
|
|
releases: createMockReleases(),
|
|
},
|
|
...overrides,
|
|
})
|
|
|
|
// Mock external dependencies
|
|
const mockNotify = vi.fn()
|
|
vi.mock('@/app/components/base/toast', () => ({
|
|
default: {
|
|
notify: (props: { type: string, message: string }) => mockNotify(props),
|
|
},
|
|
}))
|
|
|
|
const mockGetIconUrl = vi.fn()
|
|
vi.mock('@/app/components/plugins/install-plugin/base/use-get-icon', () => ({
|
|
default: () => ({ getIconUrl: mockGetIconUrl }),
|
|
}))
|
|
|
|
const mockFetchReleases = vi.fn()
|
|
vi.mock('../hooks', () => ({
|
|
useGitHubReleases: () => ({ fetchReleases: mockFetchReleases }),
|
|
}))
|
|
|
|
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/setURL', () => ({
|
|
default: ({ repoUrl, onChange, onNext, onCancel }: {
|
|
repoUrl: string
|
|
onChange: (value: string) => void
|
|
onNext: () => void
|
|
onCancel: () => void
|
|
}) => (
|
|
<div data-testid="set-url-step">
|
|
<input
|
|
data-testid="repo-url-input"
|
|
value={repoUrl}
|
|
onChange={e => onChange(e.target.value)}
|
|
/>
|
|
<button data-testid="next-btn" onClick={onNext}>Next</button>
|
|
<button data-testid="cancel-btn" onClick={onCancel}>Cancel</button>
|
|
</div>
|
|
),
|
|
}))
|
|
|
|
vi.mock('./steps/selectPackage', () => ({
|
|
default: ({
|
|
repoUrl,
|
|
selectedVersion,
|
|
versions,
|
|
onSelectVersion,
|
|
selectedPackage,
|
|
packages,
|
|
onSelectPackage,
|
|
onUploaded,
|
|
onFailed,
|
|
onBack,
|
|
}: {
|
|
repoUrl: string
|
|
selectedVersion: string
|
|
versions: { value: string, name: string }[]
|
|
onSelectVersion: (item: { value: string, name: string }) => void
|
|
selectedPackage: string
|
|
packages: { value: string, name: string }[]
|
|
onSelectPackage: (item: { value: string, name: string }) => void
|
|
onUploaded: (result: { uniqueIdentifier: string, manifest: PluginDeclaration }) => void
|
|
onFailed: (errorMsg: string) => void
|
|
onBack: () => void
|
|
}) => (
|
|
<div data-testid="select-package-step">
|
|
<span data-testid="repo-url-display">{repoUrl}</span>
|
|
<span data-testid="selected-version">{selectedVersion}</span>
|
|
<span data-testid="selected-package">{selectedPackage}</span>
|
|
<span data-testid="versions-count">{versions.length}</span>
|
|
<span data-testid="packages-count">{packages.length}</span>
|
|
<button
|
|
data-testid="select-version-btn"
|
|
onClick={() => onSelectVersion({ value: 'v1.0.0', name: 'v1.0.0' })}
|
|
>
|
|
Select Version
|
|
</button>
|
|
<button
|
|
data-testid="select-package-btn"
|
|
onClick={() => onSelectPackage({ value: 'package.zip', name: 'package.zip' })}
|
|
>
|
|
Select Package
|
|
</button>
|
|
<button
|
|
data-testid="trigger-upload-btn"
|
|
onClick={() => onUploaded({
|
|
uniqueIdentifier: 'test-unique-id',
|
|
manifest: createMockManifest(),
|
|
})}
|
|
>
|
|
Trigger Upload
|
|
</button>
|
|
<button
|
|
data-testid="trigger-upload-fail-btn"
|
|
onClick={() => onFailed('Upload failed error')}
|
|
>
|
|
Trigger Upload Fail
|
|
</button>
|
|
<button data-testid="back-btn" onClick={onBack}>Back</button>
|
|
</div>
|
|
),
|
|
}))
|
|
|
|
vi.mock('./steps/loaded', () => ({
|
|
default: ({
|
|
uniqueIdentifier,
|
|
payload,
|
|
repoUrl,
|
|
selectedVersion,
|
|
selectedPackage,
|
|
onBack,
|
|
onStartToInstall,
|
|
onInstalled,
|
|
onFailed,
|
|
}: {
|
|
uniqueIdentifier: string
|
|
payload: PluginDeclaration
|
|
repoUrl: string
|
|
selectedVersion: string
|
|
selectedPackage: string
|
|
onBack: () => void
|
|
onStartToInstall: () => void
|
|
onInstalled: (notRefresh?: boolean) => void
|
|
onFailed: (message?: string) => void
|
|
}) => (
|
|
<div data-testid="loaded-step">
|
|
<span data-testid="unique-identifier">{uniqueIdentifier}</span>
|
|
<span data-testid="payload-name">{payload?.name}</span>
|
|
<span data-testid="loaded-repo-url">{repoUrl}</span>
|
|
<span data-testid="loaded-version">{selectedVersion}</span>
|
|
<span data-testid="loaded-package">{selectedPackage}</span>
|
|
<button data-testid="loaded-back-btn" onClick={onBack}>Back</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('Install failed')}>Install Fail</button>
|
|
<button data-testid="install-fail-no-msg-btn" onClick={() => onFailed()}>Install Fail No Msg</button>
|
|
</div>
|
|
),
|
|
}))
|
|
|
|
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">{payload?.name || 'no-payload'}</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('InstallFromGitHub', () => {
|
|
const defaultProps = {
|
|
onClose: vi.fn(),
|
|
onSuccess: vi.fn(),
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
mockGetIconUrl.mockResolvedValue('processed-icon-url')
|
|
mockFetchReleases.mockResolvedValue(createMockReleases())
|
|
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 new installation', () => {
|
|
render(<InstallFromGitHub {...defaultProps} />)
|
|
|
|
expect(screen.getByTestId('set-url-step')).toBeInTheDocument()
|
|
expect(screen.getByTestId('repo-url-input')).toHaveValue('')
|
|
})
|
|
|
|
it('should render modal with selectPackage step when updatePayload is provided', () => {
|
|
const updatePayload = createUpdatePayload()
|
|
|
|
render(<InstallFromGitHub {...defaultProps} updatePayload={updatePayload} />)
|
|
|
|
expect(screen.getByTestId('select-package-step')).toBeInTheDocument()
|
|
expect(screen.getByTestId('repo-url-display')).toHaveTextContent('https://github.com/owner/repo')
|
|
})
|
|
|
|
it('should render install note text in non-terminal steps', () => {
|
|
render(<InstallFromGitHub {...defaultProps} />)
|
|
|
|
expect(screen.getByText('plugin.installFromGitHub.installNote')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should apply modal className from useHideLogic', () => {
|
|
// Verify useHideLogic provides modalClassName
|
|
// The actual className application is handled by Modal component internally
|
|
// We verify the hook integration by checking that it returns the expected class
|
|
expect(mockHideLogicState.modalClassName).toBe('test-modal-class')
|
|
})
|
|
})
|
|
|
|
// ================================
|
|
// Title Tests
|
|
// ================================
|
|
describe('Title Display', () => {
|
|
it('should show install title when no updatePayload', () => {
|
|
render(<InstallFromGitHub {...defaultProps} />)
|
|
|
|
expect(screen.getByText('plugin.installFromGitHub.installPlugin')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should show update title when updatePayload is provided', () => {
|
|
render(<InstallFromGitHub {...defaultProps} updatePayload={createUpdatePayload()} />)
|
|
|
|
expect(screen.getByText('plugin.installFromGitHub.updatePlugin')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
// ================================
|
|
// State Management Tests
|
|
// ================================
|
|
describe('State Management', () => {
|
|
it('should update repoUrl when user types in input', () => {
|
|
render(<InstallFromGitHub {...defaultProps} />)
|
|
|
|
const input = screen.getByTestId('repo-url-input')
|
|
fireEvent.change(input, { target: { value: 'https://github.com/test/repo' } })
|
|
|
|
expect(input).toHaveValue('https://github.com/test/repo')
|
|
})
|
|
|
|
it('should transition from setUrl to selectPackage on successful URL submit', async () => {
|
|
render(<InstallFromGitHub {...defaultProps} />)
|
|
|
|
const input = screen.getByTestId('repo-url-input')
|
|
fireEvent.change(input, { target: { value: 'https://github.com/owner/repo' } })
|
|
|
|
const nextBtn = screen.getByTestId('next-btn')
|
|
fireEvent.click(nextBtn)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('select-package-step')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('should update selectedVersion when version is selected', async () => {
|
|
render(<InstallFromGitHub {...defaultProps} updatePayload={createUpdatePayload()} />)
|
|
|
|
const selectVersionBtn = screen.getByTestId('select-version-btn')
|
|
fireEvent.click(selectVersionBtn)
|
|
|
|
expect(screen.getByTestId('selected-version')).toHaveTextContent('v1.0.0')
|
|
})
|
|
|
|
it('should update selectedPackage when package is selected', async () => {
|
|
render(<InstallFromGitHub {...defaultProps} updatePayload={createUpdatePayload()} />)
|
|
|
|
const selectPackageBtn = screen.getByTestId('select-package-btn')
|
|
fireEvent.click(selectPackageBtn)
|
|
|
|
expect(screen.getByTestId('selected-package')).toHaveTextContent('package.zip')
|
|
})
|
|
|
|
it('should transition to readyToInstall step after successful upload', async () => {
|
|
render(<InstallFromGitHub {...defaultProps} updatePayload={createUpdatePayload()} />)
|
|
|
|
const uploadBtn = screen.getByTestId('trigger-upload-btn')
|
|
fireEvent.click(uploadBtn)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('loaded-step')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('should transition to installed step after successful install', async () => {
|
|
render(<InstallFromGitHub {...defaultProps} updatePayload={createUpdatePayload()} />)
|
|
|
|
// First upload
|
|
fireEvent.click(screen.getByTestId('trigger-upload-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('loaded-step')).toBeInTheDocument()
|
|
})
|
|
|
|
// Then install
|
|
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 to installFailed step on install failure', async () => {
|
|
render(<InstallFromGitHub {...defaultProps} updatePayload={createUpdatePayload()} />)
|
|
|
|
fireEvent.click(screen.getByTestId('trigger-upload-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('loaded-step')).toBeInTheDocument()
|
|
})
|
|
|
|
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('Install failed')
|
|
})
|
|
})
|
|
|
|
it('should transition to uploadFailed step on upload failure', async () => {
|
|
render(<InstallFromGitHub {...defaultProps} updatePayload={createUpdatePayload()} />)
|
|
|
|
fireEvent.click(screen.getByTestId('trigger-upload-fail-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('installed-step')).toBeInTheDocument()
|
|
expect(screen.getByTestId('is-failed')).toHaveTextContent('true')
|
|
expect(screen.getByTestId('error-msg')).toHaveTextContent('Upload failed error')
|
|
})
|
|
})
|
|
})
|
|
|
|
// ================================
|
|
// Versions and Packages Tests
|
|
// ================================
|
|
describe('Versions and Packages Computation', () => {
|
|
it('should derive versions from releases', () => {
|
|
render(<InstallFromGitHub {...defaultProps} updatePayload={createUpdatePayload()} />)
|
|
|
|
expect(screen.getByTestId('versions-count')).toHaveTextContent('2')
|
|
})
|
|
|
|
it('should derive packages from selected version', async () => {
|
|
render(<InstallFromGitHub {...defaultProps} updatePayload={createUpdatePayload()} />)
|
|
|
|
// Initially no packages (no version selected)
|
|
expect(screen.getByTestId('packages-count')).toHaveTextContent('0')
|
|
|
|
// Select a version
|
|
fireEvent.click(screen.getByTestId('select-version-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('packages-count')).toHaveTextContent('2')
|
|
})
|
|
})
|
|
})
|
|
|
|
// ================================
|
|
// URL Validation Tests
|
|
// ================================
|
|
describe('URL Validation', () => {
|
|
it('should show error toast for invalid GitHub URL', async () => {
|
|
render(<InstallFromGitHub {...defaultProps} />)
|
|
|
|
const input = screen.getByTestId('repo-url-input')
|
|
fireEvent.change(input, { target: { value: 'invalid-url' } })
|
|
|
|
const nextBtn = screen.getByTestId('next-btn')
|
|
fireEvent.click(nextBtn)
|
|
|
|
await waitFor(() => {
|
|
expect(mockNotify).toHaveBeenCalledWith({
|
|
type: 'error',
|
|
message: 'plugin.error.inValidGitHubUrl',
|
|
})
|
|
})
|
|
})
|
|
|
|
it('should show error toast when no releases are found', async () => {
|
|
mockFetchReleases.mockResolvedValue([])
|
|
|
|
render(<InstallFromGitHub {...defaultProps} />)
|
|
|
|
const input = screen.getByTestId('repo-url-input')
|
|
fireEvent.change(input, { target: { value: 'https://github.com/owner/repo' } })
|
|
|
|
const nextBtn = screen.getByTestId('next-btn')
|
|
fireEvent.click(nextBtn)
|
|
|
|
await waitFor(() => {
|
|
expect(mockNotify).toHaveBeenCalledWith({
|
|
type: 'error',
|
|
message: 'plugin.error.noReleasesFound',
|
|
})
|
|
})
|
|
})
|
|
|
|
it('should show error toast when fetchReleases throws', async () => {
|
|
mockFetchReleases.mockRejectedValue(new Error('Network error'))
|
|
|
|
render(<InstallFromGitHub {...defaultProps} />)
|
|
|
|
const input = screen.getByTestId('repo-url-input')
|
|
fireEvent.change(input, { target: { value: 'https://github.com/owner/repo' } })
|
|
|
|
const nextBtn = screen.getByTestId('next-btn')
|
|
fireEvent.click(nextBtn)
|
|
|
|
await waitFor(() => {
|
|
expect(mockNotify).toHaveBeenCalledWith({
|
|
type: 'error',
|
|
message: 'plugin.error.fetchReleasesError',
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
// ================================
|
|
// Back Navigation Tests
|
|
// ================================
|
|
describe('Back Navigation', () => {
|
|
it('should go back from selectPackage to setUrl', async () => {
|
|
render(<InstallFromGitHub {...defaultProps} />)
|
|
|
|
// Navigate to selectPackage
|
|
const input = screen.getByTestId('repo-url-input')
|
|
fireEvent.change(input, { target: { value: 'https://github.com/owner/repo' } })
|
|
fireEvent.click(screen.getByTestId('next-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('select-package-step')).toBeInTheDocument()
|
|
})
|
|
|
|
// Go back
|
|
fireEvent.click(screen.getByTestId('back-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('set-url-step')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('should go back from readyToInstall to selectPackage', async () => {
|
|
render(<InstallFromGitHub {...defaultProps} updatePayload={createUpdatePayload()} />)
|
|
|
|
// Navigate to readyToInstall
|
|
fireEvent.click(screen.getByTestId('trigger-upload-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('loaded-step')).toBeInTheDocument()
|
|
})
|
|
|
|
// Go back
|
|
fireEvent.click(screen.getByTestId('loaded-back-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('select-package-step')).toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|
|
|
|
// ================================
|
|
// Callback Tests
|
|
// ================================
|
|
describe('Callbacks', () => {
|
|
it('should call onClose when cancel button is clicked', () => {
|
|
render(<InstallFromGitHub {...defaultProps} />)
|
|
|
|
fireEvent.click(screen.getByTestId('cancel-btn'))
|
|
|
|
expect(defaultProps.onClose).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('should call foldAnimInto when modal close is triggered', () => {
|
|
render(<InstallFromGitHub {...defaultProps} />)
|
|
|
|
// The modal's onClose is bound to foldAnimInto
|
|
// We verify the hook is properly connected
|
|
expect(mockHideLogicState.foldAnimInto).toBeDefined()
|
|
})
|
|
|
|
it('should call onSuccess when installation completes', async () => {
|
|
render(<InstallFromGitHub {...defaultProps} updatePayload={createUpdatePayload()} />)
|
|
|
|
fireEvent.click(screen.getByTestId('trigger-upload-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('loaded-step')).toBeInTheDocument()
|
|
})
|
|
|
|
fireEvent.click(screen.getByTestId('install-success-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(defaultProps.onSuccess).toHaveBeenCalledTimes(1)
|
|
})
|
|
})
|
|
|
|
it('should call refreshPluginList when installation completes without notRefresh flag', async () => {
|
|
render(<InstallFromGitHub {...defaultProps} updatePayload={createUpdatePayload()} />)
|
|
|
|
fireEvent.click(screen.getByTestId('trigger-upload-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('loaded-step')).toBeInTheDocument()
|
|
})
|
|
|
|
fireEvent.click(screen.getByTestId('install-success-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(mockRefreshPluginList).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
it('should not call refreshPluginList when notRefresh flag is true', async () => {
|
|
render(<InstallFromGitHub {...defaultProps} updatePayload={createUpdatePayload()} />)
|
|
|
|
fireEvent.click(screen.getByTestId('trigger-upload-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('loaded-step')).toBeInTheDocument()
|
|
})
|
|
|
|
fireEvent.click(screen.getByTestId('install-success-no-refresh-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(mockRefreshPluginList).not.toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
it('should call setIsInstalling(false) when installation completes', async () => {
|
|
render(<InstallFromGitHub {...defaultProps} updatePayload={createUpdatePayload()} />)
|
|
|
|
fireEvent.click(screen.getByTestId('trigger-upload-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('loaded-step')).toBeInTheDocument()
|
|
})
|
|
|
|
fireEvent.click(screen.getByTestId('install-success-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(mockHideLogicState.setIsInstalling).toHaveBeenCalledWith(false)
|
|
})
|
|
})
|
|
|
|
it('should call handleStartToInstall when start install is triggered', async () => {
|
|
render(<InstallFromGitHub {...defaultProps} updatePayload={createUpdatePayload()} />)
|
|
|
|
fireEvent.click(screen.getByTestId('trigger-upload-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('loaded-step')).toBeInTheDocument()
|
|
})
|
|
|
|
fireEvent.click(screen.getByTestId('start-install-btn'))
|
|
|
|
expect(mockHideLogicState.handleStartToInstall).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('should call setIsInstalling(false) when installation fails', async () => {
|
|
render(<InstallFromGitHub {...defaultProps} updatePayload={createUpdatePayload()} />)
|
|
|
|
fireEvent.click(screen.getByTestId('trigger-upload-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('loaded-step')).toBeInTheDocument()
|
|
})
|
|
|
|
fireEvent.click(screen.getByTestId('install-fail-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(mockHideLogicState.setIsInstalling).toHaveBeenCalledWith(false)
|
|
})
|
|
})
|
|
})
|
|
|
|
// ================================
|
|
// Callback Stability Tests (Memoization)
|
|
// ================================
|
|
describe('Callback Stability', () => {
|
|
it('should maintain stable handleUploadFail callback reference', async () => {
|
|
const { rerender } = render(<InstallFromGitHub {...defaultProps} updatePayload={createUpdatePayload()} />)
|
|
|
|
const firstRender = screen.getByTestId('select-package-step')
|
|
expect(firstRender).toBeInTheDocument()
|
|
|
|
// Rerender with same props
|
|
rerender(<InstallFromGitHub {...defaultProps} updatePayload={createUpdatePayload()} />)
|
|
|
|
// The component should still work correctly
|
|
fireEvent.click(screen.getByTestId('trigger-upload-fail-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('installed-step')).toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|
|
|
|
// ================================
|
|
// Icon Processing Tests
|
|
// ================================
|
|
describe('Icon Processing', () => {
|
|
it('should process icon URL on successful upload', async () => {
|
|
render(<InstallFromGitHub {...defaultProps} updatePayload={createUpdatePayload()} />)
|
|
|
|
fireEvent.click(screen.getByTestId('trigger-upload-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(mockGetIconUrl).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
it('should handle icon processing error gracefully', async () => {
|
|
mockGetIconUrl.mockRejectedValue(new Error('Icon processing failed'))
|
|
|
|
render(<InstallFromGitHub {...defaultProps} updatePayload={createUpdatePayload()} />)
|
|
|
|
fireEvent.click(screen.getByTestId('trigger-upload-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('installed-step')).toBeInTheDocument()
|
|
expect(screen.getByTestId('is-failed')).toHaveTextContent('true')
|
|
})
|
|
})
|
|
})
|
|
|
|
// ================================
|
|
// Edge Cases Tests
|
|
// ================================
|
|
describe('Edge Cases', () => {
|
|
it('should handle empty releases array from updatePayload', () => {
|
|
const updatePayload = createUpdatePayload({
|
|
originalPackageInfo: {
|
|
id: 'original-id',
|
|
repo: 'owner/repo',
|
|
version: 'v0.9.0',
|
|
package: 'plugin.zip',
|
|
releases: [],
|
|
},
|
|
})
|
|
|
|
render(<InstallFromGitHub {...defaultProps} updatePayload={updatePayload} />)
|
|
|
|
expect(screen.getByTestId('versions-count')).toHaveTextContent('0')
|
|
})
|
|
|
|
it('should handle release with no assets', async () => {
|
|
const updatePayload = createUpdatePayload({
|
|
originalPackageInfo: {
|
|
id: 'original-id',
|
|
repo: 'owner/repo',
|
|
version: 'v0.9.0',
|
|
package: 'plugin.zip',
|
|
releases: [{ tag_name: 'v1.0.0', assets: [] }],
|
|
},
|
|
})
|
|
|
|
render(<InstallFromGitHub {...defaultProps} updatePayload={updatePayload} />)
|
|
|
|
// Select the version
|
|
fireEvent.click(screen.getByTestId('select-version-btn'))
|
|
|
|
// Should have 0 packages
|
|
expect(screen.getByTestId('packages-count')).toHaveTextContent('0')
|
|
})
|
|
|
|
it('should handle selected version not found in releases', async () => {
|
|
const updatePayload = createUpdatePayload({
|
|
originalPackageInfo: {
|
|
id: 'original-id',
|
|
repo: 'owner/repo',
|
|
version: 'v0.9.0',
|
|
package: 'plugin.zip',
|
|
releases: [],
|
|
},
|
|
})
|
|
|
|
render(<InstallFromGitHub {...defaultProps} updatePayload={updatePayload} />)
|
|
|
|
fireEvent.click(screen.getByTestId('select-version-btn'))
|
|
|
|
expect(screen.getByTestId('packages-count')).toHaveTextContent('0')
|
|
})
|
|
|
|
it('should handle install failure without error message', async () => {
|
|
render(<InstallFromGitHub {...defaultProps} updatePayload={createUpdatePayload()} />)
|
|
|
|
fireEvent.click(screen.getByTestId('trigger-upload-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('loaded-step')).toBeInTheDocument()
|
|
})
|
|
|
|
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 handle URL without trailing slash', async () => {
|
|
render(<InstallFromGitHub {...defaultProps} />)
|
|
|
|
const input = screen.getByTestId('repo-url-input')
|
|
fireEvent.change(input, { target: { value: 'https://github.com/owner/repo' } })
|
|
|
|
fireEvent.click(screen.getByTestId('next-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(mockFetchReleases).toHaveBeenCalledWith('owner', 'repo')
|
|
})
|
|
})
|
|
|
|
it('should preserve state correctly through step transitions', async () => {
|
|
render(<InstallFromGitHub {...defaultProps} />)
|
|
|
|
// Set URL
|
|
const input = screen.getByTestId('repo-url-input')
|
|
fireEvent.change(input, { target: { value: 'https://github.com/test/myrepo' } })
|
|
|
|
// Navigate to selectPackage
|
|
fireEvent.click(screen.getByTestId('next-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('select-package-step')).toBeInTheDocument()
|
|
})
|
|
|
|
// Verify URL is preserved
|
|
expect(screen.getByTestId('repo-url-display')).toHaveTextContent('https://github.com/test/myrepo')
|
|
|
|
// Select version and package
|
|
fireEvent.click(screen.getByTestId('select-version-btn'))
|
|
fireEvent.click(screen.getByTestId('select-package-btn'))
|
|
|
|
// Navigate to readyToInstall
|
|
fireEvent.click(screen.getByTestId('trigger-upload-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('loaded-step')).toBeInTheDocument()
|
|
})
|
|
|
|
// Verify all data is preserved
|
|
expect(screen.getByTestId('loaded-repo-url')).toHaveTextContent('https://github.com/test/myrepo')
|
|
expect(screen.getByTestId('loaded-version')).toHaveTextContent('v1.0.0')
|
|
expect(screen.getByTestId('loaded-package')).toHaveTextContent('package.zip')
|
|
})
|
|
})
|
|
|
|
// ================================
|
|
// Terminal Steps Rendering Tests
|
|
// ================================
|
|
describe('Terminal Steps Rendering', () => {
|
|
it('should render Installed component for installed step', async () => {
|
|
render(<InstallFromGitHub {...defaultProps} updatePayload={createUpdatePayload()} />)
|
|
|
|
fireEvent.click(screen.getByTestId('trigger-upload-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('loaded-step')).toBeInTheDocument()
|
|
})
|
|
|
|
fireEvent.click(screen.getByTestId('install-success-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('installed-step')).toBeInTheDocument()
|
|
expect(screen.queryByText('plugin.installFromGitHub.installNote')).not.toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('should render Installed component for uploadFailed step', async () => {
|
|
render(<InstallFromGitHub {...defaultProps} updatePayload={createUpdatePayload()} />)
|
|
|
|
fireEvent.click(screen.getByTestId('trigger-upload-fail-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('installed-step')).toBeInTheDocument()
|
|
expect(screen.getByTestId('is-failed')).toHaveTextContent('true')
|
|
})
|
|
})
|
|
|
|
it('should render Installed component for installFailed step', async () => {
|
|
render(<InstallFromGitHub {...defaultProps} updatePayload={createUpdatePayload()} />)
|
|
|
|
fireEvent.click(screen.getByTestId('trigger-upload-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('loaded-step')).toBeInTheDocument()
|
|
})
|
|
|
|
fireEvent.click(screen.getByTestId('install-fail-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('installed-step')).toBeInTheDocument()
|
|
expect(screen.getByTestId('is-failed')).toHaveTextContent('true')
|
|
})
|
|
})
|
|
|
|
it('should call onClose when close button is clicked in installed step', async () => {
|
|
render(<InstallFromGitHub {...defaultProps} updatePayload={createUpdatePayload()} />)
|
|
|
|
fireEvent.click(screen.getByTestId('trigger-upload-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('loaded-step')).toBeInTheDocument()
|
|
})
|
|
|
|
fireEvent.click(screen.getByTestId('install-success-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('installed-step')).toBeInTheDocument()
|
|
})
|
|
|
|
fireEvent.click(screen.getByTestId('installed-close-btn'))
|
|
|
|
expect(defaultProps.onClose).toHaveBeenCalledTimes(1)
|
|
})
|
|
})
|
|
|
|
// ================================
|
|
// Title Update Tests
|
|
// ================================
|
|
describe('Title Updates', () => {
|
|
it('should show success title when installed', async () => {
|
|
render(<InstallFromGitHub {...defaultProps} updatePayload={createUpdatePayload()} />)
|
|
|
|
fireEvent.click(screen.getByTestId('trigger-upload-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('loaded-step')).toBeInTheDocument()
|
|
})
|
|
|
|
fireEvent.click(screen.getByTestId('install-success-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('plugin.installFromGitHub.installedSuccessfully')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('should show failed title when install failed', async () => {
|
|
render(<InstallFromGitHub {...defaultProps} updatePayload={createUpdatePayload()} />)
|
|
|
|
fireEvent.click(screen.getByTestId('trigger-upload-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('loaded-step')).toBeInTheDocument()
|
|
})
|
|
|
|
fireEvent.click(screen.getByTestId('install-fail-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('plugin.installFromGitHub.installFailed')).toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|
|
|
|
// ================================
|
|
// Data Flow Tests
|
|
// ================================
|
|
describe('Data Flow', () => {
|
|
it('should pass correct uniqueIdentifier to Loaded component', async () => {
|
|
render(<InstallFromGitHub {...defaultProps} updatePayload={createUpdatePayload()} />)
|
|
|
|
fireEvent.click(screen.getByTestId('trigger-upload-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('unique-identifier')).toHaveTextContent('test-unique-id')
|
|
})
|
|
})
|
|
|
|
it('should pass processed manifest to Loaded component', async () => {
|
|
render(<InstallFromGitHub {...defaultProps} updatePayload={createUpdatePayload()} />)
|
|
|
|
fireEvent.click(screen.getByTestId('trigger-upload-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('payload-name')).toHaveTextContent('Test Plugin')
|
|
})
|
|
})
|
|
|
|
it('should pass manifest with processed icon to Loaded component', async () => {
|
|
mockGetIconUrl.mockResolvedValue('https://processed-icon.com/icon.png')
|
|
|
|
render(<InstallFromGitHub {...defaultProps} updatePayload={createUpdatePayload()} />)
|
|
|
|
fireEvent.click(screen.getByTestId('trigger-upload-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(mockGetIconUrl).toHaveBeenCalledWith('test-icon.png')
|
|
})
|
|
})
|
|
})
|
|
|
|
// ================================
|
|
// Prop Variations Tests
|
|
// ================================
|
|
describe('Prop Variations', () => {
|
|
it('should work without updatePayload (fresh install flow)', async () => {
|
|
render(<InstallFromGitHub {...defaultProps} />)
|
|
|
|
// Start from setUrl step
|
|
expect(screen.getByTestId('set-url-step')).toBeInTheDocument()
|
|
|
|
// Enter URL
|
|
const input = screen.getByTestId('repo-url-input')
|
|
fireEvent.change(input, { target: { value: 'https://github.com/owner/repo' } })
|
|
fireEvent.click(screen.getByTestId('next-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('select-package-step')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('should work with updatePayload (update flow)', async () => {
|
|
const updatePayload = createUpdatePayload()
|
|
|
|
render(<InstallFromGitHub {...defaultProps} updatePayload={updatePayload} />)
|
|
|
|
// Start from selectPackage step
|
|
expect(screen.getByTestId('select-package-step')).toBeInTheDocument()
|
|
expect(screen.getByTestId('repo-url-display')).toHaveTextContent('https://github.com/owner/repo')
|
|
})
|
|
|
|
it('should use releases from updatePayload', () => {
|
|
const customReleases: GitHubRepoReleaseResponse[] = [
|
|
{ tag_name: 'v2.0.0', assets: [{ id: 1, name: 'custom.zip', browser_download_url: 'url' }] },
|
|
{ tag_name: 'v1.5.0', assets: [{ id: 2, name: 'custom2.zip', browser_download_url: 'url2' }] },
|
|
{ tag_name: 'v1.0.0', assets: [{ id: 3, name: 'custom3.zip', browser_download_url: 'url3' }] },
|
|
]
|
|
|
|
const updatePayload = createUpdatePayload({
|
|
originalPackageInfo: {
|
|
id: 'id',
|
|
repo: 'owner/repo',
|
|
version: 'v1.0.0',
|
|
package: 'pkg.zip',
|
|
releases: customReleases,
|
|
},
|
|
})
|
|
|
|
render(<InstallFromGitHub {...defaultProps} updatePayload={updatePayload} />)
|
|
|
|
expect(screen.getByTestId('versions-count')).toHaveTextContent('3')
|
|
})
|
|
|
|
it('should convert repo to URL correctly', () => {
|
|
const updatePayload = createUpdatePayload({
|
|
originalPackageInfo: {
|
|
id: 'id',
|
|
repo: 'myorg/myrepo',
|
|
version: 'v1.0.0',
|
|
package: 'pkg.zip',
|
|
releases: createMockReleases(),
|
|
},
|
|
})
|
|
|
|
render(<InstallFromGitHub {...defaultProps} updatePayload={updatePayload} />)
|
|
|
|
expect(screen.getByTestId('repo-url-display')).toHaveTextContent('https://github.com/myorg/myrepo')
|
|
})
|
|
})
|
|
|
|
// ================================
|
|
// Error Handling Tests
|
|
// ================================
|
|
describe('Error Handling', () => {
|
|
it('should handle API error with response message', async () => {
|
|
mockGetIconUrl.mockRejectedValue({
|
|
response: { message: 'API Error Message' },
|
|
})
|
|
|
|
render(<InstallFromGitHub {...defaultProps} updatePayload={createUpdatePayload()} />)
|
|
|
|
fireEvent.click(screen.getByTestId('trigger-upload-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('installed-step')).toBeInTheDocument()
|
|
expect(screen.getByTestId('is-failed')).toHaveTextContent('true')
|
|
expect(screen.getByTestId('error-msg')).toHaveTextContent('API Error Message')
|
|
})
|
|
})
|
|
|
|
it('should handle API error without response message', async () => {
|
|
mockGetIconUrl.mockRejectedValue(new Error('Generic error'))
|
|
|
|
render(<InstallFromGitHub {...defaultProps} updatePayload={createUpdatePayload()} />)
|
|
|
|
fireEvent.click(screen.getByTestId('trigger-upload-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('installed-step')).toBeInTheDocument()
|
|
expect(screen.getByTestId('is-failed')).toHaveTextContent('true')
|
|
expect(screen.getByTestId('error-msg')).toHaveTextContent('plugin.installModal.installFailedDesc')
|
|
})
|
|
})
|
|
})
|
|
|
|
// ================================
|
|
// handleBack Default Case Tests
|
|
// ================================
|
|
describe('handleBack Edge Cases', () => {
|
|
it('should not change state when back is called from setUrl step', async () => {
|
|
// This tests the default case in handleBack switch
|
|
// When in setUrl step, calling back should keep the state unchanged
|
|
render(<InstallFromGitHub {...defaultProps} />)
|
|
|
|
// Verify we're on setUrl step
|
|
expect(screen.getByTestId('set-url-step')).toBeInTheDocument()
|
|
|
|
// The setUrl step doesn't expose onBack in the real component,
|
|
// but our mock doesn't have it either - this is correct behavior
|
|
// as setUrl is the first step with no back option
|
|
})
|
|
|
|
it('should handle multiple back navigations correctly', async () => {
|
|
render(<InstallFromGitHub {...defaultProps} />)
|
|
|
|
// Navigate to selectPackage
|
|
const input = screen.getByTestId('repo-url-input')
|
|
fireEvent.change(input, { target: { value: 'https://github.com/owner/repo' } })
|
|
fireEvent.click(screen.getByTestId('next-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('select-package-step')).toBeInTheDocument()
|
|
})
|
|
|
|
// Navigate to readyToInstall
|
|
fireEvent.click(screen.getByTestId('trigger-upload-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('loaded-step')).toBeInTheDocument()
|
|
})
|
|
|
|
// Go back to selectPackage
|
|
fireEvent.click(screen.getByTestId('loaded-back-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('select-package-step')).toBeInTheDocument()
|
|
})
|
|
|
|
// Go back to setUrl
|
|
fireEvent.click(screen.getByTestId('back-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('set-url-step')).toBeInTheDocument()
|
|
})
|
|
|
|
// Verify URL is preserved after back navigation
|
|
expect(screen.getByTestId('repo-url-input')).toHaveValue('https://github.com/owner/repo')
|
|
})
|
|
})
|
|
})
|
|
|
|
// ================================
|
|
// Utility Functions Tests
|
|
// ================================
|
|
describe('Install Plugin Utils', () => {
|
|
describe('parseGitHubUrl', () => {
|
|
it('should parse valid GitHub URL correctly', () => {
|
|
const result = parseGitHubUrl('https://github.com/owner/repo')
|
|
|
|
expect(result.isValid).toBe(true)
|
|
expect(result.owner).toBe('owner')
|
|
expect(result.repo).toBe('repo')
|
|
})
|
|
|
|
it('should parse GitHub URL with trailing slash', () => {
|
|
const result = parseGitHubUrl('https://github.com/owner/repo/')
|
|
|
|
expect(result.isValid).toBe(true)
|
|
expect(result.owner).toBe('owner')
|
|
expect(result.repo).toBe('repo')
|
|
})
|
|
|
|
it('should return invalid for non-GitHub URL', () => {
|
|
const result = parseGitHubUrl('https://gitlab.com/owner/repo')
|
|
|
|
expect(result.isValid).toBe(false)
|
|
expect(result.owner).toBeUndefined()
|
|
expect(result.repo).toBeUndefined()
|
|
})
|
|
|
|
it('should return invalid for malformed URL', () => {
|
|
const result = parseGitHubUrl('not-a-url')
|
|
|
|
expect(result.isValid).toBe(false)
|
|
})
|
|
|
|
it('should return invalid for GitHub URL with extra path segments', () => {
|
|
const result = parseGitHubUrl('https://github.com/owner/repo/tree/main')
|
|
|
|
expect(result.isValid).toBe(false)
|
|
})
|
|
|
|
it('should return invalid for empty string', () => {
|
|
const result = parseGitHubUrl('')
|
|
|
|
expect(result.isValid).toBe(false)
|
|
})
|
|
|
|
it('should handle URL with special characters in owner/repo names', () => {
|
|
const result = parseGitHubUrl('https://github.com/my-org/my-repo-123')
|
|
|
|
expect(result.isValid).toBe(true)
|
|
expect(result.owner).toBe('my-org')
|
|
expect(result.repo).toBe('my-repo-123')
|
|
})
|
|
})
|
|
|
|
describe('convertRepoToUrl', () => {
|
|
it('should convert repo string to full GitHub URL', () => {
|
|
const result = convertRepoToUrl('owner/repo')
|
|
|
|
expect(result).toBe('https://github.com/owner/repo')
|
|
})
|
|
|
|
it('should return empty string for empty repo', () => {
|
|
const result = convertRepoToUrl('')
|
|
|
|
expect(result).toBe('')
|
|
})
|
|
|
|
it('should handle repo with organization name', () => {
|
|
const result = convertRepoToUrl('my-organization/my-repository')
|
|
|
|
expect(result).toBe('https://github.com/my-organization/my-repository')
|
|
})
|
|
})
|
|
|
|
describe('pluginManifestToCardPluginProps', () => {
|
|
it('should convert PluginDeclaration to Plugin props correctly', () => {
|
|
const manifest: PluginDeclaration = {
|
|
plugin_unique_identifier: 'test-uid',
|
|
version: '1.0.0',
|
|
author: 'test-author',
|
|
icon: 'icon.png',
|
|
icon_dark: 'icon-dark.png',
|
|
name: 'Test Plugin',
|
|
category: PluginCategoryEnum.tool,
|
|
label: { 'en-US': 'Test Label' } 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: ['tag1', 'tag2'],
|
|
agent_strategy: null,
|
|
meta: { version: '1.0.0' },
|
|
trigger: {} as PluginDeclaration['trigger'],
|
|
}
|
|
|
|
const result = pluginManifestToCardPluginProps(manifest)
|
|
|
|
expect(result.plugin_id).toBe('test-uid')
|
|
expect(result.type).toBe('tool')
|
|
expect(result.category).toBe(PluginCategoryEnum.tool)
|
|
expect(result.name).toBe('Test Plugin')
|
|
expect(result.version).toBe('1.0.0')
|
|
expect(result.latest_version).toBe('')
|
|
expect(result.org).toBe('test-author')
|
|
expect(result.author).toBe('test-author')
|
|
expect(result.icon).toBe('icon.png')
|
|
expect(result.icon_dark).toBe('icon-dark.png')
|
|
expect(result.verified).toBe(true)
|
|
expect(result.tags).toEqual([{ name: 'tag1' }, { name: 'tag2' }])
|
|
expect(result.from).toBe('package')
|
|
})
|
|
|
|
it('should handle manifest with empty tags', () => {
|
|
const manifest: PluginDeclaration = {
|
|
plugin_unique_identifier: 'test-uid',
|
|
version: '1.0.0',
|
|
author: 'author',
|
|
icon: 'icon.png',
|
|
name: 'Plugin',
|
|
category: PluginCategoryEnum.model,
|
|
label: {} as PluginDeclaration['label'],
|
|
description: {} as PluginDeclaration['description'],
|
|
created_at: '2024-01-01',
|
|
resource: {},
|
|
plugins: [],
|
|
verified: false,
|
|
endpoint: { settings: [], endpoints: [] },
|
|
model: null,
|
|
tags: [],
|
|
agent_strategy: null,
|
|
meta: { version: '1.0.0' },
|
|
trigger: {} as PluginDeclaration['trigger'],
|
|
}
|
|
|
|
const result = pluginManifestToCardPluginProps(manifest)
|
|
|
|
expect(result.tags).toEqual([])
|
|
expect(result.verified).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('pluginManifestInMarketToPluginProps', () => {
|
|
it('should convert PluginManifestInMarket to Plugin props correctly', () => {
|
|
const manifest: PluginManifestInMarket = {
|
|
plugin_unique_identifier: 'market-uid',
|
|
name: 'Market Plugin',
|
|
org: 'market-org',
|
|
icon: 'market-icon.png',
|
|
label: { 'en-US': 'Market Label' } as PluginManifestInMarket['label'],
|
|
category: PluginCategoryEnum.extension,
|
|
version: '1.0.0',
|
|
latest_version: '2.0.0',
|
|
brief: { 'en-US': 'Brief Description' } as PluginManifestInMarket['brief'],
|
|
introduction: 'Full introduction text',
|
|
verified: true,
|
|
install_count: 1000,
|
|
badges: ['featured', 'verified'],
|
|
verification: { authorized_category: 'partner' },
|
|
from: 'marketplace',
|
|
}
|
|
|
|
const result = pluginManifestInMarketToPluginProps(manifest)
|
|
|
|
expect(result.plugin_id).toBe('market-uid')
|
|
expect(result.type).toBe('extension')
|
|
expect(result.name).toBe('Market Plugin')
|
|
expect(result.version).toBe('2.0.0')
|
|
expect(result.latest_version).toBe('2.0.0')
|
|
expect(result.org).toBe('market-org')
|
|
expect(result.introduction).toBe('Full introduction text')
|
|
expect(result.badges).toEqual(['featured', 'verified'])
|
|
expect(result.verification.authorized_category).toBe('partner')
|
|
expect(result.from).toBe('marketplace')
|
|
})
|
|
|
|
it('should use default verification when empty', () => {
|
|
const manifest: PluginManifestInMarket = {
|
|
plugin_unique_identifier: 'uid',
|
|
name: 'Plugin',
|
|
org: 'org',
|
|
icon: 'icon.png',
|
|
label: {} as PluginManifestInMarket['label'],
|
|
category: PluginCategoryEnum.tool,
|
|
version: '1.0.0',
|
|
latest_version: '1.0.0',
|
|
brief: {} as PluginManifestInMarket['brief'],
|
|
introduction: '',
|
|
verified: false,
|
|
install_count: 0,
|
|
badges: [],
|
|
verification: {} as PluginManifestInMarket['verification'],
|
|
from: 'github',
|
|
}
|
|
|
|
const result = pluginManifestInMarketToPluginProps(manifest)
|
|
|
|
expect(result.verification.authorized_category).toBe('langgenius')
|
|
expect(result.verified).toBe(true) // always true in this function
|
|
})
|
|
|
|
it('should handle marketplace plugin with from github source', () => {
|
|
const manifest: PluginManifestInMarket = {
|
|
plugin_unique_identifier: 'github-uid',
|
|
name: 'GitHub Plugin',
|
|
org: 'github-org',
|
|
icon: 'icon.png',
|
|
label: {} as PluginManifestInMarket['label'],
|
|
category: PluginCategoryEnum.agent,
|
|
version: '0.1.0',
|
|
latest_version: '0.2.0',
|
|
brief: {} as PluginManifestInMarket['brief'],
|
|
introduction: 'From GitHub',
|
|
verified: true,
|
|
install_count: 50,
|
|
badges: [],
|
|
verification: { authorized_category: 'community' },
|
|
from: 'github',
|
|
}
|
|
|
|
const result = pluginManifestInMarketToPluginProps(manifest)
|
|
|
|
expect(result.from).toBe('github')
|
|
expect(result.verification.authorized_category).toBe('community')
|
|
})
|
|
})
|
|
})
|
|
|
|
// ================================
|
|
// Steps Components Tests
|
|
// ================================
|
|
|
|
// SetURL Component Tests
|
|
describe('SetURL Component', () => {
|
|
// Import the real component for testing
|
|
const SetURL = vi.fn()
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
// Re-mock the SetURL component with a more testable version
|
|
vi.doMock('./steps/setURL', () => ({
|
|
default: SetURL,
|
|
}))
|
|
})
|
|
|
|
describe('Rendering', () => {
|
|
it('should render label with correct text', () => {
|
|
render(<InstallFromGitHub onClose={vi.fn()} onSuccess={vi.fn()} />)
|
|
|
|
// The mocked component should be rendered
|
|
expect(screen.getByTestId('set-url-step')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render input field with placeholder', () => {
|
|
render(<InstallFromGitHub onClose={vi.fn()} onSuccess={vi.fn()} />)
|
|
|
|
const input = screen.getByTestId('repo-url-input')
|
|
expect(input).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render cancel and next buttons', () => {
|
|
render(<InstallFromGitHub onClose={vi.fn()} onSuccess={vi.fn()} />)
|
|
|
|
expect(screen.getByTestId('cancel-btn')).toBeInTheDocument()
|
|
expect(screen.getByTestId('next-btn')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('Props', () => {
|
|
it('should display repoUrl value in input', () => {
|
|
render(<InstallFromGitHub onClose={vi.fn()} onSuccess={vi.fn()} />)
|
|
|
|
const input = screen.getByTestId('repo-url-input')
|
|
fireEvent.change(input, { target: { value: 'https://github.com/test/repo' } })
|
|
|
|
expect(input).toHaveValue('https://github.com/test/repo')
|
|
})
|
|
|
|
it('should call onChange when input value changes', () => {
|
|
render(<InstallFromGitHub onClose={vi.fn()} onSuccess={vi.fn()} />)
|
|
|
|
const input = screen.getByTestId('repo-url-input')
|
|
fireEvent.change(input, { target: { value: 'new-value' } })
|
|
|
|
expect(input).toHaveValue('new-value')
|
|
})
|
|
})
|
|
|
|
describe('User Interactions', () => {
|
|
it('should call onNext when next button is clicked', async () => {
|
|
mockFetchReleases.mockResolvedValue(createMockReleases())
|
|
|
|
render(<InstallFromGitHub onClose={vi.fn()} onSuccess={vi.fn()} />)
|
|
|
|
const input = screen.getByTestId('repo-url-input')
|
|
fireEvent.change(input, { target: { value: 'https://github.com/owner/repo' } })
|
|
|
|
fireEvent.click(screen.getByTestId('next-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(mockFetchReleases).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
it('should call onCancel when cancel button is clicked', () => {
|
|
const onClose = vi.fn()
|
|
render(<InstallFromGitHub onClose={onClose} onSuccess={vi.fn()} />)
|
|
|
|
fireEvent.click(screen.getByTestId('cancel-btn'))
|
|
|
|
expect(onClose).toHaveBeenCalledTimes(1)
|
|
})
|
|
})
|
|
|
|
describe('Edge Cases', () => {
|
|
it('should handle empty URL input', () => {
|
|
render(<InstallFromGitHub onClose={vi.fn()} onSuccess={vi.fn()} />)
|
|
|
|
const input = screen.getByTestId('repo-url-input')
|
|
expect(input).toHaveValue('')
|
|
})
|
|
|
|
it('should handle URL with whitespace only', () => {
|
|
render(<InstallFromGitHub onClose={vi.fn()} onSuccess={vi.fn()} />)
|
|
|
|
const input = screen.getByTestId('repo-url-input')
|
|
fireEvent.change(input, { target: { value: ' ' } })
|
|
|
|
// With whitespace only, next should still be submittable but validation will fail
|
|
fireEvent.click(screen.getByTestId('next-btn'))
|
|
|
|
// Should show error for invalid URL
|
|
expect(mockNotify).toHaveBeenCalledWith({
|
|
type: 'error',
|
|
message: 'plugin.error.inValidGitHubUrl',
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
// SelectPackage Component Tests
|
|
describe('SelectPackage Component', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
mockFetchReleases.mockResolvedValue(createMockReleases())
|
|
mockGetIconUrl.mockResolvedValue('processed-icon-url')
|
|
})
|
|
|
|
describe('Rendering', () => {
|
|
it('should render version selector', () => {
|
|
render(
|
|
<InstallFromGitHub
|
|
onClose={vi.fn()}
|
|
onSuccess={vi.fn()}
|
|
updatePayload={createUpdatePayload()}
|
|
/>,
|
|
)
|
|
|
|
expect(screen.getByTestId('select-package-step')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render package selector', () => {
|
|
render(
|
|
<InstallFromGitHub
|
|
onClose={vi.fn()}
|
|
onSuccess={vi.fn()}
|
|
updatePayload={createUpdatePayload()}
|
|
/>,
|
|
)
|
|
|
|
expect(screen.getByTestId('selected-package')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should show back button when not in edit mode', async () => {
|
|
render(<InstallFromGitHub onClose={vi.fn()} onSuccess={vi.fn()} />)
|
|
|
|
// Navigate to selectPackage step
|
|
const input = screen.getByTestId('repo-url-input')
|
|
fireEvent.change(input, { target: { value: 'https://github.com/owner/repo' } })
|
|
fireEvent.click(screen.getByTestId('next-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('back-btn')).toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('Props', () => {
|
|
it('should display versions count correctly', () => {
|
|
render(
|
|
<InstallFromGitHub
|
|
onClose={vi.fn()}
|
|
onSuccess={vi.fn()}
|
|
updatePayload={createUpdatePayload()}
|
|
/>,
|
|
)
|
|
|
|
expect(screen.getByTestId('versions-count')).toHaveTextContent('2')
|
|
})
|
|
|
|
it('should display packages count based on selected version', async () => {
|
|
render(
|
|
<InstallFromGitHub
|
|
onClose={vi.fn()}
|
|
onSuccess={vi.fn()}
|
|
updatePayload={createUpdatePayload()}
|
|
/>,
|
|
)
|
|
|
|
// Initially 0 packages
|
|
expect(screen.getByTestId('packages-count')).toHaveTextContent('0')
|
|
|
|
// Select version
|
|
fireEvent.click(screen.getByTestId('select-version-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('packages-count')).toHaveTextContent('2')
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('User Interactions', () => {
|
|
it('should call onSelectVersion when version is selected', () => {
|
|
render(
|
|
<InstallFromGitHub
|
|
onClose={vi.fn()}
|
|
onSuccess={vi.fn()}
|
|
updatePayload={createUpdatePayload()}
|
|
/>,
|
|
)
|
|
|
|
fireEvent.click(screen.getByTestId('select-version-btn'))
|
|
|
|
expect(screen.getByTestId('selected-version')).toHaveTextContent('v1.0.0')
|
|
})
|
|
|
|
it('should call onSelectPackage when package is selected', () => {
|
|
render(
|
|
<InstallFromGitHub
|
|
onClose={vi.fn()}
|
|
onSuccess={vi.fn()}
|
|
updatePayload={createUpdatePayload()}
|
|
/>,
|
|
)
|
|
|
|
fireEvent.click(screen.getByTestId('select-package-btn'))
|
|
|
|
expect(screen.getByTestId('selected-package')).toHaveTextContent('package.zip')
|
|
})
|
|
|
|
it('should call onBack when back button is clicked', async () => {
|
|
render(<InstallFromGitHub onClose={vi.fn()} onSuccess={vi.fn()} />)
|
|
|
|
// Navigate to selectPackage
|
|
const input = screen.getByTestId('repo-url-input')
|
|
fireEvent.change(input, { target: { value: 'https://github.com/owner/repo' } })
|
|
fireEvent.click(screen.getByTestId('next-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('select-package-step')).toBeInTheDocument()
|
|
})
|
|
|
|
fireEvent.click(screen.getByTestId('back-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('set-url-step')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('should trigger upload when conditions are met', async () => {
|
|
render(
|
|
<InstallFromGitHub
|
|
onClose={vi.fn()}
|
|
onSuccess={vi.fn()}
|
|
updatePayload={createUpdatePayload()}
|
|
/>,
|
|
)
|
|
|
|
fireEvent.click(screen.getByTestId('trigger-upload-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('loaded-step')).toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('Upload Handling', () => {
|
|
it('should call onUploaded on successful upload', async () => {
|
|
render(
|
|
<InstallFromGitHub
|
|
onClose={vi.fn()}
|
|
onSuccess={vi.fn()}
|
|
updatePayload={createUpdatePayload()}
|
|
/>,
|
|
)
|
|
|
|
fireEvent.click(screen.getByTestId('trigger-upload-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(mockGetIconUrl).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
it('should call onFailed on upload failure', async () => {
|
|
render(
|
|
<InstallFromGitHub
|
|
onClose={vi.fn()}
|
|
onSuccess={vi.fn()}
|
|
updatePayload={createUpdatePayload()}
|
|
/>,
|
|
)
|
|
|
|
fireEvent.click(screen.getByTestId('trigger-upload-fail-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('installed-step')).toBeInTheDocument()
|
|
expect(screen.getByTestId('is-failed')).toHaveTextContent('true')
|
|
})
|
|
})
|
|
|
|
it('should handle upload error with response message', async () => {
|
|
render(
|
|
<InstallFromGitHub
|
|
onClose={vi.fn()}
|
|
onSuccess={vi.fn()}
|
|
updatePayload={createUpdatePayload()}
|
|
/>,
|
|
)
|
|
|
|
fireEvent.click(screen.getByTestId('trigger-upload-fail-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('error-msg')).toHaveTextContent('Upload failed error')
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('Edge Cases', () => {
|
|
it('should handle empty versions array', () => {
|
|
const updatePayload = createUpdatePayload({
|
|
originalPackageInfo: {
|
|
id: 'id',
|
|
repo: 'owner/repo',
|
|
version: 'v1.0.0',
|
|
package: 'pkg.zip',
|
|
releases: [],
|
|
},
|
|
})
|
|
|
|
render(
|
|
<InstallFromGitHub
|
|
onClose={vi.fn()}
|
|
onSuccess={vi.fn()}
|
|
updatePayload={updatePayload}
|
|
/>,
|
|
)
|
|
|
|
expect(screen.getByTestId('versions-count')).toHaveTextContent('0')
|
|
})
|
|
|
|
it('should handle version with no assets', () => {
|
|
const updatePayload = createUpdatePayload({
|
|
originalPackageInfo: {
|
|
id: 'id',
|
|
repo: 'owner/repo',
|
|
version: 'v1.0.0',
|
|
package: 'pkg.zip',
|
|
releases: [{ tag_name: 'v1.0.0', assets: [] }],
|
|
},
|
|
})
|
|
|
|
render(
|
|
<InstallFromGitHub
|
|
onClose={vi.fn()}
|
|
onSuccess={vi.fn()}
|
|
updatePayload={updatePayload}
|
|
/>,
|
|
)
|
|
|
|
// Select the empty version
|
|
fireEvent.click(screen.getByTestId('select-version-btn'))
|
|
|
|
expect(screen.getByTestId('packages-count')).toHaveTextContent('0')
|
|
})
|
|
})
|
|
})
|
|
|
|
// Loaded Component Tests
|
|
describe('Loaded Component', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
mockGetIconUrl.mockResolvedValue('processed-icon-url')
|
|
mockFetchReleases.mockResolvedValue(createMockReleases())
|
|
mockHideLogicState = {
|
|
modalClassName: 'test-modal-class',
|
|
foldAnimInto: vi.fn(),
|
|
setIsInstalling: vi.fn(),
|
|
handleStartToInstall: vi.fn(),
|
|
}
|
|
})
|
|
|
|
describe('Rendering', () => {
|
|
it('should render ready to install message', async () => {
|
|
render(
|
|
<InstallFromGitHub
|
|
onClose={vi.fn()}
|
|
onSuccess={vi.fn()}
|
|
updatePayload={createUpdatePayload()}
|
|
/>,
|
|
)
|
|
|
|
fireEvent.click(screen.getByTestId('trigger-upload-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('loaded-step')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('should render plugin card with correct payload', async () => {
|
|
render(
|
|
<InstallFromGitHub
|
|
onClose={vi.fn()}
|
|
onSuccess={vi.fn()}
|
|
updatePayload={createUpdatePayload()}
|
|
/>,
|
|
)
|
|
|
|
fireEvent.click(screen.getByTestId('trigger-upload-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('payload-name')).toHaveTextContent('Test Plugin')
|
|
})
|
|
})
|
|
|
|
it('should render back button when not installing', async () => {
|
|
render(
|
|
<InstallFromGitHub
|
|
onClose={vi.fn()}
|
|
onSuccess={vi.fn()}
|
|
updatePayload={createUpdatePayload()}
|
|
/>,
|
|
)
|
|
|
|
fireEvent.click(screen.getByTestId('trigger-upload-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('loaded-back-btn')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('should render install button', async () => {
|
|
render(
|
|
<InstallFromGitHub
|
|
onClose={vi.fn()}
|
|
onSuccess={vi.fn()}
|
|
updatePayload={createUpdatePayload()}
|
|
/>,
|
|
)
|
|
|
|
fireEvent.click(screen.getByTestId('trigger-upload-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('install-success-btn')).toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('Props', () => {
|
|
it('should display correct uniqueIdentifier', async () => {
|
|
render(
|
|
<InstallFromGitHub
|
|
onClose={vi.fn()}
|
|
onSuccess={vi.fn()}
|
|
updatePayload={createUpdatePayload()}
|
|
/>,
|
|
)
|
|
|
|
fireEvent.click(screen.getByTestId('trigger-upload-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('unique-identifier')).toHaveTextContent('test-unique-id')
|
|
})
|
|
})
|
|
|
|
it('should display correct repoUrl', async () => {
|
|
render(
|
|
<InstallFromGitHub
|
|
onClose={vi.fn()}
|
|
onSuccess={vi.fn()}
|
|
updatePayload={createUpdatePayload()}
|
|
/>,
|
|
)
|
|
|
|
fireEvent.click(screen.getByTestId('trigger-upload-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('loaded-repo-url')).toHaveTextContent('https://github.com/owner/repo')
|
|
})
|
|
})
|
|
|
|
it('should display selected version and package', async () => {
|
|
render(
|
|
<InstallFromGitHub
|
|
onClose={vi.fn()}
|
|
onSuccess={vi.fn()}
|
|
updatePayload={createUpdatePayload()}
|
|
/>,
|
|
)
|
|
|
|
// First select version and package
|
|
fireEvent.click(screen.getByTestId('select-version-btn'))
|
|
fireEvent.click(screen.getByTestId('select-package-btn'))
|
|
|
|
// Then trigger upload
|
|
fireEvent.click(screen.getByTestId('trigger-upload-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('loaded-version')).toHaveTextContent('v1.0.0')
|
|
expect(screen.getByTestId('loaded-package')).toHaveTextContent('package.zip')
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('User Interactions', () => {
|
|
it('should call onBack when back button is clicked', async () => {
|
|
render(
|
|
<InstallFromGitHub
|
|
onClose={vi.fn()}
|
|
onSuccess={vi.fn()}
|
|
updatePayload={createUpdatePayload()}
|
|
/>,
|
|
)
|
|
|
|
fireEvent.click(screen.getByTestId('trigger-upload-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('loaded-step')).toBeInTheDocument()
|
|
})
|
|
|
|
fireEvent.click(screen.getByTestId('loaded-back-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('select-package-step')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('should call onStartToInstall when install is triggered', async () => {
|
|
render(
|
|
<InstallFromGitHub
|
|
onClose={vi.fn()}
|
|
onSuccess={vi.fn()}
|
|
updatePayload={createUpdatePayload()}
|
|
/>,
|
|
)
|
|
|
|
fireEvent.click(screen.getByTestId('trigger-upload-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('loaded-step')).toBeInTheDocument()
|
|
})
|
|
|
|
fireEvent.click(screen.getByTestId('start-install-btn'))
|
|
|
|
expect(mockHideLogicState.handleStartToInstall).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('should call onInstalled on successful installation', async () => {
|
|
const onSuccess = vi.fn()
|
|
render(
|
|
<InstallFromGitHub
|
|
onClose={vi.fn()}
|
|
onSuccess={onSuccess}
|
|
updatePayload={createUpdatePayload()}
|
|
/>,
|
|
)
|
|
|
|
fireEvent.click(screen.getByTestId('trigger-upload-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('loaded-step')).toBeInTheDocument()
|
|
})
|
|
|
|
fireEvent.click(screen.getByTestId('install-success-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(onSuccess).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
it('should call onFailed on installation failure', async () => {
|
|
render(
|
|
<InstallFromGitHub
|
|
onClose={vi.fn()}
|
|
onSuccess={vi.fn()}
|
|
updatePayload={createUpdatePayload()}
|
|
/>,
|
|
)
|
|
|
|
fireEvent.click(screen.getByTestId('trigger-upload-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('loaded-step')).toBeInTheDocument()
|
|
})
|
|
|
|
fireEvent.click(screen.getByTestId('install-fail-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('installed-step')).toBeInTheDocument()
|
|
expect(screen.getByTestId('is-failed')).toHaveTextContent('true')
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('Installation Flows', () => {
|
|
it('should handle fresh install flow', async () => {
|
|
const onSuccess = vi.fn()
|
|
render(
|
|
<InstallFromGitHub
|
|
onClose={vi.fn()}
|
|
onSuccess={onSuccess}
|
|
updatePayload={createUpdatePayload()}
|
|
/>,
|
|
)
|
|
|
|
// Navigate to loaded step
|
|
fireEvent.click(screen.getByTestId('trigger-upload-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('loaded-step')).toBeInTheDocument()
|
|
})
|
|
|
|
// Trigger install
|
|
fireEvent.click(screen.getByTestId('install-success-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('installed-step')).toBeInTheDocument()
|
|
expect(onSuccess).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
it('should handle update flow with updatePayload', async () => {
|
|
const onSuccess = vi.fn()
|
|
const updatePayload = createUpdatePayload()
|
|
|
|
render(
|
|
<InstallFromGitHub
|
|
onClose={vi.fn()}
|
|
onSuccess={onSuccess}
|
|
updatePayload={updatePayload}
|
|
/>,
|
|
)
|
|
|
|
// Navigate to loaded step
|
|
fireEvent.click(screen.getByTestId('trigger-upload-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('loaded-step')).toBeInTheDocument()
|
|
})
|
|
|
|
// Trigger install (update)
|
|
fireEvent.click(screen.getByTestId('install-success-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(onSuccess).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
it('should refresh plugin list after successful install', async () => {
|
|
render(
|
|
<InstallFromGitHub
|
|
onClose={vi.fn()}
|
|
onSuccess={vi.fn()}
|
|
updatePayload={createUpdatePayload()}
|
|
/>,
|
|
)
|
|
|
|
fireEvent.click(screen.getByTestId('trigger-upload-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('loaded-step')).toBeInTheDocument()
|
|
})
|
|
|
|
fireEvent.click(screen.getByTestId('install-success-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(mockRefreshPluginList).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
it('should not refresh plugin list when notRefresh is true', async () => {
|
|
render(
|
|
<InstallFromGitHub
|
|
onClose={vi.fn()}
|
|
onSuccess={vi.fn()}
|
|
updatePayload={createUpdatePayload()}
|
|
/>,
|
|
)
|
|
|
|
fireEvent.click(screen.getByTestId('trigger-upload-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('loaded-step')).toBeInTheDocument()
|
|
})
|
|
|
|
fireEvent.click(screen.getByTestId('install-success-no-refresh-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(mockRefreshPluginList).not.toHaveBeenCalled()
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('Error Handling', () => {
|
|
it('should display error message on failure', async () => {
|
|
render(
|
|
<InstallFromGitHub
|
|
onClose={vi.fn()}
|
|
onSuccess={vi.fn()}
|
|
updatePayload={createUpdatePayload()}
|
|
/>,
|
|
)
|
|
|
|
fireEvent.click(screen.getByTestId('trigger-upload-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('loaded-step')).toBeInTheDocument()
|
|
})
|
|
|
|
fireEvent.click(screen.getByTestId('install-fail-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('error-msg')).toHaveTextContent('Install failed')
|
|
})
|
|
})
|
|
|
|
it('should handle failure without error message', async () => {
|
|
render(
|
|
<InstallFromGitHub
|
|
onClose={vi.fn()}
|
|
onSuccess={vi.fn()}
|
|
updatePayload={createUpdatePayload()}
|
|
/>,
|
|
)
|
|
|
|
fireEvent.click(screen.getByTestId('trigger-upload-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('loaded-step')).toBeInTheDocument()
|
|
})
|
|
|
|
fireEvent.click(screen.getByTestId('install-fail-no-msg-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('installed-step')).toBeInTheDocument()
|
|
expect(screen.getByTestId('is-failed')).toHaveTextContent('true')
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('Edge Cases', () => {
|
|
it('should handle missing optional props', async () => {
|
|
render(
|
|
<InstallFromGitHub
|
|
onClose={vi.fn()}
|
|
onSuccess={vi.fn()}
|
|
updatePayload={createUpdatePayload()}
|
|
/>,
|
|
)
|
|
|
|
fireEvent.click(screen.getByTestId('trigger-upload-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('loaded-step')).toBeInTheDocument()
|
|
})
|
|
|
|
// Should not throw when onStartToInstall is called
|
|
expect(() => {
|
|
fireEvent.click(screen.getByTestId('start-install-btn'))
|
|
}).not.toThrow()
|
|
})
|
|
|
|
it('should preserve state through component updates', async () => {
|
|
const { rerender } = render(
|
|
<InstallFromGitHub
|
|
onClose={vi.fn()}
|
|
onSuccess={vi.fn()}
|
|
updatePayload={createUpdatePayload()}
|
|
/>,
|
|
)
|
|
|
|
fireEvent.click(screen.getByTestId('trigger-upload-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('loaded-step')).toBeInTheDocument()
|
|
})
|
|
|
|
// Rerender
|
|
rerender(
|
|
<InstallFromGitHub
|
|
onClose={vi.fn()}
|
|
onSuccess={vi.fn()}
|
|
updatePayload={createUpdatePayload()}
|
|
/>,
|
|
)
|
|
|
|
// State should be preserved
|
|
expect(screen.getByTestId('loaded-step')).toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|