This is an automated email from the ASF dual-hosted git repository.
github-merge-queue[bot] pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/texera.git
The following commit(s) were added to refs/heads/main by this push:
new 42027e8c38 test(frontend): extend UndoRedoService,
WorkflowConsoleService, and NotificationService coverage (#6603)
42027e8c38 is described below
commit 42027e8c38d248a60cab32871887bc754ef311b5
Author: Meng Wang <[email protected]>
AuthorDate: Mon Jul 20 12:11:20 2026 -0700
test(frontend): extend UndoRedoService, WorkflowConsoleService, and
NotificationService coverage (#6603)
### What changes were proposed in this PR?
Extends three near-empty workspace/common service specs (Vitest; no
production
code changed).
**`UndoRedoService`** (+7 tests) — a new describe drives the service
through a
`Y.UndoManager` double via `setUndoManager`: `undoAction` / `redoAction`
delegate
to the manager only when it can undo/redo (and toggle the joint-command
guard
back on), and are no-ops when the manager can't or while workflow
modification is
disabled; `canUndo` / `canRedo` require both the guard and the manager;
`getUndoLength` / `getRedoLength` reflect the stacks; `clearUndoStack` /
`clearRedoStack` clear the correct side; and with no manager `canUndo` /
`canRedo` are false.
**`WorkflowConsoleService`** (+3 tests) — a controlled
`WorkflowWebsocketService`
double drives the constructor's event subscriptions: a
`ConsoleUpdateEvent`
buffers messages exposed per operator via `getConsoleMessages` /
`hasConsoleMessages` and pushes an update;
`registerAutoClearConsoleMessages`
clears the store on an `Initializing` `WorkflowStateEvent` (and leaves
it alone
for other states).
**`NotificationService`** (+4 tests) — `NzMessageService` /
`NzNotificationService`
doubles verify `success` / `info` / `error` / `warning` / `loading`
delegate to
the message service (with the content/options and returned ref) and
`blank` /
`remove` delegate to the notification service.
> Note: the issue lists `remove` as delegating to the message service;
the code
> actually routes `blank` and `remove` to `NzNotificationService`, which
the test
> asserts.
### Any related issues, documentation, discussions?
Closes #6589
### How was this PR tested?
Extended unit tests, run locally in `frontend/` (all green; each spec's
failure
path was verified by breaking a new assertion to confirm it goes red):
```
ng test --watch=false --include
src/app/workspace/service/undo-redo/undo-redo.service.spec.ts #
11 passed
ng test --watch=false --include
src/app/workspace/service/workflow-console/workflow-console.service.spec.ts # 5
passed
ng test --watch=false --include
src/app/common/service/notification/notification.service.spec.ts # 6
passed
prettier --write <specs> # clean
eslint <specs> # clean
```
All collaborators 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])
---
.../notification/notification.service.spec.ts | 56 ++++++++++++-
.../service/undo-redo/undo-redo.service.spec.ts | 95 ++++++++++++++++++++++
.../workflow-console.service.spec.ts | 66 ++++++++++++++-
3 files changed, 213 insertions(+), 4 deletions(-)
diff --git
a/frontend/src/app/common/service/notification/notification.service.spec.ts
b/frontend/src/app/common/service/notification/notification.service.spec.ts
index c193a7f404..1313cadbdd 100644
--- a/frontend/src/app/common/service/notification/notification.service.spec.ts
+++ b/frontend/src/app/common/service/notification/notification.service.spec.ts
@@ -18,18 +18,70 @@
*/
import { TestBed } from "@angular/core/testing";
-
+import { NzMessageService } from "ng-zorro-antd/message";
+import { NzNotificationService } from "ng-zorro-antd/notification";
import { NotificationService } from "./notification.service";
describe("NotificationService", () => {
let service: NotificationService;
+ let message: Record<"success" | "info" | "error" | "warning" | "loading",
ReturnType<typeof vi.fn>>;
+ let notification: Record<"blank" | "remove", ReturnType<typeof vi.fn>>;
beforeEach(() => {
- TestBed.configureTestingModule({});
+ message = { success: vi.fn(), info: vi.fn(), error: vi.fn(), warning:
vi.fn(), loading: vi.fn() };
+ notification = { blank: vi.fn(), remove: vi.fn() };
+
+ TestBed.configureTestingModule({
+ providers: [
+ NotificationService,
+ { provide: NzMessageService, useValue: message },
+ { provide: NzNotificationService, useValue: notification },
+ ],
+ });
service = TestBed.inject(NotificationService);
});
it("should be created", () => {
expect(service).toBeTruthy();
});
+
+ it("success / info / error / warning delegate to the message service with
the content and options", () => {
+ const options = { nzDuration: 5000 };
+ service.success("s", options);
+ service.info("i", options);
+ service.error("e", options);
+ service.warning("w", options);
+
+ expect(message.success).toHaveBeenCalledWith("s", options);
+ expect(message.info).toHaveBeenCalledWith("i", options);
+ expect(message.error).toHaveBeenCalledWith("e", options);
+ expect(message.warning).toHaveBeenCalledWith("w", options);
+ });
+
+ it("defaults to empty options when none are given", () => {
+ service.error("boom");
+ expect(message.error).toHaveBeenCalledWith("boom", {});
+ });
+
+ it("loading delegates to the message service and returns its ref", () => {
+ const ref = { messageId: "m1" };
+ message.loading.mockReturnValue(ref);
+
+ expect(service.loading("working", { nzDuration: 0 })).toBe(ref);
+ expect(message.loading).toHaveBeenCalledWith("working", { nzDuration: 0 });
+ });
+
+ it("blank delegates to the notification service and returns its ref", () => {
+ const ref = { messageId: "n1" };
+ notification.blank.mockReturnValue(ref);
+ const options = { nzDuration: 0 };
+
+ expect(service.blank("title", "content", options)).toBe(ref);
+ expect(notification.blank).toHaveBeenCalledWith("title", "content",
options);
+ });
+
+ it("remove delegates to the notification service", () => {
+ service.remove();
+ expect(notification.remove).toHaveBeenCalledTimes(1);
+ });
});
diff --git
a/frontend/src/app/workspace/service/undo-redo/undo-redo.service.spec.ts
b/frontend/src/app/workspace/service/undo-redo/undo-redo.service.spec.ts
index 20932d3900..4e17f5005b 100644
--- a/frontend/src/app/workspace/service/undo-redo/undo-redo.service.spec.ts
+++ b/frontend/src/app/workspace/service/undo-redo/undo-redo.service.spec.ts
@@ -27,6 +27,7 @@ import { inject, TestBed } from "@angular/core/testing";
import { UndoRedoService } from "./undo-redo.service";
import { WorkflowUtilService } from
"../workflow-graph/util/workflow-util.service";
import { commonTestProviders } from "../../../common/testing/test-utils";
+import * as Y from "yjs";
describe("UndoRedoService", () => {
let service: UndoRedoService;
@@ -83,4 +84,98 @@ describe("UndoRedoService", () => {
expect(service.getUndoLength()).toEqual(1);
expect(service.getRedoLength()).toEqual(0);
});
+
+ describe("with a stubbed undo manager", () => {
+ let undoRedo: UndoRedoService;
+ let manager: {
+ canUndo: ReturnType<typeof vi.fn>;
+ canRedo: ReturnType<typeof vi.fn>;
+ undo: ReturnType<typeof vi.fn>;
+ redo: ReturnType<typeof vi.fn>;
+ clear: ReturnType<typeof vi.fn>;
+ undoStack: unknown[];
+ redoStack: unknown[];
+ };
+
+ beforeEach(() => {
+ undoRedo = new UndoRedoService();
+ manager = {
+ canUndo: vi.fn().mockReturnValue(true),
+ canRedo: vi.fn().mockReturnValue(true),
+ undo: vi.fn(),
+ redo: vi.fn(),
+ clear: vi.fn(),
+ undoStack: [1, 2, 3],
+ redoStack: [1],
+ };
+ undoRedo.setUndoManager(manager as unknown as Y.UndoManager);
+ });
+
+ afterEach(() => vi.restoreAllMocks()); // guarantees any console spy is
restored even if an assertion throws
+
+ it("undoAction / redoAction delegate to the manager and toggle the
joint-command guard back on", () => {
+ undoRedo.undoAction();
+ undoRedo.redoAction();
+
+ expect(manager.undo).toHaveBeenCalledTimes(1);
+ expect(manager.redo).toHaveBeenCalledTimes(1);
+ expect(undoRedo.listenJointCommand).toBe(true);
+ });
+
+ it("undoAction / redoAction are no-ops when the manager cannot undo/redo",
() => {
+ manager.canUndo.mockReturnValue(false);
+ manager.canRedo.mockReturnValue(false);
+
+ undoRedo.undoAction();
+ undoRedo.redoAction();
+
+ expect(manager.undo).not.toHaveBeenCalled();
+ expect(manager.redo).not.toHaveBeenCalled();
+ });
+
+ it("undoAction / redoAction are no-ops while workflow modification is
disabled", () => {
+ vi.spyOn(console, "error").mockImplementation(() => {}); // restored by
afterEach
+ undoRedo.disableWorkFlowModification();
+
+ undoRedo.undoAction();
+ undoRedo.redoAction();
+
+ expect(manager.undo).not.toHaveBeenCalled();
+ expect(manager.redo).not.toHaveBeenCalled();
+ });
+
+ it("canUndo / canRedo require both the modification guard and the
manager", () => {
+ expect(undoRedo.canUndo()).toBe(true);
+ expect(undoRedo.canRedo()).toBe(true);
+
+ undoRedo.disableWorkFlowModification();
+ expect(undoRedo.canUndo()).toBe(false);
+ expect(undoRedo.canRedo()).toBe(false);
+
+ undoRedo.enableWorkFlowModification();
+ manager.canUndo.mockReturnValue(false);
+ expect(undoRedo.canUndo()).toBe(false);
+ });
+
+ it("getUndoLength / getRedoLength reflect the manager stacks", () => {
+ expect(undoRedo.getUndoLength()).toBe(3);
+ expect(undoRedo.getRedoLength()).toBe(1);
+ });
+
+ it("clearUndoStack / clearRedoStack clear the correct side of the
manager", () => {
+ undoRedo.clearUndoStack();
+ expect(manager.clear).toHaveBeenCalledWith(true, false);
+
+ undoRedo.clearRedoStack();
+ expect(manager.clear).toHaveBeenCalledWith(false, true);
+ });
+ });
+
+ describe("without an undo manager", () => {
+ it("canUndo / canRedo are false", () => {
+ const undoRedo = new UndoRedoService();
+ expect(undoRedo.canUndo()).toBe(false);
+ expect(undoRedo.canRedo()).toBe(false);
+ });
+ });
});
diff --git
a/frontend/src/app/workspace/service/workflow-console/workflow-console.service.spec.ts
b/frontend/src/app/workspace/service/workflow-console/workflow-console.service.spec.ts
index a211eea52b..a098d80515 100644
---
a/frontend/src/app/workspace/service/workflow-console/workflow-console.service.spec.ts
+++
b/frontend/src/app/workspace/service/workflow-console/workflow-console.service.spec.ts
@@ -18,15 +18,46 @@
*/
import { TestBed } from "@angular/core/testing";
+import { Subject } from "rxjs";
import { WorkflowConsoleService } from "./workflow-console.service";
+import { WorkflowWebsocketService } from
"../workflow-websocket/workflow-websocket.service";
import { commonTestProviders } from "../../../common/testing/test-utils";
+import { ExecutionState } from "../../types/execute-workflow.interface";
+import { ConsoleMessage, ConsoleUpdateEvent } 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",
+ };
+}
describe("WorkflowConsoleService", () => {
let service: WorkflowConsoleService;
+ // The service subscribes to these websocket events in its constructor, so
the
+ // doubles must be in place before it is injected.
+ let consoleUpdateEvent$: Subject<ConsoleUpdateEvent>;
+ let workflowStateEvent$: Subject<{ state: ExecutionState }>;
beforeEach(() => {
+ consoleUpdateEvent$ = new Subject<ConsoleUpdateEvent>();
+ workflowStateEvent$ = new Subject<{ state: ExecutionState }>();
+ const subscribeToEvent = vi.fn((eventType: string) => {
+ if (eventType === "ConsoleUpdateEvent") return
consoleUpdateEvent$.asObservable();
+ if (eventType === "WorkflowStateEvent") return
workflowStateEvent$.asObservable();
+ return new Subject().asObservable();
+ });
+
TestBed.configureTestingModule({
- providers: [WorkflowConsoleService, ...commonTestProviders],
+ providers: [
+ WorkflowConsoleService,
+ { provide: WorkflowWebsocketService, useValue: { subscribeToEvent } },
+ ...commonTestProviders,
+ ],
});
service = TestBed.inject(WorkflowConsoleService);
});
@@ -35,8 +66,22 @@ describe("WorkflowConsoleService", () => {
expect(service).toBeTruthy();
});
+ it("buffers messages from a ConsoleUpdateEvent and exposes them per
operator", () => {
+ let updates = 0;
+ service.getConsoleMessageUpdateStream().subscribe(() => updates++);
+ const messages = [consoleMessage("PRINT"), consoleMessage("ERROR")];
+
+ consoleUpdateEvent$.next({ operatorId: "op1", messages });
+
+ expect(service.hasConsoleMessages("op1")).toBe(true);
+ expect(service.getConsoleMessages("op1")).toEqual(messages);
+ expect(service.getConsoleMessages("missing")).toBeUndefined();
+ expect(service.hasConsoleMessages("missing")).toBe(false);
+ expect(updates).toBe(1);
+ });
+
it("clearConsoleMessages() removes all messages and notifies subscribers",
() => {
- (service as any).consoleMessages.set("op1", []);
+ consoleUpdateEvent$.next({ operatorId: "op1", messages:
[consoleMessage("PRINT")] });
expect(service.hasConsoleMessages("op1")).toBe(true);
let notified = false;
@@ -47,4 +92,21 @@ describe("WorkflowConsoleService", () => {
expect(service.hasConsoleMessages("op1")).toBe(false);
expect(notified).toBe(true);
});
+
+ it("clears the console store when the workflow re-initializes
(WorkflowStateEvent)", () => {
+ consoleUpdateEvent$.next({ operatorId: "op1", messages:
[consoleMessage("PRINT")] });
+ expect(service.hasConsoleMessages("op1")).toBe(true);
+
+ workflowStateEvent$.next({ state: ExecutionState.Initializing });
+
+ expect(service.hasConsoleMessages("op1")).toBe(false);
+ });
+
+ it("leaves the store untouched for a non-initializing workflow state event",
() => {
+ consoleUpdateEvent$.next({ operatorId: "op1", messages:
[consoleMessage("PRINT")] });
+
+ workflowStateEvent$.next({ state: ExecutionState.Running });
+
+ expect(service.hasConsoleMessages("op1")).toBe(true);
+ });
});