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

commit fe89db4315c4b23c04ae06160030566ce8410281
Author: Xinyuan Lin <[email protected]>
AuthorDate: Sat Aug 8 00:16:28 2026 -0700

    test(frontend): render the hub browse section's entity cards (#7408)
    
    ### What changes were proposed in this PR?
    
    The browse section's specs cover the route map and the cover-URL cache,
    but no card had ever been rendered, so every per-entity binding and
    fallback in the template was unpinned. The template was at roughly
    **8%** of statements locally.
    
    Adds 8 tests over what the template decides on its own:
    
    - the section disappears entirely when it holds no entities
    - the heading, and one card per entity, with name and description
    - `{{ entity.description || 'No description available' }}` — an entity
    published without a description would otherwise render an empty
    paragraph and collapse the card
    - `[src]="getCoverImage(entity)"` and the inline `(error)` handler that
    swaps in `defaultBackground`; a cached cover URL can still 404, and that
    handler is the only thing standing between the user and a broken image
    - the avatar labelled with the entity id, and the owner name defaulting
    to empty
    
    **Verified by mutation**, all reverted (template diff empty):
    
    | Mutation | Result |
    |---|---|
    | render the section even when empty | red |
    | drop the description fallback | red |
    | drop the owner-name fallback | red |
    | ignore the cover cache and always use the default | red |
    | remove the image error handler | red |
    | label the avatar with the name instead of the id | red |
    | show the description as the card title | red |
    | render only the first entity | red |
    
    Local coverage for the component directory: **~8% → 94.73%** of
    statements.
    
    The real `UserService` is replaced with the shared `StubUserService`:
    the embedded `texera-user-avatar` injects it, and the real one drags in
    `AuthService` and from there `JwtHelperService` and `NzModalService`.
    The stub cuts that chain in one step.
    
    No production file is touched.
    
    ### Any related issues, documentation, discussions?
    
    Closes #7405
    
    ### How was this PR tested?
    
    ```
    npx ng test --watch=false --include="**/browse-section.component.spec.ts"
    ```
    
    ```
     Test Files  1 passed (1)
          Tests  17 passed (17)
    ```
    
    8 new on top of the existing 9. `yarn format:ci` passes.
    
    ### Was this PR authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code (Opus 5)
---
 .../browse-section.component.spec.ts               | 106 ++++++++++++++++++++-
 1 file changed, 104 insertions(+), 2 deletions(-)

diff --git 
a/frontend/src/app/hub/component/browse-section/browse-section.component.spec.ts
 
b/frontend/src/app/hub/component/browse-section/browse-section.component.spec.ts
index 8207eeab5f..0be21b86fe 100644
--- 
a/frontend/src/app/hub/component/browse-section/browse-section.component.spec.ts
+++ 
b/frontend/src/app/hub/component/browse-section/browse-section.component.spec.ts
@@ -18,10 +18,13 @@
  */
 
 import { ComponentFixture, TestBed } from "@angular/core/testing";
+import { RouterTestingModule } from "@angular/router/testing";
+import { By } from "@angular/platform-browser";
+import { UserService } from "src/app/common/service/user/user.service";
+import { StubUserService } from 
"src/app/common/service/user/stub-user.service";
 import { BrowseSectionComponent } from "./browse-section.component";
 import { WorkflowPersistService } from 
"../../../common/service/workflow-persist/workflow-persist.service";
 import { DatasetService } from 
"../../../dashboard/service/user/dataset/dataset.service";
-import { ChangeDetectorRef } from "@angular/core";
 import { commonTestProviders } from "../../../common/testing/test-utils";
 import { DashboardEntry } from "../../../dashboard/type/dashboard-entry";
 import { AppSettings } from "../../../common/app-setting";
@@ -42,7 +45,6 @@ describe("BrowseSectionComponent", () => {
       providers: [
         { provide: WorkflowPersistService, useValue: {} },
         { provide: DatasetService, useValue: {} },
-        { provide: ChangeDetectorRef, useValue: {} },
         ...commonTestProviders,
       ],
     });
@@ -122,3 +124,103 @@ describe("BrowseSectionComponent", () => {
     });
   });
 });
