This is an automated email from the ASF dual-hosted git repository.

github-merge-queue[bot] pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/texera.git

commit d2ef447ff7eb3a77bdcd66952926a5f2ad43c53f
Author: Meng Wang <[email protected]>
AuthorDate: Sun Aug 9 22:28:38 2026 -0700

    test(frontend): render four component templates for coverage (#7501)
    
    ### What changes were proposed in this PR?
    
    Extends four component specs so their templates actually render,
    covering markup
    that the existing tests never executed (they drove the classes
    directly). No
    production code was changed.
    
    **`MarkdownDescriptionComponent`** (+5) — clicking the Edit action
    enters edit
    mode and renders the edit arm; the `[innerHTML]` block renders the
    parsed
    markdown; the `#noDescription` fallback renders when there is nothing to
    show;
    the view-more button toggles both ways (label and chevron are driven by
    the same
    flag); and the control is omitted when the description does not
    overflow.
    
    **`PresetWrapperComponent`** (+5) — the save button renders and saves;
    the
    dropdown's `*ngFor` list is populated per preset with the
    title/description the
    row interpolations render; the empty-list arm; and the `applyPreset` /
    `deletePreset` binding targets.
    
    **`PropertyEditorComponent`** (+2) — the docked collapse item closes the
    panel and
    the collapsed item reopens it, so both arms of the `*ngIf` pair and both
    `(click)`
    bindings execute.
    
    **`AdminExecutionComponent`** (+3) — a seeded execution renders one row
    whose
    cells contain `maxStringLength(...)` and `convertSecondsToTime(...)`
    output; the
    "Not Available" arm renders for a negative execution time; and the kill
    control
    routes the workflow id.
    
    Two behaviours worth noting for future template specs, recorded in
    comments:
    
    - `ngOnInit` refills the list-backed state from the service, so rows
    must be fed
    through the service stub rather than assigned afterwards (the fetch
    would
      overwrite them).
    - `nz-dropdown`'s menu only mounts into a CDK overlay on a real user
    open, which
    jsdom does not drive. Rather than asserting on markup that cannot render
    there
    (which would be brittle), those tests assert the list the `*ngFor` is
    bound to
    and the interpolations/handlers each row uses. `nz-table` also renders
    an
      internal measure row, which the row helper filters out.
    
    Per the issue's determinism constraints: no fake timers are introduced,
    no
    date/time string is asserted, and nothing asserts layout or geometry.
    
    ### Any related issues, documentation, discussions?
    
    Closes #7496
    
    ### How was this PR tested?
    
    Extended unit tests, run locally in `frontend/` (all green; failure
    paths were
    verified by breaking assertions to confirm the suites go red):
    
    ```
    ng test --watch=false --include .../markdown-description.component.spec.ts  
 # 24 passed
    ng test --watch=false --include .../preset-wrapper.component.spec.ts        
 # 29 passed
    ng test --watch=false --include .../property-editor.component.spec.ts       
 # 22 passed
    ng test --watch=false --include .../admin-execution.component.spec.ts       
 # 42 passed
    prettier --write <specs>   # clean
    eslint  <specs>            # clean
    ```
    
    ### Was this PR authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code (Opus 4.8 [1M context])
---
 .../preset-wrapper.component.spec.ts               | 59 ++++++++++++++
 .../execution/admin-execution.component.spec.ts    | 91 ++++++++++++++++++++++
 .../markdown-description.component.spec.ts         | 89 +++++++++++++++++++++
 .../property-editor.component.spec.ts              | 35 +++++++++
 4 files changed, 274 insertions(+)

diff --git 
a/frontend/src/app/common/formly/preset-wrapper/preset-wrapper.component.spec.ts
 
b/frontend/src/app/common/formly/preset-wrapper/preset-wrapper.component.spec.ts
index 474c0ca9a0..c074ce1a90 100644
--- 
a/frontend/src/app/common/formly/preset-wrapper/preset-wrapper.component.spec.ts
+++ 
b/frontend/src/app/common/formly/preset-wrapper/preset-wrapper.component.spec.ts
@@ -348,4 +348,63 @@ describe("PresetWrapperComponent", () => {
       expect(messageStub.error).toHaveBeenCalledTimes(1);
     });
   });
