@@ -29,8 +29,14 @@ import type VisorVtkSceneNode from '../state/appstate/vtkInfo/VisorVtkSceneNode.
2929 * delivery, after the tree has synchronized, so a delivered range reaches
3030 * the legend with no selection change.
3131 *
32- * Expected values are hand-written literals. The trigger sender is a double
33- * that absorbs every send; nothing here asserts on it.
32+ * 4. The range trigger is sent once per edit, carrying the server's
33+ * component convention, and never for a delivery. A delivered range
34+ * that differs from the one a part last applied reaches that part with
35+ * no send at all.
36+ *
37+ * Expected values are hand-written literals. The trigger-sender double
38+ * absorbs every send except in the range-trigger tests, which assert its
39+ * calls after clearing it and the renderer double at the end of their setup.
3440 *
3541 * jsdom has no `ResizeObserver` and jest here runs with no `setupFiles`, so
3642 * the stub below is this module's own.
@@ -47,6 +53,7 @@ const PART_A_ID = 1;
4753const PART_B_ID = 2 ;
4854const PRESSURE_ID = 'POINT::pressure::1' ;
4955const TEMPERATURE_ID = 'POINT::temperature::1' ;
56+ const VELOCITY_ID = 'POINT::velocity::2' ;
5057
5158/** A complete wire record for `pressure`, custom range [2, 8] inside default [0, 10]. */
5259function pressureRecord ( overrides : Record < string , unknown > = { } ) : Record < string , unknown > {
@@ -79,6 +86,27 @@ function temperatureRecord(): Record<string, unknown> {
7986 } ;
8087}
8188
89+ /** A complete wire record for two-component `velocity`, listing part A. */
90+ function velocityRecord ( ) : Record < string , unknown > {
91+ return {
92+ id : VELOCITY_ID ,
93+ arrayName : 'velocity' ,
94+ type : 'POINT' ,
95+ numComponents : 2 ,
96+ partIds : [ PART_A_ID ] ,
97+ defaultMagnitudeRange : [ 0 , 10 ] ,
98+ defaultRanges : [
99+ [ - 1 , 1 ] ,
100+ [ - 2 , 2 ] ,
101+ ] ,
102+ magnitudeRange : [ 0 , 10 ] ,
103+ ranges : [
104+ [ - 1 , 1 ] ,
105+ [ - 2 , 2 ] ,
106+ ] ,
107+ } ;
108+ }
109+
82110function makeSceneGraphNode ( ) {
83111 return {
84112 id : 0 ,
@@ -165,14 +193,21 @@ function makeRendererDouble() {
165193
166194type RendererDouble = ReturnType < typeof makeRendererDouble > ;
167195
168- function makeFrontend ( ) : { frontend : VisorFrontend ; renderer : RendererDouble } {
196+ type TriggerSenderDouble = jest . Mock < Promise < unknown > , [ string , unknown ] > ;
197+
198+ function makeFrontend ( ) : {
199+ frontend : VisorFrontend ;
200+ renderer : RendererDouble ;
201+ triggerSender : TriggerSenderDouble ;
202+ } {
169203 const renderer = makeRendererDouble ( ) ;
204+ const triggerSender = jest . fn ( async ( ) => undefined ) as unknown as TriggerSenderDouble ;
170205 const frontend = new VisorFrontend (
171206 renderer as unknown as IRenderer ,
172207 makeSceneGraphNode ( ) as unknown as VisorVtkSceneNode ,
173- jest . fn ( async ( ) => undefined )
208+ triggerSender
174209 ) ;
175- return { frontend, renderer } ;
210+ return { frontend, renderer, triggerSender } ;
176211}
177212
178213/** The tree-view util members the frontend and the mounted panel reach. */
@@ -430,49 +465,90 @@ describe('setAppStateAsync holds a delivered variable block', () => {
430465 } ) ;
431466} ) ;
432467
433- describe ( 'the legend follows a delivery with no selection change' , ( ) => {
434- /**
435- * A real frontend holding `pressure` at [2, 8], with part A coloured by
436- * its magnitude and selected in the tree, and the top-right panel mounted
437- * on it.
438- */
439- async function mountOnDeliveredRecord ( ) : Promise < {
440- frontend : VisorFrontend ;
441- container : HTMLElement ;
442- } > {
443- const { frontend } = makeFrontend ( ) ;
444- const part = frontend . sceneGraph . descendantActorNodesOrSelfDictionary [ PART_A_ID ] ;
445- frontend . setTreeViewUtil ( makeTreeViewUtilDouble ( [ part ] ) ) ;
446- await frontend . setAppStateAsync (
447- { scene : { variableStates : { [ PRESSURE_ID ] : pressureRecord ( ) } } } ,
448- false
449- ) ;
450- await part . setColorVariableAsync ( PRESSURE_ID , - 1 ) ;
468+ /**
469+ * A real frontend holding one delivered record, with part A coloured by the
470+ * given slot and selected in the tree, and the top-right panel mounted on it.
471+ */
472+ async function mountOnDeliveredRecord (
473+ record : Record < string , unknown > ,
474+ variableId : string ,
475+ component : number
476+ ) : Promise < {
477+ frontend : VisorFrontend ;
478+ container : HTMLElement ;
479+ triggerSender : TriggerSenderDouble ;
480+ } > {
481+ const { frontend, triggerSender } = makeFrontend ( ) ;
482+ const part = frontend . sceneGraph . descendantActorNodesOrSelfDictionary [ PART_A_ID ] ;
483+ frontend . setTreeViewUtil ( makeTreeViewUtilDouble ( [ part ] ) ) ;
484+ await frontend . setAppStateAsync (
485+ { scene : { variableStates : { [ variableId ] : record } } } ,
486+ false
487+ ) ;
488+ await part . setColorVariableAsync ( variableId , component ) ;
451489
452- let container : HTMLElement = null ! ;
453- await act ( async ( ) => {
454- container = render (
455- < Panel_TopRight visorState = { frontend } onLoad = { ( ) => { } } />
456- ) . container ;
457- } ) ;
458- await frontend . panelTopRightUtilPromise ;
459- return { frontend, container } ;
460- }
461-
462- /**
463- * The legend's min or max row, located by its heading: the ids in this
464- * component are `randomId()`-generated.
465- */
466- function legendRow ( container : HTMLElement , heading : 'Min' | 'Max' ) {
467- const label = within ( container ) . getByText ( heading , { selector : 'div' } ) . closest ( 'label' ) ! ;
468- return {
469- input : label . querySelector ( 'input' ) as HTMLInputElement ,
470- reset : label . querySelector ( 'a' ) as HTMLAnchorElement ,
471- } ;
472- }
490+ let container : HTMLElement = null ! ;
491+ await act ( async ( ) => {
492+ container = render (
493+ < Panel_TopRight visorState = { frontend } onLoad = { ( ) => { } } />
494+ ) . container ;
495+ } ) ;
496+ await frontend . panelTopRightUtilPromise ;
497+ return { frontend, container, triggerSender } ;
498+ }
499+
500+ /**
501+ * The legend's min or max row, located by its heading: the ids in this
502+ * component are `randomId()`-generated.
503+ */
504+ function legendRow ( container : HTMLElement , heading : 'Min' | 'Max' ) {
505+ const label = within ( container ) . getByText ( heading , { selector : 'div' } ) . closest ( 'label' ) ! ;
506+ return {
507+ input : label . querySelector ( 'input' ) as HTMLInputElement ,
508+ reset : label . querySelector ( 'a' ) as HTMLAnchorElement ,
509+ } ;
510+ }
511+
512+ /** Types a range into the legend and clicks Apply, then lets its handler settle. */
513+ async function applyRange ( container : HTMLElement , min : string , max : string ) : Promise < void > {
514+ legendRow ( container , 'Min' ) . input . value = min ;
515+ legendRow ( container , 'Max' ) . input . value = max ;
516+ const apply = within ( container ) . getByText ( 'Apply' , { selector : 'button' } ) ;
517+ await act ( async ( ) => {
518+ fireEvent . click ( apply ) ;
519+ await new Promise ( ( resolve ) => setTimeout ( resolve , 0 ) ) ;
520+ } ) ;
521+ }
473522
523+ /** The `set_variable_range` sends among a trigger-sender double's calls. */
524+ function rangeSends ( triggerSender : TriggerSenderDouble ) : unknown [ ] [ ] {
525+ return triggerSender . mock . calls . filter ( ( [ name ] ) => name === 'set_variable_range' ) ;
526+ }
527+
528+ /** A delivery holding one `pressure` record and colouring part A by its magnitude. */
529+ function deliveryColouringPartA ( record : Record < string , unknown > ) {
530+ return {
531+ scene : {
532+ variableStates : { [ PRESSURE_ID ] : record } ,
533+ datasetStates : {
534+ '0' : {
535+ id : '0' ,
536+ partStates : {
537+ '1' : { id : '1' , variableId : PRESSURE_ID , variableComponent : - 1 } ,
538+ } ,
539+ } ,
540+ } ,
541+ } ,
542+ } ;
543+ }
544+
545+ describe ( 'the legend follows a delivery with no selection change' , ( ) => {
474546 test ( 'a delivered custom range is shown in the legend' , async ( ) => {
475- const { frontend, container } = await mountOnDeliveredRecord ( ) ;
547+ const { frontend, container } = await mountOnDeliveredRecord (
548+ pressureRecord ( ) ,
549+ PRESSURE_ID ,
550+ - 1
551+ ) ;
476552 expect ( legendRow ( container , 'Min' ) . input . value ) . toBe ( '2' ) ;
477553 expect ( legendRow ( container , 'Max' ) . input . value ) . toBe ( '8' ) ;
478554
@@ -494,7 +570,11 @@ describe('the legend follows a delivery with no selection change', () => {
494570 } ) ;
495571
496572 test ( 'reset shows the delivered default range' , async ( ) => {
497- const { frontend, container } = await mountOnDeliveredRecord ( ) ;
573+ const { frontend, container } = await mountOnDeliveredRecord (
574+ pressureRecord ( ) ,
575+ PRESSURE_ID ,
576+ - 1
577+ ) ;
498578
499579 await act ( async ( ) => {
500580 await frontend . setAppStateAsync (
@@ -516,3 +596,82 @@ describe('the legend follows a delivery with no selection change', () => {
516596 expect ( minRow . input . value ) . toBe ( '-1' ) ;
517597 } ) ;
518598} ) ;
599+
600+ describe ( 'the range trigger is sent for an edit and never for a delivery' , ( ) => {
601+ test ( 'an apply click on the magnitude sends one set_variable_range with component -1' , async ( ) => {
602+ const { container, triggerSender } = await mountOnDeliveredRecord (
603+ pressureRecord ( ) ,
604+ PRESSURE_ID ,
605+ - 1
606+ ) ;
607+ triggerSender . mockClear ( ) ;
608+
609+ await applyRange ( container , '3' , '7' ) ;
610+
611+ expect ( triggerSender . mock . calls ) . toEqual ( [
612+ [
613+ 'set_variable_range' ,
614+ { variableId : 'POINT::pressure::1' , component : - 1 , min : 3 , max : 7 } ,
615+ ] ,
616+ ] ) ;
617+ } ) ;
618+
619+ test ( 'an apply click on component 1 sends component 1' , async ( ) => {
620+ const { container, triggerSender } = await mountOnDeliveredRecord (
621+ velocityRecord ( ) ,
622+ VELOCITY_ID ,
623+ 1
624+ ) ;
625+ triggerSender . mockClear ( ) ;
626+
627+ await applyRange ( container , '-1' , '1' ) ;
628+
629+ expect ( triggerSender . mock . calls ) . toEqual ( [
630+ [
631+ 'set_variable_range' ,
632+ { variableId : 'POINT::velocity::2' , component : 1 , min : - 1 , max : 1 } ,
633+ ] ,
634+ ] ) ;
635+ } ) ;
636+
637+ test ( 'a delivered push sends no set_variable_range' , async ( ) => {
638+ const { frontend, triggerSender } = makeFrontend ( ) ;
639+
640+ await frontend . setAppStateAsync ( deliveryColouringPartA ( pressureRecord ( ) ) , false ) ;
641+
642+ expect ( rangeSends ( triggerSender ) ) . toEqual ( [ ] ) ;
643+ } ) ;
644+
645+ test ( 'a changed delivered range reaches a part already coloured by that slot, sending nothing' , async ( ) => {
646+ const { frontend, renderer, triggerSender } = makeFrontend ( ) ;
647+ await frontend . setAppStateAsync ( deliveryColouringPartA ( pressureRecord ( ) ) , false ) ;
648+ renderer . setScalarRangeAsync . mockClear ( ) ;
649+ renderer . sendPartColorVariableAsync . mockClear ( ) ;
650+ triggerSender . mockClear ( ) ;
651+
652+ // Magnitude moves to [3, 7]; component 0 stays at [2, 8].
653+ await frontend . setAppStateAsync (
654+ deliveryColouringPartA ( pressureRecord ( { magnitudeRange : [ 3 , 7 ] } ) ) ,
655+ false
656+ ) ;
657+
658+ expect ( renderer . setScalarRangeAsync . mock . calls ) . toEqual ( [ [ 1 , 3 , 7 ] ] ) ;
659+ expect ( renderer . sendPartColorVariableAsync ) . not . toHaveBeenCalled ( ) ;
660+ expect ( rangeSends ( triggerSender ) ) . toEqual ( [ ] ) ;
661+ } ) ;
662+
663+ test ( 'a delivered range differing from the last edit reaches a part already coloured by that slot' , async ( ) => {
664+ const { frontend, renderer, triggerSender } = makeFrontend ( ) ;
665+ await frontend . setAppStateAsync ( deliveryColouringPartA ( pressureRecord ( ) ) , false ) ;
666+ await frontend . setVariableRangeAsync ( PRESSURE_ID , - 1 , 3 , 7 ) ;
667+ renderer . setScalarRangeAsync . mockClear ( ) ;
668+ renderer . sendPartColorVariableAsync . mockClear ( ) ;
669+ triggerSender . mockClear ( ) ;
670+
671+ await frontend . setAppStateAsync ( deliveryColouringPartA ( pressureRecord ( ) ) , false ) ;
672+
673+ expect ( renderer . setScalarRangeAsync . mock . calls ) . toEqual ( [ [ 1 , 2 , 8 ] ] ) ;
674+ expect ( renderer . sendPartColorVariableAsync ) . not . toHaveBeenCalled ( ) ;
675+ expect ( rangeSends ( triggerSender ) ) . toEqual ( [ ] ) ;
676+ } ) ;
677+ } ) ;
0 commit comments