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 ;
@@ -949,4 +950,107 @@ describe('useShikiHighlighter Hook', () => {
949950 Date . now = originalDateNow ;
950951 } ) ;
951952 } ) ;
953+
954+ describe ( 'Stable Options' , ( ) => {
955+ test ( 'returns same reference for identical object content' , ( ) => {
956+ const { result, rerender } = renderHook (
957+ ( { options } ) => useStableOptions ( options ) ,
958+ { initialProps : { options : { delay : 100 } } }
959+ ) ;
960+
961+ const firstRef = result . current ;
962+
963+ // Rerender with new object, same content
964+ rerender ( { options : { delay : 100 } } ) ;
965+ expect ( result . current ) . toBe ( firstRef ) ;
966+
967+ // Rerender with different content
968+ rerender ( { options : { delay : 200 } } ) ;
969+ expect ( result . current ) . not . toBe ( firstRef ) ;
970+ expect ( result . current ) . toEqual ( { delay : 200 } ) ;
971+ } ) ;
972+
973+ test ( 'returns same reference for identical nested objects' , ( ) => {
974+ const { result, rerender } = renderHook (
975+ ( { options } ) => useStableOptions ( options ) ,
976+ {
977+ initialProps : {
978+ options : {
979+ themes : { light : 'github-light' , dark : 'github-dark' } ,
980+ } ,
981+ } ,
982+ }
983+ ) ;
984+
985+ const firstRef = result . current ;
986+
987+ // Rerender with new object, same nested content
988+ rerender ( {
989+ options : {
990+ themes : { light : 'github-light' , dark : 'github-dark' } ,
991+ } ,
992+ } ) ;
993+ expect ( result . current ) . toBe ( firstRef ) ;
994+ } ) ;
995+
996+ test ( 'handles primitive values correctly' , ( ) => {
997+ const { result, rerender } = renderHook (
998+ ( { value } ) => useStableOptions ( value ) ,
999+ { initialProps : { value : 'typescript' } }
1000+ ) ;
1001+
1002+ expect ( result . current ) . toBe ( 'typescript' ) ;
1003+
1004+ rerender ( { value : 'typescript' } ) ;
1005+ expect ( result . current ) . toBe ( 'typescript' ) ;
1006+
1007+ rerender ( { value : 'javascript' } ) ;
1008+ expect ( result . current ) . toBe ( 'javascript' ) ;
1009+ } ) ;
1010+
1011+ test ( 'hook does not re-highlight when options object is recreated with same content' , async ( ) => {
1012+ let highlightCount = 0 ;
1013+
1014+ const CountingComponent = ( { options } : { options : object } ) => {
1015+ const highlighted = useShikiHighlighter (
1016+ 'const x = 1;' ,
1017+ 'javascript' ,
1018+ 'github-dark' ,
1019+ options
1020+ ) ;
1021+
1022+ // Count when we get a new result (indicates highlight ran)
1023+ const prevRef = useRef ( highlighted ) ;
1024+ if ( highlighted !== null && highlighted !== prevRef . current ) {
1025+ highlightCount ++ ;
1026+ prevRef . current = highlighted ;
1027+ }
1028+
1029+ return < div > { highlighted } </ div > ;
1030+ } ;
1031+
1032+ const { rerender } = render (
1033+ < CountingComponent options = { { delay : undefined } } />
1034+ ) ;
1035+
1036+ // Wait for initial highlight
1037+ await waitFor ( ( ) => {
1038+ expect ( highlightCount ) . toBe ( 1 ) ;
1039+ } ) ;
1040+
1041+ // Rerender with new object, same content - should NOT re-highlight
1042+ rerender ( < CountingComponent options = { { delay : undefined } } /> ) ;
1043+
1044+ // Small wait to ensure no additional highlights triggered
1045+ await new Promise ( ( r ) => setTimeout ( r , 50 ) ) ;
1046+ expect ( highlightCount ) . toBe ( 1 ) ;
1047+
1048+ // Rerender with different content - SHOULD re-highlight
1049+ rerender ( < CountingComponent options = { { showLineNumbers : true } } /> ) ;
1050+
1051+ await waitFor ( ( ) => {
1052+ expect ( highlightCount ) . toBe ( 2 ) ;
1053+ } ) ;
1054+ } ) ;
1055+ } ) ;
9521056} ) ;
0 commit comments