Copilot commented on code in PR #6358:
URL: https://github.com/apache/texera/pull/6358#discussion_r3565409813
##########
frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.spec.ts:
##########
@@ -334,3 +338,653 @@ describe("DatasetDetailComponent upload queue", () => {
expect(component.pendingChangesCount).toBe(1);
});
});
+
+describe("DatasetDetailComponent behavior", () => {
+ let fixture: ComponentFixture<DatasetDetailComponent>;
+ let component: DatasetDetailComponent;
+
+ type MockService = Record<string, ReturnType<typeof vi.fn>>;
+ let datasetServiceStub: MockService;
+ let notificationServiceStub: MockService;
+ let downloadServiceStub: MockService;
+ let hubServiceStub: MockService;
+ let adminSettingsServiceStub: MockService;
+
+ const CREATION_TS = 1_700_000_000_000;
+
+ const makeDataset = (overrides: Partial<Dataset> = {}): Dataset => ({
+ did: 5,
+ ownerUid: 9,
+ name: "ds",
+ isPublic: false,
+ isDownloadable: true,
+ storagePath: undefined,
+ description: "desc",
+ creationTime: undefined,
+ coverImage: undefined,
+ ...overrides,
+ });
+
+ const makeDashboardDataset = (overrides: Partial<DashboardDataset> = {}):
DashboardDataset => ({
+ isOwner: false,
+ ownerEmail: "[email protected]",
+ dataset: makeDataset(),
+ accessPrivilege: "NONE",
+ size: 0,
+ ...overrides,
+ });
+
+ const makeVersion = (overrides: Partial<DatasetVersion> = {}):
DatasetVersion => ({
+ dvid: 1,
+ did: 5,
+ creatorUid: 9,
+ name: "v1",
+ versionHash: undefined,
+ creationTime: undefined,
+ fileNodes: undefined,
+ ...overrides,
+ });
+
+ const fileLeaf = (name: string, parentDir: string, size: number):
DatasetFileNode => ({
+ name,
+ type: "file",
+ parentDir,
+ size,
+ });
+
+ const createComponent = (params: Record<string, unknown> = { did: 5 }): void
=> {
+ TestBed.configureTestingModule({
+ imports: [DatasetDetailComponent, ...commonTestImports],
+ providers: [
+ { provide: ActivatedRoute, useValue: { params: of(params), data:
of({}) } },
+ { provide: NzModalService, useValue: {} },
+ { provide: DatasetService, useValue: datasetServiceStub },
+ { provide: NotificationService, useValue: notificationServiceStub },
+ { provide: DownloadService, useValue: downloadServiceStub },
+ { provide: UserService, useClass: StubUserService },
+ { provide: HubService, useValue: hubServiceStub },
+ { provide: AdminSettingsService, useValue: adminSettingsServiceStub },
+ { provide: MarkdownService, useValue: { parse: vi.fn(() => "") } },
+ ...commonTestProviders,
+ ],
+ });
+ fixture = TestBed.createComponent(DatasetDetailComponent);
+ component = fixture.componentInstance;
+ };
+
+ // The StubUserService emits MOCK_USER in its own constructor, before the
+ // component subscribes, so currentUid starts undefined; re-emit to log in.
+ const login = (): void => {
+ (TestBed.inject(UserService) as unknown as
StubUserService).userChangeSubject.next(MOCK_USER);
+ };
+
+ beforeEach(() => {
+ datasetServiceStub = {
+ getDataset: vi.fn(() => of(makeDashboardDataset())),
+ retrieveDatasetVersionList: vi.fn(() => of([])),
+ getDatasetCoverUrl: vi.fn(() => of({ url: "http://cover" })),
+ retrieveDatasetVersionFileTree: vi.fn(() => of({ fileNodes:
[fileLeaf("a.txt", "/root", 1)], size: 1 })),
+ createDatasetVersion: vi.fn(() => of(makeVersion())),
+ updateDatasetPublicity: vi.fn(() => of({})),
+ updateDatasetDownloadable: vi.fn(() => of({})),
+ updateDatasetCoverImage: vi.fn(() => of({})),
+ updateDatasetDescription: vi.fn(() => of({})),
+ deleteDatasetFile: vi.fn(() => of({})),
+ getDatasetDiff: vi.fn(() => of([])),
+ multipartUpload: vi.fn(() => of()),
+ finalizeMultipartUpload: vi.fn(() => of({})),
+ };
+ notificationServiceStub = { success: vi.fn(), error: vi.fn(), info:
vi.fn() };
+ downloadServiceStub = {
+ downloadDatasetVersion: vi.fn(() => of(new Blob())),
+ downloadSingleFile: vi.fn(() => of(new Blob())),
+ };
+ hubServiceStub = {
+ getCounts: vi.fn(() => of([{ counts: { like: 0 } }])),
+ postView: vi.fn(() => of(0)),
+ isLiked: vi.fn(() => of([{ isLiked: false }])),
+ postLike: vi.fn(() => of(true)),
+ postUnlike: vi.fn(() => of(true)),
+ };
+ adminSettingsServiceStub = { getSetting: vi.fn(() => of("50")) };
+ });
+
+ describe("ngOnInit", () => {
+ it("loads info, versions, like and view counts but skips liked/upload
settings without a current user", () => {
+ hubServiceStub.getCounts.mockReturnValue(of([{ counts: { like: 7 } }]));
+ hubServiceStub.postView.mockReturnValue(of(42));
+
+ createComponent({ did: 5 });
+ fixture.detectChanges();
Review Comment:
This spec intends to cover the anonymous/no-user path, but with
StubUserService the component’s `isLogin` defaults to true (StubUserService
always has a user). That means `retrieveDatasetInfo()` /
`retrieveDatasetVersionList()` are still invoked with `isLogin=true`, which
doesn’t match the real logged-out state (UserService replays `undefined` and
`isLogin` would be false). Consider explicitly setting `component.isLogin =
false` in this test so the exercised path matches the intended scenario and is
less coupled to the StubUserService emission quirk.
##########
frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.spec.ts:
##########
@@ -334,3 +338,653 @@ describe("DatasetDetailComponent upload queue", () => {
expect(component.pendingChangesCount).toBe(1);
});
});
+
+describe("DatasetDetailComponent behavior", () => {
+ let fixture: ComponentFixture<DatasetDetailComponent>;
+ let component: DatasetDetailComponent;
+
+ type MockService = Record<string, ReturnType<typeof vi.fn>>;
+ let datasetServiceStub: MockService;
+ let notificationServiceStub: MockService;
+ let downloadServiceStub: MockService;
+ let hubServiceStub: MockService;
+ let adminSettingsServiceStub: MockService;
+
+ const CREATION_TS = 1_700_000_000_000;
+
+ const makeDataset = (overrides: Partial<Dataset> = {}): Dataset => ({
+ did: 5,
+ ownerUid: 9,
+ name: "ds",
+ isPublic: false,
+ isDownloadable: true,
+ storagePath: undefined,
+ description: "desc",
+ creationTime: undefined,
+ coverImage: undefined,
+ ...overrides,
+ });
+
+ const makeDashboardDataset = (overrides: Partial<DashboardDataset> = {}):
DashboardDataset => ({
+ isOwner: false,
+ ownerEmail: "[email protected]",
+ dataset: makeDataset(),
+ accessPrivilege: "NONE",
+ size: 0,
+ ...overrides,
+ });
+
+ const makeVersion = (overrides: Partial<DatasetVersion> = {}):
DatasetVersion => ({
+ dvid: 1,
+ did: 5,
+ creatorUid: 9,
+ name: "v1",
+ versionHash: undefined,
+ creationTime: undefined,
+ fileNodes: undefined,
+ ...overrides,
+ });
+
+ const fileLeaf = (name: string, parentDir: string, size: number):
DatasetFileNode => ({
+ name,
+ type: "file",
+ parentDir,
+ size,
+ });
+
+ const createComponent = (params: Record<string, unknown> = { did: 5 }): void
=> {
+ TestBed.configureTestingModule({
+ imports: [DatasetDetailComponent, ...commonTestImports],
+ providers: [
+ { provide: ActivatedRoute, useValue: { params: of(params), data:
of({}) } },
+ { provide: NzModalService, useValue: {} },
+ { provide: DatasetService, useValue: datasetServiceStub },
+ { provide: NotificationService, useValue: notificationServiceStub },
+ { provide: DownloadService, useValue: downloadServiceStub },
+ { provide: UserService, useClass: StubUserService },
+ { provide: HubService, useValue: hubServiceStub },
+ { provide: AdminSettingsService, useValue: adminSettingsServiceStub },
+ { provide: MarkdownService, useValue: { parse: vi.fn(() => "") } },
+ ...commonTestProviders,
+ ],
+ });
+ fixture = TestBed.createComponent(DatasetDetailComponent);
+ component = fixture.componentInstance;
+ };
+
+ // The StubUserService emits MOCK_USER in its own constructor, before the
+ // component subscribes, so currentUid starts undefined; re-emit to log in.
+ const login = (): void => {
+ (TestBed.inject(UserService) as unknown as
StubUserService).userChangeSubject.next(MOCK_USER);
+ };
+
+ beforeEach(() => {
+ datasetServiceStub = {
+ getDataset: vi.fn(() => of(makeDashboardDataset())),
+ retrieveDatasetVersionList: vi.fn(() => of([])),
+ getDatasetCoverUrl: vi.fn(() => of({ url: "http://cover" })),
+ retrieveDatasetVersionFileTree: vi.fn(() => of({ fileNodes:
[fileLeaf("a.txt", "/root", 1)], size: 1 })),
+ createDatasetVersion: vi.fn(() => of(makeVersion())),
+ updateDatasetPublicity: vi.fn(() => of({})),
+ updateDatasetDownloadable: vi.fn(() => of({})),
+ updateDatasetCoverImage: vi.fn(() => of({})),
+ updateDatasetDescription: vi.fn(() => of({})),
+ deleteDatasetFile: vi.fn(() => of({})),
+ getDatasetDiff: vi.fn(() => of([])),
+ multipartUpload: vi.fn(() => of()),
+ finalizeMultipartUpload: vi.fn(() => of({})),
+ };
+ notificationServiceStub = { success: vi.fn(), error: vi.fn(), info:
vi.fn() };
+ downloadServiceStub = {
+ downloadDatasetVersion: vi.fn(() => of(new Blob())),
+ downloadSingleFile: vi.fn(() => of(new Blob())),
+ };
+ hubServiceStub = {
+ getCounts: vi.fn(() => of([{ counts: { like: 0 } }])),
+ postView: vi.fn(() => of(0)),
+ isLiked: vi.fn(() => of([{ isLiked: false }])),
+ postLike: vi.fn(() => of(true)),
+ postUnlike: vi.fn(() => of(true)),
+ };
+ adminSettingsServiceStub = { getSetting: vi.fn(() => of("50")) };
+ });
+
+ describe("ngOnInit", () => {
+ it("loads info, versions, like and view counts but skips liked/upload
settings without a current user", () => {
+ hubServiceStub.getCounts.mockReturnValue(of([{ counts: { like: 7 } }]));
+ hubServiceStub.postView.mockReturnValue(of(42));
+
+ createComponent({ did: 5 });
+ fixture.detectChanges();
+
+ expect(datasetServiceStub.getDataset).toHaveBeenCalled();
+ expect(datasetServiceStub.retrieveDatasetVersionList).toHaveBeenCalled();
+ expect(component.likeCount).toBe(7);
+ expect(component.viewCount).toBe(42);
+ expect(hubServiceStub.isLiked).not.toHaveBeenCalled();
+ expect(adminSettingsServiceStub.getSetting).not.toHaveBeenCalled();
+ });
+
+ it("fetches liked status and upload settings for a logged-in user", () => {
+ hubServiceStub.isLiked.mockReturnValue(of([{ isLiked: true }]));
+
+ createComponent({ did: 5 });
+ login();
+ fixture.detectChanges();
+
+ expect(hubServiceStub.isLiked).toHaveBeenCalled();
+ expect(component.isLiked).toBe(true);
+ expect(adminSettingsServiceStub.getSetting).toHaveBeenCalled();
+ });
+
+ it("makes no hub calls when the route carries no did", () => {
+ createComponent({});
+ component.ngOnInit();
+
+ expect(datasetServiceStub.getDataset).not.toHaveBeenCalled();
+ expect(hubServiceStub.getCounts).not.toHaveBeenCalled();
+ expect(hubServiceStub.postView).not.toHaveBeenCalled();
+ });
+ });
+
+ describe("retrieveDatasetInfo", () => {
+ it("maps dataset fields, formats numeric creation time, and resolves the
cover image url", () => {
+ const dashboard = makeDashboardDataset({
+ isOwner: true,
+ ownerEmail: "[email protected]",
+ accessPrivilege: "WRITE",
+ dataset: makeDataset({
+ name: "N",
+ description: "D",
+ isPublic: true,
+ isDownloadable: false,
+ coverImage: "cover.png",
+ creationTime: CREATION_TS,
+ }),
+ });
+ datasetServiceStub.getDataset.mockReturnValue(of(dashboard));
+ datasetServiceStub.getDatasetCoverUrl.mockReturnValue(of({ url:
"http://c" }));
+
+ createComponent();
+ component.did = 5;
+ component.retrieveDatasetInfo();
+
+ expect(component.datasetName).toBe("N");
+ expect(component.datasetDescription).toBe("D");
+ expect(component.userDatasetAccessLevel).toBe("WRITE");
+ expect(component.datasetIsPublic).toBe(true);
+ expect(component.datasetIsDownloadable).toBe(false);
+ expect(component.ownerEmail).toBe("[email protected]");
+ expect(component.isOwner).toBe(true);
+ expect(component.coverImageUrl).toBe("http://c");
+ expect(component.datasetCreationTime).toEqual(format(new
Date(CREATION_TS), "MM/dd/yyyy HH:mm:ss"));
+ expect(component.datasetCreationTime).toMatch(/^\d{2}\/\d{2}\/\d{4}
\d{2}:\d{2}:\d{2}$/);
+ });
+
+ it("nulls the cover image url when its retrieval fails", () => {
+ datasetServiceStub.getDataset.mockReturnValue(
+ of(makeDashboardDataset({ dataset: makeDataset({ coverImage: "c.png"
}) }))
+ );
+ datasetServiceStub.getDatasetCoverUrl.mockReturnValue(throwError(() =>
new Error("boom")));
+
+ createComponent();
+ component.did = 5;
+ component.coverImageUrl = "stale";
+ component.retrieveDatasetInfo();
+
+ expect(component.coverImageUrl).toBeNull();
+ });
+
+ it("leaves the cover image url null and skips the cover fetch when there
is no cover image", () => {
+ datasetServiceStub.getDataset.mockReturnValue(
+ of(makeDashboardDataset({ dataset: makeDataset({ coverImage: undefined
}) }))
+ );
+
+ createComponent();
+ component.did = 5;
+ component.coverImageUrl = "stale";
+ component.retrieveDatasetInfo();
+
+ expect(component.coverImageUrl).toBeNull();
+ expect(datasetServiceStub.getDatasetCoverUrl).not.toHaveBeenCalled();
+ });
+ });
+
+ describe("retrieveDatasetVersionList", () => {
+ it("selects the first version and delegates to onVersionSelected when the
list is non-empty", () => {
+ const v1 = makeVersion({ dvid: 10, name: "v10" });
+ const v2 = makeVersion({ dvid: 9, name: "v9" });
+ datasetServiceStub.retrieveDatasetVersionList.mockReturnValue(of([v1,
v2]));
+
+ createComponent();
+ component.did = 5;
+ const spy = vi.spyOn(component, "onVersionSelected");
+ component.retrieveDatasetVersionList();
+
+ expect(component.versions).toEqual([v1, v2]);
+ expect(component.selectedVersion).toEqual(v1);
+ expect(spy).toHaveBeenCalledWith(v1);
+ });
+
+ it("makes no selection when the version list is empty", () => {
+ datasetServiceStub.retrieveDatasetVersionList.mockReturnValue(of([]));
+
+ createComponent();
+ component.did = 5;
+ component.selectedVersion = undefined;
+ const spy = vi.spyOn(component, "onVersionSelected");
+ component.retrieveDatasetVersionList();
+
+ expect(component.versions).toEqual([]);
+ expect(component.selectedVersion).toBeUndefined();
+ expect(spy).not.toHaveBeenCalled();
+ });
+ });
+
+ describe("onVersionSelected", () => {
+ it("walks nested directories to the first file leaf and loads it", () => {
+ const leaf = fileLeaf("c.txt", "/root/a", 42);
+ const tree: DatasetFileNode[] = [{ name: "a", type: "directory",
parentDir: "/root", children: [leaf] }];
+ datasetServiceStub.retrieveDatasetVersionFileTree.mockReturnValue(of({
fileNodes: tree, size: 100 }));
+
+ createComponent();
+ component.did = 5;
+ component.onVersionSelected(makeVersion({ dvid: 2, creationTime:
CREATION_TS }));
+
+ expect(component.fileTreeNodeList).toEqual(tree);
+ expect(component.currentDatasetVersionSize).toBe(100);
+
expect(component.currentDisplayedFileName).toBe(getFullPathFromDatasetFileNode(leaf));
+ expect(component.currentFileSize).toBe(42);
+
expect(component.selectedVersionCreationTime).toMatch(/^\d{2}\/\d{2}\/\d{4}
\d{2}:\d{2}:\d{2}$/);
+ });
+
+ it("does not fetch a file tree for a version without a dvid", () => {
+ createComponent();
+ component.did = 5;
+ component.onVersionSelected(makeVersion({ dvid: undefined }));
+
+
expect(datasetServiceStub.retrieveDatasetVersionFileTree).not.toHaveBeenCalled();
+ });
+ });
+
+ describe("isDownloadAllowed and userHasWriteAccess", () => {
+ beforeEach(() => createComponent());
+
+ it("always allows the owner to download, even when the dataset is not
downloadable", () => {
+ component.isOwner = true;
+ component.datasetIsDownloadable = false;
+ expect(component.isDownloadAllowed()).toBe(true);
+ });
+
+ it("allows a non-owner to download a public downloadable dataset without
explicit access", () => {
+ component.isOwner = false;
+ component.datasetIsDownloadable = true;
+ component.datasetIsPublic = true;
+ component.userDatasetAccessLevel = "NONE";
+ expect(component.isDownloadAllowed()).toBe(true);
+ });
+
+ it("blocks a non-owner from a private downloadable dataset without
access", () => {
+ component.isOwner = false;
+ component.datasetIsDownloadable = true;
+ component.datasetIsPublic = false;
+ component.userDatasetAccessLevel = "NONE";
+ expect(component.isDownloadAllowed()).toBe(false);
+ });
+
+ it("blocks download when the dataset is not downloadable", () => {
+ component.isOwner = false;
+ component.datasetIsDownloadable = false;
+ component.datasetIsPublic = true;
+ expect(component.isDownloadAllowed()).toBe(false);
+ });
+
+ it("reports write access only for the WRITE privilege", () => {
+ component.userDatasetAccessLevel = "WRITE";
+ expect(component.userHasWriteAccess()).toBe(true);
+ component.userDatasetAccessLevel = "READ";
+ expect(component.userHasWriteAccess()).toBe(false);
+ component.userDatasetAccessLevel = "NONE";
+ expect(component.userHasWriteAccess()).toBe(false);
+ });
+ });
+
+ describe("publicity and downloadable toggles", () => {
+ it("marks the dataset public and toasts on success", () => {
+ createComponent();
+ component.did = 5;
+ component.datasetName = "MyDS";
+ component.onPublicStatusChange(true);
+
+ expect(component.datasetIsPublic).toBe(true);
+ expect(notificationServiceStub.success).toHaveBeenCalledWith("Dataset
MyDS is now public");
+ });
+
+ it("keeps the public flag and toasts an error when the publicity update
fails", () => {
+ datasetServiceStub.updateDatasetPublicity.mockReturnValue(throwError(()
=> new Error("boom")));
+ createComponent();
+ component.did = 5;
+ component.datasetIsPublic = false;
+ component.onPublicStatusChange(true);
+
+ expect(component.datasetIsPublic).toBe(false);
+ expect(notificationServiceStub.error).toHaveBeenCalledWith("Fail to
change the dataset publicity");
+ });
+
+ it("marks downloads not-allowed and toasts on success", () => {
+ createComponent();
+ component.did = 5;
+ component.onDownloadableStatusChange(false);
+
+ expect(component.datasetIsDownloadable).toBe(false);
+ expect(notificationServiceStub.success).toHaveBeenCalledWith("Dataset
downloads are now not allowed");
+ });
+
+ it("keeps the downloadable flag and toasts an error when the update
fails", () => {
+
datasetServiceStub.updateDatasetDownloadable.mockReturnValue(throwError(() =>
new Error("boom")));
+ createComponent();
+ component.did = 5;
+ component.datasetIsDownloadable = true;
+ component.onDownloadableStatusChange(false);
+
+ expect(component.datasetIsDownloadable).toBe(true);
+ expect(notificationServiceStub.error).toHaveBeenCalledWith("Failed to
change the dataset download permission");
+ });
+ });
+
+ describe("onClickOpenVersionCreator", () => {
+ it("creates a version, clears the name, refreshes the list and emits a
change on success", () => {
+
datasetServiceStub.createDatasetVersion.mockReturnValue(of(makeVersion()));
+ datasetServiceStub.retrieveDatasetVersionList.mockReturnValue(of([]));
+ createComponent();
+ component.did = 5;
+ component.versionName = "v2";
+ const emit = vi.fn();
+ component.userMakeChanges.subscribe(emit);
+
+ component.onClickOpenVersionCreator();
+
+ expect(datasetServiceStub.createDatasetVersion).toHaveBeenCalledWith(5,
"v2");
+ expect(notificationServiceStub.success).toHaveBeenCalledWith("Version
Created");
+ expect(component.versionName).toBe("");
+ expect(component.isCreatingVersion).toBe(false);
+ expect(datasetServiceStub.retrieveDatasetVersionList).toHaveBeenCalled();
+ expect(emit).toHaveBeenCalled();
+ });
+
+ it("surfaces the backend message and resets the in-progress flag on
failure", () => {
+ datasetServiceStub.createDatasetVersion.mockReturnValue(throwError(() =>
({ error: { message: "boom" } })));
+ createComponent();
+ component.did = 5;
+ component.versionName = "v2";
+
+ component.onClickOpenVersionCreator();
+
+ expect(notificationServiceStub.error).toHaveBeenCalledWith("Version
creation failed: boom");
+ expect(component.isCreatingVersion).toBe(false);
+ });
+
+ it("ignores a second click while a version creation is already in
progress", () => {
+ datasetServiceStub.createDatasetVersion.mockReturnValue(new Subject());
+ createComponent();
+ component.did = 5;
+
+ component.onClickOpenVersionCreator();
+ component.onClickOpenVersionCreator();
+
+ expect(datasetServiceStub.createDatasetVersion).toHaveBeenCalledTimes(1);
+ expect(component.isCreatingVersion).toBe(true);
+ });
+ });
+
+ describe("downloads", () => {
+ it("downloads the selected version as a zip when did and dvid are
present", () => {
+ createComponent();
+ component.did = 5;
+ component.datasetName = "DS";
+ component.selectedVersion = makeVersion({ dvid: 3, name: "v3" });
+
+ component.onClickDownloadVersionAsZip();
+
+
expect(downloadServiceStub.downloadDatasetVersion).toHaveBeenCalledWith(5, 3,
"DS", "v3");
+ });
+
+ it("does not download a zip when no version is selected", () => {
+ createComponent();
+ component.did = 5;
+ component.selectedVersion = undefined;
+
+ component.onClickDownloadVersionAsZip();
+
+
expect(downloadServiceStub.downloadDatasetVersion).not.toHaveBeenCalled();
+ });
+
+ it("uses the public endpoint to download the current file for a public
non-owner dataset", () => {
+ createComponent();
+ component.did = 5;
+ component.selectedVersion = makeVersion({ dvid: 3 });
+ component.datasetIsPublic = true;
+ component.isOwner = false;
+ component.currentDisplayedFileName = "/a/b/c.txt";
+
+ component.onClickDownloadCurrentFile();
+
+
expect(downloadServiceStub.downloadSingleFile).toHaveBeenCalledWith("/a/b/c.txt",
false);
+ });
+
+ it("uses the authenticated endpoint to download the current file for the
owner", () => {
+ createComponent();
+ component.did = 5;
+ component.selectedVersion = makeVersion({ dvid: 3 });
+ component.datasetIsPublic = true;
+ component.isOwner = true;
+ component.currentDisplayedFileName = "/a/b/c.txt";
+
+ component.onClickDownloadCurrentFile();
+
+
expect(downloadServiceStub.downloadSingleFile).toHaveBeenCalledWith("/a/b/c.txt",
true);
+ });
+
+ it("does not download the current file without a selected version dvid",
() => {
+ createComponent();
+ component.did = 5;
+ component.selectedVersion = undefined;
+
+ component.onClickDownloadCurrentFile();
+
+ expect(downloadServiceStub.downloadSingleFile).not.toHaveBeenCalled();
+ });
+ });
+
+ describe("staged objects and view flags", () => {
+ beforeEach(() => createComponent());
+
+ it("tracks the pending-change count from staged objects", () => {
+ const staged: DatasetStagedObject[] = [
+ { path: "a", pathType: "file", diffType: "added", sizeBytes: 1 },
+ { path: "b", pathType: "file", diffType: "added", sizeBytes: 1 },
+ ];
+ component.onStagedObjectsUpdated(staged);
+ expect(component.pendingChangesCount).toBe(2);
+ expect(component.userHasPendingChanges).toBe(true);
+
+ component.onStagedObjectsUpdated([]);
+ expect(component.pendingChangesCount).toBe(0);
+ expect(component.userHasPendingChanges).toBe(false);
+ });
+
+ it("toggles the maximize, right-bar and precise-view-count flags", () => {
+ expect(component.isMaximized).toBe(false);
+ component.onClickScaleTheView();
+ expect(component.isMaximized).toBe(true);
+
+ expect(component.isRightBarCollapsed).toBe(false);
+ component.onClickHideRightBar();
+ expect(component.isRightBarCollapsed).toBe(true);
+
+ expect(component.displayPreciseViewCount).toBe(false);
+ component.changeViewDisplayStyle();
+ expect(component.displayPreciseViewCount).toBe(true);
+ });
+ });
+
+ describe("toggleLike", () => {
+ it("unlikes and decrements the like count when currently liked", () => {
+ hubServiceStub.postUnlike.mockReturnValue(of(true));
+ hubServiceStub.getCounts.mockReturnValue(of([{ counts: { like: 4 } }]));
+ createComponent();
+ component.did = 5;
+ component.currentUid = MOCK_USER.uid;
+ component.isLiked = true;
+ component.likeCount = 5;
+
+ component.toggleLike();
+
+ expect(hubServiceStub.postUnlike).toHaveBeenCalled();
+ expect(component.isLiked).toBe(false);
+ expect(component.likeCount).toBe(4);
+ });
+
+ it("likes and increments the like count when not currently liked", () => {
+ hubServiceStub.postLike.mockReturnValue(of(true));
+ hubServiceStub.getCounts.mockReturnValue(of([{ counts: { like: 6 } }]));
+ createComponent();
+ component.did = 5;
+ component.currentUid = MOCK_USER.uid;
+ component.isLiked = false;
+ component.likeCount = 5;
+
+ component.toggleLike();
+
+ expect(hubServiceStub.postLike).toHaveBeenCalled();
+ expect(component.isLiked).toBe(true);
+ expect(component.likeCount).toBe(6);
+ });
+
+ it("does nothing when no user is logged in", () => {
+ createComponent();
+ component.did = 5;
+ component.currentUid = undefined;
+
+ component.toggleLike();
+
+ expect(hubServiceStub.postLike).not.toHaveBeenCalled();
+ expect(hubServiceStub.postUnlike).not.toHaveBeenCalled();
+ });
+ });
+
+ describe("cover image and description persistence", () => {
+ it("refreshes the cover url and toasts success after setting a cover
image", () => {
+ datasetServiceStub.updateDatasetCoverImage.mockReturnValue(of({}));
+ datasetServiceStub.getDatasetCoverUrl.mockReturnValue(of({ url:
"http://new" }));
+ createComponent();
+ component.did = 5;
+ component.selectedVersion = makeVersion({ name: "v1" });
+
+ component.onSetCoverImage("img.png");
+
+
expect(datasetServiceStub.updateDatasetCoverImage).toHaveBeenCalledWith(5,
"v1/img.png");
+ expect(component.coverImageUrl).toBe("http://new");
+ expect(notificationServiceStub.success).toHaveBeenCalledWith("Cover
image updated.");
+ });
+
+ it("surfaces the backend message when setting the cover image fails", ()
=> {
+ datasetServiceStub.updateDatasetCoverImage.mockReturnValue(
+ throwError(() => new HttpErrorResponse({ error: { message: "nope" },
status: 400 }))
+ );
+ createComponent();
+ component.did = 5;
+ component.selectedVersion = makeVersion({ name: "v1" });
+
+ component.onSetCoverImage("img.png");
+
+ expect(notificationServiceStub.error).toHaveBeenCalledWith("nope");
+ });
+
+ it("does nothing when there is no selected version to attach the cover
to", () => {
+ createComponent();
+ component.did = 5;
+ component.selectedVersion = undefined;
+
+ component.onSetCoverImage("img.png");
+
+
expect(datasetServiceStub.updateDatasetCoverImage).not.toHaveBeenCalled();
+ });
+
+ it("persists a changed description and updates the field", () => {
+ datasetServiceStub.updateDatasetDescription.mockReturnValue(of({}));
+ createComponent();
+ component.did = 5;
+ component.datasetDescription = "old";
+
+ component.onDatasetDescriptionChange("new");
+
+
expect(datasetServiceStub.updateDatasetDescription).toHaveBeenCalledWith(5,
"new");
+ expect(component.datasetDescription).toBe("new");
+ });
+
+ it("skips the persistence call when the description is unchanged", () => {
+ createComponent();
+ component.did = 5;
+ component.datasetDescription = "same";
+
+ component.onDatasetDescriptionChange("same");
+
+
expect(datasetServiceStub.updateDatasetDescription).not.toHaveBeenCalled();
+ });
+
+ it("reverts the description and toasts an error when persistence fails",
() => {
+
datasetServiceStub.updateDatasetDescription.mockReturnValue(throwError(() =>
new Error("boom")));
+ createComponent();
+ component.did = 5;
+ component.datasetDescription = "old";
+
+ component.onDatasetDescriptionChange("new");
+
+ expect(component.datasetDescription).toBe("old");
+ expect(notificationServiceStub.error).toHaveBeenCalledWith("Failed to
update dataset description");
+ });
+ });
+
+ describe("copyCurrentFilePath", () => {
+ let originalClipboard: Clipboard;
+ let writeText: ReturnType<typeof vi.fn>;
+
+ beforeEach(() => {
+ originalClipboard = navigator.clipboard;
+ writeText = vi.fn().mockResolvedValue(undefined);
+ Object.defineProperty(navigator, "clipboard", { value: { writeText },
configurable: true });
+ createComponent();
+ });
+
+ afterEach(() => {
+ Object.defineProperty(navigator, "clipboard", { value:
originalClipboard, configurable: true });
+ });
Review Comment:
The clipboard stub restores only the *value*, not the original property
shape. If `navigator.clipboard` was not an own property (or didn’t exist), the
afterEach leaves an own `clipboard` property behind (value `undefined`), which
can leak into other test files. Track whether the property originally existed
on `navigator` and delete the stubbed property when it didn’t.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]