You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@texera.apache.org by gi...@apache.org on 2026/08/06 16:19:22 UTC

(texera) branch gh-readonly-queue/main/pr-7335-cc8308740361324f7f129657dad6dc395abeeb1c created (now 3c8e0e4fb7)

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

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


      at 3c8e0e4fb7 test(frontend): cover ContextMenuComponent template menu-item click bindings (#7335)

This branch includes the following new commits:

     new 3c8e0e4fb7 test(frontend): cover ContextMenuComponent template menu-item click bindings (#7335)

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(texera) 01/01: test(frontend): cover ContextMenuComponent template menu-item click bindings (#7335)

Posted by gi...@apache.org.
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-7335-cc8308740361324f7f129657dad6dc395abeeb1c
in repository https://gitbox.apache.org/repos/asf/texera.git

commit 3c8e0e4fb79ae67c3b439602f4d43810fdcf872e
Author: Meng Wang <me...@uci.edu>
AuthorDate: Thu Aug 6 08:47:50 2026 -0700

    test(frontend): cover ContextMenuComponent template menu-item click bindings (#7335)
    
    ### What changes were proposed in this PR?
    
    Extends `ContextMenuComponent`'s spec to cover the template's menu-item
    `(click)`
    bindings. The class file is already at 100%, but the existing tests call
    the
    handlers directly and never render/click the menu, so the template sat
    at ~54%.
    
    13 added tests render the menu (setting the state each item's `*ngIf`
    needs),
    query the `<li nz-menu-item>` by its label via `By.css`, fire
    `.triggerEventHandler("click", null)`, and assert the click reaches the
    handler
    the item declares — either the component method (`onCopy` / `onCut` /
    `onPaste` /
    `onDelete` / `onClickExportHighlightedExecutionResult`, spied) or the
    injected
    `OperatorMenuService` double (`disableHighlightedOperators`,
    `viewResultHighlightedOperators`, `reuseResultHighlightedOperator`,
    `executeUpToOperator`). Covers both variants of the toggle items
    (disable/enable,
    view/remove-view, reuse/remove-reusing) and both delete paths (operators
    vs
    links-only).
    
    This lifts `context-menu.component.html` from ~54% to **97.92%**. The
    single
    remaining line is the hardcoded-`nzDisabled` "reuse result" item's
    `(click)`:
    a disabled `nz-menu-item` genuinely swallows the click (verified —
    clicking it
    does not invoke the handler), so rather than force an unrealistic
    interaction the
    test only asserts that item renders; its handler is identical to
    "remove reusing result", which is clicked and covered.
    
    No production code was changed.
    
    ### Any related issues, documentation, discussions?
    
    Closes #7332
    
    ### How was this PR tested?
    
    Extended unit tests, run locally in `frontend/`:
    
    ```
    ng test --watch=false --include src/app/workspace/component/workflow-editor/context-menu/context-menu/context-menu.component.spec.ts
    # Test Files 1 passed (1) | Tests 42 passed (42)   — 3 consecutive runs, 0 flakes
    # context-menu.component.html: ~54% -> 97.92%
    prettier --write <spec>   # formatted
    eslint  <spec>            # clean
    ```
    
    The failure path was verified by deliberately breaking a new assertion
    and
    confirming the suite exits non-zero.
    
    ### Was this PR authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code (Opus 4.8 [1M context])
---
 .../context-menu/context-menu.component.spec.ts    | 169 +++++++++++++++++++++
 1 file changed, 169 insertions(+)

diff --git a/frontend/src/app/workspace/component/workflow-editor/context-menu/context-menu/context-menu.component.spec.ts b/frontend/src/app/workspace/component/workflow-editor/context-menu/context-menu/context-menu.component.spec.ts
index 27d740e7df..2903abc1fb 100644
--- a/frontend/src/app/workspace/component/workflow-editor/context-menu/context-menu/context-menu.component.spec.ts
+++ b/frontend/src/app/workspace/component/workflow-editor/context-menu/context-menu/context-menu.component.spec.ts
@@ -18,6 +18,7 @@
  */
 
 import { ComponentFixture, TestBed } from "@angular/core/testing";
+import { By } from "@angular/platform-browser";
 import { OperatorMetadataService } from "src/app/workspace/service/operator-metadata/operator-metadata.service";
 import { StubOperatorMetadataService } from "src/app/workspace/service/operator-metadata/stub-operator-metadata.service";
 
@@ -431,6 +432,174 @@ describe("ContextMenuComponent", () => {
       expect(component.highlightedCommentBoxIds).toEqual([]);
     });
   });
+  // ── Template menu-item (click) wiring ──
+  // The class methods are covered above; these render the menu and click each
+  // item so the template's *ngIf-gated (click) bindings are exercised too.
+  describe("menu item click bindings", () => {
+    const norm = (s: string | null) => (s ?? "").replace(/\s+/g, " ").trim().toLowerCase();
+
+    /** Click the rendered <li nz-menu-item> whose text matches `label` exactly. */
+    function clickItem(label: string): void {
+      const items = fixture.debugElement.queryAll(By.css("li[nz-menu-item]"));
+      const item = items.find(li => norm(li.nativeElement.textContent) === norm(label));
+      if (!item) {
+        throw new Error(
+          `menu item "${label}" not rendered; present: [${items.map(i => norm(i.nativeElement.textContent)).join(" | ")}]`
+        );
+      }
+      item.triggerEventHandler("click", null);
+    }
+
+    it("copy invokes onCopy", () => {
+      highlightedOperatorsSubject.next(["op1"]);
+      fixture.detectChanges();
+      const spy = vi.spyOn(component, "onCopy").mockImplementation(() => {});
+
+      clickItem("copy");
+
+      expect(spy).toHaveBeenCalledTimes(1);
+    });
+
+    it("cut invokes onCut", () => {
+      highlightedOperatorsSubject.next(["op1"]);
+      component.isWorkflowModifiable = true;
+      fixture.detectChanges();
+      const spy = vi.spyOn(component, "onCut").mockImplementation(() => {});
+
+      clickItem("cut");
+
+      expect(spy).toHaveBeenCalledTimes(1);
+    });
+
+    it("paste invokes onPaste", () => {
+      highlightedOperatorsSubject.next([]);
+      highlightedCommentBoxesSubject.next([]);
+      component.isWorkflowModifiable = true;
+      fixture.detectChanges();
+      const spy = vi.spyOn(component, "onPaste").mockImplementation(() => {});
+
+      clickItem("paste");
+
+      expect(spy).toHaveBeenCalledTimes(1);
+    });
+
+    it("disable invokes operatorMenuService.disableHighlightedOperators", () => {
+      operatorMenuService.isDisableOperator = true;
+      operatorMenuService.isDisableOperatorClickable = true;
+      fixture.detectChanges();
+
+      clickItem("disable");
+
+      expect(operatorMenuService.disableHighlightedOperators).toHaveBeenCalledTimes(1);
+    });
+
+    it("enable invokes operatorMenuService.disableHighlightedOperators", () => {
+      operatorMenuService.isDisableOperator = false;
+      operatorMenuService.isDisableOperatorClickable = true;
+      fixture.detectChanges();
+
+      clickItem("enable");
+
+      expect(operatorMenuService.disableHighlightedOperators).toHaveBeenCalledTimes(1);
+    });
+
+    it("view result invokes operatorMenuService.viewResultHighlightedOperators", () => {
+      operatorMenuService.isToViewResult = true;
+      operatorMenuService.isToViewResultClickable = true;
+      fixture.detectChanges();
+
+      clickItem("view result");
+
+      expect(operatorMenuService.viewResultHighlightedOperators).toHaveBeenCalledTimes(1);
+    });
+
+    it("remove view result invokes operatorMenuService.viewResultHighlightedOperators", () => {
+      operatorMenuService.isToViewResult = false;
+      operatorMenuService.isToViewResultClickable = true;
+      fixture.detectChanges();
+
+      clickItem("remove view result");
+
+      expect(operatorMenuService.viewResultHighlightedOperators).toHaveBeenCalledTimes(1);
+    });
+
+    it("renders the reuse result item (disabled) when marked for reuse", () => {
+      // This entry is hardcoded `nzDisabled`, so it can't be clicked; assert it renders.
+      // Its (click) handler is the same as "remove reusing result", covered by the next test.
+      operatorMenuService.isMarkForReuse = true;
+      operatorMenuService.isReuseResultClickable = true;
+      fixture.detectChanges();
+
+      const rendered = fixture.debugElement
+        .queryAll(By.css("li[nz-menu-item]"))
+        .some(li => norm(li.nativeElement.textContent) === "reuse result");
+      expect(rendered).toBe(true);
+    });
+
+    it("remove reusing result invokes operatorMenuService.reuseResultHighlightedOperator", () => {
+      operatorMenuService.isMarkForReuse = false;
+      operatorMenuService.isReuseResultClickable = true;
+      fixture.detectChanges();
+
+      clickItem("remove reusing result");
+
+      expect(operatorMenuService.reuseResultHighlightedOperator).toHaveBeenCalledTimes(1);
+    });
+
+    it("delete invokes onDelete when operators are highlighted", () => {
+      highlightedOperatorsSubject.next(["op1"]);
+      component.isWorkflowModifiable = true;
+      fixture.detectChanges();
+      const spy = vi.spyOn(component, "onDelete").mockImplementation(() => {});
+
+      clickItem("delete");
+
+      expect(spy).toHaveBeenCalledTimes(1);
+    });
+
+    it("delete invokes onDelete for a links-only selection", () => {
+      highlightedOperatorsSubject.next([]);
+      highlightedCommentBoxesSubject.next([]);
+      jointGraphWrapperSpy.getCurrentHighlightedLinkIDs.mockReturnValue(["link1"]);
+      component.isWorkflowModifiable = true;
+      fixture.detectChanges();
+      const spy = vi.spyOn(component, "onDelete").mockImplementation(() => {});
+
+      clickItem("delete");
+
+      expect(spy).toHaveBeenCalledTimes(1);
+    });
+
+    it("execute to this operator invokes executeUpToOperator", () => {
+      highlightedOperatorsSubject.next(["op1"]);
+      component.isWorkflowModifiable = true;
+      jointGraphWrapperSpy.getCurrentHighlightedOperatorIDs.mockReturnValue(["op1"]);
+      validationWorkflowService.validateOperator.mockReturnValue({ isValid: true });
+      (workflowActionService.getTexeraGraph() as unknown as Mocked<WorkflowGraph>).isOperatorDisabled.mockReturnValue(
+        false
+      );
+      fixture.detectChanges();
+      expect(component.canExecuteOperator()).toBe(true); // item is enabled
+
+      clickItem("execute to this operator");
+
+      expect(operatorMenuService.executeUpToOperator).toHaveBeenCalledTimes(1);
+    });
+
+    it("Export result invokes onClickExportHighlightedExecutionResult", () => {
+      (
+        workflowResultExportService as unknown as { hasResultToExportOnHighlightedOperators: boolean }
+      ).hasResultToExportOnHighlightedOperators = true;
+      (component as unknown as { config: { env: Record<string, unknown> } }).config.env.exportExecutionResultEnabled =
+        true;
+      fixture.detectChanges();
+      const spy = vi.spyOn(component, "onClickExportHighlightedExecutionResult").mockImplementation(() => {});
+
+      clickItem("export result");
+
+      expect(spy).toHaveBeenCalledTimes(1);
+    });
+  });
 });
 
 describe("ContextMenuComponent onDelete with real WorkflowActionService", () => {