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 2e5c99c77e test(frontend): add unit test coverage for dashboard
type-predicates (#6425)
2e5c99c77e is described below
commit 2e5c99c77ef53b1ed2dc0548e31d5313da1d4daf
Author: Eugene Gu <[email protected]>
AuthorDate: Tue Jul 14 17:59:44 2026 -0700
test(frontend): add unit test coverage for dashboard type-predicates (#6425)
### What changes were proposed in this PR?
Adds `type-predicates.spec.ts` to cover the five previously untested
type guards in `dashboard/type/type-predicates.ts`
(`isDashboardWorkflow`, `isDashboardProject`, `isDashboardFile`,
`isDashboardDataset`, `isDashboardWorkflowComputingUnit`), which
`DashboardEntry` uses to classify every dashboard and search-result item
through an ordered if/else dispatch. The spec uses fully-typed realistic
fixtures for each of the five interfaces and covers the positive path,
null/undefined inputs (asserted with `toBeFalsy`, since the guards
return the falsy input itself via the `value && ...` short-circuit),
wrong-shape negatives (missing or mistyped key fields), and the
`isDashboardProject` `!value.workflow` exclusion branch, which was never
exercised anywhere in the test tree.
Five tests deliberately pin a known quirk as current behavior: because
`typeof null === "object"`, a null
`workflow`/`file`/`dataset`/`computingUnit` field passes its guard. Each
is titled "(current behavior)" with an explanatory comment, so a future
fix can flip these assertions intentionally rather than discover them by
surprise.
A generated 5x5 cross-classification matrix (25 tests) runs every
fixture through every predicate and asserts only the matching one
returns true, protecting the mutual-exclusivity assumption that
`DashboardEntry`'s dispatch chain relies on.
No production code was changed.
### Any related issues, documentation, discussions?
Closes #6400
### How was this PR tested?
Added 53 new unit tests in `type-predicates.spec.ts` (28 per-predicate
cases plus the 25-cell cross-classification matrix). Ran locally via
`yarn ng test --watch=false
--include='**/dashboard/type/type-predicates.spec.ts'`; all 53 pass.
### Was this PR authored or co-authored using generative AI tooling?
Co-authored using Claude (Fable 5).
---------
Signed-off-by: Xinyuan Lin <[email protected]>
Co-authored-by: Xinyuan Lin <[email protected]>
Co-authored-by: Copilot Autofix powered by AI
<[email protected]>
---
.../src/app/dashboard/type/type-predicates.spec.ts | 305 +++++++++++++++++++++
1 file changed, 305 insertions(+)
diff --git a/frontend/src/app/dashboard/type/type-predicates.spec.ts
b/frontend/src/app/dashboard/type/type-predicates.spec.ts
new file mode 100644
index 0000000000..1557010023
--- /dev/null
+++ b/frontend/src/app/dashboard/type/type-predicates.spec.ts
@@ -0,0 +1,305 @@
+/**
+ * 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 {
+ isDashboardDataset,
+ isDashboardFile,
+ isDashboardProject,
+ isDashboardWorkflow,
+ isDashboardWorkflowComputingUnit,
+} from "./type-predicates";
+import { DashboardWorkflow } from "./dashboard-workflow.interface";
+import { DashboardProject } from "./dashboard-project.interface";
+import { DashboardFile } from "./dashboard-file.interface";
+import { DashboardDataset } from "./dashboard-dataset.interface";
+import { DashboardWorkflowComputingUnit } from
"../../common/type/workflow-computing-unit";
+import { ExecutionMode } from "../../common/type/workflow";
+
+const workflowFixture: DashboardWorkflow = {
+ isOwner: true,
+ ownerName: "Alice",
+ workflow: {
+ content: {
+ operators: [],
+ operatorPositions: {},
+ links: [],
+ commentBoxes: [],
+ settings: {
+ dataTransferBatchSize: 400,
+ executionMode: ExecutionMode.PIPELINED,
+ },
+ },
+ name: "My Workflow",
+ description: "A sample workflow",
+ wid: 1,
+ creationTime: 1700000000000,
+ lastModifiedTime: 1700000001000,
+ isPublished: 0,
+ readonly: false,
+ },
+ projectIDs: [1, 2],
+ accessLevel: "WRITE",
+ ownerId: 10,
+ coverImage: null,
+};
+
+const projectFixture: DashboardProject = {
+ pid: 5,
+ name: "My Project",
+ description: "A sample project",
+ ownerId: 10,
+ creationTime: 1700000000000,
+ color: "#ff0000",
+ accessLevel: "WRITE",
+};
+
+const fileFixture: DashboardFile = {
+ ownerEmail: "[email protected]",
+ accessLevel: "READ",
+ file: {
+ ownerUid: 10,
+ fid: 7,
+ size: 1024,
+ name: "data.csv",
+ path: "/files/data.csv",
+ description: "A sample file",
+ uploadTime: 1700000000000,
+ },
+};
+
+const datasetFixture: DashboardDataset = {
+ isOwner: false,
+ ownerEmail: "[email protected]",
+ dataset: {
+ did: 3,
+ ownerUid: 11,
+ name: "My Dataset",
+ isPublic: true,
+ isDownloadable: true,
+ storagePath: "/datasets/3",
+ description: "A sample dataset",
+ creationTime: 1700000000000,
+ coverImage: undefined,
+ },
+ accessPrivilege: "READ",
+ size: 2048,
+};
+
+const computingUnitFixture: DashboardWorkflowComputingUnit = {
+ computingUnit: {
+ cuid: 9,
+ uid: 10,
+ name: "My Computing Unit",
+ creationTime: 1700000000000,
+ terminateTime: undefined,
+ type: "kubernetes",
+ uri: "urn:texera:cu:9",
+ resource: {
+ cpuLimit: "2",
+ memoryLimit: "4Gi",
+ gpuLimit: "0",
+ jvmMemorySize: "2G",
+ shmSize: "64Mi",
+ nodeAddresses: ["10.0.0.1"],
+ },
+ },
+ status: "Running",
+ metrics: {
+ cpuUsage: "0.5",
+ memoryUsage: "1Gi",
+ },
+ isOwner: true,
+ accessPrivilege: "WRITE",
+ ownerGoogleAvatar: "",
+ ownerName: "Alice",
+};
+
+describe("isDashboardWorkflow", () => {
+ it("should return true for a realistic DashboardWorkflow", () => {
+ expect(isDashboardWorkflow(workflowFixture)).toBe(true);
+ });
+
+ // The guard returns the falsy input itself via the `value && ...`
short-circuit,
+ // so the result is null/undefined rather than the boolean false.
+ it("should be falsy for null and undefined", () => {
+ expect(isDashboardWorkflow(null)).toBeFalsy();
+ expect(isDashboardWorkflow(undefined)).toBeFalsy();
+ });
+
+ it("should return false for an object without a workflow field", () => {
+ expect(isDashboardWorkflow({})).toBe(false);
+ });
+
+ it("should return false when workflow is not an object", () => {
+ expect(isDashboardWorkflow({ workflow: "not an object" })).toBe(false);
+ });
+
+ it("should return true when workflow is null (current behavior)", () => {
+ // Documents current behavior: typeof null === "object", so a null field
passes the guard.
+ expect(isDashboardWorkflow({ workflow: null })).toBe(true);
+ });
+});
+
+describe("isDashboardProject", () => {
+ it("should return true for a realistic DashboardProject", () => {
+ expect(isDashboardProject(projectFixture)).toBe(true);
+ });
+
+ // The guard returns the falsy input itself via the `value && ...`
short-circuit,
+ // so the result is null/undefined rather than the boolean false.
+ it("should be falsy for null and undefined", () => {
+ expect(isDashboardProject(null)).toBeFalsy();
+ expect(isDashboardProject(undefined)).toBeFalsy();
+ });
+
+ it("should return false for an object without a name field", () => {
+ expect(isDashboardProject({})).toBe(false);
+ });
+
+ it("should return false when name is not a string", () => {
+ expect(isDashboardProject({ name: 42 })).toBe(false);
+ });
+
+ it("should return false when a workflow field is also present", () => {
+ expect(isDashboardProject({ name: "x", workflow: workflowFixture.workflow
})).toBe(false);
+ });
+
+ it("should return true when name is a string and workflow is null (current
behavior)", () => {
+ // Documents current behavior: `!value.workflow` is true for a null
workflow,
+ // so the exclusion branch does not reject it.
+ expect(isDashboardProject({ name: "x", workflow: null })).toBe(true);
+ });
+});
+
+describe("isDashboardFile", () => {
+ it("should return true for a realistic DashboardFile", () => {
+ expect(isDashboardFile(fileFixture)).toBe(true);
+ });
+
+ // The guard returns the falsy input itself via the `value && ...`
short-circuit,
+ // so the result is null/undefined rather than the boolean false.
+ it("should be falsy for null and undefined", () => {
+ expect(isDashboardFile(null)).toBeFalsy();
+ expect(isDashboardFile(undefined)).toBeFalsy();
+ });
+
+ it("should return false for an empty object", () => {
+ expect(isDashboardFile({})).toBe(false);
+ });
+
+ it("should return false when ownerEmail is missing", () => {
+ expect(isDashboardFile({ file: fileFixture.file })).toBe(false);
+ });
+
+ it("should return false when file is missing", () => {
+ expect(isDashboardFile({ ownerEmail: "[email protected]" })).toBe(false);
+ });
+
+ it("should return false when ownerEmail is not a string", () => {
+ expect(isDashboardFile({ ownerEmail: 42, file: fileFixture.file
})).toBe(false);
+ });
+
+ it("should return true when file is null (current behavior)", () => {
+ // Documents current behavior: typeof null === "object", so a null field
passes the guard.
+ expect(isDashboardFile({ ownerEmail: "[email protected]", file: null
})).toBe(true);
+ });
+});
+
+describe("isDashboardDataset", () => {
+ it("should return true for a realistic DashboardDataset", () => {
+ expect(isDashboardDataset(datasetFixture)).toBe(true);
+ });
+
+ // The guard returns the falsy input itself via the `value && ...`
short-circuit,
+ // so the result is null/undefined rather than the boolean false.
+ it("should be falsy for null and undefined", () => {
+ expect(isDashboardDataset(null)).toBeFalsy();
+ expect(isDashboardDataset(undefined)).toBeFalsy();
+ });
+
+ it("should return false for an object without a dataset field", () => {
+ expect(isDashboardDataset({})).toBe(false);
+ });
+
+ it("should return false when dataset is not an object", () => {
+ expect(isDashboardDataset({ dataset: "not an object" })).toBe(false);
+ });
+
+ it("should return true when dataset is null (current behavior)", () => {
+ // Documents current behavior: typeof null === "object", so a null field
passes the guard.
+ expect(isDashboardDataset({ dataset: null })).toBe(true);
+ });
+});
+
+describe("isDashboardWorkflowComputingUnit", () => {
+ it("should return true for a realistic DashboardWorkflowComputingUnit", ()
=> {
+ expect(isDashboardWorkflowComputingUnit(computingUnitFixture)).toBe(true);
+ });
+
+ // The guard returns the falsy input itself via the `value && ...`
short-circuit,
+ // so the result is null/undefined rather than the boolean false.
+ it("should be falsy for null and undefined", () => {
+ expect(isDashboardWorkflowComputingUnit(null)).toBeFalsy();
+ expect(isDashboardWorkflowComputingUnit(undefined)).toBeFalsy();
+ });
+
+ it("should return false for an object without a computingUnit field", () => {
+ expect(isDashboardWorkflowComputingUnit({})).toBe(false);
+ });
+
+ it("should return false when computingUnit is not an object", () => {
+ expect(isDashboardWorkflowComputingUnit({ computingUnit: "not an object"
})).toBe(false);
+ });
+
+ it("should return true when computingUnit is null (current behavior)", () =>
{
+ // Documents current behavior: typeof null === "object", so a null field
passes the guard.
+ expect(isDashboardWorkflowComputingUnit({ computingUnit: null
})).toBe(true);
+ });
+});
+
+// Every realistic fixture must satisfy exactly one predicate. DashboardEntry
relies on
+// this mutual exclusivity: its constructor dispatches through an ordered
if/else chain,
+// so a fixture matching a predicate other than its own would be classified
incorrectly.
+describe("type predicate cross-classification", () => {
+ const fixtures: ReadonlyArray<[string, unknown, string]> = [
+ ["DashboardWorkflow fixture", workflowFixture, "isDashboardWorkflow"],
+ ["DashboardProject fixture", projectFixture, "isDashboardProject"],
+ ["DashboardFile fixture", fileFixture, "isDashboardFile"],
+ ["DashboardDataset fixture", datasetFixture, "isDashboardDataset"],
+ ["DashboardWorkflowComputingUnit fixture", computingUnitFixture,
"isDashboardWorkflowComputingUnit"],
+ ];
+
+ const predicates: ReadonlyArray<[string, (value: unknown) => boolean]> = [
+ ["isDashboardWorkflow", isDashboardWorkflow],
+ ["isDashboardProject", isDashboardProject],
+ ["isDashboardFile", isDashboardFile],
+ ["isDashboardDataset", isDashboardDataset],
+ ["isDashboardWorkflowComputingUnit", isDashboardWorkflowComputingUnit],
+ ];
+
+ fixtures.forEach(([fixtureName, fixture, expectedPredicate]) => {
+ predicates.forEach(([predicateName, predicate]) => {
+ const expected = predicateName === expectedPredicate;
+ it(`${predicateName} should return ${expected} for the ${fixtureName}`,
() => {
+ // The it() title identifies the failing fixture/predicate pair.
+ expect(predicate(fixture)).toBe(expected);
+ });
+ });
+ });
+});