You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@texera.apache.org by "mengw15 (via GitHub)" <gi...@apache.org> on 2026/08/05 11:12:06 UTC

[PR] test(frontend): extend UserVenvComponent template coverage [texera]

mengw15 opened a new pull request, #7334:
URL: https://github.com/apache/texera/pull/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])
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] test(frontend): extend UserVenvComponent template coverage [texera]

Posted by "codecov-commenter (via GitHub)" <gi...@apache.org>.
codecov-commenter commented on PR #7334:
URL: https://github.com/apache/texera/pull/7334#issuecomment-5192848542

   ## [Codecov](https://app.codecov.io/gh/apache/texera/pull/7334?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) Report
   :white_check_mark: All modified and coverable lines are covered by tests.
   :white_check_mark: Project coverage is 83.45%. Comparing base ([`0f2cd49`](https://app.codecov.io/gh/apache/texera/commit/0f2cd49c39781d683f35e02547da45fbe5adc3c3?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache)) to head ([`18fe57c`](https://app.codecov.io/gh/apache/texera/commit/18fe57c9e2fba157eee5787fc64fd753391fc758?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache)).
   
   <details><summary>Additional details and impacted files</summary>
   
   
   
   ```diff
   @@             Coverage Diff              @@
   ##               main    #7334      +/-   ##
   ============================================
   + Coverage     83.36%   83.45%   +0.09%     
     Complexity     4128     4128              
   ============================================
     Files          1166     1166              
     Lines         46428    46428              
     Branches       5174     5174              
   ============================================
   + Hits          38705    38747      +42     
   + Misses         6003     5962      -41     
   + Partials       1720     1719       -1     
   ```
   
   | [Flag](https://app.codecov.io/gh/apache/texera/pull/7334/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | Coverage Δ | | *Carryforward flag |
   |---|---|---|---|
   | [access-control-service](https://app.codecov.io/gh/apache/texera/pull/7334/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `70.00% <ø> (ø)` | | Carriedforward from [0f2cd49](https://app.codecov.io/gh/apache/texera/commit/0f2cd49c39781d683f35e02547da45fbe5adc3c3?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
   | [agent-service](https://app.codecov.io/gh/apache/texera/pull/7334/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `83.65% <ø> (ø)` | | Carriedforward from [0f2cd49](https://app.codecov.io/gh/apache/texera/commit/0f2cd49c39781d683f35e02547da45fbe5adc3c3?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
   | [amber](https://app.codecov.io/gh/apache/texera/pull/7334/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `80.65% <ø> (ø)` | | Carriedforward from [0f2cd49](https://app.codecov.io/gh/apache/texera/commit/0f2cd49c39781d683f35e02547da45fbe5adc3c3?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
   | [computing-unit-managing-service](https://app.codecov.io/gh/apache/texera/pull/7334/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `43.60% <ø> (ø)` | | Carriedforward from [0f2cd49](https://app.codecov.io/gh/apache/texera/commit/0f2cd49c39781d683f35e02547da45fbe5adc3c3?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
   | [config-service](https://app.codecov.io/gh/apache/texera/pull/7334/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `65.97% <ø> (ø)` | | Carriedforward from [0f2cd49](https://app.codecov.io/gh/apache/texera/commit/0f2cd49c39781d683f35e02547da45fbe5adc3c3?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
   | [file-service](https://app.codecov.io/gh/apache/texera/pull/7334/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `69.05% <ø> (ø)` | | Carriedforward from [0f2cd49](https://app.codecov.io/gh/apache/texera/commit/0f2cd49c39781d683f35e02547da45fbe5adc3c3?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
   | [frontend](https://app.codecov.io/gh/apache/texera/pull/7334/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `84.23% <ø> (+0.20%)` | :arrow_up: | |
   | [notebook-migration-service](https://app.codecov.io/gh/apache/texera/pull/7334/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `78.89% <ø> (ø)` | | Carriedforward from [0f2cd49](https://app.codecov.io/gh/apache/texera/commit/0f2cd49c39781d683f35e02547da45fbe5adc3c3?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
   | [pyamber](https://app.codecov.io/gh/apache/texera/pull/7334/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `97.36% <ø> (ø)` | | Carriedforward from [0f2cd49](https://app.codecov.io/gh/apache/texera/commit/0f2cd49c39781d683f35e02547da45fbe5adc3c3?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
   | [workflow-compiling-service](https://app.codecov.io/gh/apache/texera/pull/7334/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `26.31% <ø> (ø)` | | Carriedforward from [0f2cd49](https://app.codecov.io/gh/apache/texera/commit/0f2cd49c39781d683f35e02547da45fbe5adc3c3?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
   
   *This pull request uses carry forward flags. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) to find out more.
   </details>
   
   [:umbrella: View full report in Codecov by Harness](https://app.codecov.io/gh/apache/texera/pull/7334?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache).   
   :loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache).
   <details><summary> :rocket: New features to boost your workflow: </summary>
   
   - :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
   </details>


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] test(frontend): extend UserVenvComponent template coverage [texera]

Posted by "mengw15 (via GitHub)" <gi...@apache.org>.
mengw15 commented on code in PR #7334:
URL: https://github.com/apache/texera/pull/7334#discussion_r3720293408


##########
frontend/src/app/dashboard/component/user/user-venv/user-venv.component.spec.ts:
##########
@@ -377,4 +387,116 @@ 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();
+    };
+    const overlay = (): HTMLElement => document.querySelector(".cdk-overlay-container") as HTMLElement;

Review Comment:
   `overlay()` now goes through a null-asserting `q()` helper. Cross-test overlay contamination is already prevented — the top-level `afterEach` runs `fixture.destroy()` and clears `.cdk-overlay-container`, so every test opens into a clean overlay.



##########
frontend/src/app/dashboard/component/user/user-venv/user-venv.component.spec.ts:
##########
@@ -377,4 +387,116 @@ 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();
+    };
+    const overlay = (): HTMLElement => document.querySelector(".cdk-overlay-container") as HTMLElement;
+
+    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" }] });
+
+      (o.querySelector(".add-btn button") as HTMLButtonElement).click();

Review Comment:
   Routed through a `q<E>(root, selector)` helper that asserts the element exists first, so a missing selector fails as `expected to find "…"` instead of a null dereference.



##########
frontend/src/app/dashboard/component/user/user-venv/user-venv.component.spec.ts:
##########
@@ -377,4 +387,116 @@ 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();
+    };
+    const overlay = (): HTMLElement => document.querySelector(".cdk-overlay-container") as HTMLElement;
+
+    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" }] });
+
+      (o.querySelector(".add-btn button") as HTMLButtonElement).click();
+      flushOverlay();
+      expect(component.currentDraft?.newPackages.length).toBe(2);
+
+      (o.querySelector(".package-row .user-package-inputs button") as HTMLButtonElement).click();

Review Comment:
   Routed through a `q<E>(root, selector)` helper that asserts the element exists first, so a missing selector fails as `expected to find "…"` instead of a null dereference.



##########
frontend/src/app/dashboard/component/user/user-venv/user-venv.component.spec.ts:
##########
@@ -377,4 +387,116 @@ 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();
+    };
+    const overlay = (): HTMLElement => document.querySelector(".cdk-overlay-container") as HTMLElement;
+
+    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" }] });
+
+      (o.querySelector(".add-btn button") as HTMLButtonElement).click();
+      flushOverlay();
+      expect(component.currentDraft?.newPackages.length).toBe(2);
+
+      (o.querySelector(".package-row .user-package-inputs button") as HTMLButtonElement).click();
+      expect(component.currentDraft?.newPackages[0].deleteToggle).toBe(true);
+
+      (o.querySelectorAll(".footer-all button")[1] as HTMLButtonElement).click();

Review Comment:
   Switched to a `footerButton(root, label)` helper that matches by button text (`"Save"` / `"Close"`), so the tests survive footer-button reordering.



##########
frontend/src/app/dashboard/component/user/user-venv/user-venv.component.spec.ts:
##########
@@ -377,4 +387,116 @@ 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();
+    };
+    const overlay = (): HTMLElement => document.querySelector(".cdk-overlay-container") as HTMLElement;
+
+    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" }] });
+
+      (o.querySelector(".add-btn button") as HTMLButtonElement).click();
+      flushOverlay();
+      expect(component.currentDraft?.newPackages.length).toBe(2);
+
+      (o.querySelector(".package-row .user-package-inputs button") as HTMLButtonElement).click();
+      expect(component.currentDraft?.newPackages[0].deleteToggle).toBe(true);
+
+      (o.querySelectorAll(".footer-all button")[1] as HTMLButtonElement).click();
+      expect(pveServiceSpy.savePve).toHaveBeenCalledWith("envDrive", {});
+    });
+
+    it("closes the modal from the footer Close button", () => {
+      fixture.detectChanges();
+      const o = openModalWith({ name: "envClose", newPackages: [] });
+      (o.querySelectorAll(".footer-all button")[0] as HTMLButtonElement).click();

Review Comment:
   Switched to a `footerButton(root, label)` helper that matches by button text (`"Save"` / `"Close"`), so the tests survive footer-button reordering.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] test(frontend): extend UserVenvComponent template coverage [texera]

Posted by "github-actions[bot] (via GitHub)" <gi...@apache.org>.
github-actions[bot] commented on PR #7334:
URL: https://github.com/apache/texera/pull/7334#issuecomment-5191346001

   <!-- texera-reviewer-suggestion -->
   ### Automated Reviewer Suggestions
   
   Based on the `git blame` history of the changed files, we recommend the following reviewers:
   
   - **Contributors with relevant context:** `@aglinxinyuan`
     You can notify them by mentioning `@aglinxinyuan` in a comment.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] test(frontend): extend UserVenvComponent template coverage [texera]

Posted by "Copilot (via GitHub)" <gi...@apache.org>.
Copilot commented on code in PR #7334:
URL: https://github.com/apache/texera/pull/7334#discussion_r3720165449


##########
frontend/src/app/dashboard/component/user/user-venv/user-venv.component.spec.ts:
##########
@@ -377,4 +387,116 @@ 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();
+    };
+    const overlay = (): HTMLElement => document.querySelector(".cdk-overlay-container") as HTMLElement;

Review Comment:
   This helper unsafely casts the result of `document.querySelector(...)` to `HTMLElement`. If the overlay container isn’t present (or is removed between tests), this becomes `null` at runtime and will fail with a cryptic error later. Prefer using Angular CDK’s `OverlayContainer` test utility (or at minimum assert non-null before casting) and consider cleaning the overlay container in `afterEach` to avoid cross-test contamination.



##########
frontend/src/app/dashboard/component/user/user-venv/user-venv.component.spec.ts:
##########
@@ -377,4 +387,116 @@ 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();
+    };
+    const overlay = (): HTMLElement => document.querySelector(".cdk-overlay-container") as HTMLElement;
+
+    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" }] });
+
+      (o.querySelector(".add-btn button") as HTMLButtonElement).click();
+      flushOverlay();
+      expect(component.currentDraft?.newPackages.length).toBe(2);
+
+      (o.querySelector(".package-row .user-package-inputs button") as HTMLButtonElement).click();
+      expect(component.currentDraft?.newPackages[0].deleteToggle).toBe(true);
+
+      (o.querySelectorAll(".footer-all button")[1] as HTMLButtonElement).click();
+      expect(pveServiceSpy.savePve).toHaveBeenCalledWith("envDrive", {});
+    });
+
+    it("closes the modal from the footer Close button", () => {
+      fixture.detectChanges();
+      const o = openModalWith({ name: "envClose", newPackages: [] });
+      (o.querySelectorAll(".footer-all button")[0] as HTMLButtonElement).click();

Review Comment:
   Index-based selection of footer buttons (`[1]` for Save, `[0]` for Close) is brittle and can break if button order changes (or if another button is added). Use a more stable selector (e.g., `data-testid`, an `aria-label`, or matching by button text/content) so the tests fail only when behavior changes, not when markup is rearranged.



##########
frontend/src/app/dashboard/component/user/user-venv/user-venv.component.spec.ts:
##########
@@ -377,4 +387,116 @@ 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();
+    };
+    const overlay = (): HTMLElement => document.querySelector(".cdk-overlay-container") as HTMLElement;
+
+    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" }] });
+
+      (o.querySelector(".add-btn button") as HTMLButtonElement).click();
+      flushOverlay();
+      expect(component.currentDraft?.newPackages.length).toBe(2);
+
+      (o.querySelector(".package-row .user-package-inputs button") as HTMLButtonElement).click();
+      expect(component.currentDraft?.newPackages[0].deleteToggle).toBe(true);
+
+      (o.querySelectorAll(".footer-all button")[1] as HTMLButtonElement).click();

Review Comment:
   Index-based selection of footer buttons (`[1]` for Save, `[0]` for Close) is brittle and can break if button order changes (or if another button is added). Use a more stable selector (e.g., `data-testid`, an `aria-label`, or matching by button text/content) so the tests fail only when behavior changes, not when markup is rearranged.



##########
frontend/src/app/dashboard/component/user/user-venv/user-venv.component.spec.ts:
##########
@@ -377,4 +387,116 @@ 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();
+    };
+    const overlay = (): HTMLElement => document.querySelector(".cdk-overlay-container") as HTMLElement;
+
+    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" }] });
+
+      (o.querySelector(".add-btn button") as HTMLButtonElement).click();

