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 07:28:43 UTC

(texera) branch gh-readonly-queue/main/pr-7415-69c7391a5f613b165ce7276f81811a98de48c442 created (now 97dac3c2db)

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-7415-69c7391a5f613b165ce7276f81811a98de48c442
in repository https://gitbox.apache.org/repos/asf/texera.git


      at 97dac3c2db test(frontend): render the project list item's permission and description rules (#7415)

This branch includes the following new commits:

     new 97dac3c2db test(frontend): render the project list item's permission and description rules (#7415)

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): render the project list item's permission and description rules (#7415)

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-7415-69c7391a5f613b165ce7276f81811a98de48c442
in repository https://gitbox.apache.org/repos/asf/texera.git

commit 97dac3c2db0055a5bb98a23d6741ace3fc106742
Author: Xinyuan Lin <xi...@uci.edu>
AuthorDate: Sat Aug 8 00:24:58 2026 -0700

    test(frontend): render the project list item's permission and description rules (#7415)
    
    ### What changes were proposed in this PR?
    
    `UserProjectListItemComponent` decides in its template what a viewer may
    touch, and none of it was rendered — the existing specs call the save
    and colour methods directly.
    
    Adds 8 tests. The one that matters most is the `editable` gating: a
    project the viewer only holds READ on must not be offered the rename,
    add-description, share or delete controls, and that decision lives
    entirely in two `*ngIf="editable"` guards plus one on the action list.
    
    Also covered: the name/edit-input swap, the description starting
    collapsed and expanding on request, the `trim()` guard that stops a
    whitespace-only description rendering an empty expander, the character
    counter, the save icon appearing only once the text actually differs,
    and the creation-date format.
    
    **Verified by mutation**, all reverted (template diff empty):
    
    | Mutation | Result |
    |---|---|
    | show the rename button to a read-only viewer | red |
    | show the share/delete actions to a read-only viewer | red |
    | invert the name / edit-input branch | red |
    | drop the collapse guard | red |
    | drop the whitespace `trim()` guard | red |
    | always show the save icon | red |
    | count characters against the max instead of the text | red |
    | change the creation-date format | red |
    
    Two things worth recording:
    
    - `MarkdownModule.forRoot()` joins the TestBed. An expanded description
    renders a `<markdown>` element, and no existing test reached that path,
    so `MarkdownService` had never been needed.
    - `descriptionCollapsed` defaults to **true**. My first version of the
    collapse test asserted the opposite and failed, which also revealed that
    the whitespace test would have passed vacuously — collapsed hides the
    block regardless. It now expands first, so the `trim()` guard is the
    only thing left doing the work.
    
    No production file is touched.
    
    ### Any related issues, documentation, discussions?
    
    Closes #7412
    
    ### How was this PR tested?
    
    ```
    npx ng test --watch=false --include="**/user-project-list-item.component.spec.ts"
    ```
    
    ```
     Test Files  1 passed (1)
          Tests  18 passed (18)
    ```
    
    8 new on top of the existing 10. `yarn format:ci` passes.
    
    ### Was this PR authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code (Opus 5)
---
 .../user-project-list-item.component.spec.ts       | 116 ++++++++++++++++++++-
 1 file changed, 115 insertions(+), 1 deletion(-)

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 2263f8553d..f2198854b1 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
@@ -31,7 +31,9 @@ import { StubUserService } from "../../../../../common/service/user/stub-user.se
 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 { of } from "rxjs";
+import { MarkdownModule } from "ngx-markdown";
 
 // UserProjectListItemComponent is rooted at <nz-list-item>; instantiating it
 // outside an <nz-list> host throws "No provider found for NzListComponent".
