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/19 02:51:28 UTC
(texera) 01/01: test(frontend): extend ConsoleFrameComponent coverage with helper, render, and debug-control tests (#6539)
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-6539-10e7cb35000e68f6ab3a21348bba92886acd0152
in repository https://gitbox.apache.org/repos/asf/texera.git
commit 99fce9d6555a45ca63bac6f9bad7c97f560c5362
Author: Meng Wang <me...@uci.edu>
AuthorDate: Sat Jul 18 19:51:05 2026 -0700
test(frontend): extend ConsoleFrameComponent coverage with helper, render, and debug-control tests (#6539)
### What changes were proposed in this PR?
Extends the existing `ConsoleFrameComponent` spec (which only asserted
`should create`) to cover its previously untested helpers, rendering,
and debug
controls
(`frontend/src/app/workspace/component/result-panel/console-frame/console-frame.component.ts`,
~38% coverage). The five collaborating services
(`ExecuteWorkflowService`,
`WorkflowConsoleService`, `WorkflowWebsocketService`,
`NotificationService`,
`UdfDebugService`) are stubbed, with the execution-state and
console-update
streams driven by test `Subject`s.
18 new tests cover:
- **Pure helpers** — `getWorkerIndex` (trailing numeric token),
`workerIdToAbbr` (`W<index>`), `getWorkerColor` (deterministic hex
color), and `getMessageLabel` (message-type → tag color, with the
unknown-type fallback).
- **Rendering** — `clearConsole` empties the list;
`displayConsoleMessages` loads the operator's messages; `renderConsole`
loads worker ids + messages when an operator is set and is a no-op
otherwise.
- **Debug controls** — `onClickContinue` / `onClickStep` dispatch per
worker; `onClickSkipTuples` / `onClickRetryTuples` forward the worker
ids and surface failures via the notification service;
`submitDebugCommand` targets all workers or a single worker and clears
the input, and is a no-op without an operator id.
- **`registerAutoConsoleRerender`** — clears the console on an
Initializing→Running transition, re-renders on any other state change,
and re-renders on a console-message update.
No production code was changed.
### Any related issues, documentation, discussions?
Closes #6531
### How was this PR tested?
Extended unit tests, run locally in `frontend/` (all green; the failure
path was
verified by deliberately breaking a subscription assertion to confirm
the suite
goes red):
```
ng test --watch=false --include src/app/workspace/component/result-panel/console-frame/console-frame.component.spec.ts
# Test Files 1 passed (1) | Tests 19 passed (19)
prettier --write <spec> # unchanged
eslint <spec> # clean
```
All collaborating services are stubbed, so the suite makes no
network/WebSocket
calls (see `frontend/TESTING.md`).
### Was this PR authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Opus 4.8 [1M context])
---------
Signed-off-by: Meng Wang <me...@uci.edu>
Co-authored-by: Copilot Autofix powered by AI <17...@users.noreply.github.com>
---
.../console-frame/console-frame.component.spec.ts | 247 ++++++++++++++++++++-
1 file changed, 239 insertions(+), 8 deletions(-)
diff --git a/frontend/src/app/workspace/component/result-panel/console-frame/console-frame.component.spec.ts b/frontend/src/app/workspace/component/result-panel/console-frame/console-frame.component.spec.ts
index 0c21cf7503..34205e2ae7 100644
--- a/frontend/src/app/workspace/component/result-panel/console-frame/console-frame.component.spec.ts
+++ b/frontend/src/app/workspace/component/result-panel/console-frame/console-frame.component.spec.ts
@@ -18,7 +18,7 @@
*/
import { ComponentFixture, TestBed } from "@angular/core/testing";
-
+import { Subject } from "rxjs";
import { ConsoleFrameComponent } from "./console-frame.component";
import { OperatorMetadataService } from "../../../service/operator-metadata/operator-metadata.service";
import { StubOperatorMetadataService } from "../../../service/operator-metadata/stub-operator-metadata.service";
@@ -27,35 +27,266 @@ import { NzDropDownModule } from "ng-zorro-antd/dropdown";
import { ComputingUnitStatusService } from "../../../../common/service/computing-unit/computing-unit-status/computing-unit-status.service";
import { MockComputingUnitStatusService } from "../../../../common/service/computing-unit/computing-unit-status/mock-computing-unit-status.service";
import { commonTestProviders } from "../../../../common/testing/test-utils";
+import { ExecuteWorkflowService } from "../../../service/execute-workflow/execute-workflow.service";
+import { WorkflowConsoleService } from "../../../service/workflow-console/workflow-console.service";
+import { WorkflowWebsocketService } from "../../../service/workflow-websocket/workflow-websocket.service";
+import { NotificationService } from "../../../../common/service/notification/notification.service";
+import { UdfDebugService } from "../../../service/operator-debug/udf-debug.service";
+import { ExecutionState } from "../../../types/execute-workflow.interface";
+import { ConsoleMessage } from "../../../types/workflow-common.interface";
+
+function consoleMessage(name: string): ConsoleMessage {
+ return {
+ workerId: "w",
+ timestamp: { nanos: 0, seconds: 0 },
+ msgType: { name },
+ source: "src",
+ title: "title",
+ message: "message",
+ };
+}
+
+type StateEvent = { previous: { state: ExecutionState }; current: { state: ExecutionState } };
describe("ConsoleFrameComponent", () => {
let component: ConsoleFrameComponent;
let fixture: ComponentFixture<ConsoleFrameComponent>;
+ let getWorkerIds: ReturnType<typeof vi.fn>;
+ let getConsoleMessages: ReturnType<typeof vi.fn>;
+ let skipTuples: ReturnType<typeof vi.fn>;
+ let retryExecution: ReturnType<typeof vi.fn>;
+ let send: ReturnType<typeof vi.fn>;
+ let notifyError: ReturnType<typeof vi.fn>;
+ let doStep: ReturnType<typeof vi.fn>;
+ let doContinue: ReturnType<typeof vi.fn>;
+ let executionStateStream: Subject<StateEvent>;
+ let consoleUpdateStream: Subject<void>;
+
beforeEach(async () => {
+ getWorkerIds = vi.fn().mockReturnValue([]);
+ getConsoleMessages = vi.fn().mockReturnValue([]);
+ skipTuples = vi.fn();
+ retryExecution = vi.fn();
+ send = vi.fn();
+ notifyError = vi.fn();
+ doStep = vi.fn();
+ doContinue = vi.fn();
+ executionStateStream = new Subject<StateEvent>();
+ consoleUpdateStream = new Subject<void>();
+
await TestBed.configureTestingModule({
imports: [ConsoleFrameComponent, HttpClientTestingModule, NzDropDownModule],
providers: [
+ { provide: OperatorMetadataService, useClass: StubOperatorMetadataService },
+ { provide: ComputingUnitStatusService, useClass: MockComputingUnitStatusService },
{
- provide: OperatorMetadataService,
- useClass: StubOperatorMetadataService,
+ provide: ExecuteWorkflowService,
+ useValue: {
+ getExecutionStateStream: () => executionStateStream.asObservable(),
+ getWorkerIds,
+ skipTuples,
+ retryExecution,
+ },
},
{
- provide: ComputingUnitStatusService,
- useClass: MockComputingUnitStatusService,
+ provide: WorkflowConsoleService,
+ useValue: { getConsoleMessageUpdateStream: () => consoleUpdateStream.asObservable(), getConsoleMessages },
},
+ { provide: WorkflowWebsocketService, useValue: { send } },
+ { provide: NotificationService, useValue: { error: notifyError } },
+ { provide: UdfDebugService, useValue: { doStep, doContinue } },
...commonTestProviders,
],
}).compileComponents();
- });
- beforeEach(() => {
fixture = TestBed.createComponent(ConsoleFrameComponent);
component = fixture.componentInstance;
- fixture.detectChanges();
+ fixture.detectChanges(); // runs ngOnInit -> registerAutoConsoleRerender
});
it("should create", () => {
expect(component).toBeTruthy();
});
+
+ describe("pure helpers", () => {
+ it("getWorkerIndex parses the trailing numeric token", () => {
+ expect(component.getWorkerIndex("worker-op-3")).toBe(3);
+ expect(component.getWorkerIndex("W-0-12")).toBe(12);
+ expect(component.getWorkerIndex("")).toBe(0);
+ });
+
+ it("workerIdToAbbr prefixes the worker index with 'W'", () => {
+ expect(component.workerIdToAbbr("worker-op-3")).toBe("W3");
+ });
+
+ it("getWorkerColor returns a deterministic hex color", () => {
+ const color = component.getWorkerColor(0);
+ expect(color).toMatch(/^#[0-9a-fA-F]{6}$/);
+ expect(component.getWorkerColor(0)).toBe(color); // deterministic
+ expect(component.getWorkerColor(5)).toMatch(/^#[0-9a-fA-F]{6}$/);
+ });
+
+ it("getMessageLabel maps the message type to its tag color", () => {
+ expect(component.getMessageLabel(consoleMessage("PRINT"))).toBe("default");
+ expect(component.getMessageLabel(consoleMessage("COMMAND"))).toBe("processing");
+ expect(component.getMessageLabel(consoleMessage("DEBUGGER"))).toBe("warning");
+ expect(component.getMessageLabel(consoleMessage("ERROR"))).toBe("error");
+ expect(component.getMessageLabel(consoleMessage("UNKNOWN"))).toBe("");
+ });
+ });
+
+ describe("console rendering", () => {
+ it("clearConsole empties the message list", () => {
+ component.consoleMessages = [consoleMessage("PRINT")];
+ component.clearConsole();
+ expect(component.consoleMessages).toEqual([]);
+ });
+
+ it("displayConsoleMessages loads the operator's messages from the service", () => {
+ const messages = [consoleMessage("PRINT"), consoleMessage("ERROR")];
+ getConsoleMessages.mockReturnValue(messages);
+
+ component.displayConsoleMessages("op1");
+
+ expect(getConsoleMessages).toHaveBeenCalledWith("op1");
+ expect(component.consoleMessages).toEqual(messages);
+ });
+
+ it("renderConsole loads the worker ids and messages when an operator is set", () => {
+ component.operatorId = "op1";
+ getWorkerIds.mockReturnValue(["w-1", "w-2"]);
+ getConsoleMessages.mockReturnValue([consoleMessage("PRINT")]);
+
+ component.renderConsole();
+
+ expect(component.workerIds).toEqual(["w-1", "w-2"]);
+ expect(getConsoleMessages).toHaveBeenCalledWith("op1");
+ });
+
+ it("renderConsole is a no-op without an operator id", () => {
+ component.operatorId = "";
+ getWorkerIds.mockClear();
+ component.renderConsole();
+ expect(getWorkerIds).not.toHaveBeenCalled();
+ });
+ });
+
+ describe("debug controls", () => {
+ it("onClickContinue continues every worker via the debug service", () => {
+ component.operatorId = "op1";
+ component.workerIds = ["w-1", "w-2"];
+
+ component.onClickContinue();
+
+ expect(doContinue).toHaveBeenCalledTimes(2);
+ expect(doContinue).toHaveBeenCalledWith("op1", "w-1");
+ expect(doContinue).toHaveBeenCalledWith("op1", "w-2");
+ });
+
+ it("onClickStep steps every worker via the debug service", () => {
+ component.operatorId = "op1";
+ component.workerIds = ["w-1"];
+
+ component.onClickStep();
+
+ expect(doStep).toHaveBeenCalledWith("op1", "w-1");
+ });
+
+ it("onClickSkipTuples forwards the worker ids and surfaces failures", () => {
+ component.workerIds = ["w-1", "w-2"];
+ component.onClickSkipTuples();
+ expect(skipTuples).toHaveBeenCalledWith(["w-1", "w-2"]);
+
+ skipTuples.mockImplementation(() => {
+ throw new Error("skip failed");
+ });
+ component.onClickSkipTuples();
+ expect(notifyError).toHaveBeenCalledWith("skip failed");
+ });
+
+ it("onClickRetryTuples forwards the worker ids and surfaces failures", () => {
+ component.workerIds = ["w-1"];
+ component.onClickRetryTuples();
+ expect(retryExecution).toHaveBeenCalledWith(["w-1"]);
+
+ retryExecution.mockImplementation(() => {
+ throw new Error("retry failed");
+ });
+ component.onClickRetryTuples();
+ expect(notifyError).toHaveBeenCalledWith("retry failed");
+ });
+
+ it("submitDebugCommand sends the command to every worker when All Workers is targeted", () => {
+ component.operatorId = "op1";
+ component.workerIds = ["w-1", "w-2"];
+ component.targetWorker = component.ALL_WORKERS;
+ component.command = "break";
+
+ component.submitDebugCommand();
+
+ expect(send).toHaveBeenCalledTimes(2);
+ expect(send).toHaveBeenCalledWith("DebugCommandRequest", { operatorId: "op1", workerId: "w-1", cmd: "break" });
+ expect(send).toHaveBeenCalledWith("DebugCommandRequest", { operatorId: "op1", workerId: "w-2", cmd: "break" });
+ expect(component.command).toBe(""); // input cleared after sending
+ });
+
+ it("submitDebugCommand sends only to the selected worker", () => {
+ component.operatorId = "op1";
+ component.workerIds = ["w-1", "w-2"];
+ component.targetWorker = "w-2";
+ component.command = "continue";
+
+ component.submitDebugCommand();
+
+ expect(send).toHaveBeenCalledTimes(1);
+ expect(send).toHaveBeenCalledWith("DebugCommandRequest", { operatorId: "op1", workerId: "w-2", cmd: "continue" });
+ });
+
+ it("submitDebugCommand does nothing without an operator id", () => {
+ component.workerIds = ["w-1"];
+ component.command = "break";
+ // operatorId is left undefined
+ component.submitDebugCommand();
+ expect(send).not.toHaveBeenCalled();
+ });
+ });
+
+ describe("registerAutoConsoleRerender", () => {
+ it("clears the console when execution transitions from Initializing to Running", () => {
+ component.consoleMessages = [consoleMessage("PRINT")];
+
+ executionStateStream.next({
+ previous: { state: ExecutionState.Initializing },
+ current: { state: ExecutionState.Running },
+ });
+
+ expect(component.consoleMessages).toEqual([]);
+ });
+
+ it("re-renders the console on any other execution state change", () => {
+ component.operatorId = "op1";
+ getWorkerIds.mockReturnValue(["w-9"]);
+ getConsoleMessages.mockReturnValue([consoleMessage("DEBUGGER")]);
+
+ executionStateStream.next({
+ previous: { state: ExecutionState.Running },
+ current: { state: ExecutionState.Paused },
+ });
+
+ expect(component.workerIds).toEqual(["w-9"]);
+ expect(component.consoleMessages).toEqual([consoleMessage("DEBUGGER")]);
+ });
+
+ it("re-renders the console when a console message update arrives", () => {
+ component.operatorId = "op1";
+ getWorkerIds.mockReturnValue(["w-5"]);
+ getConsoleMessages.mockReturnValue([consoleMessage("PRINT")]);
+
+ consoleUpdateStream.next();
+
+ expect(component.workerIds).toEqual(["w-5"]);
+ expect(component.consoleMessages).toEqual([consoleMessage("PRINT")]);
+ });
+ });
});