From a8fa3ff3cf57a3dda166be89f540c91ec8a4ddc3 Mon Sep 17 00:00:00 2001 From: Joel Date: Tue, 10 Mar 2026 18:30:09 +0800 Subject: [PATCH] chore: add tests --- .../markdown-with-directive/index.spec.tsx | 180 ++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 web/app/components/base/markdown-with-directive/index.spec.tsx diff --git a/web/app/components/base/markdown-with-directive/index.spec.tsx b/web/app/components/base/markdown-with-directive/index.spec.tsx new file mode 100644 index 0000000000..8725a8080f --- /dev/null +++ b/web/app/components/base/markdown-with-directive/index.spec.tsx @@ -0,0 +1,180 @@ +import { render, screen } from '@testing-library/react' +import DOMPurify from 'dompurify' +import { validateDirectiveProps } from './components/markdown-with-directive-schema' +import WithIconCardItem from './components/with-icon-card-item' +import WithIconCardList from './components/with-icon-card-list' +import { MarkdownWithDirective } from './index' + +vi.mock('next/image', () => ({ + default: (props: React.ImgHTMLAttributes) => , +})) + +describe('markdown-with-directive', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + // Validate directive prop schemas and error paths. + describe('Directive schema validation', () => { + it('should return true when withiconcardlist props are valid', () => { + expect(validateDirectiveProps('withiconcardlist', { className: 'custom-list' })).toBe(true) + }) + + it('should return true when withiconcarditem props are valid', () => { + expect(validateDirectiveProps('withiconcarditem', { icon: 'https://example.com/icon.png' })).toBe(true) + }) + + it('should return false and log when directive name is unknown', () => { + const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + + const isValid = validateDirectiveProps('unknown-directive', { className: 'custom-list' }) + + expect(isValid).toBe(false) + expect(consoleErrorSpy).toHaveBeenCalledWith( + '[markdown-with-directive] Unknown directive name.', + expect.objectContaining({ + attributes: { className: 'custom-list' }, + directive: 'unknown-directive', + }), + ) + }) + + it('should return false and log when withiconcarditem icon is not http/https', () => { + const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + + const isValid = validateDirectiveProps('withiconcarditem', { icon: 'ftp://example.com/icon.png' }) + + expect(isValid).toBe(false) + expect(consoleErrorSpy).toHaveBeenCalledWith( + '[markdown-with-directive] Invalid directive props.', + expect.objectContaining({ + attributes: { icon: 'ftp://example.com/icon.png' }, + directive: 'withiconcarditem', + issues: expect.arrayContaining([ + expect.objectContaining({ + path: 'icon', + }), + ]), + }), + ) + }) + + it('should return false when extra props are provided to strict schema', () => { + const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + + const isValid = validateDirectiveProps('withiconcardlist', { + className: 'custom-list', + extra: 'not-allowed', + }) + + expect(isValid).toBe(false) + expect(consoleErrorSpy).toHaveBeenCalledWith( + '[markdown-with-directive] Invalid directive props.', + expect.objectContaining({ + directive: 'withiconcardlist', + }), + ) + }) + }) + + // Validate WithIconCardList rendering and class merge behavior. + describe('WithIconCardList component', () => { + it('should render children and merge className with base class', () => { + const { container } = render( + + List child + , + ) + + expect(screen.getByText('List child')).toBeInTheDocument() + expect(container.firstElementChild).toHaveClass('space-y-1') + expect(container.firstElementChild).toHaveClass('custom-list-class') + }) + + it('should render base class when className is not provided', () => { + const { container } = render( + + Only base class + , + ) + + expect(screen.getByText('Only base class')).toBeInTheDocument() + expect(container.firstElementChild).toHaveClass('space-y-1') + }) + }) + + // Validate WithIconCardItem rendering and image prop forwarding. + describe('WithIconCardItem component', () => { + it('should render icon image and child content', () => { + render( + + Card item content + , + ) + + const icon = screen.getByAltText('icon') + expect(icon).toBeInTheDocument() + expect(icon).toHaveAttribute('src', 'https://example.com/icon.png') + expect(screen.getByText('Card item content')).toBeInTheDocument() + }) + }) + + // Validate markdown parsing pipeline, sanitizer usage, and invalid fallback. + describe('MarkdownWithDirective component', () => { + it('should render directives when markdown is valid', () => { + const markdown = [ + '::withiconcardlist {className="custom-list"}', + ':withiconcarditem[Card Title] {icon="https://example.com/icon.png"} {className="custom-item"}', + '::', + ].join('\n') + + const { container } = render() + + const list = container.querySelector('.custom-list') + expect(list).toBeInTheDocument() + expect(list).toHaveClass('space-y-1') + expect(screen.getByText('Card Title')).toBeInTheDocument() + expect(screen.getByAltText('icon')).toHaveAttribute('src', 'https://example.com/icon.png') + }) + + it('should replace output with invalid content when directive is unknown', () => { + const markdown = ':unknown[Bad Content]{foo="bar"}' + + render() + + expect(screen.getByText('invalid content')).toBeInTheDocument() + expect(screen.queryByText('Bad Content')).not.toBeInTheDocument() + }) + + it('should replace output with invalid content when directive props are invalid', () => { + const markdown = ':withiconcarditem[Invalid Icon]{icon="not-a-url"}' + + render() + + expect(screen.getByText('invalid content')).toBeInTheDocument() + expect(screen.queryByText('Invalid Icon')).not.toBeInTheDocument() + }) + + it('should call sanitizer and render based on sanitized markdown', () => { + const sanitizeSpy = vi.spyOn(DOMPurify, 'sanitize') + .mockReturnValue(':withiconcarditem[Sanitized]{icon="https://example.com/safe.png"}') + + render() + + expect(sanitizeSpy).toHaveBeenCalledWith('', { + ALLOWED_ATTR: [], + ALLOWED_TAGS: [], + }) + expect(screen.getByText('Sanitized')).toBeInTheDocument() + expect(screen.getByAltText('icon')).toHaveAttribute('src', 'https://example.com/safe.png') + }) + + it('should render empty output and skip sanitizer when markdown is empty', () => { + const sanitizeSpy = vi.spyOn(DOMPurify, 'sanitize') + const { container } = render() + + expect(sanitizeSpy).not.toHaveBeenCalled() + expect(container).toBeEmptyDOMElement() + }) + }) +})