+
+  // ─── template rendering 
────────────────────────────────────────────────────
+  describe("template rendering", () => {
+    const initWith = (presets: Preset[]): void => {
+      // ngOnInit re-populates searchResults from the service, so feed the 
presets
+      // through the stub rather than assigning after init (which it would 
overwrite).
+      presetServiceStub.getPresets.mockReturnValue(of(presets));
+      component.field = buildField();
+      component.ngOnInit();
+      fixture.detectChanges();
+    };
+
+    it("renders the save button and saves the preset when it is clicked", () 
=> {
+      initWith([]);
+
+      const saveBtn = fixture.nativeElement.querySelector(".save-button") as 
HTMLButtonElement;
+      expect(saveBtn).toBeTruthy();
+
+      const savePreset = vi.spyOn(component, 
"savePreset").mockImplementation(() => {});
+      saveBtn.click();
+
+      expect(savePreset).toHaveBeenCalled();
+    });
+
+    it("feeds the dropdown *ngFor with one entry per preset, titled and 
described", () => {
+      // The rows live in an nz-dropdown-menu that only mounts into a CDK 
overlay on a
+      // real user open, which jsdom does not drive; assert the list the 
*ngFor is bound
+      // to and the interpolations it renders for each row instead.
+      initWith([testPreset, otherPreset]);
+
+      expect(component.searchResults).toEqual([testPreset, otherPreset]);
+      // the title cell renders the preset's value under the field's own key, 
and the
+      // description cell joins the remaining values
+      expect(component.getEntryTitle(testPreset)).toBe(testPreset[fieldKey]);
+      
expect(component.getEntryDescription(testPreset)).toBe("otherPresetValue");
+    });
+
+    it("binds an empty dropdown list when there are no presets", () => {
+      initWith([]);
+
+      expect(component.searchResults).toEqual([]);
+    });
+
+    it("applies the preset the row's (click) binding targets", () => {
+      initWith([testPreset]);
+
+      component.applyPreset(testPreset);
+
+      
expect(presetServiceStub.applyPreset).toHaveBeenCalledWith(expect.anything(), 
expect.anything(), testPreset);
+    });
+
+    it("deletes the preset the delete button's (click) binding targets", () => 
{
+      initWith([testPreset]);
+
+      component.deletePreset(testPreset);
+
+      expect(presetServiceStub.deletePreset).toHaveBeenCalled();
+    });
+  });
 });
diff --git 
a/frontend/src/app/dashboard/component/admin/execution/admin-execution.component.spec.ts
 
b/frontend/src/app/dashboard/component/admin/execution/admin-execution.component.spec.ts
index 734123b441..a46cedef38 100644
--- 
a/frontend/src/app/dashboard/component/admin/execution/admin-execution.component.spec.ts
+++ 
b/frontend/src/app/dashboard/component/admin/execution/admin-execution.component.spec.ts
@@ -537,3 +537,94 @@ describe("AdminExecutionComponent methods (#6550)", () => {
     });
   });
 });
