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-6765-5aa2b79e8f32260a6b588cc1fa1f704d2d1f4365
in repository https://gitbox.apache.org/repos/asf/texera.git

commit 31a28b9e567a7f30dc543dc95fda03986893b906
Author: Meng Wang <[email protected]>
AuthorDate: Wed Jul 22 16:59:20 2026 -0700

    test(frontend): cover ComputingUnitStatusService polling/state and 
WorkflowWebsocketService events (#6765)
    
    ### What changes were proposed in this PR?
    
    Extends two related workspace service specs
    
    
(`frontend/src/app/common/service/computing-unit/computing-unit-status/computing-unit-status.service.ts`,
    
    
`frontend/src/app/workspace/service/workflow-websocket/workflow-websocket.service.ts`)
    to cover the state-management, polling and connection logic they had
    left
    untested. No production code was changed.
    
    **`ComputingUnitStatusService`** (+5 tests)
    
    - `updateUnitInList` — replaces the matching unit and leaves the others
    untouched;
    - `setComputingUnitsState` — refreshes the selected unit when it is
    still present,
    and clears the selection + stops polling when the selected unit
    disappears;
    - `startPollingSelectedUnit` — with `vi.useFakeTimers()`, re-fetches the
    unit on
    each interval tick and merges the result; `stopPollingSelectedUnit`
    halts further
      polling (real timers restored in a `finally`).
    
    **`WorkflowWebsocketService`** (+3 tests)
    
    - `websocketEvent` — surfaces events pushed onto the response stream;
    - `getConnectionStatusStream` / `updateConnectionStatus` — reflect
    connect/disconnect
      transitions and guard duplicate values;
    - `openWebsocket` — routes an incoming socket message to
    `websocketEvent` and marks
    the connection up, driven through a fake `window.WebSocket` (the spec's
    existing
      test double), so no real socket is opened.
    
    ### Any related issues, documentation, discussions?
    
    Closes #6754
    
    ### How was this PR tested?
    
    Extended unit tests, run locally in `frontend/` (all green; the failure
    path was
    verified by breaking the polling and connection assertions to confirm
    the suite
    goes red):
    
    ```
    ng test --watch=false --include 
src/app/common/service/computing-unit/computing-unit-status/computing-unit-status.service.spec.ts
   # 18 passed
    ng test --watch=false --include 
src/app/workspace/service/workflow-websocket/workflow-websocket.service.spec.ts 
                    # 6 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])
---
 .../computing-unit-status.service.spec.ts          | 71 ++++++++++++++++++++++
 .../workflow-websocket.service.spec.ts             | 62 +++++++++++++++++++
 2 files changed, 133 insertions(+)

diff --git 
a/frontend/src/app/common/service/computing-unit/computing-unit-status/computing-unit-status.service.spec.ts
 
b/frontend/src/app/common/service/computing-unit/computing-unit-status/computing-unit-status.service.spec.ts
index 4889a43270..bae16d79ec 100644
--- 
a/frontend/src/app/common/service/computing-unit/computing-unit-status/computing-unit-status.service.spec.ts
+++ 
b/frontend/src/app/common/service/computing-unit/computing-unit-status/computing-unit-status.service.spec.ts
@@ -223,4 +223,75 @@ describe("ComputingUnitStatusService", () => {
     expect(listSpy).toHaveBeenCalled();
     expect(latest).toEqual(newUnits);
   });
+
+  it("updateUnitInList replaces the matching unit and leaves the others 
untouched", () => {
+    const unitA = mockUnit(1);
+    const unitB = mockUnit(2);
+    (service as any).allComputingUnitsSubject.next([unitA, unitB]);
+
+    const updatedA = { computingUnit: { cuid: 1 }, status: "Running" } as 
unknown as DashboardWorkflowComputingUnit;
+    (service as any).updateUnitInList(updatedA);
+
+    expect((service as any).allComputingUnitsSubject.value).toEqual([updatedA, 
unitB]);
+  });
+
+  it("setComputingUnitsState refreshes the selected unit when it is still 
present in the new list", () => {
+    (service as any).selectedUnitSubject.next(mockUnit(7));
+
+    const updated = { computingUnit: { cuid: 7 }, status: "Running" } as 
unknown as DashboardWorkflowComputingUnit;
+    (service as any).setComputingUnitsState([updated]);
+
+    expect(service.getSelectedComputingUnitValue()).toBe(updated);
+  });
+
+  it("setComputingUnitsState clears the selection and stops polling when the 
selected unit disappears", () => {
+    (service as any).selectedUnitSubject.next(mockUnit(7));
+    const stopSpy = vi.spyOn(service as any, "stopPollingSelectedUnit");
+
+    (service as any).setComputingUnitsState([mockUnit(8)]);
+
+    expect(service.getSelectedComputingUnitValue()).toBeNull();
+    expect(stopSpy).toHaveBeenCalled();
+  });
+
+  it("startPollingSelectedUnit polls the unit on each interval tick and merges 
the result", () => {
+    vi.useFakeTimers();
+    try {
+      const managing = TestBed.inject(WorkflowComputingUnitManagingService);
+      const polled = { computingUnit: { cuid: 3 }, status: "Running" } as 
unknown as DashboardWorkflowComputingUnit;
+      const getSpy = vi.spyOn(managing, 
"getComputingUnit").mockReturnValue(of(polled));
+      (service as any).allComputingUnitsSubject.next([mockUnit(3)]);
+
+      (service as any).startPollingSelectedUnit(3);
+      // interval() fires only after the first period elapses
+      expect(getSpy).not.toHaveBeenCalled();
+
+      vi.advanceTimersByTime((service as any).REFRESH_INTERVAL_MS);
+
+      expect(getSpy).toHaveBeenCalledWith(3);
+      expect((service as 
any).allComputingUnitsSubject.value).toEqual([polled]);
+    } finally {
+      // Stop the interval poll here so the test is self-contained rather than
+      // relying on afterEach's ngOnDestroy to tear it down.
+      (service as any).stopPollingSelectedUnit();
+      vi.useRealTimers();
+    }
+  });
+
+  it("stopPollingSelectedUnit halts further polling", () => {
+    vi.useFakeTimers();
+    try {
+      const managing = TestBed.inject(WorkflowComputingUnitManagingService);
+      const getSpy = vi.spyOn(managing, 
"getComputingUnit").mockReturnValue(of(mockUnit(3)));
+
+      (service as any).startPollingSelectedUnit(3);
+      (service as any).stopPollingSelectedUnit();
+
+      vi.advanceTimersByTime(10000);
+
+      expect(getSpy).not.toHaveBeenCalled();
+    } finally {
+      vi.useRealTimers();
+    }
+  });
 });
diff --git 
a/frontend/src/app/workspace/service/workflow-websocket/workflow-websocket.service.spec.ts
 
b/frontend/src/app/workspace/service/workflow-websocket/workflow-websocket.service.spec.ts
index 4fb0fe1d10..db6ecc6aad 100644
--- 
a/frontend/src/app/workspace/service/workflow-websocket/workflow-websocket.service.spec.ts
+++ 
b/frontend/src/app/workspace/service/workflow-websocket/workflow-websocket.service.spec.ts
@@ -18,6 +18,7 @@
  */
 
 import { TestBed } from "@angular/core/testing";
+import { Subscription } from "rxjs";
 import { WorkflowWebsocketService } from "./workflow-websocket.service";
 import { commonTestProviders } from "../../../common/testing/test-utils";
 
@@ -102,4 +103,65 @@ describe("WorkflowWebsocketService", () => {
     service.closeWebsocket();
     expect(service.numWorkers).toBe(-1);
   });
+
+  it("websocketEvent surfaces events pushed onto the response stream", () => {
+    const received: unknown[] = [];
+    const sub = service.websocketEvent().subscribe(event => 
received.push(event));
+
+    const event = { type: "WorkflowStateEvent", state: "RUNNING" };
+    (service as any).webSocketResponseSubject.next(event);
+    sub.unsubscribe();
+
+    expect(received).toEqual([event]);
+  });
+
+  it("getConnectionStatusStream reflects updateConnectionStatus transitions 
and guards duplicates", () => {
+    const emissions: boolean[] = [];
+    const sub = service.getConnectionStatusStream().subscribe(value => 
emissions.push(value));
+
+    // BehaviorSubject seeds `false`; a repeated value is guarded and does not 
re-emit.
+    (service as any).updateConnectionStatus(true);
+    (service as any).updateConnectionStatus(true);
+    (service as any).updateConnectionStatus(false);
+    sub.unsubscribe();
+
+    expect(emissions).toEqual([false, true, false]);
+    expect(service.isConnected).toBe(false);
+  });
+
+  it("openWebsocket routes an incoming socket message to websocketEvent and 
marks the connection up", async () => {
+    const originalWebSocket = window.WebSocket;
+    const sockets: FakeWebSocket[] = [];
+    class CapturingWebSocket extends FakeWebSocket {
+      constructor(url: string) {
+        super(url);
+        sockets.push(this);
+      }
+    }
+    window.WebSocket = CapturingWebSocket as unknown as typeof WebSocket;
+
+    const subscriptions: Subscription[] = [];
+    try {
+      const events: unknown[] = [];
+      subscriptions.push(service.websocketEvent().subscribe(event => 
events.push(event)));
+      let connected: boolean | undefined;
+      subscriptions.push(service.getConnectionStatusStream().subscribe(value 
=> (connected = value)));
+
+      service.openWebsocket(1, 1, 1);
+      await Promise.resolve(); // let the fake socket transition to OPEN
+
+      const socket = sockets[sockets.length - 1];
+      const event = { type: "WorkflowStateEvent", state: "RUNNING" };
+      socket.onmessage?.(new MessageEvent("message", { data: 
JSON.stringify(event) }));
+
+      expect(events).toContainEqual(event);
+      expect(connected).toBe(true);
+    } finally {
+      // The service's subjects never complete, so unsubscribe our observers
+      // explicitly rather than leaving them attached past the test.
+      subscriptions.forEach(subscription => subscription.unsubscribe());
+      service.closeWebsocket();
+      window.WebSocket = originalWebSocket;
+    }
+  });
 });

Reply via email to