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/30 03:51:50 UTC

(texera) branch gh-readonly-queue/main/pr-8102-317c822e6bb3464578cb7dfea3cce511888d42fe created (now e944ce89c5)

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-8102-317c822e6bb3464578cb7dfea3cce511888d42fe
in repository https://gitbox.apache.org/repos/asf/texera.git


      at e944ce89c5 test(frontend): render the computing-unit rows and the metrics popover (#8102)

This branch includes the following new commits:

     new e944ce89c5 test(frontend): render the computing-unit rows and the metrics popover (#8102)

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): render the computing-unit rows and the metrics popover (#8102)

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-8102-317c822e6bb3464578cb7dfea3cce511888d42fe
in repository https://gitbox.apache.org/repos/asf/texera.git

commit e944ce89c5902fc9671a1d8577eb110bbb23c337
Author: Meng Wang <me...@uci.edu>
AuthorDate: Sun Aug 30 03:36:08 2026 +0000

    test(frontend): render the computing-unit rows and the metrics popover (#8102)
    
    ### What changes were proposed in this PR?
    
    `computing-unit-selection.component.html` never rendered its unit list
    or its metrics
    popover with data, so the per-unit row controls and every metric row sat
    unexecuted. This
    PR adds 17 cases that open both for real and drive them from the markup.
    No production
    code changes.
    
    - unit rows: selecting a row, the inline rename editor (focusout, Enter,
    Escape, and the
    click it swallows), and the pencil / plus / share / details / terminate
    icons — each
    asserting the row's own click handler did *not* also fire — plus the
    create-unit row
    - metrics popover: the CPU and RAM rows with their value, limit and
    percentage, and each
    optional row (GPU, JVM memory, shared memory) on **both** sides of its
    guard
    - the python-environment modal closing from its footer button and from
    `nzOnCancel`
    
    Template statement coverage goes from 71.35 % to 96.56 % (57 -> 7
    uncovered lines),
    measured locally. The 7 that remain are inside the PVE modal body,
    outside this issue.
    
    A comment in the spec said the dropdown's overlay "does not attach under
    jsdom", and this
    turned out not to be the reason those lines were uncovered — the overlay
    does attach. The
    real obstacle is that ng-zorro gates it behind an `auditTime(150)`
    scheduled outside the
    Angular zone, which `fakeAsync`'s `tick()`/`flush()` cannot reach,
    `whenStable()` waits on
    forever (the component polls), and Vitest's fake timers cannot intercept
    because zone.js
    captured the native timer first. The helper therefore polls for the
    rendered rows rather
    than sleeping a fixed amount, so a slow runner costs extra iterations
    instead of a
    failure. The stale comment is replaced with this explanation.
    
    ### Any related issues, documentation, discussions?
    
    Closes #8099
    
    ### How was this PR tested?
    
    `ng test --watch=false --include
    src/app/workspace/component/power-button/computing-unit-selection.component.spec.ts`
    -> 126 passed, run five times with identical results (five rather than
    three because of
    the polling helper). Five of the new assertions were verified to fail
    (non-zero exit) when
    deliberately broken. `prettier --check` and `eslint` are clean on the
    touched file.
    
    ### Was this PR authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code (Opus 5)
---
 .../computing-unit-selection.component.spec.ts     | 302 +++++++++++++++++++++
 1 file changed, 302 insertions(+)

diff --git a/frontend/src/app/workspace/component/power-button/computing-unit-selection.component.spec.ts b/frontend/src/app/workspace/component/power-button/computing-unit-selection.component.spec.ts
index 715c776827..a80d089af7 100644
--- a/frontend/src/app/workspace/component/power-button/computing-unit-selection.component.spec.ts
+++ b/frontend/src/app/workspace/component/power-button/computing-unit-selection.component.spec.ts
@@ -54,6 +54,8 @@ import { WorkflowMetadata } from "../../../dashboard/type/workflow-metadata.inte
 import { ExecutionState } from "../../types/execute-workflow.interface";
 import { ComputingUnitActionsService } from "../../../common/service/computing-unit/computing-unit-actions/computing-unit-actions.service";
 import { ComputingUnitMetadataComponent } from "../../../common/util/computing-unit.util";
+import { GuiConfigService } from "../../../common/service/gui-config.service";
+import { NzPopoverDirective } from "ng-zorro-antd/popover";
 
 /**
  * Builds a fully-populated DashboardWorkflowComputingUnit for driving the
@@ -1577,4 +1579,304 @@ describe("PowerButtonComponent", () => {
       }
     });
   });
+
+  // ──────────────────────────────────────────────────────────────────────────
+  // Rows of the dropdown menu, driven through the markup ng-zorro stamps into
+  // the CDK overlay rather than by calling the handlers on the instance.
+  // ──────────────────────────────────────────────────────────────────────────
+  describe("dropdown menu rows (rendered)", () => {
+    /**
+     * ng-zorro gates the dropdown's overlay behind an `auditTime(150)` that it
+     * schedules outside the Angular zone, so neither `fakeAsync`'s `tick()` nor
+     * Vitest's fake timers reach it — zone.js captured the native timer before
+     * either could patch it. Poll for the rows instead of sleeping a fixed
+     * amount, so a slow runner costs extra iterations rather than a failure.
+     */
+    async function openDropdown(): Promise<HTMLElement[]> {
+      fixture.debugElement.query(By.css(".computing-units-dropdown-button")).nativeElement.click();
+      for (let i = 0; i < 80 && document.querySelectorAll(".computing-unit-option").length === 0; i++) {
+        await new Promise(resolve => setTimeout(resolve, 25));
+        fixture.detectChanges();
+      }
+      fixture.detectChanges();
+      const rows = Array.from(document.querySelectorAll<HTMLElement>(".computing-unit-option"));
+      expect(rows.length).toBeGreaterThan(0);
+      return rows;
+    }
+
+    function click(element: Element | null | undefined): void {
+      expect(element).toBeTruthy();
+      element!.dispatchEvent(new MouseEvent("click", { bubbles: true }));
+      fixture.detectChanges();
+    }
+
+    let selectSpy: ReturnType<typeof vi.spyOn>;
+    let sharingDefault: boolean;
+
+    beforeEach(() => {
+      sharingDefault = TestBed.inject(GuiConfigService).env.sharingComputingUnitEnabled;
+      component.workflowId = 5;
+      component.allComputingUnits = [
+        makeComputingUnit({ cuid: 1, name: "Alpha" }),
+        makeComputingUnit({ cuid: 2, name: "Beta" }),
+      ];
+      selectSpy = vi
+        .spyOn(TestBed.inject(ComputingUnitStatusService), "selectComputingUnit")
+        .mockImplementation(() => {});
+      fixture.detectChanges();
+    });
+
+    afterEach(() => {
+      // The share test flips this flag. TestBed hands every test its own
+      // MockGuiConfigService, so it cannot leak today, but restore it anyway so the
+      // block stays correct if these tests ever share one config instance.
+      TestBed.inject(GuiConfigService).env.sharingComputingUnitEnabled = sharingDefault;
+      // Clear the overlay contents rather than the container itself, which CDK caches.
+      document.querySelectorAll(".cdk-overlay-container").forEach(el => (el.innerHTML = ""));
+      vi.restoreAllMocks();
+    });
+
+    it("selects the unit whose row is clicked", async () => {
+      const rows = await openDropdown();
+
+      click(rows[1]);
+
+      expect(component.selectedComputingUnit?.computingUnit.cuid).toBe(2);
+      expect(selectSpy).toHaveBeenCalledWith(5, 2);
+    });
+
+    it("commits a rename when the inline editor loses focus", async () => {
+      const renameSpy = vi
+        .spyOn(TestBed.inject(WorkflowComputingUnitManagingService), "renameComputingUnit")
+        .mockReturnValue(of({} as any));
+      component.editingNameOfUnit = 1;
+      component.editingUnitName = "Alpha";
+      const rows = await openDropdown();
+
+      const editor = rows[0].querySelector<HTMLInputElement>(".unit-name-edit-input");
+      expect(editor?.value).toBe("Alpha");
+      editor!.value = "Renamed";
+      editor!.dispatchEvent(new FocusEvent("focusout", { bubbles: true }));
+      fixture.detectChanges();
+
+      expect(renameSpy).toHaveBeenCalledWith(1, "Renamed");
+    });
+
+    it("commits a rename when Enter is pressed in the inline editor", async () => {
+      const renameSpy = vi
+        .spyOn(TestBed.inject(WorkflowComputingUnitManagingService), "renameComputingUnit")
+        .mockReturnValue(of({} as any));
+      component.editingNameOfUnit = 1;
+      component.editingUnitName = "Alpha";
+      const rows = await openDropdown();
+
+      const editor = rows[0].querySelector<HTMLInputElement>(".unit-name-edit-input");
+      editor!.value = "Entered";
+      editor!.dispatchEvent(new KeyboardEvent("keyup", { key: "Enter", bubbles: true }));
+      fixture.detectChanges();
+
+      expect(renameSpy).toHaveBeenCalledWith(1, "Entered");
+    });
+
+    it("abandons the rename when Escape is pressed in the inline editor", async () => {
+      const renameSpy = vi.spyOn(TestBed.inject(WorkflowComputingUnitManagingService), "renameComputingUnit");
+      component.editingNameOfUnit = 1;
+      component.editingUnitName = "Alpha";
+      const rows = await openDropdown();
+
+      rows[0]
+        .querySelector<HTMLInputElement>(".unit-name-edit-input")!
+        .dispatchEvent(new KeyboardEvent("keyup", { key: "Escape", bubbles: true }));
+      fixture.detectChanges();
+
+      expect(component.editingNameOfUnit).toBeNull();
+      expect(renameSpy).not.toHaveBeenCalled();
+    });
+
+    it("swallows a click inside the inline editor instead of selecting the row", async () => {
+      component.editingNameOfUnit = 1;
+      component.editingUnitName = "Alpha";
+      const rows = await openDropdown();
+
+      click(rows[0].querySelector(".unit-name-edit-input"));
+
+      expect(selectSpy).not.toHaveBeenCalled();
+      expect(component.editingNameOfUnit).toBe(1);
+    });
+    it("opens the inline editor from the pencil without selecting the row", async () => {
+      const rows = await openDropdown();
+
+      click(rows[0].querySelector(".anticon-edit"));
+
+      expect(component.editingNameOfUnit).toBe(1);
+      expect(component.editingUnitName).toBe("Alpha");
+      expect(selectSpy).not.toHaveBeenCalled();
+    });
+
+    it("opens the python-environment modal from the plus icon without selecting the row", async () => {
+      const rows = await openDropdown();
+
+      click(rows[1].querySelector(".anticon-plus"));
+
+      expect(component.pveModalVisible).toBe(true);
+      expect(component.selectedComputingUnit?.computingUnit.cuid).toBe(2);
+      expect(selectSpy).not.toHaveBeenCalled();
+    });
+
+    it("opens share access from the share icon without selecting the row", async () => {
+      TestBed.inject(GuiConfigService).env.sharingComputingUnitEnabled = true;
+      const shareSpy = vi
+        .spyOn(TestBed.inject(ComputingUnitActionsService), "openShareAccessModal")
+        .mockImplementation(() => {});
+      fixture.detectChanges();
+      const rows = await openDropdown();
+
+      click(rows[0].querySelector(".anticon-share-alt"));
+
+      expect(shareSpy).toHaveBeenCalledWith(1, true);
+      expect(selectSpy).not.toHaveBeenCalled();
+    });
+
+    it("opens the metadata modal from the eye icon without selecting the row", async () => {
+      const createSpy = vi.spyOn(TestBed.inject(NzModalService), "create").mockReturnValue({} as any);
+      const rows = await openDropdown();
+
+      click(rows[1].querySelector(".anticon-eye"));
+
+      expect(createSpy).toHaveBeenCalledTimes(1);
+      expect(createSpy.mock.calls[0][0].nzData).toBe(component.allComputingUnits[1]);
+      expect(selectSpy).not.toHaveBeenCalled();
+    });
+
+    it("terminates from the delete icon without selecting the row", async () => {
+      const terminateSpy = vi
+        .spyOn(TestBed.inject(ComputingUnitActionsService), "confirmAndTerminate")
+        .mockImplementation(() => {});
+      const rows = await openDropdown();
+
+      click(rows[0].querySelector(".anticon-delete"));
+
+      expect(terminateSpy).toHaveBeenCalledWith(1, component.allComputingUnits[0]);
+      expect(selectSpy).not.toHaveBeenCalled();
+    });
+
+    it("opens the create-unit modal from the row below the divider", async () => {
+      await openDropdown();
+
+      click(document.querySelector(".create-computing-unit"));
+
+      expect(component.addComputeUnitModalVisible).toBe(true);
+    });
+  });
+
+  // ──────────────────────────────────────────────────────────────────────────
+  // The metrics popover's own template, and the PVE modal's close controls.
+  // ──────────────────────────────────────────────────────────────────────────
+  describe("metrics popover (rendered)", () => {
+    /** A running kubernetes unit whose resource limits the popover reads. */
+    function unitWithResources(resource: Partial<Record<string, string>>): DashboardWorkflowComputingUnit {
+      const unit = makeComputingUnit({ cuid: 7, name: "Metrics" });
+      unit.computingUnit.resource = { ...unit.computingUnit.resource, ...(resource as any) };
+      return unit;
+    }
+
+    /** Opens the metrics popover and returns the metric names it rendered, in order. */
+    async function openMetrics(): Promise<{ names: string[]; overlay: HTMLElement }> {
+      const container = fixture.debugElement.query(By.css("#metrics-container-id"));
+      expect(container).toBeTruthy();
+      container.injector.get(NzPopoverDirective).show();
+      fixture.detectChanges();
+      // the tooltip base positions its overlay in a microtask
+      await Promise.resolve();
+      fixture.detectChanges();
+      const overlay = document.querySelector<HTMLElement>(".cdk-overlay-container")!;
+      const names = Array.from(overlay.querySelectorAll(".resource-metrics .general-metric .metric-name")).map(
+        el => el.textContent?.trim() ?? ""
+      );
+      return { names, overlay };
+    }
+
+    afterEach(() => {
+      document.querySelectorAll(".cdk-overlay-container").forEach(el => (el.innerHTML = ""));
+      vi.restoreAllMocks();
+    });
+
+    it("renders the CPU and RAM rows with their value, limit and percentage", async () => {
+      component.selectedComputingUnit = unitWithResources({ cpuLimit: "2", memoryLimit: "4Gi" });
+      fixture.detectChanges();
+
+      const { names, overlay } = await openMetrics();
+
+      expect(names).toContain("CPU");
+      expect(names).toContain("RAM");
+      const cpu = overlay.querySelector(".cpu-metric .metric-value")!;
+      expect(cpu.querySelector(".metric-unit")?.textContent).toContain("2");
+      expect(cpu.querySelector(".metric-percentage")?.textContent).toContain("%");
+      const memory = overlay.querySelector(".memory-metric .metric-value")!;
+      expect(memory.querySelector(".metric-unit")?.textContent).toContain("4");
+      expect(memory.querySelector(".metric-percentage")?.textContent).toContain("%");
+    });
+
+    it("adds the GPU, JVM and shared-memory rows when the unit declares those limits", async () => {
+      component.gpuOptions = ["0", "1"];
+      component.selectedComputingUnit = unitWithResources({
+        gpuLimit: "2",
+        jvmMemorySize: "1Gi",
+        shmSize: "64Mi",
+      });
+      fixture.detectChanges();
+
+      const { names, overlay } = await openMetrics();
+
+      expect(names).toEqual(["CPU", "RAM", "GPU", "JVM Memory Size", "Shared Memory Size"]);
+      expect(overlay.textContent).toContain("2 GPU(s)");
+    });
+
+    it("drops the GPU row when the deployment offers no GPU at all", async () => {
+      component.gpuOptions = ["0"];
+      component.selectedComputingUnit = unitWithResources({ gpuLimit: "2" });
+      fixture.detectChanges();
+
+      expect((await openMetrics()).names).not.toContain("GPU");
+    });
+
+    it("drops each optional row whose limit reads as zero or NaN", async () => {
+      component.gpuOptions = ["0", "1"];
+      component.selectedComputingUnit = unitWithResources({
+        gpuLimit: "0",
+        jvmMemorySize: "NaN",
+        shmSize: "0",
+      });
+      fixture.detectChanges();
+
+      expect((await openMetrics()).names).toEqual(["CPU", "RAM"]);
+    });
+
+    it("has no popover content to show while no unit is selected", () => {
+      component.selectedComputingUnit = null;
+      fixture.detectChanges();
+
+      expect(fixture.debugElement.query(By.css("#metrics-container-id"))).toBeNull();
+    });
+
+    it("closes the python-environment modal from its footer button and its cancel", () => {
+      component.pveModalVisible = true;
+      fixture.detectChanges();
+
+      document.querySelector<HTMLButtonElement>(".footer-all button")!.click();
+      fixture.detectChanges();
+
+      expect(component.pveModalVisible).toBe(false);
+
+      component.pveModalVisible = true;
+      fixture.detectChanges();
+      // the create-unit child renders an nz-modal too, so pick this one by its title
+      const pveModal = fixture.debugElement
+        .queryAll(By.css("nz-modal"))
+        .find(modal => modal.componentInstance?.nzTitle === "Python Environments");
+      pveModal!.triggerEventHandler("nzOnCancel", undefined);
+
+      expect(component.pveModalVisible).toBe(false);
+    });
+  });
 });