mirror of
https://github.com/langgenius/dify.git
synced 2026-05-26 22:00:53 -04:00
refactor(web): simplify github install focus (#36314)
This commit is contained in:
@@ -101,25 +101,6 @@ vi.mock('../../hooks/use-hide-logic', () => ({
|
||||
}))
|
||||
|
||||
// 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,
|
||||
@@ -236,6 +217,10 @@ vi.mock('../../base/installed', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
const getRepoUrlInput = () => screen.getByLabelText('plugin.installFromGitHub.gitHubRepo')
|
||||
const getNextButton = () => screen.getByRole('button', { name: 'plugin.installModal.next' })
|
||||
const getCancelButton = () => screen.getByRole('button', { name: 'plugin.installModal.cancel' })
|
||||
|
||||
describe('InstallFromGitHub', () => {
|
||||
const defaultProps = {
|
||||
onClose: vi.fn(),
|
||||
@@ -261,8 +246,8 @@ describe('InstallFromGitHub', () => {
|
||||
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('')
|
||||
expect(getRepoUrlInput()).toBeInTheDocument()
|
||||
expect(getRepoUrlInput()).toHaveValue('')
|
||||
})
|
||||
|
||||
it('should render modal with selectPackage step when updatePayload is provided', () => {
|
||||
@@ -312,7 +297,7 @@ describe('InstallFromGitHub', () => {
|
||||
it('should update repoUrl when user types in input', () => {
|
||||
render(<InstallFromGitHub {...defaultProps} />)
|
||||
|
||||
const input = screen.getByTestId('repo-url-input')
|
||||
const input = getRepoUrlInput()
|
||||
fireEvent.change(input, { target: { value: 'https://github.com/test/repo' } })
|
||||
|
||||
expect(input).toHaveValue('https://github.com/test/repo')
|
||||
@@ -321,10 +306,10 @@ describe('InstallFromGitHub', () => {
|
||||
it('should transition from setUrl to selectPackage on successful URL submit', async () => {
|
||||
render(<InstallFromGitHub {...defaultProps} />)
|
||||
|
||||
const input = screen.getByTestId('repo-url-input')
|
||||
const input = getRepoUrlInput()
|
||||
fireEvent.change(input, { target: { value: 'https://github.com/owner/repo' } })
|
||||
|
||||
const nextBtn = screen.getByTestId('next-btn')
|
||||
const nextBtn = getNextButton()
|
||||
fireEvent.click(nextBtn)
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -443,10 +428,10 @@ describe('InstallFromGitHub', () => {
|
||||
it('should show error toast for invalid GitHub URL', async () => {
|
||||
render(<InstallFromGitHub {...defaultProps} />)
|
||||
|
||||
const input = screen.getByTestId('repo-url-input')
|
||||
const input = getRepoUrlInput()
|
||||
fireEvent.change(input, { target: { value: 'invalid-url' } })
|
||||
|
||||
const nextBtn = screen.getByTestId('next-btn')
|
||||
const nextBtn = getNextButton()
|
||||
fireEvent.click(nextBtn)
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -462,10 +447,10 @@ describe('InstallFromGitHub', () => {
|
||||
|
||||
render(<InstallFromGitHub {...defaultProps} />)
|
||||
|
||||
const input = screen.getByTestId('repo-url-input')
|
||||
const input = getRepoUrlInput()
|
||||
fireEvent.change(input, { target: { value: 'https://github.com/owner/repo' } })
|
||||
|
||||
const nextBtn = screen.getByTestId('next-btn')
|
||||
const nextBtn = getNextButton()
|
||||
fireEvent.click(nextBtn)
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -481,10 +466,10 @@ describe('InstallFromGitHub', () => {
|
||||
|
||||
render(<InstallFromGitHub {...defaultProps} />)
|
||||
|
||||
const input = screen.getByTestId('repo-url-input')
|
||||
const input = getRepoUrlInput()
|
||||
fireEvent.change(input, { target: { value: 'https://github.com/owner/repo' } })
|
||||
|
||||
const nextBtn = screen.getByTestId('next-btn')
|
||||
const nextBtn = getNextButton()
|
||||
fireEvent.click(nextBtn)
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -504,9 +489,9 @@ describe('InstallFromGitHub', () => {
|
||||
render(<InstallFromGitHub {...defaultProps} />)
|
||||
|
||||
// Navigate to selectPackage
|
||||
const input = screen.getByTestId('repo-url-input')
|
||||
const input = getRepoUrlInput()
|
||||
fireEvent.change(input, { target: { value: 'https://github.com/owner/repo' } })
|
||||
fireEvent.click(screen.getByTestId('next-btn'))
|
||||
fireEvent.click(getNextButton())
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('select-package-step')).toBeInTheDocument()
|
||||
@@ -516,7 +501,7 @@ describe('InstallFromGitHub', () => {
|
||||
fireEvent.click(screen.getByTestId('back-btn'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('set-url-step')).toBeInTheDocument()
|
||||
expect(getRepoUrlInput()).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -546,7 +531,7 @@ describe('InstallFromGitHub', () => {
|
||||
it('should call onClose when cancel button is clicked', () => {
|
||||
render(<InstallFromGitHub {...defaultProps} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('cancel-btn'))
|
||||
fireEvent.click(getCancelButton())
|
||||
|
||||
expect(defaultProps.onClose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
@@ -783,10 +768,10 @@ describe('InstallFromGitHub', () => {
|
||||
it('should handle URL without trailing slash', async () => {
|
||||
render(<InstallFromGitHub {...defaultProps} />)
|
||||
|
||||
const input = screen.getByTestId('repo-url-input')
|
||||
const input = getRepoUrlInput()
|
||||
fireEvent.change(input, { target: { value: 'https://github.com/owner/repo' } })
|
||||
|
||||
fireEvent.click(screen.getByTestId('next-btn'))
|
||||
fireEvent.click(getNextButton())
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockFetchReleases).toHaveBeenCalledWith('owner', 'repo')
|
||||
@@ -797,11 +782,11 @@ describe('InstallFromGitHub', () => {
|
||||
render(<InstallFromGitHub {...defaultProps} />)
|
||||
|
||||
// Set URL
|
||||
const input = screen.getByTestId('repo-url-input')
|
||||
const input = getRepoUrlInput()
|
||||
fireEvent.change(input, { target: { value: 'https://github.com/test/myrepo' } })
|
||||
|
||||
// Navigate to selectPackage
|
||||
fireEvent.click(screen.getByTestId('next-btn'))
|
||||
fireEvent.click(getNextButton())
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('select-package-step')).toBeInTheDocument()
|
||||
@@ -980,12 +965,12 @@ describe('InstallFromGitHub', () => {
|
||||
render(<InstallFromGitHub {...defaultProps} />)
|
||||
|
||||
// Start from setUrl step
|
||||
expect(screen.getByTestId('set-url-step')).toBeInTheDocument()
|
||||
expect(getRepoUrlInput()).toBeInTheDocument()
|
||||
|
||||
// Enter URL
|
||||
const input = screen.getByTestId('repo-url-input')
|
||||
const input = getRepoUrlInput()
|
||||
fireEvent.change(input, { target: { value: 'https://github.com/owner/repo' } })
|
||||
fireEvent.click(screen.getByTestId('next-btn'))
|
||||
fireEvent.click(getNextButton())
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('select-package-step')).toBeInTheDocument()
|
||||
@@ -1086,7 +1071,7 @@ describe('InstallFromGitHub', () => {
|
||||
render(<InstallFromGitHub {...defaultProps} />)
|
||||
|
||||
// Verify we're on setUrl step
|
||||
expect(screen.getByTestId('set-url-step')).toBeInTheDocument()
|
||||
expect(getRepoUrlInput()).toBeInTheDocument()
|
||||
|
||||
// The setUrl step doesn't expose onBack in the real component,
|
||||
// but our mock doesn't have it either - this is correct behavior
|
||||
@@ -1097,9 +1082,9 @@ describe('InstallFromGitHub', () => {
|
||||
render(<InstallFromGitHub {...defaultProps} />)
|
||||
|
||||
// Navigate to selectPackage
|
||||
const input = screen.getByTestId('repo-url-input')
|
||||
const input = getRepoUrlInput()
|
||||
fireEvent.change(input, { target: { value: 'https://github.com/owner/repo' } })
|
||||
fireEvent.click(screen.getByTestId('next-btn'))
|
||||
fireEvent.click(getNextButton())
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('select-package-step')).toBeInTheDocument()
|
||||
@@ -1123,11 +1108,11 @@ describe('InstallFromGitHub', () => {
|
||||
fireEvent.click(screen.getByTestId('back-btn'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('set-url-step')).toBeInTheDocument()
|
||||
expect(getRepoUrlInput()).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Verify URL is preserved after back navigation
|
||||
expect(screen.getByTestId('repo-url-input')).toHaveValue('https://github.com/owner/repo')
|
||||
expect(getRepoUrlInput()).toHaveValue('https://github.com/owner/repo')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1368,114 +1353,6 @@ describe('Install Plugin Utils', () => {
|
||||
// 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(() => {
|
||||
@@ -1513,9 +1390,9 @@ describe('SelectPackage Component', () => {
|
||||
render(<InstallFromGitHub onClose={vi.fn()} onSuccess={vi.fn()} />)
|
||||
|
||||
// Navigate to selectPackage step
|
||||
const input = screen.getByTestId('repo-url-input')
|
||||
const input = getRepoUrlInput()
|
||||
fireEvent.change(input, { target: { value: 'https://github.com/owner/repo' } })
|
||||
fireEvent.click(screen.getByTestId('next-btn'))
|
||||
fireEvent.click(getNextButton())
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('back-btn')).toBeInTheDocument()
|
||||
@@ -1590,9 +1467,9 @@ describe('SelectPackage Component', () => {
|
||||
render(<InstallFromGitHub onClose={vi.fn()} onSuccess={vi.fn()} />)
|
||||
|
||||
// Navigate to selectPackage
|
||||
const input = screen.getByTestId('repo-url-input')
|
||||
const input = getRepoUrlInput()
|
||||
fireEvent.change(input, { target: { value: 'https://github.com/owner/repo' } })
|
||||
fireEvent.click(screen.getByTestId('next-btn'))
|
||||
fireEvent.click(getNextButton())
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('select-package-step')).toBeInTheDocument()
|
||||
@@ -1601,7 +1478,7 @@ describe('SelectPackage Component', () => {
|
||||
fireEvent.click(screen.getByTestId('back-btn'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('set-url-step')).toBeInTheDocument()
|
||||
expect(getRepoUrlInput()).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import type { PluginDeclaration, UpdateFromGitHubPayload } from '../../types'
|
||||
import type { InstallState } from '@/app/components/plugins/types'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Dialog, DialogCloseButton, DialogContent } from '@langgenius/dify-ui/dialog'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
@@ -17,7 +18,6 @@ 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 = 'installFromGitHub'
|
||||
|
||||
@@ -194,12 +194,41 @@ const InstallFromGitHub: React.FC<InstallFromGitHubProps> = ({ updatePayload, on
|
||||
: (
|
||||
<div className={`flex min-h-0 flex-1 flex-col items-start justify-center self-stretch overflow-y-auto 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}
|
||||
/>
|
||||
<>
|
||||
<label
|
||||
htmlFor="repoUrl"
|
||||
className="flex flex-col items-start justify-center self-stretch text-text-secondary"
|
||||
>
|
||||
<span className="system-sm-semibold">{t('installFromGitHub.gitHubRepo', { ns: 'plugin' })}</span>
|
||||
</label>
|
||||
<input
|
||||
autoFocus
|
||||
type="url"
|
||||
id="repoUrl"
|
||||
name="repoUrl"
|
||||
value={state.repoUrl}
|
||||
onChange={e => setState(prevState => ({ ...prevState, repoUrl: e.target.value }))}
|
||||
className="flex grow items-center gap-0.5 self-stretch overflow-hidden rounded-lg border border-components-input-border-active bg-components-input-bg-active p-2 system-sm-regular text-ellipsis text-components-input-text-filled outline-hidden"
|
||||
placeholder="Please enter GitHub repo URL"
|
||||
/>
|
||||
<div className="mt-4 flex items-center justify-end gap-2 self-stretch">
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="min-w-18"
|
||||
onClick={onClose}
|
||||
>
|
||||
{t('installModal.cancel', { ns: 'plugin' })}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
className="min-w-18"
|
||||
onClick={handleUrlSubmit}
|
||||
disabled={!state.repoUrl.trim()}
|
||||
>
|
||||
{t('installModal.next', { ns: 'plugin' })}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{state.step === InstallStepFromGitHub.selectPackage && (
|
||||
<SelectPackage
|
||||
@@ -216,11 +245,11 @@ const InstallFromGitHub: React.FC<InstallFromGitHubProps> = ({ updatePayload, on
|
||||
onBack={handleBack}
|
||||
/>
|
||||
)}
|
||||
{state.step === InstallStepFromGitHub.readyToInstall && (
|
||||
{state.step === InstallStepFromGitHub.readyToInstall && manifest && uniqueIdentifier && (
|
||||
<Loaded
|
||||
updatePayload={updatePayload!}
|
||||
uniqueIdentifier={uniqueIdentifier!}
|
||||
payload={manifest as any}
|
||||
uniqueIdentifier={uniqueIdentifier}
|
||||
payload={manifest}
|
||||
repoUrl={state.repoUrl}
|
||||
selectedVersion={state.selectedVersion}
|
||||
selectedPackage={state.selectedPackage}
|
||||
|
||||
@@ -1,189 +0,0 @@
|
||||
import { fireEvent, render, screen, waitFor } 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()
|
||||
})
|
||||
|
||||
it('should auto-focus the input on mount', async () => {
|
||||
render(<SetURL {...defaultProps} />)
|
||||
|
||||
const input = screen.getByRole('textbox')
|
||||
await waitFor(() => {
|
||||
expect(input).toHaveFocus()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// 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,69 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type SetURLProps = {
|
||||
repoUrl: string
|
||||
onChange: (value: string) => void
|
||||
onNext: () => void
|
||||
onCancel: () => void
|
||||
}
|
||||
|
||||
const SetURL: React.FC<SetURLProps> = ({ repoUrl, onChange, onNext, onCancel }) => {
|
||||
const { t } = useTranslation()
|
||||
const inputRef = React.useRef<HTMLInputElement>(null)
|
||||
|
||||
// Focus the input after the dropdown's focus-return animation settles.
|
||||
// Using rAF avoids racing the DropdownMenu FloatingFocusManager that returns
|
||||
// focus to the trigger on close.
|
||||
React.useEffect(() => {
|
||||
const frame = requestAnimationFrame(() => {
|
||||
inputRef.current?.focus()
|
||||
})
|
||||
return () => cancelAnimationFrame(frame)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<label
|
||||
htmlFor="repoUrl"
|
||||
className="flex flex-col items-start justify-center self-stretch text-text-secondary"
|
||||
>
|
||||
<span className="system-sm-semibold">{t('installFromGitHub.gitHubRepo', { ns: 'plugin' })}</span>
|
||||
</label>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="url"
|
||||
id="repoUrl"
|
||||
name="repoUrl"
|
||||
value={repoUrl}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
className="shadows-shadow-xs flex grow items-center gap-[2px] self-stretch
|
||||
overflow-hidden rounded-lg border border-components-input-border-active bg-components-input-bg-active p-2
|
||||
system-sm-regular text-ellipsis text-components-input-text-filled outline-hidden"
|
||||
placeholder="Please enter GitHub repo URL"
|
||||
/>
|
||||
<div className="mt-4 flex items-center justify-end gap-2 self-stretch">
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="min-w-[72px]"
|
||||
onClick={onCancel}
|
||||
>
|
||||
{t('installModal.cancel', { ns: 'plugin' })}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
className="min-w-[72px]"
|
||||
onClick={onNext}
|
||||
disabled={!repoUrl.trim()}
|
||||
>
|
||||
{t('installModal.next', { ns: 'plugin' })}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SetURL
|
||||
Reference in New Issue
Block a user