11import { render , renderHook , waitFor } from '@testing-library/react' ;
2+ import { useRef } from 'react' ;
23import { vi } from 'vitest' ;
34import {
45 useShikiHighlighter ,
@@ -13,7 +14,7 @@ import type {
1314 TokensResult ,
1415} from '../src/lib/types' ;
1516import type { ShikiTransformer } from 'shiki' ;
16- import { throttleHighlighting } from '../src/lib/utils' ;
17+ import { throttleHighlighting , useStableOptions } from '../src/lib/utils' ;
1718
1819interface TestComponentProps {
1920 code : string ;
@@ -933,4 +934,107 @@ describe('useShikiHighlighter Hook', () => {
933934 Date . now = originalDateNow ;
934935 } ) ;
935936 } ) ;
937+
938+ describe ( 'Stable Options' , ( ) => {
939+ test ( 'returns same reference for identical object content' , ( ) => {
940+ const { result, rerender } = renderHook (
941+ ( { options } ) => useStableOptions ( options ) ,
942+ { initialProps : { options : { delay : 100 } } }
943+ ) ;
944+
945+ const firstRef = result . current ;
946+
947+ // Rerender with new object, same content
948+ rerender ( { options : { delay : 100 } } ) ;
949+ expect ( result . current ) . toBe ( firstRef ) ;
950+
951+ // Rerender with different content
952+ rerender ( { options : { delay : 200 } } ) ;
953+ expect ( result . current ) . not . toBe ( firstRef ) ;
954+ expect ( result . current ) . toEqual ( { delay : 200 } ) ;
955+ } ) ;
956+
957+ test ( 'returns same reference for identical nested objects' , ( ) => {
958+ const { result, rerender } = renderHook (
959+ ( { options } ) => useStableOptions ( options ) ,
960+ {
961+ initialProps : {
962+ options : {
963+ themes : { light : 'github-light' , dark : 'github-dark' } ,
964+ } ,
965+ } ,
966+ }
967+ ) ;
968+
969+ const firstRef = result . current ;
970+
971+ // Rerender with new object, same nested content
972+ rerender ( {
973+ options : {
974+ themes : { light : 'github-light' , dark : 'github-dark' } ,
975+ } ,
976+ } ) ;
977+ expect ( result . current ) . toBe ( firstRef ) ;
978+ } ) ;
979+
980+ test ( 'handles primitive values correctly' , ( ) => {
981+ const { result, rerender } = renderHook (
982+ ( { value } ) => useStableOptions ( value ) ,
983+ { initialProps : { value : 'typescript' } }
984+ ) ;
985+
986+ expect ( result . current ) . toBe ( 'typescript' ) ;
987+
988+ rerender ( { value : 'typescript' } ) ;
989+ expect ( result . current ) . toBe ( 'typescript' ) ;
990+
991+ rerender ( { value : 'javascript' } ) ;
992+ expect ( result . current ) . toBe ( 'javascript' ) ;
993+ } ) ;
994+
995+ test ( 'hook does not re-highlight when options object is recreated with same content' , async ( ) => {
996+ let highlightCount = 0 ;
997+
998+ const CountingComponent = ( { options } : { options : object } ) => {
999+ const highlighted = useShikiHighlighter (
1000+ 'const x = 1;' ,
1001+ 'javascript' ,
1002+ 'github-dark' ,
1003+ options
1004+ ) ;
1005+
1006+ // Count when we get a new result (indicates highlight ran)
1007+ const prevRef = useRef ( highlighted ) ;
1008+ if ( highlighted !== null && highlighted !== prevRef . current ) {
1009+ highlightCount ++ ;
1010+ prevRef . current = highlighted ;
1011+ }
1012+
1013+ return < div > { highlighted } </ div > ;
1014+ } ;
1015+
1016+ const { rerender } = render (
1017+ < CountingComponent options = { { delay : undefined } } />
1018+ ) ;
1019+
1020+ // Wait for initial highlight
1021+ await waitFor ( ( ) => {
1022+ expect ( highlightCount ) . toBe ( 1 ) ;
1023+ } ) ;
1024+
1025+ // Rerender with new object, same content - should NOT re-highlight
1026+ rerender ( < CountingComponent options = { { delay : undefined } } /> ) ;
1027+
1028+ // Small wait to ensure no additional highlights triggered
1029+ await new Promise ( ( r ) => setTimeout ( r , 50 ) ) ;
1030+ expect ( highlightCount ) . toBe ( 1 ) ;
1031+
1032+ // Rerender with different content - SHOULD re-highlight
1033+ rerender ( < CountingComponent options = { { showLineNumbers : true } } /> ) ;
1034+
1035+ await waitFor ( ( ) => {
1036+ expect ( highlightCount ) . toBe ( 2 ) ;
1037+ } ) ;
1038+ } ) ;
1039+ } ) ;
9361040} ) ;
0 commit comments