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

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

commit e87ec126afdbdb12a9fc90e4904e54dc9a59c7ca
Author: Xinyuan Lin <[email protected]>
AuthorDate: Fri Aug 28 02:43:29 2026 +0000

    test(frontend): cover the computing-unit actions service and result panel 
(#8031)
    
    ### What changes were proposed in this PR?
    
    Two small frontend files. Measured with the identical spec-file filter
    on both sides (two repeated `--include` globs, never a test-name
    phrase), `rm -rf coverage` between runs, reading
    `frontend/coverage/gui/lcov.info`.
    
    | File | Codecov metric | Delta |
    |---|---|---|
    | `computing-unit-actions.service.ts` | 26/29 = 89.7% → **29/29 = 100%**
    | +3 lines |
    | `result-panel.component.html` | 50/53 = 94.3% → **53/53 = 100%** | +3
    lines |
    
    **+6 fully-covered lines and +5 branch arms.** The before figure on the
    template reproduces Codecov's published 94.3% exactly.
    
    This is a small PR and I would rather say so than dress it up. What
    makes it worth having is the template's function counter.
    
    ### The template's bindings were never entered
    
    `result-panel.component.html` sat at **FNF 7 / FNH 4** — three of its
    seven functions uncovered — because the existing spec called
    `handleEndDrag(...)` and `handleStartDrag()` **directly on the
    component**. The bindings themselves stayed at `FNDA:0`:
    covered-looking, unexercised. Driving them through the rendered element
    is what closes them.
    
    That is the seventh time in this campaign a file has sat at a healthy
    line percentage with uncovered functions, and the second time the
    specific cause was a spec calling a handler directly instead of firing
    the binding.
    
    ### Verification
    
    18 mutations, **all 18 killed.**
    
    The first draft claimed `survivors: []` alongside "NO DEFECTS FOUND" and
    "nothing in the advertised gap was unreachable". Its own mutants were
    re-run against the bundle as shipped first, and several survived it; all
    now die.
    
    Two further corrections to that draft:
    
    - **An arithmetic error.** It reported the service as `23/26 → 26/26`.
    Lines 101 and 109 carry `BRDA` records but **no `DA`** record — v8
    attributes the `DA` for the whole `modalService.confirm({...})`
    statement to line 99 — so on the `union(DA, BRDA)` line set the correct
    figures are 26/29 → 29/29. Same delta, different denominator, and worth
    stating so the numbers reproduce.
    - **A miscredited kill.** One mutation's row named a single test with
    the second in a parenthetical "also:". Re-derived, it fails two tests
    outright.
    
    ### One vacuity worth naming, though it is not a survivor
    
    The pre-existing test "renders the resize handles when the panel is
    docked" still passes a swap of the line-120 `nzDirections` arm on its
    own, because it never distinguishes the two directions. It is not a
    file-level survivor — the new tests kill that mutant — but the older
    test does not constrain what its name suggests.
    
    ### Deliberately not included
    
    Nothing. Both files reach 100% on lines, branches and functions, and
    neither carries an unreachable partial — which is unusual at this point
    in the codebase and is why this bundle was taken at six lines when three
    sibling bundles at the same size were dropped.
    
    No production file is touched. The worktree used a real yarn install
    rather than a `node_modules` junction.
    
    ### Any related issues, documentation, discussions?
    
    Closes #8030
    
    ### How was this PR tested?
    
    ```
    npx ng test --watch=false 
--include="**/computing-unit-actions.service.spec.ts" 
--include="**/result-panel.component.spec.ts"
    ```
    
    ```
     Test Files  2 passed (2)
    ```
    
    `yarn format:ci` passes. `frontend/junit.xml` and `frontend/coverage/`
    are regenerated by every run and are not committed.
    
    ### Was this PR authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code (Opus 5)
---
 .../computing-unit-actions.service.spec.ts         |  34 +++++
 .../result-panel/result-panel.component.spec.ts    | 167 +++++++++++++++++++--
 2 files changed, 192 insertions(+), 9 deletions(-)

diff --git 
a/frontend/src/app/common/service/computing-unit/computing-unit-actions/computing-unit-actions.service.spec.ts
 
b/frontend/src/app/common/service/computing-unit/computing-unit-actions/computing-unit-actions.service.spec.ts
index fe208457a4..d31ce59095 100644
--- 
a/frontend/src/app/common/service/computing-unit/computing-unit-actions/computing-unit-actions.service.spec.ts
+++ 
b/frontend/src/app/common/service/computing-unit/computing-unit-actions/computing-unit-actions.service.spec.ts
@@ -149,6 +149,40 @@ describe("ComputingUnitActionsService", () => {
       expect(notificationService.error).toHaveBeenCalledWith("Failed to 
terminate computing unit");
     });
 
+    it("uses the disconnect wording and shows no destructive warning for a 
local unit", () => {
+      service.confirmAndTerminate(3, unit({ type: "local", name: "local1" }));
+
+      const config = modalService.confirm.mock.calls[0][0];
+      expect(config.nzTitle).toBe("Disconnect from Local Computing Unit");
+      expect(config.nzOkText).toBe("Disconnect");
+      expect(config.nzContent).toContain("disconnect from 
<strong>local1</strong>");
+      // A local unit holds no execution results, so the data-loss warning 
must not appear.
+      // Assert the copy itself, not only its colour, so restyling the shared 
warning
+      // (a CSS class, a theme token) cannot silently void this guard.
+      expect(config.nzContent).not.toContain("Warning:");
+      expect(config.nzContent).not.toContain("will be lost");
+      expect(config.nzContent).not.toContain("#ff4d4f");
+      // the cancel label is a fixed word; it must not co-vary with the unit 
type
+      expect(config.nzCancelText).toBe("Cancel");
+      expect(config.nzOkType).toBe("primary");
+    });
+
+    it("falls back to the kubernetes copy when the unit carries no type", () 
=> {
+      // `type` is declared non-optional, so this pins the explicit `|| 
"kubernetes"` fallback.
+      // The name deliberately reuses the local case's "local1" so that `type` 
and `name` are not
+      // correlated across the suite: the copy cannot be keyed off the unit's 
name instead.
+      service.confirmAndTerminate(4, unit({ type: undefined, name: "local1" 
}));
+
+      const config = modalService.confirm.mock.calls[0][0];
+      expect(config.nzTitle).toBe("Terminate Computing Unit");
+      expect(config.nzOkText).toBe("Terminate");
+      expect(config.nzContent).toContain("terminate <strong>local1</strong>");
+      expect(config.nzContent).toContain("Warning:");
+      expect(config.nzContent).toContain("#ff4d4f");
+      expect(config.nzCancelText).toBe("Cancel");
+      expect(config.nzOkType).toBe("primary");
+    });
+
     it("notifies an error with the extracted message when the termination 
observable errors", () => {
       statusService.terminateComputingUnit.mockReturnValue(throwError(() => 
new Error("kaboom")));
       service.confirmAndTerminate(7, unit());
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 c4236299b7..339c772b52 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
@@ -18,9 +18,10 @@
  */
 
 import { ComponentFixture, TestBed } from "@angular/core/testing";
-import { Component, ElementRef } from "@angular/core";
-import { CdkDragEnd } from "@angular/cdk/drag-drop";
+import { Component, ElementRef, Input } from "@angular/core";
+import { CdkDrag, CdkDragEnd } from "@angular/cdk/drag-drop";
 import { NzResizeEvent } from "ng-zorro-antd/resizable";
+import { NzTooltipDirective } from "ng-zorro-antd/tooltip";
 
 import { DEFAULT_HEIGHT, DEFAULT_WIDTH, ResultPanelComponent } from 
"./result-panel.component";
 import { ExecuteWorkflowService } from 
"../../service/execute-workflow/execute-workflow.service";
@@ -173,8 +174,12 @@ describe("ResultPanelComponent", () => {
       component.dragPosition = { x: 5, y: 7 };
       expect(component.isPanelDocked()).toBe(true);
 
+      // vary one axis at a time so each half of the conjunction is 
individually load-bearing
       component.dragPosition = { x: 5, y: 8 };
       expect(component.isPanelDocked()).toBe(false);
+
+      component.dragPosition = { x: 6, y: 7 };
+      expect(component.isPanelDocked()).toBe(false);
     });
 
     it("clearResultPanel empties the frame configs", () => {
@@ -681,12 +686,20 @@ describe("ResultPanelComponent", () => {
       const resetSpy = vi.spyOn(component, 
"resetPanelPosition").mockImplementation(() => {});
 
       expect(fixture.debugElement.query(By.css("#content"))).toBeTruthy();
+      // Assert after each fire so the three controls are individually 
attributable: a
+      // single spy call cannot stand in for all of them, and swapping the two 
in-panel
+      // handlers changes which counter moves.
       fire("#result-buttons li[nz-menu-item]", "click", new 
MouseEvent("click")); // header close
+      expect(closeSpy).toHaveBeenCalledTimes(1);
+      expect(resetSpy).not.toHaveBeenCalled();
+
       fire("#panel-button button", "click", new MouseEvent("click")); // 
reset-position
-      fire("#panel-button li[nz-menu-item]", "click", new 
MouseEvent("click")); // in-panel close
+      expect(resetSpy).toHaveBeenCalledTimes(1);
+      expect(closeSpy).toHaveBeenCalledTimes(1);
 
-      expect(closeSpy).toHaveBeenCalled();
-      expect(resetSpy).toHaveBeenCalled();
+      fire("#panel-button li[nz-menu-item]", "click", new 
MouseEvent("click")); // in-panel close
+      expect(closeSpy).toHaveBeenCalledTimes(2);
+      expect(resetSpy).toHaveBeenCalledTimes(1);
     });
 
     it("renders only the collapsed open button and wires openPanel when width 
is 0", () => {
@@ -729,14 +742,29 @@ describe("ResultPanelComponent", () => {
 
     it("renders a tab per frame when frames are present", () => {
       component.width = DEFAULT_WIDTH;
-      component.frameComponentConfigs.set("Result", { component: StubFrame, 
componentInputs: {} });
-      component.frameComponentConfigs.set("Console", { component: StubFrame, 
componentInputs: {} });
+      // Seed real inputs: the outlet binds `inputs: 
config.value.componentInputs`, and that is
+      // the only thing that delivers operatorId into the real result / 
console / error frames.
+      component.frameComponentConfigs.set("Result", {
+        component: StubFrame,
+        componentInputs: { operatorId: "op-result" },
+      });
+      component.frameComponentConfigs.set("Console", {
+        component: StubFrame,
+        componentInputs: { operatorId: "op-console" },
+      });
       fixture.detectChanges();
 
       const text = (fixture.nativeElement as HTMLElement).textContent ?? "";
       expect(text).toContain("Result");
       expect(text).toContain("Console");
       expect(text).not.toContain("No results available to display.");
+
+      const renderedInputs = fixture.debugElement
+        .queryAll(By.directive(StubFrame))
+        .map(frame => (frame.componentInstance as StubFrame).operatorId);
+      expect(renderedInputs.length).toBeGreaterThan(0);
+      expect(renderedInputs).not.toContain(undefined);
+      renderedInputs.forEach(operatorId => expect(["op-result", 
"op-console"]).toContain(operatorId));
     });
 
     it("renders the resize handles when the panel is docked", () => {
@@ -746,10 +774,131 @@ describe("ResultPanelComponent", () => {
 
       
expect(fixture.debugElement.query(By.css("nz-resize-handles"))).toBeTruthy();
     });
+
+    // The two tests below leave isPanelDocked unmocked on purpose: the real 
predicate has to run
+    // for the template's `isPanelDocked() ? ... : ...` direction list to be 
exercised both ways.
+    // The spy is installed without an implementation, so behaviour is 
untouched; asserting that
+    // it was called is what stops the call being replaced by an inline 
template expression.
+    it("offers only the right handle while the panel sits docked in its return 
position", () => {
+      component.width = DEFAULT_WIDTH;
+      component.returnPosition = { x: 0, y: 0 };
+      component.dragPosition = { x: 0, y: 0 };
+      const dockedSpy = vi.spyOn(component, "isPanelDocked");
+      fixture.detectChanges();
+
+      const handles = 
fixture.debugElement.queryAll(By.css("nz-resize-handle"));
+      expect(dockedSpy).toHaveBeenCalled();
+      expect(handles.length).toBe(1);
+      expect((handles[0].nativeElement as 
HTMLElement).className).toContain("nz-resizable-handle-right");
+    });
+
+    it("offers all three resize directions once the panel has been dragged 
away from its return position", () => {
+      component.width = DEFAULT_WIDTH;
+      component.returnPosition = { x: 0, y: 0 };
+      // Only the y axis moves, so a direction list keyed off the x axis alone 
would still
+      // report the panel as docked and render a single handle.
+      component.dragPosition = { x: 0, y: 60 };
+      const dockedSpy = vi.spyOn(component, "isPanelDocked");
+      fixture.detectChanges();
+
+      const classNames = fixture.debugElement
+        .queryAll(By.css("nz-resize-handle"))
+        .map(handle => (handle.nativeElement as HTMLElement).className);
+      expect(dockedSpy).toHaveBeenCalled();
+      expect(classNames.length).toBe(3);
+      expect(classNames.some(c => 
c.includes("nz-resizable-handle-right"))).toBe(true);
+      expect(classNames.some(c => 
c.includes("nz-resizable-handle-bottom"))).toBe(true);
+      expect(classNames.some(c => 
c.includes("nz-resizable-handle-bottomRight"))).toBe(true);
+    });
+
+    it("names the selected operator in the collapsed open button's tooltip", 
() => {
+      component.width = 0;
+      component.operatorTitle = "My Operator";
+      fixture.detectChanges();
+
+      const openButton = fixture.debugElement.query(By.css("#result-buttons 
li[nz-menu-item]"));
+      
expect(openButton.injector.get(NzTooltipDirective).directiveTitle).toBe("Open 
Result Panel: My Operator");
+    });
+
+    it("omits the separator from the collapsed open button's tooltip when no 
operator is selected", () => {
+      component.width = 0;
+      component.operatorTitle = "";
+      fixture.detectChanges();
+
+      const openButton = fixture.debugElement.query(By.css("#result-buttons 
li[nz-menu-item]"));
+      
expect(openButton.injector.get(NzTooltipDirective).directiveTitle).toBe("Open 
Result Panel");
+    });
+
+    // The expanded panel swaps the same menu slot for a Close item carrying 
its own,
+    // near-identical tooltip; both arms of its ternary need their own 
assertions or the
+    // interpolation is only executed, never read.
+    it("names the selected operator in the expanded close button's tooltip", 
() => {
+      component.width = DEFAULT_WIDTH;
+      component.operatorTitle = "My Operator";
+      fixture.detectChanges();
+
+      const closeButton = fixture.debugElement.query(By.css("#result-buttons 
li[nz-menu-item]"));
+      
expect(closeButton.injector.get(NzTooltipDirective).directiveTitle).toBe("Close 
Result Panel: My Operator");
+    });
+
+    it("omits the separator from the expanded close button's tooltip when no 
operator is selected", () => {
+      component.width = DEFAULT_WIDTH;
+      component.operatorTitle = "";
+      fixture.detectChanges();
+
+      const closeButton = fixture.debugElement.query(By.css("#result-buttons 
li[nz-menu-item]"));
+      
expect(closeButton.injector.get(NzTooltipDirective).directiveTitle).toBe("Close 
Result Panel");
+    });
+
+    it("routes the container's drag and resize outputs to the component 
handlers", () => {
+      // onResize defers its assignment through requestAnimationFrame; run the 
callback
+      // synchronously so the width/height assertions are deterministic.
+      const cancelSpy = vi.spyOn(window, 
"cancelAnimationFrame").mockImplementation(() => {});
+      const rafSpy = vi.spyOn(window, 
"requestAnimationFrame").mockImplementation(cb => {
+        cb(0);
+        return 1;
+      });
+      // handleStartDrag/handleEndDrag dereference the #dynamicComponent 
ViewChild, which only
+      // resolves once the panel is open and has at least one frame to render.
+      component.width = DEFAULT_WIDTH;
+      component.returnPosition = { x: 0, y: 0 };
+      component.frameComponentConfigs.set("Result", { component: StubFrame, 
componentInputs: {} });
+      fixture.detectChanges();
+      const startDragSpy = vi.spyOn(component, "handleStartDrag");
+
+      try {
+        fire("#result-container", "cdkDragStarted", {});
+        fire("#result-container", "cdkDragEnded", {
+          source: { getFreeDragPosition: () => ({ x: 11, y: 22 }) },
+        });
+        fire("#result-container", "nzResize", { width: 700, height: 400 });
+
+        expect(startDragSpy).toHaveBeenCalledTimes(1);
+        expect(component.dragPosition).toEqual({ x: 11, y: 22 });
+        expect(component.width).toBe(700);
+        expect(component.height).toBe(400);
+
+        // Recording the fields is only half the wiring: push a 
change-detection pass and
+        // assert the recorded values actually reach the DOM, i.e. that the 
container is
+        // sized from width/height and keeps the dropped position rather than 
snapping back.
+        fixture.detectChanges();
+        const container = 
fixture.debugElement.query(By.css("#result-container"));
+        expect(container.injector.get(CdkDrag).freeDragPosition).toEqual({ x: 
11, y: 22 });
+        const host = container.nativeElement as HTMLElement;
+        expect(host.style.width).toBe("700px");
+        expect(host.style.height).toBe("400px");
+      } finally {
+        rafSpy.mockRestore();
+        cancelSpy.mockRestore();
+      }
+    });
   });
 });
 
 // A trivial standalone frame so *ngComponentOutlet can instantiate a tab's 
content
-// without pulling in a real result-frame component's dependency graph.
+// without pulling in a real result-frame component's dependency graph. It 
declares the
+// one input every real frame takes, so the outlet's `inputs:` half is 
observable.
 @Component({ standalone: true, template: "" })
-class StubFrame {}
+class StubFrame {
+  @Input() operatorId?: string;
+}

Reply via email to