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

Reply via email to