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-6513-3e62b8c06430bdee00250ee0c149c6c0b6d99e92 in repository https://gitbox.apache.org/repos/asf/texera.git
commit 501bc137ffff94867cdf0a533fdff0041b4f3888 Author: Meng Wang <[email protected]> AuthorDate: Sat Jul 18 12:01:05 2026 -0700 test(frontend): add unit test coverage for UserDatasetVersionCreatorComponent (#6513) ### What changes were proposed in this PR? Adds a Vitest spec for `UserDatasetVersionCreatorComponent` (`frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-version-creator/user-dataset-version-creator.component.ts`), previously without a spec (~3% coverage). `NZ_MODAL_DATA`, `NzModalRef`, `DatasetService`, and `NotificationService` are stubbed; a minimal formly "input" type is registered so the declared fields materialize into real form controls (with validators) on render. 10 tests cover: - `should create` and render the formly form. - `ngOnInit` builds the version-description field when `isCreatingVersion`, and the name + description fields otherwise. - Form validity gating — `onClickCreate` is a no-op (no service call, modal stays open) while the required `name` is empty. - `onClickCreate` (dataset mode) — creates the dataset with a sanitized name and the public/downloadable flags, then closes the modal with the result on success. - `onClickCreate` (version mode) — calls `createDatasetVersion(did, description)` and closes on success; notifies and closes with `null` on failure. - `onClickCancel` closes the modal with `null`. - `datasetNameSanitization` lowercases, dashes non-alphanumerics, and flags the change. - The public / downloadable switch handlers update their flags. No production code was changed. ### Any related issues, documentation, discussions? Closes #6508 ### How was this PR tested? New unit tests, run locally in `frontend/` (all green; the failure path was verified by deliberately breaking the form-validity assertion to confirm the suite goes red): ``` ng test --watch=false --include src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-version-creator/user-dataset-version-creator.component.spec.ts # Test Files 1 passed (1) | Tests 10 passed (10) prettier --write <spec> # formatted eslint <spec> # clean ``` The dataset service and modal ref are stubbed, so the suite makes no network calls and opens no real modal (see `frontend/TESTING.md`). ### Was this PR authored or co-authored using generative AI tooling? Generated-by: Claude Code (Opus 4.8 [1M context]) --- .../user-dataset-version-creator.component.spec.ts | 189 +++++++++++++++++++++ 1 file changed, 189 insertions(+) diff --git a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-version-creator/user-dataset-version-creator.component.spec.ts b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-version-creator/user-dataset-version-creator.component.spec.ts new file mode 100644 index 0000000000..17369b27f7 --- /dev/null +++ b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-version-creator/user-dataset-version-creator.component.spec.ts @@ -0,0 +1,189 @@ +/** + * 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 { Component } from "@angular/core"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; +import { FormsModule, ReactiveFormsModule } from "@angular/forms"; +import { FieldType, FieldTypeConfig, FormlyModule } from "@ngx-formly/core"; +import { HttpClientTestingModule } from "@angular/common/http/testing"; +import { NZ_MODAL_DATA, NzModalRef } from "ng-zorro-antd/modal"; +import { of, throwError } from "rxjs"; +import { UserDatasetVersionCreatorComponent } from "./user-dataset-version-creator.component"; +import { DatasetService } from "../../../../../service/user/dataset/dataset.service"; +import { NotificationService } from "../../../../../../common/service/notification/notification.service"; +import { commonTestProviders } from "../../../../../../common/testing/test-utils"; + +// Minimal stand-in for the "input" formly type (the real one comes from the app's +// UI formly module). Registering it lets formly materialize the declared fields +// into real form controls — with their validators — on render. +@Component({ template: "", standalone: true }) +class StubFormlyInputComponent extends FieldType<FieldTypeConfig> {} + +describe("UserDatasetVersionCreatorComponent", () => { + let modalClose: ReturnType<typeof vi.fn>; + let createDataset: ReturnType<typeof vi.fn>; + let createDatasetVersion: ReturnType<typeof vi.fn>; + let notifySuccess: ReturnType<typeof vi.fn>; + let notifyError: ReturnType<typeof vi.fn>; + + async function createFixture(modalData: { + isCreatingVersion: boolean; + did?: number; + }): Promise<ComponentFixture<UserDatasetVersionCreatorComponent>> { + modalClose = vi.fn(); + createDataset = vi.fn(); + createDatasetVersion = vi.fn(); + notifySuccess = vi.fn(); + notifyError = vi.fn(); + + await TestBed.configureTestingModule({ + imports: [ + UserDatasetVersionCreatorComponent, + BrowserAnimationsModule, + FormsModule, + ReactiveFormsModule, + FormlyModule.forRoot({ types: [{ name: "input", component: StubFormlyInputComponent }] }), + HttpClientTestingModule, + ], + providers: [ + { provide: NZ_MODAL_DATA, useValue: modalData }, + { provide: NzModalRef, useValue: { close: modalClose } }, + { provide: DatasetService, useValue: { createDataset, createDatasetVersion } }, + { provide: NotificationService, useValue: { success: notifySuccess, error: notifyError } }, + ...commonTestProviders, + ], + }).compileComponents(); + + return TestBed.createComponent(UserDatasetVersionCreatorComponent); + } + + it("should create and render the formly form (dataset-creation mode)", async () => { + const fixture = await createFixture({ isCreatingVersion: false }); + fixture.detectChanges(); + + expect(fixture.componentInstance).toBeTruthy(); + // Formly materializes the declared fields into real form controls on render. + expect(fixture.componentInstance.form.contains("name")).toBe(true); + }); + + it("ngOnInit builds the version-description field when creating a version", async () => { + const fixture = await createFixture({ isCreatingVersion: true, did: 5 }); + fixture.detectChanges(); + + expect(fixture.componentInstance.fields.map(f => f.key)).toEqual(["versionDescription"]); + }); + + it("ngOnInit builds the name + description fields when creating a dataset", async () => { + const fixture = await createFixture({ isCreatingVersion: false }); + fixture.detectChanges(); + + expect(fixture.componentInstance.fields.map(f => f.key)).toEqual(["name", "description"]); + }); + + it("onClickCreate does nothing when the required name is empty (invalid form)", async () => { + const fixture = await createFixture({ isCreatingVersion: false }); + fixture.detectChanges(); + const component = fixture.componentInstance; + + expect(component.form.valid).toBe(false); // name is required and empty + component.onClickCreate(); + + expect(createDataset).not.toHaveBeenCalled(); + expect(modalClose).not.toHaveBeenCalled(); + }); + + it("onClickCreate creates a dataset with a sanitized name and closes the modal on success", async () => { + const fixture = await createFixture({ isCreatingVersion: false }); + fixture.detectChanges(); + const component = fixture.componentInstance; + component.form.get("name")?.setValue("My Dataset"); + component.form.get("description")?.setValue("desc"); + component.onPublicStatusChange(true); + component.onDownloadableStatusChange(true); + createDataset.mockReturnValue(of({ did: 7 })); + + component.onClickCreate(); + + expect(createDataset).toHaveBeenCalledTimes(1); + expect(createDataset.mock.calls[0][0]).toMatchObject({ + name: "my-dataset", + description: "desc", + isPublic: true, + isDownloadable: true, + }); + expect(notifySuccess).toHaveBeenCalled(); + expect(modalClose).toHaveBeenCalledWith({ did: 7 }); + }); + + it("onClickCreate creates a dataset version and closes the modal on success", async () => { + const fixture = await createFixture({ isCreatingVersion: true, did: 42 }); + fixture.detectChanges(); + const component = fixture.componentInstance; + component.form.get("versionDescription")?.setValue("v2 notes"); + createDatasetVersion.mockReturnValue(of({ dvid: 1 })); + + component.onClickCreate(); + + expect(createDatasetVersion).toHaveBeenCalledWith(42, "v2 notes"); + expect(notifySuccess).toHaveBeenCalledWith("Version Created"); + expect(modalClose).toHaveBeenCalledWith({ dvid: 1 }); + }); + + it("onClickCreate notifies and closes with null when version creation fails", async () => { + const fixture = await createFixture({ isCreatingVersion: true, did: 42 }); + fixture.detectChanges(); + const component = fixture.componentInstance; + component.form.get("versionDescription")?.setValue("v2"); + createDatasetVersion.mockReturnValue(throwError(() => ({ error: { message: "boom" } }))); + + component.onClickCreate(); + + expect(notifyError).toHaveBeenCalled(); + expect(modalClose).toHaveBeenCalledWith(null); + }); + + it("onClickCancel closes the modal with null", async () => { + const fixture = await createFixture({ isCreatingVersion: false }); + fixture.detectChanges(); + + fixture.componentInstance.onClickCancel(); + + expect(modalClose).toHaveBeenCalledWith(null); + }); + + it("datasetNameSanitization lowercases, dashes non-alphanumerics, and flags the change", async () => { + const fixture = await createFixture({ isCreatingVersion: false }); + const component = fixture.componentInstance; + + expect(component.datasetNameSanitization(" My Data Set!! ")).toBe("my-data-set-"); + expect(component.isDatasetNameSanitized).toBe(true); + }); + + it("switch handlers toggle the public and downloadable flags", async () => { + const fixture = await createFixture({ isCreatingVersion: false }); + const component = fixture.componentInstance; + + component.onPublicStatusChange(true); + component.onDownloadableStatusChange(true); + + expect(component.isDatasetPublic).toBe(true); + expect(component.isDatasetDownloadable).toBe(true); + }); +});
