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/13 07:36:36 UTC
(texera) 01/01: test(frontend): add unit test coverage for UserComputingUnitListItemComponent (#6391)
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-6391-fd77b32bfc6156a95c88aa07c3cd05cdb13b540d
in repository https://gitbox.apache.org/repos/asf/texera.git
commit b4b1ecb07c71c6755712532c6b72afdf73932cec
Author: Meng Wang <me...@uci.edu>
AuthorDate: Mon Jul 13 00:36:29 2026 -0700
test(frontend): add unit test coverage for UserComputingUnitListItemComponent (#6391)
### What changes were proposed in this PR?
Adds a Vitest spec for `UserComputingUnitListItemComponent`
(`frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit-list-item/`),
which had no spec (codecov ~9%). The spec follows `frontend/TESTING.md`
(TestBed +
`fixture.detectChanges()` for template coverage) and mirrors the sibling
`user-computing-unit.component.spec.ts` /
`user-dataset-list-item.component.spec.ts`
mocking conventions.
20 behavior-focused tests:
- **create / `ngOnInit`** — renders the template; populates `gpuOptions`
from
`getComputingUnitLimitOptions`, and notifies on that request failing.
- **`entry` / `unit` getters** — throw before `entry` is set / when the
entry has
no computing unit; return the bound values otherwise.
- **`startEditingUnitName`** — rejects non-owners with an error; enters
edit mode
for owners.
- **`cancelEditingUnitName`** — clears the editing state.
- **`confirmUpdateUnitName`** — rejects an empty name without calling
the endpoint;
on success renames with the *trimmed* name, updates
`entry.computingUnit.name`
(only when the cuid matches), refreshes the list, and resets editing; on
failure
notifies, leaves the name unchanged, and still resets editing.
- **`onClickOpenShareAccess`** — delegates to
`ComputingUnitActionsService.openShareAccessModal(cuid, false)`.
- **`deleted` output** — is an `EventEmitter` and fires when the delete
button is clicked.
- **resource getters** — read limits straight from the unit and return
zero usage
values when metrics are unavailable.
No production code was changed.
### Any related issues, documentation, discussions?
Closes #6389
### How was this PR tested?
New Vitest spec, run locally in `frontend/` (all green; failure path
verified by
deliberately breaking an assertion to confirm the suite goes red):
```
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 20 passed (20)
eslint <spec> # clean
prettier --check # clean
```
Services (`WorkflowComputingUnitManagingService`, `NotificationService`,
`ComputingUnitStatusService`, `ComputingUnitActionsService`) are mocked,
so the
spec makes no network calls and touches no live services.
### 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 | 292 +++++++++++++++++++++
1 file changed, 292 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
new file mode 100644
index 0000000000..ed8971229e
--- /dev/null
+++ b/frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit-list-item/user-computing-unit-list-item.component.spec.ts
@@ -0,0 +1,292 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { EventEmitter } from "@angular/core";
+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 { of, throwError } from "rxjs";
+import type { Mocked } from "vitest";
+import { UserComputingUnitListItemComponent } from "./user-computing-unit-list-item.component";
+import { NotificationService } from "../../../../../common/service/notification/notification.service";
+import { WorkflowComputingUnitManagingService } from "../../../../../common/service/computing-unit/workflow-computing-unit/workflow-computing-unit-managing.service";
+import { ComputingUnitStatusService } from "../../../../../common/service/computing-unit/computing-unit-status/computing-unit-status.service";
+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 { commonTestProviders } from "../../../../../common/testing/test-utils";
+
+function makeEntry(overrides: Partial<DashboardWorkflowComputingUnit> = {}): DashboardWorkflowComputingUnit {
+ return {
+ computingUnit: {
+ cuid: 1,
+ uid: 1,
+ name: "unit-1",
+ creationTime: 0,
+ terminateTime: undefined,
+ type: "local",
+ uri: "",
+ resource: {
+ cpuLimit: "1",
+ memoryLimit: "1Gi",
+ gpuLimit: "0",
+ jvmMemorySize: "0",
+ shmSize: "0",
+ nodeAddresses: [],
+ },
+ },
+ status: "Running",
+ metrics: { cpuUsage: "N/A", memoryUsage: "N/A" },
+ isOwner: true,
+ accessPrivilege: "WRITE",
+ ownerGoogleAvatar: "",
+ ownerName: "owner",
+ ...overrides,
+ };
+}
+
+describe("UserComputingUnitListItemComponent", () => {
+ let fixture: ComponentFixture<UserComputingUnitListItemComponent>;
+ let component: UserComputingUnitListItemComponent;
+ let computingUnitService: Mocked<WorkflowComputingUnitManagingService>;
+ let notificationService: Mocked<NotificationService>;
+ let actionsService: Mocked<ComputingUnitActionsService>;
+ let statusService: ComputingUnitStatusService;
+
+ beforeEach(async () => {
+ const computingUnitServiceSpy = {
+ getComputingUnitLimitOptions: vi.fn(),
+ renameComputingUnit: vi.fn(),
+ } as unknown as Mocked<WorkflowComputingUnitManagingService>;
+ computingUnitServiceSpy.getComputingUnitLimitOptions.mockReturnValue(
+ of({ cpuLimitOptions: [], memoryLimitOptions: [], gpuLimitOptions: ["0", "1"] })
+ );
+
+ const notificationServiceSpy = {
+ success: vi.fn(),
+ error: vi.fn(),
+ info: vi.fn(),
+ warning: vi.fn(),
+ loading: vi.fn(),
+ blank: vi.fn(),
+ };
+ const actionsServiceSpy = { openShareAccessModal: vi.fn() };
+
+ await TestBed.configureTestingModule({
+ imports: [UserComputingUnitListItemComponent, HttpClientTestingModule],
+ providers: [
+ NzModalService,
+ { provide: NotificationService, useValue: notificationServiceSpy },
+ { provide: WorkflowComputingUnitManagingService, useValue: computingUnitServiceSpy },
+ { provide: ComputingUnitStatusService, useClass: MockComputingUnitStatusService },
+ { provide: ComputingUnitActionsService, useValue: actionsServiceSpy },
+ ...commonTestProviders,
+ ],
+ }).compileComponents();
+
+ computingUnitService = TestBed.inject(
+ WorkflowComputingUnitManagingService
+ ) as unknown as Mocked<WorkflowComputingUnitManagingService>;
+ notificationService = TestBed.inject(NotificationService) as unknown as Mocked<NotificationService>;
+ actionsService = TestBed.inject(ComputingUnitActionsService) as unknown as Mocked<ComputingUnitActionsService>;
+ statusService = TestBed.inject(ComputingUnitStatusService);
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(UserComputingUnitListItemComponent);
+ component = fixture.componentInstance;
+ component.entry = makeEntry();
+ fixture.detectChanges();
+ });
+
+ it("should create", () => {
+ expect(component).toBeTruthy();
+ });
+
+ describe("ngOnInit", () => {
+ it("populates gpuOptions from the service", () => {
+ expect(component.gpuOptions).toEqual(["0", "1"]);
+ });
+
+ it("notifies the user when the resource options request fails", () => {
+ computingUnitService.getComputingUnitLimitOptions.mockReturnValue(throwError(() => new Error("boom")));
+ const freshFixture = TestBed.createComponent(UserComputingUnitListItemComponent);
+ freshFixture.componentInstance.entry = makeEntry();
+ freshFixture.detectChanges();
+ expect(notificationService.error).toHaveBeenCalledWith(
+ expect.stringContaining("Failed to fetch resource options")
+ );
+ });
+ });
+
+ describe("entry and unit getters", () => {
+ function bare(): UserComputingUnitListItemComponent {
+ return new UserComputingUnitListItemComponent(
+ {} as any,
+ {} as any,
+ {} as any,
+ {} as any,
+ {} as any,
+ {} as any,
+ {} as any
+ );
+ }
+
+ it("throws when entry is accessed before being set", () => {
+ expect(() => bare().entry).toThrowError("entry property must be provided to UserComputingUnitListItemComponent.");
+ });
+
+ it("returns the entry once set", () => {
+ const entry = makeEntry();
+ const item = bare();
+ item.entry = entry;
+ expect(item.entry).toBe(entry);
+ });
+
+ it("exposes the computing unit via the unit getter", () => {
+ expect(component.unit).toBe(component.entry.computingUnit);
+ });
+
+ it("throws when the entry has no computing unit", () => {
+ const item = bare();
+ item.entry = {
+ ...makeEntry(),
+ computingUnit: undefined as unknown as DashboardWorkflowComputingUnit["computingUnit"],
+ };
+ expect(() => item.unit).toThrowError(
+ "Incorrect type of DashboardEntry provided to UserComputingUnitListItemComponent. Entry must be computing unit."
+ );
+ });
+ });
+
+ describe("startEditingUnitName", () => {
+ it("rejects renaming for non-owners", () => {
+ const entry = makeEntry({ isOwner: false });
+ component.startEditingUnitName(entry);
+ expect(notificationService.error).toHaveBeenCalledWith("Only owners can rename computing units");
+ expect(component.editingNameOfUnit).toBeNull();
+ });
+
+ it("enters editing mode for owners", () => {
+ const entry = makeEntry();
+ component.startEditingUnitName(entry);
+ expect(component.editingNameOfUnit).toBe(entry.computingUnit.cuid);
+ expect(component.editingUnitName).toBe(entry.computingUnit.name);
+ });
+ });
+
+ describe("cancelEditingUnitName", () => {
+ it("clears the editing state", () => {
+ component.editingNameOfUnit = 1;
+ component.editingUnitName = "half-typed";
+ component.cancelEditingUnitName();
+ expect(component.editingNameOfUnit).toBeNull();
+ expect(component.editingUnitName).toBe("");
+ });
+ });
+
+ describe("confirmUpdateUnitName", () => {
+ it("rejects an empty name without calling the rename endpoint", () => {
+ component.editingNameOfUnit = 1;
+ component.confirmUpdateUnitName(1, " ");
+ expect(notificationService.error).toHaveBeenCalledWith("Computing unit name cannot be empty");
+ expect(computingUnitService.renameComputingUnit).not.toHaveBeenCalled();
+ expect(component.editingNameOfUnit).toBeNull();
+ });
+
+ it("renames with the trimmed name, updates the entry, refreshes, and resets editing on success", () => {
+ const refreshSpy = vi.spyOn(statusService, "refreshComputingUnitList");
+ computingUnitService.renameComputingUnit.mockReturnValue(of({} as Response));
+ component.editingNameOfUnit = 1;
+
+ component.confirmUpdateUnitName(1, " new-name ");
+
+ expect(computingUnitService.renameComputingUnit).toHaveBeenCalledExactlyOnceWith(1, "new-name");
+ expect(notificationService.success).toHaveBeenCalledWith("Successfully renamed computing unit");
+ expect(component.entry.computingUnit.name).toBe("new-name");
+ expect(refreshSpy).toHaveBeenCalledTimes(1);
+ expect(component.editingNameOfUnit).toBeNull();
+ expect(component.editingUnitName).toBe("");
+ });
+
+ it("does not rewrite the entry name when the cuid does not match", () => {
+ computingUnitService.renameComputingUnit.mockReturnValue(of({} as Response));
+
+ component.confirmUpdateUnitName(999, "other-name");
+
+ expect(computingUnitService.renameComputingUnit).toHaveBeenCalledExactlyOnceWith(999, "other-name");
+ expect(component.entry.computingUnit.name).toBe("unit-1");
+ });
+
+ it("notifies on failure, leaves the name unchanged, and resets editing", () => {
+ const refreshSpy = vi.spyOn(statusService, "refreshComputingUnitList");
+ computingUnitService.renameComputingUnit.mockReturnValue(throwError(() => new Error("boom")));
+ component.editingNameOfUnit = 1;
+
+ component.confirmUpdateUnitName(1, "new-name");
+
+ expect(notificationService.error).toHaveBeenCalledWith(
+ expect.stringContaining("Failed to rename computing unit:")
+ );
+ expect(component.entry.computingUnit.name).toBe("unit-1");
+ expect(refreshSpy).not.toHaveBeenCalled();
+ expect(component.editingNameOfUnit).toBeNull();
+ });
+ });
+
+ describe("onClickOpenShareAccess", () => {
+ it("delegates to the actions service with inWorkspace = false", async () => {
+ await component.onClickOpenShareAccess(42);
+ expect(actionsService.openShareAccessModal).toHaveBeenCalledExactlyOnceWith(42, false);
+ });
+ });
+
+ describe("deleted output", () => {
+ it("is an EventEmitter", () => {
+ expect(component.deleted).toBeInstanceOf(EventEmitter);
+ });
+
+ it("emits when the delete button is clicked", () => {
+ const emitted = vi.fn();
+ component.deleted.subscribe(emitted);
+ fixture.debugElement.query(By.css('button[title="Delete"]')).triggerEventHandler("click", null);
+ expect(emitted).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe("resource getters", () => {
+ it("reads resource limits straight from the unit", () => {
+ expect(component.getGpuLimit()).toBe("0");
+ expect(component.getJvmMemorySize()).toBe("0");
+ expect(component.getSharedMemorySize()).toBe("0");
+ expect(typeof component.getCpuLimit()).toBe("number");
+ expect(typeof component.getMemoryLimit()).toBe("number");
+ });
+
+ it("returns zero usage values when metrics are unavailable", () => {
+ expect(component.getCpuValue()).toBe(0);
+ expect(component.getMemoryValue()).toBe(0);
+ });
+
+ it("shows GPU selection when more than one option is available", () => {
+ expect(component.showGpuSelection()).toBe(true);
+ });
+ });
+});