test: add comprehensive tests for develop components including ApiServer, Code, and SecretKey functionalities

This commit is contained in:
CodingOnStar
2026-01-27 16:40:55 +08:00
parent b66bd5f5a8
commit a8ecd540b4
9 changed files with 3575 additions and 0 deletions

View File

@ -0,0 +1,220 @@
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { act } from 'react'
import ApiServer from './ApiServer'
// Mock the secret-key-modal since it involves complex API interactions
vi.mock('@/app/components/develop/secret-key/secret-key-modal', () => ({
default: ({ isShow, onClose }: { isShow: boolean, onClose: () => void }) => (
isShow ? <div data-testid="secret-key-modal"><button onClick={onClose}>Close Modal</button></div> : null
),
}))
describe('ApiServer', () => {
const defaultProps = {
apiBaseUrl: 'https://api.example.com',
}
describe('rendering', () => {
it('should render the API server label', () => {
render(<ApiServer {...defaultProps} />)
expect(screen.getByText('appApi.apiServer')).toBeInTheDocument()
})
it('should render the API base URL', () => {
render(<ApiServer {...defaultProps} />)
expect(screen.getByText('https://api.example.com')).toBeInTheDocument()
})
it('should render the OK status badge', () => {
render(<ApiServer {...defaultProps} />)
expect(screen.getByText('appApi.ok')).toBeInTheDocument()
})
it('should render the API key button', () => {
render(<ApiServer {...defaultProps} />)
expect(screen.getByText('appApi.apiKey')).toBeInTheDocument()
})
it('should render CopyFeedback component', () => {
render(<ApiServer {...defaultProps} />)
// CopyFeedback renders a button for copying
const copyButtons = screen.getAllByRole('button')
expect(copyButtons.length).toBeGreaterThan(0)
})
})
describe('with different API URLs', () => {
it('should render localhost URL', () => {
render(<ApiServer apiBaseUrl="http://localhost:3000/api" />)
expect(screen.getByText('http://localhost:3000/api')).toBeInTheDocument()
})
it('should render production URL', () => {
render(<ApiServer apiBaseUrl="https://api.dify.ai/v1" />)
expect(screen.getByText('https://api.dify.ai/v1')).toBeInTheDocument()
})
it('should render URL with path', () => {
render(<ApiServer apiBaseUrl="https://api.example.com/v1/chat" />)
expect(screen.getByText('https://api.example.com/v1/chat')).toBeInTheDocument()
})
})
describe('with appId prop', () => {
it('should render without appId', () => {
render(<ApiServer apiBaseUrl="https://api.example.com" />)
expect(screen.getByText('https://api.example.com')).toBeInTheDocument()
})
it('should render with appId', () => {
render(<ApiServer apiBaseUrl="https://api.example.com" appId="app-123" />)
expect(screen.getByText('https://api.example.com')).toBeInTheDocument()
})
})
describe('SecretKeyButton interaction', () => {
it('should open modal when API key button is clicked', async () => {
const user = userEvent.setup()
render(<ApiServer {...defaultProps} appId="app-123" />)
const apiKeyButton = screen.getByText('appApi.apiKey')
await act(async () => {
await user.click(apiKeyButton)
})
expect(screen.getByTestId('secret-key-modal')).toBeInTheDocument()
})
it('should close modal when close button is clicked', async () => {
const user = userEvent.setup()
render(<ApiServer {...defaultProps} appId="app-123" />)
// Open modal
const apiKeyButton = screen.getByText('appApi.apiKey')
await act(async () => {
await user.click(apiKeyButton)
})
expect(screen.getByTestId('secret-key-modal')).toBeInTheDocument()
// Close modal
const closeButton = screen.getByText('Close Modal')
await act(async () => {
await user.click(closeButton)
})
expect(screen.queryByTestId('secret-key-modal')).not.toBeInTheDocument()
})
})
describe('styling', () => {
it('should have flex layout with wrap', () => {
const { container } = render(<ApiServer {...defaultProps} />)
const wrapper = container.firstChild as HTMLElement
expect(wrapper.className).toContain('flex')
expect(wrapper.className).toContain('flex-wrap')
})
it('should have items-center alignment', () => {
const { container } = render(<ApiServer {...defaultProps} />)
const wrapper = container.firstChild as HTMLElement
expect(wrapper.className).toContain('items-center')
})
it('should have gap-y-2 for vertical spacing', () => {
const { container } = render(<ApiServer {...defaultProps} />)
const wrapper = container.firstChild as HTMLElement
expect(wrapper.className).toContain('gap-y-2')
})
it('should apply green styling to OK badge', () => {
render(<ApiServer {...defaultProps} />)
const okBadge = screen.getByText('appApi.ok')
expect(okBadge.className).toContain('bg-[#ECFDF3]')
expect(okBadge.className).toContain('text-[#039855]')
})
it('should have border styling on URL container', () => {
render(<ApiServer {...defaultProps} />)
const urlText = screen.getByText('https://api.example.com')
const urlContainer = urlText.closest('div[class*="rounded-lg"]')
expect(urlContainer).toBeInTheDocument()
})
})
describe('API server label', () => {
it('should have correct styling for label', () => {
render(<ApiServer {...defaultProps} />)
const label = screen.getByText('appApi.apiServer')
expect(label.className).toContain('rounded-md')
expect(label.className).toContain('border')
})
it('should have tertiary text color on label', () => {
render(<ApiServer {...defaultProps} />)
const label = screen.getByText('appApi.apiServer')
expect(label.className).toContain('text-text-tertiary')
})
})
describe('URL display', () => {
it('should have truncate class for long URLs', () => {
render(<ApiServer {...defaultProps} />)
const urlText = screen.getByText('https://api.example.com')
expect(urlText.className).toContain('truncate')
})
it('should have font-medium class on URL', () => {
render(<ApiServer {...defaultProps} />)
const urlText = screen.getByText('https://api.example.com')
expect(urlText.className).toContain('font-medium')
})
it('should have secondary text color on URL', () => {
render(<ApiServer {...defaultProps} />)
const urlText = screen.getByText('https://api.example.com')
expect(urlText.className).toContain('text-text-secondary')
})
})
describe('divider', () => {
it('should render vertical divider between URL and copy button', () => {
const { container } = render(<ApiServer {...defaultProps} />)
const divider = container.querySelector('.bg-divider-regular')
expect(divider).toBeInTheDocument()
})
it('should have correct divider dimensions', () => {
const { container } = render(<ApiServer {...defaultProps} />)
const divider = container.querySelector('.bg-divider-regular')
expect(divider?.className).toContain('h-[14px]')
expect(divider?.className).toContain('w-[1px]')
})
})
describe('SecretKeyButton styling', () => {
it('should have shrink-0 class to prevent shrinking', () => {
render(<ApiServer {...defaultProps} appId="app-123" />)
// The SecretKeyButton wraps a Button component
const button = screen.getByRole('button', { name: /apiKey/i })
// Check parent container has shrink-0
const buttonContainer = button.closest('.shrink-0')
expect(buttonContainer).toBeInTheDocument()
})
})
describe('accessibility', () => {
it('should have accessible button for API key', () => {
render(<ApiServer {...defaultProps} />)
const button = screen.getByRole('button', { name: /apiKey/i })
expect(button).toBeInTheDocument()
})
it('should have multiple buttons (copy + API key)', () => {
render(<ApiServer {...defaultProps} />)
const buttons = screen.getAllByRole('button')
expect(buttons.length).toBeGreaterThanOrEqual(2)
})
})
})

View File

