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-7420-2c57707a762486a7935860d8fee7b8eb0ff89a7e in repository https://gitbox.apache.org/repos/asf/texera.git
commit 85c5fb2ec52b75a91d4915a61717ced425458a5e Author: Meng Wang <[email protected]> AuthorDate: Fri Aug 7 22:02:17 2026 -0700 test(frontend): cover UserComputingUnitListItemComponent template bindings (#7420) ### What changes were proposed in this PR? Extends `UserComputingUnitListItemComponent`'s spec to exercise the row through the DOM. The class file is already at 100%, but the existing tests call the handlers directly and never click the row's buttons, so `user-computing-unit-list-item.component.html` sat at ~52%. 10 added tests render the row and drive it via `fixture.debugElement.query(By.css(...))`: - **Buttons** — the rename button starts inline editing; clicking the unit name opens the metadata modal (`NzModalService.create` spied); the delete button emits the `deleted` output. - **Inline rename** — `Escape` cancels and `Enter` confirms with the typed value (both fired as real `KeyboardEvent`s), and a click inside the input does not bubble to the row. - **Sharing** — the share button is omitted while `sharingComputingUnitEnabled` is off, and opens the share-access modal when it is on. - **Metrics popover** — the CPU/RAM rows render, and the GPU / JVM-memory / shared-memory rows appear only when those limits are set (covering both arms of their `*ngIf`s). This lifts the template from **~52% to 100%** (statements *and* branches); the class stays at 100%. Two things worth noting, both found by checking rather than assuming: - The `Escape`/`Enter` tests dispatch **real** `KeyboardEvent`s. With `triggerEventHandler("keydown.escape", …)` the handler does fire (verified with a spy), but it bypasses Angular's key-filtering, so the real dispatch is both more representative and what the coverage reflects. - The last uncovered line was reported as html:79, but reading the coverage `statementMap` showed the uncovered statement actually spans to **line 84** — the input's `(click)="$event.stopPropagation()"`. That is what the added "click does not bubble" test covers. **Determinism:** the added tests introduce no *new* `vi.useFakeTimers()` usage (one pre-existing test in this file already uses fake timers; nothing was added on top) — the component's `setTimeout` never runs in a synchronous test body, and layering fake timers over zone.js's patched timers is Node-version dependent. No layout/geometry assertions. Overlay contents are cleared in an `afterEach` so no popover DOM leaks into a later test (clearing `innerHTML` rather than removing the container, since CDK caches that element). The popover is opened synchronously via `.injector.get(NzPopoverDirective).show()` + `detectChanges()` — the pattern already used in `user-dataset-staged-objects-list.component.spec.ts` — and hidden again afterwards. The sharing flag is flipped through `TestBed.inject(GuiConfigService)` (the same DI instance the component holds, whose config object is per-instance), so nothing leaks between tests. No production code was changed. ### Any related issues, documentation, discussions? Closes #7411 ### How was this PR tested? Extended unit tests, run locally in `frontend/`: ``` ng test --watch=false --include src/app/dashboard/component/user/user-computing-unit/user-computing-unit-list-item/user-computing-unit-list-item.component.spec.ts # Test Files 1 passed (1) | Tests 45 passed (45) — 3 consecutive runs, 0 flakes # user-computing-unit-list-item.component.html: ~52% -> 100% (statements & branches) 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]) --- ...user-computing-unit-list-item.component.spec.ts | 140 +++++++++++++++++++++ 1 file changed, 140 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 9fd46da1c1..9fd52ee80d 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 @@ -22,6 +22,7 @@ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { HttpClientTestingModule } from "@angular/common/http/testing"; import { By } from "@angular/platform-browser"; import { NzModalService } from "ng-zorro-antd/modal"; +import { NzPopoverDirective } from "ng-zorro-antd/popover"; import { of, throwError } from "rxjs"; import type { Mocked } from "vitest"; import { UserComputingUnitListItemComponent } from "./user-computing-unit-list-item.component"; @@ -31,6 +32,7 @@ import { ComputingUnitStatusService } from "../../../../../common/service/comput import { MockComputingUnitStatusService } from "../../../../../common/service/computing-unit/computing-unit-status/mock-computing-unit-status.service"; import { ComputingUnitActionsService } from "../../../../../common/service/computing-unit/computing-unit-actions/computing-unit-actions.service"; import { DashboardWorkflowComputingUnit } from "../../../../../common/type/workflow-computing-unit"; +import { GuiConfigService } from "../../../../../common/service/gui-config.service"; import { commonTestProviders } from "../../../../../common/testing/test-utils"; import { ComputingUnitMetadataComponent } from "../../../../../common/util/computing-unit.util"; @@ -490,4 +492,142 @@ describe("UserComputingUnitListItemComponent", () => { expect(component.showGpuSelection()).toBe(true); }); }); + + // ── Rendered-template interactions (the row's bindings) ── + + describe("template interactions", () => { + /** The handlers that call `$event.stopPropagation()` need a real-ish event. */ + const clickEvent = () => ({ stopPropagation: vi.fn() }) as unknown as MouseEvent; + + /** Show the metrics popover synchronously and return its overlay text. */ + function openMetricsPopover(): { popover: NzPopoverDirective; text: string } { + const popover = fixture.debugElement.query(By.css(".metrics-container")).injector.get(NzPopoverDirective); + popover.show(); + fixture.detectChanges(); + return { popover, text: document.querySelector(".cdk-overlay-container")?.textContent ?? "" }; + } + + afterEach(() => { + // Clear the overlay contents rather than removing the container element itself: + // CDK's OverlayContainer caches that element, so removing it would make later + // overlays render into a detached node. + document.querySelectorAll(".cdk-overlay-container").forEach(el => (el.innerHTML = "")); + }); + + it("the rename button starts inline editing", () => { + const renameButton = fixture.debugElement.query(By.css(".edit-button button")); + expect(renameButton).toBeTruthy(); + + renameButton.triggerEventHandler("click", clickEvent()); + + expect(component.editingNameOfUnit).toBe(1); + expect(component.editingUnitName).toBe("unit-1"); + }); + + it("clicking the unit name opens the metadata modal", () => { + const createSpy = vi + .spyOn(TestBed.inject(NzModalService), "create") + .mockReturnValue({} as ReturnType<NzModalService["create"]>); + + fixture.debugElement.query(By.css(".resource-name")).triggerEventHandler("click", null); + + expect(createSpy).toHaveBeenCalledWith(expect.objectContaining({ nzData: component.entry })); + }); + + it("escape on the rename input cancels editing", () => { + component.editingNameOfUnit = 1; + fixture.detectChanges(); + + const input = fixture.debugElement.query(By.css("input.unit-name-edit-input")); + expect(input).toBeTruthy(); + input.nativeElement.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true })); + + expect(component.editingNameOfUnit).toBeNull(); + }); + + it("enter on the rename input confirms with the typed value", () => { + computingUnitService.renameComputingUnit.mockReturnValue(of({} as Response)); + component.editingNameOfUnit = 1; + fixture.detectChanges(); + + const input = fixture.debugElement.query(By.css("input.unit-name-edit-input")); + input.nativeElement.value = " renamed "; + input.nativeElement.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true })); + + expect(computingUnitService.renameComputingUnit).toHaveBeenCalledExactlyOnceWith(1, "renamed"); + }); + + it("a click inside the rename input does not bubble to the row", () => { + component.editingNameOfUnit = 1; + fixture.detectChanges(); + const stopPropagation = vi.fn(); + + fixture.debugElement + .query(By.css("input.unit-name-edit-input")) + .triggerEventHandler("click", { stopPropagation } as unknown as MouseEvent); + + expect(stopPropagation).toHaveBeenCalledTimes(1); + }); + + it("the delete button emits the deleted output", () => { + const deletedSpy = vi.fn(); + component.deleted.subscribe(deletedSpy); + const deleteButton = fixture.debugElement.query(By.css(".button-group button[title='Delete']")); + expect(deleteButton).toBeTruthy(); + + deleteButton.triggerEventHandler("click", null); + + expect(deletedSpy).toHaveBeenCalledTimes(1); + }); + + it("omits the share button while sharing is disabled", () => { + expect(fixture.debugElement.query(By.css("button[aria-label='Share computing unit']"))).toBeNull(); + }); + + it("the share button opens the share-access modal when sharing is enabled", () => { + // same DI instance the component holds + TestBed.inject(GuiConfigService).env.sharingComputingUnitEnabled = true; + fixture.detectChanges(); + + const shareButton = fixture.debugElement.query(By.css("button[aria-label='Share computing unit']")); + expect(shareButton).toBeTruthy(); + shareButton.triggerEventHandler("click", clickEvent()); + + expect(actionsService.openShareAccessModal).toHaveBeenCalledWith(1, false); + }); + + it("the metrics popover renders the CPU and RAM rows", () => { + const { popover, text } = openMetricsPopover(); + + expect(text).toContain("CPU"); + expect(text).toContain("RAM"); + // the default entry has no GPU / JVM / shared-memory limits + expect(text).not.toContain("GPU(s)"); + expect(text).not.toContain("JVM Memory Size"); + expect(text).not.toContain("Shared Memory Size"); + + popover.hide(); + fixture.detectChanges(); + }); + + it("the metrics popover adds the GPU, JVM and shared-memory rows when those limits are set", () => { + const base = makeEntry(); + component.entry = makeEntry({ + computingUnit: { + ...base.computingUnit, + resource: { ...base.computingUnit.resource, gpuLimit: "2", jvmMemorySize: "2Gi", shmSize: "1Gi" }, + }, + }); + fixture.detectChanges(); + + const { popover, text } = openMetricsPopover(); + + expect(text).toContain("2 GPU(s)"); + expect(text).toContain("JVM Memory Size"); + expect(text).toContain("Shared Memory Size"); + + popover.hide(); + fixture.detectChanges(); + }); + }); });
