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-7334-c76d7b3f864808ba95d5847f661731bfcc8fd248 in repository https://gitbox.apache.org/repos/asf/texera.git
commit cc8308740361324f7f129657dad6dc395abeeb1c Author: Meng Wang <[email protected]> AuthorDate: Wed Aug 5 22:46:01 2026 -0700 test(frontend): extend UserVenvComponent template coverage (#7334) ### What changes were proposed in this PR? Extends `user-venv.component.spec.ts` to render the template branches the existing class-focused tests never exercised, taking the template from ~36% to 100% (`user-venv.component.html` 102/102). 9 new tests plus one class-guard test: - **list branches** — the empty-state message when there are no environments; a row per environment (with the `(unnamed)` fallback); clicking a row opens its draft; the delete icon fires `confirmDeletePve` and stops row-open propagation. - **modal** — the Create button opens an empty draft; opening the modal renders the form, the package header, one row per package, and the footer into the CDK overlay; the add-package / delete-toggle / Save / Close controls are driven through the DOM; the `nz-modal` cancel (X/mask) output closes the modal. - a `saveEnvironment` no-op-without-draft guard test brings the class to 100% too. Interactions go through the DOM (`By.css` + `triggerEventHandler` / native `click`); the `nz-modal` body/footer render into the overlay, flushed with `ApplicationRef.tick()`. The backing `WorkflowPveService` is mocked; no layout assertions. No production code was changed. ### Any related issues, documentation, discussions? Closes #7331. ### How was this PR tested? `ng test --watch=false --include src/app/dashboard/component/user/user-venv/user-venv.component.spec.ts` — 33 passed, run 3× for determinism (the modal renders into the overlay). Coverage (`--coverage`) confirms `user-venv.component.html` at 102/102 and the component class at 89/89. The failure path was verified by breaking an assertion (red, non-zero exit); eslint and prettier are clean. ### Was this PR authored or co-authored using generative AI tooling? Generated-by: Claude Code (Opus 4.8 [1M context]) --- .../user/user-venv/user-venv.component.spec.ts | 136 +++++++++++++++++++++ 1 file changed, 136 insertions(+) diff --git a/frontend/src/app/dashboard/component/user/user-venv/user-venv.component.spec.ts b/frontend/src/app/dashboard/component/user/user-venv/user-venv.component.spec.ts index 359c687780..c1807a3048 100644 --- a/frontend/src/app/dashboard/component/user/user-venv/user-venv.component.spec.ts +++ b/frontend/src/app/dashboard/component/user/user-venv/user-venv.component.spec.ts @@ -17,7 +17,9 @@ * under the License. */ +import { ApplicationRef } from "@angular/core"; import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { By } from "@angular/platform-browser"; import { NoopAnimationsModule } from "@angular/platform-browser/animations"; import { DeleteOutline, FileAddOutline, PlusOutline } from "@ant-design/icons-angular/icons"; import { NzIconModule } from "ng-zorro-antd/icon"; @@ -306,6 +308,14 @@ describe("UserVenvComponent", () => { expect(component.currentDraft).not.toBeNull(); expect(component.pveModalVisible).toBe(true); }); + + it("is a no-op when there is no draft", () => { + component.currentDraft = null; + component.saveEnvironment(); + expect(pveServiceSpy.savePve).not.toHaveBeenCalled(); + expect(pveServiceSpy.updateUserPve).not.toHaveBeenCalled(); + expect(component.saving).toBe(false); + }); }); describe("confirmDeletePve", () => { @@ -377,4 +387,130 @@ describe("UserVenvComponent", () => { expect(component.trackByVeid(1, { name: "", newPackages: [] })).toBeUndefined(); }); }); + + // The class is well covered above; these exercise the template itself — the list + // branches and the nz-modal body/footer, which render into the CDK overlay. + describe("template rendering", () => { + type Draft = NonNullable<UserVenvComponent["currentDraft"]>; + + // nz-modal renders into the overlay attached to ApplicationRef, so tick() after + // detectChanges to flush its embedded view. + const flushOverlay = (): void => { + fixture.detectChanges(); + TestBed.inject(ApplicationRef).tick(); + }; + // Assert an element exists so a bad selector fails as "not found" instead of a null deref. + const q = <E extends Element>(root: ParentNode, selector: string): E => { + const el = root.querySelector(selector); + expect(el, `expected to find "${selector}"`).not.toBeNull(); + return el as unknown as E; + }; + const overlay = (): HTMLElement => q<HTMLElement>(document, ".cdk-overlay-container"); + // Pick a footer button by its label so the tests survive button reordering. + const footerButton = (root: ParentNode, label: string): HTMLButtonElement => { + const btn = Array.from(root.querySelectorAll<HTMLButtonElement>(".footer-all button")).find( + b => b.textContent?.trim() === label + ); + expect(btn, `expected a footer button labelled "${label}"`).toBeDefined(); + return btn as HTMLButtonElement; + }; + + const openModalWith = (draft: Draft): HTMLElement => { + component.currentDraft = draft; + component.pveModalVisible = true; + flushOverlay(); + return overlay(); + }; + + const seedList = (records: UserPveRecord[]): void => { + pveServiceSpy.listUserPves.mockReturnValue(of(records)); + fixture.detectChanges(); + }; + + it("shows the empty-state message and no list when there are no environments", () => { + seedList([]); + const host = fixture.nativeElement as HTMLElement; + expect(host.querySelector(".python-env-page-empty")?.textContent).toContain("No environments yet"); + expect(host.querySelector("ul.python-env-page-list")).toBeNull(); + }); + + it("opens an empty draft modal from the Create button", () => { + fixture.detectChanges(); + fixture.debugElement.query(By.css(".create-btn")).triggerEventHandler("click", {}); + expect(component.pveModalVisible).toBe(true); + expect(component.currentDraft).toEqual({ name: "", newPackages: [] }); + }); + + it("renders a row per environment (with the unnamed fallback) and opens the row on click", () => { + seedList([ + { veid: 1, name: "envA", packages: {} }, + { veid: 2, name: "", packages: {} }, + ] as UserPveRecord[]); + + const rows = fixture.debugElement.queryAll(By.css("li.python-env-page-item")); + expect(rows.length).toBe(2); + expect((fixture.nativeElement as HTMLElement).textContent).toContain("(unnamed)"); + + rows[0].triggerEventHandler("click", {}); + expect(component.pveModalVisible).toBe(true); + expect(component.currentDraft?.name).toBe("envA"); + }); + + it("fires confirmDeletePve from the row delete icon and stops row-open propagation", () => { + seedList([{ veid: 3, name: "envDel", packages: {} }] as UserPveRecord[]); + const stopPropagation = vi.fn(); + fixture.debugElement.query(By.css(".python-env-delete-icon")).triggerEventHandler("click", { stopPropagation }); + expect(stopPropagation).toHaveBeenCalled(); + expect(confirmSpy).toHaveBeenCalledTimes(1); + expect(component.pveModalVisible).toBe(false); + }); + + it("renders the modal form, package header, one row per package, and the footer when open", () => { + fixture.detectChanges(); + const o = openModalWith({ + name: "envForm", + newPackages: [ + { name: "numpy", versionOp: "==", version: "1.2" }, + { name: "pandas", versionOp: ">=", version: "2.0" }, + ], + }); + + expect(o.querySelector(".ve-form")).not.toBeNull(); + // header row (*ngIf newPackages.length > 0) + one row per package + expect(o.querySelectorAll(".package-row").length).toBe(3); + expect(o.querySelector(".add-btn button")).not.toBeNull(); + expect(o.querySelectorAll(".footer-all button").length).toBe(2); + }); + + it("drives the modal package controls and the Save footer button through the DOM", () => { + fixture.detectChanges(); + const o = openModalWith({ name: "envDrive", newPackages: [{ name: "x", versionOp: "==", version: "1" }] }); + + q<HTMLButtonElement>(o, ".add-btn button").click(); + flushOverlay(); + expect(component.currentDraft?.newPackages.length).toBe(2); + + q<HTMLButtonElement>(o, ".package-row .user-package-inputs button").click(); + expect(component.currentDraft?.newPackages[0].deleteToggle).toBe(true); + + footerButton(o, "Save").click(); + expect(pveServiceSpy.savePve).toHaveBeenCalledWith("envDrive", {}); + }); + + it("closes the modal from the footer Close button", () => { + fixture.detectChanges(); + const o = openModalWith({ name: "envClose", newPackages: [] }); + footerButton(o, "Close").click(); + expect(component.pveModalVisible).toBe(false); + expect(component.currentDraft).toBeNull(); + }); + + it("closes the modal on the nz-modal cancel (X / mask) output", () => { + fixture.detectChanges(); + openModalWith({ name: "envCancel", newPackages: [] }); + fixture.debugElement.query(By.css("nz-modal")).triggerEventHandler("nzOnCancel", null); + expect(component.pveModalVisible).toBe(false); + expect(component.currentDraft).toBeNull(); + }); + }); });
