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 <[email protected]> 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); + }); + }); });
