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:05:56 UTC
(texera) 01/01: test(frontend): add unit test coverage for WorkflowResultExportService, WorkflowExecutionsService, and areOperatorSchemasEqual (#6570)
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-028cbd6b9b57cc2e09d56dab1a1b8d6c1f32904b
in repository https://gitbox.apache.org/repos/asf/texera.git
commit 4b0bbc746056cde617086cca8186024f140b8b15
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);
+ });
+});