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

commit aba6e6e162dc9da8fe23c41b4063253403a98c3d
Author: Xinyuan Lin <[email protected]>
AuthorDate: Wed Jul 22 19:46:14 2026 -0700

    test(frontend): extend ShareAccessComponent unit test coverage (#6771)
    
    ### What changes were proposed in this PR?
    
    Extends `share-access.component.spec.ts` with 19 tests (44 -> 63) for
    `ShareAccessComponent`, covering the grant/revoke/change-access flows,
    the publish/unpublish error branches, the modal Cancel guard paths, and
    the input validation guards. Coverage ~79% -> 100% statements/lines. No
    existing tests modified.
    
    ### Any related issues, documentation, discussions?
    
    Closes #6767.
    
    ### How was this PR tested?
    
    `ng test --include='**/share-access.component.spec.ts'` -> 63/63
    passing. `yarn format:ci` passes.
    
    ### Was this PR authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code (Opus 4.8 [1M context])
---
 .../share-access/share-access.component.spec.ts    | 204 +++++++++++++++++++++
 1 file changed, 204 insertions(+)

diff --git 
a/frontend/src/app/dashboard/component/user/share-access/share-access.component.spec.ts
 
b/frontend/src/app/dashboard/component/user/share-access/share-access.component.spec.ts
index d25ed19622..ea3a5af6c9 100644
--- 
a/frontend/src/app/dashboard/component/user/share-access/share-access.component.spec.ts
+++ 
b/frontend/src/app/dashboard/component/user/share-access/share-access.component.spec.ts
@@ -512,4 +512,208 @@ describe("ShareAccessComponent", () => {
       expect(notificationSpy.success).toHaveBeenCalledWith("Dataset 
unpublished successfully");
     });
   });