@ -0,0 +1,590 @@
import { act, render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { Code, CodeGroup, Embed, Pre } from './code'
// Mock the clipboard utility
vi.mock('@/utils/clipboard', () => ({
writeTextToClipboard: vi.fn().mockResolvedValue(undefined),
}))
describe('code.tsx components', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.useFakeTimers({ shouldAdvanceTime: true })
})
afterEach(() => {
vi.runOnlyPendingTimers()
vi.useRealTimers()
})
describe('Code', () => {
it('should render children', () => {
render(<Code>const x = 1</Code>)
expect(screen.getByText('const x = 1')).toBeInTheDocument()
})
it('should render as code element', () => {
render(<Code>code snippet</Code>)
const codeElement = screen.getByText('code snippet')
expect(codeElement.tagName).toBe('CODE')
})
it('should pass through additional props', () => {
render(<Code data-testid="custom-code" className="custom-class">snippet</Code>)
const codeElement = screen.getByTestId('custom-code')
expect(codeElement).toHaveClass('custom-class')
})
it('should render with complex children', () => {
render(
<Code>
<span>part1</span>
<span>part2</span>
</Code>,
)
expect(screen.getByText('part1')).toBeInTheDocument()
expect(screen.getByText('part2')).toBeInTheDocument()
})
})
describe('Embed', () => {
it('should render value prop', () => {
render(<Embed value="embedded content">ignored children</Embed>)
expect(screen.getByText('embedded content')).toBeInTheDocument()
})
it('should render as span element', () => {
render(<Embed value="test value">children</Embed>)
const span = screen.getByText('test value')
expect(span.tagName).toBe('SPAN')
})
it('should pass through additional props', () => {
render(<Embed value="content" data-testid="embed-test" className="embed-class">children</Embed>)
const embed = screen.getByTestId('embed-test')
expect(embed).toHaveClass('embed-class')
})
it('should not render children, only value', () => {
render(<Embed value="shown">hidden children</Embed>)
expect(screen.getByText('shown')).toBeInTheDocument()
expect(screen.queryByText('hidden children')).not.toBeInTheDocument()
})
})
describe('CodeGroup', () => {
describe('with string targetCode', () => {
it('should render code from targetCode string', () => {
render(
<CodeGroup targetCode="const hello = 'world'">
<pre><code>fallback</code></pre>
</CodeGroup>,
)
expect(screen.getByText('const hello = \'world\'')).toBeInTheDocument()
})
it('should have shadow and rounded styles', () => {
const { container } = render(
<CodeGroup targetCode="code here">
<pre><code>fallback</code></pre>
</CodeGroup>,
)
const codeGroup = container.querySelector('.shadow-md')
expect(codeGroup).toBeInTheDocument()
expect(codeGroup).toHaveClass('rounded-2xl')
})
it('should have bg-zinc-900 background', () => {
const { container } = render(
<CodeGroup targetCode="code">
<pre><code>fallback</code></pre>
</CodeGroup>,
)
const codeGroup = container.querySelector('.bg-zinc-900')
expect(codeGroup).toBeInTheDocument()
})
})
describe('with array targetCode', () => {
it('should render single code example without tabs', () => {
const examples = [{ code: 'single example' }]
render(
<CodeGroup targetCode={examples}>
<pre><code>fallback</code></pre>
</CodeGroup>,
)
expect(screen.getByText('single example')).toBeInTheDocument()
})
it('should render multiple code examples with tabs', () => {
const examples = [
{ title: 'JavaScript', code: 'console.log("js")' },
{ title: 'Python', code: 'print("py")' },
]
render(
<CodeGroup targetCode={examples}>
<pre><code>fallback</code></pre>
</CodeGroup>,
)
expect(screen.getByRole('tab', { name: 'JavaScript' })).toBeInTheDocument()
expect(screen.getByRole('tab', { name: 'Python' })).toBeInTheDocument()
})
it('should show first tab content by default', () => {
const examples = [
{ title: 'Tab1', code: 'first content' },
{ title: 'Tab2', code: 'second content' },
]
render(
<CodeGroup targetCode={examples}>
<pre><code>fallback</code></pre>
</CodeGroup>,
)
expect(screen.getByText('first content')).toBeInTheDocument()
})
it('should switch tabs on click', async () => {
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime })
const examples = [
{ title: 'Tab1', code: 'first content' },
{ title: 'Tab2', code: 'second content' },
]
render(
<CodeGroup targetCode={examples}>
<pre><code>fallback</code></pre>
</CodeGroup>,
)
const tab2 = screen.getByRole('tab', { name: 'Tab2' })
await act(async () => {
await user.click(tab2)
})
await waitFor(() => {
expect(screen.getByText('second content')).toBeInTheDocument()
})
})
it('should use "Code" as default title when title not provided', () => {
const examples = [
{ code: 'example 1' },
{ code: 'example 2' },
]
render(
<CodeGroup targetCode={examples}>
<pre><code>fallback</code></pre>
</CodeGroup>,
)
const codeTabs = screen.getAllByRole('tab', { name: 'Code' })
expect(codeTabs).toHaveLength(2)
})
})
describe('with title prop', () => {
it('should render title in header', () => {
render(
<CodeGroup title="API Example" targetCode="code">
<pre><code>fallback</code></pre>
</CodeGroup>,
)
expect(screen.getByText('API Example')).toBeInTheDocument()
})
it('should render title in h3 element', () => {
render(
<CodeGroup title="Example Title" targetCode="code">
<pre><code>fallback</code></pre>
</CodeGroup>,
)
const h3 = screen.getByRole('heading', { level: 3 })
expect(h3).toHaveTextContent('Example Title')
})
})
describe('with tag and label props', () => {
it('should render tag in code panel header', () => {
render(
<CodeGroup tag="GET" targetCode="code">
<pre><code>fallback</code></pre>
</CodeGroup>,
)
expect(screen.getByText('GET')).toBeInTheDocument()
})
it('should render label in code panel header', () => {
render(
<CodeGroup label="/api/users" targetCode="code">
<pre><code>fallback</code></pre>
</CodeGroup>,
)
expect(screen.getByText('/api/users')).toBeInTheDocument()
})
it('should render both tag and label with separator', () => {
const { container } = render(
<CodeGroup tag="POST" label="/api/create" targetCode="code">
<pre><code>fallback</code></pre>
</CodeGroup>,
)
expect(screen.getByText('POST')).toBeInTheDocument()
expect(screen.getByText('/api/create')).toBeInTheDocument()
// Separator should be present
const separator = container.querySelector('.rounded-full.bg-zinc-500')
expect(separator).toBeInTheDocument()
})
})
describe('CopyButton functionality', () => {
it('should render copy button', () => {
render(
<CodeGroup targetCode="copyable code">
<pre><code>fallback</code></pre>
</CodeGroup>,
)
const copyButton = screen.getByRole('button')
expect(copyButton).toBeInTheDocument()
})
it('should show "Copy" text initially', () => {
render(
<CodeGroup targetCode="code">
<pre><code>fallback</code></pre>
</CodeGroup>,
)
expect(screen.getByText('Copy')).toBeInTheDocument()
})
it('should show "Copied!" after clicking copy button', async () => {
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime })
const { writeTextToClipboard } = await import('@/utils/clipboard')
render(
<CodeGroup targetCode="code to copy">
<pre><code>fallback</code></pre>
</CodeGroup>,
)
const copyButton = screen.getByRole('button')
await act(async () => {
await user.click(copyButton)
})
await waitFor(() => {
expect(writeTextToClipboard).toHaveBeenCalledWith('code to copy')
})
expect(screen.getByText('Copied!')).toBeInTheDocument()
})
it('should reset copy state after timeout', async () => {
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime })
render(
<CodeGroup targetCode="code">
<pre><code>fallback</code></pre>
</CodeGroup>,
)
const copyButton = screen.getByRole('button')
await act(async () => {
await user.click(copyButton)
})
await waitFor(() => {
expect(screen.getByText('Copied!')).toBeInTheDocument()
})
// Advance time past the timeout
await act(async () => {
vi.advanceTimersByTime(1500)
})
await waitFor(() => {
expect(screen.getByText('Copy')).toBeInTheDocument()
})
})
})
describe('without targetCode (using children)', () => {
it('should render children when no targetCode provided', () => {
render(
<CodeGroup>
<pre><code>child code content</code></pre>
</CodeGroup>,
)
expect(screen.getByText('child code content')).toBeInTheDocument()
})
})
describe('styling', () => {
it('should have not-prose class to prevent prose styling', () => {
const { container } = render(
<CodeGroup targetCode="code">
<pre><code>fallback</code></pre>
</CodeGroup>,
)
const codeGroup = container.querySelector('.not-prose')
expect(codeGroup).toBeInTheDocument()
})
it('should have my-6 margin', () => {
const { container } = render(
<CodeGroup targetCode="code">
<pre><code>fallback</code></pre>
</CodeGroup>,
)
const codeGroup = container.querySelector('.my-6')
expect(codeGroup).toBeInTheDocument()
})
it('should have overflow-hidden', () => {
const { container } = render(
<CodeGroup targetCode="code">
<pre><code>fallback</code></pre>
</CodeGroup>,
)
const codeGroup = container.querySelector('.overflow-hidden')
expect(codeGroup).toBeInTheDocument()
})
})
})
describe('Pre', () => {
describe('when outside CodeGroup context', () => {
it('should wrap children in CodeGroup', () => {
const { container } = render(
<Pre>
<pre><code>code content</code></pre>
</Pre>,
)
// Should render within a CodeGroup structure
const codeGroup = container.querySelector('.bg-zinc-900')
expect(codeGroup).toBeInTheDocument()
})
it('should pass props to CodeGroup', () => {
render(
<Pre title="Pre Title">
<pre><code>code</code></pre>
</Pre>,
)
expect(screen.getByText('Pre Title')).toBeInTheDocument()
})
})
describe('when inside CodeGroup context (isGrouped)', () => {
it('should return children directly without wrapping', () => {
render(
<CodeGroup targetCode="outer code">
<Pre>
<code>inner code</code>
</Pre>
</CodeGroup>,
)
// The outer code should be rendered (from targetCode)
expect(screen.getByText('outer code')).toBeInTheDocument()
})
})
})
describe('CodePanelHeader (via CodeGroup)', () => {
it('should not render when neither tag nor label provided', () => {
const { container } = render(
<CodeGroup targetCode="code">
<pre><code>fallback</code></pre>
</CodeGroup>,
)
const headerDivider = container.querySelector('.border-b-white\\/7\\.5')
expect(headerDivider).not.toBeInTheDocument()
})
it('should render when only tag is provided', () => {
render(
<CodeGroup tag="GET" targetCode="code">
<pre><code>fallback</code></pre>
</CodeGroup>,
)
expect(screen.getByText('GET')).toBeInTheDocument()
})
it('should render when only label is provided', () => {
render(
<CodeGroup label="/api/endpoint" targetCode="code">
<pre><code>fallback</code></pre>
</CodeGroup>,
)
expect(screen.getByText('/api/endpoint')).toBeInTheDocument()
})
it('should render label with font-mono styling', () => {
render(
<CodeGroup label="/api/test" targetCode="code">
<pre><code>fallback</code></pre>
</CodeGroup>,
)
const label = screen.getByText('/api/test')
expect(label.className).toContain('font-mono')
expect(label.className).toContain('text-xs')
})
})
describe('CodeGroupHeader (via CodeGroup with multiple tabs)', () => {
it('should render tab list for multiple examples', () => {
const examples = [
{ title: 'cURL', code: 'curl example' },
{ title: 'Node.js', code: 'node example' },
]
render(
<CodeGroup targetCode={examples}>
<pre><code>fallback</code></pre>
</CodeGroup>,
)
expect(screen.getByRole('tablist')).toBeInTheDocument()
})
it('should style active tab differently', () => {
const examples = [
{ title: 'Active', code: 'active code' },
{ title: 'Inactive', code: 'inactive code' },
]
render(
<CodeGroup targetCode={examples}>
<pre><code>fallback</code></pre>
</CodeGroup>,
)
const activeTab = screen.getByRole('tab', { name: 'Active' })
expect(activeTab.className).toContain('border-emerald-500')
expect(activeTab.className).toContain('text-emerald-400')
})
it('should have header background styling', () => {
const examples = [
{ title: 'Tab1', code: 'code1' },
{ title: 'Tab2', code: 'code2' },
]
const { container } = render(
<CodeGroup targetCode={examples}>
<pre><code>fallback</code></pre>
</CodeGroup>,
)
const header = container.querySelector('.bg-zinc-800')
expect(header).toBeInTheDocument()
})
})
describe('CodePanel (via CodeGroup)', () => {
it('should render code in pre element', () => {
render(
<CodeGroup targetCode="pre content">
<pre><code>fallback</code></pre>
</CodeGroup>,
)
const preElement = screen.getByText('pre content').closest('pre')
expect(preElement).toBeInTheDocument()
})
it('should have text-white class on pre', () => {
render(
<CodeGroup targetCode="white text">
<pre><code>fallback</code></pre>
</CodeGroup>,
)
const preElement = screen.getByText('white text').closest('pre')
expect(preElement?.className).toContain('text-white')
})
it('should have text-xs class on pre', () => {
render(
<CodeGroup targetCode="small text">
<pre><code>fallback</code></pre>
</CodeGroup>,
)
const preElement = screen.getByText('small text').closest('pre')
expect(preElement?.className).toContain('text-xs')
})
it('should have overflow-x-auto on pre', () => {
render(
<CodeGroup targetCode="scrollable">
<pre><code>fallback</code></pre>
</CodeGroup>,
)
const preElement = screen.getByText('scrollable').closest('pre')
expect(preElement?.className).toContain('overflow-x-auto')
})
it('should have p-4 padding on pre', () => {
render(
<CodeGroup targetCode="padded">
<pre><code>fallback</code></pre>
</CodeGroup>,
)
const preElement = screen.getByText('padded').closest('pre')
expect(preElement?.className).toContain('p-4')
})
})
describe('ClipboardIcon (via CopyButton in CodeGroup)', () => {
it('should render clipboard icon in copy button', () => {
render(
<CodeGroup targetCode="code">
<pre><code>fallback</code></pre>
</CodeGroup>,
)
const copyButton = screen.getByRole('button')
const svg = copyButton.querySelector('svg')
expect(svg).toBeInTheDocument()
expect(svg).toHaveAttribute('viewBox', '0 0 20 20')
})
})
describe('edge cases', () => {
it('should handle empty string targetCode', () => {
render(
<CodeGroup targetCode="">
<pre><code>fallback</code></pre>
</CodeGroup>,
)
// Should render copy button even with empty code
expect(screen.getByRole('button')).toBeInTheDocument()
})
it('should handle targetCode with special characters', () => {
const specialCode = '<div class="test">&amp;</div>'
render(
<CodeGroup targetCode={specialCode}>
<pre><code>fallback</code></pre>
</CodeGroup>,
)
expect(screen.getByText(specialCode)).toBeInTheDocument()
})
it('should handle multiline targetCode', () => {
const multilineCode = `line1
line2
line3`
render(
<CodeGroup targetCode={multilineCode}>
<pre><code>fallback</code></pre>
</CodeGroup>,
)
// Multiline code should be rendered - use a partial match
expect(screen.getByText(/line1/)).toBeInTheDocument()
expect(screen.getByText(/line2/)).toBeInTheDocument()
expect(screen.getByText(/line3/)).toBeInTheDocument()
})
it('should handle examples with tag property', () => {
const examples = [
{ title: 'Example', tag: 'v1', code: 'versioned code' },
]
render(
<CodeGroup targetCode={examples}>
<pre><code>fallback</code></pre>
</CodeGroup>,
)
expect(screen.getByText('versioned code')).toBeInTheDocument()
})
})
})

View File