+/**
+ * The cards themselves are template-only: the specs above assert the route 
map and the cover-URL
+ * cache, but nothing had ever rendered a card, so the per-entity bindings and 
their fallbacks were
+ * unpinned. RouterTestingModule supplies the Router that the cards' 
routerLink needs.
+ */
+describe("BrowseSectionComponent rendering", () => {
+  let fixture: ComponentFixture<BrowseSectionComponent>;
+
+  beforeEach(() => {
+    TestBed.resetTestingModule();
+    TestBed.configureTestingModule({
+      imports: [BrowseSectionComponent, RouterTestingModule.withRoutes([])],
+      providers: [
+        // The cards embed texera-user-avatar, which injects UserService; the 
real one drags in
+        // AuthService and its whole dependency chain, so the shared stub 
stands in for it.
+        { provide: UserService, useClass: StubUserService },
+        { provide: WorkflowPersistService, useValue: {} },
+        { provide: DatasetService, useValue: {} },
+        ...commonTestProviders,
+      ],
+    });
+    fixture = TestBed.createComponent(BrowseSectionComponent);
+  });
+
+  /** Renders the section with the given entities. */
+  function render(entities: DashboardEntry[], title = "Workflows"): 
HTMLElement {
+    // Set the inputs and let the first change-detection cycle drive ngOnInit, 
as Angular does at
+    // runtime. Calling ngOnInit() by hand as well would run it twice and 
rebuild the cover-image
+    // cache on top of itself, hiding any non-idempotent init.
+    fixture.componentRef.setInput("entities", entities);
+    fixture.componentRef.setInput("sectionTitle", title);
+    fixture.detectChanges();
+    return fixture.nativeElement as HTMLElement;
+  }
+
+  const entity = (over: Partial<Record<string, unknown>> = {}) =>
+    ({ id: 1, type: "dataset", accessibleUserIds: [], name: "flow", ...over }) 
as unknown as DashboardEntry;
+
+  it("renders nothing at all for an empty section", () => {
+    const el = render([]);
+
+    expect(el.querySelector(".results-container")).toBeNull();
+  });
+
+  it("renders the section heading and one card per entity", () => {
+    const el = render([entity({ id: 1 }), entity({ id: 2 })], "Public 
Datasets");
+
+    
expect(el.querySelector(".results-title")?.textContent?.trim()).toBe("Public 
Datasets");
+    expect(el.querySelectorAll("nz-card")).toHaveLength(2);
+  });
+
+  it("shows each entity's name and description", () => {
+    const el = render([entity({ name: "sales", description: "quarterly 
numbers" })]);
+
+    expect(el.querySelector(".card-title")?.textContent?.trim()).toBe("sales");
+    
expect(el.querySelector(".card-description")?.textContent?.trim()).toBe("quarterly
 numbers");
+  });
+
+  it("substitutes a placeholder for a missing description", () => {
+    // Datasets published without a description would otherwise render an 
empty paragraph and
+    // collapse the card's layout.
+    const el = render([entity({ description: undefined })]);
+
+    
expect(el.querySelector(".card-description")?.textContent?.trim()).toBe("No 
description available");
+  });
+
+  it("uses the cached cover image when the entity has one", () => {
+    const el = render([entity({ id: 5, coverImageUrl: "has-cover" })]);
+
+    const img = el.querySelector<HTMLImageElement>(".card-cover-image")!;
+    
expect(img.getAttribute("src")).toBe(`${AppSettings.getApiEndpoint()}/dataset/5/cover`);
+  });
+
+  it("falls back to the default background when the cover image fails to 
load", () => {
+    // A cached cover URL can still 404; the inline error handler is the only 
thing that stops the
+    // card from showing a broken image.
+    const el = render([entity({ id: 5, coverImageUrl: "has-cover" })]);
+    const img = el.querySelector<HTMLImageElement>(".card-cover-image")!;
+
+    img.dispatchEvent(new Event("error"));
+
+    expect(img.src).toContain("card_background.jpg");
+  });
+
+  it("labels the avatar with the entity id", () => {
+    const el = render([entity({ id: 42 })]);
+
+    expect(el.querySelector("nz-avatar")?.textContent?.trim()).toBe("42");
+  });
+
+  it("passes the owner through to the avatar, defaulting to an empty name", () 
=> {
+    const withOwner = 
fixture.debugElement.queryAll(By.css("texera-user-avatar"));
+    expect(withOwner).toHaveLength(0);
+
+    render([entity({ ownerName: "ada" }), entity({ id: 2, ownerName: undefined 
})]);
+
+    const avatars = 
fixture.debugElement.queryAll(By.css("texera-user-avatar"));
+    expect(avatars.map(a => a.componentInstance.userName)).toEqual(["ada", 
""]);
+  });
+});

Reply via email to