+
+describe("AdminExecutionComponent template rendering", () => {
+  let component: AdminExecutionComponent;
+  let fixture: ComponentFixture<AdminExecutionComponent>;
+  let service: AdminExecutionService;
+
+  const makeExecution = (overrides: Partial<Execution> = {}): Execution => ({
+    workflowName: "a-very-long-workflow-name-to-truncate",
+    workflowId: 11,
+    userName: "alice",
+    userId: 1,
+    executionId: 21,
+    executionStatus: "COMPLETED",
+    executionTime: 3661_000,
+    executionName: "run-1",
+    startTime: 1_700_000_000_000,
+    endTime: 1_700_000_100_000,
+    access: true,
+    ...overrides,
+  });
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      providers: [AdminExecutionService, ...commonTestProviders],
+      imports: [AdminExecutionComponent, HttpClientTestingModule, 
NzDropDownModule, NzModalModule],
+    }).compileComponents();
+
+    fixture = TestBed.createComponent(AdminExecutionComponent);
+    component = fixture.componentInstance;
+    service = TestBed.inject(AdminExecutionService);
+    // Keep the fetches inert; the rows are seeded directly so ngOnInit's 
pollers have
+    // nothing to do (they are covered by the lifecycle tests above).
+    vi.spyOn(service, "getExecutionList").mockReturnValue(of([]));
+    vi.spyOn(service, "getTotalWorkflows").mockReturnValue(of(0));
+  });
+
+  afterEach(() => fixture.destroy());
+
+  const renderRows = (executions: Execution[]): void => {
+    // ngOnInit refills listOfExecutions from the service, so supply the rows 
through the
+    // stub rather than assigning them (which the fetch would overwrite).
+    vi.mocked(service.getExecutionList).mockReturnValue(of(executions));
+    fixture.detectChanges();
+    fixture.detectChanges();
+  };
+
+  // nz-table renders an internal measure row with empty cells; keep only real 
data rows.
+  const dataRows = () =>
+    fixture.debugElement
+      .queryAll(By.css("tbody tr"))
+      .filter(row => !row.nativeElement.hasAttribute("nz-table-measure-row"));
+
+  const cellsOf = (rowIndex: number): string[] =>
+    dataRows()
+      [rowIndex].queryAll(By.css("td"))
+      .map(td => (td.nativeElement.textContent ?? "").trim());
+
+  it("renders one row per execution with the truncated name and formatted 
duration", () => {
+    const execution = makeExecution();
+    renderRows([execution]);
+
+    expect(dataRows()).toHaveLength(1);
+    const cells = cellsOf(0);
+    // the interpolations run the component's own helpers, so assert against 
them
+    
expect(cells[0]).toContain(component.maxStringLength(execution.workflowName, 
16));
+    expect(cells[0]).toContain(String(execution.workflowId));
+    expect(cells.join(" 
")).toContain(component.convertSecondsToTime(execution.executionTime));
+  });
+
+  it("renders the Not Available arm when the execution time is negative", () 
=> {
+    renderRows([makeExecution({ executionTime: -1, endTime: 0 })]);
+
+    expect(cellsOf(0).join(" ")).toContain("Not Available");
+  });
+
+  it("kills the execution with its workflow id when the kill control is 
clicked", () => {
+    const killSpy = vi.spyOn(component, "killExecution").mockImplementation(() 
=> {});
+    renderRows([makeExecution({ executionStatus: "RUNNING", workflowId: 42 
})]);
+
+    // nz-icon renders [nzType] as an `anticon-<type>` class; the kill button 
is the one
+    // carrying the "stop" icon (nz-tooltip is a directive input and never 
reaches the DOM).
+    const killButton = fixture.debugElement
+      .queryAll(By.css("tbody tr button"))
+      .find(btn => btn.nativeElement.querySelector("i.anticon-stop"));
+    expect(killButton).toBeTruthy();
+
+    killButton!.triggerEventHandler("click", null);
+
+    expect(killSpy).toHaveBeenCalledWith(42);
+  });
+});
diff --git 
a/frontend/src/app/dashboard/component/user/markdown-description/markdown-description.component.spec.ts
 
b/frontend/src/app/dashboard/component/user/markdown-description/markdown-description.component.spec.ts
index 7d8b6858c1..bec5010beb 100644
--- 
a/frontend/src/app/dashboard/component/user/markdown-description/markdown-description.component.spec.ts
+++ 
b/frontend/src/app/dashboard/component/user/markdown-description/markdown-description.component.spec.ts
@@ -309,4 +309,93 @@ describe("MarkdownDescriptionComponent", () => {
     expect(parse).not.toHaveBeenCalled();
     expect(component.renderedDescription).toBe("");
   });
