@@ -12,9 +12,10 @@ import {
1212 formatRerenders ,
1313 formatTimeline ,
1414 formatCommitDetail ,
15+ formatChangedKeys ,
1516} from '../formatters.js' ;
1617import type { TreeNode } from '../component-tree.js' ;
17- import type { InspectedElement , StatusInfo , ComponentRenderReport , ConnectionHealth } from '../types.js' ;
18+ import type { InspectedElement , StatusInfo , ComponentRenderReport , ConnectionHealth , ChangedKeys } from '../types.js' ;
1819import type { ProfileSummary , TimelineEntry , CommitDetail } from '../profiler.js' ;
1920
2021describe ( 'formatTree' , ( ) => {
@@ -252,7 +253,7 @@ describe('formatProfileSummary', () => {
252253} ) ;
253254
254255describe ( 'formatProfileReport' , ( ) => {
255- it ( 'should format a render report with type tag ' , ( ) => {
256+ it ( 'should format a render report with changed keys ' , ( ) => {
256257 const report : ComponentRenderReport = {
257258 id : 5 ,
258259 displayName : 'UserProfile' ,
@@ -263,6 +264,7 @@ describe('formatProfileReport', () => {
263264 avgDuration : 45 ,
264265 maxDuration : 120 ,
265266 causes : [ 'props-changed' , 'state-changed' ] ,
267+ changedKeys : { props : [ 'userId' , 'theme' ] , state : [ 'isEditing' ] , hooks : [ ] } ,
266268 } ;
267269
268270 const result = formatProfileReport ( report ) ;
@@ -271,6 +273,23 @@ describe('formatProfileReport', () => {
271273 expect ( result ) . toContain ( 'avg:45.0ms' ) ;
272274 expect ( result ) . toContain ( 'max:120.0ms' ) ;
273275 expect ( result ) . toContain ( 'props-changed' ) ;
276+ expect ( result ) . toContain ( 'changed: props: userId, theme state: isEditing' ) ;
277+ } ) ;
278+
279+ it ( 'should omit changed line when keys are empty' , ( ) => {
280+ const report : ComponentRenderReport = {
281+ id : 5 ,
282+ displayName : 'UserProfile' ,
283+ renderCount : 1 ,
284+ totalDuration : 10 ,
285+ avgDuration : 10 ,
286+ maxDuration : 10 ,
287+ causes : [ 'first-mount' ] ,
288+ changedKeys : { props : [ ] , state : [ ] , hooks : [ ] } ,
289+ } ;
290+
291+ const result = formatProfileReport ( report , '@c5' ) ;
292+ expect ( result ) . not . toContain ( 'changed:' ) ;
274293 } ) ;
275294
276295 it ( 'should prefer explicit label param over report.label' , ( ) => {
@@ -296,31 +315,42 @@ describe('formatSlowest', () => {
296315 expect ( formatSlowest ( [ ] ) ) . toContain ( 'No profiling data' ) ;
297316 } ) ;
298317
299- it ( 'should format slowest components with labels and all causes ' , ( ) => {
318+ it ( 'should format slowest components with labels and changed keys ' , ( ) => {
300319 const reports : ComponentRenderReport [ ] = [
301- { id : 1 , displayName : 'SlowComp' , label : '@c1' , type : 'function' , renderCount : 5 , totalDuration : 250 , avgDuration : 50 , maxDuration : 100 , causes : [ 'props-changed' , 'state-changed' ] } ,
302- { id : 2 , displayName : 'FastComp' , label : '@c2' , type : 'memo' , renderCount : 10 , totalDuration : 100 , avgDuration : 10 , maxDuration : 20 , causes : [ 'state-changed' ] } ,
320+ { id : 1 , displayName : 'SlowComp' , label : '@c1' , type : 'function' , renderCount : 5 , totalDuration : 250 , avgDuration : 50 , maxDuration : 100 , causes : [ 'props-changed' , 'state-changed' ] , changedKeys : { props : [ 'data' ] , state : [ 'count' ] , hooks : [ ] } } ,
321+ { id : 2 , displayName : 'FastComp' , label : '@c2' , type : 'memo' , renderCount : 10 , totalDuration : 100 , avgDuration : 10 , maxDuration : 20 , causes : [ 'state-changed' ] , changedKeys : { props : [ ] , state : [ 'count' ] , hooks : [ ] } } ,
303322 ] ;
304323
305324 const result = formatSlowest ( reports ) ;
306325 expect ( result ) . toContain ( 'Slowest' ) ;
307326 expect ( result ) . toContain ( '@c1 [fn] SlowComp' ) ;
308327 expect ( result ) . toContain ( '@c2 [memo] FastComp' ) ;
309328 expect ( result ) . toContain ( 'causes:props-changed, state-changed' ) ;
310- expect ( result ) . toContain ( 'causes:state-changed' ) ;
329+ expect ( result ) . toContain ( 'changed: props: data state: count' ) ;
330+ expect ( result ) . toContain ( 'changed: state: count' ) ;
311331 } ) ;
312332} ) ;
313333
314334describe ( 'formatRerenders' , ( ) => {
315- it ( 'should format rerender data with labels and all causes ' , ( ) => {
335+ it ( 'should format rerender data with labels and changed keys ' , ( ) => {
316336 const reports : ComponentRenderReport [ ] = [
317- { id : 1 , displayName : 'Chatty' , label : '@c1' , type : 'function' , renderCount : 50 , totalDuration : 100 , avgDuration : 2 , maxDuration : 5 , causes : [ 'parent-rendered' , 'props-changed' ] } ,
337+ { id : 1 , displayName : 'Chatty' , label : '@c1' , type : 'function' , renderCount : 50 , totalDuration : 100 , avgDuration : 2 , maxDuration : 5 , causes : [ 'parent-rendered' , 'props-changed' ] , changedKeys : { props : [ 'value' ] , state : [ ] , hooks : [ ] } } ,
318338 ] ;
319339
320340 const result = formatRerenders ( reports ) ;
321341 expect ( result ) . toContain ( '50 renders' ) ;
322342 expect ( result ) . toContain ( '@c1 [fn] Chatty' ) ;
323343 expect ( result ) . toContain ( 'causes:parent-rendered, props-changed' ) ;
344+ expect ( result ) . toContain ( 'changed: props: value' ) ;
345+ } ) ;
346+
347+ it ( 'should omit changed line when keys are empty' , ( ) => {
348+ const reports : ComponentRenderReport [ ] = [
349+ { id : 1 , displayName : 'Chatty' , label : '@c1' , type : 'function' , renderCount : 50 , totalDuration : 100 , avgDuration : 2 , maxDuration : 5 , causes : [ 'parent-rendered' ] , changedKeys : { props : [ ] , state : [ ] , hooks : [ ] } } ,
350+ ] ;
351+
352+ const result = formatRerenders ( reports ) ;
353+ expect ( result ) . not . toContain ( 'changed:' ) ;
324354 } ) ;
325355} ) ;
326356
@@ -340,14 +370,14 @@ describe('formatTimeline', () => {
340370} ) ;
341371
342372describe ( 'formatCommitDetail' , ( ) => {
343- it ( 'should format commit detail with labels and types ' , ( ) => {
373+ it ( 'should format commit detail with labels, types, and changed keys ' , ( ) => {
344374 const detail : CommitDetail = {
345375 index : 0 ,
346376 timestamp : 1000 ,
347377 duration : 15.5 ,
348378 components : [
349- { id : 1 , displayName : 'App' , label : '@c1' , type : 'function' , actualDuration : 15.5 , selfDuration : 5.2 , causes : [ 'state-changed' ] } ,
350- { id : 2 , displayName : 'Header' , label : '@c2' , type : 'memo' , actualDuration : 10.3 , selfDuration : 10.3 , causes : [ 'props-changed' , 'hooks-changed' ] } ,
379+ { id : 1 , displayName : 'App' , label : '@c1' , type : 'function' , actualDuration : 15.5 , selfDuration : 5.2 , causes : [ 'state-changed' ] , changedKeys : { props : [ ] , state : [ 'count' ] , hooks : [ ] } } ,
380+ { id : 2 , displayName : 'Header' , label : '@c2' , type : 'memo' , actualDuration : 10.3 , selfDuration : 10.3 , causes : [ 'props-changed' , 'hooks-changed' ] , changedKeys : { props : [ 'onClick' , 'className' ] , state : [ ] , hooks : [ 0 ] } } ,
351381 ] ,
352382 totalComponents : 2 ,
353383 } ;
@@ -360,8 +390,10 @@ describe('formatCommitDetail', () => {
360390 expect ( result ) . toContain ( 'self:5.2ms' ) ;
361391 expect ( result ) . toContain ( 'total:15.5ms' ) ;
362392 expect ( result ) . toContain ( 'causes:state-changed' ) ;
393+ expect ( result ) . toContain ( 'changed: state: count' ) ;
363394 expect ( result ) . toContain ( '@c2 [memo] Header' ) ;
364395 expect ( result ) . toContain ( 'causes:props-changed, hooks-changed' ) ;
396+ expect ( result ) . toContain ( 'changed: props: onClick, className hooks: #0' ) ;
365397 } ) ;
366398
367399 it ( 'should show hidden count' , ( ) => {
@@ -370,12 +402,45 @@ describe('formatCommitDetail', () => {
370402 timestamp : 2000 ,
371403 duration : 10 ,
372404 components : [
373- { id : 1 , displayName : 'App' , label : '@c1' , type : 'function' , actualDuration : 10 , selfDuration : 10 , causes : [ ] } ,
405+ { id : 1 , displayName : 'App' , label : '@c1' , type : 'function' , actualDuration : 10 , selfDuration : 10 , causes : [ ] , changedKeys : { props : [ ] , state : [ ] , hooks : [ ] } } ,
374406 ] ,
375407 totalComponents : 5 ,
376408 } ;
377409
378410 const result = formatCommitDetail ( detail ) ;
379411 expect ( result ) . toContain ( '... 4 more' ) ;
380412 } ) ;
413+
414+ it ( 'should omit changed keys when empty' , ( ) => {
415+ const detail : CommitDetail = {
416+ index : 0 ,
417+ timestamp : 1000 ,
418+ duration : 5 ,
419+ components : [
420+ { id : 1 , displayName : 'App' , label : '@c1' , type : 'function' , actualDuration : 5 , selfDuration : 5 , causes : [ 'first-mount' ] , changedKeys : { props : [ ] , state : [ ] , hooks : [ ] } } ,
421+ ] ,
422+ totalComponents : 1 ,
423+ } ;
424+
425+ const result = formatCommitDetail ( detail ) ;
426+ expect ( result ) . toContain ( 'App' ) ;
427+ expect ( result ) . not . toContain ( 'changed:' ) ;
428+ } ) ;
429+ } ) ;
430+
431+ describe ( 'formatChangedKeys' , ( ) => {
432+ it ( 'should format all key categories' , ( ) => {
433+ const keys : ChangedKeys = { props : [ 'onClick' , 'className' ] , state : [ 'count' ] , hooks : [ 0 , 3 ] } ;
434+ expect ( formatChangedKeys ( keys ) ) . toBe ( 'props: onClick, className state: count hooks: #0, #3' ) ;
435+ } ) ;
436+
437+ it ( 'should return empty string when no keys' , ( ) => {
438+ const keys : ChangedKeys = { props : [ ] , state : [ ] , hooks : [ ] } ;
439+ expect ( formatChangedKeys ( keys ) ) . toBe ( '' ) ;
440+ } ) ;
441+
442+ it ( 'should omit empty categories' , ( ) => {
443+ const keys : ChangedKeys = { props : [ 'theme' ] , state : [ ] , hooks : [ ] } ;
444+ expect ( formatChangedKeys ( keys ) ) . toBe ( 'props: theme' ) ;
445+ } ) ;
381446} ) ;
0 commit comments