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);
+ });
+ });
});