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