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