feat(tests): add integration tests for API key management and develop page flow

This commit is contained in:
CodingOnStar
2026-02-11 17:42:29 +08:00
parent 5b4c7b2a40
commit 1f3014bbc4
24 changed files with 1366 additions and 412 deletions

View File

@ -0,0 +1,214 @@
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { act } from 'react'
import ApiServer from '../ApiServer'
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} />)
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" />)
const apiKeyButton = screen.getByText('appApi.apiKey')
await act(async () => {
await user.click(apiKeyButton)
})
expect(screen.getByTestId('secret-key-modal')).toBeInTheDocument()
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" />)
const button = screen.getByRole('button', { name: /apiKey/i })
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,592 @@
import { act, render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { Code, CodeGroup, Embed, Pre } from '../code'
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>,
)
await act(async () => {
vi.runAllTimers()
})
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()
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>,
)
await act(async () => {
vi.runAllTimers()
})
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>,
)
await act(async () => {
vi.runAllTimers()
})
const copyButton = screen.getByRole('button')
await act(async () => {
await user.click(copyButton)
})
await waitFor(() => {
expect(screen.getByText('Copied!')).toBeInTheDocument()
})
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>,
)
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>,
)
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>,
)
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>,
)
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,206 @@
import { act, fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { AppModeEnum, Theme } from '@/types/app'
import Doc from '../doc'
// The vitest mdx-stub plugin makes .mdx files parseable; these mocks replace
vi.mock('../template/template.en.mdx', () => ({
default: (_props: Record<string, unknown>) => <div data-testid="template-completion-en" />,
}))
vi.mock('../template/template.zh.mdx', () => ({
default: (_props: Record<string, unknown>) => <div data-testid="template-completion-zh" />,
}))
vi.mock('../template/template.ja.mdx', () => ({
default: (_props: Record<string, unknown>) => <div data-testid="template-completion-ja" />,
}))
vi.mock('../template/template_chat.en.mdx', () => ({
default: (_props: Record<string, unknown>) => <div data-testid="template-chat-en" />,
}))
vi.mock('../template/template_chat.zh.mdx', () => ({
default: (_props: Record<string, unknown>) => <div data-testid="template-chat-zh" />,
}))
vi.mock('../template/template_chat.ja.mdx', () => ({
default: (_props: Record<string, unknown>) => <div data-testid="template-chat-ja" />,
}))
vi.mock('../template/template_advanced_chat.en.mdx', () => ({
default: (_props: Record<string, unknown>) => <div data-testid="template-advanced-chat-en" />,
}))
vi.mock('../template/template_advanced_chat.zh.mdx', () => ({
default: (_props: Record<string, unknown>) => <div data-testid="template-advanced-chat-zh" />,
}))
vi.mock('../template/template_advanced_chat.ja.mdx', () => ({
default: (_props: Record<string, unknown>) => <div data-testid="template-advanced-chat-ja" />,
}))
vi.mock('../template/template_workflow.en.mdx', () => ({
default: (_props: Record<string, unknown>) => <div data-testid="template-workflow-en" />,
}))
vi.mock('../template/template_workflow.zh.mdx', () => ({
default: (_props: Record<string, unknown>) => <div data-testid="template-workflow-zh" />,
}))
vi.mock('../template/template_workflow.ja.mdx', () => ({
default: (_props: Record<string, unknown>) => <div data-testid="template-workflow-ja" />,
}))
const mockLocale = vi.fn().mockReturnValue('en-US')
vi.mock('@/context/i18n', () => ({
useLocale: () => mockLocale(),
}))
const mockTheme = vi.fn().mockReturnValue(Theme.light)
vi.mock('@/hooks/use-theme', () => ({
default: () => ({ theme: mockTheme() }),
}))
vi.mock('@/i18n-config/language', () => ({
LanguagesSupported: ['en-US', 'zh-Hans', 'zh-Hant', 'pt-BR', 'es-ES', 'fr-FR', 'de-DE', 'ja-JP'],
}))
describe('Doc', () => {
const makeAppDetail = (mode: AppModeEnum, variables: Array<{ key: string, name: string }> = []) => ({
mode,
model_config: {
configs: {
prompt_variables: variables,
},
},
})
beforeEach(() => {
vi.clearAllMocks()
mockLocale.mockReturnValue('en-US')
mockTheme.mockReturnValue(Theme.light)
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockReturnValue({ matches: false }),
})
})
describe('template selection by app mode', () => {
it.each([
[AppModeEnum.CHAT, 'template-chat-en'],
[AppModeEnum.AGENT_CHAT, 'template-chat-en'],
[AppModeEnum.ADVANCED_CHAT, 'template-advanced-chat-en'],
[AppModeEnum.WORKFLOW, 'template-workflow-en'],
[AppModeEnum.COMPLETION, 'template-completion-en'],
])('should render correct EN template for mode %s', (mode, testId) => {
render(<Doc appDetail={makeAppDetail(mode)} />)
expect(screen.getByTestId(testId)).toBeInTheDocument()
})
})
describe('template selection by locale', () => {
it('should render ZH template when locale is zh-Hans', () => {
mockLocale.mockReturnValue('zh-Hans')
render(<Doc appDetail={makeAppDetail(AppModeEnum.CHAT)} />)
expect(screen.getByTestId('template-chat-zh')).toBeInTheDocument()
})
it('should render JA template when locale is ja-JP', () => {
mockLocale.mockReturnValue('ja-JP')
render(<Doc appDetail={makeAppDetail(AppModeEnum.CHAT)} />)
expect(screen.getByTestId('template-chat-ja')).toBeInTheDocument()
})
it('should fall back to EN template for unsupported locales', () => {
mockLocale.mockReturnValue('fr-FR')
render(<Doc appDetail={makeAppDetail(AppModeEnum.COMPLETION)} />)
expect(screen.getByTestId('template-completion-en')).toBeInTheDocument()
})
it('should render ZH advanced-chat template', () => {
mockLocale.mockReturnValue('zh-Hans')
render(<Doc appDetail={makeAppDetail(AppModeEnum.ADVANCED_CHAT)} />)
expect(screen.getByTestId('template-advanced-chat-zh')).toBeInTheDocument()
})
it('should render JA workflow template', () => {
mockLocale.mockReturnValue('ja-JP')
render(<Doc appDetail={makeAppDetail(AppModeEnum.WORKFLOW)} />)
expect(screen.getByTestId('template-workflow-ja')).toBeInTheDocument()
})
})
describe('null/undefined appDetail', () => {
it('should render nothing when appDetail has no mode', () => {
render(<Doc appDetail={{}} />)
expect(screen.queryByTestId('template-completion-en')).not.toBeInTheDocument()
expect(screen.queryByTestId('template-chat-en')).not.toBeInTheDocument()
})
it('should render nothing when appDetail is null', () => {
render(<Doc appDetail={null} />)
expect(screen.queryByTestId('template-completion-en')).not.toBeInTheDocument()
})
})
describe('TOC toggle', () => {
it('should show collapsed TOC button by default on small screens', () => {
render(<Doc appDetail={makeAppDetail(AppModeEnum.CHAT)} />)
expect(screen.getByLabelText('Open table of contents')).toBeInTheDocument()
})
it('should show expanded TOC on wide screens', () => {
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockReturnValue({ matches: true }),
})
render(<Doc appDetail={makeAppDetail(AppModeEnum.CHAT)} />)
expect(screen.getByText('appApi.develop.toc')).toBeInTheDocument()
expect(screen.getByLabelText('Close')).toBeInTheDocument()
})
it('should expand TOC when toggle button is clicked', async () => {
render(<Doc appDetail={makeAppDetail(AppModeEnum.CHAT)} />)
const toggleBtn = screen.getByLabelText('Open table of contents')
await act(async () => {
fireEvent.click(toggleBtn)
})
expect(screen.getByText('appApi.develop.toc')).toBeInTheDocument()
})
it('should collapse TOC when close button is clicked', async () => {
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockReturnValue({ matches: true }),
})
render(<Doc appDetail={makeAppDetail(AppModeEnum.CHAT)} />)
const closeBtn = screen.getByLabelText('Close')
await act(async () => {
fireEvent.click(closeBtn)
})
expect(screen.getByLabelText('Open table of contents')).toBeInTheDocument()
})
})
describe('dark theme', () => {
it('should apply prose-invert class in dark mode', () => {
mockTheme.mockReturnValue(Theme.dark)
const { container } = render(<Doc appDetail={makeAppDetail(AppModeEnum.CHAT)} />)
const article = container.querySelector('article')
expect(article?.className).toContain('prose-invert')
})
it('should not apply prose-invert class in light mode', () => {
mockTheme.mockReturnValue(Theme.light)
const { container } = render(<Doc appDetail={makeAppDetail(AppModeEnum.CHAT)} />)
const article = container.querySelector('article')
expect(article?.className).not.toContain('prose-invert')
})
})
describe('article structure', () => {
it('should render article with prose classes', () => {
const { container } = render(<Doc appDetail={makeAppDetail(AppModeEnum.COMPLETION)} />)
const article = container.querySelector('article')
expect(article).toBeInTheDocument()
expect(article?.className).toContain('prose')
})
it('should render flex layout wrapper', () => {
const { container } = render(<Doc appDetail={makeAppDetail(AppModeEnum.CHAT)} />)
expect(container.querySelector('.flex')).toBeInTheDocument()
})
})
})

View File

@ -0,0 +1,335 @@
import { render, screen } from '@testing-library/react'
import DevelopMain from '../index'
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)
},
}))
vi.mock('@/app/components/develop/doc', () => ({
default: ({ appDetail }: { appDetail: { name?: string } | null }) => (
<div data-testid="doc-component">
Doc Component -
{appDetail?.name}
</div>
),
}))
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" />)
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,239 @@
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')
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')
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')
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()
})
})
})
})