@ -0,0 +1,339 @@
import { render, screen } from '@testing-library/react'
import DevelopMain from './index'
// Mock the app store with a factory function to control state
const mockAppDetailValue: { current: unknown } = { current: undefined }
vi.mock('@/app/components/app/store', () => ({
useStore: (selector: (state: unknown) => unknown) => {
const state = { appDetail: mockAppDetailValue.current }
return selector(state)
},
}))
// Mock the Doc component since it has complex dependencies
vi.mock('@/app/components/develop/doc', () => ({
default: ({ appDetail }: { appDetail: { name?: string } | null }) => (
<div data-testid="doc-component">
Doc Component -
{appDetail?.name}
</div>
),
}))
// Mock the ApiServer component
vi.mock('@/app/components/develop/ApiServer', () => ({
default: ({ apiBaseUrl, appId }: { apiBaseUrl: string, appId: string }) => (
<div data-testid="api-server">
API Server -
{apiBaseUrl}
{' '}
-
{appId}
</div>
),
}))
describe('DevelopMain', () => {
beforeEach(() => {
vi.clearAllMocks()
mockAppDetailValue.current = undefined
})
describe('loading state', () => {
it('should show loading when appDetail is undefined', () => {
mockAppDetailValue.current = undefined
render(<DevelopMain appId="app-123" />)
// Loading component renders with role="status"
expect(screen.getByRole('status')).toBeInTheDocument()
})
it('should show loading when appDetail is null', () => {
mockAppDetailValue.current = null
render(<DevelopMain appId="app-123" />)
expect(screen.getByRole('status')).toBeInTheDocument()
})
it('should have centered loading container', () => {
mockAppDetailValue.current = undefined
const { container } = render(<DevelopMain appId="app-123" />)
const loadingContainer = container.querySelector('.flex.h-full.items-center.justify-center')
expect(loadingContainer).toBeInTheDocument()
})
it('should have correct background on loading state', () => {
mockAppDetailValue.current = undefined
const { container } = render(<DevelopMain appId="app-123" />)
const loadingContainer = container.querySelector('.bg-background-default')
expect(loadingContainer).toBeInTheDocument()
})
})
describe('with appDetail loaded', () => {
const mockAppDetail = {
id: 'app-123',
name: 'Test Application',
api_base_url: 'https://api.example.com/v1',
mode: 'chat',
}
beforeEach(() => {
mockAppDetailValue.current = mockAppDetail
})
it('should render ApiServer component', () => {
render(<DevelopMain appId="app-123" />)
expect(screen.getByTestId('api-server')).toBeInTheDocument()
})
it('should pass api_base_url to ApiServer', () => {
render(<DevelopMain appId="app-123" />)
expect(screen.getByTestId('api-server')).toHaveTextContent('https://api.example.com/v1')
})
it('should pass appId to ApiServer', () => {
render(<DevelopMain appId="app-123" />)
expect(screen.getByTestId('api-server')).toHaveTextContent('app-123')
})
it('should render Doc component', () => {
render(<DevelopMain appId="app-123" />)
expect(screen.getByTestId('doc-component')).toBeInTheDocument()
})
it('should pass appDetail to Doc component', () => {
render(<DevelopMain appId="app-123" />)
expect(screen.getByTestId('doc-component')).toHaveTextContent('Test Application')
})
it('should not show loading when appDetail exists', () => {
render(<DevelopMain appId="app-123" />)
expect(screen.queryByRole('status')).not.toBeInTheDocument()
})
})
describe('layout structure', () => {
const mockAppDetail = {
id: 'app-123',
name: 'Test Application',
api_base_url: 'https://api.example.com',
mode: 'chat',
}
beforeEach(() => {
mockAppDetailValue.current = mockAppDetail
})
it('should have flex column layout', () => {
const { container } = render(<DevelopMain appId="app-123" />)
const mainContainer = container.firstChild as HTMLElement
expect(mainContainer.className).toContain('flex')
expect(mainContainer.className).toContain('flex-col')
})
it('should have relative positioning', () => {
const { container } = render(<DevelopMain appId="app-123" />)
const mainContainer = container.firstChild as HTMLElement
expect(mainContainer.className).toContain('relative')
})
it('should have full height', () => {
const { container } = render(<DevelopMain appId="app-123" />)
const mainContainer = container.firstChild as HTMLElement
expect(mainContainer.className).toContain('h-full')
})
it('should have overflow-hidden', () => {
const { container } = render(<DevelopMain appId="app-123" />)
const mainContainer = container.firstChild as HTMLElement
expect(mainContainer.className).toContain('overflow-hidden')
})
})
describe('header section', () => {
const mockAppDetail = {
id: 'app-123',
name: 'Test Application',
api_base_url: 'https://api.example.com',
mode: 'chat',
}
beforeEach(() => {
mockAppDetailValue.current = mockAppDetail
})
it('should have header with border', () => {
const { container } = render(<DevelopMain appId="app-123" />)
const header = container.querySelector('.border-b')
expect(header).toBeInTheDocument()
})
it('should have shrink-0 on header to prevent shrinking', () => {
const { container } = render(<DevelopMain appId="app-123" />)
const header = container.querySelector('.shrink-0')
expect(header).toBeInTheDocument()
})
it('should have horizontal padding on header', () => {
const { container } = render(<DevelopMain appId="app-123" />)
const header = container.querySelector('.px-6')
expect(header).toBeInTheDocument()
})
it('should have vertical padding on header', () => {
const { container } = render(<DevelopMain appId="app-123" />)
const header = container.querySelector('.py-2')
expect(header).toBeInTheDocument()
})
it('should have items centered in header', () => {
const { container } = render(<DevelopMain appId="app-123" />)
const header = container.querySelector('.items-center')
expect(header).toBeInTheDocument()
})
it('should have justify-between in header', () => {
const { container } = render(<DevelopMain appId="app-123" />)
const header = container.querySelector('.justify-between')
expect(header).toBeInTheDocument()
})
})
describe('content section', () => {
const mockAppDetail = {
id: 'app-123',
name: 'Test Application',
api_base_url: 'https://api.example.com',
mode: 'chat',
}
beforeEach(() => {
mockAppDetailValue.current = mockAppDetail
})
it('should have grow class for content area', () => {
const { container } = render(<DevelopMain appId="app-123" />)
const content = container.querySelector('.grow')
expect(content).toBeInTheDocument()
})
it('should have overflow-auto for content scrolling', () => {
const { container } = render(<DevelopMain appId="app-123" />)
const content = container.querySelector('.overflow-auto')
expect(content).toBeInTheDocument()
})
it('should have horizontal padding on content', () => {
const { container } = render(<DevelopMain appId="app-123" />)
const content = container.querySelector('.px-4')
expect(content).toBeInTheDocument()
})
it('should have vertical padding on content', () => {
const { container } = render(<DevelopMain appId="app-123" />)
const content = container.querySelector('.py-4')
expect(content).toBeInTheDocument()
})
it('should have responsive padding', () => {
const { container } = render(<DevelopMain appId="app-123" />)
const content = container.querySelector('[class*="sm:px-10"]')
expect(content).toBeInTheDocument()
})
})
describe('with different appIds', () => {
const mockAppDetail = {
id: 'app-456',
name: 'Another App',
api_base_url: 'https://another-api.com',
mode: 'completion',
}
beforeEach(() => {
mockAppDetailValue.current = mockAppDetail
})
it('should pass different appId to ApiServer', () => {
render(<DevelopMain appId="app-456" />)
expect(screen.getByTestId('api-server')).toHaveTextContent('app-456')
})
it('should handle app with different api_base_url', () => {
render(<DevelopMain appId="app-456" />)
expect(screen.getByTestId('api-server')).toHaveTextContent('https://another-api.com')
})
})
describe('empty state handling', () => {
it('should handle appDetail with minimal properties', () => {
mockAppDetailValue.current = {
api_base_url: 'https://api.test.com',
}
render(<DevelopMain appId="app-minimal" />)
expect(screen.getByTestId('api-server')).toBeInTheDocument()
})
it('should handle appDetail with empty api_base_url', () => {
mockAppDetailValue.current = {
api_base_url: '',
name: 'Empty URL App',
}
render(<DevelopMain appId="app-empty-url" />)
expect(screen.getByTestId('api-server')).toBeInTheDocument()
})
})
describe('title element', () => {
const mockAppDetail = {
id: 'app-123',
name: 'Test Application',
api_base_url: 'https://api.example.com',
mode: 'chat',
}
beforeEach(() => {
mockAppDetailValue.current = mockAppDetail
})
it('should have title div with correct styling', () => {
const { container } = render(<DevelopMain appId="app-123" />)
const title = container.querySelector('.text-lg.font-medium.text-text-primary')
expect(title).toBeInTheDocument()
})
it('should render empty title div', () => {
const { container } = render(<DevelopMain appId="app-123" />)
const title = container.querySelector('.text-lg.font-medium.text-text-primary')
expect(title?.textContent).toBe('')
})
})
describe('border styling', () => {
const mockAppDetail = {
id: 'app-123',
name: 'Test Application',
api_base_url: 'https://api.example.com',
mode: 'chat',
}
beforeEach(() => {
mockAppDetailValue.current = mockAppDetail
})
it('should have solid border style', () => {
const { container } = render(<DevelopMain appId="app-123" />)
const header = container.querySelector('.border-solid')
expect(header).toBeInTheDocument()
})
it('should have divider regular color on border', () => {
const { container } = render(<DevelopMain appId="app-123" />)
const header = container.querySelector('.border-b-divider-regular')
expect(header).toBeInTheDocument()
})
})
})

View File

