mirror of
https://github.com/langgenius/dify.git
synced 2026-05-05 09:58:04 +08:00
Merge commit '657eeb65' into sandboxed-agent-rebase
Made-with: Cursor # Conflicts: # api/core/agent/cot_chat_agent_runner.py # api/core/agent/fc_agent_runner.py # api/core/memory/token_buffer_memory.py # api/core/variables/segments.py # api/core/workflow/file/file_manager.py # api/core/workflow/nodes/agent/agent_node.py # api/core/workflow/nodes/llm/llm_utils.py # api/core/workflow/nodes/parameter_extractor/parameter_extractor_node.py # api/core/workflow/workflow_entry.py # api/factories/variable_factory.py # api/pyproject.toml # api/services/variable_truncator.py # api/uv.lock # web/app/components/app/app-publisher/index.tsx # web/app/components/app/overview/settings/index.tsx # web/app/components/apps/app-card.tsx # web/app/components/apps/index.tsx # web/app/components/apps/list.tsx # web/app/components/base/chat/chat-with-history/header-in-mobile.tsx # web/app/components/base/features/new-feature-panel/conversation-opener/modal.tsx # web/app/components/base/features/new-feature-panel/file-upload/setting-content.tsx # web/app/components/base/features/new-feature-panel/moderation/moderation-setting-modal.tsx # web/app/components/base/features/new-feature-panel/text-to-speech/param-config-content.tsx # web/app/components/base/message-log-modal/index.tsx # web/app/components/base/switch/index.tsx # web/app/components/base/tab-slider-plain/index.tsx # web/app/components/explore/try-app/app-info/index.tsx # web/app/components/plugins/plugin-detail-panel/tool-selector/components/reasoning-config-form.tsx # web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/edit-card/required-switch.tsx # web/app/components/workflow/nodes/llm/panel.tsx # web/contract/router.ts # web/eslint-suppressions.json # web/i18n/fa-IR/workflow.json
This commit is contained in:
73
web/app/components/base/markdown-blocks/audio-block.spec.tsx
Normal file
73
web/app/components/base/markdown-blocks/audio-block.spec.tsx
Normal file
@ -0,0 +1,73 @@
|
||||
import type { NamedExoticComponent } from 'react'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
|
||||
// AudioBlock.integration.spec.tsx
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import AudioBlock from './audio-block'
|
||||
|
||||
// Mock the nested AudioPlayer used by AudioGallery (do not mock AudioGallery itself)
|
||||
const audioPlayerMock = vi.fn()
|
||||
vi.mock('@/app/components/base/audio-gallery/AudioPlayer', () => ({
|
||||
default: (props: { srcs: string[] }) => {
|
||||
audioPlayerMock(props)
|
||||
return <div data-testid="audio-player" data-srcs={JSON.stringify(props.srcs)} />
|
||||
},
|
||||
})) // adjust path if AudioBlock sits elsewhere
|
||||
|
||||
describe('AudioBlock (integration - real AudioGallery)', () => {
|
||||
beforeEach(() => {
|
||||
audioPlayerMock.mockClear()
|
||||
})
|
||||
|
||||
it('renders AudioGallery with multiple srcs extracted from node.children', () => {
|
||||
const node = {
|
||||
children: [
|
||||
{ properties: { src: 'one.mp3' } },
|
||||
{ properties: { src: 'two.mp3' } },
|
||||
{ type: 'text', value: 'plain' },
|
||||
],
|
||||
properties: {},
|
||||
}
|
||||
|
||||
const { container } = render(<AudioBlock node={node} />)
|
||||
|
||||
const gallery = screen.getByTestId('audio-player')
|
||||
expect(gallery).toBeInTheDocument()
|
||||
|
||||
expect(audioPlayerMock).toHaveBeenCalledTimes(1)
|
||||
expect(audioPlayerMock).toHaveBeenCalledWith({ srcs: ['one.mp3', 'two.mp3'] })
|
||||
|
||||
expect(container.firstChild).not.toBeNull()
|
||||
})
|
||||
|
||||
it('renders AudioGallery with single src from node.properties when no children with properties', () => {
|
||||
const node = {
|
||||
children: [{ type: 'text', value: 'no-src' }],
|
||||
properties: { src: 'single.mp3' },
|
||||
}
|
||||
|
||||
render(<AudioBlock node={node} />)
|
||||
|
||||
expect(audioPlayerMock).toHaveBeenCalledTimes(1)
|
||||
expect(audioPlayerMock).toHaveBeenCalledWith({ srcs: ['single.mp3'] })
|
||||
expect(screen.getByTestId('audio-player')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('returns null when there are no audio sources', () => {
|
||||
const node = {
|
||||
children: [{ type: 'text', value: 'nothing here' }],
|
||||
properties: {},
|
||||
}
|
||||
|
||||
const { container } = render(<AudioBlock node={node} />)
|
||||
expect(container.firstChild).toBeNull()
|
||||
expect(audioPlayerMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('has displayName set to AudioBlock', () => {
|
||||
const component = AudioBlock as NamedExoticComponent<{ node: unknown }>
|
||||
expect(component.displayName).toBe('AudioBlock')
|
||||
})
|
||||
})
|
||||
121
web/app/components/base/markdown-blocks/button.spec.tsx
Normal file
121
web/app/components/base/markdown-blocks/button.spec.tsx
Normal file
@ -0,0 +1,121 @@
|
||||
import type { NamedExoticComponent } from 'react'
|
||||
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'
|
||||
|
||||
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 = {
|
||||
properties?: {
|
||||
dataVariant?: string
|
||||
dataMessage?: string
|
||||
dataLink?: string
|
||||
dataSize?: string
|
||||
}
|
||||
children?: Array<{ value?: string }>
|
||||
}
|
||||
|
||||
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 as unknown as Record<string, unknown>} />
|
||||
</ChatContextProvider>,
|
||||
)
|
||||
}
|
||||
|
||||
it('renders button text from node children', () => {
|
||||
const node: TestNode = { children: [{ value: 'Click me' }], properties: {} }
|
||||
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: TestNode = {
|
||||
properties: { dataLink: 'https://example.com' },
|
||||
children: [{ value: '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: TestNode = {
|
||||
properties: { dataLink: 'not-a-url', dataMessage: 'hello!' },
|
||||
children: [{ value: '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: TestNode = { properties: {}, children: [{ value: '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: TestNode = {
|
||||
properties: { dataMessage: 'msg-only' },
|
||||
children: [{ value: 'Msg' }],
|
||||
}
|
||||
|
||||
renderWithCtx(node)
|
||||
await user.click(screen.getByRole('button'))
|
||||
|
||||
expect(onSendSpy).toHaveBeenCalledWith('msg-only')
|
||||
})
|
||||
|
||||
it('has displayName set to MarkdownButton', () => {
|
||||
const comp = MarkdownButton as NamedExoticComponent<{ node: unknown }>
|
||||
expect(comp.displayName).toBe('MarkdownButton')
|
||||
})
|
||||
})
|
||||
96
web/app/components/base/markdown-blocks/paragraph.spec.tsx
Normal file
96
web/app/components/base/markdown-blocks/paragraph.spec.tsx
Normal file
@ -0,0 +1,96 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import Paragraph from './paragraph'
|
||||
|
||||
vi.mock('@/app/components/base/image-gallery', () => ({
|
||||
default: ({ srcs }: { srcs: string[] }) => (
|
||||
<div data-testid="image-gallery">{srcs.join(',')}</div>
|
||||
),
|
||||
}))
|
||||
|
||||
type MockNode = {
|
||||
children?: Array<{
|
||||
tagName?: string
|
||||
properties?: {
|
||||
src?: string
|
||||
}
|
||||
}>
|
||||
}
|
||||
|
||||
type ParagraphProps = {
|
||||
node: MockNode
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
const renderParagraph = (props: ParagraphProps) => {
|
||||
return render(<Paragraph {...props} />)
|
||||
}
|
||||
|
||||
describe('Paragraph', () => {
|
||||
it('should render normal paragraph when no image child exists', () => {
|
||||
renderParagraph({
|
||||
node: { children: [] },
|
||||
children: 'Hello world',
|
||||
})
|
||||
|
||||
expect(screen.getByText('Hello world').tagName).toBe('P')
|
||||
})
|
||||
|
||||
it('should render image gallery when first child is img', () => {
|
||||
renderParagraph({
|
||||
node: {
|
||||
children: [
|
||||
{
|
||||
tagName: 'img',
|
||||
properties: { src: 'test.png' },
|
||||
},
|
||||
],
|
||||
},
|
||||
children: ['Image only'],
|
||||
})
|
||||
|
||||
expect(screen.getByTestId('image-gallery')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('image-gallery')).toHaveTextContent('test.png')
|
||||
})
|
||||
|
||||
it('should render additional content after image when children length > 1', () => {
|
||||
renderParagraph({
|
||||
node: {
|
||||
children: [
|
||||
{
|
||||
tagName: 'img',
|
||||
properties: { src: 'test.png' },
|
||||
},
|
||||
],
|
||||
},
|
||||
children: ['Image', <span key="1">Caption</span>],
|
||||
})
|
||||
|
||||
expect(screen.getByTestId('image-gallery')).toBeInTheDocument()
|
||||
expect(screen.getByText('Caption')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render paragraph when first child exists but is not img', () => {
|
||||
renderParagraph({
|
||||
node: {
|
||||
children: [
|
||||
{
|
||||
tagName: 'div',
|
||||
},
|
||||
],
|
||||
},
|
||||
children: 'Not image',
|
||||
})
|
||||
|
||||
expect(screen.getByText('Not image').tagName).toBe('P')
|
||||
})
|
||||
|
||||
it('should render paragraph when children_node is undefined', () => {
|
||||
renderParagraph({
|
||||
node: {},
|
||||
children: 'Fallback',
|
||||
})
|
||||
|
||||
expect(screen.getByText('Fallback').tagName).toBe('P')
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,181 @@
|
||||
/* eslint-disable next/no-img-element */
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { usePluginReadmeAsset } from '@/service/use-plugins'
|
||||
import { PluginParagraph } from './plugin-paragraph'
|
||||
import { getMarkdownImageURL } from './utils'
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('@/service/use-plugins', () => ({
|
||||
usePluginReadmeAsset: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('./utils', () => ({
|
||||
getMarkdownImageURL: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/image-uploader/image-preview', () => ({
|
||||
default: ({ url, onCancel }: { url: string, onCancel: () => void }) => (
|
||||
<div data-testid="image-preview-modal">
|
||||
<span>{url}</span>
|
||||
<button onClick={onCancel} type="button">Close</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
/**
|
||||
* Interfaces to avoid 'any' and satisfy strict linting
|
||||
*/
|
||||
type MockNode = {
|
||||
children?: Array<{
|
||||
tagName?: string
|
||||
properties?: { src?: string }
|
||||
}>
|
||||
}
|
||||
|
||||
type HookReturn = {
|
||||
data?: Blob
|
||||
isLoading?: boolean
|
||||
error?: Error | null
|
||||
}
|
||||
|
||||
describe('PluginParagraph', () => {
|
||||
const mockPluginInfo = {
|
||||
pluginUniqueIdentifier: 'test-plugin-id',
|
||||
pluginId: 'plugin-123',
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
// Ensure URL globals exist in the test environment using globalThis
|
||||
if (!globalThis.URL.createObjectURL) {
|
||||
globalThis.URL.createObjectURL = vi.fn()
|
||||
globalThis.URL.revokeObjectURL = vi.fn()
|
||||
}
|
||||
|
||||
// Default mock return to prevent destructuring errors
|
||||
vi.mocked(usePluginReadmeAsset).mockReturnValue({
|
||||
data: undefined,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
} as HookReturn as ReturnType<typeof usePluginReadmeAsset>)
|
||||
})
|
||||
|
||||
it('should render a standard paragraph when not an image', () => {
|
||||
const node: MockNode = { children: [{ tagName: 'span' }] }
|
||||
render(
|
||||
<PluginParagraph node={node}>
|
||||
Hello World
|
||||
</PluginParagraph>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('standard-paragraph')).toHaveTextContent('Hello World')
|
||||
})
|
||||
|
||||
it('should render an ImageGallery when the first child is an image', () => {
|
||||
const node: MockNode = {
|
||||
children: [{ tagName: 'img', properties: { src: 'test-img.png' } }],
|
||||
}
|
||||
vi.mocked(getMarkdownImageURL).mockReturnValue('https://cdn.com/test-img.png')
|
||||
|
||||
const { container } = render(
|
||||
<PluginParagraph pluginInfo={mockPluginInfo} node={node}>
|
||||
<img src="test-img.png" alt="" />
|
||||
</PluginParagraph>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('image-paragraph-wrapper')).toBeInTheDocument()
|
||||
// Query by selector since alt="" removes the 'img' role from the accessibility tree
|
||||
const img = container.querySelector('img')
|
||||
expect(img).toHaveAttribute('src', 'https://cdn.com/test-img.png')
|
||||
})
|
||||
|
||||
it('should use a blob URL when asset data is successfully fetched', () => {
|
||||
const node: MockNode = {
|
||||
children: [{ tagName: 'img', properties: { src: 'test-img.png' } }],
|
||||
}
|
||||
const mockBlob = new Blob([''], { type: 'image/png' })
|
||||
vi.mocked(usePluginReadmeAsset).mockReturnValue({
|
||||
data: mockBlob,
|
||||
} as HookReturn as ReturnType<typeof usePluginReadmeAsset>)
|
||||
|
||||
vi.spyOn(globalThis.URL, 'createObjectURL').mockReturnValue('blob:actual-blob-url')
|
||||
|
||||
const { container } = render(
|
||||
<PluginParagraph pluginInfo={mockPluginInfo} node={node}>
|
||||
<img src="test-img.png" alt="" />
|
||||
</PluginParagraph>,
|
||||
)
|
||||
|
||||
const img = container.querySelector('img')
|
||||
expect(img).toHaveAttribute('src', 'blob:actual-blob-url')
|
||||
})
|
||||
|
||||
it('should render remaining children below the image gallery', () => {
|
||||
const node: MockNode = {
|
||||
children: [
|
||||
{ tagName: 'img', properties: { src: 'test-img.png' } },
|
||||
{ tagName: 'text' },
|
||||
],
|
||||
}
|
||||
|
||||
render(
|
||||
<PluginParagraph pluginInfo={mockPluginInfo} node={node}>
|
||||
<img src="test-img.png" alt="" />
|
||||
<span>Caption Text</span>
|
||||
</PluginParagraph>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('remaining-children')).toHaveTextContent('Caption Text')
|
||||
})
|
||||
|
||||
it('should revoke the blob URL on unmount to prevent memory leaks', () => {
|
||||
const node: MockNode = {
|
||||
children: [{ tagName: 'img', properties: { src: 'test-img.png' } }],
|
||||
}
|
||||
const mockBlob = new Blob([''], { type: 'image/png' })
|
||||
vi.mocked(usePluginReadmeAsset).mockReturnValue({
|
||||
data: mockBlob,
|
||||
} as HookReturn as ReturnType<typeof usePluginReadmeAsset>)
|
||||
|
||||
const revokeSpy = vi.spyOn(globalThis.URL, 'revokeObjectURL')
|
||||
vi.spyOn(globalThis.URL, 'createObjectURL').mockReturnValue('blob:cleanup-test')
|
||||
|
||||
const { unmount } = render(
|
||||
<PluginParagraph pluginInfo={mockPluginInfo} node={node}>
|
||||
<img src="test-img.png" alt="" />
|
||||
</PluginParagraph>,
|
||||
)
|
||||
|
||||
unmount()
|
||||
expect(revokeSpy).toHaveBeenCalledWith('blob:cleanup-test')
|
||||
})
|
||||
|
||||
it('should open the image preview modal when an image in the gallery is clicked', async () => {
|
||||
const user = userEvent.setup()
|
||||
const node: MockNode = {
|
||||
children: [{ tagName: 'img', properties: { src: 'test-img.png' } }],
|
||||
}
|
||||
vi.mocked(getMarkdownImageURL).mockReturnValue('https://cdn.com/gallery.png')
|
||||
|
||||
const { container } = render(
|
||||
<PluginParagraph pluginInfo={mockPluginInfo} node={node}>
|
||||
<img src="test-img.png" alt="" />
|
||||
</PluginParagraph>,
|
||||
)
|
||||
|
||||
const img = container.querySelector('img')
|
||||
if (img)
|
||||
await user.click(img)
|
||||
|
||||
// ImageGallery is not mocked, so it should trigger the preview
|
||||
expect(screen.getByTestId('image-preview-modal')).toBeInTheDocument()
|
||||
expect(screen.getByText('https://cdn.com/gallery.png')).toBeInTheDocument()
|
||||
|
||||
const closeBtn = screen.getByText('Close')
|
||||
await user.click(closeBtn)
|
||||
expect(screen.queryByTestId('image-preview-modal')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -58,13 +58,13 @@ export const PluginParagraph: React.FC<PluginParagraphProps> = ({ pluginInfo, no
|
||||
const remainingChildren = Array.isArray(children) && children.length > 1 ? children.slice(1) : undefined
|
||||
|
||||
return (
|
||||
<div className="markdown-img-wrapper">
|
||||
<div className="markdown-img-wrapper" data-testid="image-paragraph-wrapper">
|
||||
<ImageGallery srcs={[imageUrl]} />
|
||||
{remainingChildren && (
|
||||
<div className="mt-2">{remainingChildren}</div>
|
||||
<div className="mt-2" data-testid="remaining-children">{remainingChildren}</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return <p>{children}</p>
|
||||
return <p data-testid="standard-paragraph">{children}</p>
|
||||
}
|
||||
|
||||
61
web/app/components/base/markdown-blocks/pre-code.spec.tsx
Normal file
61
web/app/components/base/markdown-blocks/pre-code.spec.tsx
Normal file
@ -0,0 +1,61 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import PreCode from './pre-code'
|
||||
|
||||
describe('PreCode Component', () => {
|
||||
it('renders children correctly inside the pre tag', () => {
|
||||
const { container } = render(
|
||||
<PreCode>
|
||||
<code data-testid="test-code">console.log("hello world")</code>
|
||||
</PreCode>,
|
||||
)
|
||||
|
||||
const preElement = container.querySelector('pre')
|
||||
const codeElement = screen.getByTestId('test-code')
|
||||
|
||||
expect(preElement).toBeInTheDocument()
|
||||
expect(codeElement).toBeInTheDocument()
|
||||
// Verify code is a descendant of pre
|
||||
expect(preElement).toContainElement(codeElement)
|
||||
expect(codeElement.textContent).toBe('console.log("hello world")')
|
||||
})
|
||||
|
||||
it('contains the copy button span for CSS targeting', () => {
|
||||
const { container } = render(
|
||||
<PreCode>
|
||||
<code>test content</code>
|
||||
</PreCode>,
|
||||
)
|
||||
|
||||
const copySpan = container.querySelector('.copy-code-button')
|
||||
expect(copySpan).toBeInTheDocument()
|
||||
expect(copySpan?.tagName).toBe('SPAN')
|
||||
})
|
||||
|
||||
it('renders as a <pre> element', () => {
|
||||
const { container } = render(<PreCode>Content</PreCode>)
|
||||
expect(container.querySelector('pre')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('handles multiple children correctly', () => {
|
||||
render(
|
||||
<PreCode>
|
||||
<span>Line 1</span>
|
||||
<span>Line 2</span>
|
||||
</PreCode>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('Line 1')).toBeInTheDocument()
|
||||
expect(screen.getByText('Line 2')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('correctly instantiates the pre element node', () => {
|
||||
const { container } = render(<PreCode>Ref check</PreCode>)
|
||||
const pre = container.querySelector('pre')
|
||||
|
||||
// Verifies the node is an actual HTMLPreElement,
|
||||
// confirming the ref-linked element rendered correctly.
|
||||
expect(pre).toBeInstanceOf(HTMLPreElement)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user