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

commit d8c254c75a024f00106528dfb66f4ccc4084f012
Author: Matthew B. <[email protected]>
AuthorDate: Wed May 27 00:00:23 2026 -0700

    test: add unit spec for UserDatasetComponent (#5238)
    
    ### What changes were proposed in this PR?
    - Adds `user-dataset.component.spec.ts` covering `UserDatasetComponent`
    (previously uncovered apart from its file-renderer child).
    - Verifies user-state tracking via `UserService.userChanged()`,
    `ngAfterViewInit` re-search behavior, `search()` filterScope variants
    (`private` / `public` / `all`), the full 8-argument
    `SearchService.executeSearch` call shape, the LakeFS mismatch warning
    (text + 4000ms duration), the `UserDatasetVersionCreatorComponent` modal
    config and post-close navigation, and `deleteDataset` (undefined-`did`
    no-op plus filtering of the deleted entry).
    - Uses direct component instantiation with `vi.fn()` mocks rather than
    the heavier TestBed pattern, so assertions can target exact positional
    argument shapes without bootstrapping the modal content component or the
    ng-zorro module graph.
    ### Any related issues, documentation, or discussions?
    Closes: #5229
    ### How was this PR tested?
    - `yarn test --include='**/user-dataset/user-dataset.component.spec.ts'
    --watch=false` (14 tests passed, ~12ms).
    ### Was this PR authored or co-authored using generative AI tooling?
    Co-authored with Claude Opus 4.7 in compliance with ASF
---
 .../user-dataset/user-dataset.component.spec.ts    | 312 +++++++++++++++++++++
 1 file changed, 312 insertions(+)

diff --git 
a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset.component.spec.ts
 
b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset.component.spec.ts
new file mode 100644
index 0000000000..1556c2db63
--- /dev/null
+++ 
b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset.component.spec.ts
@@ -0,0 +1,312 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { of, Subject } from "rxjs";
+import { UserDatasetComponent } from "./user-dataset.component";
+import { DASHBOARD_USER_DATASET } from "../../../../app-routing.constant";
+import { UserDatasetVersionCreatorComponent } from 
"./user-dataset-explorer/user-dataset-version-creator/user-dataset-version-creator.component";
+import { SortMethod } from "../../../type/sort-method";
+import { User } from "../../../../common/type/user";
+
+type LoadMoreFn = (start: number, count: number) => Promise<{ entries: any[]; 
more: boolean }>;
+
+describe("UserDatasetComponent", () => {
+  let component: UserDatasetComponent;
+
+  let userChangedSubject: Subject<User | undefined>;
+  let isLoginSpy: ReturnType<typeof vi.fn>;
+  let getCurrentUserSpy: ReturnType<typeof vi.fn>;
+
+  let modalServiceMock: { create: ReturnType<typeof vi.fn> };
+  let routerMock: { navigate: ReturnType<typeof vi.fn> };
+  let searchServiceMock: { executeSearch: ReturnType<typeof vi.fn> };
+  let datasetServiceMock: { deleteDatasets: ReturnType<typeof vi.fn> };
+  let messageMock: { warning: ReturnType<typeof vi.fn> };
+
+  let filtersStub: any;
+  let searchResultsStub: any;
+  let capturedLoadMoreFn: LoadMoreFn | null;
+
+  const buildEntry = (did: number | undefined, name = `dataset-${did}`) =>
+    ({
+      type: "dataset",
+      dataset: {
+        dataset: {
+          did,
+          name,
+          ownerUid: 1,
+          isPublic: false,
+          isDownloadable: false,
+          storagePath: undefined,
+          description: "",
+          creationTime: 0,
+          coverImage: undefined,
+        },
+      },
+    }) as any;
+
+  beforeEach(() => {
+    userChangedSubject = new Subject<User | undefined>();
+    isLoginSpy = vi.fn(() => true);
+    getCurrentUserSpy = vi.fn(() => ({ uid: 42 }) as User);
+
+    const userServiceMock = {
+      userChanged: () => userChangedSubject.asObservable(),
+      isLogin: isLoginSpy,
+      getCurrentUser: getCurrentUserSpy,
+    };
+
+    modalServiceMock = { create: vi.fn() };
+    routerMock = { navigate: vi.fn() };
+    searchServiceMock = {
+      executeSearch: vi.fn(() => of({ entries: [], more: false, hasMismatch: 
false })),
+    };
+    datasetServiceMock = { deleteDatasets: vi.fn(() => of({} as Response)) };
+    messageMock = { warning: vi.fn() };
+
+    component = new UserDatasetComponent(
+      modalServiceMock as any,
+      userServiceMock as any,
+      routerMock as any,
+      searchServiceMock as any,
+      datasetServiceMock as any,
+      messageMock as any
+    );
+
+    capturedLoadMoreFn = null;
+    filtersStub = {
+      masterFilterList: [] as string[],
+      masterFilterListChange: new Subject<void>(),
+      getSearchKeywords: vi.fn(() => ["kw1"]),
+      getSearchFilterParameters: vi.fn(() => ({ ids: [1, 2] })),
+    };
+    searchResultsStub = {
+      entries: [] as any[],
+      reset: vi.fn((fn: LoadMoreFn) => {
+        capturedLoadMoreFn = fn;
+      }),
+      loadMore: vi.fn(async () => {}),
+    };
+
+    component.filters = filtersStub;
+    component.searchResultsComponent = searchResultsStub;
+  });
+
+  describe("user state tracking", () => {
+    it("updates isLogin and currentUid when userChanged emits", () => {
+      // initial state pulled synchronously in field initializers
+      expect(component.isLogin).toBe(true);
+      expect(component.currentUid).toBe(42);
+
+      isLoginSpy.mockReturnValue(false);
+      getCurrentUserSpy.mockReturnValue(undefined);
+      userChangedSubject.next(undefined);
+
+      expect(component.isLogin).toBe(false);
+      expect(component.currentUid).toBeUndefined();
+
+      isLoginSpy.mockReturnValue(true);
+      getCurrentUserSpy.mockReturnValue({ uid: 99 } as User);
+      userChangedSubject.next({ uid: 99 } as User);
+
+      expect(component.isLogin).toBe(true);
+      expect(component.currentUid).toBe(99);
+    });
+  });
+
+  describe("ngAfterViewInit", () => {
+    it("subscribes to userChanged and calls search on each emission", () => {
+      const searchSpy = vi.spyOn(component, "search").mockResolvedValue();
+      component.ngAfterViewInit();
+
+      expect(searchSpy).not.toHaveBeenCalled();
+      userChangedSubject.next({ uid: 42 } as User);
+      expect(searchSpy).toHaveBeenCalledTimes(1);
+
+      userChangedSubject.next(undefined);
+      expect(searchSpy).toHaveBeenCalledTimes(2);
+    });
+  });
+
+  describe("search filterScope variants", () => {
+    it('defaults to "private": passes isLogin through, includePublic = false', 
async () => {
+      component.isLogin = true;
+      component.sortMethod = SortMethod.EditTimeDesc;
+      await component.search();
+
+      expect(searchResultsStub.reset).toHaveBeenCalledTimes(1);
+      expect(searchResultsStub.loadMore).toHaveBeenCalledTimes(1);
+      expect(capturedLoadMoreFn).not.toBeNull();
+
+      await capturedLoadMoreFn!(5, 10);
+      expect(searchServiceMock.executeSearch).toHaveBeenCalledWith(
+        ["kw1"],
+        { ids: [1, 2] },
+        5,
+        10,
+        "dataset",
+        SortMethod.EditTimeDesc,
+        true,
+        false
+      );
+    });
+
+    it('"public": forces isLogin = false, includePublic = true', async () => {
+      component.isLogin = true;
+      await component.search(false, "public");
+      await capturedLoadMoreFn!(0, 20);
+
+      const args = searchServiceMock.executeSearch.mock.calls[0];
+      expect(args[6]).toBe(false); // isLogin
+      expect(args[7]).toBe(true); // includePublic
+    });
+
+    it('"all": passes isLogin through, includePublic = true', async () => {
+      component.isLogin = true;
+      await component.search(false, "all");
+      await capturedLoadMoreFn!(0, 20);
+
+      const args = searchServiceMock.executeSearch.mock.calls[0];
+      expect(args[6]).toBe(true);
+      expect(args[7]).toBe(true);
+    });
+
+    it('"private" with isLogin = false: passes false through, includePublic = 
false', async () => {
+      component.isLogin = false;
+      await component.search(false, "private");
+      await capturedLoadMoreFn!(0, 20);
+
+      const args = searchServiceMock.executeSearch.mock.calls[0];
+      expect(args[6]).toBe(false);
+      expect(args[7]).toBe(false);
+    });
+  });
+
+  describe("search call shape", () => {
+    it("invokes executeSearch with the documented argument order via 
reset(...) then loadMore()", async () => {
+      filtersStub.getSearchKeywords.mockReturnValue(["alpha", "beta"]);
+      filtersStub.getSearchFilterParameters.mockReturnValue({ resourceType: 
"dataset" });
+      component.sortMethod = SortMethod.NameAsc;
+      component.isLogin = true;
+
+      await component.search();
+      expect(searchResultsStub.reset).toHaveBeenCalledTimes(1);
+      expect(searchResultsStub.loadMore).toHaveBeenCalledTimes(1);
+      expect(searchResultsStub.reset.mock.invocationCallOrder[0]).toBeLessThan(
+        searchResultsStub.loadMore.mock.invocationCallOrder[0]
+      );
+
+      await capturedLoadMoreFn!(7, 25);
+      expect(searchServiceMock.executeSearch).toHaveBeenCalledWith(
+        ["alpha", "beta"],
+        { resourceType: "dataset" },
+        7,
+        25,
+        "dataset",
+        SortMethod.NameAsc,
+        true,
+        false
+      );
+    });
+  });
+
+  describe("mismatch warning", () => {
+    it("when hasMismatch = true: sets component.hasMismatch and warns for 
4000ms", async () => {
+      searchServiceMock.executeSearch.mockReturnValue(of({ entries: [], more: 
false, hasMismatch: true }));
+
+      await component.search();
+      await capturedLoadMoreFn!(0, 20);
+
+      expect(component.hasMismatch).toBe(true);
+      expect(messageMock.warning).toHaveBeenCalledTimes(1);
+      const [msg, opts] = messageMock.warning.mock.calls[0];
+      expect(typeof msg).toBe("string");
+      expect(msg.length).toBeGreaterThan(0);
+      expect(opts).toEqual({ nzDuration: 4000 });
+    });
+
+    it("when hasMismatch is missing/false: does not warn and clears 
hasMismatch", async () => {
+      component.hasMismatch = true;
+      searchServiceMock.executeSearch.mockReturnValue(of({ entries: [], more: 
false }));
+
+      await component.search();
+      await capturedLoadMoreFn!(0, 20);
+
+      expect(component.hasMismatch).toBe(false);
+      expect(messageMock.warning).not.toHaveBeenCalled();
+    });
+  });
+
+  describe("onClickOpenDatasetAddComponent", () => {
+    it("opens UserDatasetVersionCreatorComponent with isCreatingVersion: 
false", () => {
+      modalServiceMock.create.mockReturnValue({ afterClose: of(null) });
+
+      component.onClickOpenDatasetAddComponent();
+
+      expect(modalServiceMock.create).toHaveBeenCalledTimes(1);
+      const config = modalServiceMock.create.mock.calls[0][0];
+      expect(config.nzContent).toBe(UserDatasetVersionCreatorComponent);
+      expect(config.nzData).toEqual({ isCreatingVersion: false });
+      expect(config.nzFooter).toBeNull();
+    });
+
+    it("on close with a dataset result: navigates to the new dataset URL", () 
=> {
+      const dashboardDataset = {
+        isOwner: true,
+        ownerEmail: "[email protected]",
+        accessPrivilege: "WRITE",
+        size: 0,
+        dataset: { did: 123 },
+      };
+      modalServiceMock.create.mockReturnValue({ afterClose: 
of(dashboardDataset) });
+
+      component.onClickOpenDatasetAddComponent();
+
+      
expect(routerMock.navigate).toHaveBeenCalledWith([`${DASHBOARD_USER_DATASET}/123`]);
+    });
+
+    it("on close with null result: does not navigate", () => {
+      modalServiceMock.create.mockReturnValue({ afterClose: of(null) });
+
+      component.onClickOpenDatasetAddComponent();
+
+      expect(routerMock.navigate).not.toHaveBeenCalled();
+    });
+  });
+
+  describe("deleteDataset", () => {
+    it("is a no-op when entry.dataset.dataset.did is undefined", () => {
+      component.deleteDataset(buildEntry(undefined));
+
+      expect(datasetServiceMock.deleteDatasets).not.toHaveBeenCalled();
+    });
+
+    it("calls deleteDatasets(did) and filters the entry out of 
searchResultsComponent.entries", () => {
+      const e1 = buildEntry(1, "first");
+      const e2 = buildEntry(2, "second");
+      const e3 = buildEntry(3, "third");
+      searchResultsStub.entries = [e1, e2, e3];
+
+      component.deleteDataset(e2);
+
+      expect(datasetServiceMock.deleteDatasets).toHaveBeenCalledWith(2);
+      expect(searchResultsStub.entries).toEqual([e1, e3]);
+    });
+  });
+});

Reply via email to