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/07/23 00:22:05 UTC

(texera) branch gh-readonly-queue/main/pr-6783-1a17bc7892489011f8e1e481bbbac5d5115fb32d created (now 1988b1755e)

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-6783-1a17bc7892489011f8e1e481bbbac5d5115fb32d
in repository https://gitbox.apache.org/repos/asf/texera.git


      at 1988b1755e test(frontend): extend UserComputingUnitListItemComponent unit test coverage (#6783)

This branch includes the following new commits:

     new 1988b1755e test(frontend): extend UserComputingUnitListItemComponent unit test coverage (#6783)

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): extend UserComputingUnitListItemComponent unit test coverage (#6783)

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-6783-1a17bc7892489011f8e1e481bbbac5d5115fb32d
in repository https://gitbox.apache.org/repos/asf/texera.git

commit 1988b1755e0d6d9490fa17b12888e6d6be042759
Author: Xinyuan Lin <xi...@uci.edu>
AuthorDate: Wed Jul 22 17:07:30 2026 -0700

    test(frontend): extend UserComputingUnitListItemComponent unit test coverage (#6783)
    
    ### What changes were proposed in this PR?
    
    Extends `user-computing-unit-list-item.component.spec.ts` (35 tests),
    covering the uncovered status/label computations, the
    start/stop/delete/select action handlers, and guard branches. Coverage
    77% -> high. No existing tests modified.
    
    ### Any related issues, documentation, discussions?
    
    Closes #6777.
    
    ### How was this PR tested?
    
    `ng test --include='**/user-computing-unit-list-item.component.spec.ts'`
    -> 35/35 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])
---
 ...user-computing-unit-list-item.component.spec.ts | 201 +++++++++++++++++++++
 1 file changed, 201 insertions(+)

diff --git a/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit-list-item/user-computing-unit-list-item.component.spec.ts b/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit-list-item/user-computing-unit-list-item.component.spec.ts
index ed8971229e..9fd46da1c1 100644
--- a/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit-list-item/user-computing-unit-list-item.component.spec.ts
+++ b/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit-list-item/user-computing-unit-list-item.component.spec.ts
@@ -32,6 +32,21 @@ import { MockComputingUnitStatusService } from "../../../../../common/service/co
 import { ComputingUnitActionsService } from "../../../../../common/service/computing-unit/computing-unit-actions/computing-unit-actions.service";
 import { DashboardWorkflowComputingUnit } from "../../../../../common/type/workflow-computing-unit";
 import { commonTestProviders } from "../../../../../common/testing/test-utils";
+import { ComputingUnitMetadataComponent } from "../../../../../common/util/computing-unit.util";
+
+function bareComponent(entry: DashboardWorkflowComputingUnit): UserComputingUnitListItemComponent {
+  const item = new UserComputingUnitListItemComponent(
+    {} as any,
+    {} as any,
+    {} as any,
+    {} as any,
+    {} as any,
+    {} as any,
+    {} as any
+  );
+  item.entry = entry;
+  return item;
+}
 
 function makeEntry(overrides: Partial<DashboardWorkflowComputingUnit> = {}): DashboardWorkflowComputingUnit {
   return {
@@ -116,6 +131,12 @@ describe("UserComputingUnitListItemComponent", () => {
     fixture.detectChanges();
   });
 
+  afterEach(() => {
+    vi.useRealTimers();
+    fixture?.destroy();
+    vi.restoreAllMocks();
+  });
+
   it("should create", () => {
     expect(component).toBeTruthy();
   });
@@ -289,4 +310,184 @@ describe("UserComputingUnitListItemComponent", () => {
       expect(component.showGpuSelection()).toBe(true);
     });
   });
+
+  describe("ngOnInit gpuOptions fallback", () => {
+    it("defaults gpuOptions to an empty array when the service omits gpuLimitOptions", () => {
+      computingUnitService.getComputingUnitLimitOptions.mockReturnValue(
+        of({ cpuLimitOptions: [], memoryLimitOptions: [], gpuLimitOptions: undefined as unknown as string[] })
+      );
+      const freshFixture = TestBed.createComponent(UserComputingUnitListItemComponent);
+      freshFixture.componentInstance.entry = makeEntry();
+      freshFixture.detectChanges();
+      expect(freshFixture.componentInstance.gpuOptions).toEqual([]);
+      freshFixture.destroy();
+    });
+  });
+
+  describe("startEditingUnitName focus behavior", () => {
+    it("focuses and selects the rendered input after the timeout fires", () => {
+      const host = fixture.nativeElement as HTMLElement;
+      document.body.appendChild(host);
+      const focusSpy = vi.spyOn(HTMLInputElement.prototype, "focus").mockImplementation(() => {});
+      const selectSpy = vi.spyOn(HTMLInputElement.prototype, "select").mockImplementation(() => {});
+      vi.useFakeTimers();
+      try {
+        component.startEditingUnitName(makeEntry());
+        // The editable input is rendered synchronously via cdr.detectChanges().
+        // Scope the query to this fixture's host so it can't match another fixture's input.
+        const input = (fixture.nativeElement as HTMLElement).querySelector(".unit-name-edit-input");
+        expect(input).toBeTruthy();
+        // The focus/select happen inside a setTimeout(0) callback.
+        expect(focusSpy).not.toHaveBeenCalled();
+        vi.advanceTimersByTime(1);
+        expect(focusSpy).toHaveBeenCalledTimes(1);
+        expect(selectSpy).toHaveBeenCalledTimes(1);
+      } finally {
+        host.remove();
+      }
+    });
+  });
+
+  describe("openComputingUnitMetadataModal", () => {
+    it("opens the metadata modal with the expected configuration", () => {
+      const modalService = TestBed.inject(NzModalService);
+      const createSpy = vi.spyOn(modalService, "create").mockReturnValue({} as any);
+      const entry = makeEntry();
+
+      component.openComputingUnitMetadataModal(entry);
+
+      expect(createSpy).toHaveBeenCalledTimes(1);
+      expect(createSpy).toHaveBeenCalledWith(
+        expect.objectContaining({
+          nzTitle: "Computing Unit Information",
+          nzContent: ComputingUnitMetadataComponent,
+          nzData: entry,
+          nzFooter: null,
+          nzMaskClosable: true,
+          nzWidth: "600px",
+        })
+      );
+    });
+  });
+
+  describe("status and label computations", () => {
+    it("delegates badge color to the status util", () => {
+      expect(component.getBadgeColor("Running")).toBe("green");
+      expect(component.getBadgeColor("Pending")).toBe("gold");
+      expect(component.getBadgeColor("Terminated")).toBe("red");
+    });
+
+    it("delegates the status tooltip to the status util", () => {
+      expect(component.getUnitStatusTooltip(makeEntry({ status: "Running" }))).toBe("Ready to use");
+      expect(component.getUnitStatusTooltip(makeEntry({ status: "Pending" }))).toBe("Computing unit is starting up");
+      const terminated = {
+        ...makeEntry(),
+        status: "Terminated" as unknown as DashboardWorkflowComputingUnit["status"],
+      };
+      expect(component.getUnitStatusTooltip(terminated)).toBe("Terminated");
+    });
+
+    it("computes CPU/memory percentages and maps them to progress statuses under heavy load", () => {
+      component.entry = makeEntry({
+        metrics: { cpuUsage: "950m", memoryUsage: "950Mi" },
+      });
+      expect(component.getCpuPercentage()).toBeCloseTo(95, 1);
+      expect(component.getMemoryPercentage()).toBeCloseTo(92.77, 1);
+      expect(component.getCpuStatus()).toBe("exception");
+      expect(component.getMemoryStatus()).toBe("exception");
+    });
+
+    it("reports 0% and success statuses for the default entry's N/A (unavailable) metrics", () => {
+      expect(component.getCpuPercentage()).toBe(0);
+      expect(component.getMemoryPercentage()).toBe(0);
+      expect(component.getCpuStatus()).toBe("success");
+      expect(component.getMemoryStatus()).toBe("success");
+    });
+  });
+
+  describe("resource limit units", () => {
+    it("returns CPU for a unitless core limit and passes through the raw unit otherwise", () => {
+      const coreEntry = bareComponent(makeEntry());
+      expect(coreEntry.getCpuLimitUnit()).toBe("CPU");
+
+      const milliEntry = bareComponent(
+        makeEntry({
+          computingUnit: {
+            ...makeEntry().computingUnit,
+            resource: { ...makeEntry().computingUnit.resource, cpuLimit: "2000m" },
+          },
+        })
+      );
+      expect(milliEntry.getCpuLimitUnit()).toBe("m");
+    });
+
+    it("returns the memory limit unit parsed from the limit string", () => {
+      expect(bareComponent(makeEntry()).getMemoryLimitUnit()).toBe("Gi");
+    });
+  });
+
+  describe("getCpuValue / getMemoryValue with real usage", () => {
+    function entryWith(cpuLimit: string, memoryLimit: string, cpuUsage: string, memoryUsage: string) {
+      const base = makeEntry();
+      return makeEntry({
+        metrics: { cpuUsage, memoryUsage },
+        computingUnit: {
+          ...base.computingUnit,
+          resource: { ...base.computingUnit.resource, cpuLimit, memoryLimit },
+        },
+      });
+    }
+
+    it("converts CPU usage to cores when the limit is expressed in cores", () => {
+      component.entry = entryWith("1", "1Gi", "500m", "512Mi");
+      expect(component.getCpuValue()).toBeCloseTo(0.5, 4);
+      expect(component.getMemoryValue()).toBeCloseTo(0.5, 4);
+    });
+
+    it("converts CPU usage into the limit's unit when the limit carries a suffix", () => {
+      component.entry = entryWith("2000m", "1Gi", "500m", "512Mi");
+      // limit unit is "m", so usage is reported in millicores rather than cores
+      expect(component.getCpuValue()).toBeCloseTo(500, 2);
+    });
+  });
+
+  describe("fallback getters when metrics/resource are absent", () => {
+    it("returns N/A for every usage and limit getter", () => {
+      const base = makeEntry();
+      const item = bareComponent(
+        makeEntry({
+          metrics: undefined,
+          computingUnit: {
+            ...base.computingUnit,
+            resource: undefined as unknown as DashboardWorkflowComputingUnit["computingUnit"]["resource"],
+          },
+        })
+      );
+
+      expect(item.getCurrentComputingUnitCpuUsage()).toBe("N/A");
+      expect(item.getCurrentComputingUnitMemoryUsage()).toBe("N/A");
+      expect(item.getCurrentComputingUnitCpuLimit()).toBe("N/A");
+      expect(item.getCurrentComputingUnitMemoryLimit()).toBe("N/A");
+      expect(item.getCurrentComputingUnitGpuLimit()).toBe("N/A");
+      expect(item.getCurrentComputingUnitJvmMemorySize()).toBe("N/A");
+      expect(item.getCurrentSharedMemorySize()).toBe("N/A");
+    });
+  });
+
+  describe("showGpuSelection edge cases", () => {
+    it("hides the selection when the only option is the zero option", () => {
+      component.gpuOptions = ["0"];
+      expect(component.showGpuSelection()).toBe(false);
+    });
+
+    it("hides the selection when there are no options", () => {
+      component.gpuOptions = [];
+      expect(component.showGpuSelection()).toBe(false);
+    });
+
+    it("shows the selection when the single option is a non-zero value", () => {
+      component.gpuOptions = ["2"];
+      expect(component.showGpuSelection()).toBe(true);
+    });
+  });
 });