+
+  // ─── template rendering 
────────────────────────────────────────────────────
+  // Drive the markup through the DOM so the (click) attributes and 
interpolations
+  // in the template actually execute.
+  describe("template rendering", () => {
+    it("enters edit mode when the Edit button is clicked", async () => {
+      const fixture = await createFixture();
+      const component = fixture.componentInstance;
+      component.editable = true;
+      fixture.detectChanges();
+
+      const editBtn = fixture.nativeElement.querySelector(".md-actions 
button") as HTMLButtonElement;
+      expect(editBtn).toBeTruthy();
+      editBtn.click();
+      fixture.detectChanges();
+
+      expect(component.currentMode).toBe("edit");
+      // the edit-mode arm of the template now renders
+      expect(fixture.nativeElement.querySelector(".md-split")).toBeTruthy();
+    });
+
+    it("renders the parsed markdown through the innerHTML binding", async () 
=> {
+      const fixture = await createFixture();
+      const component = fixture.componentInstance;
+      component.description = "hello";
+      fixture.detectChanges();
+      await fixture.whenStable();
+      fixture.detectChanges();
+
+      const rendered = fixture.nativeElement.querySelector(".md-rendered") as 
HTMLElement;
+      expect(rendered).toBeTruthy();
+      expect(rendered.innerHTML).toContain("hello");
+    });
+
+    it("falls back to the no-description template when nothing is rendered", 
async () => {
+      const fixture = await createFixture();
+      fixture.componentInstance.description = "";
+      fixture.detectChanges();
+      await fixture.whenStable();
+      fixture.detectChanges();
+
+      expect(fixture.nativeElement.querySelector(".md-rendered")).toBeNull();
+      expect(fixture.nativeElement.textContent).toContain("No description 
provided.");
+    });
+
+    it("toggles view-more from the template and flips the chevron binding", 
async () => {
+      const fixture = await createFixture();
+      const component = fixture.componentInstance;
+      // the button is behind enableViewMore && hasOverflow
+      component.enableViewMore = true;
+      component.hasOverflow = true;
+      fixture.detectChanges();
+
+      const viewMoreBtn = 
fixture.nativeElement.querySelector(".view-more-btn") as HTMLButtonElement;
+      expect(viewMoreBtn).toBeTruthy();
+      // nz-icon renders [nzType] as an `anticon-<type>` class, so the chevron 
binding is
+      // observable alongside the label interpolation
+      const chevronType = (): string | undefined =>
+        Array.from(viewMoreBtn.querySelector("i")?.classList ?? [])
+          .find(cls => cls.startsWith("anticon-"))
+          ?.replace("anticon-", "");
+
+      expect(viewMoreBtn.textContent).toContain("View more");
+      expect(chevronType()).toBe("down");
+
+      viewMoreBtn.click();
+      fixture.detectChanges();
+
+      expect(component.isExpanded).toBe(true);
+      expect(viewMoreBtn.textContent).toContain("View less");
+      expect(chevronType()).toBe("up");
+
+      viewMoreBtn.click();
+      fixture.detectChanges();
+
+      expect(component.isExpanded).toBe(false);
+      expect(viewMoreBtn.textContent).toContain("View more");
+      expect(chevronType()).toBe("down");
+    });
+
+    it("omits the view-more control when the description does not overflow", 
async () => {
+      const fixture = await createFixture();
+      fixture.componentInstance.enableViewMore = true;
+      fixture.componentInstance.hasOverflow = false;
+      fixture.detectChanges();
+
+      expect(fixture.nativeElement.querySelector(".view-more-btn")).toBeNull();
+    });
+  });
 });
diff --git 
a/frontend/src/app/workspace/component/property-editor/property-editor.component.spec.ts
 
b/frontend/src/app/workspace/component/property-editor/property-editor.component.spec.ts
index ccc1192033..e0713c37b1 100644
--- 
a/frontend/src/app/workspace/component/property-editor/property-editor.component.spec.ts
+++ 
b/frontend/src/app/workspace/component/property-editor/property-editor.component.spec.ts
@@ -388,4 +388,39 @@ describe("PropertyEditorComponent", () => {
       freshFixture.destroy();
     }
   });
+
+  // ─── template rendering 
────────────────────────────────────────────────────
+  // The docked buttons swap between a collapse and an expand item depending 
on the
+  // panel width; drive both through the DOM so each (click) binding executes.
+  describe("template rendering", () => {
+    const dockedItems = (): HTMLElement[] =>
+      Array.from(fixture.nativeElement.querySelectorAll("#docked-buttons li")) 
as HTMLElement[];
+
+    it("collapses the panel when the docked collapse item is clicked", () => {
+      component.width = 280;
+      fixture.detectChanges();
+
+      const items = dockedItems();
+      expect(items).toHaveLength(1); // only the collapse arm renders while 
open
+      items[0].click();
+      fixture.detectChanges();
+
+      expect(component.width).toBe(0);
+    });
+
+    it("reopens the panel when the collapsed docked item is clicked", () => {
+      // openPanel() schedules a setTimeout that would call detectChanges() 
after the
+      // fixture is destroyed in afterEach; stub it the way the specs above do.
+      vi.spyOn(component as any, 
"updateHeightBasedOnContent").mockImplementation(() => {});
+      component.width = 0;
+      fixture.detectChanges();
+
+      const items = dockedItems();
+      expect(items).toHaveLength(1); // only the expand arm renders while 
collapsed
+      items[0].click();
+      fixture.detectChanges();
+
+      expect(component.width).toBe(280);
+    });
+  });
 });

Reply via email to