@@ -190,6 +190,68 @@ describe('MarkdownRenderer', () => {
190190 } ) ;
191191 } ) ;
192192
193+ describe ( 'Math rendering' , ( ) => {
194+ it ( 'renders display math blocks via KaTeX' , ( ) => {
195+ const { container } = render (
196+ < MarkdownRenderer content = { '$$x = \\frac{-b}{2a}$$' } /> ,
197+ ) ;
198+ expect ( container . querySelector ( '.katex' ) ) . not . toBeNull ( ) ;
199+ expect ( container . textContent ) . not . toContain ( '$$' ) ;
200+ } ) ;
201+
202+ it ( 'renders inline math via KaTeX' , ( ) => {
203+ const { container } = render (
204+ < MarkdownRenderer content = { 'Energy is $E = mc^2$ at rest' } /> ,
205+ ) ;
206+ expect ( container . querySelector ( '.katex' ) ) . not . toBeNull ( ) ;
207+ expect ( container . textContent ) . not . toContain ( '$E = mc^2$' ) ;
208+ } ) ;
209+
210+ it ( 'does not render raw LaTeX source as plain text' , ( ) => {
211+ const { container } = render (
212+ < MarkdownRenderer content = { '$$\\sum_{i=0}^{n} i$$' } /> ,
213+ ) ;
214+ expect ( container . querySelector ( '.katex' ) ) . not . toBeNull ( ) ;
215+ // MathML <annotation> always contains the raw LaTeX source for
216+ // accessibility; check only the visible katex-html portion.
217+ const katexHtml = container . querySelector ( '.katex-html' ) ;
218+ expect ( katexHtml ) . not . toBeNull ( ) ;
219+ expect ( katexHtml ! . textContent ) . not . toContain ( '\\sum' ) ;
220+ } ) ;
221+
222+ it ( 'preserves XSS protection alongside math rendering' , ( ) => {
223+ const { container } = render (
224+ < MarkdownRenderer
225+ content = { '$E = mc^2$\n\n<script>alert("xss")</script>' }
226+ /> ,
227+ ) ;
228+ expect ( container . querySelector ( '.katex' ) ) . not . toBeNull ( ) ;
229+ expect ( container . querySelector ( 'script' ) ) . toBeNull ( ) ;
230+ expect ( container . innerHTML ) . not . toContain ( '<script' ) ;
231+ } ) ;
232+
233+ it ( 'blocks javascript: URLs in LaTeX \\href via trust:false' , ( ) => {
234+ const { container } = render (
235+ < MarkdownRenderer content = { '$\\href{javascript:alert(1)}{click}$' } /> ,
236+ ) ;
237+ // trust:false renders \href as a red error span, not a clickable link.
238+ // MathML <annotation> contains raw source as non-executable text;
239+ // check only the visible katex-html portion for any live href.
240+ const katexHtml = container . querySelector ( '.katex-html' ) ;
241+ expect ( katexHtml ) . not . toBeNull ( ) ;
242+ expect ( katexHtml ! . innerHTML ) . not . toMatch ( / j a v a s c r i p t : / i) ;
243+ expect ( container . querySelector ( 'a[href]' ) ) . toBeNull ( ) ;
244+ } ) ;
245+
246+ it ( 'renders math in streaming mode without leaking raw LaTeX' , ( ) => {
247+ const { container } = render (
248+ < MarkdownRenderer content = { '$$E = mc^2$$' } isStreaming = { true } /> ,
249+ ) ;
250+ expect ( container . querySelector ( '.katex' ) ) . not . toBeNull ( ) ;
251+ expect ( container . textContent ) . not . toContain ( '$$' ) ;
252+ } ) ;
253+ } ) ;
254+
193255 describe ( 'Edge cases' , ( ) => {
194256 it ( 'handles empty string' , ( ) => {
195257 const { container } = render ( < MarkdownRenderer content = "" /> ) ;
0 commit comments