Review Comment:
   These assertions rely on `querySelector(...) as HTMLButtonElement` and will throw if the selector doesn’t match (e.g., template refactor), producing less actionable failures. Prefer asserting the element exists before interacting (or use `fixture.debugElement.query(...)` with expectations) so failures clearly indicate 'element not found' vs. a null dereference.



##########
frontend/src/app/dashboard/component/user/user-venv/user-venv.component.spec.ts:
##########
@@ -377,4 +387,116 @@ 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();
+    };
+    const overlay = (): HTMLElement => document.querySelector(".cdk-overlay-container") as HTMLElement;
+
+    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" }] });
+
+      (o.querySelector(".add-btn button") as HTMLButtonElement).click();
+      flushOverlay();
+      expect(component.currentDraft?.newPackages.length).toBe(2);
+
+      (o.querySelector(".package-row .user-package-inputs button") as HTMLButtonElement).click();

Review Comment:
   These assertions rely on `querySelector(...) as HTMLButtonElement` and will throw if the selector doesn’t match (e.g., template refactor), producing less actionable failures. Prefer asserting the element exists before interacting (or use `fixture.debugElement.query(...)` with expectations) so failures clearly indicate 'element not found' vs. a null dereference.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] test(frontend): extend UserVenvComponent template coverage [texera]

Posted by "aglinxinyuan (via GitHub)" <gi...@apache.org>.
aglinxinyuan merged PR #7334:
URL: https://github.com/apache/texera/pull/7334


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org