@@ -1038,6 +1038,137 @@ describe('popup helpers', () => {
10381038 } ) ;
10391039} ) ;
10401040
1041+ describe ( 'showPopup sigma theme settings' , ( ) => {
1042+ beforeEach ( ( ) => {
1043+ document . body . innerHTML = `
1044+ <div class="overlay" id="overlay" style="display:none"></div>
1045+ <div class="popup" id="popup-classGraph" style="display:none">
1046+ <div id="graph-container-classGraph"></div>
1047+ </div>` ;
1048+ } ) ;
1049+
1050+ afterEach ( ( ) => {
1051+ delete window . graphlibDot ;
1052+ delete window . graphology ;
1053+ delete window . Sigma ;
1054+ delete window . matchMedia ;
1055+ } ) ;
1056+
1057+ // Installs the theme radios (which replace document.body.innerHTML) and
1058+ // re-appends the popup DOM they wiped, so showPopup finds its overlay.
1059+ function installRadiosAndPopup ( mode , osPrefersDark ) {
1060+ installThemeRadios ( mode , osPrefersDark ) ;
1061+ document . body . insertAdjacentHTML ( 'beforeend' , `
1062+ <div class="overlay" id="overlay" style="display:none"></div>
1063+ <div class="popup" id="popup-classGraph" style="display:none">
1064+ <div id="graph-container-classGraph"></div>
1065+ </div>` ) ;
1066+ }
1067+
1068+ // Minimal widget stubs so showPopup reaches the Sigma constructor: an
1069+ // empty graphlib-dot graph (the sugiyama layout then no-ops) and a Sigma
1070+ // "constructor" recording its arguments.
1071+ function stubSigmaStack ( ) {
1072+ const constructed = [ ] ;
1073+ window . graphlibDot = { read : ( ) => ( { nodes : ( ) => [ ] , edges : ( ) => [ ] } ) } ;
1074+ window . graphology = {
1075+ Graph : class {
1076+ addNode ( ) { }
1077+ addEdge ( ) { }
1078+ nodes ( ) { return [ ] ; }
1079+ forEachNeighbor ( ) { }
1080+ setNodeAttribute ( ) { }
1081+ }
1082+ } ;
1083+ window . Sigma = function ( graph , container , settings ) {
1084+ constructed . push ( { graph, container, settings } ) ;
1085+ } ;
1086+ return constructed ;
1087+ }
1088+
1089+ it ( 'keeps the sigma defaults in light mode' , ( ) => {
1090+ installRadiosAndPopup ( 'light' ) ;
1091+ const constructed = stubSigmaStack ( ) ;
1092+ showPopup ( 'popup-classGraph' , 'graph-container-classGraph' , 'strict digraph G {}' ) ;
1093+ expect ( constructed . length ) . toBe ( 1 ) ;
1094+ expect ( constructed [ 0 ] . settings ) . toBeUndefined ( ) ;
1095+ } ) ;
1096+
1097+ it ( 'passes the dark legend text color and a hover renderer in dark mode' , ( ) => {
1098+ installRadiosAndPopup ( 'dark' ) ;
1099+ const constructed = stubSigmaStack ( ) ;
1100+ showPopup ( 'popup-classGraph' , 'graph-container-classGraph' , 'strict digraph G {}' ) ;
1101+ const settings = constructed [ 0 ] . settings ;
1102+ expect ( settings . labelColor . color ) . toBe ( '#9fb0c0' ) ;
1103+ expect ( typeof settings . hoverRenderer ) . toBe ( 'function' ) ;
1104+ } ) ;
1105+
1106+ it ( 'resolves the dark settings for the system radio when the OS prefers dark' , ( ) => {
1107+ installRadiosAndPopup ( 'system' , true ) ;
1108+ const constructed = stubSigmaStack ( ) ;
1109+ showPopup ( 'popup-classGraph' , 'graph-container-classGraph' , 'strict digraph G {}' ) ;
1110+ expect ( constructed [ 0 ] . settings . labelColor . color ) . toBe ( '#9fb0c0' ) ;
1111+ } ) ;
1112+
1113+ it ( 'draws the hover box in the dark popup surface with light label text' , ( ) => {
1114+ installRadiosAndPopup ( 'dark' ) ;
1115+ const constructed = stubSigmaStack ( ) ;
1116+ showPopup ( 'popup-classGraph' , 'graph-container-classGraph' , 'strict digraph G {}' ) ;
1117+ // Representative resolved settings, as sigma's resolveSettings merges the
1118+ // popup partial over its DEFAULT_SETTINGS (labelSize 14, Arial, normal).
1119+ const resolved = {
1120+ labelSize : 14 ,
1121+ labelFont : 'Arial' ,
1122+ labelWeight : 'normal' ,
1123+ ...constructed [ 0 ] . settings
1124+ } ;
1125+
1126+ const fills = [ ] ;
1127+ const drawn = [ ] ;
1128+ const coords = [ ] ;
1129+ const context = {
1130+ font : '' ,
1131+ shadowOffsetX : 0 , shadowOffsetY : 0 , shadowBlur : 0 , shadowColor : '' ,
1132+ measureText : ( ) => ( { width : 42 } ) ,
1133+ beginPath ( ) { } , closePath ( ) { } ,
1134+ moveTo : ( ...args ) => coords . push ( ...args ) ,
1135+ lineTo : ( ...args ) => coords . push ( ...args ) ,
1136+ arc : ( ...args ) => coords . push ( ...args ) ,
1137+ fill ( ) { drawn . push ( { op : 'fill' , color : fills [ fills . length - 1 ] } ) ; } ,
1138+ fillText ( text , ...args ) {
1139+ drawn . push ( { op : 'text' , text, color : fills [ fills . length - 1 ] } ) ;
1140+ coords . push ( ...args ) ;
1141+ } ,
1142+ set fillStyle ( value ) { fills . push ( value ) ; } ,
1143+ get fillStyle ( ) { return fills [ fills . length - 1 ] ; }
1144+ } ;
1145+ resolved . hoverRenderer ( context , { x : 10 , y : 20 , size : 5 , label : 'Foo' } , resolved ) ;
1146+ // The label-box geometry must be finite everywhere (NaN coordinates would
1147+ // silently skip painting the box and the label).
1148+ expect ( coords . length ) . toBeGreaterThan ( 0 ) ;
1149+ expect ( coords . every ( Number . isFinite ) ) . toBe ( true ) ;
1150+ // The label font comes from the resolved label settings.
1151+ expect ( context . font ) . toBe ( 'normal 14px Arial' ) ;
1152+ // The hovered node's label box fills with the dark --card-bg surface…
1153+ expect ( drawn . some ( d => d . op === 'fill' && d . color === '#1a2330' ) ) . toBe ( true ) ;
1154+ // …and its text uses the dark chart legend color (>= 4.5:1 on it).
1155+ expect ( drawn . some ( d => d . op === 'text' && d . text === 'Foo' && d . color === '#9fb0c0' ) ) . toBe ( true ) ;
1156+ } ) ;
1157+
1158+ it ( 'keeps the dark sigma label readable on the dark hover box (>= 4.5:1)' , ( ) => {
1159+ // #1a2330 is the dark palette's --card-bg (the popup surface) in
1160+ // app/globals.css; #9fb0c0 is CHART_LEGEND_TEXT.dark.
1161+ expect ( contrastAgainst ( '#9fb0c0' , [ 0x1a , 0x23 , 0x30 ] ) ) . toBeGreaterThanOrEqual ( 4.5 ) ;
1162+ } ) ;
1163+
1164+ it ( 'falls back to sigma defaults for the system radio when the OS prefers light' , ( ) => {
1165+ installRadiosAndPopup ( 'system' , false ) ;
1166+ const constructed = stubSigmaStack ( ) ;
1167+ showPopup ( 'popup-classGraph' , 'graph-container-classGraph' , 'strict digraph G {}' ) ;
1168+ expect ( constructed [ 0 ] . settings ) . toBeUndefined ( ) ;
1169+ } ) ;
1170+ } ) ;
1171+
10411172describe ( 'bindPopupHandlers (template uses data attributes, no inline handlers)' , ( ) => {
10421173 beforeEach ( ( ) => {
10431174 document . body . innerHTML = `
0 commit comments