@ -0,0 +1,655 @@
import { render, screen } from '@testing-library/react'
import { Col, Heading, Properties, Property, PropertyInstruction, Row, SubProperty } from './md'
describe('md.tsx components', () => {
describe('Heading', () => {
const defaultProps = {
url: '/api/messages',
method: 'GET' as const,
title: 'Get Messages',
name: '#get-messages',
}
describe('rendering', () => {
it('should render the method badge', () => {
render(<Heading {...defaultProps} />)
expect(screen.getByText('GET')).toBeInTheDocument()
})
it('should render the url', () => {
render(<Heading {...defaultProps} />)
expect(screen.getByText('/api/messages')).toBeInTheDocument()
})
it('should render the title as a link', () => {
render(<Heading {...defaultProps} />)
const link = screen.getByRole('link', { name: 'Get Messages' })
expect(link).toBeInTheDocument()
expect(link).toHaveAttribute('href', '#get-messages')
})
it('should render an anchor span with correct id', () => {
const { container } = render(<Heading {...defaultProps} />)
const anchor = container.querySelector('#get-messages')
expect(anchor).toBeInTheDocument()
})
it('should strip # prefix from name for id', () => {
const { container } = render(<Heading {...defaultProps} name="#with-hash" />)
const anchor = container.querySelector('#with-hash')
expect(anchor).toBeInTheDocument()
})
})
describe('method styling', () => {
it('should apply emerald styles for GET method', () => {
render(<Heading {...defaultProps} method="GET" />)
const badge = screen.getByText('GET')
expect(badge.className).toContain('text-emerald')
expect(badge.className).toContain('bg-emerald-400/10')
expect(badge.className).toContain('ring-emerald-300')
})
it('should apply sky styles for POST method', () => {
render(<Heading {...defaultProps} method="POST" />)
const badge = screen.getByText('POST')
expect(badge.className).toContain('text-sky')
expect(badge.className).toContain('bg-sky-400/10')
expect(badge.className).toContain('ring-sky-300')
})
it('should apply amber styles for PUT method', () => {
render(<Heading {...defaultProps} method="PUT" />)
const badge = screen.getByText('PUT')
expect(badge.className).toContain('text-amber')
expect(badge.className).toContain('bg-amber-400/10')
expect(badge.className).toContain('ring-amber-300')
})
it('should apply rose styles for DELETE method', () => {
render(<Heading {...defaultProps} method="DELETE" />)
const badge = screen.getByText('DELETE')
expect(badge.className).toContain('text-red')
expect(badge.className).toContain('bg-rose')
expect(badge.className).toContain('ring-rose')
})
it('should apply violet styles for PATCH method', () => {
render(<Heading {...defaultProps} method="PATCH" />)
const badge = screen.getByText('PATCH')
expect(badge.className).toContain('text-violet')
expect(badge.className).toContain('bg-violet-400/10')
expect(badge.className).toContain('ring-violet-300')
})
})
describe('badge base styles', () => {
it('should have rounded-lg class', () => {
render(<Heading {...defaultProps} />)
const badge = screen.getByText('GET')
expect(badge.className).toContain('rounded-lg')
})
it('should have font-mono class', () => {
render(<Heading {...defaultProps} />)
const badge = screen.getByText('GET')
expect(badge.className).toContain('font-mono')
})
it('should have font-semibold class', () => {
render(<Heading {...defaultProps} />)
const badge = screen.getByText('GET')
expect(badge.className).toContain('font-semibold')
})
it('should have ring-1 and ring-inset classes', () => {
render(<Heading {...defaultProps} />)
const badge = screen.getByText('GET')
expect(badge.className).toContain('ring-1')
expect(badge.className).toContain('ring-inset')
})
})
describe('url styles', () => {
it('should have font-mono class on url', () => {
render(<Heading {...defaultProps} />)
const url = screen.getByText('/api/messages')
expect(url.className).toContain('font-mono')
})
it('should have text-xs class on url', () => {
render(<Heading {...defaultProps} />)
const url = screen.getByText('/api/messages')
expect(url.className).toContain('text-xs')
})
it('should have zinc text color on url', () => {
render(<Heading {...defaultProps} />)
const url = screen.getByText('/api/messages')
expect(url.className).toContain('text-zinc-400')
})
})
describe('h2 element', () => {
it('should render title inside h2', () => {
render(<Heading {...defaultProps} />)
const h2 = screen.getByRole('heading', { level: 2 })
expect(h2).toBeInTheDocument()
expect(h2).toHaveTextContent('Get Messages')
})
it('should have scroll-mt-32 class on h2', () => {
render(<Heading {...defaultProps} />)
const h2 = screen.getByRole('heading', { level: 2 })
expect(h2.className).toContain('scroll-mt-32')
})
})
})
describe('Row', () => {
it('should render children', () => {
render(
<Row anchor={false}>
<div>Child 1</div>
<div>Child 2</div>
</Row>,
)
expect(screen.getByText('Child 1')).toBeInTheDocument()
expect(screen.getByText('Child 2')).toBeInTheDocument()
})
it('should have grid layout', () => {
const { container } = render(
<Row anchor={false}>
<div>Content</div>
</Row>,
)
const row = container.firstChild as HTMLElement
expect(row.className).toContain('grid')
expect(row.className).toContain('grid-cols-1')
})
it('should have gap classes', () => {
const { container } = render(
<Row anchor={false}>
<div>Content</div>
</Row>,
)
const row = container.firstChild as HTMLElement
expect(row.className).toContain('gap-x-16')
expect(row.className).toContain('gap-y-10')
})
it('should have xl responsive classes', () => {
const { container } = render(
<Row anchor={false}>
<div>Content</div>
</Row>,
)
const row = container.firstChild as HTMLElement
expect(row.className).toContain('xl:grid-cols-2')
expect(row.className).toContain('xl:!max-w-none')
})
it('should have items-start class', () => {
const { container } = render(
<Row anchor={false}>
<div>Content</div>
</Row>,
)
const row = container.firstChild as HTMLElement
expect(row.className).toContain('items-start')
})
})
describe('Col', () => {
it('should render children', () => {
render(
<Col anchor={false} sticky={false}>
<div>Column Content</div>
</Col>,
)
expect(screen.getByText('Column Content')).toBeInTheDocument()
})
it('should have first/last child margin classes', () => {
const { container } = render(
<Col anchor={false} sticky={false}>
<div>Content</div>
</Col>,
)
const col = container.firstChild as HTMLElement
expect(col.className).toContain('[&>:first-child]:mt-0')
expect(col.className).toContain('[&>:last-child]:mb-0')
})
it('should apply sticky classes when sticky is true', () => {
const { container } = render(
<Col anchor={false} sticky={true}>
<div>Sticky Content</div>
</Col>,
)
const col = container.firstChild as HTMLElement
expect(col.className).toContain('xl:sticky')
expect(col.className).toContain('xl:top-24')
})
it('should not apply sticky classes when sticky is false', () => {
const { container } = render(
<Col anchor={false} sticky={false}>
<div>Non-sticky Content</div>
</Col>,
)
const col = container.firstChild as HTMLElement
expect(col.className).not.toContain('xl:sticky')
expect(col.className).not.toContain('xl:top-24')
})
})
describe('Properties', () => {
it('should render children', () => {
render(
<Properties anchor={false}>
<li>Property 1</li>
<li>Property 2</li>
</Properties>,
)
expect(screen.getByText('Property 1')).toBeInTheDocument()
expect(screen.getByText('Property 2')).toBeInTheDocument()
})
it('should render as ul with role list', () => {
render(
<Properties anchor={false}>
<li>Property</li>
</Properties>,
)
const list = screen.getByRole('list')
expect(list).toBeInTheDocument()
expect(list.tagName).toBe('UL')
})
it('should have my-6 margin class', () => {
const { container } = render(
<Properties anchor={false}>
<li>Property</li>
</Properties>,
)
const wrapper = container.firstChild as HTMLElement
expect(wrapper.className).toContain('my-6')
})
it('should have list-none class on ul', () => {
render(
<Properties anchor={false}>
<li>Property</li>
</Properties>,
)
const list = screen.getByRole('list')
expect(list.className).toContain('list-none')
})
it('should have m-0 and p-0 classes on ul', () => {
render(
<Properties anchor={false}>
<li>Property</li>
</Properties>,
)
const list = screen.getByRole('list')
expect(list.className).toContain('m-0')
expect(list.className).toContain('p-0')
})
it('should have divide-y class on ul', () => {
render(
<Properties anchor={false}>
<li>Property</li>
</Properties>,
)
const list = screen.getByRole('list')
expect(list.className).toContain('divide-y')
})
it('should have max-w constraint class', () => {
render(
<Properties anchor={false}>
<li>Property</li>
</Properties>,
)
const list = screen.getByRole('list')
expect(list.className).toContain('max-w-[calc(theme(maxWidth.lg)-theme(spacing.8))]')
})
})
describe('Property', () => {
const defaultProps = {
name: 'user_id',
type: 'string',
anchor: false,
}
it('should render name in code element', () => {
render(
<Property {...defaultProps}>
User identifier
</Property>,
)
const code = screen.getByText('user_id')
expect(code.tagName).toBe('CODE')
})
it('should render type', () => {
render(
<Property {...defaultProps}>
User identifier
</Property>,
)
expect(screen.getByText('string')).toBeInTheDocument()
})
it('should render children as description', () => {
render(
<Property {...defaultProps}>
User identifier
</Property>,
)
expect(screen.getByText('User identifier')).toBeInTheDocument()
})
it('should render as li element', () => {
const { container } = render(
<Property {...defaultProps}>
Description
</Property>,
)
expect(container.querySelector('li')).toBeInTheDocument()
})
it('should have m-0 class on li', () => {
const { container } = render(
<Property {...defaultProps}>
Description
</Property>,
)
const li = container.querySelector('li')!
expect(li.className).toContain('m-0')
})
it('should have padding classes on li', () => {
const { container } = render(
<Property {...defaultProps}>
Description
</Property>,
)
const li = container.querySelector('li')!
expect(li.className).toContain('px-0')
expect(li.className).toContain('py-4')
})
it('should have first:pt-0 and last:pb-0 classes', () => {
const { container } = render(
<Property {...defaultProps}>
Description
</Property>,
)
const li = container.querySelector('li')!
expect(li.className).toContain('first:pt-0')
expect(li.className).toContain('last:pb-0')
})
it('should render dl element with proper structure', () => {
const { container } = render(
<Property {...defaultProps}>
Description
</Property>,
)
expect(container.querySelector('dl')).toBeInTheDocument()
})
it('should have sr-only dt elements for accessibility', () => {
const { container } = render(
<Property {...defaultProps}>
User identifier
</Property>,
)
const dtElements = container.querySelectorAll('dt')
expect(dtElements.length).toBe(3)
dtElements.forEach((dt) => {
expect(dt.className).toContain('sr-only')
})
})
it('should have font-mono class on type', () => {
render(
<Property {...defaultProps}>
Description
</Property>,
)
const typeElement = screen.getByText('string')
expect(typeElement.className).toContain('font-mono')
expect(typeElement.className).toContain('text-xs')
})
})
describe('SubProperty', () => {
const defaultProps = {
name: 'sub_field',
type: 'number',
anchor: false,
}
it('should render name in code element', () => {
render(
<SubProperty {...defaultProps}>
Sub field description
</SubProperty>,
)
const code = screen.getByText('sub_field')
expect(code.tagName).toBe('CODE')
})
it('should render type', () => {
render(
<SubProperty {...defaultProps}>
Sub field description
</SubProperty>,
)
expect(screen.getByText('number')).toBeInTheDocument()
})
it('should render children as description', () => {
render(
<SubProperty {...defaultProps}>
Sub field description
</SubProperty>,
)
expect(screen.getByText('Sub field description')).toBeInTheDocument()
})
it('should render as li element', () => {
const { container } = render(
<SubProperty {...defaultProps}>
Description
</SubProperty>,
)
expect(container.querySelector('li')).toBeInTheDocument()
})
it('should have m-0 class on li', () => {
const { container } = render(
<SubProperty {...defaultProps}>
Description
</SubProperty>,
)
const li = container.querySelector('li')!
expect(li.className).toContain('m-0')
})
it('should have different padding than Property (py-1 vs py-4)', () => {
const { container } = render(
<SubProperty {...defaultProps}>
Description
</SubProperty>,
)
const li = container.querySelector('li')!
expect(li.className).toContain('px-0')
expect(li.className).toContain('py-1')
})
it('should have last:pb-0 class', () => {
const { container } = render(
<SubProperty {...defaultProps}>
Description
</SubProperty>,
)
const li = container.querySelector('li')!
expect(li.className).toContain('last:pb-0')
})
it('should render dl element with proper structure', () => {
const { container } = render(
<SubProperty {...defaultProps}>
Description
</SubProperty>,
)
expect(container.querySelector('dl')).toBeInTheDocument()
})
it('should have sr-only dt elements for accessibility', () => {
const { container } = render(
<SubProperty {...defaultProps}>
Sub field description
</SubProperty>,
)
const dtElements = container.querySelectorAll('dt')
expect(dtElements.length).toBe(3)
dtElements.forEach((dt) => {
expect(dt.className).toContain('sr-only')
})
})
it('should have font-mono and text-xs on type', () => {
render(
<SubProperty {...defaultProps}>
Description
</SubProperty>,
)
const typeElement = screen.getByText('number')
expect(typeElement.className).toContain('font-mono')
expect(typeElement.className).toContain('text-xs')
})
})
describe('PropertyInstruction', () => {
it('should render children', () => {
render(
<PropertyInstruction>
This is an instruction
</PropertyInstruction>,
)
expect(screen.getByText('This is an instruction')).toBeInTheDocument()
})
it('should render as li element', () => {
const { container } = render(
<PropertyInstruction>
Instruction text
</PropertyInstruction>,
)
expect(container.querySelector('li')).toBeInTheDocument()
})
it('should have m-0 class', () => {
const { container } = render(
<PropertyInstruction>
Instruction
</PropertyInstruction>,
)
const li = container.querySelector('li')!
expect(li.className).toContain('m-0')
})
it('should have padding classes', () => {
const { container } = render(
<PropertyInstruction>
Instruction
</PropertyInstruction>,
)
const li = container.querySelector('li')!
expect(li.className).toContain('px-0')
expect(li.className).toContain('py-4')
})
it('should have italic class', () => {
const { container } = render(
<PropertyInstruction>
Instruction
</PropertyInstruction>,
)
const li = container.querySelector('li')!
expect(li.className).toContain('italic')
})
it('should have first:pt-0 class', () => {
const { container } = render(
<PropertyInstruction>
Instruction
</PropertyInstruction>,
)
const li = container.querySelector('li')!
expect(li.className).toContain('first:pt-0')
})
})
describe('integration tests', () => {
it('should render Property inside Properties', () => {
render(
<Properties anchor={false}>
<Property name="id" type="string" anchor={false}>
Unique identifier
</Property>
<Property name="name" type="string" anchor={false}>
Display name
</Property>
</Properties>,
)
expect(screen.getByText('id')).toBeInTheDocument()
expect(screen.getByText('name')).toBeInTheDocument()
expect(screen.getByText('Unique identifier')).toBeInTheDocument()
expect(screen.getByText('Display name')).toBeInTheDocument()
})
it('should render Col inside Row', () => {
render(
<Row anchor={false}>
<Col anchor={false} sticky={false}>
<div>Left column</div>
</Col>
<Col anchor={false} sticky={true}>
<div>Right column</div>
</Col>
</Row>,
)
expect(screen.getByText('Left column')).toBeInTheDocument()
expect(screen.getByText('Right column')).toBeInTheDocument()
})
it('should render PropertyInstruction inside Properties', () => {
render(
<Properties anchor={false}>
<PropertyInstruction>
Note: All fields are required
</PropertyInstruction>
<Property name="required_field" type="string" anchor={false}>
A required field
</Property>
</Properties>,
)
expect(screen.getByText('Note: All fields are required')).toBeInTheDocument()
expect(screen.getByText('required_field')).toBeInTheDocument()
})
})
})

View File

