dify/web/app/components/base/markdown-blocks/__tests__/button.spec.tsx
yyh 563cf2602f
refactor(dify-ui): govern public component APIs (#39806)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-07-30 10:12:34 +00:00

123 lines
3.9 KiB
TypeScript

import type { ExtraProps } from 'streamdown'
import type { ChatContextValue } from '@/app/components/base/chat/chat/context'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
// markdown-button.spec.tsx
import * as React from 'react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ChatContextProvider } from '@/app/components/base/chat/chat/context-provider'
import MarkdownButton from '../button'
// Only mock the URL utility so behavior is deterministic
const isValidUrlSpy = vi.fn()
vi.mock('../utils', () => ({
isValidUrl: (u: string) => isValidUrlSpy(u),
})) // test subject
type TestNode = NonNullable<ExtraProps['node']>
function createButtonNode(properties: TestNode['properties'], label?: string): TestNode {
return {
type: 'element',
tagName: 'button',
properties,
children: label === undefined ? [] : [{ type: 'text', value: label }],
}
}
describe('MarkdownButton (integration)', () => {
const onSendSpy = vi.fn()
beforeEach(() => {
vi.clearAllMocks()
})
function renderWithCtx(node: TestNode) {
// Provide minimal ChatContext; cast to ChatContextValue to satisfy the provider signature
const ctx = {
onSend: (msg: unknown) => onSendSpy(msg),
// other props are optional at runtime; assert type to satisfy TS
} as unknown as ChatContextValue
return render(
<ChatContextProvider {...ctx}>
<MarkdownButton node={node} />
</ChatContextProvider>,
)
}
it('renders button text from node children', () => {
const node = createButtonNode({}, 'Click me')
renderWithCtx(node)
expect(screen.getByRole('button')).toHaveTextContent('Click me')
})
it('opens new tab when link is valid and does not call onSend', async () => {
isValidUrlSpy.mockReturnValue(true)
const openSpy = vi.spyOn(window, 'open').mockImplementation(() => null)
const user = userEvent.setup()
const node = createButtonNode({ dataLink: 'https://example.com' }, 'Go')
renderWithCtx(node)
await user.click(screen.getByRole('button'))
expect(isValidUrlSpy).toHaveBeenCalledWith('https://example.com')
expect(openSpy).toHaveBeenCalledWith('https://example.com', '_blank')
expect(onSendSpy).not.toHaveBeenCalled()
openSpy.mockRestore()
})
it('calls onSend when link is invalid but message exists', async () => {
isValidUrlSpy.mockReturnValue(false)
const user = userEvent.setup()
const node = createButtonNode({ dataLink: 'not-a-url', dataMessage: 'hello!' }, 'Send')
renderWithCtx(node)
await user.click(screen.getByRole('button'))
expect(isValidUrlSpy).toHaveBeenCalledWith('not-a-url')
expect(onSendSpy).toHaveBeenCalledTimes(1)
expect(onSendSpy).toHaveBeenCalledWith('hello!')
})
it('does nothing when no link and no message', async () => {
isValidUrlSpy.mockReturnValue(false)
const user = userEvent.setup()
const node = createButtonNode({}, 'Empty')
renderWithCtx(node)
await user.click(screen.getByRole('button'))
expect(isValidUrlSpy).not.toHaveBeenCalled()
expect(onSendSpy).not.toHaveBeenCalled()
})
it('calls onSend when message present and no link', async () => {
const user = userEvent.setup()
const node = createButtonNode({ dataMessage: 'msg-only' }, 'Msg')
renderWithCtx(node)
await user.click(screen.getByRole('button'))
expect(onSendSpy).toHaveBeenCalledWith('msg-only')
})
it('falls back to empty label when first child value is missing', () => {
const node = createButtonNode({})
renderWithCtx(node)
expect(screen.getByRole('button')).toHaveTextContent('')
})
it('maps the legacy warning variant to a destructive primary button', () => {
renderWithCtx(createButtonNode({ dataVariant: 'warning' }, 'Delete'))
expect(screen.getByRole('button', { name: 'Delete' })).toHaveClass(
'bg-components-button-destructive-primary-bg',
)
})
})