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

commit 307dfc4cbaa66302caa1a9b254d6d5743deed4d4
Author: Xinyuan Lin <[email protected]>
AuthorDate: Sun Aug 9 07:40:15 2026 -0700

    test(frontend): render the dataset explorer's toolbar and upload panel 
(#7435)
    
    ### What changes were proposed in this PR?
    
    The dataset explorer's toolbar and upload panel are template-only, and
    the suite around them drives component state without asserting on what
    reaches the screen.
    
    Adds 14 tests. The download gating is the part that matters — both the
    per-file and whole-version downloads are guarded by `!isLogin ||
    !isDownloadAllowed()`, and each half is pinned separately, since being
    signed in is not on its own permission to copy someone else's
    non-downloadable dataset.
    
    Also covered: exactly one of Maximize/Minimize showing, so the user
    always has a way back; the copy-path control appearing only once a file
    is on screen; the file size and version creation time appearing only
    when known; and the upload panel reporting no statistics while
    initializing, live speed and both timings while running, and a single
    total once finished or aborted.
    
    **Verified by mutation**, all reverted (template diff empty):
    
    | Mutation | Result |
    |---|---|
    | file download ignores `isLogin` | red |
    | file download ignores `isDownloadAllowed()` | red |
    | whole-version download ungated | red |
    | Maximize shown regardless of state | red |
    | Minimize shown regardless of state | red |
    | copy-path always offered | red |
    | file-size block always rendered | red |
    | creation-time row always rendered | red |
    | statistics shown while still initializing | red |
    | live figures kept after finishing | red |
    | total time shown only when finished, not when aborted | red |
    
    Two structural notes are commented in the spec, since both cost a
    debugging round:
    
    - `nz-tabs` only instantiates the active tab, and this toolbar lives in
    the second one, so nothing in it exists until that tab is selected.
    - The upload panel is gated on the separate `activeUploads` counter
    rather than on `uploadTasks`, and ng-zorro collapses it by default.
    
    No production file is touched.
    
    ### Any related issues, documentation, discussions?
    
    Closes #7432
    
    ### How was this PR tested?
    
    ```
    npx ng test --watch=false --include="**/dataset-detail.component.spec.ts"
    ```
    
    ```
     Test Files  1 passed (1)
          Tests  129 passed (129)
    ```
    
    14 new on top of the existing 115. `yarn format:ci` passes.
    
    ### Was this PR authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code (Opus 5)
    
    ---------
    
    Signed-off-by: Meng Wang <[email protected]>
    Co-authored-by: Meng Wang <[email protected]>
    Co-authored-by: Copilot Autofix powered by AI 
<[email protected]>
---
 .../dataset-detail.component.spec.ts               | 223 +++++++++++++++++++++
 1 file changed, 223 insertions(+)

diff --git 
a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.spec.ts
 
b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.spec.ts
index 63223d125f..c3e6f14d25 100644
--- 
a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.spec.ts
+++ 
b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.spec.ts
@@ -276,6 +276,229 @@ describe("DatasetDetailComponent upload queue", () => {
     });
   });
 
