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-6570-547d5051aade41c2fb0a38c6c0d714967a6c502f
in repository https://gitbox.apache.org/repos/asf/texera.git

commit 8937f71c616ae2beb3606a43c8c38fe907380987
Author: Meng Wang <[email protected]>
AuthorDate: Sun Jul 19 23:05:48 2026 -0700

    test(frontend): add unit test coverage for WorkflowResultExportService, 
WorkflowExecutionsService, and areOperatorSchemasEqual (#6570)
    
    ### What changes were proposed in this PR?
    
    Adds/extends Vitest coverage for three previously-thin logic-layer
    targets
    (28 tests total, all jsdom, no browser mode):
    
    - **`areOperatorSchemasEqual`**
    (`workspace/types/operator-schema.interface.ts`,
    was ~9%, no spec) — new pure-function spec (no TestBed). Covers
    identical
      schemas → `true`, and `false` for differences in `operatorType` /
    `operatorVersion` / nested `jsonSchema` fields / `jsonSchema` array
    order /
    each `additionalMetadata` field / input & output port count,
    `displayName`,
      `disallowMultiLinks`, and port array order.
    
    - **`WorkflowResultExportService`**
    (`workspace/service/workflow-result-export/`,
      was ~42%, creation-only spec) — extended with `resetFlags` (resets the
    export-availability flags to defaults), plus a new suite for the
    exported
    `WorkflowResultDownloadability` helper class: `getExportableOperatorIds`
    /
    `getBlockedOperatorIds` partition operators against the restricted map,
    and
    `getBlockingDatasets` returns the deduped union of blocking labels. The
    HTTP/side-effecting `performExport` / `exportWorkflowExecutionResult`
    paths
      are intentionally out of scope.
    
    - **`WorkflowExecutionsService`**
    (`dashboard/service/user/workflow-executions/`,
    was ~33%, `it.todo` stub) — replaced the stub with
    `HttpClientTestingModule` +
    `HttpTestingController` tests asserting method / URL / body / query
    params for
    `retrieveLatestWorkflowExecution`, `retrieveWorkflowExecutions` (with
    and
    without a status filter), `groupSetIsBookmarked`,
    `groupDeleteWorkflowExecutions`,
    `updateWorkflowExecutionsName`, and `retrieveWorkflowRuntimeStatistics`.
    
    All tests are deterministic (pure functions, synchronous
    `of(...)`/flushed HTTP,
    `httpMock.verify()` in `afterEach`). No production code was changed.
    
    ### Any related issues, documentation, discussions?
    
    Closes #6567
    
    ### How was this PR tested?
    
    New/extended unit tests, run locally in `frontend/` (all green; each
    file's
    failure path was verified by breaking an assertion to confirm the suite
    goes red):
    
    ```
    ng test --watch=false --include 
src/app/workspace/types/operator-schema.interface.spec.ts               # 14 
passed
    ng test --watch=false --include 
src/app/workspace/service/workflow-result-export/workflow-result-export.service.spec.ts
  # 7 passed
    ng test --watch=false --include 
src/app/dashboard/service/user/workflow-executions/workflow-executions.service.spec.ts
   # 7 passed
    eslint <specs>      # clean
    prettier --check    # clean
    ```
    
    ### Was this PR authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code (Opus 4.8 [1M context])
---
 .../workflow-executions.service.spec.ts            |  94 ++++++++++++++-
 .../workflow-result-export.service.spec.ts         |  50 +++++++-
 .../types/operator-schema.interface.spec.ts        | 133 +++++++++++++++++++++
 3 files changed, 272 insertions(+), 5 deletions(-)

diff --git 
a/frontend/src/app/dashboard/service/user/workflow-executions/workflow-executions.service.spec.ts
 
b/frontend/src/app/dashboard/service/user/workflow-executions/workflow-executions.service.spec.ts
index 69bc54c80d..66d89ece5c 100644
--- 
a/frontend/src/app/dashboard/service/user/workflow-executions/workflow-executions.service.spec.ts
+++ 
b/frontend/src/app/dashboard/service/user/workflow-executions/workflow-executions.service.spec.ts
@@ -17,9 +17,95 @@
  * under the License.
  */
 
+import { TestBed } from "@angular/core/testing";
+import { HttpClientTestingModule, HttpTestingController } from 
"@angular/common/http/testing";
+import { WORKFLOW_EXECUTIONS_API_BASE_URL, WorkflowExecutionsService } from 
"./workflow-executions.service";
+import { ExecutionState } from 
"../../../../workspace/types/execute-workflow.interface";
+import { WorkflowExecutionsEntry } from 
"../../../type/workflow-executions-entry";
+import { WorkflowRuntimeStatistics } from 
"../../../type/workflow-runtime-statistics";
+
 describe("WorkflowExecutionsService", () => {
-  // This spec was created without test bodies. The placeholder below keeps
-  // Vitest's discovery happy so the file compiles cleanly; real tests for
-  // WorkflowExecutionsService are tracked in #4861.
-  it.todo("add unit tests for WorkflowExecutionsService");
+  let service: WorkflowExecutionsService;
+  let httpMock: HttpTestingController;
+
+  beforeEach(() => {
+    TestBed.configureTestingModule({
+      imports: [HttpClientTestingModule],
+      providers: [WorkflowExecutionsService],
+    });
+    service = TestBed.inject(WorkflowExecutionsService);
+    httpMock = TestBed.inject(HttpTestingController);
+  });
+
+  afterEach(() => {
+    httpMock.verify();
+  });
+
+  it("retrieveLatestWorkflowExecution issues GET to the latest url", () => {
+    let result: WorkflowExecutionsEntry | undefined;
+    service.retrieveLatestWorkflowExecution(1).subscribe(res => (result = 
res));
+
+    const req = 
httpMock.expectOne(`${WORKFLOW_EXECUTIONS_API_BASE_URL}/1/latest`);
+    expect(req.request.method).toBe("GET");
+    const entry = { eId: 7 } as unknown as WorkflowExecutionsEntry;
+    req.flush(entry);
+    expect(result).toEqual(entry);
+  });
+
+  it("retrieveWorkflowExecutions issues GET without a status param when no 
statuses are given", () => {
+    service.retrieveWorkflowExecutions(1).subscribe();
+
+    const req = httpMock.expectOne(r => r.url === 
`${WORKFLOW_EXECUTIONS_API_BASE_URL}/1`);
+    expect(req.request.method).toBe("GET");
+    expect(req.request.params.has("status")).toBe(false);
+    req.flush([]);
+  });
+
+  it("retrieveWorkflowExecutions sets the status param when statuses are 
given", () => {
+    service.retrieveWorkflowExecutions(1, [ExecutionState.Running, 
ExecutionState.Completed]).subscribe();
+
+    const req = httpMock.expectOne(r => r.url === 
`${WORKFLOW_EXECUTIONS_API_BASE_URL}/1`);
+    expect(req.request.method).toBe("GET");
+    expect(req.request.params.get("status")).toBe([ExecutionState.Running, 
ExecutionState.Completed].join(","));
+    req.flush([]);
+  });
+
+  it("groupSetIsBookmarked issues PUT with the bookmark payload", () => {
+    service.groupSetIsBookmarked(1, [10, 11], true).subscribe();
+
+    const req = 
httpMock.expectOne(`${WORKFLOW_EXECUTIONS_API_BASE_URL}/set_execution_bookmarks`);
+    expect(req.request.method).toBe("PUT");
+    expect(req.request.body).toEqual({ wid: 1, eIds: [10, 11], isBookmarked: 
true });
+    req.flush({});
+  });
+
+  it("groupDeleteWorkflowExecutions issues PUT with the delete payload", () => 
{
+    service.groupDeleteWorkflowExecutions(1, [10, 11]).subscribe();
+
+    const req = 
httpMock.expectOne(`${WORKFLOW_EXECUTIONS_API_BASE_URL}/delete_executions`);
+    expect(req.request.method).toBe("PUT");
+    expect(req.request.body).toEqual({ wid: 1, eIds: [10, 11] });
+    req.flush({});
+  });
+
+  it("updateWorkflowExecutionsName issues POST with the rename payload", () => 
{
+    service.updateWorkflowExecutionsName(1, 10, "renamed").subscribe();
+
+    const req = 
httpMock.expectOne(`${WORKFLOW_EXECUTIONS_API_BASE_URL}/update_execution_name`);
+    expect(req.request.method).toBe("POST");
+    expect(req.request.body).toEqual({ wid: 1, eId: 10, executionName: 
"renamed" });
+    req.flush({});
+  });
+
+  it("retrieveWorkflowRuntimeStatistics issues GET with the cuid param", () => 
{
+    let result: WorkflowRuntimeStatistics[] | undefined;
+    service.retrieveWorkflowRuntimeStatistics(1, 10, 5).subscribe(res => 
(result = res));
+
+    const req = httpMock.expectOne(r => r.url === 
`${WORKFLOW_EXECUTIONS_API_BASE_URL}/1/stats/10`);
+    expect(req.request.method).toBe("GET");
+    expect(req.request.params.get("cuid")).toBe("5");
+    const stats: WorkflowRuntimeStatistics[] = [];
+    req.flush(stats);
+    expect(result).toEqual(stats);
+  });
 });
