You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@texera.apache.org by gi...@apache.org on 2026/08/09 23:09:37 UTC

(texera) 01/01: test(frontend): extend TimeTravelComponent template coverage (#7471)

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

commit f00b4ca9a8fa52bd7892b3f50619e2bfffb80936
Author: Meng Wang <me...@uci.edu>
AuthorDate: Sun Aug 9 16:09:15 2026 -0700

    test(frontend): extend TimeTravelComponent template coverage (#7471)
    
    ### What changes were proposed in this PR?
    
    Extends `time-travel.component.spec.ts` to render the execution table.
    The class
    was already ~92% covered but the template sat at ~42% — the existing
    tests call
    the class methods directly and never render a row. Adds 6 DOM-driven
    tests:
    
    - **Rows** — one `<tr>` per execution with its eid cell and a
    starting-time cell;
      no execution rows when the list is empty.
    - **Expand / collapse** — clicking a row expands the `*ngIf` detail row
    and a
      second click collapses it.
    - **Interaction list** — the expanded row renders one button per entry
    of
      `interactionHistories[eId]` with its label.
    - **Click wiring** — an interaction button passes the row's `vId`/`eId`
    and the
      interaction to `onInteractionClick`.
    - **Disabled binding** — only the interaction already reverted to is
    disabled.
    
    This takes the template from 8/19 to **19/19 instrumented lines
    (100%)**.
    
    Per the issue's determinism note the starting-time cell is rendered but
    its
    formatted text is not asserted (it is timezone-dependent); the test only
    checks
    the pipe produced output. Execution rows are counted by their two data
    cells
    rather than a raw `tbody tr` count, so the nz-table empty placeholder
    and the
    colspan detail row don't skew the count. No production code was changed.
    
    ### Any related issues, documentation, discussions?
    
    Closes #7468.
    
    ### How was this PR tested?
    
    `ng test --watch=false --include
    src/app/workspace/component/left-panel/time-travel/time-travel.component.spec.ts`
    — 18 passed (12 existing + 6 new). Template coverage confirmed at 19/19
    lines via the lcov report. `eslint` and `prettier --check` clean.
    Failure path verified by breaking a new assertion (→ non-zero exit) and
    restoring.
    
    ### Was this PR authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code (Opus 5)
---
 .../time-travel/time-travel.component.spec.ts      | 106 +++++++++++++++++++++
 1 file changed, 106 insertions(+)

diff --git a/frontend/src/app/workspace/component/left-panel/time-travel/time-travel.component.spec.ts b/frontend/src/app/workspace/component/left-panel/time-travel/time-travel.component.spec.ts
index 3d93392119..90f989b482 100644
--- a/frontend/src/app/workspace/component/left-panel/time-travel/time-travel.component.spec.ts
+++ b/frontend/src/app/workspace/component/left-panel/time-travel/time-travel.component.spec.ts
@@ -20,6 +20,7 @@
 import { ComponentFixture, TestBed } from "@angular/core/testing";
 import { WorkflowActionService } from "../../../service/workflow-graph/model/workflow-action.service";
 import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
+import { By } from "@angular/platform-browser";
 import { FormsModule, ReactiveFormsModule } from "@angular/forms";
 import { FormlyModule } from "@ngx-formly/core";
 import { TEXERA_FORMLY_CONFIG } from "../../../../common/formly/formly-config";
@@ -42,6 +43,12 @@ function makeExecution(eId: number, logLocation: string | undefined): WorkflowEx
   return { eId, logLocation } as unknown as WorkflowExecutionsEntry;
 }
 
+// A row the template can render: it also needs a vId (passed to onInteractionClick)
+// and a startingTime (fed to the date pipe).
+function makeRow(eId: number, vId: number): WorkflowExecutionsEntry {
+  return { eId, vId, logLocation: "s3://log", startingTime: 1700000000000 } as unknown as WorkflowExecutionsEntry;
+}
+
 describe("TimeTravelComponent", () => {
   let component: TimeTravelComponent;
   let fixture: ComponentFixture<TimeTravelComponent>;
@@ -223,4 +230,103 @@ describe("TimeTravelComponent", () => {
       });
     });
   });
