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

commit 2109103457a1dcada80f712a09dec0bc757f664c
Author: Xinyuan Lin <[email protected]>
AuthorDate: Sun Jul 19 23:32:54 2026 -0700

    test(frontend): add unit tests for UserProjectComponent (#6574)
    
    ### What changes were proposed in this PR?
    
    Adds a Vitest unit spec for `UserProjectComponent`, which previously had
    no test coverage. The 14 tests cover:
    - constructor resolving `uid` from `getCurrentUser()` (default,
    specific, undefined).
    - `ngOnInit` populating `userProjectEntries` from `getProjectList`.
    - `deleteProject` calling the service then reloading.
    - `clickCreateButton`/`unclickCreateButton` flag + name reset.
    - `createNewProject` valid (creates + reloads) vs empty and duplicate
    names (notification error, no create).
    - `sortByCreationTime`/`sortByNameAsc`/`sortByNameDesc` orderings.
    - `openPublicProject` modal args (`nzData.disabledList` set of existing
    pids) and the `afterClose`-driven reload.
    
    The heavy `UserProjectListItemComponent` child is swapped for a
    same-selector stub via the additive `remove`/`add` override.
    
    ### Any related issues, documentation, discussions?
    
    Closes #6571.
    
    ### How was this PR tested?
    
    `npx ng test --watch=false
    --include='**/user-project.component.spec.ts'` -> 14/14 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])
    
    ---------
    
    Signed-off-by: Xinyuan Lin <[email protected]>
    Co-authored-by: Copilot Autofix powered by AI 
<[email protected]>
---
 .../user-project/user-project.component.spec.ts    | 262 +++++++++++++++++++++
 1 file changed, 262 insertions(+)

diff --git 
a/frontend/src/app/dashboard/component/user/user-project/user-project.component.spec.ts
 
