Skip to content

Commit 392f4de

Browse files
bobbai00claude
andcommitted
refactor(frontend): reuse WorkflowFatalError and render agent-interaction in tests
Import the existing WorkflowFatalError from workflow-websocket.interface instead of redeclaring it in agent.service.ts, and rewrite the agent-interaction spec onto a TestBed fixture with detectChanges() so the changed template is actually rendered and covered. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent a645ef2 commit 392f4de

2 files changed

Lines changed: 76 additions & 45 deletions

File tree

frontend/src/app/workspace/component/agent/agent-interaction/agent-interaction.component.spec.ts

Lines changed: 75 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -17,62 +17,110 @@
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";
2224
import { of } from "rxjs";
2325
import { AgentInteractionComponent } from "./agent-interaction.component";
2426
import { AgentService, OperatorResultMode, SampleRow } from "../../../service/agent/agent.service";
2527
import { WorkflowActionService } from "../../../service/workflow-graph/model/workflow-action.service";
2628
import { 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-
*/
3331
describe("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

frontend/src/app/workspace/service/agent/agent.service.ts

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import { AppSettings } from "../../../common/app-setting";
4040
import { AgentState, ReActStep, ModelMessage } from "./agent-types";
4141
import { Workflow, WorkflowContent } from "../../../common/type/workflow";
4242
import { ComputingUnitStatusService } from "../../../common/service/computing-unit/computing-unit-status/computing-unit-status.service";
43+
import { WorkflowFatalError } from "../../types/workflow-websocket.interface";
4344

4445
/**
4546
* Agent settings for API (serializable format).
@@ -96,24 +97,6 @@ export interface ModelType {
9697
/**
9798
* API response types
9899
*/
99-
/**
100-
* Per-operator execution summary as sent by the agent-service over the
101-
* operator-results endpoint (mirror of its OperatorExecutionSummary).
102-
*/
103-
export interface WorkflowFatalError {
104-
type: { name: WorkflowFatalErrorType };
105-
timestamp: { seconds: number; nanos: number };
106-
message: string;
107-
details: string;
108-
operatorId: string;
109-
workerId: string;
110-
}
111-
112-
export enum WorkflowFatalErrorType {
113-
COMPILATION_ERROR = "COMPILATION_ERROR",
114-
EXECUTION_FAILURE = "EXECUTION_FAILURE",
115-
}
116-
117100
export interface ConsoleMessage {
118101
msgType: ConsoleMessageType;
119102
title: string;

0 commit comments

Comments
 (0)