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 <[email protected]>
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();
+    });
+  });
 });

Reply via email to