b/frontend/src/app/dashboard/component/user/user-project/user-project.component.spec.ts
new file mode 100644
index 0000000000..e1fd78c9f5
--- /dev/null
+++ 
b/frontend/src/app/dashboard/component/user/user-project/user-project.component.spec.ts
@@ -0,0 +1,262 @@
+/**
+ * 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, EventEmitter, Input, Output } from "@angular/core";
+import { ComponentFixture, TestBed } from "@angular/core/testing";
+import { NoopAnimationsModule } from "@angular/platform-browser/animations";
+import { of, Subject } from "rxjs";
+import { UserProjectComponent } from "./user-project.component";
+import { UserProjectListItemComponent } from 
"./user-project-list-item/user-project-list-item.component";
+import { PublicProjectComponent } from 
"./public-project/public-project.component";
+import { UserProjectService } from 
"../../../service/user/project/user-project.service";
+import { DashboardProject } from "../../../type/dashboard-project.interface";
+import { NotificationService } from 
"../../../../common/service/notification/notification.service";
+import { UserService } from "../../../../common/service/user/user.service";
+import { MOCK_USER, MOCK_USER_ID, StubUserService } from 
"../../../../common/service/user/stub-user.service";
+import { NzModalService } from "ng-zorro-antd/modal";
+import { commonTestProviders } from "../../../../common/testing/test-utils";
+
+// Minimal stand-in for the heavyweight list-item child (pulls in ngx-markdown,
+// color-picker, share-access, etc.). It keeps the same selector and declares 
the
+// inputs/outputs the parent template binds so change-detection compiles.
+@Component({
+  selector: "texera-user-project-list-item",
+  standalone: true,
+  template: "",
+})
+class StubUserProjectListItemComponent {
+  @Input() entry?: DashboardProject;
+  @Input() editable = false;
+  @Input() uid?: number;
+  @Output() deleted = new EventEmitter<void>();
+  @Output() refresh = new EventEmitter<void>();
+}
+
+describe("UserProjectComponent", () => {
+  let fixture: ComponentFixture<UserProjectComponent>;
+  let component: UserProjectComponent;
+
+  let projectServiceMock: {
+    getProjectList: ReturnType<typeof vi.fn>;
+    deleteProject: ReturnType<typeof vi.fn>;
+    createProject: ReturnType<typeof vi.fn>;
+  };
+  let notificationServiceMock: { error: ReturnType<typeof vi.fn> };
+  let modalServiceMock: { create: ReturnType<typeof vi.fn> };
+  let afterClose$: Subject<void>;
+
+  const project = (pid: number, name: string, creationTime: number): 
DashboardProject => ({
+    pid,
+    name,
+    description: `desc-${pid}`,
+    ownerId: 1,
+    creationTime,
+    color: null,
+    accessLevel: "WRITE",
+  });
+
+  // A fresh copy every call so the component's in-place sorts don't bleed 
between tests.
+  const projectFixture = (): DashboardProject[] => [project(1, "Alpha", 100), 
project(2, "Beta", 200)];
+
+  const newProject = project(3, "Gamma", 300);
+
+  const createComponent = (): void => {
+    fixture = TestBed.createComponent(UserProjectComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  };
+
+  beforeEach(async () => {
+    afterClose$ = new Subject<void>();
+    projectServiceMock = {
+      getProjectList: vi.fn(() => of(projectFixture())),
+      deleteProject: vi.fn(() => of({} as Response)),
+      createProject: vi.fn(() => of(newProject)),
+    };
+    notificationServiceMock = { error: vi.fn() };
+    modalServiceMock = { create: vi.fn(() => ({ afterClose: afterClose$ })) };
+
+    await TestBed.configureTestingModule({
+      imports: [UserProjectComponent, NoopAnimationsModule],
+      providers: [
+        { provide: UserProjectService, useValue: projectServiceMock },
+        { provide: NotificationService, useValue: notificationServiceMock },
+        { provide: NzModalService, useValue: modalServiceMock },
+        { provide: UserService, useClass: StubUserService },
+        ...commonTestProviders,
+      ],
+    })
+      // Swap the heavyweight list-item child for the stub above; additive
+      // remove/add form (the `set` form has known bugs with standalone 
imports).
+      .overrideComponent(UserProjectComponent, {
+        remove: { imports: [UserProjectListItemComponent] },
+        add: { imports: [StubUserProjectListItemComponent] },
+      })
+      .compileComponents();
+  });
+
+  afterEach(() => {
+    fixture?.destroy();
+    document.querySelectorAll(".cdk-overlay-container").forEach(c => 
(c.innerHTML = ""));
+    vi.restoreAllMocks();
+  });
+
+  it("reads the current uid from UserService in the constructor", () => {
+    createComponent();
+    expect(component.uid).toBe(MOCK_USER_ID);
+  });
+
+  it("reflects a specific uid returned by getCurrentUser", () => {
+    const userService = TestBed.inject(UserService);
+    vi.spyOn(userService, "getCurrentUser").mockReturnValue({ ...MOCK_USER, 
uid: 42 });
+
+    createComponent();
+
+    expect(component.uid).toBe(42);
+  });
+
+  it("leaves uid undefined when there is no current user", () => {
+    const userService = TestBed.inject(UserService);
+    vi.spyOn(userService, "getCurrentUser").mockReturnValue(undefined);
+
+    createComponent();
+
+    expect(component.uid).toBeUndefined();
+  });
+
+  it("populates userProjectEntries from getProjectList on ngOnInit", () => {
+    createComponent();
+
+    expect(projectServiceMock.getProjectList).toHaveBeenCalledTimes(1);
+    expect(component.userProjectEntries.map(p => p.name)).toEqual(["Alpha", 
"Beta"]);
+  });
+
+  it("deleteProject deletes then reloads the list", () => {
+    createComponent();
+
+    component.deleteProject(2);
+
+    expect(projectServiceMock.deleteProject).toHaveBeenCalledWith(2);
+    // once on init, once from the reload triggered by the delete
+    expect(projectServiceMock.getProjectList).toHaveBeenCalledTimes(2);
+  });
+
+  it("clickCreateButton flips the create flag on", () => {
+    createComponent();
+    component.createButtonIsClicked = false;
+
+    component.clickCreateButton();
+
+    expect(component.createButtonIsClicked).toBe(true);
+  });
+
+  it("unclickCreateButton resets the flag and clears the pending name", () => {
+    createComponent();
+    component.createButtonIsClicked = true;
+    component.createProjectName = "half-typed";
+
+    component.unclickCreateButton();
+
+    expect(component.createButtonIsClicked).toBe(false);
+    expect(component.createProjectName).toBe("");
+  });
+
+  it("createNewProject creates and reloads for a valid unique name", () => {
+    createComponent();
+    component.createProjectName = "Gamma";
+
+    component.createNewProject();
+
+    expect(projectServiceMock.createProject).toHaveBeenCalledTimes(1);
+    expect(projectServiceMock.createProject).toHaveBeenCalledWith("Gamma");
+    expect(projectServiceMock.getProjectList).toHaveBeenCalledTimes(2);
+    expect(notificationServiceMock.error).not.toHaveBeenCalled();
+  });
+
+  it("createNewProject rejects an empty name without calling the service", () 
=> {
+    createComponent();
+    component.createProjectName = "";
+
+    component.createNewProject();
+
+    expect(projectServiceMock.createProject).not.toHaveBeenCalled();
+    expect(notificationServiceMock.error).toHaveBeenCalledTimes(1);
+  });
+
+  it("createNewProject rejects a duplicate name without calling the service", 
() => {
+    createComponent();
+    // "Alpha" is already present from the initial getProjectList fixture.
+    component.createProjectName = "Alpha";
+
+    component.createNewProject();
+
+    expect(projectServiceMock.createProject).not.toHaveBeenCalled();
+    expect(notificationServiceMock.error).toHaveBeenCalledTimes(1);
+  });
+
+  it("sortByCreationTime orders entries ascending by creationTime", () => {
+    createComponent();
+    component.userProjectEntries = [project(1, "c", 30), project(2, "a", 10), 
project(3, "b", 20)];
+
+    component.sortByCreationTime();
+
+    expect(component.userProjectEntries.map(p => p.creationTime)).toEqual([10, 
20, 30]);
+  });
+
+  it("sortByNameAsc orders entries A -> Z case-insensitively", () => {
+    createComponent();
+    component.userProjectEntries = [project(1, "Banana", 1), project(2, 
"apple", 2), project(3, "Cherry", 3)];
+
+    component.sortByNameAsc();
+
+    expect(component.userProjectEntries.map(p => p.name)).toEqual(["apple", 
"Banana", "Cherry"]);
+  });
+
+  it("sortByNameDesc orders entries Z -> A case-insensitively", () => {
+    createComponent();
+    component.userProjectEntries = [project(1, "Banana", 1), project(2, 
"apple", 2), project(3, "Cherry", 3)];
+
+    component.sortByNameDesc();
+
+    expect(component.userProjectEntries.map(p => p.name)).toEqual(["Cherry", 
"Banana", "apple"]);
+  });
+
+  it("openPublicProject opens the modal with the existing pids disabled and 
reloads on close", () => {
+    createComponent();
+
+    component.openPublicProject();
+
+    expect(modalServiceMock.create).toHaveBeenCalledTimes(1);
+    const createArg = modalServiceMock.create.mock.calls[0][0];
+    expect(createArg.nzContent).toBe(PublicProjectComponent);
+    expect(createArg.nzTitle).toBe("Add Public Projects");
+    const disabledList: Set<number> = createArg.nzData.disabledList;
+    expect(disabledList).toBeInstanceOf(Set);
+    expect(disabledList.has(1)).toBe(true);
+    expect(disabledList.has(2)).toBe(true);
+    expect(disabledList.size).toBe(2);
+
+    // no reload yet: still just the initial ngOnInit fetch
+    expect(projectServiceMock.getProjectList).toHaveBeenCalledTimes(1);
+
+    afterClose$.next();
+
+    expect(projectServiceMock.getProjectList).toHaveBeenCalledTimes(2);
+  });
+});

Reply via email to