@ -0,0 +1,314 @@
import { act, render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import copy from 'copy-to-clipboard'
import InputCopy from './input-copy'
// Mock copy-to-clipboard
vi.mock('copy-to-clipboard', () => ({
default: vi.fn().mockReturnValue(true),
}))
describe('InputCopy', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.useFakeTimers({ shouldAdvanceTime: true })
})
afterEach(() => {
vi.runOnlyPendingTimers()
vi.useRealTimers()
})
describe('rendering', () => {
it('should render the value', () => {
render(<InputCopy value="test-api-key-12345" />)
expect(screen.getByText('test-api-key-12345')).toBeInTheDocument()
})
it('should render with empty value by default', () => {
render(<InputCopy />)
// Empty string should be rendered
expect(screen.getByRole('button')).toBeInTheDocument()
})
it('should render children when provided', () => {
render(
<InputCopy value="key">
<span data-testid="custom-child">Custom Content</span>
</InputCopy>,
)
expect(screen.getByTestId('custom-child')).toBeInTheDocument()
})
it('should render CopyFeedback component', () => {
render(<InputCopy value="test" />)
// CopyFeedback should render a button
const buttons = screen.getAllByRole('button')
expect(buttons.length).toBeGreaterThan(0)
})
})
describe('styling', () => {
it('should apply custom className', () => {
const { container } = render(<InputCopy value="test" className="custom-class" />)
const wrapper = container.firstChild as HTMLElement
expect(wrapper.className).toContain('custom-class')
})
it('should have flex layout', () => {
const { container } = render(<InputCopy value="test" />)
const wrapper = container.firstChild as HTMLElement
expect(wrapper.className).toContain('flex')
})
it('should have items-center alignment', () => {
const { container } = render(<InputCopy value="test" />)
const wrapper = container.firstChild as HTMLElement
expect(wrapper.className).toContain('items-center')
})
it('should have rounded-lg class', () => {
const { container } = render(<InputCopy value="test" />)
const wrapper = container.firstChild as HTMLElement
expect(wrapper.className).toContain('rounded-lg')
})
it('should have background class', () => {
const { container } = render(<InputCopy value="test" />)
const wrapper = container.firstChild as HTMLElement
expect(wrapper.className).toContain('bg-components-input-bg-normal')
})
it('should have hover state', () => {
const { container } = render(<InputCopy value="test" />)
const wrapper = container.firstChild as HTMLElement
expect(wrapper.className).toContain('hover:bg-state-base-hover')
})
it('should have py-2 padding', () => {
const { container } = render(<InputCopy value="test" />)
const wrapper = container.firstChild as HTMLElement
expect(wrapper.className).toContain('py-2')
})
})
describe('copy functionality', () => {
it('should copy value when clicked', async () => {
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime })
render(<InputCopy value="copy-this-value" />)
const copyableArea = screen.getByText('copy-this-value')
await act(async () => {
await user.click(copyableArea)
})
expect(copy).toHaveBeenCalledWith('copy-this-value')
})
it('should update copied state after clicking', async () => {
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime })
render(<InputCopy value="test-value" />)
const copyableArea = screen.getByText('test-value')
await act(async () => {
await user.click(copyableArea)
})
// Copy function should have been called
expect(copy).toHaveBeenCalledWith('test-value')
})
it('should reset copied state after timeout', async () => {
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime })
render(<InputCopy value="test-value" />)
const copyableArea = screen.getByText('test-value')
await act(async () => {
await user.click(copyableArea)
})
expect(copy).toHaveBeenCalledWith('test-value')
// Advance time to reset the copied state
await act(async () => {
vi.advanceTimersByTime(1500)
})
// Component should still be functional
expect(screen.getByText('test-value')).toBeInTheDocument()
})
it('should render tooltip on value', () => {
render(<InputCopy value="test-value" />)
// Value should be wrapped in tooltip (tooltip shows on hover, not as visible text)
const valueText = screen.getByText('test-value')
expect(valueText).toBeInTheDocument()
})
})
describe('tooltip', () => {
it('should render tooltip wrapper', () => {
render(<InputCopy value="test" />)
const valueText = screen.getByText('test')
expect(valueText).toBeInTheDocument()
})
it('should have cursor-pointer on clickable area', () => {
render(<InputCopy value="test" />)
const valueText = screen.getByText('test')
const clickableArea = valueText.closest('div[class*="cursor-pointer"]')
expect(clickableArea).toBeInTheDocument()
})
})
describe('divider', () => {
it('should render vertical divider', () => {
const { container } = render(<InputCopy value="test" />)
const divider = container.querySelector('.bg-divider-regular')
expect(divider).toBeInTheDocument()
})
it('should have correct divider dimensions', () => {
const { container } = render(<InputCopy value="test" />)
const divider = container.querySelector('.bg-divider-regular')
expect(divider?.className).toContain('h-4')
expect(divider?.className).toContain('w-px')
})
it('should have shrink-0 on divider', () => {
const { container } = render(<InputCopy value="test" />)
const divider = container.querySelector('.bg-divider-regular')
expect(divider?.className).toContain('shrink-0')
})
})
describe('value display', () => {
it('should have truncate class for long values', () => {
render(<InputCopy value="very-long-api-key-value-that-might-overflow" />)
const valueText = screen.getByText('very-long-api-key-value-that-might-overflow')
const container = valueText.closest('div[class*="truncate"]')
expect(container).toBeInTheDocument()
})
it('should have text-secondary color on value', () => {
render(<InputCopy value="test-value" />)
const valueText = screen.getByText('test-value')
expect(valueText.className).toContain('text-text-secondary')
})
it('should have absolute positioning for overlay', () => {
render(<InputCopy value="test" />)
const valueText = screen.getByText('test')
const container = valueText.closest('div[class*="absolute"]')
expect(container).toBeInTheDocument()
})
})
describe('inner container', () => {
it('should have grow class on inner container', () => {
const { container } = render(<InputCopy value="test" />)
const innerContainer = container.querySelector('.grow')
expect(innerContainer).toBeInTheDocument()
})
it('should have h-5 height on inner container', () => {
const { container } = render(<InputCopy value="test" />)
const innerContainer = container.querySelector('.h-5')
expect(innerContainer).toBeInTheDocument()
})
})
describe('with children', () => {
it('should render children before value', () => {
const { container } = render(
<InputCopy value="key">
<span data-testid="prefix">Prefix:</span>
</InputCopy>,
)
const children = container.querySelector('[data-testid="prefix"]')
expect(children).toBeInTheDocument()
})
it('should render both children and value', () => {
render(
<InputCopy value="api-key">
<span>Label:</span>
</InputCopy>,
)
expect(screen.getByText('Label:')).toBeInTheDocument()
expect(screen.getByText('api-key')).toBeInTheDocument()
})
})
describe('CopyFeedback section', () => {
it('should have margin on CopyFeedback container', () => {
const { container } = render(<InputCopy value="test" />)
const copyFeedbackContainer = container.querySelector('.mx-1')
expect(copyFeedbackContainer).toBeInTheDocument()
})
})
describe('relative container', () => {
it('should have relative positioning on value container', () => {
const { container } = render(<InputCopy value="test" />)
const relativeContainer = container.querySelector('.relative')
expect(relativeContainer).toBeInTheDocument()
})
it('should have grow on value container', () => {
const { container } = render(<InputCopy value="test" />)
// Find the relative container that also has grow
const valueContainer = container.querySelector('.relative.grow')
expect(valueContainer).toBeInTheDocument()
})
it('should have full height on value container', () => {
const { container } = render(<InputCopy value="test" />)
const valueContainer = container.querySelector('.relative.h-full')
expect(valueContainer).toBeInTheDocument()
})
})
describe('edge cases', () => {
it('should handle undefined value', () => {
render(<InputCopy value={undefined} />)
// Should not crash
expect(screen.getByRole('button')).toBeInTheDocument()
})
it('should handle empty string value', () => {
render(<InputCopy value="" />)
expect(screen.getByRole('button')).toBeInTheDocument()
})
it('should handle very long values', () => {
const longValue = 'a'.repeat(500)
render(<InputCopy value={longValue} />)
expect(screen.getByText(longValue)).toBeInTheDocument()
})
it('should handle special characters in value', () => {
const specialValue = 'key-with-special-chars!@#$%^&*()'
render(<InputCopy value={specialValue} />)
expect(screen.getByText(specialValue)).toBeInTheDocument()
})
})
describe('multiple clicks', () => {
it('should handle multiple rapid clicks', async () => {
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime })
render(<InputCopy value="test" />)
const copyableArea = screen.getByText('test')
// Click multiple times rapidly
await act(async () => {
await user.click(copyableArea)
await user.click(copyableArea)
await user.click(copyableArea)
})
expect(copy).toHaveBeenCalledTimes(3)
})
})
})

View File

