This is an automated email from the ASF dual-hosted git repository.

github-merge-queue[bot] pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/texera.git


The following commit(s) were added to refs/heads/main by this push:
     new ba06ab8eed test(frontend): extend ResultPanelComponent unit test 
coverage (#6743)
ba06ab8eed is described below

commit ba06ab8eedb04fde4e975e94013ff040967daf79
Author: Xinyuan Lin <[email protected]>
AuthorDate: Wed Jul 22 20:17:05 2026 -0700

    test(frontend): extend ResultPanelComponent unit test coverage (#6743)
    
    ### What changes were proposed in this PR?
    
    Extends `result-panel.component.spec.ts` covering frame-switching /
    result-state reactions, tab and visibility toggles, and guard branches.
    Coverage 74% -> high. No existing tests modified.
    
    ### Any related issues, documentation, discussions?
    
    Closes #6736.
    
    ### How was this PR tested?
    
    `ng test --include='**/result-panel.component.spec.ts'` -> 45/45
    passing. `yarn format:ci` passes.
    
    ### Was this PR authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code (Opus 4.8 [1M context])
---
 .../result-panel/result-panel.component.spec.ts    | 383 ++++++++++++++++++++-
 1 file changed, 382 insertions(+), 1 deletion(-)

diff --git 
a/frontend/src/app/workspace/component/result-panel/result-panel.component.spec.ts
 
b/frontend/src/app/workspace/component/result-panel/result-panel.component.spec.ts
index b245494373..d4998ef95c 100644
--- 
a/frontend/src/app/workspace/component/result-panel/result-panel.component.spec.ts
+++ 
b/frontend/src/app/workspace/component/result-panel/result-panel.component.spec.ts
@@ -37,10 +37,21 @@ import { By } from "@angular/platform-browser";
 import { HttpClientTestingModule } from "@angular/common/http/testing";
 import { NzModalModule } from "ng-zorro-antd/modal";
 import { ExecutionState } from "../../types/execute-workflow.interface";
-import { mockPoint, mockResultPredicate } from 
"../../service/workflow-graph/model/mock-workflow-data";
+import {
+  mockPoint,
+  mockResultPredicate,
+  mockScanPredicate,
+} from "../../service/workflow-graph/model/mock-workflow-data";
 import { ComputingUnitStatusService } from 
"../../../common/service/computing-unit/computing-unit-status/computing-unit-status.service";
 import { MockComputingUnitStatusService } from 
"../../../common/service/computing-unit/computing-unit-status/mock-computing-unit-status.service";
 import { commonTestProviders } from "../../../common/testing/test-utils";
+import { PanelService } from "../../service/panel/panel.service";
+import { WorkflowCompilingService } from 
"../../service/compile-workflow/workflow-compiling.service";
+import { WorkflowConsoleService } from 
"../../service/workflow-console/workflow-console.service";
+import { CompilationState } from "../../types/workflow-compiling.interface";
+import { WorkflowFatalError } from "../../types/workflow-websocket.interface";
+import { PYTHON_UDF_V2_OP_TYPE } from 
"../../service/workflow-graph/model/workflow-graph";
+import { OperatorPredicate } from "../../types/workflow-common.interface";
 
 describe("ResultPanelComponent", () => {
   let component: ResultPanelComponent;
@@ -76,6 +87,29 @@ describe("ResultPanelComponent", () => {
     fixture.detectChanges();
   });
 
+  afterEach(() => {
+    // Several added tests install spies on the global `window` / shared 
service
+    // instances and drive real streams; restore everything and tear the 
fixture
+    // down so state never leaks into the next test.
+    vi.restoreAllMocks();
+    fixture.destroy();
+    // fixture.destroy() runs ngOnDestroy, which persists the panel 
dimensions/style
+    // into localStorage; those keys are read back in the constructor/ngOnInit 
and would
+    // otherwise leak across specs. Remove only the keys this component writes.
+    localStorage.removeItem("result-panel-width");
+    localStorage.removeItem("result-panel-height");
+    localStorage.removeItem("result-panel-style");
+  });
+
+  const makeFatalError = (operatorId: string): WorkflowFatalError => ({
+    message: "boom",
+    details: "detail",
+    operatorId,
+    workerId: "",
+    type: { name: "ExecutionError" },
+    timestamp: { nanos: 0, seconds: 0 },
+  });
+
   it("should create", () => expect(component).toBeTruthy());
 
   it("should show nothing by default", () => {
@@ -282,5 +316,352 @@ describe("ResultPanelComponent", () => {
 
       expect(clearSpy).not.toHaveBeenCalled();
     });
+
+    it("resets the operator title when the highlight selection is cleared", () 
=> {
+      // Nothing highlighted -> the single-highlight ternary takes its 
`undefined` branch,
+      // currentOperatorId flips to undefined and the title is wiped.
+      vi.spyOn(workflowActionService.getJointGraphWrapper(), 
"getCurrentHighlightedOperatorIDs").mockReturnValue([]);
+      component.currentOperatorId = "3";
+      component.operatorTitle = "Old Title";
+
+      component.rerenderResultPanel();
+
+      expect(component.currentOperatorId).toBeUndefined();
+      expect(component.operatorTitle).toBe("");
+    });
+
+    it("shows an error frame for the whole workflow when execution failed with 
no operator selected", () => {
+      vi.spyOn(workflowActionService.getJointGraphWrapper(), 
"getCurrentHighlightedOperatorIDs").mockReturnValue([]);
+      vi.spyOn(executeWorkflowService, "getExecutionState").mockReturnValue({
+        state: ExecutionState.Failed,
+        errorMessages: [],
+      });
+
+      component.rerenderResultPanel();
+
+      const errorConfig = component.frameComponentConfigs.get("Static Error");
+      expect(errorConfig?.component).toBe(ErrorFrameComponent);
+      expect(errorConfig?.componentInputs).toEqual({ operatorId: undefined });
+    });
+
+    it("shows an operator error frame when the failed execution has a matching 
fatal error", () => {
+      workflowActionService.addOperator(mockResultPredicate, mockPoint);
+      vi.spyOn(workflowActionService.getJointGraphWrapper(), 
"getCurrentHighlightedOperatorIDs").mockReturnValue(["3"]);
+      vi.spyOn(executeWorkflowService, "getExecutionState").mockReturnValue({
+        state: ExecutionState.Failed,
+        errorMessages: [makeFatalError("3")],
+      });
+      // getWorkflowFatalErrors reads getErrorMessages() (guarded on the real 
currentState),
+      // so it must be stubbed alongside getExecutionState.
+      vi.spyOn(executeWorkflowService, 
"getErrorMessages").mockReturnValue([makeFatalError("3")]);
+
+      component.rerenderResultPanel();
+
+      const errorConfig = component.frameComponentConfigs.get("Static Error");
+      expect(errorConfig?.component).toBe(ErrorFrameComponent);
+      expect(errorConfig?.componentInputs).toEqual({ operatorId: "3" });
+    });
+
+    it("tears down a stale error frame when the failed execution has no error 
for the selected operator", () => {
+      workflowActionService.addOperator(mockResultPredicate, mockPoint);
+      // Keep the highlight aligned with currentOperatorId so the 
clear-on-selection-change
+      // path is skipped and we can prove the delete branch (not the wholesale 
clear) fires.
+      component.currentOperatorId = "3";
+      component.frameComponentConfigs.set("Static Error", {
+        component: ErrorFrameComponent,
+        componentInputs: { operatorId: "3" },
+      });
+      vi.spyOn(workflowActionService.getJointGraphWrapper(), 
"getCurrentHighlightedOperatorIDs").mockReturnValue(["3"]);
+      vi.spyOn(executeWorkflowService, "getExecutionState").mockReturnValue({
+        state: ExecutionState.Failed,
+        errorMessages: [makeFatalError("someOtherOperator")],
+      });
+      // A real fatal error exists, but it belongs to a different operator, so 
the id filter
+      // strips it and the stale frame is removed.
+      vi.spyOn(executeWorkflowService, 
"getErrorMessages").mockReturnValue([makeFatalError("someOtherOperator")]);
+
+      component.rerenderResultPanel();
+
+      expect(component.frameComponentConfigs.has("Static Error")).toBe(false);
+    });
+
+    it("shows an operator error frame when the compilation failed with a 
matching error", () => {
+      workflowActionService.addOperator(mockResultPredicate, mockPoint);
+      const compilingService = TestBed.inject(WorkflowCompilingService);
+      vi.spyOn(workflowActionService.getJointGraphWrapper(), 
"getCurrentHighlightedOperatorIDs").mockReturnValue(["3"]);
+      vi.spyOn(compilingService, 
"getWorkflowCompilationState").mockReturnValue(CompilationState.Failed);
+      vi.spyOn(compilingService, 
"getWorkflowCompilationErrors").mockReturnValue({ "3": makeFatalError("3") });
+
+      component.rerenderResultPanel();
+
+      const errorConfig = component.frameComponentConfigs.get("Static Error");
+      expect(errorConfig?.component).toBe(ErrorFrameComponent);
+      expect(errorConfig?.componentInputs).toEqual({ operatorId: "3" });
+    });
+
+    it("displays a console frame when the selected operator has console 
messages", () => {
+      workflowActionService.addOperator(mockResultPredicate, mockPoint);
+      const consoleService = TestBed.inject(WorkflowConsoleService);
+      vi.spyOn(workflowActionService.getJointGraphWrapper(), 
"getCurrentHighlightedOperatorIDs").mockReturnValue(["3"]);
+      vi.spyOn(consoleService, "hasConsoleMessages").mockReturnValue(true);
+
+      component.rerenderResultPanel();
+
+      const consoleConfig = component.frameComponentConfigs.get("Console");
+      expect(consoleConfig?.component).toBe(ConsoleFrameComponent);
+      // A SimpleSink is not a Python UDF, so the console input stays disabled.
+      expect(consoleConfig?.componentInputs).toEqual({ operatorId: "3", 
consoleInputEnabled: false });
+    });
+
+    it("enables console input when the selected operator is a Python UDF", () 
=> {
+      const pythonOp: OperatorPredicate = {
+        ...mockResultPredicate,
+        operatorID: "3",
+        operatorType: PYTHON_UDF_V2_OP_TYPE,
+      };
+      const consoleService = TestBed.inject(WorkflowConsoleService);
+      vi.spyOn(workflowActionService.getJointGraphWrapper(), 
"getCurrentHighlightedOperatorIDs").mockReturnValue(["3"]);
+      vi.spyOn(workflowActionService.getTexeraGraph(), 
"getOperator").mockReturnValue(pythonOp);
+      // No stored console messages -> the Python-UDF branch of the OR is what 
enables the console.
+      vi.spyOn(consoleService, "hasConsoleMessages").mockReturnValue(false);
+
+      component.rerenderResultPanel();
+
+      const consoleConfig = component.frameComponentConfigs.get("Console");
+      expect(consoleConfig?.component).toBe(ConsoleFrameComponent);
+      expect(consoleConfig?.componentInputs).toEqual({ operatorId: "3", 
consoleInputEnabled: true });
+    });
+  });
+
+  describe("auto-open reactions to execution state", () => {
+    it("highlights the first active sink when a run completes and it is not 
already the sole selection", () => {
+      // Add the operator first: adding auto-highlights it, so spy only after 
that settles.
+      workflowActionService.addOperator(mockResultPredicate, mockPoint);
+      const wrapper = workflowActionService.getJointGraphWrapper();
+      vi.spyOn(wrapper, 
"getCurrentHighlightedOperatorIDs").mockReturnValue([]);
+      const highlightSpy = vi.spyOn(wrapper, 
"highlightOperators").mockImplementation(() => {});
+      const unhighlightSpy = vi.spyOn(wrapper, 
"unhighlightOperators").mockImplementation(() => {});
+
+      executeWorkflowService["updateExecutionState"]({ state: 
ExecutionState.Running });
+      executeWorkflowService["updateExecutionState"]({ state: 
ExecutionState.Completed });
+
+      expect(unhighlightSpy).toHaveBeenCalled();
+      
expect(highlightSpy).toHaveBeenCalledWith(mockResultPredicate.operatorID);
+    });
+
+    it("does not re-highlight a sink that is already the sole highlighted 
operator", () => {
+      workflowActionService.addOperator(mockResultPredicate, mockPoint);
+      const wrapper = workflowActionService.getJointGraphWrapper();
+      vi.spyOn(wrapper, 
"getCurrentHighlightedOperatorIDs").mockReturnValue([mockResultPredicate.operatorID]);
+      const highlightSpy = vi.spyOn(wrapper, 
"highlightOperators").mockImplementation(() => {});
+
+      executeWorkflowService["updateExecutionState"]({ state: 
ExecutionState.Running });
+      executeWorkflowService["updateExecutionState"]({ state: 
ExecutionState.Completed });
+
+      expect(highlightSpy).not.toHaveBeenCalled();
+    });
+
+    it("highlights an active Python UDF operator when the workflow starts 
running", () => {
+      const pythonOp: OperatorPredicate = {
+        ...mockResultPredicate,
+        operatorID: "py1",
+        operatorType: PYTHON_UDF_V2_OP_TYPE,
+      };
+      const wrapper = workflowActionService.getJointGraphWrapper();
+      vi.spyOn(wrapper, 
"getCurrentHighlightedOperatorIDs").mockReturnValue([]);
+      const highlightSpy = vi.spyOn(wrapper, 
"highlightOperators").mockImplementation(() => {});
+      vi.spyOn(workflowActionService.getTexeraGraph(), 
"getAllOperators").mockReturnValue([pythonOp]);
+
+      executeWorkflowService["updateExecutionState"]({ state: 
ExecutionState.Running });
+
+      expect(highlightSpy).toHaveBeenCalledWith("py1");
+    });
+
+    it("does not re-highlight a Python UDF when exactly one operator is 
already highlighted", () => {
+      const pythonOp: OperatorPredicate = {
+        ...mockResultPredicate,
+        operatorID: "py1",
+        operatorType: PYTHON_UDF_V2_OP_TYPE,
+      };
+      workflowActionService.addOperator(mockResultPredicate, mockPoint);
+      const wrapper = workflowActionService.getJointGraphWrapper();
+      // A single (real) operator is already selected, so the guard 
short-circuits and skips highlighting.
+      vi.spyOn(wrapper, 
"getCurrentHighlightedOperatorIDs").mockReturnValue([mockResultPredicate.operatorID]);
+      const highlightSpy = vi.spyOn(wrapper, 
"highlightOperators").mockImplementation(() => {});
+      vi.spyOn(workflowActionService.getTexeraGraph(), 
"getAllOperators").mockReturnValue([pythonOp]);
+
+      executeWorkflowService["updateExecutionState"]({ state: 
ExecutionState.Running });
+
+      expect(highlightSpy).not.toHaveBeenCalled();
+    });
+
+    it("does not touch the highlight selection when a run completes with no 
sink operators", () => {
+      // A source-only workflow has no sink, so the sink-highlight guard takes 
its empty branch.
+      workflowActionService.addOperator(mockScanPredicate, mockPoint);
+      const wrapper = workflowActionService.getJointGraphWrapper();
+      const highlightSpy = vi.spyOn(wrapper, 
"highlightOperators").mockImplementation(() => {});
+
+      executeWorkflowService["updateExecutionState"]({ state: 
ExecutionState.Running });
+      executeWorkflowService["updateExecutionState"]({ state: 
ExecutionState.Completed });
+
+      expect(highlightSpy).not.toHaveBeenCalled();
+    });
+  });
+
+  describe("state-change driven rerender", () => {
+    it("renders an error frame when the execution transitions into the failed 
state", () => {
+      vi.spyOn(workflowActionService.getJointGraphWrapper(), 
"getCurrentHighlightedOperatorIDs").mockReturnValue([]);
+
+      executeWorkflowService["updateExecutionState"]({ state: 
ExecutionState.Failed, errorMessages: [] });
+
+      expect(component.frameComponentConfigs.has("Static Error")).toBe(true);
+    });
+
+    it("clears the error frame when the execution recovers out of the failed 
state", () => {
+      vi.spyOn(workflowActionService.getJointGraphWrapper(), 
"getCurrentHighlightedOperatorIDs").mockReturnValue([]);
+
+      // into Failed: the error frame appears...
+      executeWorkflowService["updateExecutionState"]({ state: 
ExecutionState.Failed, errorMessages: [] });
+      expect(component.frameComponentConfigs.has("Static Error")).toBe(true);
+
+      // ...and leaving Failed forces a rerender that tears it back down.
+      executeWorkflowService["updateExecutionState"]({ state: 
ExecutionState.Running });
+      expect(component.frameComponentConfigs.has("Static Error")).toBe(false);
+    });
+  });
+
+  describe("operator display name changes", () => {
+    beforeEach(() => {
+      workflowActionService.addOperator(mockResultPredicate, mockPoint);
+      // Drive a rerender with the operator selected so the display-name 
handler subscribes.
+      vi.spyOn(workflowActionService.getJointGraphWrapper(), 
"getCurrentHighlightedOperatorIDs").mockReturnValue(["3"]);
+      executeWorkflowService["updateExecutionState"]({ state: 
ExecutionState.Running });
+    });
+
+    it("updates the operator title when the selected operator is renamed", () 
=> {
+      // The subject is hidden from the readonly graph type; reach it to drive 
the stream.
+      (workflowActionService.getTexeraGraph() as 
any).operatorDisplayNameChangedSubject.next({
+        operatorID: "3",
+        newDisplayName: "Renamed Result",
+      });
+
+      expect(component.operatorTitle).toBe("Renamed Result");
+    });
+
+    it("ignores rename events for other operators", () => {
+      const titleBefore = component.operatorTitle;
+
+      (workflowActionService.getTexeraGraph() as 
any).operatorDisplayNameChangedSubject.next({
+        operatorID: "someOtherOperator",
+        newDisplayName: "Should Not Apply",
+      });
+
+      expect(component.operatorTitle).toBe(titleBefore);
+    });
+  });
+
+  describe("panel open/close reactions", () => {
+    it("collapses the panel when the panel service requests a close", () => {
+      const panelService = TestBed.inject(PanelService);
+      component.openPanel();
+
+      panelService.closePanels();
+
+      expect(component.width).toBe(0);
+      expect(component.height).toBe(32.5);
+    });
+
+    it("re-docks and re-opens the panel when the panel service requests a 
reset", () => {
+      const panelService = TestBed.inject(PanelService);
+      component.returnPosition = { x: 11, y: 22 };
+      component.dragPosition = { x: 99, y: 88 };
+
+      panelService.resetPanels();
+
+      expect(component.dragPosition).toEqual({ x: 11, y: 22 });
+      expect(component.width).toBe(DEFAULT_WIDTH);
+      expect(component.height).toBe(DEFAULT_HEIGHT);
+    });
+
+    it("opens the panel when the result-panel-open stream emits true", () => {
+      component.closePanel();
+
+      workflowActionService.openResultPanel();
+
+      expect(component.width).toBe(DEFAULT_WIDTH);
+      expect(component.height).toBe(DEFAULT_HEIGHT);
+    });
+
+    it("closes the panel when the result-panel-open stream emits false", () => 
{
+      component.openPanel();
+
+      workflowActionService.closeResultPanel();
+
+      expect(component.width).toBe(0);
+      expect(component.height).toBe(32.5);
+    });
+  });
+
+  describe("drag overlay restore", () => {
+    it("handleEndDrag restores the visualization overlay z-index when it is 
present", () => {
+      const vizEl = { style: { zIndex: -1 } };
+      component.componentOutlets = {
+        nativeElement: { querySelector: () => vizEl },
+      } as unknown as ElementRef;
+      const source = { getFreeDragPosition: () => ({ x: 7, y: 8 }) };
+
+      component.handleEndDrag({ source } as unknown as CdkDragEnd);
+
+      expect(vizEl.style.zIndex).toBe(0);
+      expect(component.dragPosition).toEqual({ x: 7, y: 8 });
+    });
+
+    it("handleStartDrag is a no-op when no visualization overlay is present", 
() => {
+      component.componentOutlets = {
+        nativeElement: { querySelector: () => null },
+      } as unknown as ElementRef;
+
+      expect(() => component.handleStartDrag()).not.toThrow();
+    });
+  });
+
+  describe("getWorkflowFatalErrors filtering", () => {
+    it("returns every fatal error unfiltered when no operator id is provided", 
() => {
+      vi.spyOn(executeWorkflowService, "getErrorMessages").mockReturnValue([
+        makeFatalError("opA"),
+        makeFatalError("opB"),
+      ]);
+
+      const errors = (component as any).getWorkflowFatalErrors();
+
+      expect(errors.map((e: WorkflowFatalError) => 
e.operatorId)).toEqual(["opA", "opB"]);
+    });
+  });
+
+  describe("persistence on unload", () => {
+    it("writes the current panel dimensions to localStorage on window 
beforeunload", () => {
+      const setItemSpy = vi.spyOn(Storage.prototype, "setItem");
+      component.width = 640;
+      component.height = 480;
+
+      window.dispatchEvent(new Event("beforeunload"));
+
+      expect(setItemSpy).toHaveBeenCalledWith("result-panel-width", "640");
+      expect(setItemSpy).toHaveBeenCalledWith("result-panel-height", "480");
+    });
+
+    it("persists dimensions but skips the style entry when the result 
container is gone", () => {
+      // If the container element has already been torn down, only the 
width/height are saved.
+      vi.spyOn(document, "getElementById").mockReturnValue(null);
+      const setItemSpy = vi.spyOn(Storage.prototype, "setItem");
+      component.width = 640;
+      component.height = 480;
+
+      component.ngOnDestroy();
+
+      expect(setItemSpy).toHaveBeenCalledWith("result-panel-width", "640");
+      expect(setItemSpy).toHaveBeenCalledWith("result-panel-height", "480");
+      expect(setItemSpy).not.toHaveBeenCalledWith("result-panel-style", 
expect.anything());
+    });
   });
 });

Reply via email to