1717 * under the License.
1818 */
1919
20- import { ChangeDetectorRef , SimpleChange , SimpleChanges } from "@angular/core" ;
21- import { DomSanitizer , SafeHtml } from "@angular/platform-browser" ;
20+ import { SimpleChange , SimpleChanges } from "@angular/core" ;
21+ import { ComponentFixture , TestBed } from "@angular/core/testing" ;
22+ import { DomSanitizer } from "@angular/platform-browser" ;
23+ import { HttpClientTestingModule } from "@angular/common/http/testing" ;
2224import { of } from "rxjs" ;
2325import { AgentInteractionComponent } from "./agent-interaction.component" ;
2426import { AgentService , OperatorResultMode , SampleRow } from "../../../service/agent/agent.service" ;
2527import { WorkflowActionService } from "../../../service/workflow-graph/model/workflow-action.service" ;
2628import { NotificationService } from "../../../../common/service/notification/notification.service" ;
29+ import { commonTestProviders } from "../../../../common/testing/test-utils" ;
2730
28- /**
29- * These tests exercise the component's pure presentation logic (visualization
30- * caching, column/row derivation) by constructing it directly with lightweight
31- * stubbed dependencies, so no Angular template/DI bootstrapping is required.
32- */
3331describe ( "AgentInteractionComponent" , ( ) => {
32+ let fixture : ComponentFixture < AgentInteractionComponent > ;
3433 let component : AgentInteractionComponent ;
34+ let sanitizer : DomSanitizer ;
3535
36- const agentService = {
36+ const agentServiceStub = {
3737 getAllAgents : ( ) => of ( [ ] ) ,
3838 agentChange$ : of ( null ) ,
3939 getActivelyConnectedAgentIds : ( ) => [ ] ,
4040 isAgentActivelyConnected : ( ) => false ,
4141 sendMessage : ( ) => { } ,
4242 } as unknown as AgentService ;
4343
44- const workflowActionService = { } as unknown as WorkflowActionService ;
45- const notificationService = { } as unknown as NotificationService ;
46- const changeDetectorRef = { detectChanges : ( ) => { } } as unknown as ChangeDetectorRef ;
47- // Echo the html back so we can assert the cached value flows through.
48- const sanitizer = {
49- bypassSecurityTrustHtml : ( html : string ) => html as unknown as SafeHtml ,
50- } as unknown as DomSanitizer ;
51-
5244 function row ( rowIndex : number , tuple : Record < string , any > ) : SampleRow {
5345 return { rowIndex, tuple } ;
5446 }
5547
48+ beforeEach ( async ( ) => {
49+ await TestBed . configureTestingModule ( {
50+ imports : [ AgentInteractionComponent , HttpClientTestingModule ] ,
51+ providers : [
52+ { provide : AgentService , useValue : agentServiceStub } ,
53+ { provide : WorkflowActionService , useValue : { } } ,
54+ { provide : NotificationService , useValue : { error : vi . fn ( ) , success : vi . fn ( ) } } ,
55+ ...commonTestProviders ,
56+ ] ,
57+ } ) . compileComponents ( ) ;
58+ } ) ;
59+
5660 beforeEach ( ( ) => {
57- component = new AgentInteractionComponent (
58- agentService ,
59- workflowActionService ,
60- notificationService ,
61- changeDetectorRef ,
62- sanitizer
63- ) ;
61+ fixture = TestBed . createComponent ( AgentInteractionComponent ) ;
62+ component = fixture . componentInstance ;
63+ sanitizer = TestBed . inject ( DomSanitizer ) ;
64+ fixture . componentRef . setInput ( "operatorId" , "op-1" ) ;
65+ fixture . detectChanges ( ) ;
6466 } ) ;
6567
6668 it ( "should create" , ( ) => {
6769 expect ( component ) . toBeTruthy ( ) ;
6870 } ) ;
6971
72+ describe ( "template rendering" , ( ) => {
73+ it ( "renders the sample table with a leading Row column when sample rows are present" , ( ) => {
74+ component . sampleTuples = [ row ( 0 , { a : 1 , b : "x" } ) , row ( 1 , { a : 2 , b : "y" } ) ] ;
75+ component . resultMode = OperatorResultMode . TABLE ;
76+ fixture . detectChanges ( ) ;
77+
78+ const headers : string [ ] = Array . from (
79+ fixture . nativeElement . querySelectorAll ( ".sample-records-table thead th" ) as NodeListOf < HTMLElement >
80+ ) . map ( th => th . textContent ?. trim ( ) ?? "" ) ;
81+ expect ( headers ) . toEqual ( [ "Row" , "a" , "b" ] ) ;
82+
83+ const firstDataRowCells : string [ ] = Array . from (
84+ fixture . nativeElement . querySelectorAll (
85+ ".sample-records-table tbody tr:first-child td"
86+ ) as NodeListOf < HTMLElement >
87+ ) . map ( td => td . textContent ?. trim ( ) ?? "" ) ;
88+ expect ( firstDataRowCells ) . toEqual ( [ "0" , "1" , "x" ] ) ;
89+ } ) ;
90+
91+ it ( "renders an ellipsis row spanning all columns plus the Row column when indices have a gap" , ( ) => {
92+ component . sampleTuples = [ row ( 0 , { a : 1 , b : "x" } ) , row ( 5 , { a : 2 , b : "y" } ) ] ;
93+ component . resultMode = OperatorResultMode . TABLE ;
94+ fixture . detectChanges ( ) ;
95+
96+ const ellipsisCell = fixture . nativeElement . querySelector ( ".ellipsis-row td" ) as HTMLElement ;
97+ expect ( ellipsisCell ) . toBeTruthy ( ) ;
98+ expect ( ellipsisCell . textContent ?. trim ( ) ) . toEqual ( "..." ) ;
99+ // 2 tuple columns + 1 leading "Row" column
100+ expect ( ellipsisCell . getAttribute ( "colspan" ) ) . toEqual ( "3" ) ;
101+ } ) ;
102+
103+ it ( "renders the visualization iframe instead of the table in visualization mode" , ( ) => {
104+ fixture . componentRef . setInput ( "sampleTuples" , [ row ( 0 , { "html-content" : "<h1>chart</h1>" } ) ] ) ;
105+ fixture . componentRef . setInput ( "resultMode" , OperatorResultMode . VISUALIZATION ) ;
106+ fixture . detectChanges ( ) ;
107+
108+ expect ( fixture . nativeElement . querySelector ( ".visualization-iframe" ) ) . toBeTruthy ( ) ;
109+ expect ( fixture . nativeElement . querySelector ( ".sample-records-table" ) ) . toBeNull ( ) ;
110+ } ) ;
111+
112+ it ( "renders neither the table nor the iframe when there are no sample rows" , ( ) => {
113+ expect ( fixture . nativeElement . querySelector ( ".sample-records-table" ) ) . toBeNull ( ) ;
114+ expect ( fixture . nativeElement . querySelector ( ".visualization-iframe" ) ) . toBeNull ( ) ;
115+ } ) ;
116+ } ) ;
117+
70118 describe ( "ngOnChanges - visualization html caching" , ( ) => {
71119 it ( "caches sanitized html when a visualization tuple carries html-content" , ( ) => {
72120 component . sampleTuples = [ row ( 0 , { "html-content" : "<h1>chart</h1>" } ) ] ;
73121 component . ngOnChanges ( { sampleTuples : new SimpleChange ( undefined , component . sampleTuples , true ) } ) ;
74122
75- expect ( component . getVisualizationHtml ( ) ) . toEqual ( "<h1>chart</h1>" as unknown as SafeHtml ) ;
123+ expect ( component . getVisualizationHtml ( ) ) . toEqual ( sanitizer . bypassSecurityTrustHtml ( "<h1>chart</h1>" ) ) ;
76124 } ) ;
77125
78126 it ( "keeps the cached html when the content is unchanged across calls" , ( ) => {
@@ -82,7 +130,7 @@ describe("AgentInteractionComponent", () => {
82130 component . ngOnChanges ( changes ) ;
83131 component . ngOnChanges ( changes ) ; // identical html -> unchanged branch, cache reused
84132
85- expect ( component . getVisualizationHtml ( ) ) . toEqual ( "<p>same</p>" as unknown as SafeHtml ) ;
133+ expect ( component . getVisualizationHtml ( ) ) . toEqual ( sanitizer . bypassSecurityTrustHtml ( "<p>same</p>" ) ) ;
86134 } ) ;
87135
88136 it ( "clears the cached html when no html-content is present" , ( ) => {
@@ -92,12 +140,12 @@ describe("AgentInteractionComponent", () => {
92140 component . sampleTuples = [ row ( 0 , { value : 1 } ) ] ;
93141 component . ngOnChanges ( { sampleTuples : new SimpleChange ( undefined , component . sampleTuples , false ) } ) ;
94142
95- expect ( component . getVisualizationHtml ( ) ) . toEqual ( "" as unknown as SafeHtml ) ;
143+ expect ( component . getVisualizationHtml ( ) ) . toEqual ( sanitizer . bypassSecurityTrustHtml ( "" ) ) ;
96144 } ) ;
97145
98146 it ( "ignores changes unrelated to sampleTuples/resultMode" , ( ) => {
99147 component . ngOnChanges ( { operatorId : new SimpleChange ( undefined , "op-1" , true ) } ) ;
100- expect ( component . getVisualizationHtml ( ) ) . toEqual ( "" as unknown as SafeHtml ) ;
148+ expect ( component . getVisualizationHtml ( ) ) . toEqual ( sanitizer . bypassSecurityTrustHtml ( "" ) ) ;
101149 } ) ;
102150 } ) ;
103151
0 commit comments