+
+  describe("template rendering", () => {
+    // Query, assert the element is present, then dispatch — a missing selector fails
+    // with a clear message instead of a null dereference.
+    const fire = (css: string, index: number, event: string, payload: unknown): void => {
+      const els = fixture.debugElement.queryAll(By.css(css));
+      expect(els.length).toBeGreaterThan(index);
+      els[index].triggerEventHandler(event, payload);
+    };
+
+    // Rows are only rendered by the *ngFor; the ngOnInit poller stays inert because
+    // the outer beforeEach stubs the workflow metadata to no wid.
+    const seedRows = (...rows: WorkflowExecutionsEntry[]): void => {
+      component.executionList = rows;
+      fixture.detectChanges();
+    };
+
+    // An execution row is the one with both data cells (eid + starting time). The
+    // nz-table empty placeholder and the expanded detail row each use a single
+    // colspan cell, so this counts execution rows without depending on nz classes.
+    const executionRows = () =>
+      fixture.debugElement.queryAll(By.css("tbody tr")).filter(r => r.queryAll(By.css("td")).length === 2);
+
+    it("renders one row per execution with its eid and a starting-time cell", () => {
+      seedRows(makeRow(1, 10), makeRow(2, 20));
+
+      const rows = executionRows();
+      expect(rows.length).toBe(2);
+
+      const firstCells = rows[0].queryAll(By.css("td"));
+      expect(firstCells.length).toBe(2);
+      expect(firstCells[0].nativeElement.textContent.trim()).toBe("1");
+      // The second cell runs `startingTime | date:'short'`; its exact text is
+      // timezone-dependent, so only assert that the pipe rendered something.
+      expect(firstCells[1].nativeElement.textContent.trim().length).toBeGreaterThan(0);
+    });
+
+    it("renders no body rows when the execution list is empty", () => {
+      seedRows();
+      expect(executionRows().length).toBe(0);
+    });
+
+    it("expands the detail row on a row click and collapses it on a second click", () => {
+      seedRows(makeRow(1, 10));
+
+      fire("tbody tr", 0, "click", new MouseEvent("click"));
+      fixture.detectChanges();
+      expect(component.expandedRows.has(1)).toBe(true);
+      // the detail row is a second <tr> rendered under the same *ngFor
+      expect(fixture.debugElement.query(By.css(".interaction-container"))).toBeTruthy();
+
+      fire("tbody tr", 0, "click", new MouseEvent("click"));
+      fixture.detectChanges();
+      expect(component.expandedRows.has(1)).toBe(false);
+      expect(executionRows().length).toBe(1);
+      expect(fixture.debugElement.query(By.css(".interaction-container"))).toBeNull();
+    });
+
+    it("renders one button per interaction in the expanded row", () => {
+      component.interactionHistories = { 1: ["click-a", "click-b"] };
+      seedRows(makeRow(1, 10));
+
+      fire("tbody tr", 0, "click", new MouseEvent("click"));
+      fixture.detectChanges();
+
+      const buttons = fixture.debugElement.queryAll(By.css(".interaction-item"));
+      expect(buttons.map(b => b.nativeElement.textContent.trim())).toEqual(["click-a", "click-b"]);
+    });
+
+    it("passes the row's vid/eid and the interaction to the click handler", () => {
+      component.interactionHistories = { 1: ["click-a", "click-b"] };
+      seedRows(makeRow(1, 10));
+      fire("tbody tr", 0, "click", new MouseEvent("click"));
+      fixture.detectChanges();
+
+      // Spying the handler is what isolates the template binding itself — the
+      // handler's own behavior is covered by the onInteractionClick tests above.
+      const clickSpy = vi.spyOn(component, "onInteractionClick").mockImplementation(() => {});
+      fire(".interaction-item", 1, "click", new MouseEvent("click"));
+
+      expect(clickSpy).toHaveBeenCalledWith(10, 1, "click-b");
+    });
+
+    it("disables only the interaction the panel has already reverted to", () => {
+      component.interactionHistories = { 1: ["click-a", "click-b"] };
+      component.revertedToInteraction = { eid: 1, interaction: "click-b" };
+      // revertedToInteraction makes ngOnDestroy (run by fixture.destroy in afterEach)
+      // take the replay-cleanup path; keep those service calls inert.
+      vi.spyOn(TestBed.inject(WorkflowVersionService), "closeReadonlyWorkflowDisplay").mockImplementation(() => {});
+      vi.spyOn(TestBed.inject(ExecuteWorkflowService), "killWorkflow").mockImplementation(() => {});
+      seedRows(makeRow(1, 10));
+      fire("tbody tr", 0, "click", new MouseEvent("click"));
+      fixture.detectChanges();
+
+      const buttons = fixture.debugElement.queryAll(By.css(".interaction-item"));
+      expect((buttons[0].nativeElement as HTMLButtonElement).disabled).toBe(false);
+      expect((buttons[1].nativeElement as HTMLButtonElement).disabled).toBe(true);
+    });
+  });
 });