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-5234-d8c254c75a024f00106528dfb66f4ccc4084f012 in repository https://gitbox.apache.org/repos/asf/texera.git
commit a4a3df5af33ef4b67ca5ebb3d4be52efd00d4254 Author: Matthew B. <[email protected]> AuthorDate: Wed May 27 23:47:49 2026 -0700 test: unit tests for LandingPageComponent (#5234) ### What changes were proposed in this PR? - Add `landing-page.component.spec.ts` covering creation, the constructor's `userChanged()` subscription (login/logout transitions update `isLogin` and `currentUid`), `ngOnInit` calling both `getWorkflowCount` and `loadTops`, count population from `HubService.getCount`, the three top-loved buckets populated by `loadTops`, the `loadTops` error path logging via `console.error`, `getTopLovedEntries` enriching items via `SearchService.extendSearchResultsWithHubActivityInfo(items, true, ["access"])` per action and returning a map keyed by action, and `navigateToSearch` routing for `"workflow"`, `"dataset"`, and unknown types. - Stub `HubService`, `SearchService`, `WorkflowPersistService`, and `DatasetService`; reuse `StubUserService` for a real `userChanged()` subject; replace `Router.navigate` with a vitest spy. ### Any related issues, documentation, or discussions? Closes: #5225 ### How was this PR tested? - Ran `yarn ng test --watch=false --include='src/app/hub/component/landing-page/landing-page.component.spec.ts'`, 10/10 tests pass. - Ran `yarn format:fix` (1 file formatted, 505 unchanged). ### Was this PR authored or co-authored using generative AI tooling? Co-authored with Claude Opus 4.7 in compliance with ASF --- .../landing-page/landing-page.component.spec.ts | 213 +++++++++++++++++++++ 1 file changed, 213 insertions(+) diff --git a/frontend/src/app/hub/component/landing-page/landing-page.component.spec.ts b/frontend/src/app/hub/component/landing-page/landing-page.component.spec.ts new file mode 100644 index 0000000000..8585ee1c54 --- /dev/null +++ b/frontend/src/app/hub/component/landing-page/landing-page.component.spec.ts @@ -0,0 +1,213 @@ +/** + * 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 { ComponentFixture, TestBed } from "@angular/core/testing"; +import { Router } from "@angular/router"; +import { RouterTestingModule } from "@angular/router/testing"; +import { of, throwError } from "rxjs"; +import { vi } from "vitest"; + +import { LandingPageComponent } from "./landing-page.component"; +import { ActionType, EntityType, HubService } from "../../service/hub.service"; +import { SearchService } from "../../../dashboard/service/user/search.service"; +import { UserService } from "../../../common/service/user/user.service"; +import { StubUserService } from "../../../common/service/user/stub-user.service"; +import { WorkflowPersistService } from "../../../common/service/workflow-persist/workflow-persist.service"; +import { DatasetService } from "../../../dashboard/service/user/dataset/dataset.service"; +import { + DASHBOARD_HOME, + DASHBOARD_HUB_DATASET_RESULT, + DASHBOARD_HUB_WORKFLOW_RESULT, +} from "../../../app-routing.constant"; +import { commonTestProviders } from "../../../common/testing/test-utils"; + +describe("LandingPageComponent", () => { + let component: LandingPageComponent; + let fixture: ComponentFixture<LandingPageComponent>; + let hubServiceStub: { + getCount: ReturnType<typeof vi.fn>; + getTops: ReturnType<typeof vi.fn>; + }; + let searchServiceStub: { + extendSearchResultsWithHubActivityInfo: ReturnType<typeof vi.fn>; + }; + let userService: StubUserService; + let routerNavigateSpy: ReturnType<typeof vi.fn>; + + // Workflow tops are returned for both Like and Clone; dataset tops for Like only. + // Each call to `extendSearchResultsWithHubActivityInfo` is given a tag so the + // tests can assert which action bucket each enriched payload landed in. + const workflowLikeItems = [{ id: "wf-like-item" }] as any; + const workflowCloneItems = [{ id: "wf-clone-item" }] as any; + const datasetLikeItems = [{ id: "ds-like-item" }] as any; + const workflowLikeEnriched = [{ id: "wf-like-enriched" }] as any; + const workflowCloneEnriched = [{ id: "wf-clone-enriched" }] as any; + const datasetLikeEnriched = [{ id: "ds-like-enriched" }] as any; + + function configureModule() { + hubServiceStub = { + getCount: vi.fn((entityType: EntityType) => { + if (entityType === EntityType.Workflow) return of(42); + if (entityType === EntityType.Dataset) return of(7); + return of(0); + }), + getTops: vi.fn((entityType: EntityType, _actions: ActionType[], _uid?: number) => { + if (entityType === EntityType.Workflow) { + return of({ [ActionType.Like]: workflowLikeItems, [ActionType.Clone]: workflowCloneItems }); + } + return of({ [ActionType.Like]: datasetLikeItems }); + }), + }; + + searchServiceStub = { + extendSearchResultsWithHubActivityInfo: vi.fn((items: any[]) => { + if (items === workflowLikeItems) return of(workflowLikeEnriched); + if (items === workflowCloneItems) return of(workflowCloneEnriched); + if (items === datasetLikeItems) return of(datasetLikeEnriched); + return of([]); + }), + }; + + TestBed.configureTestingModule({ + imports: [LandingPageComponent, RouterTestingModule.withRoutes([])], + providers: [ + { provide: HubService, useValue: hubServiceStub }, + { provide: SearchService, useValue: searchServiceStub }, + { provide: UserService, useClass: StubUserService }, + { provide: WorkflowPersistService, useValue: {} }, + { provide: DatasetService, useValue: {} }, + ...commonTestProviders, + ], + }); + + userService = TestBed.inject(UserService) as unknown as StubUserService; + const router = TestBed.inject(Router); + routerNavigateSpy = vi.fn().mockResolvedValue(true); + router.navigate = routerNavigateSpy as any; + } + + function build() { + fixture = TestBed.createComponent(LandingPageComponent); + component = fixture.componentInstance; + } + + beforeEach(() => { + configureModule(); + }); + + it("should create", () => { + build(); + expect(component).toBeTruthy(); + }); + + it("updates isLogin and currentUid when userChanged() emits", () => { + build(); + // Emit a logged-out state. + userService.user = undefined; + userService.userChangeSubject.next(undefined); + expect(component.isLogin).toBe(false); + expect(component.currentUid).toBeUndefined(); + + // Emit a logged-in state. + const newUser = { uid: 99, name: "x", email: "x@x", role: "REGULAR" } as any; + userService.user = newUser; + userService.userChangeSubject.next(newUser); + expect(component.isLogin).toBe(true); + expect(component.currentUid).toBe(99); + }); + + it("ngOnInit invokes getWorkflowCount and loadTops", () => { + build(); + const countSpy = vi.spyOn(component, "getWorkflowCount"); + const loadSpy = vi.spyOn(component, "loadTops").mockResolvedValue(undefined as any); + component.ngOnInit(); + expect(countSpy).toHaveBeenCalledTimes(1); + expect(loadSpy).toHaveBeenCalledTimes(1); + }); + + it("getWorkflowCount populates workflowCount and datasetCount from HubService.getCount", () => { + build(); + component.getWorkflowCount(); + expect(hubServiceStub.getCount).toHaveBeenCalledWith(EntityType.Workflow); + expect(hubServiceStub.getCount).toHaveBeenCalledWith(EntityType.Dataset); + expect(component.workflowCount).toBe(42); + expect(component.datasetCount).toBe(7); + }); + + it("loadTops resolves workflow Like/Clone and dataset Like buckets", async () => { + build(); + await component.loadTops(); + + expect(hubServiceStub.getTops).toHaveBeenCalledWith( + EntityType.Workflow, + [ActionType.Like, ActionType.Clone], + component.currentUid + ); + expect(hubServiceStub.getTops).toHaveBeenCalledWith(EntityType.Dataset, [ActionType.Like], component.currentUid); + + expect(component.topLovedWorkflows).toBe(workflowLikeEnriched); + expect(component.topClonedWorkflows).toBe(workflowCloneEnriched); + expect(component.topLovedDatasets).toBe(datasetLikeEnriched); + }); + + it("loadTops swallows errors and logs them via console.error", async () => { + hubServiceStub.getTops.mockReturnValueOnce(throwError(() => new Error("boom"))); + const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + build(); + await component.loadTops(); + expect(errorSpy).toHaveBeenCalledWith("Failed to load top entries:", expect.any(Error)); + // Arrays remain at their initial empty state. + expect(component.topLovedWorkflows).toEqual([]); + expect(component.topClonedWorkflows).toEqual([]); + expect(component.topLovedDatasets).toEqual([]); + errorSpy.mockRestore(); + }); + + it("getTopLovedEntries extends each action's items with SearchService and returns a map keyed by action", async () => { + build(); + const result = await component.getTopLovedEntries(EntityType.Workflow, [ActionType.Like, ActionType.Clone]); + + expect(searchServiceStub.extendSearchResultsWithHubActivityInfo).toHaveBeenCalledWith(workflowLikeItems, true, [ + "access", + ]); + expect(searchServiceStub.extendSearchResultsWithHubActivityInfo).toHaveBeenCalledWith(workflowCloneItems, true, [ + "access", + ]); + expect(result[ActionType.Like]).toBe(workflowLikeEnriched); + expect(result[ActionType.Clone]).toBe(workflowCloneEnriched); + }); + + it("navigateToSearch routes to the workflow hub result for 'workflow'", () => { + build(); + component.navigateToSearch("workflow"); + expect(routerNavigateSpy).toHaveBeenCalledWith([DASHBOARD_HUB_WORKFLOW_RESULT]); + }); + + it("navigateToSearch routes to the dataset hub result for 'dataset'", () => { + build(); + component.navigateToSearch("dataset"); + expect(routerNavigateSpy).toHaveBeenCalledWith([DASHBOARD_HUB_DATASET_RESULT]); + }); + + it("navigateToSearch routes to the dashboard home for an unknown type", () => { + build(); + component.navigateToSearch("something-else"); + expect(routerNavigateSpy).toHaveBeenCalledWith([DASHBOARD_HOME]); + }); +});
