11import { render , renderHook , waitFor } from '@testing-library/react' ;
2+ import { useRef } from 'react' ;
23import { vi } from 'vitest' ;
34import {
45 useShikiHighlighter ,
@@ -11,7 +12,7 @@ import type {
1112 TokensResult ,
1213} from '../src/lib/types' ;
1314import type { ShikiTransformer } from 'shiki' ;
14- import { throttleHighlighting } from '../src/lib/utils' ;
15+ import { throttleHighlighting , useStableOptions } from '../src/lib/utils' ;
1516
1617interface TestComponentProps {
1718 code : string ;
@@ -617,4 +618,107 @@ describe('useShikiHighlighter Hook', () => {
617618 Date . now = originalDateNow ;
618619 } ) ;
619620 } ) ;
621+
622+ describe ( 'Stable Options' , ( ) => {
623+ test ( 'returns same reference for identical object content' , ( ) => {
624+ const { result, rerender } = renderHook (
625+ ( { options } ) => useStableOptions ( options ) ,
626+ { initialProps : { options : { delay : 100 } } }
627+ ) ;
628+
629+ const firstRef = result . current ;
630+
631+ // Rerender with new object, same content
632+ rerender ( { options : { delay : 100 } } ) ;
633+ expect ( result . current ) . toBe ( firstRef ) ;
634+
635+ // Rerender with different content
636+ rerender ( { options : { delay : 200 } } ) ;
637+ expect ( result . current ) . not . toBe ( firstRef ) ;
638+ expect ( result . current ) . toEqual ( { delay : 200 } ) ;
639+ } ) ;
640+
641+ test ( 'returns same reference for identical nested objects' , ( ) => {
642+ const { result, rerender } = renderHook (
643+ ( { options } ) => useStableOptions ( options ) ,
644+ {
645+ initialProps : {
646+ options : {
647+ themes : { light : 'github-light' , dark : 'github-dark' } ,
648+ } ,
649+ } ,
650+ }
651+ ) ;
652+
653+ const firstRef = result . current ;
654+
655+ // Rerender with new object, same nested content
656+ rerender ( {
657+ options : {
658+ themes : { light : 'github-light' , dark : 'github-dark' } ,
659+ } ,
660+ } ) ;
661+ expect ( result . current ) . toBe ( firstRef ) ;
662+ } ) ;
663+
664+ test ( 'handles primitive values correctly' , ( ) => {
665+ const { result, rerender } = renderHook (
666+ ( { value } ) => useStableOptions ( value ) ,
667+ { initialProps : { value : 'typescript' } }
668+ ) ;
669+
670+ expect ( result . current ) . toBe ( 'typescript' ) ;
671+
672+ rerender ( { value : 'typescript' } ) ;
673+ expect ( result . current ) . toBe ( 'typescript' ) ;
674+
675+ rerender ( { value : 'javascript' } ) ;
676+ expect ( result . current ) . toBe ( 'javascript' ) ;
677+ } ) ;
678+
679+ test ( 'hook does not re-highlight when options object is recreated with same content' , async ( ) => {
680+ let highlightCount = 0 ;
681+
682+ const CountingComponent = ( { options } : { options : object } ) => {
683+ const highlighted = useShikiHighlighter (
684+ 'const x = 1;' ,
685+ 'javascript' ,
686+ 'github-dark' ,
687+ options
688+ ) ;
689+
690+ // Count when we get a new result (indicates highlight ran)
691+ const prevRef = useRef ( highlighted ) ;
692+ if ( highlighted !== null && highlighted !== prevRef . current ) {
693+ highlightCount ++ ;
694+ prevRef . current = highlighted ;
695+ }
696+
697+ return < div > { highlighted } </ div > ;
698+ } ;
699+
700+ const { rerender } = render (
701+ < CountingComponent options = { { delay : undefined } } />
702+ ) ;
703+
704+ // Wait for initial highlight
705+ await waitFor ( ( ) => {
706+ expect ( highlightCount ) . toBe ( 1 ) ;
707+ } ) ;
708+
709+ // Rerender with new object, same content - should NOT re-highlight
710+ rerender ( < CountingComponent options = { { delay : undefined } } /> ) ;
711+
712+ // Small wait to ensure no additional highlights triggered
713+ await new Promise ( ( r ) => setTimeout ( r , 50 ) ) ;
714+ expect ( highlightCount ) . toBe ( 1 ) ;
715+
716+ // Rerender with different content - SHOULD re-highlight
717+ rerender ( < CountingComponent options = { { showLineNumbers : true } } /> ) ;
718+
719+ await waitFor ( ( ) => {
720+ expect ( highlightCount ) . toBe ( 2 ) ;
721+ } ) ;
722+ } ) ;
723+ } ) ;
620724} ) ;
0 commit comments