diff --git a/web/app/components/plugins/base/__tests__/key-value-item.spec.tsx b/web/app/components/plugins/base/__tests__/key-value-item.spec.tsx index 8575eabf626..e238bcde0b4 100644 --- a/web/app/components/plugins/base/__tests__/key-value-item.spec.tsx +++ b/web/app/components/plugins/base/__tests__/key-value-item.spec.tsx @@ -1,4 +1,5 @@ -import { cleanup, fireEvent, render, screen } from '@testing-library/react' +import { cleanup, render, screen, within } from '@testing-library/react' +import userEvent from '@testing-library/user-event' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import KeyValueItem from '../key-value-item' @@ -34,9 +35,20 @@ describe('KeyValueItem', () => { expect(screen.queryByText('sk-secret')).not.toBeInTheDocument() }) - it('copies actual value (not masked) when copy button is clicked', () => { + it('associates the label with the copy action and announces the result', async () => { + vi.useRealTimers() + const user = userEvent.setup() render() - fireEvent.click(screen.getByRole('button', { name: 'common.operation.copy' })) + + const keyGroup = screen.getByRole('group', { name: 'Key' }) + const copyButton = within(keyGroup).getByRole('button', { + name: 'common.operation.copy: Key', + }) + + await user.click(copyButton) + expect(mockCopy).toHaveBeenCalledWith('sk-secret') + expect(copyButton).toHaveAccessibleName('common.operation.copy: Key') + expect(within(keyGroup).getByRole('status')).toHaveTextContent('common.operation.copied: Key') }) }) diff --git a/web/app/components/plugins/base/key-value-item.tsx b/web/app/components/plugins/base/key-value-item.tsx index 9e288e5e821..eae60f88c77 100644 --- a/web/app/components/plugins/base/key-value-item.tsx +++ b/web/app/components/plugins/base/key-value-item.tsx @@ -3,7 +3,7 @@ import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import copy from 'copy-to-clipboard' -import { useCallback, useEffect, useState } from 'react' +import { useCallback, useEffect, useId, useState } from 'react' import { useTranslation } from 'react-i18next' import { CopyCheck } from '../../base/icons/src/vender/line/files' @@ -24,6 +24,7 @@ function KeyValueItem({ }: Props) { const { t } = useTranslation() const [isCopied, setIsCopied] = useState(false) + const labelId = useId() const handleCopy = useCallback(() => { copy(value) setIsCopied(true) @@ -40,11 +41,16 @@ function KeyValueItem({ } }, [isCopied]) - const copyLabel = t(($) => $[`operation.${isCopied ? 'copied' : 'copy'}`], { ns: 'common' }) + const copiedLabel = t(($) => $['operation.copied'], { ns: 'common' }) + const copyLabel = t(($) => $['operation.copy'], { ns: 'common' }) + const copyButtonLabel = `${copyLabel}: ${label}` + const copyStatus = `${copiedLabel}: ${label}` + const tooltipLabel = isCopied ? copiedLabel : copyLabel return ( -
+
@@ -78,9 +84,12 @@ function KeyValueItem({ } /> - {copyLabel} + {tooltipLabel}
+ + {isCopied ? copyStatus : ''} +
) }