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-6425-38202faa6991dd22aa462d6a95d3fd6f7e611045
in repository https://gitbox.apache.org/repos/asf/texera.git

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

Reply via email to