+
+  describe("hasWriteAccess without a resolved email", () => {
+    it("returns false when the current user has no email at all", () => {
+      const c = setupComponent();
+      // Exercise the no-email early-return guard directly, independent of how 
the
+      // user service happens to resolve an empty/absent email.
+      c.currentEmail = undefined;
+      expect(c.hasWriteAccess).toBe(false);
+    });
+  });
+
+  describe("removeEmailTag", () => {
+    it("removes the matching email and keeps the others", () => {
+      const c = setupComponent();
+      c.emailTags = ["[email protected]", "[email protected]", 
"[email protected]"];
+      c.removeEmailTag("[email protected]");
+      expect(c.emailTags).toEqual(["[email protected]", "[email protected]"]);
+    });
+
+    it("leaves tags unchanged when the email is not present", () => {
+      const c = setupComponent();
+      c.emailTags = ["[email protected]"];
+      c.removeEmailTag("[email protected]");
+      expect(c.emailTags).toEqual(["[email protected]"]);
+    });
+  });
+
+  describe("onChange", () => {
+    it("filters allOwners case-insensitively by the typed value", () => {
+      const c = setupComponent();
+      c.allOwners.push("Alice", "Bob", "alfred");
+      c.onChange("al");
+      expect(c.filteredOwners).toEqual(["Alice", "alfred"]);
+    });
+
+    it("clears filteredOwners when the value is null", () => {
+      const c = setupComponent();
+      c.allOwners.push("Alice");
+      c.filteredOwners = ["stale"];
+      c.onChange(null as unknown as string);
+      expect(c.filteredOwners).toEqual([]);
+    });
+  });
+
+  describe("onPaste with an empty existing value", () => {
+    it("defaults the existing email value to an empty string before 
appending", () => {
+      const c = setupComponent();
+      c.validateForm.get("email")?.reset();
+      const event = {
+        preventDefault: vi.fn(),
+        clipboardData: { getData: vi.fn().mockReturnValue("[email protected]") 
},
+      } as unknown as ClipboardEvent;
+      c.onPaste(event);
+      expect(c.emailTags).toEqual(["[email protected]"]);
+    });
+  });
+
+  describe("modal Cancel buttons", () => {
+    function captureModalRefs(): any[] {
+      const modalRefs: any[] = [];
+      modalServiceSpy.create.mockImplementation((config: any) => {
+        capturedModalConfigs.push(config);
+        const ref = { close: vi.fn() };
+        modalRefs.push(ref);
+        return ref;
+      });
+      return modalRefs;
+    }
+
+    it("closes the revoke confirmation modal without revoking when Cancel is 
clicked", () => {
+      const modalRefs = captureModalRefs();
+      const c = setupComponent({ currentEmail: "[email protected]" });
+      c.verifyRevokeAccess("[email protected]");
+      getFooterButton(capturedModalConfigs[0], "Cancel").onClick();
+      expect(modalRefs[0].close).toHaveBeenCalled();
+      expect(accessServiceSpy.revokeAccess).not.toHaveBeenCalled();
+    });
+
+    it("closes the downgrade modal and reloads without granting when Cancel is 
clicked", () => {
+      accessServiceSpy.getAccessList.mockReturnValue(
+        of([{ email: "[email protected]", name: "Me", privilege: 
Privilege.WRITE }])
+      );
+      const modalRefs = captureModalRefs();
+      const c = setupComponent({ currentEmail: "[email protected]", type: 
"workflow", id: 3 });
+      accessServiceSpy.grantAccess.mockClear();
+      accessServiceSpy.getAccessList.mockClear();
+      c.changeAccessLevel("[email protected]", "READ");
+      getFooterButton(capturedModalConfigs[0], "Cancel").onClick();
+      expect(modalRefs[0].close).toHaveBeenCalled();
+      expect(accessServiceSpy.grantAccess).not.toHaveBeenCalled();
+      // Cancel re-runs ngOnInit to restore the previous access level in the UI
+      expect(accessServiceSpy.getAccessList).toHaveBeenCalledWith("workflow", 
3);
+    });
+
+    it("closes the publish modal without publishing when Cancel is clicked", 
() => {
+      workflowPersistSpy.getWorkflowIsPublished.mockReturnValue(of("Private"));
+      const modalRefs = captureModalRefs();
+      const c = setupComponent({ type: "workflow", inWorkspace: true });
+      c.verifyPublish();
+      getFooterButton(capturedModalConfigs[0], "Cancel").onClick();
+      expect(modalRefs[0].close).toHaveBeenCalled();
+      
expect(workflowPersistSpy.updateWorkflowIsPublished).not.toHaveBeenCalled();
+      expect(workflowActionSpy.setWorkflowIsPublished).not.toHaveBeenCalled();
+    });
+
+    it("closes the unpublish modal without unpublishing when Cancel is 
clicked", () => {
+      workflowPersistSpy.getWorkflowIsPublished.mockReturnValue(of("Public"));
+      const modalRefs = captureModalRefs();
+      const c = setupComponent({ type: "workflow" });
+      c.verifyUnpublish();
+      getFooterButton(capturedModalConfigs[0], "Cancel").onClick();
+      expect(modalRefs[0].close).toHaveBeenCalled();
+      
expect(workflowPersistSpy.updateWorkflowIsPublished).not.toHaveBeenCalled();
+    });
+  });
+
+  describe("applyAccessLevelChange error branch", () => {
+    it("surfaces HttpErrorResponse and reloads the access list on failure", () 
=> {
+      accessServiceSpy.grantAccess.mockReturnValue(
+        throwError(() => new HttpErrorResponse({ error: { message: "change 
failed" }, status: 500 }))
+      );
+      const c = setupComponent({ currentEmail: "[email protected]", type: 
"workflow", id: 3 });
+      accessServiceSpy.getAccessList.mockClear();
+      c.changeAccessLevel("[email protected]", "READ");
+      expect(notificationSpy.error).toHaveBeenCalledWith("change failed");
+      // the error branch reloads the access list so the UI reflects the 
unchanged level
+      expect(accessServiceSpy.getAccessList).toHaveBeenCalledWith("workflow", 
3);
+    });
+  });
+
+  describe("unpublish error branches", () => {
+    it("unpublishWorkflow surfaces HttpErrorResponse and leaves isPublic 
unchanged", () => {
+      workflowPersistSpy.getWorkflowIsPublished.mockReturnValue(of("Public"));
+      workflowPersistSpy.updateWorkflowIsPublished.mockReturnValue(
+        throwError(() => new HttpErrorResponse({ error: { message: "unpublish 
failed" }, status: 500 }))
+      );
+      const c = setupComponent({ type: "workflow" });
+      c.unpublishWorkflow();
+      expect(notificationSpy.error).toHaveBeenCalledWith("unpublish failed");
+      expect(c.isPublic).toBe(true);
+    });
+
+    it("unpublishDataset surfaces HttpErrorResponse and leaves isPublic 
unchanged", () => {
+      datasetServiceSpy.getDataset.mockReturnValue(of({ dataset: { isPublic: 
true } }));
+      datasetServiceSpy.updateDatasetPublicity.mockReturnValue(
+        throwError(() => new HttpErrorResponse({ error: { message: "dataset 
unpublish failed" }, status: 500 }))
+      );
+      const c = setupComponent({ type: "dataset" });
+      c.unpublishDataset();
+      expect(notificationSpy.error).toHaveBeenCalledWith("dataset unpublish 
failed");
+      expect(c.isPublic).toBe(true);
+    });
+  });
+
+  describe("guard branches (no-ops)", () => {
+    it("handleInputConfirm skips empty tokens produced by trailing 
separators", () => {
+      const c = setupComponent();
+      c.validateForm.get("email")?.setValue("[email protected], ; ");
+      c.handleInputConfirm();
+      expect(c.emailTags).toEqual(["[email protected]"]);
+      expect(messageSpy.error).not.toHaveBeenCalled();
+    });
+
+    it("grantAccess does nothing when there are no email tags", () => {
+      const c = setupComponent({ type: "workflow", id: 5 });
+      accessServiceSpy.grantAccess.mockClear();
+      c.emailTags = [];
+      c.grantAccess();
+      expect(accessServiceSpy.grantAccess).not.toHaveBeenCalled();
+      expect(gmailSpy.sendEmail).not.toHaveBeenCalled();
+    });
+
+    it("publishWorkflow is a no-op when the workflow is already public", () => 
{
+      workflowPersistSpy.getWorkflowIsPublished.mockReturnValue(of("Public"));
+      const c = setupComponent({ type: "workflow" });
+      workflowPersistSpy.updateWorkflowIsPublished.mockClear();
+      c.publishWorkflow();
+      
expect(workflowPersistSpy.updateWorkflowIsPublished).not.toHaveBeenCalled();
+    });
+
+    it("unpublishWorkflow is a no-op when the workflow is already private", () 
=> {
+      workflowPersistSpy.getWorkflowIsPublished.mockReturnValue(of("Private"));
+      const c = setupComponent({ type: "workflow" });
+      workflowPersistSpy.updateWorkflowIsPublished.mockClear();
+      c.unpublishWorkflow();
+      
expect(workflowPersistSpy.updateWorkflowIsPublished).not.toHaveBeenCalled();
+    });
+
+    it("publishDataset is a no-op when the dataset is already public", () => {
+      datasetServiceSpy.getDataset.mockReturnValue(of({ dataset: { isPublic: 
true } }));
+      const c = setupComponent({ type: "dataset" });
+      datasetServiceSpy.updateDatasetPublicity.mockClear();
+      c.publishDataset();
+      expect(datasetServiceSpy.updateDatasetPublicity).not.toHaveBeenCalled();
+    });
+
+    it("unpublishDataset is a no-op when the dataset is already private", () 
=> {
+      datasetServiceSpy.getDataset.mockReturnValue(of({ dataset: { isPublic: 
false } }));
+      const c = setupComponent({ type: "dataset" });
+      datasetServiceSpy.updateDatasetPublicity.mockClear();
+      c.unpublishDataset();
+      expect(datasetServiceSpy.updateDatasetPublicity).not.toHaveBeenCalled();
+    });
+  });
 });

Reply via email to