diff --git 
a/frontend/src/app/workspace/service/workflow-result-export/workflow-result-export.service.spec.ts
 
b/frontend/src/app/workspace/service/workflow-result-export/workflow-result-export.service.spec.ts
index 01900ccffb..e53d8ca0ab 100644
--- 
a/frontend/src/app/workspace/service/workflow-result-export/workflow-result-export.service.spec.ts
+++ 
b/frontend/src/app/workspace/service/workflow-result-export/workflow-result-export.service.spec.ts
@@ -18,7 +18,7 @@
  */
 
 import { TestBed } from "@angular/core/testing";
-import { WorkflowResultExportService } from "./workflow-result-export.service";
+import { WorkflowResultDownloadability, WorkflowResultExportService } from 
"./workflow-result-export.service";
 import { HttpClientTestingModule } from "@angular/common/http/testing";
 import { WorkflowWebsocketService } from 
"../workflow-websocket/workflow-websocket.service";
 import { WorkflowActionService } from 
"../workflow-graph/model/workflow-action.service";
@@ -126,4 +126,52 @@ describe("WorkflowResultExportService", () => {
   it("should be created", () => {
     expect(service).toBeTruthy();
   });
+
+  it("resetFlags clears the highlighted flag and resets the all-operators 
subject to false", () => {
+    service.hasResultToExportOnHighlightedOperators = true;
+    service.hasResultToExportOnAllOperators.next(true);
+
+    service.resetFlags();
+
+    expect(service.hasResultToExportOnHighlightedOperators).toBe(false);
+    expect(service.hasResultToExportOnAllOperators.value).toBe(false);
+  });
+});
+
+describe("WorkflowResultDownloadability", () => {
+  const restrictedMap = new Map<string, Set<string>>([
+    ["opB", new Set(["ds1 ([email protected])"])],
+    ["opD", new Set(["ds2 ([email protected])", "ds1 ([email protected])"])],
+  ]);
+  const ids = ["opA", "opB", "opC", "opD"];
+
+  it("getExportableOperatorIds keeps only operators absent from the restricted 
map", () => {
+    const downloadability = new WorkflowResultDownloadability(restrictedMap);
+    expect(downloadability.getExportableOperatorIds(ids)).toEqual(["opA", 
"opC"]);
+  });
+
+  it("getBlockedOperatorIds keeps only operators present in the restricted 
map", () => {
+    const downloadability = new WorkflowResultDownloadability(restrictedMap);
+    expect(downloadability.getBlockedOperatorIds(ids)).toEqual(["opB", "opD"]);
+  });
+
+  it("getBlockingDatasets returns the deduped union of blocking dataset 
labels", () => {
+    const downloadability = new WorkflowResultDownloadability(restrictedMap);
+    const result = downloadability.getBlockingDatasets(["opB", "opD"]);
+    // ds1 is shared by opB and opD, so the union must be deduped (length 2, 
not 3).
+    // The return builds a Set, so assert membership order-independently.
+    expect(result).toHaveLength(2);
+    expect([...result].sort()).toEqual(["ds1 ([email protected])", "ds2 
([email protected])"]);
+  });
+
+  it("getBlockingDatasets returns an empty array for unrestricted operators", 
() => {
+    const downloadability = new WorkflowResultDownloadability(restrictedMap);
+    expect(downloadability.getBlockingDatasets(["opA", "opC"])).toEqual([]);
+  });
+
+  it("treats every operator as exportable when the restricted map is empty", 
() => {
+    const downloadability = new WorkflowResultDownloadability(new Map());
+    expect(downloadability.getExportableOperatorIds(ids)).toEqual(ids);
+    expect(downloadability.getBlockedOperatorIds(ids)).toEqual([]);
+  });
 });
