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);
+ });
+ });
});