From ddd546ef88f9293da956ad0f235d2eaffd92b7e7 Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Thu, 23 Apr 2026 14:46:54 +0800 Subject: [PATCH] fix(web): three small UX fixes on /datasets and /plugins (#35514) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- eslint-suppressions.json | 5 - .../service-api/__tests__/card.spec.tsx | 45 +- .../service-api/__tests__/index.spec.tsx | 518 +----------------- .../datasets/extra-info/service-api/card.tsx | 44 +- .../datasets/extra-info/service-api/index.tsx | 17 +- .../datasets/list/__tests__/index.spec.tsx | 8 + .../steps/__tests__/setURL.spec.tsx | 11 +- .../install-from-github/steps/setURL.tsx | 13 + .../plugin-page/__tests__/debug-info.spec.tsx | 41 +- .../plugins/plugin-page/debug-info.tsx | 81 +-- 10 files changed, 168 insertions(+), 615 deletions(-) diff --git a/eslint-suppressions.json b/eslint-suppressions.json index dcb58d4b57..405ce77400 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -3542,11 +3542,6 @@ "count": 1 } }, - "web/app/components/plugins/plugin-page/debug-info.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/plugins/plugin-page/empty/index.tsx": { "react/set-state-in-effect": { "count": 2 diff --git a/web/app/components/datasets/extra-info/service-api/__tests__/card.spec.tsx b/web/app/components/datasets/extra-info/service-api/__tests__/card.spec.tsx index 30f5f095eb..678434b8c0 100644 --- a/web/app/components/datasets/extra-info/service-api/__tests__/card.spec.tsx +++ b/web/app/components/datasets/extra-info/service-api/__tests__/card.spec.tsx @@ -1,3 +1,4 @@ +import { Popover } from '@langgenius/dify-ui/popover' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { fireEvent, render, screen } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest' @@ -7,18 +8,15 @@ vi.mock('@/hooks/use-api-access-url', () => ({ useDatasetApiAccessUrl: () => 'https://docs.dify.ai/api-reference/datasets', })) -vi.mock('@/app/components/develop/secret-key/secret-key-modal', () => ({ - default: ({ isShow, onClose }: { isShow: boolean, onClose: () => void }) => - isShow ?
: null, -})) - const createWrapper = () => { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } }, }) return ({ children }: { children: React.ReactNode }) => ( - {children} + + {children} + ) } @@ -28,8 +26,10 @@ const renderWithProviders = (ui: React.ReactElement) => { } describe('Card (Service API)', () => { + const onOpenSecretKeyModal = vi.fn() const defaultProps = { apiBaseUrl: 'https://api.dify.ai/v1', + onOpenSecretKeyModal, } beforeEach(() => { @@ -77,48 +77,33 @@ describe('Card (Service API)', () => { // Props: tests different apiBaseUrl values describe('Props', () => { it('should display provided apiBaseUrl', () => { - renderWithProviders() + renderWithProviders() expect(screen.getByText('https://custom-api.example.com')).toBeInTheDocument() }) it('should show green indicator when apiBaseUrl is provided', () => { - renderWithProviders() + renderWithProviders() // The Indicator component receives color="green" when apiBaseUrl is truthy const statusText = screen.getByText(/serviceApi\.enabled/) expect(statusText).toHaveClass('text-text-success') }) it('should show yellow indicator when apiBaseUrl is empty', () => { - renderWithProviders() + renderWithProviders() // Still shows "enabled" text but indicator color differs expect(screen.getByText(/serviceApi\.enabled/)).toBeInTheDocument() }) }) - // User Interactions: tests button clicks and modal + // User Interactions: tests button clicks describe('User Interactions', () => { - it('should open secret key modal when API key button is clicked', () => { - renderWithProviders() - - // Modal should not be visible before clicking - expect(screen.queryByTestId('secret-key-modal')).not.toBeInTheDocument() - - const apiKeyButton = screen.getByText(/serviceApi\.card\.apiKey/).closest('button') - fireEvent.click(apiKeyButton!) - - // Modal should appear after clicking - expect(screen.getByTestId('secret-key-modal')).toBeInTheDocument() - }) - - it('should close secret key modal when onClose is called', () => { + it('should call onOpenSecretKeyModal when API key button is clicked', () => { renderWithProviders() const apiKeyButton = screen.getByText(/serviceApi\.card\.apiKey/).closest('button') fireEvent.click(apiKeyButton!) - expect(screen.getByTestId('secret-key-modal')).toBeInTheDocument() - fireEvent.click(screen.getByText('close')) - expect(screen.queryByTestId('secret-key-modal')).not.toBeInTheDocument() + expect(onOpenSecretKeyModal).toHaveBeenCalledTimes(1) }) it('should render API reference as a link', () => { @@ -148,20 +133,20 @@ describe('Card (Service API)', () => { // Edge Cases: tests empty/long URLs describe('Edge Cases', () => { it('should handle empty apiBaseUrl', () => { - renderWithProviders() + renderWithProviders() // Should still render the structure expect(screen.getByText(/serviceApi\.card\.endpoint/)).toBeInTheDocument() }) it('should handle very long apiBaseUrl', () => { const longUrl = `https://api.dify.ai/${'path/'.repeat(50)}` - renderWithProviders() + renderWithProviders() expect(screen.getByText(longUrl)).toBeInTheDocument() }) it('should handle apiBaseUrl with special characters', () => { const specialUrl = 'https://api.dify.ai/v1?key=value&foo=bar' - renderWithProviders() + renderWithProviders() expect(screen.getByText(specialUrl)).toBeInTheDocument() }) }) diff --git a/web/app/components/datasets/extra-info/service-api/__tests__/index.spec.tsx b/web/app/components/datasets/extra-info/service-api/__tests__/index.spec.tsx index 8137052383..93b752d9a0 100644 --- a/web/app/components/datasets/extra-info/service-api/__tests__/index.spec.tsx +++ b/web/app/components/datasets/extra-info/service-api/__tests__/index.spec.tsx @@ -1,9 +1,8 @@ +import { Popover } from '@langgenius/dify-ui/popover' import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { beforeEach, describe, expect, it, vi } from 'vitest' -// Component Imports (after mocks) - import Card from '../card' import ServiceApi from '../index' @@ -43,7 +42,8 @@ vi.mock('@/app/components/develop/secret-key/secret-key-modal', () => ({ ), })) -// ServiceApi Component Tests +const renderCard = (ui: React.ReactElement) => + render({ui}) describe('ServiceApi', () => { beforeEach(() => { @@ -80,18 +80,15 @@ describe('ServiceApi', () => { }) }) - // Props Variations Tests describe('Props Variations', () => { - it('should show green Indicator when apiBaseUrl is provided', () => { + it('should show Indicator when apiBaseUrl is provided', () => { const { container } = render() - // When apiBaseUrl is truthy, Indicator color is green const triggerContainer = container.querySelector('.relative.flex.h-8') expect(triggerContainer).toBeInTheDocument() }) - it('should show yellow Indicator when apiBaseUrl is empty', () => { + it('should show Indicator when apiBaseUrl is empty', () => { const { container } = render() - // When apiBaseUrl is falsy, Indicator color is yellow const triggerContainer = container.querySelector('.relative.flex.h-8') expect(triggerContainer).toBeInTheDocument() }) @@ -110,28 +107,7 @@ describe('ServiceApi', () => { }) describe('User Interactions', () => { - it('should toggle popup open state on click', async () => { - const user = userEvent.setup() - - render() - - const trigger = screen.getByText(/serviceApi\.title/i).closest('[class*="cursor-pointer"]') - expect(trigger).toBeInTheDocument() - - if (trigger) - await user.click(trigger) - - // After click, the Card should be rendered - }) - - it('should apply hover styles on trigger', () => { - render() - - const trigger = screen.getByText(/serviceApi\.title/i).closest('div[class*="cursor-pointer"]') - expect(trigger).toHaveClass('cursor-pointer') - }) - - it('should toggle open state from false to true on first click', async () => { + it('should open popup on trigger click', async () => { const user = userEvent.setup() render() @@ -140,56 +116,13 @@ describe('ServiceApi', () => { if (trigger) await user.click(trigger) - // Card should be visible after clicking await waitFor(() => { expect(screen.getByText(/serviceApi\.card\.title/i)).toBeInTheDocument() }) }) - - it('should toggle open state back to false on second click', async () => { - const user = userEvent.setup() - - render() - - const trigger = screen.getByText(/serviceApi\.title/i).closest('[class*="cursor-pointer"]') - if (trigger) { - await user.click(trigger) // open - await user.click(trigger) // close - } - - // Component should handle the toggle without errors - }) - - it('should apply open state styling when popup is open', async () => { - const user = userEvent.setup() - - render() - - const trigger = screen.getByText(/serviceApi\.title/i).closest('[class*="cursor-pointer"]') - if (trigger) - await user.click(trigger) - - // When open, the trigger should have hover background class - }) }) - // Portal and Card Integration Tests describe('Portal and Card Integration', () => { - it('should render Card component inside portal when open', async () => { - const user = userEvent.setup() - - render() - - const trigger = screen.getByText(/serviceApi\.title/i).closest('[class*="cursor-pointer"]') - if (trigger) - await user.click(trigger) - - // Wait for portal content to appear - await waitFor(() => { - expect(screen.getByText(/serviceApi\.card\.title/i)).toBeInTheDocument() - }) - }) - it('should pass apiBaseUrl prop to Card component', async () => { const user = userEvent.setup() const testUrl = 'https://test-api.example.com' @@ -204,38 +137,9 @@ describe('ServiceApi', () => { expect(screen.getByText(testUrl)).toBeInTheDocument() }) }) - - it('should use correct portal placement configuration', () => { - render() - // PortalToFollowElem is configured with placement="top-start" - expect(screen.getByText(/serviceApi\.title/i)).toBeInTheDocument() - }) - - it('should use correct portal offset configuration', () => { - render() - // PortalToFollowElem is configured with offset={{ mainAxis: 4, crossAxis: -4 }} - expect(screen.getByText(/serviceApi\.title/i)).toBeInTheDocument() - }) }) describe('Edge Cases', () => { - it('should handle rapid toggle clicks gracefully', async () => { - const user = userEvent.setup() - - render() - - const trigger = screen.getByText(/serviceApi\.title/i).closest('[class*="cursor-pointer"]') - if (trigger) { - // Rapid clicks - await user.click(trigger) - await user.click(trigger) - await user.click(trigger) - } - - // Component should handle state changes without errors - expect(screen.getByText(/serviceApi\.title/i)).toBeInTheDocument() - }) - it('should render correctly with empty apiBaseUrl', () => { render() expect(screen.getByText(/serviceApi\.title/i)).toBeInTheDocument() @@ -248,391 +152,60 @@ describe('ServiceApi', () => { rerender() - // Component should still render after prop change - expect(screen.getByText(/serviceApi\.title/i)).toBeInTheDocument() - }) - - it('should handle undefined-like apiBaseUrl values', () => { - // Empty string is the closest to undefined for this prop - render() - expect(screen.getByText(/serviceApi\.title/i)).toBeInTheDocument() - }) - }) - - describe('Memoization', () => { - it('should be memoized with React.memo', () => { - const { rerender } = render() - - rerender() - - expect(screen.getByText(/serviceApi\.title/i)).toBeInTheDocument() - }) - - it('should not re-render unnecessarily with same props', () => { - const { rerender } = render() - - rerender() - rerender() - - expect(screen.getByText(/serviceApi\.title/i)).toBeInTheDocument() - }) - - it('should update when apiBaseUrl prop changes', () => { - const { rerender } = render() - - rerender() - expect(screen.getByText(/serviceApi\.title/i)).toBeInTheDocument() }) }) }) -// Card Component Tests - describe('Card (service-api)', () => { + const onOpenSecretKeyModal = vi.fn() + beforeEach(() => { vi.clearAllMocks() }) describe('Rendering', () => { it('should render without crashing', () => { - render() + renderCard() expect(screen.getByText(/serviceApi\.card\.title/i)).toBeInTheDocument() }) - it('should display card title', () => { - render() - expect(screen.getByText(/serviceApi\.card\.title/i)).toBeInTheDocument() - }) - - it('should display enabled status', () => { - render() - expect(screen.getByText(/serviceApi\.enabled/i)).toBeInTheDocument() - }) - - it('should render endpoint label', () => { - render() - expect(screen.getByText(/serviceApi\.card\.endpoint/i)).toBeInTheDocument() - }) - it('should display apiBaseUrl in endpoint field', () => { const testUrl = 'https://api.example.com' - render() + renderCard() expect(screen.getByText(testUrl)).toBeInTheDocument() }) - it('should render Indicator component', () => { - const { container } = render() - // Card container should be present - const cardContainer = container.querySelector('.flex.w-\\[360px\\]') - expect(cardContainer).toBeInTheDocument() - }) - it('should render API Key button', () => { - render() + renderCard() expect(screen.getByText(/serviceApi\.card\.apiKey/i)).toBeInTheDocument() }) it('should render API Reference button', () => { - render() + renderCard() expect(screen.getByText(/serviceApi\.card\.apiReference/i)).toBeInTheDocument() }) - - it('should render CopyFeedback component for endpoint', () => { - const { container } = render() - // CopyFeedback should be in the endpoint section - const copyButton = container.querySelector('[class*="bg-components-input-bg-normal"]') - expect(copyButton).toBeInTheDocument() - }) - - it('should render ApiAggregate icon in header', () => { - const { container } = render() - const icon = container.querySelector('svg') - expect(icon).toBeInTheDocument() - }) - }) - - // Props Variations Tests - describe('Props Variations', () => { - it('should show green Indicator when apiBaseUrl is provided', () => { - const { container } = render() - const cardContainer = container.querySelector('.flex.w-\\[360px\\]') - expect(cardContainer).toBeInTheDocument() - }) - - it('should show yellow Indicator when apiBaseUrl is empty', () => { - const { container } = render() - const cardContainer = container.querySelector('.flex.w-\\[360px\\]') - expect(cardContainer).toBeInTheDocument() - }) - - it('should display different apiBaseUrl values correctly', () => { - const testUrls = [ - 'https://api.example.com', - 'https://localhost:3000', - 'https://api.production.example.com/v1', - ] - - testUrls.forEach((url) => { - const { unmount } = render() - expect(screen.getByText(url)).toBeInTheDocument() - unmount() - }) - }) - - it('should handle empty apiBaseUrl', () => { - render() - expect(screen.getByText(/serviceApi\.card\.title/i)).toBeInTheDocument() - }) - - it('should truncate long apiBaseUrl', () => { - const longUrl = 'https://api.example.com/v1/very/long/path/to/endpoint/that/should/truncate' - const { container } = render() - const truncateElement = container.querySelector('.truncate') - expect(truncateElement).toBeInTheDocument() - }) }) describe('User Interactions', () => { - it('should open SecretKeyModal when API Key button is clicked', async () => { + it('should call onOpenSecretKeyModal when API Key button is clicked', async () => { const user = userEvent.setup() - render() - - const apiKeyButton = screen.getByText(/serviceApi\.card\.apiKey/i).closest('button') - expect(apiKeyButton).toBeInTheDocument() - - if (apiKeyButton) - await user.click(apiKeyButton) - - await waitFor(() => { - expect(screen.getByTestId('secret-key-modal')).toBeInTheDocument() - }) - }) - - it('should close SecretKeyModal when close button is clicked', async () => { - const user = userEvent.setup() - - render() + renderCard() const apiKeyButton = screen.getByText(/serviceApi\.card\.apiKey/i).closest('button') if (apiKeyButton) await user.click(apiKeyButton) - await waitFor(() => { - expect(screen.getByTestId('secret-key-modal')).toBeInTheDocument() - }) - - const closeButton = screen.getByTestId('close-modal-btn') - await user.click(closeButton) - - await waitFor(() => { - expect(screen.queryByTestId('secret-key-modal')).not.toBeInTheDocument() - }) + expect(onOpenSecretKeyModal).toHaveBeenCalledTimes(1) }) it('should have correct href for API Reference link', () => { - render() + renderCard() const apiRefLink = screen.getByText(/serviceApi\.card\.apiReference/i).closest('a') expect(apiRefLink).toHaveAttribute('href', 'https://docs.dify.ai/api-reference/datasets') }) - - it('should open API Reference in new tab', () => { - render() - - const apiRefLink = screen.getByText(/serviceApi\.card\.apiReference/i).closest('a') - expect(apiRefLink).toHaveAttribute('target', '_blank') - expect(apiRefLink).toHaveAttribute('rel', 'noopener noreferrer') - }) - - it('should toggle modal visibility correctly', async () => { - const user = userEvent.setup() - - render() - - // Initially modal should not be visible - expect(screen.queryByTestId('secret-key-modal')).not.toBeInTheDocument() - - const apiKeyButton = screen.getByText(/serviceApi\.card\.apiKey/i).closest('button') - if (apiKeyButton) - await user.click(apiKeyButton) - - // Modal should be visible - await waitFor(() => { - expect(screen.getByTestId('secret-key-modal')).toBeInTheDocument() - }) - - const closeButton = screen.getByTestId('close-modal-btn') - await user.click(closeButton) - - // Modal should not be visible again - await waitFor(() => { - expect(screen.queryByTestId('secret-key-modal')).not.toBeInTheDocument() - }) - }) - }) - - // Modal State Tests - describe('Modal State', () => { - it('should initialize with modal closed', () => { - render() - expect(screen.queryByTestId('secret-key-modal')).not.toBeInTheDocument() - }) - - it('should open modal on handleOpenSecretKeyModal', async () => { - const user = userEvent.setup() - - render() - - const apiKeyButton = screen.getByText(/serviceApi\.card\.apiKey/i).closest('button') - if (apiKeyButton) - await user.click(apiKeyButton) - - await waitFor(() => { - expect(screen.getByTestId('secret-key-modal')).toBeInTheDocument() - }) - }) - - it('should close modal on handleCloseSecretKeyModal', async () => { - const user = userEvent.setup() - - render() - - // Open modal first - const apiKeyButton = screen.getByText(/serviceApi\.card\.apiKey/i).closest('button') - if (apiKeyButton) - await user.click(apiKeyButton) - - await waitFor(() => { - expect(screen.getByTestId('secret-key-modal')).toBeInTheDocument() - }) - - const closeButton = screen.getByTestId('close-modal-btn') - await user.click(closeButton) - - await waitFor(() => { - expect(screen.queryByTestId('secret-key-modal')).not.toBeInTheDocument() - }) - }) - - it('should handle multiple open/close cycles', async () => { - const user = userEvent.setup() - - render() - - const apiKeyButton = screen.getByText(/serviceApi\.card\.apiKey/i).closest('button') - - // First cycle - if (apiKeyButton) - await user.click(apiKeyButton) - - await waitFor(() => { - expect(screen.getByTestId('secret-key-modal')).toBeInTheDocument() - }) - - await user.click(screen.getByTestId('close-modal-btn')) - - await waitFor(() => { - expect(screen.queryByTestId('secret-key-modal')).not.toBeInTheDocument() - }) - - // Second cycle - if (apiKeyButton) - await user.click(apiKeyButton) - - await waitFor(() => { - expect(screen.getByTestId('secret-key-modal')).toBeInTheDocument() - }) - }) - }) - - describe('Edge Cases', () => { - it('should handle empty apiBaseUrl gracefully', () => { - render() - expect(screen.getByText(/serviceApi\.card\.title/i)).toBeInTheDocument() - // Endpoint field should show empty string - }) - - it('should handle very long apiBaseUrl', () => { - const longUrl = 'https://'.concat('a'.repeat(500), '.com') - render() - expect(screen.getByText(/serviceApi\.card\.title/i)).toBeInTheDocument() - }) - - it('should handle special characters in apiBaseUrl', () => { - const specialUrl = 'https://api.example.com/path?query=test¶m=value#anchor' - render() - expect(screen.getByText(specialUrl)).toBeInTheDocument() - }) - - it('should render without errors when all buttons are clickable', async () => { - const user = userEvent.setup() - - render() - - const apiKeyButton = screen.getByText(/serviceApi\.card\.apiKey/i).closest('button') - if (apiKeyButton) - await user.click(apiKeyButton) - - await waitFor(() => { - expect(screen.getByTestId('secret-key-modal')).toBeInTheDocument() - }) - - await user.click(screen.getByTestId('close-modal-btn')) - - await waitFor(() => { - expect(screen.queryByTestId('secret-key-modal')).not.toBeInTheDocument() - }) - - // Component should still be functional - expect(screen.getByText(/serviceApi\.card\.title/i)).toBeInTheDocument() - }) - }) - - describe('Memoization', () => { - it('should be memoized with React.memo', () => { - const { rerender } = render() - - rerender() - - expect(screen.getByText(/serviceApi\.card\.title/i)).toBeInTheDocument() - }) - - it('should use useCallback for handlers', () => { - const { rerender } = render() - - rerender() - - // Component should render without issues with memoized callbacks - expect(screen.getByText(/serviceApi\.card\.apiKey/i)).toBeInTheDocument() - }) - - it('should update when apiBaseUrl prop changes', () => { - const { rerender } = render() - - expect(screen.getByText('https://api.example.com')).toBeInTheDocument() - - rerender() - - expect(screen.getByText('https://new-api.example.com')).toBeInTheDocument() - }) - }) - - // Copy Functionality Tests - describe('Copy Functionality', () => { - it('should render CopyFeedback component for apiBaseUrl', () => { - const { container } = render() - const copyContainer = container.querySelector('[class*="bg-components-input-bg-normal"]') - expect(copyContainer).toBeInTheDocument() - }) - - it('should pass apiBaseUrl to CopyFeedback component', () => { - const testUrl = 'https://api.example.com' - render() - // The URL should be displayed in the copy section - expect(screen.getByText(testUrl)).toBeInTheDocument() - }) }) }) @@ -641,78 +214,33 @@ describe('ServiceApi Integration', () => { vi.clearAllMocks() }) - it('should open Card popup and display endpoint', async () => { - const user = userEvent.setup() - const testUrl = 'https://api.example.com' - - render() - - // Open popup - const trigger = screen.getByText(/serviceApi\.title/i).closest('[class*="cursor-pointer"]') - if (trigger) - await user.click(trigger) - - // Wait for Card to appear - await waitFor(() => { - expect(screen.getByText(/serviceApi\.card\.title/i)).toBeInTheDocument() - expect(screen.getByText(testUrl)).toBeInTheDocument() - }) - }) - - it('should complete full workflow: open -> view endpoint -> access API key', async () => { + it('should close popover and open modal when API Key button is clicked', async () => { const user = userEvent.setup() render() - // Open popup + // Open popover const trigger = screen.getByText(/serviceApi\.title/i).closest('[class*="cursor-pointer"]') if (trigger) await user.click(trigger) - // Verify Card content await waitFor(() => { - expect(screen.getByText(/serviceApi\.card\.title/i)).toBeInTheDocument() - expect(screen.getByText(/serviceApi\.enabled/i)).toBeInTheDocument() + expect(screen.getByText(/serviceApi\.card\.apiKey/i)).toBeInTheDocument() }) - // Open API Key modal + // Click API Key button (wrapped by PopoverClose) const apiKeyButton = screen.getByText(/serviceApi\.card\.apiKey/i).closest('button') if (apiKeyButton) await user.click(apiKeyButton) - // Verify modal appears + // Modal should appear await waitFor(() => { expect(screen.getByTestId('secret-key-modal')).toBeInTheDocument() }) - }) - it('should navigate to API Reference from Card', async () => { - const user = userEvent.setup() - - render() - - // Open popup - const trigger = screen.getByText(/serviceApi\.title/i).closest('[class*="cursor-pointer"]') - if (trigger) - await user.click(trigger) - - // Wait for Card to appear + // Popover should be closed — Card title no longer in document await waitFor(() => { - expect(screen.getByText(/serviceApi\.card\.apiReference/i)).toBeInTheDocument() + expect(screen.queryByText(/serviceApi\.card\.title/i)).not.toBeInTheDocument() }) - - // Verify link - const apiRefLink = screen.getByText(/serviceApi\.card\.apiReference/i).closest('a') - expect(apiRefLink).toHaveAttribute('href', 'https://docs.dify.ai/api-reference/datasets') - }) - - it('should reflect apiBaseUrl status in Indicator color', () => { - // With URL - should be green - const { rerender } = render() - expect(screen.getByText(/serviceApi\.title/i)).toBeInTheDocument() - - // Without URL - should be yellow - rerender() - expect(screen.getByText(/serviceApi\.title/i)).toBeInTheDocument() }) }) diff --git a/web/app/components/datasets/extra-info/service-api/card.tsx b/web/app/components/datasets/extra-info/service-api/card.tsx index 6f024c3f5d..efa5cf7f6a 100644 --- a/web/app/components/datasets/extra-info/service-api/card.tsx +++ b/web/app/components/datasets/extra-info/service-api/card.tsx @@ -1,35 +1,27 @@ import { Button } from '@langgenius/dify-ui/button' +import { PopoverClose } from '@langgenius/dify-ui/popover' import { RiBookOpenLine, RiKey2Line } from '@remixicon/react' import * as React from 'react' -import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import CopyFeedback from '@/app/components/base/copy-feedback' import { ApiAggregate } from '@/app/components/base/icons/src/vender/knowledge' -import SecretKeyModal from '@/app/components/develop/secret-key/secret-key-modal' import Indicator from '@/app/components/header/indicator' import { useDatasetApiAccessUrl } from '@/hooks/use-api-access-url' import Link from '@/next/link' type CardProps = { apiBaseUrl: string + onOpenSecretKeyModal: () => void } const Card = ({ apiBaseUrl, + onOpenSecretKeyModal, }: CardProps) => { const { t } = useTranslation() - const [isSecretKeyModalVisible, setIsSecretKeyModalVisible] = useState(false) const apiReferenceUrl = useDatasetApiAccessUrl() - const handleOpenSecretKeyModal = useCallback(() => { - setIsSecretKeyModalVisible(true) - }, []) - - const handleCloseSecretKeyModal = useCallback(() => { - setIsSecretKeyModalVisible(false) - }, []) - return (
@@ -74,17 +66,21 @@ const Card = ({
{/* Actions */}
- + + + + {t('serviceApi.card.apiKey', { ns: 'dataset' })} + + + )} + />
-
) } diff --git a/web/app/components/datasets/extra-info/service-api/index.tsx b/web/app/components/datasets/extra-info/service-api/index.tsx index c3fe05248d..453715bfe3 100644 --- a/web/app/components/datasets/extra-info/service-api/index.tsx +++ b/web/app/components/datasets/extra-info/service-api/index.tsx @@ -1,8 +1,9 @@ import { cn } from '@langgenius/dify-ui/cn' import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover' import * as React from 'react' -import { useState } from 'react' +import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' +import SecretKeyModal from '@/app/components/develop/secret-key/secret-key-modal' import Indicator from '@/app/components/header/indicator' import Card from './card' @@ -15,6 +16,15 @@ const ServiceApi = ({ }: ServiceApiProps) => { const { t } = useTranslation() const [open, setOpen] = useState(false) + const [isSecretKeyModalVisible, setIsSecretKeyModalVisible] = useState(false) + + const handleOpenSecretKeyModal = useCallback(() => { + setIsSecretKeyModalVisible(true) + }, []) + + const handleCloseSecretKeyModal = useCallback(() => { + setIsSecretKeyModalVisible(false) + }, []) return (
@@ -49,9 +59,14 @@ const ServiceApi = ({ > +
) } diff --git a/web/app/components/datasets/list/__tests__/index.spec.tsx b/web/app/components/datasets/list/__tests__/index.spec.tsx index beee35b06d..adc53debbd 100644 --- a/web/app/components/datasets/list/__tests__/index.spec.tsx +++ b/web/app/components/datasets/list/__tests__/index.spec.tsx @@ -99,6 +99,14 @@ vi.mock('../../external-api/external-api-panel', () => ({ ), })) +// Mock SecretKeyModal — it depends on user profile context and service APIs +// not configured in this test. ServiceApi always mounts the modal (controlled +// by `isShow`) so we provide a lightweight stub. +vi.mock('@/app/components/develop/secret-key/secret-key-modal', () => ({ + default: ({ isShow }: { isShow: boolean }) => + isShow ?
: null, +})) + // Mock TagManagementModal vi.mock('@/app/components/base/tag-management', () => ({ default: () =>
, diff --git a/web/app/components/plugins/install-plugin/install-from-github/steps/__tests__/setURL.spec.tsx b/web/app/components/plugins/install-plugin/install-from-github/steps/__tests__/setURL.spec.tsx index fca64ac096..327e0bddcf 100644 --- a/web/app/components/plugins/install-plugin/install-from-github/steps/__tests__/setURL.spec.tsx +++ b/web/app/components/plugins/install-plugin/install-from-github/steps/__tests__/setURL.spec.tsx @@ -1,4 +1,4 @@ -import { fireEvent, render, screen } from '@testing-library/react' +import { fireEvent, render, screen, waitFor } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest' import SetURL from '../setURL' @@ -53,6 +53,15 @@ describe('SetURL', () => { const input = screen.getByLabelText('plugin.installFromGitHub.gitHubRepo') expect(input).toBeInTheDocument() }) + + it('should auto-focus the input on mount', async () => { + render() + + const input = screen.getByRole('textbox') + await waitFor(() => { + expect(input).toHaveFocus() + }) + }) }) // ================================ diff --git a/web/app/components/plugins/install-plugin/install-from-github/steps/setURL.tsx b/web/app/components/plugins/install-plugin/install-from-github/steps/setURL.tsx index f1b149a8bf..b4f0741eb2 100644 --- a/web/app/components/plugins/install-plugin/install-from-github/steps/setURL.tsx +++ b/web/app/components/plugins/install-plugin/install-from-github/steps/setURL.tsx @@ -13,6 +13,18 @@ type SetURLProps = { const SetURL: React.FC = ({ repoUrl, onChange, onNext, onCancel }) => { const { t } = useTranslation() + const inputRef = React.useRef(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 ( <> ({ useDebugKey: () => mockDebugKey, })) -vi.mock('@langgenius/dify-ui/button', () => ({ - Button: ({ children }: { children: React.ReactNode }) => , -})) - -vi.mock('@/app/components/base/tooltip', () => ({ - default: ({ - children, - disabled, - popupContent, - }: { - children: React.ReactNode - disabled?: boolean - popupContent: React.ReactNode - }) => ( -
- {children} - {!disabled &&
{popupContent}
} -
- ), -})) - vi.mock('../../base/key-value-item', () => ({ default: ({ label, @@ -68,16 +48,31 @@ describe('DebugInfo', () => { expect(container.innerHTML).toBe('') }) - it('renders debug metadata and masks the key when info is available', () => { + it('renders a disabled trigger when debug info is unavailable', () => { + render() + + const trigger = screen.getByRole('button') + expect(trigger).toBeDisabled() + }) + + it('opens a popover with debug metadata and masks the key when info is available', async () => { mockDebugKey.data = { host: '127.0.0.1', port: 5001, key: '12345678abcdefghijklmnopqrst87654321', } + const user = userEvent.setup() render() - expect(screen.getByTestId('debug-button')).toBeInTheDocument() + const trigger = screen.getByRole('button') + expect(trigger).toBeEnabled() + + // Popover is closed initially — content not rendered yet + expect(screen.queryByText('plugin.debugInfo.title')).not.toBeInTheDocument() + + await user.click(trigger) + expect(screen.getByText('plugin.debugInfo.title')).toBeInTheDocument() expect(screen.getByRole('link')).toHaveAttribute( 'href', diff --git a/web/app/components/plugins/plugin-page/debug-info.tsx b/web/app/components/plugins/plugin-page/debug-info.tsx index 4e02fdc2be..9e4404c39c 100644 --- a/web/app/components/plugins/plugin-page/debug-info.tsx +++ b/web/app/components/plugins/plugin-page/debug-info.tsx @@ -1,13 +1,13 @@ 'use client' import type { FC } from 'react' import { Button } from '@langgenius/dify-ui/button' +import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover' import { RiArrowRightUpLine, RiBugLine, } from '@remixicon/react' import * as React from 'react' import { useTranslation } from 'react-i18next' -import Tooltip from '@/app/components/base/tooltip' import { useDocLink } from '@/context/i18n' import { useDebugKey } from '@/service/use-plugins' import KeyValueItem from '../base/key-value-item' @@ -25,41 +25,54 @@ const DebugInfo: FC = () => { if (isLoading) return null - return ( - -
- {t(`${i18nPrefix}.title`, { ns: 'plugin' })} - - {t(`${i18nPrefix}.viewDocs`, { ns: 'plugin' })} - - -
-
- - -
- - )} - popupClassName="flex flex-col items-start w-[256px] px-4 py-3.5 gap-1 border border-components-panel-border - rounded-xl bg-components-tooltip-bg shadows-shadow-lg z-50" - asChild={false} - position="bottom" - > - -
+ ) + } + + return ( + + + + + )} + /> + +
+ + {t(`${i18nPrefix}.title`, { ns: 'plugin' })} + + + {t(`${i18nPrefix}.viewDocs`, { ns: 'plugin' })} + + +
+
+ + +
+
+
) }