diff --git a/frontend/src/app/workspace/types/operator-schema.interface.spec.ts 
b/frontend/src/app/workspace/types/operator-schema.interface.spec.ts
new file mode 100644
index 0000000000..069f5bd78f
--- /dev/null
+++ b/frontend/src/app/workspace/types/operator-schema.interface.spec.ts
@@ -0,0 +1,133 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { OperatorSchema, areOperatorSchemasEqual } from 
"./operator-schema.interface";
+
+function schema(
+  overrides: {
+    operatorType?: string;
+    operatorVersion?: string;
+    jsonSchema?: object;
+    userFriendlyName?: string;
+    operatorGroupName?: string;
+    operatorDescription?: string;
+    supportReconfiguration?: boolean;
+    allowPortCustomization?: boolean;
+    inputPorts?: { displayName?: string; disallowMultiLinks?: boolean }[];
+    outputPorts?: { displayName?: string }[];
+  } = {}
+): OperatorSchema {
+  return {
+    operatorType: overrides.operatorType ?? "ScanSource",
+    operatorVersion: overrides.operatorVersion ?? "1.0",
+    jsonSchema: (overrides.jsonSchema ?? {
+      type: "object",
+      properties: { a: { type: "string" }, b: { type: "number" } },
+    }) as unknown as OperatorSchema["jsonSchema"],
+    additionalMetadata: {
+      userFriendlyName: overrides.userFriendlyName ?? "Scan",
+      operatorGroupName: overrides.operatorGroupName ?? "Source",
+      operatorDescription: overrides.operatorDescription ?? "reads data",
+      inputPorts: overrides.inputPorts ?? [{ displayName: "in0", 
disallowMultiLinks: false }],
+      outputPorts: overrides.outputPorts ?? [{ displayName: "out0" }],
+      supportReconfiguration: overrides.supportReconfiguration ?? false,
+      allowPortCustomization: overrides.allowPortCustomization ?? false,
+    },
+  };
+}
+
+describe("areOperatorSchemasEqual", () => {
+  it("returns true for two identically-shaped schemas", () => {
+    expect(areOperatorSchemasEqual(schema(), schema())).toBe(true);
+  });
+
+  it("returns false when operatorType differs", () => {
+    expect(areOperatorSchemasEqual(schema(), schema({ operatorType: 
"CSVFileScan" }))).toBe(false);
+  });
+
+  it("returns false when operatorVersion differs", () => {
+    expect(areOperatorSchemasEqual(schema(), schema({ operatorVersion: "2.0" 
}))).toBe(false);
+  });
+
+  it("returns false when a nested jsonSchema field differs", () => {
+    const other = schema({ jsonSchema: { type: "object", properties: { a: { 
type: "integer" } } } });
+    expect(areOperatorSchemasEqual(schema(), other)).toBe(false);
+  });
+
+  it("returns false when jsonSchema arrays differ only in order", () => {
+    const a = schema({ jsonSchema: { type: "object", required: ["a", "b"] } });
+    const b = schema({ jsonSchema: { type: "object", required: ["b", "a"] } });
+    expect(areOperatorSchemasEqual(a, b)).toBe(false);
+  });
+
+  it("returns false when additionalMetadata.userFriendlyName differs", () => {
+    expect(areOperatorSchemasEqual(schema(), schema({ userFriendlyName: 
"Scanner" }))).toBe(false);
+  });
+
+  it("returns false when additionalMetadata.operatorGroupName differs", () => {
+    expect(areOperatorSchemasEqual(schema(), schema({ operatorGroupName: 
"Analysis" }))).toBe(false);
+  });
+
+  it("returns false when a boolean metadata flag differs", () => {
+    expect(areOperatorSchemasEqual(schema(), schema({ supportReconfiguration: 
true }))).toBe(false);
+  });
+
+  it("returns false when additionalMetadata.operatorDescription differs", () 
=> {
+    expect(areOperatorSchemasEqual(schema(), schema({ operatorDescription: 
"different" }))).toBe(false);
+  });
+
+  it("returns false when additionalMetadata.allowPortCustomization differs", 
() => {
+    expect(areOperatorSchemasEqual(schema(), schema({ allowPortCustomization: 
true }))).toBe(false);
+  });
+
+  it("returns false when the number of input ports differs", () => {
+    const other = schema({ inputPorts: [{ displayName: "in0" }, { displayName: 
"in1" }] });
+    expect(areOperatorSchemasEqual(schema(), other)).toBe(false);
+  });
+
+  it("returns false when an input port's displayName differs", () => {
+    expect(areOperatorSchemasEqual(schema(), schema({ inputPorts: [{ 
displayName: "renamed" }] }))).toBe(false);
+  });
+
+  it("returns false when an input port's disallowMultiLinks differs", () => {
+    const other = schema({ inputPorts: [{ displayName: "in0", 
disallowMultiLinks: true }] });
+    expect(areOperatorSchemasEqual(schema(), other)).toBe(false);
+  });
+
+  it("returns false when input ports match as a set but differ in order", () 
=> {
+    const a = schema({ inputPorts: [{ displayName: "in0" }, { displayName: 
"in1" }] });
+    const b = schema({ inputPorts: [{ displayName: "in1" }, { displayName: 
"in0" }] });
+    expect(areOperatorSchemasEqual(a, b)).toBe(false);
+  });
+
+  it("returns false when the number of output ports differs", () => {
+    const other = schema({ outputPorts: [{ displayName: "out0" }, { 
displayName: "out1" }] });
+    expect(areOperatorSchemasEqual(schema(), other)).toBe(false);
+  });
+
+  it("returns false when an output port's displayName differs", () => {
+    expect(areOperatorSchemasEqual(schema(), schema({ outputPorts: [{ 
displayName: "renamed" }] }))).toBe(false);
+  });
+
+  it("returns false when output ports match as a set but differ in order", () 
=> {
+    const a = schema({ outputPorts: [{ displayName: "out0" }, { displayName: 
"out1" }] });
+    const b = schema({ outputPorts: [{ displayName: "out1" }, { displayName: 
"out0" }] });
+    expect(areOperatorSchemasEqual(a, b)).toBe(false);
+  });
+});

Reply via email to