diff --git a/shell/components/fleet/FleetGitRepoPaths.vue b/shell/components/fleet/FleetGitRepoPaths.vue index bea1d51a24..b3b87ea40f 100644 --- a/shell/components/fleet/FleetGitRepoPaths.vue +++ b/shell/components/fleet/FleetGitRepoPaths.vue @@ -35,7 +35,7 @@ function _cl(str: string) { return trim; } -function getRelevantPrefixes(paths: string[]): string[] { +export function getRelevantPrefixes(paths: string[]): string[] { const prefixes: string[] = []; getPrefixesRecursive(prefixes, paths, '', { name: '', children: pathArrayToTree(paths) }); diff --git a/shell/components/fleet/__tests__/FleetGitRepoPaths.test.ts b/shell/components/fleet/__tests__/FleetGitRepoPaths.test.ts new file mode 100644 index 0000000000..fe133dd38c --- /dev/null +++ b/shell/components/fleet/__tests__/FleetGitRepoPaths.test.ts @@ -0,0 +1,103 @@ +import { getRelevantPrefixes } from '@shell/components/fleet/FleetGitRepoPaths.vue'; + +describe('fx: getRelevantPrefixes', () => { + it('should return an empty array for an empty input array', () => { + const paths: string[] = []; + + expect(getRelevantPrefixes(paths)).toStrictEqual([]); + }); + + it('should return the single path if only one is provided', () => { + const paths: string[] = ['aaa']; + + expect(getRelevantPrefixes(paths)).toStrictEqual(['aaa']); + }); + + it('should return only 2nd level prefixes', () => { + const paths: string[] = [ + 'folderA/aaa', + 'folderA/subfolderB/bbb', + 'folderC/ccc' + ]; + + const res = getRelevantPrefixes(paths); + + expect(res).toStrictEqual([ + 'folderC', + 'folderA/subfolderB', + 'folderA' + ]); + }); + + it('should return common prefix between 2 paths with same prefix', () => { + const paths: string[] = [ + 'level1/level2', + 'level1/level2/aaa' + ]; + + const res = getRelevantPrefixes(paths); + + expect(res).toStrictEqual([ + 'level1/level2' + ]); + }); + + it('should return common prefix between 2 paths with same low level prefix', () => { + const paths: string[] = [ + 'aaa/bbb/ccc', + 'aaa/bbb/eee/fff' + ]; + + const res = getRelevantPrefixes(paths); + + expect(res).toStrictEqual([ + 'aaa/bbb/eee', + 'aaa/bbb' + ]); + }); + + it('should return all original paths if they are leaves or satisfy the one-child condition', () => { + const paths: string[] = [ + 'aaa', + 'bbb', + 'ccc' + ]; + + const res = getRelevantPrefixes(paths); + + expect(res).toStrictEqual(['ccc', 'bbb', 'aaa']); + }); + + it('should handle multiple prefixes', () => { + const paths: string[] = [ + 'root/file1.txt', + 'root/file2.txt', + 'root/file3.txt' + ]; + + const res = getRelevantPrefixes(paths); + + expect(res).toStrictEqual([ + 'root' + ]); + }); + + it('should add leaf nodes and single-child original paths, not grouping parents', () => { + const paths: string[] = [ + 'driven/kustomize/path1', + 'driven/kustomize/path2', + 'driven/kustomize', + 'driven/simple', + 'driven/helm' + ]; + + const res = getRelevantPrefixes(paths); + + expect(res).toStrictEqual([ + 'driven/kustomize', + 'driven', + ].sort((a, b) => b.localeCompare(a))); + }); +}); + +describe.skip('test UI elements from YAML resource', () => {}); diff --git a/shell/edit/__tests__/fleet.cattle.io.gitrepo.test.ts b/shell/edit/__tests__/fleet.cattle.io.gitrepo.test.ts index e8fa291ac5..a5331eeb94 100644 --- a/shell/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +++ b/shell/edit/__tests__/fleet.cattle.io.gitrepo.test.ts @@ -232,4 +232,6 @@ describe.each([ expect(pollingIntervalWebhookWarning.exists()).toBe(visible); }); + + it.todo('test paths and subpaths'); }); diff --git a/shell/utils/__tests__/string.test.ts b/shell/utils/__tests__/string.test.ts index f3f327babf..db04681343 100644 --- a/shell/utils/__tests__/string.test.ts +++ b/shell/utils/__tests__/string.test.ts @@ -1,4 +1,4 @@ -import { decodeHtml, resourceNames } from '@shell/utils/string'; +import { decodeHtml, resourceNames, pathArrayToTree } from '@shell/utils/string'; describe('fx: decodeHtml', () => { it('should decode HTML values from escaped string into valid markup', () => { @@ -89,3 +89,275 @@ describe('fx: resourceNames', () => { }); }); }); + +describe('fx: pathArrayToTree', () => { + interface TreeNode { + name: string; + children: TreeNode[]; + } + + function sortTree(nodes: TreeNode[]): void { + if (!nodes) return; + nodes.sort((a, b) => a.name.localeCompare(b.name)); + nodes.forEach((node) => { + if (node.children && node.children.length > 0) { + sortTree(node.children); + } + }); + } + + it('should return an empty array for an empty input array', () => { + const paths: string[] = []; + + expect(pathArrayToTree(paths)).toStrictEqual([]); + }); + + it('should convert simple single-level paths correctly', () => { + const paths: string[] = [ + 'file1.txt', + 'folderA', + 'itemB' + ]; + const expected: TreeNode[] = [ + { name: 'file1.txt', children: [] }, + { name: 'folderA', children: [] }, + { name: 'itemB', children: [] }, + ]; + const actual = pathArrayToTree(paths); + + sortTree(actual); // Sort the actual output for deterministic comparison + expect(actual).toStrictEqual(expected); + }); + + it('should convert two-level nested paths correctly', () => { + const paths: string[] = [ + 'folderA/file1.txt', + 'folderC/file3.txt' + ]; + const expected: TreeNode[] = [ + { + name: 'folderA', + children: [ + { name: 'file1.txt', children: [] }, + ] + }, + { + name: 'folderC', + children: [ + { name: 'file3.txt', children: [] } + ] + }, + ]; + const actual = pathArrayToTree(paths); + + sortTree(actual); + expect(actual).toStrictEqual(expected); + }); + + it('should handle deep nesting and common prefixes correctly', () => { + const paths: string[] = [ + 'root/level1/level2/file.doc', + 'root/level1/another_file.txt', + 'root/diff_level1/config.json' + ]; + const expected: TreeNode[] = [ + { + name: 'root', + children: [ + { + name: 'diff_level1', + children: [ + { name: 'config.json', children: [] } + ] + }, + { + name: 'level1', + children: [ + { name: 'another_file.txt', children: [] }, + { + name: 'level2', + children: [ + { name: 'file.doc', children: [] } + ] + } + ] + } + ] + } + ]; + const actual = pathArrayToTree(paths); + + sortTree(actual); + expect(actual).toStrictEqual(expected); + }); + + it('should handle leading/trailing/multiple slashes by treating them as path segments', () => { + const paths: string[] = [ + '/path/to/file.txt', // Starts with a slash + 'path/to/another.txt/', // Ends with a slash (adds an empty segment) + '//root/item//subitem//', // Multiple slashes (adds empty segments) + 'root/item/another_subitem' + ]; + const expected: TreeNode[] = [ + { + name: '', + children: [ + { + name: '', + children: [ + { + name: 'root', + children: [ + { + name: 'item', + children: [ + { + name: '', + children: [ + { + name: 'subitem', + children: [ + { + name: '', + children: [ + { + name: '', + children: [ + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + { + name: 'path', + children: [ + { + name: 'to', + children: [ + { + name: 'file.txt', + children: [ + ], + }, + ], + }, + ], + }, + ], + }, + { + name: 'path', + children: [ + { + name: 'to', + children: [ + { + name: 'another.txt', + children: [ + { + name: '', + children: [ + ], + }, + ], + }, + ], + }, + ], + }, + { + name: 'root', + children: [ + { + name: 'item', + children: [ + { + name: 'another_subitem', + children: [ + ], + }, + ], + }, + ], + }, + ]; + const actual = pathArrayToTree(paths); + + sortTree(actual); + expect(actual).toStrictEqual(expected); + }); + + it('should produce the same structure regardless of input order', () => { + const paths1: string[] = ['a/b', 'a/c']; + const paths2: string[] = ['a/c', 'a/b']; + + const actual1 = pathArrayToTree(paths1); + + sortTree(actual1); + const actual2 = pathArrayToTree(paths2); + + sortTree(actual2); + + expect(actual1).toStrictEqual(actual2); + }); + + it('should handle names with special characters (not slashes) correctly', () => { + const paths: string[] = [ + 'my-folder/file_name.1.txt', + 'another folder/item with spaces' + ]; + const expected: TreeNode[] = [ + { + name: 'another folder', + children: [ + { name: 'item with spaces', children: [] } + ] + }, + { + name: 'my-folder', + children: [ + { name: 'file_name.1.txt', children: [] } + ] + } + ]; + const actual = pathArrayToTree(paths); + + sortTree(actual); + expect(actual).toStrictEqual(expected); + }); + + it('should handle duplicate paths correctly, only adding unique structure', () => { + const paths: string[] = [ + 'folder/file.txt', + 'folder/file.txt', // Duplicate + 'other_folder/another_file.txt' + ]; + const expected: TreeNode[] = [ + { + name: 'folder', + children: [ + { name: 'file.txt', children: [] } + ] + }, + { + name: 'other_folder', + children: [ + { name: 'another_file.txt', children: [] } + ] + } + ]; + const actual = pathArrayToTree(paths); + + sortTree(actual); + expect(actual).toStrictEqual(expected); + }); +});