@@ -71,7 +73,8 @@ describe("UserProjectListItemComponent", () => {
 
   beforeEach(async () => {
     await TestBed.configureTestingModule({
-      imports: [TestHostComponent, HttpClientTestingModule],
+      // MarkdownModule.forRoot() backs the <markdown> element in an expanded description.
+      imports: [TestHostComponent, HttpClientTestingModule, MarkdownModule.forRoot()],
       providers: [
         NotificationService,
         UserProjectService,
@@ -218,4 +221,115 @@ describe("UserProjectListItemComponent", () => {
       expect(refreshSpy).toHaveBeenCalled();
     });
   });
+  /**
+   * The list item decides in its template what a viewer is allowed to touch and how much of a long
+   * description to show. The specs above call the save/colour methods directly, so none of the
+   * rendered gating had been pinned.
+   */
+  describe("rendered item", () => {
+    /** Re-renders the host with the given entry/editable combination. */
+    function render(over: Partial<DashboardProject> = {}, editable = true): HTMLElement {
+      hostFixture.componentInstance.entry = { ...testProject, ...over };
+      hostFixture.componentInstance.editable = editable;
+      hostFixture.detectChanges();
+      return hostFixture.nativeElement as HTMLElement;
+    }
+
+    it("shows the project name and its creation date", () => {
+      const el = render({ name: "quarterly", creationTime: januaryFirst1970 });
+
+      expect(el.textContent).toContain("quarterly");
+      // Expected value is formatted here with the same pipe and format string, so the assertion
+      // pins both the yyyy-MM-dd HH:mm format and the timestamp it was given, without hard-coding
+      // a literal that would only hold in one timezone.
+      const expected = new DatePipe("en-US").transform(januaryFirst1970, "yyyy-MM-dd HH:mm");
+      expect(el.querySelector("nz-list-item-meta-description p")?.textContent?.trim()).toBe(`Created: ${expected}`);
+    });
+
+    it("hides every editing control from a read-only viewer", () => {
+      // accessLevel READ reaches this component as editable=false; if the template ignored it the
+      // viewer would be shown share and delete buttons for a project they cannot change.
+      const el = render({}, false);
+
+      expect(el.querySelector(".edit-name-icon")).toBeNull();
+      expect(el.querySelector(".edit-description-icon")).toBeNull();
+      expect(el.querySelector("ul[nz-list-item-actions]")).toBeNull();
+    });
+
+    it("offers the editing controls to a viewer with write access", () => {
+      const el = render({}, true);
+
+      expect(el.querySelector(".edit-name-icon")).not.toBeNull();
+      expect(el.querySelector(".edit-description-icon")).not.toBeNull();
+      // Share and delete both live in that list; count the buttons rather than just the container
+      // (nz-list-item-action renders as an <li>, so the element selector finds nothing).
+      expect(el.querySelectorAll("ul[nz-list-item-actions] button").length).toBe(2);
+    });
+
+    it("swaps the name for an input once the name is being edited", () => {
+      render();
+      expect(hostFixture.nativeElement.querySelector("nz-list-item-meta-title input")).toBeNull();
+
+      component.editingName = true;
+      hostFixture.detectChanges();
+
+      expect(hostFixture.nativeElement.querySelector("nz-list-item-meta-title input")).not.toBeNull();
+    });
+
+    it("starts with the description collapsed and expands it on request", () => {
+      // descriptionCollapsed defaults to true, so a list of projects stays compact until the user
+      // opens one.
+      const el = render({ description: "a long description" });
+      expect(el.querySelector(".description-container")).toBeNull();
+
+      component.descriptionCollapsed = false;
+      hostFixture.detectChanges();
+
+      expect(hostFixture.nativeElement.querySelector(".description-container")).not.toBeNull();
+    });
+
+    it("shows no description block when the description is only whitespace", () => {
+      // Expanded, so the trim() guard is the only thing left to hide it: a whitespace-only
+      // description would otherwise render an empty expander with nothing in it.
+      render({ description: "   " });
+      component.descriptionCollapsed = false;
+      hostFixture.detectChanges();
+
+      expect(hostFixture.nativeElement.querySelector(".description-container")).toBeNull();
+    });
+
+    it("counts the characters typed into the description editor", () => {
+      render({ description: "abc" });
+      component.editingDescription = true;
+      hostFixture.detectChanges();
+
+      const count = hostFixture.nativeElement.querySelector(".character-count")!;
+      expect(count.textContent?.trim()).toBe(`3/${component.MAX_PROJECT_DESCRIPTION_CHAR_COUNT}`);
+
+      // It must follow what is in the box, not the value the description started at.
+      const textarea = hostFixture.nativeElement.querySelector("textarea")!;
+      textarea.value = "abcdef";
+      textarea.dispatchEvent(new Event("input"));
+      hostFixture.detectChanges();
+
+      expect(hostFixture.nativeElement.querySelector(".character-count")!.textContent?.trim()).toBe(
+        `6/${component.MAX_PROJECT_DESCRIPTION_CHAR_COUNT}`
+      );
+    });
+
+    it("offers the save button only once the description has actually changed", () => {
+      render({ description: "abc" });
+      component.editingDescription = true;
+      hostFixture.detectChanges();
+      const textarea = hostFixture.nativeElement.querySelector("textarea")!;
+
+      expect(hostFixture.nativeElement.querySelector(".ant-input-clear-icon")).toBeNull();
+
+      textarea.value = "abcd";
+      textarea.dispatchEvent(new Event("input"));
+      hostFixture.detectChanges();
+
+      expect(hostFixture.nativeElement.querySelector(".ant-input-clear-icon")).not.toBeNull();
+    });
+  });
 });