This is an automated email from the ASF dual-hosted git repository.

github-merge-queue[bot] pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/texera.git


The following commit(s) were added to refs/heads/main by this push:
     new cc83087403 test(frontend): extend UserVenvComponent template coverage 
(#7334)
cc83087403 is described below

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

Reply via email to