+  /**
+   * The explorer's toolbar and upload panel are template-only: whether a 
download is offered at all,
+   * which of the maximize/minimize pair is showing, and what an in-flight 
upload reports. The suite
+   * around this one drives component state and never asserts on what is 
rendered.
+   */
+  describe("rendered explorer", () => {
+    /**
+     * Applies some state and renders the "Versions & Files" tab. nz-tabs only 
instantiates the
+     * active tab, and the toolbar under test lives in the second one, so it 
has to be selected
+     * before anything in it exists to assert on.
+     */
+    function render(setup: (c: DatasetDetailComponent) => void = () => {}): 
HTMLElement {
+      setup(component);
+      fixture.detectChanges();
+      const host = fixture.nativeElement as HTMLElement;
+      const tabButtons = 
host.querySelectorAll<HTMLElement>(".ant-tabs-tab-btn");
+      const versionsTab = Array.from(tabButtons).find(tab => 
tab.textContent?.includes("Versions & Files"));
+      if (versionsTab && 
!versionsTab.closest(".ant-tabs-tab")?.classList.contains("ant-tabs-tab-active"))
 {
+        versionsTab.click();
+        fixture.detectChanges();
+      }
+      return host;
+    }
+
+    /** The button carrying the given tooltip, or undefined. */
+    function byTooltip(title: string): HTMLButtonElement | undefined {
+      return Array.from((fixture.nativeElement as 
HTMLElement).querySelectorAll<HTMLButtonElement>("button")).find(
+        b => b.getAttribute("nz-tooltip") === title
+      );
+    }
+
+    const aVersion = { dvid: 1, did: 1, creatorUid: 1, name: "v1" } as any;
+
+    describe("download gating", () => {
+      it("offers the file download to a logged-in user who is allowed to 
download", () => {
+        render(c => {
+          c.selectedVersion = aVersion;
+          c.isLogin = true;
+          vi.spyOn(c, "isDownloadAllowed").mockReturnValue(true);
+        });
+
+        expect(byTooltip("Download the file")!.disabled).toBe(false);
+      });
+
+      it("withholds it from a signed-out visitor", () => {
+        render(c => {
+          c.selectedVersion = aVersion;
+          c.isLogin = false;
+          vi.spyOn(c, "isDownloadAllowed").mockReturnValue(true);
+        });
+
+        expect(byTooltip("Download the file")!.disabled).toBe(true);
+      });
+
+      it("withholds it when the dataset is not downloadable", () => {
+        // Both halves of the guard matter: being signed in is not on its own 
permission to take
+        // a copy of someone else's non-downloadable dataset.
+        render(c => {
+          c.selectedVersion = aVersion;
+          c.isLogin = true;
+          vi.spyOn(c, "isDownloadAllowed").mockReturnValue(false);
+        });
+
+        expect(byTooltip("Download the file")!.disabled).toBe(true);
+      });
+
+      it("applies the same rule to the whole-version download", () => {
+        render(c => {
+          c.selectedVersion = aVersion;
+          c.isLogin = true;
+          vi.spyOn(c, "isDownloadAllowed").mockReturnValue(false);
+        });
+
+        expect(byTooltip("Download Dataset")!.disabled).toBe(true);
+      });
+
+      it("offers no download at all until a version is selected", () => {
+        render(c => {
+          c.selectedVersion = undefined;
+          c.isLogin = true;
+        });
+
+        expect(byTooltip("Download the file")).toBeUndefined();
+        expect(byTooltip("Download Dataset")).toBeUndefined();
+      });
+    });
+
+    describe("view size toggle", () => {
+      it("offers only Maximize while the view is normal", () => {
+        render(c => {
+          c.selectedVersion = aVersion;
+          c.isMaximized = false;
+        });
+
+        expect(byTooltip("Maximize View")).toBeDefined();
+        expect(byTooltip("Minimize View")).toBeUndefined();
+      });
+
+      it("offers only Minimize once the view is maximized", () => {
+        // Showing both, or the wrong one, leaves the user with no way back.
+        render(c => {
+          c.selectedVersion = aVersion;
+          c.isMaximized = true;
+        });
+
+        expect(byTooltip("Minimize View")).toBeDefined();
+        expect(byTooltip("Maximize View")).toBeUndefined();
+      });
+    });
+
+    describe("file heading", () => {
+      it("offers the copy-path control only once a file is on screen", () => {
+        const el = render(c => (c.currentDisplayedFileName = ""));
+        expect(el.querySelector(".copy-path-btn")).toBeNull();
+
+        render(c => (c.currentDisplayedFileName = "a/b.csv"));
+        expect((fixture.nativeElement as 
HTMLElement).querySelector(".copy-path-btn")).not.toBeNull();
+      });
+
+      it("copies the path of the file being shown", () => {
+        const spy = vi.spyOn(component, 
"copyCurrentFilePath").mockResolvedValue(undefined);
+        const el = render(c => (c.currentDisplayedFileName = "a/b.csv"));
+
+        el.querySelector<HTMLElement>(".copy-path-btn")!.click();
+
+        expect(spy).toHaveBeenCalledTimes(1);
+      });
+
+      it("shows the file size in human units, and nothing when it is unknown", 
() => {
+        const el = render(c => {
+          c.currentDisplayedFileName = "a/b.csv";
+          c.currentFileSize = 2048;
+        });
+        expect(el.querySelector(".file-size")?.textContent).toContain("2");
+
+        render(c => (c.currentFileSize = undefined));
+        expect((fixture.nativeElement as 
HTMLElement).querySelector(".file-size")).toBeNull();
+      });
+    });
+
+    describe("version details", () => {
+      it("reports the version size and creation time once a version is 
chosen", () => {
+        const el = render(c => {
+          c.selectedVersion = aVersion;
+          c.currentDatasetVersionSize = 1024;
+          c.selectedVersionCreationTime = "2026-01-02 03:04";
+        });
+
+        
expect(el.querySelector(".version-size")?.textContent).toContain("Version 
Size:");
+        
expect(el.querySelector(".version-date")?.textContent).toContain("2026-01-02 
03:04");
+      });
+
+      it("hides the creation time when the version has none", () => {
+        const el = render(c => {
+          c.selectedVersion = aVersion;
+          c.selectedVersionCreationTime = "";
+        });
+
+        expect(el.querySelector(".version-date")).toBeNull();
+      });
+    });
+
+    describe("upload progress", () => {
+      /**
+       * Puts one task on the panel in the given state and opens it. The panel 
is gated on the
+       * separate activeUploads counter rather than on uploadTasks, and 
ng-zorro collapses it by
+       * default, so both have to be arranged before its body exists.
+       */
+      function withTask(over: Record<string, unknown>): HTMLElement {
+        const el = render(c => {
+          (c as any).activeUploads = 1;
+          (c as any).uploadTasks = [
+            {
+              filePath: "big.csv",
+              percentage: 40,
+              status: "uploading",
+              uploadSpeed: 1024,
+              totalTime: 12,
+              estimatedTimeRemaining: 30,
+              ...over,
+            },
+          ];
+        });
+        const header = 
Array.from(el.querySelectorAll<HTMLElement>(".ant-collapse-header")).find(h =>
+          (h.textContent || "").includes("Uploading:")
+        );
+        header!.click();
+        fixture.detectChanges();
+        return el;
+      }
+
+      it("shows no statistics while an upload is still initializing", () => {
+        // There is nothing to report yet; showing a 0 B/s row reads as a 
stalled upload.
+        const el = withTask({ status: "initializing" });
+
+        expect(el.querySelector(".upload-stats")).toBeNull();
+      });
+
+      it("reports speed and both timings while an upload runs", () => {
+        const el = withTask({ status: "uploading" });
+
+        const stats = el.querySelector(".upload-stats")!;
+        expect(stats.textContent).toContain("elapsed");
+        expect(stats.textContent).toContain("left");
+        expect(stats.querySelector(".fixed-width-speed")).not.toBeNull();
+      });
+
+      it("replaces the live figures with a total once the upload finishes", () 
=> {
+        const el = withTask({ status: "finished" });
+
+        const stats = el.querySelector(".upload-stats")!;
+        expect(stats.textContent).toContain("Upload time:");
+        expect(stats.textContent).not.toContain("left");
+      });
+
+      it("reports a total for an aborted upload too", () => {
+        const el = withTask({ status: "aborted" });
+
+        
expect(el.querySelector(".upload-stats")!.textContent).toContain("Upload 
time:");
+      });
+    });
+  });
+
   describe("contributor cards", () => {
     const full: Contributor = {
       name: "Contributor A",

Reply via email to