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 bde8e88971 test(frontend): add unit tests for datasetVersionFileTree
(#6651)
bde8e88971 is described below
commit bde8e88971c33b3e3948f96ef8d86e68b7713962
Author: Matthew B. <[email protected]>
AuthorDate: Mon Jul 20 15:27:53 2026 -0700
test(frontend): add unit tests for datasetVersionFileTree (#6651)
### What changes were proposed in this PR?
- Add `frontend/src/app/common/type/datasetVersionFileTree.spec.ts`, a
new Vitest spec for the dataset version file tree helpers, which
previously had no dedicated unit tests.
- Cover getFullPathFromDatasetFileNode and
getRelativePathFromDatasetFileNode (three-segment strip, empty-segment
filtering, and the <=3 returns empty case).
- Cover recursive getPathsUnderOrEqualDatasetFileNode for file,
nested-dir, and empty-dir nodes.
- Cover DatasetVersionFileTreeManager: nested build, add idempotency,
siblings, and removal by identity and by path including subtree, absent,
synthetic-root guard, and double-removal no-ops.
### Any related issues, documentation, discussions?
Closes: #6650
### How was this PR tested?
- Run: `cd frontend && node --max-old-space-size=8192
./node_modules/nx/dist/bin/nx.js test gui --watch=false
--include=src/app/common/type/datasetVersionFileTree.spec.ts`, expect
all 22 tests passing.
- Test-only change; no production code is modified.
### Was this PR authored or co-authored using generative AI tooling?
Co-authored with Claude Opus 4.8 in compliance with ASF
---------
Signed-off-by: Matthew B. <[email protected]>
Co-authored-by: Copilot Autofix powered by AI
<[email protected]>
---
.../app/common/type/datasetVersionFileTree.spec.ts | 241 +++++++++++++++++++++
1 file changed, 241 insertions(+)
diff --git a/frontend/src/app/common/type/datasetVersionFileTree.spec.ts
b/frontend/src/app/common/type/datasetVersionFileTree.spec.ts
new file mode 100644
index 0000000000..76aa8f286d
--- /dev/null
+++ b/frontend/src/app/common/type/datasetVersionFileTree.spec.ts
@@ -0,0 +1,241 @@
+/**
+ * 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 {
+ DatasetFileNode,
+ DatasetVersionFileTreeManager,
+ getFullPathFromDatasetFileNode,
+ getPathsUnderOrEqualDatasetFileNode,
+ getRelativePathFromDatasetFileNode,
+} from "./datasetVersionFileTree";
+
+describe("getFullPathFromDatasetFileNode", () => {
+ it("joins parentDir and name with a slash", () => {
+ const node: DatasetFileNode = { name: "c.txt", type: "file", parentDir:
"/a/b" };
+ expect(getFullPathFromDatasetFileNode(node)).toBe("/a/b/c.txt");
+ });
+
+ it("produces a leading slash for a node whose parentDir is empty", () => {
+ const node: DatasetFileNode = { name: "root", type: "directory",
parentDir: "" };
+ expect(getFullPathFromDatasetFileNode(node)).toBe("/root");
+ });
+});
+
+describe("getRelativePathFromDatasetFileNode", () => {
+ it("strips the first three path segments", () => {
+ const node: DatasetFileNode = { name: "file.csv", type: "file", parentDir:
"/owner/dataset/v1" };
+ // full path is /owner/dataset/v1/file.csv -> segments [owner, dataset,
v1, file.csv]
+ expect(getRelativePathFromDatasetFileNode(node)).toBe("file.csv");
+ });
+
+ it("preserves nested relative segments beyond the first three", () => {
+ const node: DatasetFileNode = { name: "f.txt", type: "file", parentDir:
"/owner/dataset/v1/sub/dir" };
+ expect(getRelativePathFromDatasetFileNode(node)).toBe("sub/dir/f.txt");
+ });
+
+ it("returns an empty string when there are three or fewer segments", () => {
+ const node: DatasetFileNode = { name: "v1", type: "directory", parentDir:
"/owner/dataset" };
+ // full path /owner/dataset/v1 -> exactly 3 segments -> no relative path
+ expect(getRelativePathFromDatasetFileNode(node)).toBe("");
+ });
+
+ it("ignores empty segments from duplicate slashes when counting", () => {
+ const node: DatasetFileNode = { name: "file.csv", type: "file", parentDir:
"/owner//dataset/v1" };
+ // empty segment between the duplicate slashes is filtered out, leaving 4
real segments
+ expect(getRelativePathFromDatasetFileNode(node)).toBe("file.csv");
+ });
+});
+
+describe("getPathsUnderOrEqualDatasetFileNode", () => {
+ it("returns the single path for a file node", () => {
+ const file: DatasetFileNode = { name: "a.txt", type: "file", parentDir:
"/dir" };
+ expect(getPathsUnderOrEqualDatasetFileNode(file)).toEqual(["/dir/a.txt"]);
+ });
+
+ it("collects every file path under a directory", () => {
+ const file1: DatasetFileNode = { name: "file1.txt", type: "file",
parentDir: "/dir" };
+ const file2: DatasetFileNode = { name: "file2.txt", type: "file",
parentDir: "/dir" };
+ const dir: DatasetFileNode = { name: "dir", type: "directory", parentDir:
"", children: [file1, file2] };
+
expect(getPathsUnderOrEqualDatasetFileNode(dir)).toEqual(["/dir/file1.txt",
"/dir/file2.txt"]);
+ });
+
+ it("recurses into nested directories", () => {
+ const deepFile: DatasetFileNode = { name: "deep.txt", type: "file",
parentDir: "/a/b" };
+ const subDir: DatasetFileNode = { name: "b", type: "directory", parentDir:
"/a", children: [deepFile] };
+ const topDir: DatasetFileNode = { name: "a", type: "directory", parentDir:
"", children: [subDir] };
+
expect(getPathsUnderOrEqualDatasetFileNode(topDir)).toEqual(["/a/b/deep.txt"]);
+ });
+
+ it("returns an empty array for an empty directory", () => {
+ const emptyChildren: DatasetFileNode = { name: "dir", type: "directory",
parentDir: "", children: [] };
+ const noChildrenProp: DatasetFileNode = { name: "dir", type: "directory",
parentDir: "" };
+ expect(getPathsUnderOrEqualDatasetFileNode(emptyChildren)).toEqual([]);
+ expect(getPathsUnderOrEqualDatasetFileNode(noChildrenProp)).toEqual([]);
+ });
+});
+
+describe("DatasetVersionFileTreeManager", () => {
+ describe("addNodeWithPath", () => {
+ it("starts with no root nodes", () => {
+ const manager = new DatasetVersionFileTreeManager();
+ expect(manager.getRootNodes()).toEqual([]);
+ });
+
+ it("builds the intermediate directory structure and returns the leaf file
node", () => {
+ const manager = new DatasetVersionFileTreeManager();
+ const leaf = manager.addNodeWithPath("/a/b/c.txt");
+
+ expect(leaf.name).toBe("c.txt");
+ expect(leaf.type).toBe("file");
+ expect(getFullPathFromDatasetFileNode(leaf)).toBe("/a/b/c.txt");
+
+ const roots = manager.getRootNodes();
+ expect(roots.length).toBe(1);
+ expect(roots[0].name).toBe("a");
+ expect(roots[0].type).toBe("directory");
+
+ const dirB = roots[0].children![0];
+ expect(dirB.name).toBe("b");
+ expect(dirB.type).toBe("directory");
+ expect(dirB.children![0]).toBe(leaf);
+ });
+
+ it("is idempotent when adding the same path twice", () => {
+ const manager = new DatasetVersionFileTreeManager();
+ const first = manager.addNodeWithPath("/a/b/c.txt");
+ const second = manager.addNodeWithPath("/a/b/c.txt");
+
+ expect(second).toBe(first);
+ expect(manager.getRootNodes().length).toBe(1);
+ const dirB = manager.getRootNodes()[0].children![0];
+ expect(dirB.children!.length).toBe(1);
+ });
+
+ it("adds siblings under an existing directory", () => {
+ const manager = new DatasetVersionFileTreeManager();
+ manager.addNodeWithPath("/a/b/c.txt");
+ manager.addNodeWithPath("/a/b/d.txt");
+
+ const dirB = manager.getRootNodes()[0].children![0];
+ expect(dirB.children!.map(child => child.name).sort()).toEqual(["c.txt",
"d.txt"]);
+ });
+
+ it("handles paths without a leading slash", () => {
+ const manager = new DatasetVersionFileTreeManager();
+ const leaf = manager.addNodeWithPath("x/y.txt");
+ expect(getFullPathFromDatasetFileNode(leaf)).toBe("/x/y.txt");
+ expect(manager.getRootNodes()[0].name).toBe("x");
+ });
+ });
+
+ describe("initializeWithRootNodes / constructor", () => {
+ it("exposes provided root nodes", () => {
+ const dir: DatasetFileNode = {
+ name: "dir",
+ type: "directory",
+ parentDir: "/",
+ children: [{ name: "f.txt", type: "file", parentDir: "/dir" }],
+ };
+ const manager = new DatasetVersionFileTreeManager([dir]);
+ expect(manager.getRootNodes()).toEqual([dir]);
+ });
+ });
+
+ describe("removeNode", () => {
+ it("removes a leaf node found by identity via BFS", () => {
+ const manager = new DatasetVersionFileTreeManager();
+ const leaf = manager.addNodeWithPath("/a/b/c.txt");
+ const dirB = manager.getRootNodes()[0].children![0];
+
+ manager.removeNode(leaf);
+ expect(dirB.children).toEqual([]);
+ });
+
+ it("removes a whole subtree when removing an inner directory", () => {
+ const manager = new DatasetVersionFileTreeManager();
+ manager.addNodeWithPath("/a/b/c.txt");
+ const rootA = manager.getRootNodes()[0];
+
+ manager.removeNode(rootA);
+ expect(manager.getRootNodes()).toEqual([]);
+ });
+
+ it("does nothing for a node that is not present in the tree", () => {
+ const manager = new DatasetVersionFileTreeManager();
+ manager.addNodeWithPath("/a/b/c.txt");
+ const stranger: DatasetFileNode = { name: "z.txt", type: "file",
parentDir: "/q" };
+
+ manager.removeNode(stranger);
+ expect(manager.getRootNodes().length).toBe(1);
+ expect(manager.getRootNodes()[0].children![0].children!.length).toBe(1);
+ });
+
+ it("refuses to remove the synthetic root node", () => {
+ const manager = new DatasetVersionFileTreeManager();
+ manager.addNodeWithPath("/a/b/c.txt");
+ const fakeRoot: DatasetFileNode = { name: "/", type: "directory",
parentDir: "" };
+
+ manager.removeNode(fakeRoot);
+ expect(manager.getRootNodes().length).toBe(1);
+ });
+ });
+
+ describe("removeNodeWithPath", () => {
+ it("removes a node from its parent's children and the internal map", () =>
{
+ const file1: DatasetFileNode = { name: "file1.txt", type: "file",
parentDir: "/dir" };
+ const file2: DatasetFileNode = { name: "file2.txt", type: "file",
parentDir: "/dir" };
+ const dir: DatasetFileNode = { name: "dir", type: "directory",
parentDir: "/", children: [file1, file2] };
+ const manager = new DatasetVersionFileTreeManager([dir]);
+
+ manager.removeNodeWithPath("/dir/file1.txt");
+ expect(dir.children!.map(child => child.name)).toEqual(["file2.txt"]);
+
+ // A second removal of the same (now absent) path is a no-op.
+ manager.removeNodeWithPath("/dir/file1.txt");
+ expect(dir.children!.map(child => child.name)).toEqual(["file2.txt"]);
+ });
+
+ it("removes a whole subtree when removing a directory path", () => {
+ const file: DatasetFileNode = { name: "f.txt", type: "file", parentDir:
"/dir/sub" };
+ const subDir: DatasetFileNode = { name: "sub", type: "directory",
parentDir: "/dir", children: [file] };
+ const dir: DatasetFileNode = { name: "dir", type: "directory",
parentDir: "/", children: [subDir] };
+ const manager = new DatasetVersionFileTreeManager([dir]);
+
+ manager.removeNodeWithPath("/dir/sub");
+ expect(dir.children).toEqual([]);
+
+ // Removing a descendant path after the subtree is gone is also a no-op.
+ manager.removeNodeWithPath("/dir/sub/f.txt");
+ expect(dir.children).toEqual([]);
+ });
+
+ it("does nothing for an unknown path", () => {
+ const dir: DatasetFileNode = {
+ name: "dir",
+ type: "directory",
+ parentDir: "/",
+ children: [{ name: "f.txt", type: "file", parentDir: "/dir" }],
+ };
+ const manager = new DatasetVersionFileTreeManager([dir]);
+
+ manager.removeNodeWithPath("/does/not/exist");
+ expect(dir.children!.length).toBe(1);
+ });
+ });
+});