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-18a277c3009e71c94c12033bb9af2a9e41089684 in repository https://gitbox.apache.org/repos/asf/texera.git
commit 26aa6caa70067ffaf4493b798460878bada226b5 Author: Meng Wang <[email protected]> AuthorDate: Thu Aug 6 17:07:12 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", () => {
