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/07/20 00:00:56 UTC

(texera) 01/01: test(frontend): extend AdminExecutionComponent coverage for helpers, data, table and actions (#6554)

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

commit dfb9400176f6b4263b95daf1ed965e2e37253d78
Author: Meng Wang <me...@uci.edu>
AuthorDate: Sun Jul 19 17:00:33 2026 -0700

    test(frontend): extend AdminExecutionComponent coverage for helpers, data, table and actions (#6554)
    
    ### What changes were proposed in this PR?
    
    Extends the existing `AdminExecutionComponent` spec (previously ~43%
    coverage) to
    cover the untested helpers, data flow, table handlers and execution
    actions.
    
    20 added tests, in a new describe block that mocks
    `AdminExecutionService` and
    does **not** call `detectChanges()` (so `ngOnInit`'s pollers never
    start):
    
    - **Pure helpers** — `padZero`, `convertSecondsToTime`,
    `maxStringLength`, `getStatusColor` (incl. the default),
    `convertTimeToTimestamp`, and `calculateTime` (both the fixed-duration
    and the live-elapsed branches).
    - **Time status** — `specifyCompletedStatus` (COMPLETED→JUST COMPLETED
    within 5s and the reverse after 5s), `updateTimeDifferences`,
    `updateTimeStatus` (delegation), and `dataCheck`.
    - **Data / table** — `fetchData` (populates list/total/loading),
    `onFilterChange`, and `onSortChange` (set + reset-to-`NO_SORT`).
    - **Actions** — `killExecution` / `pauseExecution` / `resumeExecution`
    (open the socket for the workflow and send the right request, then
    refresh) and `clickToViewHistory` (opens the history modal).
    
    **Determinism (no flaky tests):** the `Date.now()`-based methods run
    under
    `vi.useFakeTimers()` + a fixed `setSystemTime`; `convertTimeToTimestamp`
    is
    asserted against the same `toLocaleString` call so it is
    timezone-independent;
    the websocket-based actions spy `WorkflowWebsocketService.prototype`
    `openWebsocket`/`send` (so no real socket opens and the constructor's
    heartbeat
    interval never fires under fake timers); `fixture.destroy()` runs in
    `afterEach`.
    Verified locally: 30/30 pass, **10 consecutive runs with zero flakes**,
    and the
    failure path checked.
    
    No production code was changed.
    
    ### Any related issues, documentation, discussions?
    
    Closes #6550
    
    ### How was this PR tested?
    
    Extended unit tests, run locally in `frontend/`:
    
    ```
    ng test --watch=false --include src/app/dashboard/component/admin/execution/admin-execution.component.spec.ts
    # Test Files 1 passed (1) | Tests 30 passed (30)   (10x consecutive, 0 flakes)
    prettier --write <spec>   # unchanged
    eslint  <spec>            # clean
    ```
    
    ### Was this PR authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code (Opus 4.8 [1M context])
---
 .../execution/admin-execution.component.spec.ts    | 231 +++++++++++++++++++++
 1 file changed, 231 insertions(+)

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 653b47ee53..3b8a7e52bd 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
@@ -27,6 +27,9 @@ import { NzModalModule } from "ng-zorro-antd/modal";
 import { commonTestProviders } from "../../../../common/testing/test-utils";
 import { Execution } from "../../../../common/type/execution";
 import { NzTableQueryParams } from "ng-zorro-antd/table";
+import { NzModalService } from "ng-zorro-antd/modal";
+import { WorkflowWebsocketService } from "../../../../workspace/service/workflow-websocket/workflow-websocket.service";
+import { NO_SORT } from "./admin-execution.component";
 import { of } from "rxjs";
 
 describe("AdminDashboardComponent", () => {
@@ -186,3 +189,231 @@ describe("AdminExecutionComponent pagination (#3586)", () => {
     expect(service.getExecutionList).toHaveBeenCalledTimes(1);
   });
 });
+
+describe("AdminExecutionComponent methods (#6550)", () => {
+  let component: AdminExecutionComponent;
+  let fixture: ComponentFixture<AdminExecutionComponent>;
+  let service: AdminExecutionService;
+
+  const NOW = 1_700_000_000_000; // fixed clock (ms) for Date.now()-based logic
+
+  function makeExecution(over: Partial<Execution> = {}): Execution {
+    return {
+      access: true,
+      workflowId: 1,
+      workflowName: "wf",
+      executionId: 1,
+      executionName: "exec",
+      userName: "alice",
+      executionStatus: "COMPLETED",
+      startTime: 0,
+      endTime: 0,
+      executionTime: 0,
+      ...over,
+    } as unknown as Execution;
+  }
+
+  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 data fetches inert and synchronous. We deliberately do NOT call
+    // detectChanges(), so ngOnInit's pollers never start.
+    vi.spyOn(service, "getExecutionList").mockReturnValue(of([]));
+    vi.spyOn(service, "getTotalWorkflows").mockReturnValue(of(0));
+    // Fixed clock makes the Date.now()-based methods deterministic and also keeps the
+    // websocket heartbeat interval (created in the action handlers) from ever firing.
+    vi.useFakeTimers();
+    vi.setSystemTime(NOW);
+  });
+
+  afterEach(() => {
+    vi.useRealTimers();
+    vi.restoreAllMocks();
+    fixture.destroy();
+  });
+
+  describe("pure helpers", () => {
+    it("padZero pads single digits to two characters", () => {
+      expect(component.padZero(5)).toBe("05");
+      expect(component.padZero(12)).toBe("12");
+    });
+
+    it("convertSecondsToTime formats seconds as HH:MM:SS", () => {
+      expect(component.convertSecondsToTime(0)).toBe("00:00:00");
+      expect(component.convertSecondsToTime(45)).toBe("00:00:45");
+      expect(component.convertSecondsToTime(3661)).toBe("01:01:01");
+    });
+
+    it("maxStringLength truncates only when longer than the limit", () => {
+      expect(component.maxStringLength("hello world", 5)).toBe("hello . . . ");
+      expect(component.maxStringLength("hi", 5)).toBe("hi");
+    });
+
+    it("getStatusColor maps statuses to colors and falls back to black", () => {
+      expect(component.getStatusColor("RUNNING")).toBe("orange");
+      expect(component.getStatusColor("COMPLETED")).toBe("green");
+      expect(component.getStatusColor("KILLED")).toBe("red");
+      expect(component.getStatusColor("SOMETHING_ELSE")).toBe("black");
+    });
+
+    it("convertTimeToTimestamp renders the timestamp via toLocaleString", () => {
+      // Assert against the same locale call so the test is timezone-independent.
+      const expected = new Date(NOW).toLocaleString("en-US", { timeZoneName: "short" });
+      expect(component.convertTimeToTimestamp("COMPLETED", NOW)).toBe(expected);
+    });
+
+    it("calculateTime uses the fixed final duration for a completed execution", () => {
+      expect(component.calculateTime(10000, 4000, "COMPLETED", "w")).toBe(6);
+    });
+
+    it("calculateTime uses the live elapsed time for a running execution", () => {
+      // now = NOW/1000 seconds; start = NOW - 5000 ms -> elapsed 5 s.
+      expect(component.calculateTime(0, NOW - 5000, "RUNNING", "w")).toBe(5);
+    });
+  });
+
+  describe("time status", () => {
+    it("specifyCompletedStatus flips COMPLETED to JUST COMPLETED within the 5s window", () => {
+      const exec = makeExecution({ executionStatus: "COMPLETED", endTime: NOW - 2000 });
+      component.listOfExecutions = [exec];
+
+      component.specifyCompletedStatus();
+
+      expect(exec.executionStatus).toBe("JUST COMPLETED");
+    });
+
+    it("specifyCompletedStatus reverts JUST COMPLETED to COMPLETED after 5s", () => {
+      const exec = makeExecution({ executionStatus: "JUST COMPLETED", endTime: NOW - 10000 });
+      component.listOfExecutions = [exec];
+
+      component.specifyCompletedStatus();
+
+      expect(exec.executionStatus).toBe("COMPLETED");
+    });
+
+    it("updateTimeDifferences assigns the elapsed time for each execution", () => {
+      const exec = makeExecution({ executionStatus: "COMPLETED", startTime: 4000, endTime: 10000 });
+      component.listOfExecutions = [exec];
+
+      component.updateTimeDifferences();
+
+      expect(exec.executionTime).toBe(6);
+    });
+
+    it("updateTimeStatus delegates to specifyCompletedStatus and updateTimeDifferences", () => {
+      const specify = vi.spyOn(component, "specifyCompletedStatus");
+      const diffs = vi.spyOn(component, "updateTimeDifferences");
+
+      component.updateTimeStatus();
+
+      expect(specify).toHaveBeenCalledTimes(1);
+      expect(diffs).toHaveBeenCalledTimes(1);
+    });
+
+    it("dataCheck flags a status change and ignores a fresh JUST COMPLETED", () => {
+      const oldRunning = makeExecution({ executionStatus: "RUNNING" });
+      const newCompleted = makeExecution({ executionStatus: "COMPLETED" });
+      expect(component.dataCheck(oldRunning, newCompleted)).toBe(true);
+
+      const oldJustCompleted = makeExecution({ executionStatus: "JUST COMPLETED" });
+      const newFresh = makeExecution({ executionStatus: "COMPLETED", endTime: NOW - 2000 });
+      expect(component.dataCheck(oldJustCompleted, newFresh)).toBe(false);
+    });
+  });
+
+  describe("data + table", () => {
+    it("fetchData populates the list, total and loading flag", () => {
+      const exec = makeExecution({ workflowId: 7 });
+      vi.mocked(service.getExecutionList).mockReturnValue(of([exec]));
+      vi.mocked(service.getTotalWorkflows).mockReturnValue(of(3));
+
+      component.fetchData();
+
+      expect(component.listOfExecutions).toEqual([exec]);
+      expect(component.totalWorkflows).toBe(3);
+      expect(component.isLoading).toBe(false);
+    });
+
+    it("onFilterChange stringifies the filter and refetches with it", () => {
+      vi.mocked(service.getExecutionList).mockClear();
+
+      component.onFilterChange(["RUNNING", "COMPLETED"]);
+
+      expect(component.filter).toEqual(["RUNNING", "COMPLETED"]);
+      expect(vi.mocked(service.getExecutionList).mock.calls[0][4]).toEqual(["RUNNING", "COMPLETED"]);
+    });
+
+    it("onSortChange sets the field/direction and refetches", () => {
+      vi.mocked(service.getExecutionList).mockClear();
+
+      component.onSortChange("executionName", "ascend");
+
+      expect(component.sortField).toBe("executionName");
+      expect(component.sortDirection).toBe("asc");
+      expect(service.getExecutionList).toHaveBeenCalledTimes(1);
+    });
+
+    it("onSortChange resets to NO_SORT when the active field is cleared", () => {
+      component.sortField = "executionName";
+      component.sortDirection = "asc";
+
+      component.onSortChange("executionName", null);
+
+      expect(component.sortField).toBe(NO_SORT);
+      expect(component.sortDirection).toBe(NO_SORT);
+    });
+  });
+
+  describe("execution actions", () => {
+    let openWebsocket: ReturnType<typeof vi.fn>;
+    let send: ReturnType<typeof vi.fn>;
+
+    beforeEach(() => {
+      openWebsocket = vi.spyOn(WorkflowWebsocketService.prototype, "openWebsocket").mockImplementation(() => {});
+      send = vi.spyOn(WorkflowWebsocketService.prototype, "send").mockImplementation(() => {});
+    });
+
+    it("killExecution opens the socket for the workflow, sends a kill request and refreshes", () => {
+      vi.mocked(service.getExecutionList).mockClear();
+
+      component.killExecution(42);
+
+      expect(openWebsocket).toHaveBeenCalledWith(42);
+      expect(send).toHaveBeenCalledWith("WorkflowKillRequest", {});
+      expect(service.getExecutionList).toHaveBeenCalledTimes(1);
+    });
+
+    it("pauseExecution sends a pause request", () => {
+      component.pauseExecution(9);
+
+      expect(openWebsocket).toHaveBeenCalledWith(9);
+      expect(send).toHaveBeenCalledWith("WorkflowPauseRequest", {});
+    });
+
+    it("resumeExecution sends a resume request", () => {
+      component.resumeExecution(9);
+
+      expect(openWebsocket).toHaveBeenCalledWith(9);
+      expect(send).toHaveBeenCalledWith("WorkflowResumeRequest", {});
+    });
+
+    it("clickToViewHistory opens the history modal for the workflow", () => {
+      const modal = TestBed.inject(NzModalService);
+      const create = vi.spyOn(modal, "create").mockReturnValue({} as ReturnType<NzModalService["create"]>);
+
+      component.clickToViewHistory(7, "My Workflow");
+
+      expect(create).toHaveBeenCalledTimes(1);
+      expect(create.mock.calls[0][0]).toMatchObject({
+        nzData: { wid: 7 },
+        nzTitle: "Execution results of Workflow: My Workflow",
+      });
+    });
+  });
+});