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/20 06:23:20 UTC

(texera) branch gh-readonly-queue/main/pr-6570-547d5051aade41c2fb0a38c6c0d714967a6c502f created (now 8937f71c61)

This is an automated email from the ASF dual-hosted git repository.

github-merge-queue[bot] pushed a change to branch gh-readonly-queue/main/pr-6570-547d5051aade41c2fb0a38c6c0d714967a6c502f
in repository https://gitbox.apache.org/repos/asf/texera.git


      at 8937f71c61 test(frontend): add unit test coverage for WorkflowResultExportService, WorkflowExecutionsService, and areOperatorSchemasEqual (#6570)

This branch includes the following new commits:

     new 8937f71c61 test(frontend): add unit test coverage for WorkflowResultExportService, WorkflowExecutionsService, and areOperatorSchemasEqual (#6570)

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(texera) 01/01: test(frontend): add unit test coverage for WorkflowResultExportService, WorkflowExecutionsService, and areOperatorSchemasEqual (#6570)

Posted by gi...@apache.org.
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 <me...@uci.edu>
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 (a@x.com)"])],
+    ["opD", new Set(["ds2 (b@y.com)", "ds1 (a@x.com)"])],
+  ]);
+  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 (a@x.com)", "ds2 (b@y.com)"]);
+  });
+
+  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);
+  });
+});