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/08/08 11:52:40 UTC

(texera) branch gh-readonly-queue/main/pr-7418-96ed553760a299f2031f4cd83c024e9327cb5346 created (now 35346629ce)

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

github-merge-queue[bot] pushed a change to branch gh-readonly-queue/main/pr-7418-96ed553760a299f2031f4cd83c024e9327cb5346
in repository https://gitbox.apache.org/repos/asf/texera.git


      at 35346629ce test(frontend): extend UserProjectListItemComponent template coverage (#7418)

This branch includes the following new commits:

     new 35346629ce test(frontend): extend UserProjectListItemComponent template coverage (#7418)

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(texera) 01/01: test(frontend): extend UserProjectListItemComponent template coverage (#7418)

Posted by gi...@apache.org.
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-7418-96ed553760a299f2031f4cd83c024e9327cb5346
in repository https://gitbox.apache.org/repos/asf/texera.git

commit 35346629ce00493927556df92fc839cb79b78dfa
Author: Meng Wang <me...@uci.edu>
AuthorDate: Sat Aug 8 04:52:20 2026 -0700

    test(frontend): extend UserProjectListItemComponent template coverage (#7418)
    
    ### What changes were proposed in this PR?
    
    Rebased onto `main` after #7415 landed on the same file, and reduced to
    what that
    PR left uncovered. #7415 pins what each template branch *looks like* by
    putting the
    component into the state directly; this PR pins the wiring that produces
    those
    states, so a control losing its handler fails here. Together they take
    `user-project-list-item.component.html` to 100% (110/110 statements, no
    uncovered
    branches — it was 77/110 after #7415).
    
    8 new tests:
    
    - **colour panel** — the `[(colorPicker)]` / `(colorPickerSelect)`
    outputs, and the
    `cpExtraTemplate` menu, whose markup exists only while the picker is
    open: its
    Save action, and its Delete action in both states (disabled while the
    project has
    no colour, enabled and wired once one is set). None of this was
    previously
      rendered — it is the bulk of the gap #7410 describes.
    - **name** — the edit button opens the input, `keyup.enter` saves,
    `focusout` closes.
    - **description** — the expand/collapse controls, the edit button,
    `focusout`
      saving, and the suffix save icon closing the editor.
    - **actions** — the share button and the delete popconfirm's
    `nzOnConfirm`.
    - three class-level gaps: the `entry` getter's guard, `ngOnInit`
    adopting a stored
    colour, and `updateProjectColor` skipping the service when the colour is
      unchanged (class 63/67 -> 66/67).
    
    This block queries with `By.css` + `triggerEventHandler` rather than
    `querySelector`: `nzOnConfirm`, `keyup.enter` and `colorPickerSelect`
    are directive
    outputs, not DOM events, so a native dispatch cannot reach them. #7415's
    `MarkdownModule.forRoot()` is kept as-is. No production code was
    changed.
    
    Tests that #7415 already covers (the creation date, the `editable`
    gating, the
    read-only delete branch) were dropped from this PR rather than
    duplicated.
    
    One statement stays uncovered: the `if (!this.entry) throw` guard inside
    `saveProjectName`'s subscribe. The `entry` getter already throws when no
    entry was
    provided, so that branch cannot be reached — it is dead code rather than
    a coverage
    gap, and removing it felt out of scope for a test-only PR.
    
    ### Any related issues, documentation, discussions?
    
    Closes #7410. Builds on #7415 (#7412), which covers the same template
    from the
    rendering side.
    
    ### How was this PR tested?
    
    `ng test --watch=false --include
    src/app/dashboard/component/user/user-project/user-project-list-item/user-project-list-item.component.spec.ts`
    — 28 passed (20 existing + 8 new), run 3x for determinism. Coverage
    (`--coverage`)
    confirms `user-project-list-item.component.html` at 110/110 statements
    with no
    uncovered branches, and the class at 66/67. 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-project-list-item.component.spec.ts       | 167 +++++++++++++++++++++
 1 file changed, 167 insertions(+)

diff --git a/frontend/src/app/dashboard/component/user/user-project/user-project-list-item/user-project-list-item.component.spec.ts b/frontend/src/app/dashboard/component/user/user-project/user-project-list-item/user-project-list-item.component.spec.ts
index f2198854b1..58c57b18af 100644
--- a/frontend/src/app/dashboard/component/user/user-project/user-project-list-item/user-project-list-item.component.spec.ts
+++ b/frontend/src/app/dashboard/component/user/user-project/user-project-list-item/user-project-list-item.component.spec.ts
@@ -32,6 +32,8 @@ import { UserService } from "../../../../../common/service/user/user.service";
 import { commonTestProviders } from "../../../../../common/testing/test-utils";
 import { ShareAccessComponent } from "../../share-access/share-access.component";
 import { DatePipe } from "@angular/common";
+import { By } from "@angular/platform-browser";
+import { ColorPickerDirective } from "ngx-color-picker";
 import { of } from "rxjs";
 import { MarkdownModule } from "ngx-markdown";
 
@@ -131,6 +133,16 @@ describe("UserProjectListItemComponent", () => {
       expect(spy).not.toHaveBeenCalled();
     });
 
+    it("does not call the service when the color is unchanged", () => {
+      const spy = vi.spyOn(userProjectService, "updateProjectColor");
+      component.entry = { ...component.entry, color: "123456" };
+      component.color = "#123456";
+
+      component.updateProjectColor();
+
+      expect(spy).not.toHaveBeenCalled();
+    });
+
     it("is a no-op when the item is not editable", () => {
       const spy = vi.spyOn(userProjectService, "updateProjectColor");
       component.editable = false;
@@ -332,4 +344,159 @@ describe("UserProjectListItemComponent", () => {
       expect(hostFixture.nativeElement.querySelector(".ant-input-clear-icon")).not.toBeNull();
     });
   });
+
+  /**
+   * The block above pins what each branch looks like by putting the component into that state
+   * directly. What is left unpinned is the wiring in between: the controls that produce those
+   * states, the outputs the parent listens to, and the colour menu, whose markup only exists
+   * while the picker is open. These go through the DOM so a control losing its handler fails
+   * here. Events that ng-zorro / the colour picker raise as directive outputs rather than DOM
+   * events (`nzOnConfirm`, `keyup.enter`, `colorPickerSelect`) need DebugElement, so this block
+   * queries with `By.css` instead of `querySelector`.
+   */
+  describe("rendered controls", () => {
+    // Name what was missing, so a markup change fails with the element it could not find rather
+    // than "Cannot read properties of undefined".
+    const required = <T>(value: T | null | undefined, what: string): T => {
+      if (value === null || value === undefined) {
+        throw new Error(`expected ${what} to be rendered`);
+      }
+      return value;
+    };
+    const q = (selector: string) => hostFixture.debugElement.query(By.css(selector));
+    const buttonLabelled = (label: string) =>
+      required(
+        hostFixture.debugElement
+          .queryAll(By.css("button"))
+          .find(button => (button.nativeElement.textContent ?? "").trim() === label),
+        `a button labelled "${label}"`
+      );
+
+    // The colour menu lives in `cpExtraTemplate`, so it exists only once the picker is open,
+    // which `[(cpToggle)]="editingColor"` drives.
+    const openColorPanel = () => {
+      component.editingColor = true;
+      hostFixture.detectChanges();
+    };
+
+    afterEach(() => {
+      // An open colour picker keeps document-level listeners; destroying the view tears them
+      // down so a panel cannot outlive its test.
+      hostFixture.destroy();
+    });
+
+    it("wires the colour-picker outputs to the component", () => {
+      const updateSpy = vi.spyOn(component, "updateProjectColor").mockImplementation(() => {});
+      const picker = hostFixture.debugElement.query(By.directive(ColorPickerDirective));
+
+      picker.triggerEventHandler("colorPickerChange", "#abcdef");
+      picker.triggerEventHandler("colorPickerSelect", "#abcdef");
+
+      expect(component.color).toBe("#abcdef");
+      expect(updateSpy).toHaveBeenCalled();
+    });
+
+    it("wires the colour menu's Save action", () => {
+      const updateSpy = vi.spyOn(component, "updateProjectColor").mockImplementation(() => {});
+
+      openColorPanel();
+      buttonLabelled("Save").triggerEventHandler("click", null);
+
+      expect(updateSpy).toHaveBeenCalled();
+    });
+
+    it("enables the colour menu's Delete action only once a colour is set", () => {
+      const removeSpy = vi.spyOn(component, "removeProjectColor").mockImplementation(() => {});
+
+      // testProject starts with color: null, so there is nothing to delete yet.
+      openColorPanel();
+      expect(buttonLabelled("Delete").nativeElement.disabled).toBe(true);
+
+      component.entry = { ...component.entry, color: "123456" };
+      hostFixture.detectChanges();
+      const deleteButton = buttonLabelled("Delete");
+      expect(deleteButton.nativeElement.disabled).toBe(false);
+
+      deleteButton.triggerEventHandler("click", null);
+
+      expect(removeSpy).toHaveBeenCalled();
+    });
+
+    it("opens name editing, saves on enter, and closes on focusout", () => {
+      const saveSpy = vi.spyOn(component, "saveProjectName").mockImplementation(() => {});
+
+      required(q(".edit-name-icon").parent, "the edit-name button").triggerEventHandler("click", null);
+      hostFixture.detectChanges();
+      expect(component.editingName).toBe(true);
+
+      const input = q("input");
+      input.nativeElement.value = "renamed";
+      input.triggerEventHandler("keyup.enter", {});
+      expect(saveSpy).toHaveBeenCalledWith("renamed");
+
+      input.triggerEventHandler("focusout", {});
+      expect(component.editingName).toBe(false);
+    });
+
+    it("expands and re-collapses the description through its controls", () => {
+      q('[nz-tooltip="Expand Description"]').triggerEventHandler("click", null);
+      hostFixture.detectChanges();
+      expect(component.descriptionCollapsed).toBe(false);
+
+      q('[nz-tooltip="Collapse Description"]').triggerEventHandler("click", null);
+      hostFixture.detectChanges();
+      expect(component.descriptionCollapsed).toBe(true);
+    });
+
+    it("opens description editing, saves on focusout, and closes through the save icon", () => {
+      const saveSpy = vi.spyOn(component, "saveProjectDescription").mockImplementation(() => {});
+
+      required(q(".edit-description-icon").parent, "the edit-description button").triggerEventHandler("click", null);
+      hostFixture.detectChanges();
+      expect(component.editingDescription).toBe(true);
+
+      const textarea = q("textarea");
+      textarea.nativeElement.value = "a new description";
+      hostFixture.detectChanges();
+
+      textarea.triggerEventHandler("focusout", {});
+      expect(saveSpy).toHaveBeenCalledWith("a new description");
+
+      q(".ant-input-clear-icon").triggerEventHandler("click", null);
+      hostFixture.detectChanges();
+      expect(component.editingDescription).toBe(false);
+    });
+
+    it("wires the share and delete actions", () => {
+      const shareSpy = vi.spyOn(component, "onClickOpenShareAccess").mockImplementation(() => {});
+      const deletedSpy = vi.spyOn(component.deleted, "emit");
+      const [share, remove] = hostFixture.debugElement.queryAll(By.css("ul[nz-list-item-actions] button"));
+
+      share.triggerEventHandler("click", null);
+      remove.triggerEventHandler("nzOnConfirm", null);
+
+      expect(shareSpy).toHaveBeenCalled();
+      expect(deletedSpy).toHaveBeenCalled();
+    });
+  });
+
+  describe("entry input", () => {
+    it("throws when read before an entry is provided", () => {
+      component.entry = undefined as unknown as DashboardProject;
+
+      expect(() => component.entry).toThrowError("entry property must be provided");
+    });
+  });
+
+  describe("ngOnInit", () => {
+    it("adopts the project's stored colour", () => {
+      // A fresh fixture (rather than the shared one) so ngOnInit sees the colour.
+      const fixture = TestBed.createComponent(TestHostComponent);
+      fixture.componentInstance.entry = { ...testProject, color: "123456" };
+      fixture.detectChanges();
+
+      expect(fixture.componentInstance.inner.color).toBe("123456");
+      fixture.destroy();
+    });
+  });
 });