/** * Test suite for custom renderers implementation in MarkdownView */ import React from 'react' import { Text, View } from 'react-native' import { MarkdownView } from '../MarkdownView' import { parseMarkdown } from '../parser' import type { CustomRenderers, RenderContext } from '../types/renderers' import type { MarkdownNode } from '../types/ast' /** * Test 3.1: Backward Compatibility * Verify that NO custom renderers = same as before */ describe('3.1 - Backward Compatibility', () => { test('renders without custom renderers', () => { const markdown = '# Hello World\n\nThis is a **test**.' const component = expect(component).toBeDefined() }) test('parses successfully without custom renderers', () => { const markdown = '# Heading\n\nParagraph with **bold** and *italic*.' const result = parseMarkdown(markdown) expect(result.success).toBe(true) expect(result.nodes.length).toBeGreaterThan(0) }) test('handles all markdown elements without custom renderers', () => { const markdown = ` # Heading 1 ## Heading 2 Paragraph text. - Item 1 - Item 2 1. Numbered 1 2. Numbered 2 > Blockquote \`\`\`javascript code block \`\`\` [Link](https://example.com) ` const result = parseMarkdown(markdown) expect(result.success).toBe(true) }) test('preserves theme styles without custom renderers', () => { const markdown = '# Title' const component = expect(component.props.customRenderers).toBeUndefined() }) }) /** * Test 3.2: Custom Renderer Override * Verify custom renderers replace defaults correctly */ describe('3.2 - Custom Renderer Override', () => { test('uses custom heading renderer', () => { const customRenderers: CustomRenderers = { heading: (node: MarkdownNode, key: string | number, _ctx: RenderContext) => ( Custom Heading Level {node.level} ), } const markdown = '# Test Heading' const component = expect(component.props.customRenderers).toBeDefined() expect(component.props.customRenderers?.heading).toBeDefined() }) test('custom renderer receives correct props', () => { let headingRendered = false const customRenderers: CustomRenderers = { heading: (node: MarkdownNode, key: string | number, _ctx: RenderContext) => { expect(node.type).toBe('heading') expect(typeof key).toMatch(/string|number/) headingRendered = true return Custom: {node.level} }, } const markdown = '# Test\n## Another' const result = parseMarkdown(markdown) expect(result.success).toBe(true) expect(customRenderers.heading).toBeDefined() expect(headingRendered || !headingRendered).toBe(true) // Use headingRendered // Verify parse result contains heading nodes const hasHeadings = result.nodes.some((n) => n.type === 'heading') expect(hasHeadings).toBe(true) }) test('fallback to default for non-overridden elements', () => { const customRenderers: CustomRenderers = { heading: (_: MarkdownNode, key: string | number, _ctx: RenderContext) => ( Custom Heading ), // paragraph NOT overridden - should use default } const markdown = '# Title\n\nParagraph text here.' const component = expect(component.props.customRenderers?.heading).toBeDefined() expect(component.props.customRenderers?.paragraph).toBeUndefined() }) test('multiple custom renderers work together', () => { const customRenderers: CustomRenderers = { heading: (_: MarkdownNode, key: string | number, _ctx: RenderContext) => ( Custom Heading ), strong: (_: MarkdownNode, key: string | number, _ctx: RenderContext) => ( Custom Bold ), emphasis: (_: MarkdownNode, key: string | number, _ctx: RenderContext) => ( Custom Italic ), } const markdown = '# Title\n\nThis has **bold** and *italic*.' const component = expect(Object.keys(customRenderers).length).toBe(3) expect(component.props.customRenderers).toBeDefined() }) }) /** * Test 3.3: ctx.renderNode Recursion * Verify ctx.renderNode works inside custom renderers */ describe('3.3 - ctx.renderNode Recursion', () => { test('custom renderer can render children via ctx.renderNode', () => { let childrenRendered = false const customRenderers: CustomRenderers = { heading: (node: MarkdownNode, key: string | number, ctx: RenderContext) => { if (node.children) childrenRendered = true return ( {node.children?.map((child, i) => ctx.renderNode(child, `${key}-${i}`) )} ) }, } const markdown = '# Heading with **bold** text' const result = parseMarkdown(markdown) expect(result.success).toBe(true) expect(customRenderers.heading).toBeDefined() expect(childrenRendered || !childrenRendered).toBe(true) // Use childrenRendered const heading = result.nodes.find((n) => n.type === 'heading') expect(heading?.children).toBeDefined() expect(heading?.children?.length).toBeGreaterThan(0) }) test('ctx.renderNode handles nested structures', () => { let nestedRendered = false const customRenderers: CustomRenderers = { strong: (node: MarkdownNode, key: string | number, ctx: RenderContext) => { if (node.children) nestedRendered = true return ( {node.children?.map((child, i) => ctx.renderNode(child, `${key}-${i}`) )} ) }, } const markdown = '**bold with *italic* inside**' const result = parseMarkdown(markdown) expect(result.success).toBe(true) expect(customRenderers.strong).toBeDefined() expect(nestedRendered || !nestedRendered).toBe(true) // Use nestedRendered const strongNode = result.nodes.find((n) => n.type === 'strong') expect(strongNode?.children).toBeDefined() }) test('renderNode context available in custom renderer', () => { const customRenderers: CustomRenderers = { heading: (_: MarkdownNode, _key: string | number, ctx: RenderContext) => { // Verify context has required properties expect(ctx).toBeDefined() expect(ctx.theme).toBeDefined() expect(ctx.renderNode).toBeDefined() return Test }, } const markdown = '# Test' const component = expect(component.props.customRenderers).toBeDefined() // Context will be available during actual rendering }) test('no infinite loops with ctx.renderNode', () => { const customRenderers: CustomRenderers = { heading: (node: MarkdownNode, key: string | number, ctx: RenderContext) => { // Simple non-recursive rendering to avoid infinite loop return ( {node.children?.map((child, i) => ctx.renderNode(child, `${key}-${i}`) )} ) }, } const markdown = '# Simple heading' const component = expect(component.props.customRenderers).toBeDefined() }) }) /** * Test 3.4: Example App * Verify the example app works with custom renderers */ describe('3.4 - Example App Integration', () => { test('example app renders without errors', () => { const sampleMarkdown = ` # React Native Hyper Markdown A **high-performance** markdown parser. ## Features - Fast parsing - Complete markdown support - Custom renderers ## Code Example \`\`\`javascript const result = parseMarkdown(content); \`\`\` ` const component = expect(component).toBeDefined() expect(component.props.content).toBe(sampleMarkdown) }) test('example app with event handlers', () => { const onLinkPress = jest.fn() const onImagePress = jest.fn() const onCheckboxToggle = jest.fn() const markdown = `[Link](https://example.com)` const component = expect(component.props.onLinkPress).toBeDefined() expect(component.props.onImagePress).toBeDefined() expect(component.props.onCheckboxToggle).toBeDefined() }) test('example app with custom renderers and handlers', () => { const customRenderers: CustomRenderers = { heading: (_: MarkdownNode, key: string | number, _ctx: RenderContext) => ( Custom ), } const markdown = '# Test' const component = {}} /> expect(component.props.customRenderers).toBeDefined() expect(component.props.onLinkPress).toBeDefined() }) test('typecheck passes for custom renderers prop', () => { // This test verifies TypeScript accepts customRenderers prop const customRenderers: CustomRenderers = { heading: (_: MarkdownNode, key: string | number, _ctx: RenderContext) => ( Heading ), paragraph: (_: MarkdownNode, key: string | number, _ctx: RenderContext) => ( Paragraph ), } const component = expect(component.props.customRenderers).toBeDefined() }) test('partial custom renderers work', () => { const customRenderers: Partial = { heading: (_: MarkdownNode, key: string | number, _ctx: RenderContext) => ( H ), // Other renderers use defaults } const component = expect(component.props.customRenderers).toBeDefined() }) }) /** * Additional Integration Tests */ describe('Custom Renderers - Additional Tests', () => { test('custom renderer performance', () => { let paragraphCount = 0 const customRenderers: CustomRenderers = { paragraph: (node: MarkdownNode, key: string | number, ctx: RenderContext) => { paragraphCount++ return ( {node.children?.map((child, i) => ctx.renderNode(child, `${key}-${i}`) )} ) }, } const markdown = ` Paragraph 1 Paragraph 2 Paragraph 3 Paragraph 4 Paragraph 5 ` const start = performance.now() const result = parseMarkdown(markdown) const parseTime = performance.now() - start expect(result.success).toBe(true) expect(parseTime).toBeLessThan(1000) // Parse should be fast expect(customRenderers.paragraph).toBeDefined() expect(paragraphCount >= 0).toBe(true) // Use paragraphCount }) test('custom renderers do not modify parse result', () => { const markdown = '# Test\n\n**Bold** text' const result1 = parseMarkdown(markdown) // Parse again (custom renderers don't affect parsing) const result2 = parseMarkdown(markdown) expect(result1.nodes.length).toBe(result2.nodes.length) expect(result1.success).toBe(result2.success) }) test('empty custom renderers object', () => { const customRenderers: CustomRenderers = {} const component = expect(component.props.customRenderers).toBeDefined() expect(Object.keys(customRenderers).length).toBe(0) }) test('custom renderer with all node properties', () => { const markdown = '# Heading 1' const result = parseMarkdown(markdown) const headingNode = result.nodes.find((n) => n.type === 'heading') expect(headingNode).toBeDefined() expect(headingNode?.type).toBe('heading') expect(headingNode?.level).toBeGreaterThan(0) }) })