@ -0,0 +1,300 @@
import { act, render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import SecretKeyButton from './secret-key-button'
// Mock the SecretKeyModal since it has complex dependencies
vi.mock('@/app/components/develop/secret-key/secret-key-modal', () => ({
default: ({ isShow, onClose, appId }: { isShow: boolean, onClose: () => void, appId?: string }) => (
isShow
? (
<div data-testid="secret-key-modal">
<span>
Modal for
{appId || 'no-app'}
</span>
<button onClick={onClose} data-testid="close-modal">Close</button>
</div>
)
: null
),
}))
describe('SecretKeyButton', () => {
describe('rendering', () => {
it('should render the button', () => {
render(<SecretKeyButton />)
expect(screen.getByRole('button')).toBeInTheDocument()
})
it('should render the API key text', () => {
render(<SecretKeyButton />)
expect(screen.getByText('appApi.apiKey')).toBeInTheDocument()
})
it('should render the key icon', () => {
const { container } = render(<SecretKeyButton />)
// RiKey2Line icon should be rendered as an svg
const svg = container.querySelector('svg')
expect(svg).toBeInTheDocument()
})
it('should not show modal initially', () => {
render(<SecretKeyButton />)
expect(screen.queryByTestId('secret-key-modal')).not.toBeInTheDocument()
})
})
describe('button interaction', () => {
it('should open modal when button is clicked', async () => {
const user = userEvent.setup()
render(<SecretKeyButton />)
const button = screen.getByRole('button')
await act(async () => {
await user.click(button)
})
expect(screen.getByTestId('secret-key-modal')).toBeInTheDocument()
})
it('should close modal when onClose is called', async () => {
const user = userEvent.setup()
render(<SecretKeyButton />)
// Open modal
const button = screen.getByRole('button')
await act(async () => {
await user.click(button)
})
expect(screen.getByTestId('secret-key-modal')).toBeInTheDocument()
// Close modal
const closeButton = screen.getByTestId('close-modal')
await act(async () => {
await user.click(closeButton)
})
expect(screen.queryByTestId('secret-key-modal')).not.toBeInTheDocument()
})
it('should toggle modal visibility', async () => {
const user = userEvent.setup()
render(<SecretKeyButton />)
const button = screen.getByRole('button')
// Open
await act(async () => {
await user.click(button)
})
expect(screen.getByTestId('secret-key-modal')).toBeInTheDocument()
// Close
const closeButton = screen.getByTestId('close-modal')
await act(async () => {
await user.click(closeButton)
})
expect(screen.queryByTestId('secret-key-modal')).not.toBeInTheDocument()
// Open again
await act(async () => {
await user.click(button)
})
expect(screen.getByTestId('secret-key-modal')).toBeInTheDocument()
})
})
describe('props', () => {
it('should apply custom className', () => {
render(<SecretKeyButton className="custom-class" />)
const button = screen.getByRole('button')
expect(button.className).toContain('custom-class')
})
it('should pass appId to modal', async () => {
const user = userEvent.setup()
render(<SecretKeyButton appId="app-123" />)
const button = screen.getByRole('button')
await act(async () => {
await user.click(button)
})
expect(screen.getByText('Modal for app-123')).toBeInTheDocument()
})
it('should handle undefined appId', async () => {
const user = userEvent.setup()
render(<SecretKeyButton />)
const button = screen.getByRole('button')
await act(async () => {
await user.click(button)
})
expect(screen.getByText('Modal for no-app')).toBeInTheDocument()
})
it('should apply custom textCls', () => {
render(<SecretKeyButton textCls="custom-text-class" />)
const text = screen.getByText('appApi.apiKey')
expect(text.className).toContain('custom-text-class')
})
})
describe('button styling', () => {
it('should have px-3 padding', () => {
render(<SecretKeyButton />)
const button = screen.getByRole('button')
expect(button.className).toContain('px-3')
})
it('should have small size', () => {
render(<SecretKeyButton />)
const button = screen.getByRole('button')
expect(button.className).toContain('btn-small')
})
it('should have ghost variant', () => {
render(<SecretKeyButton />)
const button = screen.getByRole('button')
expect(button.className).toContain('btn-ghost')
})
})
describe('icon styling', () => {
it('should have icon container with flex layout', () => {
const { container } = render(<SecretKeyButton />)
const iconContainer = container.querySelector('.flex.items-center.justify-center')
expect(iconContainer).toBeInTheDocument()
})
it('should have correct icon dimensions', () => {
const { container } = render(<SecretKeyButton />)
const iconContainer = container.querySelector('.h-3\\.5.w-3\\.5')
expect(iconContainer).toBeInTheDocument()
})
it('should have tertiary text color on icon', () => {
const { container } = render(<SecretKeyButton />)
const icon = container.querySelector('.text-text-tertiary')
expect(icon).toBeInTheDocument()
})
})
describe('text styling', () => {
it('should have system-xs-medium class', () => {
render(<SecretKeyButton />)
const text = screen.getByText('appApi.apiKey')
expect(text.className).toContain('system-xs-medium')
})
it('should have horizontal padding', () => {
render(<SecretKeyButton />)
const text = screen.getByText('appApi.apiKey')
expect(text.className).toContain('px-[3px]')
})
it('should have tertiary text color', () => {
render(<SecretKeyButton />)
const text = screen.getByText('appApi.apiKey')
expect(text.className).toContain('text-text-tertiary')
})
})
describe('modal props', () => {
it('should pass isShow prop to modal', async () => {
const user = userEvent.setup()
render(<SecretKeyButton />)
// Initially modal should not be visible
expect(screen.queryByTestId('secret-key-modal')).not.toBeInTheDocument()
const button = screen.getByRole('button')
await act(async () => {
await user.click(button)
})
// Now modal should be visible
expect(screen.getByTestId('secret-key-modal')).toBeInTheDocument()
})
it('should pass onClose callback to modal', async () => {
const user = userEvent.setup()
render(<SecretKeyButton />)
const button = screen.getByRole('button')
await act(async () => {
await user.click(button)
})
const closeButton = screen.getByTestId('close-modal')
await act(async () => {
await user.click(closeButton)
})
// Modal should be closed after clicking close
expect(screen.queryByTestId('secret-key-modal')).not.toBeInTheDocument()
})
})
describe('accessibility', () => {
it('should have accessible button', () => {
render(<SecretKeyButton />)
const button = screen.getByRole('button')
expect(button).toBeInTheDocument()
})
it('should be keyboard accessible', async () => {
const user = userEvent.setup()
render(<SecretKeyButton />)
const button = screen.getByRole('button')
button.focus()
expect(document.activeElement).toBe(button)
// Press Enter to activate
await act(async () => {
await user.keyboard('{Enter}')
})
expect(screen.getByTestId('secret-key-modal')).toBeInTheDocument()
})
})
describe('multiple instances', () => {
it('should work independently when multiple instances exist', async () => {
const user = userEvent.setup()
render(
<>
<SecretKeyButton appId="app-1" />
<SecretKeyButton appId="app-2" />
</>,
)
const buttons = screen.getAllByRole('button')
expect(buttons).toHaveLength(2)
// Click first button
await act(async () => {
await user.click(buttons[0])
})
expect(screen.getByText('Modal for app-1')).toBeInTheDocument()
// Close first modal
const closeButton = screen.getByTestId('close-modal')
await act(async () => {
await user.click(closeButton)
})
// Click second button
await act(async () => {
await user.click(buttons[1])
})
expect(screen.getByText('Modal for app-2')).toBeInTheDocument()
})
})
})

View File

@ -0,0 +1,301 @@
import type { CreateApiKeyResponse } from '@/models/app'
import { act, render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import SecretKeyGenerateModal from './secret-key-generate'
// Helper to create a valid CreateApiKeyResponse
const createMockApiKey = (token: string): CreateApiKeyResponse => ({
id: 'mock-id',
token,
created_at: '2024-01-01T00:00:00Z',
})
describe('SecretKeyGenerateModal', () => {
const defaultProps = {
isShow: true,
onClose: vi.fn(),
}
beforeEach(() => {
vi.clearAllMocks()
})
describe('rendering when shown', () => {
it('should render the modal when isShow is true', () => {
render(<SecretKeyGenerateModal {...defaultProps} />)
expect(screen.getByText('appApi.apiKeyModal.apiSecretKey')).toBeInTheDocument()
})
it('should render the generate tips text', () => {
render(<SecretKeyGenerateModal {...defaultProps} />)
expect(screen.getByText('appApi.apiKeyModal.generateTips')).toBeInTheDocument()
})
it('should render the OK button', () => {
render(<SecretKeyGenerateModal {...defaultProps} />)
expect(screen.getByText('appApi.actionMsg.ok')).toBeInTheDocument()
})
it('should render the close icon', () => {
render(<SecretKeyGenerateModal {...defaultProps} />)
// Modal renders via portal, so query from document.body
const closeIcon = document.body.querySelector('svg.cursor-pointer')
expect(closeIcon).toBeInTheDocument()
})
it('should render InputCopy component', () => {
render(<SecretKeyGenerateModal {...defaultProps} newKey={createMockApiKey('test-token-123')} />)
expect(screen.getByText('test-token-123')).toBeInTheDocument()
})
})
describe('rendering when hidden', () => {
it('should not render content when isShow is false', () => {
render(<SecretKeyGenerateModal {...defaultProps} isShow={false} />)
expect(screen.queryByText('appApi.apiKeyModal.apiSecretKey')).not.toBeInTheDocument()
})
})
describe('newKey prop', () => {
it('should display the token when newKey is provided', () => {
render(<SecretKeyGenerateModal {...defaultProps} newKey={createMockApiKey('sk-abc123xyz')} />)
expect(screen.getByText('sk-abc123xyz')).toBeInTheDocument()
})
it('should handle undefined newKey', () => {
render(<SecretKeyGenerateModal {...defaultProps} newKey={undefined} />)
// Should not crash and modal should still render
expect(screen.getByText('appApi.apiKeyModal.apiSecretKey')).toBeInTheDocument()
})
it('should handle newKey with empty token', () => {
render(<SecretKeyGenerateModal {...defaultProps} newKey={createMockApiKey('')} />)
expect(screen.getByText('appApi.apiKeyModal.apiSecretKey')).toBeInTheDocument()
})
it('should display long tokens correctly', () => {
const longToken = `sk-${'a'.repeat(100)}`
render(<SecretKeyGenerateModal {...defaultProps} newKey={createMockApiKey(longToken)} />)
expect(screen.getByText(longToken)).toBeInTheDocument()
})
})
describe('close functionality', () => {
it('should call onClose when X icon is clicked', async () => {
const user = userEvent.setup()
const onClose = vi.fn()
render(<SecretKeyGenerateModal {...defaultProps} onClose={onClose} />)
// Modal renders via portal
const closeIcon = document.body.querySelector('svg.cursor-pointer')
expect(closeIcon).toBeInTheDocument()
await act(async () => {
await user.click(closeIcon!)
})
// HeadlessUI Dialog may trigger onClose multiple times (icon click handler + dialog close)
expect(onClose).toHaveBeenCalled()
})
it('should call onClose when OK button is clicked', async () => {
const user = userEvent.setup()
const onClose = vi.fn()
render(<SecretKeyGenerateModal {...defaultProps} onClose={onClose} />)
const okButton = screen.getByRole('button', { name: /ok/i })
await act(async () => {
await user.click(okButton)
})
expect(onClose).toHaveBeenCalledTimes(1)
})
})
describe('className prop', () => {
it('should apply custom className', () => {
render(
<SecretKeyGenerateModal {...defaultProps} className="custom-modal-class" />,
)
// Modal renders via portal
const modal = document.body.querySelector('.custom-modal-class')
expect(modal).toBeInTheDocument()
})
it('should apply shrink-0 class', () => {
render(
<SecretKeyGenerateModal {...defaultProps} className="shrink-0" />,
)
// Modal renders via portal
const modal = document.body.querySelector('.shrink-0')
expect(modal).toBeInTheDocument()
})
})
describe('modal styling', () => {
it('should have px-8 padding', () => {
render(<SecretKeyGenerateModal {...defaultProps} />)
// Modal renders via portal
const modal = document.body.querySelector('.px-8')
expect(modal).toBeInTheDocument()
})
})
describe('close icon styling', () => {
it('should have cursor-pointer class on close icon', () => {
render(<SecretKeyGenerateModal {...defaultProps} />)
// Modal renders via portal
const closeIcon = document.body.querySelector('svg.cursor-pointer')
expect(closeIcon).toBeInTheDocument()
})
it('should have correct dimensions on close icon', () => {
render(<SecretKeyGenerateModal {...defaultProps} />)
// Modal renders via portal
const closeIcon = document.body.querySelector('svg[class*="h-6"][class*="w-6"]')
expect(closeIcon).toBeInTheDocument()
})
it('should have tertiary text color on close icon', () => {
render(<SecretKeyGenerateModal {...defaultProps} />)
// Modal renders via portal
const closeIcon = document.body.querySelector('svg[class*="text-text-tertiary"]')
expect(closeIcon).toBeInTheDocument()
})
})
describe('header section', () => {
it('should have flex justify-end on close container', () => {
render(<SecretKeyGenerateModal {...defaultProps} />)
// Modal renders via portal
const closeIcon = document.body.querySelector('svg.cursor-pointer')
const closeContainer = closeIcon?.parentElement
expect(closeContainer).toBeInTheDocument()
expect(closeContainer?.className).toContain('flex')
expect(closeContainer?.className).toContain('justify-end')
})
it('should have negative margin on close container', () => {
render(<SecretKeyGenerateModal {...defaultProps} />)
// Modal renders via portal
const closeIcon = document.body.querySelector('svg.cursor-pointer')
const closeContainer = closeIcon?.parentElement
expect(closeContainer).toBeInTheDocument()
expect(closeContainer?.className).toContain('-mr-2')
expect(closeContainer?.className).toContain('-mt-6')
})
it('should have bottom margin on close container', () => {
render(<SecretKeyGenerateModal {...defaultProps} />)
// Modal renders via portal
const closeIcon = document.body.querySelector('svg.cursor-pointer')
const closeContainer = closeIcon?.parentElement
expect(closeContainer).toBeInTheDocument()
expect(closeContainer?.className).toContain('mb-4')
})
})
describe('tips text styling', () => {
it('should have mt-1 margin on tips', () => {
render(<SecretKeyGenerateModal {...defaultProps} />)
const tips = screen.getByText('appApi.apiKeyModal.generateTips')
expect(tips.className).toContain('mt-1')
})
it('should have correct font size', () => {
render(<SecretKeyGenerateModal {...defaultProps} />)
const tips = screen.getByText('appApi.apiKeyModal.generateTips')
expect(tips.className).toContain('text-[13px]')
})
it('should have normal font weight', () => {
render(<SecretKeyGenerateModal {...defaultProps} />)
const tips = screen.getByText('appApi.apiKeyModal.generateTips')
expect(tips.className).toContain('font-normal')
})
it('should have leading-5 line height', () => {
render(<SecretKeyGenerateModal {...defaultProps} />)
const tips = screen.getByText('appApi.apiKeyModal.generateTips')
expect(tips.className).toContain('leading-5')
})
it('should have tertiary text color', () => {
render(<SecretKeyGenerateModal {...defaultProps} />)
const tips = screen.getByText('appApi.apiKeyModal.generateTips')
expect(tips.className).toContain('text-text-tertiary')
})
})
describe('InputCopy section', () => {
it('should render InputCopy with token value', () => {
render(<SecretKeyGenerateModal {...defaultProps} newKey={createMockApiKey('test-token')} />)
expect(screen.getByText('test-token')).toBeInTheDocument()
})
it('should have w-full class on InputCopy', () => {
render(<SecretKeyGenerateModal {...defaultProps} newKey={createMockApiKey('test')} />)
// The InputCopy component should have w-full
const inputText = screen.getByText('test')
const inputContainer = inputText.closest('.w-full')
expect(inputContainer).toBeInTheDocument()
})
})
describe('OK button section', () => {
it('should render OK button', () => {
render(<SecretKeyGenerateModal {...defaultProps} />)
const button = screen.getByRole('button', { name: /ok/i })
expect(button).toBeInTheDocument()
})
it('should have button container with flex layout', () => {
render(<SecretKeyGenerateModal {...defaultProps} />)
const button = screen.getByRole('button', { name: /ok/i })
const container = button.parentElement
expect(container).toBeInTheDocument()
expect(container?.className).toContain('flex')
})
it('should have shrink-0 on button', () => {
render(<SecretKeyGenerateModal {...defaultProps} />)
const button = screen.getByRole('button', { name: /ok/i })
expect(button.className).toContain('shrink-0')
})
})
describe('button text styling', () => {
it('should have text-xs font size on button text', () => {
render(<SecretKeyGenerateModal {...defaultProps} />)
const buttonText = screen.getByText('appApi.actionMsg.ok')
expect(buttonText.className).toContain('text-xs')
})
it('should have font-medium on button text', () => {
render(<SecretKeyGenerateModal {...defaultProps} />)
const buttonText = screen.getByText('appApi.actionMsg.ok')
expect(buttonText.className).toContain('font-medium')
})
it('should have secondary text color on button text', () => {
render(<SecretKeyGenerateModal {...defaultProps} />)
const buttonText = screen.getByText('appApi.actionMsg.ok')
expect(buttonText.className).toContain('text-text-secondary')
})
})
describe('default prop values', () => {
it('should default isShow to false', () => {
// When isShow is explicitly set to false
render(<SecretKeyGenerateModal isShow={false} onClose={vi.fn()} />)
expect(screen.queryByText('appApi.apiKeyModal.apiSecretKey')).not.toBeInTheDocument()
})
})
describe('modal title', () => {
it('should display the correct title', () => {
render(<SecretKeyGenerateModal {...defaultProps} />)
expect(screen.getByText('appApi.apiKeyModal.apiSecretKey')).toBeInTheDocument()
})
})
})

View File

@ -0,0 +1,614 @@
import { act, render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import SecretKeyModal from './secret-key-modal'
// Mock the app context
const mockCurrentWorkspace = vi.fn().mockReturnValue({
id: 'workspace-1',
name: 'Test Workspace',
})
const mockIsCurrentWorkspaceManager = vi.fn().mockReturnValue(true)
const mockIsCurrentWorkspaceEditor = vi.fn().mockReturnValue(true)
vi.mock('@/context/app-context', () => ({
useAppContext: () => ({
currentWorkspace: mockCurrentWorkspace(),
isCurrentWorkspaceManager: mockIsCurrentWorkspaceManager(),
isCurrentWorkspaceEditor: mockIsCurrentWorkspaceEditor(),
}),
}))
// Mock the timestamp hook
vi.mock('@/hooks/use-timestamp', () => ({
default: () => ({
formatTime: vi.fn((value: number, _format: string) => `Formatted: ${value}`),
formatDate: vi.fn((value: string, _format: string) => `Formatted: ${value}`),
}),
}))
// Mock API services
const mockCreateAppApikey = vi.fn().mockResolvedValue({ token: 'new-app-token-123' })
const mockDelAppApikey = vi.fn().mockResolvedValue({})
vi.mock('@/service/apps', () => ({
createApikey: (...args: unknown[]) => mockCreateAppApikey(...args),
delApikey: (...args: unknown[]) => mockDelAppApikey(...args),
}))
const mockCreateDatasetApikey = vi.fn().mockResolvedValue({ token: 'new-dataset-token-123' })
const mockDelDatasetApikey = vi.fn().mockResolvedValue({})
vi.mock('@/service/datasets', () => ({
createApikey: (...args: unknown[]) => mockCreateDatasetApikey(...args),
delApikey: (...args: unknown[]) => mockDelDatasetApikey(...args),
}))
// Mock React Query hooks for apps
const mockAppApiKeysData = vi.fn().mockReturnValue({ data: [] })
const mockIsAppApiKeysLoading = vi.fn().mockReturnValue(false)
const mockInvalidateAppApiKeys = vi.fn()
vi.mock('@/service/use-apps', () => ({
useAppApiKeys: (_appId: string, _options: unknown) => ({
data: mockAppApiKeysData(),
isLoading: mockIsAppApiKeysLoading(),
}),
useInvalidateAppApiKeys: () => mockInvalidateAppApiKeys,
}))
// Mock React Query hooks for datasets
const mockDatasetApiKeysData = vi.fn().mockReturnValue({ data: [] })
const mockIsDatasetApiKeysLoading = vi.fn().mockReturnValue(false)
const mockInvalidateDatasetApiKeys = vi.fn()
vi.mock('@/service/knowledge/use-dataset', () => ({
useDatasetApiKeys: (_options: unknown) => ({
data: mockDatasetApiKeysData(),
isLoading: mockIsDatasetApiKeysLoading(),
}),
useInvalidateDatasetApiKeys: () => mockInvalidateDatasetApiKeys,
}))
describe('SecretKeyModal', () => {
const defaultProps = {
isShow: true,
onClose: vi.fn(),
}
beforeEach(() => {
vi.clearAllMocks()
mockCurrentWorkspace.mockReturnValue({ id: 'workspace-1', name: 'Test Workspace' })
mockIsCurrentWorkspaceManager.mockReturnValue(true)
mockIsCurrentWorkspaceEditor.mockReturnValue(true)
mockAppApiKeysData.mockReturnValue({ data: [] })
mockIsAppApiKeysLoading.mockReturnValue(false)
mockDatasetApiKeysData.mockReturnValue({ data: [] })
mockIsDatasetApiKeysLoading.mockReturnValue(false)
})
describe('rendering when shown', () => {
it('should render the modal when isShow is true', () => {
render(<SecretKeyModal {...defaultProps} />)
expect(screen.getByText('appApi.apiKeyModal.apiSecretKey')).toBeInTheDocument()
})
it('should render the tips text', () => {
render(<SecretKeyModal {...defaultProps} />)
expect(screen.getByText('appApi.apiKeyModal.apiSecretKeyTips')).toBeInTheDocument()
})
it('should render the create new key button', () => {
render(<SecretKeyModal {...defaultProps} />)
expect(screen.getByText('appApi.apiKeyModal.createNewSecretKey')).toBeInTheDocument()
})
it('should render the close icon', () => {
render(<SecretKeyModal {...defaultProps} />)
// Modal renders via portal, so we need to query from document.body
const closeIcon = document.body.querySelector('svg.cursor-pointer')
expect(closeIcon).toBeInTheDocument()
})
})
describe('rendering when hidden', () => {
it('should not render content when isShow is false', () => {
render(<SecretKeyModal {...defaultProps} isShow={false} />)
expect(screen.queryByText('appApi.apiKeyModal.apiSecretKey')).not.toBeInTheDocument()
})
})
describe('loading state', () => {
it('should show loading when app API keys are loading', () => {
mockIsAppApiKeysLoading.mockReturnValue(true)
render(<SecretKeyModal {...defaultProps} appId="app-123" />)
expect(screen.getByRole('status')).toBeInTheDocument()
})
it('should show loading when dataset API keys are loading', () => {
mockIsDatasetApiKeysLoading.mockReturnValue(true)
render(<SecretKeyModal {...defaultProps} />)
expect(screen.getByRole('status')).toBeInTheDocument()
})
it('should not show loading when data is loaded', () => {
mockIsAppApiKeysLoading.mockReturnValue(false)
render(<SecretKeyModal {...defaultProps} appId="app-123" />)
expect(screen.queryByRole('status')).not.toBeInTheDocument()
})
})
describe('API keys list for app', () => {
const apiKeys = [
{ id: 'key-1', token: 'sk-abc123def456ghi789', created_at: 1700000000, last_used_at: 1700100000 },
{ id: 'key-2', token: 'sk-xyz987wvu654tsr321', created_at: 1700050000, last_used_at: null },
]
beforeEach(() => {
mockAppApiKeysData.mockReturnValue({ data: apiKeys })
})
it('should render API keys when available', () => {
render(<SecretKeyModal {...defaultProps} appId="app-123" />)
// Token 'sk-abc123def456ghi789' (21 chars) -> first 3 'sk-' + '...' + last 20 'k-abc123def456ghi789'
expect(screen.getByText('sk-...k-abc123def456ghi789')).toBeInTheDocument()
})
it('should render created time for keys', () => {
render(<SecretKeyModal {...defaultProps} appId="app-123" />)
expect(screen.getByText('Formatted: 1700000000')).toBeInTheDocument()
})
it('should render last used time for keys', () => {
render(<SecretKeyModal {...defaultProps} appId="app-123" />)
expect(screen.getByText('Formatted: 1700100000')).toBeInTheDocument()
})
it('should render "never" for keys without last_used_at', () => {
render(<SecretKeyModal {...defaultProps} appId="app-123" />)
expect(screen.getByText('appApi.never')).toBeInTheDocument()
})
it('should render delete button for managers', () => {
render(<SecretKeyModal {...defaultProps} appId="app-123" />)
// Delete button contains RiDeleteBinLine SVG - look for SVGs with h-4 w-4 class within buttons
const buttons = screen.getAllByRole('button')
// There should be at least 3 buttons: copy feedback, delete, and create
expect(buttons.length).toBeGreaterThanOrEqual(2)
// Check for delete icon SVG - Modal renders via portal
const deleteIcon = document.body.querySelector('svg[class*="h-4"][class*="w-4"]')
expect(deleteIcon).toBeInTheDocument()
})
it('should not render delete button for non-managers', () => {
mockIsCurrentWorkspaceManager.mockReturnValue(false)
render(<SecretKeyModal {...defaultProps} appId="app-123" />)
// The specific delete action button should not be present
const actionButtons = screen.getAllByRole('button')
// Should only have copy and create buttons, not delete
expect(actionButtons.length).toBeGreaterThan(0)
})
it('should render table headers', () => {
render(<SecretKeyModal {...defaultProps} appId="app-123" />)
expect(screen.getByText('appApi.apiKeyModal.secretKey')).toBeInTheDocument()
expect(screen.getByText('appApi.apiKeyModal.created')).toBeInTheDocument()
expect(screen.getByText('appApi.apiKeyModal.lastUsed')).toBeInTheDocument()
})
})
describe('API keys list for dataset', () => {
const datasetKeys = [
{ id: 'dk-1', token: 'dk-abc123def456ghi789', created_at: 1700000000, last_used_at: 1700100000 },
]
beforeEach(() => {
mockDatasetApiKeysData.mockReturnValue({ data: datasetKeys })
})
it('should render dataset API keys when no appId', () => {
render(<SecretKeyModal {...defaultProps} />)
// Token 'dk-abc123def456ghi789' (21 chars) -> first 3 'dk-' + '...' + last 20 'k-abc123def456ghi789'
expect(screen.getByText('dk-...k-abc123def456ghi789')).toBeInTheDocument()
})
})
describe('close functionality', () => {
it('should call onClose when X icon is clicked', async () => {
const user = userEvent.setup()
const onClose = vi.fn()
render(<SecretKeyModal {...defaultProps} onClose={onClose} />)
// Modal renders via portal, so we need to query from document.body
const closeIcon = document.body.querySelector('svg.cursor-pointer')
expect(closeIcon).toBeInTheDocument()
await act(async () => {
await user.click(closeIcon!)
})
expect(onClose).toHaveBeenCalledTimes(1)
})
})
describe('create new key', () => {
it('should call create API for app when button is clicked', async () => {
const user = userEvent.setup()
render(<SecretKeyModal {...defaultProps} appId="app-123" />)
const createButton = screen.getByText('appApi.apiKeyModal.createNewSecretKey')
await act(async () => {
await user.click(createButton)
})
await waitFor(() => {
expect(mockCreateAppApikey).toHaveBeenCalledWith({
url: '/apps/app-123/api-keys',
body: {},
})
})
})
it('should call create API for dataset when no appId', async () => {
const user = userEvent.setup()
render(<SecretKeyModal {...defaultProps} />)
const createButton = screen.getByText('appApi.apiKeyModal.createNewSecretKey')
await act(async () => {
await user.click(createButton)
})
await waitFor(() => {
expect(mockCreateDatasetApikey).toHaveBeenCalledWith({
url: '/datasets/api-keys',
body: {},
})
})
})
it('should show generate modal after creating key', async () => {
const user = userEvent.setup()
render(<SecretKeyModal {...defaultProps} appId="app-123" />)
const createButton = screen.getByText('appApi.apiKeyModal.createNewSecretKey')
await act(async () => {
await user.click(createButton)
})
await waitFor(() => {
// The SecretKeyGenerateModal should be shown with the new token
expect(screen.getByText('appApi.apiKeyModal.generateTips')).toBeInTheDocument()
})
})
it('should invalidate app API keys after creating', async () => {
const user = userEvent.setup()
render(<SecretKeyModal {...defaultProps} appId="app-123" />)
const createButton = screen.getByText('appApi.apiKeyModal.createNewSecretKey')
await act(async () => {
await user.click(createButton)
})
await waitFor(() => {
expect(mockInvalidateAppApiKeys).toHaveBeenCalledWith('app-123')
})
})
it('should invalidate dataset API keys after creating (no appId)', async () => {
const user = userEvent.setup()
render(<SecretKeyModal {...defaultProps} />)
const createButton = screen.getByText('appApi.apiKeyModal.createNewSecretKey')
await act(async () => {
await user.click(createButton)
})
await waitFor(() => {
expect(mockInvalidateDatasetApiKeys).toHaveBeenCalled()
})
})
it('should disable create button when no workspace', () => {
mockCurrentWorkspace.mockReturnValue(null)
render(<SecretKeyModal {...defaultProps} />)
const createButton = screen.getByText('appApi.apiKeyModal.createNewSecretKey').closest('button')
expect(createButton).toBeDisabled()
})
it('should disable create button when not editor', () => {
mockIsCurrentWorkspaceEditor.mockReturnValue(false)
render(<SecretKeyModal {...defaultProps} />)
const createButton = screen.getByText('appApi.apiKeyModal.createNewSecretKey').closest('button')
expect(createButton).toBeDisabled()
})
})
describe('delete key', () => {
const apiKeys = [
{ id: 'key-1', token: 'sk-abc123def456ghi789', created_at: 1700000000, last_used_at: 1700100000 },
]
beforeEach(() => {
mockAppApiKeysData.mockReturnValue({ data: apiKeys })
})
it('should render delete button for managers', () => {
render(<SecretKeyModal {...defaultProps} appId="app-123" />)
// Find buttons that contain SVG (delete/copy buttons)
const actionButtons = screen.getAllByRole('button')
// There should be at least copy, delete, and create buttons
expect(actionButtons.length).toBeGreaterThanOrEqual(3)
})
it('should render API key row with actions', () => {
render(<SecretKeyModal {...defaultProps} appId="app-123" />)
// Verify the truncated token is rendered
expect(screen.getByText('sk-...k-abc123def456ghi789')).toBeInTheDocument()
})
it('should have action buttons in the key row', () => {
render(<SecretKeyModal {...defaultProps} appId="app-123" />)
// Check for action button containers - Modal renders via portal
const actionContainers = document.body.querySelectorAll('[class*="space-x-2"]')
expect(actionContainers.length).toBeGreaterThan(0)
})
it('should have delete button visible for managers', async () => {
render(<SecretKeyModal {...defaultProps} appId="app-123" />)
// Find the delete button by looking for the button with the delete icon
const deleteIcon = document.body.querySelector('svg[class*="h-4"][class*="w-4"]')
const deleteButton = deleteIcon?.closest('button')
expect(deleteButton).toBeInTheDocument()
})
it('should show confirm dialog when delete button is clicked', async () => {
const user = userEvent.setup()
render(<SecretKeyModal {...defaultProps} appId="app-123" />)
// Find delete button by action-btn class (second action button after copy)
const actionButtons = document.body.querySelectorAll('button.action-btn')
// The delete button is the second action button (first is copy)
const deleteButton = actionButtons[1]
expect(deleteButton).toBeInTheDocument()
await act(async () => {
await user.click(deleteButton!)
})
// Confirm dialog should appear
await waitFor(() => {
expect(screen.getByText('appApi.actionMsg.deleteConfirmTitle')).toBeInTheDocument()
expect(screen.getByText('appApi.actionMsg.deleteConfirmTips')).toBeInTheDocument()
})
})
it('should call delete API for app when confirmed', async () => {
const user = userEvent.setup()
render(<SecretKeyModal {...defaultProps} appId="app-123" />)
// Find and click delete button
const actionButtons = document.body.querySelectorAll('button.action-btn')
const deleteButton = actionButtons[1]
await act(async () => {
await user.click(deleteButton!)
})
// Wait for confirm dialog and click confirm
await waitFor(() => {
expect(screen.getByText('appApi.actionMsg.deleteConfirmTitle')).toBeInTheDocument()
})
// Find and click the confirm button
const confirmButton = screen.getByText('common.operation.confirm')
await act(async () => {
await user.click(confirmButton)
})
await waitFor(() => {
expect(mockDelAppApikey).toHaveBeenCalledWith({
url: '/apps/app-123/api-keys/key-1',
params: {},
})
})
})
it('should invalidate app API keys after deleting', async () => {
const user = userEvent.setup()
render(<SecretKeyModal {...defaultProps} appId="app-123" />)
// Find and click delete button
const actionButtons = document.body.querySelectorAll('button.action-btn')
const deleteButton = actionButtons[1]
await act(async () => {
await user.click(deleteButton!)
})
// Wait for confirm dialog and click confirm
await waitFor(() => {
expect(screen.getByText('appApi.actionMsg.deleteConfirmTitle')).toBeInTheDocument()
})
const confirmButton = screen.getByText('common.operation.confirm')
await act(async () => {
await user.click(confirmButton)
})
await waitFor(() => {
expect(mockInvalidateAppApiKeys).toHaveBeenCalledWith('app-123')
})
})
it('should close confirm dialog and clear delKeyId when cancel is clicked', async () => {
const user = userEvent.setup()
render(<SecretKeyModal {...defaultProps} appId="app-123" />)
// Find and click delete button
const actionButtons = document.body.querySelectorAll('button.action-btn')
const deleteButton = actionButtons[1]
await act(async () => {
await user.click(deleteButton!)
})
// Wait for confirm dialog
await waitFor(() => {
expect(screen.getByText('appApi.actionMsg.deleteConfirmTitle')).toBeInTheDocument()
})
// Click cancel button
const cancelButton = screen.getByText('common.operation.cancel')
await act(async () => {
await user.click(cancelButton)
})
// Confirm dialog should close
await waitFor(() => {
expect(screen.queryByText('appApi.actionMsg.deleteConfirmTitle')).not.toBeInTheDocument()
})
// Delete API should not be called
expect(mockDelAppApikey).not.toHaveBeenCalled()
})
})
describe('delete key for dataset', () => {
const datasetKeys = [
{ id: 'dk-1', token: 'dk-abc123def456ghi789', created_at: 1700000000, last_used_at: 1700100000 },
]
beforeEach(() => {
mockDatasetApiKeysData.mockReturnValue({ data: datasetKeys })
})
it('should call delete API for dataset when no appId', async () => {
const user = userEvent.setup()
render(<SecretKeyModal {...defaultProps} />)
// Find and click delete button
const actionButtons = document.body.querySelectorAll('button.action-btn')
const deleteButton = actionButtons[1]
await act(async () => {
await user.click(deleteButton!)
})
// Wait for confirm dialog and click confirm
await waitFor(() => {
expect(screen.getByText('appApi.actionMsg.deleteConfirmTitle')).toBeInTheDocument()
})
const confirmButton = screen.getByText('common.operation.confirm')
await act(async () => {
await user.click(confirmButton)
})
await waitFor(() => {
expect(mockDelDatasetApikey).toHaveBeenCalledWith({
url: '/datasets/api-keys/dk-1',
params: {},
})
})
})
it('should invalidate dataset API keys after deleting', async () => {
const user = userEvent.setup()
render(<SecretKeyModal {...defaultProps} />)
// Find and click delete button
const actionButtons = document.body.querySelectorAll('button.action-btn')
const deleteButton = actionButtons[1]
await act(async () => {
await user.click(deleteButton!)
})
// Wait for confirm dialog and click confirm
await waitFor(() => {
expect(screen.getByText('appApi.actionMsg.deleteConfirmTitle')).toBeInTheDocument()
})
const confirmButton = screen.getByText('common.operation.confirm')
await act(async () => {
await user.click(confirmButton)
})
await waitFor(() => {
expect(mockInvalidateDatasetApiKeys).toHaveBeenCalled()
})
})
})
describe('token truncation', () => {
it('should truncate token correctly', () => {
const apiKeys = [
{ id: 'key-1', token: 'sk-abcdefghijklmnopqrstuvwxyz1234567890', created_at: 1700000000, last_used_at: null },
]
mockAppApiKeysData.mockReturnValue({ data: apiKeys })
render(<SecretKeyModal {...defaultProps} appId="app-123" />)
// Token format: first 3 chars + ... + last 20 chars
// 'sk-abcdefghijklmnopqrstuvwxyz1234567890' -> 'sk-...qrstuvwxyz1234567890'
expect(screen.getByText('sk-...qrstuvwxyz1234567890')).toBeInTheDocument()
})
})
describe('styling', () => {
it('should render modal with expected structure', () => {
render(<SecretKeyModal {...defaultProps} />)
// Modal should render and contain the title
expect(screen.getByText('appApi.apiKeyModal.apiSecretKey')).toBeInTheDocument()
})
it('should render create button with flex styling', () => {
render(<SecretKeyModal {...defaultProps} />)
// Modal renders via portal, so query from document.body
const flexContainers = document.body.querySelectorAll('[class*="flex"]')
expect(flexContainers.length).toBeGreaterThan(0)
})
})
describe('empty state', () => {
it('should not render table when no keys', () => {
mockAppApiKeysData.mockReturnValue({ data: [] })
render(<SecretKeyModal {...defaultProps} appId="app-123" />)
expect(screen.queryByText('appApi.apiKeyModal.secretKey')).not.toBeInTheDocument()
})
it('should not render table when data is null', () => {
mockAppApiKeysData.mockReturnValue(null)
render(<SecretKeyModal {...defaultProps} appId="app-123" />)
expect(screen.queryByText('appApi.apiKeyModal.secretKey')).not.toBeInTheDocument()
})
})
describe('SecretKeyGenerateModal', () => {
it('should close generate modal on close', async () => {
const user = userEvent.setup()
render(<SecretKeyModal {...defaultProps} appId="app-123" />)
// Create a new key to open generate modal
const createButton = screen.getByText('appApi.apiKeyModal.createNewSecretKey')
await act(async () => {
await user.click(createButton)
})
await waitFor(() => {
expect(screen.getByText('appApi.apiKeyModal.generateTips')).toBeInTheDocument()
})
// Find and click the close/OK button in generate modal
const okButton = screen.getByText('appApi.actionMsg.ok')
await act(async () => {
await user.click(okButton)
})
await waitFor(() => {
expect(screen.queryByText('appApi.apiKeyModal.generateTips')).not.toBeInTheDocument()
})
})
})
})

View File

@ -0,0 +1,242 @@
import { render, screen } from '@testing-library/react'
import { Tag } from './tag'
describe('Tag', () => {
describe('rendering', () => {
it('should render children text', () => {
render(<Tag>GET</Tag>)
expect(screen.getByText('GET')).toBeInTheDocument()
})
it('should render as a span element', () => {
render(<Tag>POST</Tag>)
const tag = screen.getByText('POST')
expect(tag.tagName).toBe('SPAN')
})
})
describe('default color mapping based on HTTP methods', () => {
it('should apply emerald color for GET method', () => {
render(<Tag>GET</Tag>)
const tag = screen.getByText('GET')
expect(tag.className).toContain('text-emerald')
})
it('should apply sky color for POST method', () => {
render(<Tag>POST</Tag>)
const tag = screen.getByText('POST')
expect(tag.className).toContain('text-sky')
})
it('should apply amber color for PUT method', () => {
render(<Tag>PUT</Tag>)
const tag = screen.getByText('PUT')
expect(tag.className).toContain('text-amber')
})
it('should apply rose color for DELETE method', () => {
render(<Tag>DELETE</Tag>)
const tag = screen.getByText('DELETE')
expect(tag.className).toContain('text-red')
})
it('should apply emerald color for unknown methods', () => {
render(<Tag>UNKNOWN</Tag>)
const tag = screen.getByText('UNKNOWN')
expect(tag.className).toContain('text-emerald')
})
it('should handle lowercase method names', () => {
render(<Tag>get</Tag>)
const tag = screen.getByText('get')
expect(tag.className).toContain('text-emerald')
})
it('should handle mixed case method names', () => {
render(<Tag>Post</Tag>)
const tag = screen.getByText('Post')
expect(tag.className).toContain('text-sky')
})
})
describe('custom color prop', () => {
it('should override default color with custom emerald color', () => {
render(<Tag color="emerald">CUSTOM</Tag>)
const tag = screen.getByText('CUSTOM')
expect(tag.className).toContain('text-emerald')
})
it('should override default color with custom sky color', () => {
render(<Tag color="sky">CUSTOM</Tag>)
const tag = screen.getByText('CUSTOM')
expect(tag.className).toContain('text-sky')
})
it('should override default color with custom amber color', () => {
render(<Tag color="amber">CUSTOM</Tag>)
const tag = screen.getByText('CUSTOM')
expect(tag.className).toContain('text-amber')
})
it('should override default color with custom rose color', () => {
render(<Tag color="rose">CUSTOM</Tag>)
const tag = screen.getByText('CUSTOM')
expect(tag.className).toContain('text-red')
})
it('should override default color with custom zinc color', () => {
render(<Tag color="zinc">CUSTOM</Tag>)
const tag = screen.getByText('CUSTOM')
expect(tag.className).toContain('text-zinc')
})
it('should override automatic color mapping with explicit color', () => {
render(<Tag color="sky">GET</Tag>)
const tag = screen.getByText('GET')
expect(tag.className).toContain('text-sky')
})
})
describe('variant styles', () => {
it('should apply medium variant styles by default', () => {
render(<Tag>GET</Tag>)
const tag = screen.getByText('GET')
expect(tag.className).toContain('rounded-lg')
expect(tag.className).toContain('px-1.5')
expect(tag.className).toContain('ring-1')
expect(tag.className).toContain('ring-inset')
})
it('should apply small variant styles', () => {
render(<Tag variant="small">GET</Tag>)
const tag = screen.getByText('GET')
// Small variant should not have ring styles
expect(tag.className).not.toContain('rounded-lg')
expect(tag.className).not.toContain('ring-1')
})
})
describe('base styles', () => {
it('should always have font-mono class', () => {
render(<Tag>GET</Tag>)
const tag = screen.getByText('GET')
expect(tag.className).toContain('font-mono')
})
it('should always have correct font-size class', () => {
render(<Tag>GET</Tag>)
const tag = screen.getByText('GET')
expect(tag.className).toContain('text-[0.625rem]')
})
it('should always have font-semibold class', () => {
render(<Tag>GET</Tag>)
const tag = screen.getByText('GET')
expect(tag.className).toContain('font-semibold')
})
it('should always have leading-6 class', () => {
render(<Tag>GET</Tag>)
const tag = screen.getByText('GET')
expect(tag.className).toContain('leading-6')
})
})
describe('color styles for medium variant', () => {
it('should apply full emerald medium styles', () => {
render(<Tag color="emerald" variant="medium">TEST</Tag>)
const tag = screen.getByText('TEST')
expect(tag.className).toContain('ring-emerald-300')
expect(tag.className).toContain('bg-emerald-400/10')
expect(tag.className).toContain('text-emerald-500')
})
it('should apply full sky medium styles', () => {
render(<Tag color="sky" variant="medium">TEST</Tag>)
const tag = screen.getByText('TEST')
expect(tag.className).toContain('ring-sky-300')
expect(tag.className).toContain('bg-sky-400/10')
expect(tag.className).toContain('text-sky-500')
})
it('should apply full amber medium styles', () => {
render(<Tag color="amber" variant="medium">TEST</Tag>)
const tag = screen.getByText('TEST')
expect(tag.className).toContain('ring-amber-300')
expect(tag.className).toContain('bg-amber-400/10')
expect(tag.className).toContain('text-amber-500')
})
it('should apply full rose medium styles', () => {
render(<Tag color="rose" variant="medium">TEST</Tag>)
const tag = screen.getByText('TEST')
expect(tag.className).toContain('ring-rose-200')
expect(tag.className).toContain('bg-rose-50')
expect(tag.className).toContain('text-red-500')
})
it('should apply full zinc medium styles', () => {
render(<Tag color="zinc" variant="medium">TEST</Tag>)
const tag = screen.getByText('TEST')
expect(tag.className).toContain('ring-zinc-200')
expect(tag.className).toContain('bg-zinc-50')
expect(tag.className).toContain('text-zinc-500')
})
})
describe('color styles for small variant', () => {
it('should apply emerald small styles', () => {
render(<Tag color="emerald" variant="small">TEST</Tag>)
const tag = screen.getByText('TEST')
expect(tag.className).toContain('text-emerald-500')
// Small variant should not have background/ring styles
expect(tag.className).not.toContain('bg-emerald-400/10')
expect(tag.className).not.toContain('ring-emerald-300')
})
it('should apply sky small styles', () => {
render(<Tag color="sky" variant="small">TEST</Tag>)
const tag = screen.getByText('TEST')
expect(tag.className).toContain('text-sky-500')
})
it('should apply amber small styles', () => {
render(<Tag color="amber" variant="small">TEST</Tag>)
const tag = screen.getByText('TEST')
expect(tag.className).toContain('text-amber-500')
})
it('should apply rose small styles', () => {
render(<Tag color="rose" variant="small">TEST</Tag>)
const tag = screen.getByText('TEST')
expect(tag.className).toContain('text-red-500')
})
it('should apply zinc small styles', () => {
render(<Tag color="zinc" variant="small">TEST</Tag>)
const tag = screen.getByText('TEST')
expect(tag.className).toContain('text-zinc-400')
})
})
describe('HTTP method color combinations', () => {
it('should correctly map PATCH to emerald (default)', () => {
render(<Tag>PATCH</Tag>)
const tag = screen.getByText('PATCH')
// PATCH is not in the valueColorMap, so it defaults to emerald
expect(tag.className).toContain('text-emerald')
})
it('should correctly render all standard HTTP methods', () => {
const methods = ['GET', 'POST', 'PUT', 'DELETE']
const expectedColors = ['emerald', 'sky', 'amber', 'red']
methods.forEach((method, index) => {
const { unmount } = render(<Tag>{method}</Tag>)
const tag = screen.getByText(method)
expect(tag.className).toContain(`text-${expectedColors[index]}`)
unmount()
})
})
})
})