diff --git a/docs/README.md b/docs/README.md index b5e4aa7..a33081a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,8 +1,8 @@ -**CTRF v0.0.15** +**CTRF v0.0.16** *** -# CTRF v0.0.15 +# CTRF v0.0.16 ## Enumerations @@ -21,22 +21,33 @@ - [Summary](interfaces/Summary.md) - [Test](interfaces/Test.md) - [TestInsights](interfaces/TestInsights.md) +- [TestTree](interfaces/TestTree.md) - [Tool](interfaces/Tool.md) +- [TreeNode](interfaces/TreeNode.md) +- [TreeOptions](interfaces/TreeOptions.md) - [ValidationResult](interfaces/ValidationResult.md) ## Type Aliases - [TestStatus](type-aliases/TestStatus.md) +- [TreeTest](type-aliases/TreeTest.md) ## Functions - [enrichReportWithInsights](functions/enrichReportWithInsights.md) +- [findSuiteByName](functions/findSuiteByName.md) +- [findTestByName](functions/findTestByName.md) +- [flattenTree](functions/flattenTree.md) +- [getAllTests](functions/getAllTests.md) +- [getSuiteStats](functions/getSuiteStats.md) - [isValidCtrfReport](functions/isValidCtrfReport.md) - [mergeReports](functions/mergeReports.md) +- [organizeTestsBySuite](functions/organizeTestsBySuite.md) - [readReportFromFile](functions/readReportFromFile.md) - [readReportsFromDirectory](functions/readReportsFromDirectory.md) - [readReportsFromGlobPattern](functions/readReportsFromGlobPattern.md) - [sortReportsByTimestamp](functions/sortReportsByTimestamp.md) - [storePreviousResults](functions/storePreviousResults.md) +- [traverseTree](functions/traverseTree.md) - [validateReport](functions/validateReport.md) - [validateReportStrict](functions/validateReportStrict.md) diff --git a/docs/enumerations/SortOrder.md b/docs/enumerations/SortOrder.md index 3e2be27..9ce1ef0 100644 --- a/docs/enumerations/SortOrder.md +++ b/docs/enumerations/SortOrder.md @@ -1,4 +1,4 @@ -[**CTRF v0.0.15**](../README.md) +[**CTRF v0.0.16**](../README.md) *** diff --git a/docs/functions/enrichReportWithInsights.md b/docs/functions/enrichReportWithInsights.md index 12e4224..acb717f 100644 --- a/docs/functions/enrichReportWithInsights.md +++ b/docs/functions/enrichReportWithInsights.md @@ -1,4 +1,4 @@ -[**CTRF v0.0.15**](../README.md) +[**CTRF v0.0.16**](../README.md) *** diff --git a/docs/functions/findSuiteByName.md b/docs/functions/findSuiteByName.md new file mode 100644 index 0000000..99e9a9e --- /dev/null +++ b/docs/functions/findSuiteByName.md @@ -0,0 +1,33 @@ +[**CTRF v0.0.16**](../README.md) + +*** + +[CTRF](../README.md) / findSuiteByName + +# Function: findSuiteByName() + +> **findSuiteByName**(`nodes`, `name`): `undefined` \| [`TreeNode`](../interfaces/TreeNode.md) + +Defined in: src/methods/tree-hierarchical-structure.ts:331 + +Utility function to find a suite by name in the tree + +## Parameters + +### nodes + +[`TreeNode`](../interfaces/TreeNode.md)[] + +Array of tree nodes to search + +### name + +`string` + +Name of the suite to find + +## Returns + +`undefined` \| [`TreeNode`](../interfaces/TreeNode.md) + +The found suite node or undefined diff --git a/docs/functions/findTestByName.md b/docs/functions/findTestByName.md new file mode 100644 index 0000000..edc0cc0 --- /dev/null +++ b/docs/functions/findTestByName.md @@ -0,0 +1,33 @@ +[**CTRF v0.0.16**](../README.md) + +*** + +[CTRF](../README.md) / findTestByName + +# Function: findTestByName() + +> **findTestByName**(`nodes`, `name`): `undefined` \| [`TreeTest`](../type-aliases/TreeTest.md) + +Defined in: src/methods/tree-hierarchical-structure.ts:354 + +Utility function to find a test by name in the tree + +## Parameters + +### nodes + +[`TreeNode`](../interfaces/TreeNode.md)[] + +Array of tree nodes to search + +### name + +`string` + +Name of the test to find + +## Returns + +`undefined` \| [`TreeTest`](../type-aliases/TreeTest.md) + +The found test or undefined diff --git a/docs/functions/flattenTree.md b/docs/functions/flattenTree.md new file mode 100644 index 0000000..8103501 --- /dev/null +++ b/docs/functions/flattenTree.md @@ -0,0 +1,28 @@ +[**CTRF v0.0.16**](../README.md) + +*** + +[CTRF](../README.md) / flattenTree + +# Function: flattenTree() + +> **flattenTree**(`nodes`): `object`[] + +Defined in: src/methods/tree-hierarchical-structure.ts:379 + +Utility function to convert tree to a flat array with indentation information +Useful for displaying the tree in a linear format + +## Parameters + +### nodes + +[`TreeNode`](../interfaces/TreeNode.md)[] + +Array of tree nodes to flatten + +## Returns + +`object`[] + +Array of objects containing node, depth, and nodeType information diff --git a/docs/functions/getAllTests.md b/docs/functions/getAllTests.md new file mode 100644 index 0000000..91568a5 --- /dev/null +++ b/docs/functions/getAllTests.md @@ -0,0 +1,27 @@ +[**CTRF v0.0.16**](../README.md) + +*** + +[CTRF](../README.md) / getAllTests + +# Function: getAllTests() + +> **getAllTests**(`nodes`): [`TreeTest`](../type-aliases/TreeTest.md)[] + +Defined in: src/methods/tree-hierarchical-structure.ts:403 + +Utility function to get all tests from the tree structure as a flat array + +## Parameters + +### nodes + +[`TreeNode`](../interfaces/TreeNode.md)[] + +Array of tree nodes to extract tests from + +## Returns + +[`TreeTest`](../type-aliases/TreeTest.md)[] + +Array of all tests in the tree diff --git a/docs/functions/getSuiteStats.md b/docs/functions/getSuiteStats.md new file mode 100644 index 0000000..905505a --- /dev/null +++ b/docs/functions/getSuiteStats.md @@ -0,0 +1,33 @@ +[**CTRF v0.0.16**](../README.md) + +*** + +[CTRF](../README.md) / getSuiteStats + +# Function: getSuiteStats() + +> **getSuiteStats**(`nodes`, `suitePath`): `undefined` \| [`Summary`](../interfaces/Summary.md) + +Defined in: src/methods/tree-hierarchical-structure.ts:422 + +Utility function to get statistics for a specific suite path + +## Parameters + +### nodes + +[`TreeNode`](../interfaces/TreeNode.md)[] + +Array of tree nodes to search + +### suitePath + +`string`[] + +Array representing the path to the suite + +## Returns + +`undefined` \| [`Summary`](../interfaces/Summary.md) + +Summary statistics for the suite or undefined if not found diff --git a/docs/functions/isValidCtrfReport.md b/docs/functions/isValidCtrfReport.md index 1a9c4ee..bc2c510 100644 --- a/docs/functions/isValidCtrfReport.md +++ b/docs/functions/isValidCtrfReport.md @@ -1,4 +1,4 @@ -[**CTRF v0.0.15**](../README.md) +[**CTRF v0.0.16**](../README.md) *** @@ -8,7 +8,7 @@ > **isValidCtrfReport**(`report`): `report is { reportFormat: "CTRF" }` -Defined in: src/methods/validate-schema.ts:27 +Defined in: [src/methods/validate-schema.ts:27](https://github.com/ctrf-io/ctrf-core-js/blob/main/src/methods/validate-schema.ts#L27) Simple check to verify if an object is a CTRF report by checking the reportFormat diff --git a/docs/functions/mergeReports.md b/docs/functions/mergeReports.md index e2ee5f2..a0024ef 100644 --- a/docs/functions/mergeReports.md +++ b/docs/functions/mergeReports.md @@ -1,4 +1,4 @@ -[**CTRF v0.0.15**](../README.md) +[**CTRF v0.0.16**](../README.md) *** diff --git a/docs/functions/organizeTestsBySuite.md b/docs/functions/organizeTestsBySuite.md new file mode 100644 index 0000000..5376cd9 --- /dev/null +++ b/docs/functions/organizeTestsBySuite.md @@ -0,0 +1,68 @@ +[**CTRF v0.0.16**](../README.md) + +*** + +[CTRF](../README.md) / organizeTestsBySuite + +# Function: organizeTestsBySuite() + +> **organizeTestsBySuite**(`tests`, `options`): [`TestTree`](../interfaces/TestTree.md) + +Defined in: src/methods/tree-hierarchical-structure.ts:92 + +Organizes CTRF tests into a hierarchical tree structure based on the suite property. + +The function handles array format (['suite1', 'suite2', 'suite3']) for the suite property +as defined in the CTRF schema. The output follows the CTRF Suite Tree schema specification. + +## Parameters + +### tests + +[`Test`](../interfaces/Test.md)[] + +Array of CTRF test objects + +### options + +[`TreeOptions`](../interfaces/TreeOptions.md) = `{}` + +Options for controlling tree creation + +## Returns + +[`TestTree`](../interfaces/TestTree.md) + +TestTree object containing the hierarchical structure and statistics + +## Example + +```typescript +import { organizeTestsBySuite } from 'ctrf-js-common' + +const tests = [ + { + name: 'should login successfully', + status: 'passed', + duration: 150, + suite: ['Authentication', 'Login'] + }, + { + name: 'should logout successfully', + status: 'passed', + duration: 100, + suite: ['Authentication', 'Logout'] + } +] + +const tree = organizeTestsBySuite(tests) + +// For structure-only without summary statistics: +// const tree = organizeTestsBySuite(tests, { includeSummary: false }) + +// Convert to JSON for machine consumption +const treeJson = JSON.stringify(tree, null, 2) + +console.log(tree.roots[0].name) // 'Authentication' +console.log(tree.roots[0].suites.length) // 2 (Login, Logout) +``` diff --git a/docs/functions/readReportFromFile.md b/docs/functions/readReportFromFile.md index ae771a5..3414003 100644 --- a/docs/functions/readReportFromFile.md +++ b/docs/functions/readReportFromFile.md @@ -1,4 +1,4 @@ -[**CTRF v0.0.15**](../README.md) +[**CTRF v0.0.16**](../README.md) *** diff --git a/docs/functions/readReportsFromDirectory.md b/docs/functions/readReportsFromDirectory.md index 335c9fe..ce71f3a 100644 --- a/docs/functions/readReportsFromDirectory.md +++ b/docs/functions/readReportsFromDirectory.md @@ -1,4 +1,4 @@ -[**CTRF v0.0.15**](../README.md) +[**CTRF v0.0.16**](../README.md) *** diff --git a/docs/functions/readReportsFromGlobPattern.md b/docs/functions/readReportsFromGlobPattern.md index c232a6a..51cf25d 100644 --- a/docs/functions/readReportsFromGlobPattern.md +++ b/docs/functions/readReportsFromGlobPattern.md @@ -1,4 +1,4 @@ -[**CTRF v0.0.15**](../README.md) +[**CTRF v0.0.16**](../README.md) *** diff --git a/docs/functions/sortReportsByTimestamp.md b/docs/functions/sortReportsByTimestamp.md index 3896aed..351213e 100644 --- a/docs/functions/sortReportsByTimestamp.md +++ b/docs/functions/sortReportsByTimestamp.md @@ -1,4 +1,4 @@ -[**CTRF v0.0.15**](../README.md) +[**CTRF v0.0.16**](../README.md) *** diff --git a/docs/functions/storePreviousResults.md b/docs/functions/storePreviousResults.md index eaa01ba..1abe317 100644 --- a/docs/functions/storePreviousResults.md +++ b/docs/functions/storePreviousResults.md @@ -1,4 +1,4 @@ -[**CTRF v0.0.15**](../README.md) +[**CTRF v0.0.16**](../README.md) *** diff --git a/docs/functions/traverseTree.md b/docs/functions/traverseTree.md new file mode 100644 index 0000000..23cba20 --- /dev/null +++ b/docs/functions/traverseTree.md @@ -0,0 +1,37 @@ +[**CTRF v0.0.16**](../README.md) + +*** + +[CTRF](../README.md) / traverseTree + +# Function: traverseTree() + +> **traverseTree**(`nodes`, `callback`, `depth`): `void` + +Defined in: src/methods/tree-hierarchical-structure.ts:302 + +Utility function to traverse the tree and apply a function to each node + +## Parameters + +### nodes + +[`TreeNode`](../interfaces/TreeNode.md)[] + +Array of tree nodes to traverse + +### callback + +(`node`, `depth`, `nodeType`) => `void` + +Function to call for each node (suites and tests) + +### depth + +`number` = `0` + +Current depth in the tree (starts at 0) + +## Returns + +`void` diff --git a/docs/functions/validateReport.md b/docs/functions/validateReport.md index 8ba1685..66506b6 100644 --- a/docs/functions/validateReport.md +++ b/docs/functions/validateReport.md @@ -1,4 +1,4 @@ -[**CTRF v0.0.15**](../README.md) +[**CTRF v0.0.16**](../README.md) *** @@ -8,9 +8,9 @@ > **validateReport**(`report`): [`ValidationResult`](../interfaces/ValidationResult.md) -Defined in: src/methods/validate-schema.ts:43 +Defined in: [src/methods/validate-schema.ts:43](https://github.com/ctrf-io/ctrf-core-js/blob/main/src/methods/validate-schema.ts#L43) -Validates a CTRF report against the JSON schema using AJV +Validates a CTRF report against the JSON schema ## Parameters diff --git a/docs/functions/validateReportStrict.md b/docs/functions/validateReportStrict.md index 6b54d82..ea91b9e 100644 --- a/docs/functions/validateReportStrict.md +++ b/docs/functions/validateReportStrict.md @@ -1,4 +1,4 @@ -[**CTRF v0.0.15**](../README.md) +[**CTRF v0.0.16**](../README.md) *** @@ -8,7 +8,7 @@ > **validateReportStrict**(`report`): `asserts report is Report` -Defined in: src/methods/validate-schema.ts:69 +Defined in: [src/methods/validate-schema.ts:69](https://github.com/ctrf-io/ctrf-core-js/blob/main/src/methods/validate-schema.ts#L69) Validates a CTRF report and throws an error if invalid diff --git a/docs/interfaces/Attachment.md b/docs/interfaces/Attachment.md index 67b30fb..5b9c20f 100644 --- a/docs/interfaces/Attachment.md +++ b/docs/interfaces/Attachment.md @@ -1,4 +1,4 @@ -[**CTRF v0.0.15**](../README.md) +[**CTRF v0.0.16**](../README.md) *** diff --git a/docs/interfaces/Environment.md b/docs/interfaces/Environment.md index 1dfe38f..7f47459 100644 --- a/docs/interfaces/Environment.md +++ b/docs/interfaces/Environment.md @@ -1,4 +1,4 @@ -[**CTRF v0.0.15**](../README.md) +[**CTRF v0.0.16**](../README.md) *** diff --git a/docs/interfaces/Insights.md b/docs/interfaces/Insights.md index eb44722..a6cb468 100644 --- a/docs/interfaces/Insights.md +++ b/docs/interfaces/Insights.md @@ -1,4 +1,4 @@ -[**CTRF v0.0.15**](../README.md) +[**CTRF v0.0.16**](../README.md) *** diff --git a/docs/interfaces/InsightsMetric.md b/docs/interfaces/InsightsMetric.md index af0020f..3fc9ec4 100644 --- a/docs/interfaces/InsightsMetric.md +++ b/docs/interfaces/InsightsMetric.md @@ -1,4 +1,4 @@ -[**CTRF v0.0.15**](../README.md) +[**CTRF v0.0.16**](../README.md) *** diff --git a/docs/interfaces/Report.md b/docs/interfaces/Report.md index 4efdfed..dd843e2 100644 --- a/docs/interfaces/Report.md +++ b/docs/interfaces/Report.md @@ -1,4 +1,4 @@ -[**CTRF v0.0.15**](../README.md) +[**CTRF v0.0.16**](../README.md) *** diff --git a/docs/interfaces/Results.md b/docs/interfaces/Results.md index 1e23824..fd3a59d 100644 --- a/docs/interfaces/Results.md +++ b/docs/interfaces/Results.md @@ -1,4 +1,4 @@ -[**CTRF v0.0.15**](../README.md) +[**CTRF v0.0.16**](../README.md) *** diff --git a/docs/interfaces/RetryAttempt.md b/docs/interfaces/RetryAttempt.md index 4808b63..9a471c0 100644 --- a/docs/interfaces/RetryAttempt.md +++ b/docs/interfaces/RetryAttempt.md @@ -1,4 +1,4 @@ -[**CTRF v0.0.15**](../README.md) +[**CTRF v0.0.16**](../README.md) *** diff --git a/docs/interfaces/Step.md b/docs/interfaces/Step.md index 05ff3bc..65255e7 100644 --- a/docs/interfaces/Step.md +++ b/docs/interfaces/Step.md @@ -1,4 +1,4 @@ -[**CTRF v0.0.15**](../README.md) +[**CTRF v0.0.16**](../README.md) *** diff --git a/docs/interfaces/Summary.md b/docs/interfaces/Summary.md index 413c511..0f59bad 100644 --- a/docs/interfaces/Summary.md +++ b/docs/interfaces/Summary.md @@ -1,4 +1,4 @@ -[**CTRF v0.0.15**](../README.md) +[**CTRF v0.0.16**](../README.md) *** diff --git a/docs/interfaces/Test.md b/docs/interfaces/Test.md index 58d277e..0dffb22 100644 --- a/docs/interfaces/Test.md +++ b/docs/interfaces/Test.md @@ -1,4 +1,4 @@ -[**CTRF v0.0.15**](../README.md) +[**CTRF v0.0.16**](../README.md) *** diff --git a/docs/interfaces/TestInsights.md b/docs/interfaces/TestInsights.md index 853c34f..527abce 100644 --- a/docs/interfaces/TestInsights.md +++ b/docs/interfaces/TestInsights.md @@ -1,4 +1,4 @@ -[**CTRF v0.0.15**](../README.md) +[**CTRF v0.0.16**](../README.md) *** diff --git a/docs/interfaces/TestTree.md b/docs/interfaces/TestTree.md new file mode 100644 index 0000000..48a85a0 --- /dev/null +++ b/docs/interfaces/TestTree.md @@ -0,0 +1,31 @@ +[**CTRF v0.0.16**](../README.md) + +*** + +[CTRF](../README.md) / TestTree + +# Interface: TestTree + +Defined in: src/methods/tree-hierarchical-structure.ts:44 + +Result of converting tests to tree structure + +## Properties + +### roots + +> **roots**: [`TreeNode`](TreeNode.md)[] + +Defined in: src/methods/tree-hierarchical-structure.ts:46 + +Root nodes of the tree (top-level suites) + +*** + +### summary? + +> `optional` **summary**: [`Summary`](Summary.md) + +Defined in: src/methods/tree-hierarchical-structure.ts:48 + +Overall statistics for all tests (only present when includeSummary is true) diff --git a/docs/interfaces/Tool.md b/docs/interfaces/Tool.md index 35f4ed6..8eb2793 100644 --- a/docs/interfaces/Tool.md +++ b/docs/interfaces/Tool.md @@ -1,4 +1,4 @@ -[**CTRF v0.0.15**](../README.md) +[**CTRF v0.0.16**](../README.md) *** diff --git a/docs/interfaces/TreeNode.md b/docs/interfaces/TreeNode.md new file mode 100644 index 0000000..1cc7d5c --- /dev/null +++ b/docs/interfaces/TreeNode.md @@ -0,0 +1,82 @@ +[**CTRF v0.0.16**](../README.md) + +*** + +[CTRF](../README.md) / TreeNode + +# Interface: TreeNode + +Defined in: src/methods/tree-hierarchical-structure.ts:16 + +Represents a tree node (suite) that can contain tests and child suites +Following the CTRF Suite Tree schema specification + +## Properties + +### duration + +> **duration**: `number` + +Defined in: src/methods/tree-hierarchical-structure.ts:22 + +Total duration of all tests in this suite and children + +*** + +### extra? + +> `optional` **extra**: `Record`\<`string`, `unknown`\> + +Defined in: src/methods/tree-hierarchical-structure.ts:30 + +Additional properties + +*** + +### name + +> **name**: `string` + +Defined in: src/methods/tree-hierarchical-structure.ts:18 + +The name of this suite + +*** + +### status + +> **status**: [`TestStatus`](../type-aliases/TestStatus.md) + +Defined in: src/methods/tree-hierarchical-structure.ts:20 + +The status of this suite (derived from child test results) + +*** + +### suites + +> **suites**: `TreeNode`[] + +Defined in: src/methods/tree-hierarchical-structure.ts:28 + +Child suites contained within this suite + +*** + +### summary? + +> `optional` **summary**: [`Summary`](Summary.md) + +Defined in: src/methods/tree-hierarchical-structure.ts:24 + +Aggregated statistics for this suite (only present when includeSummary is true) + +*** + +### tests + +> **tests**: [`TreeTest`](../type-aliases/TreeTest.md)[] + +Defined in: src/methods/tree-hierarchical-structure.ts:26 + +Tests directly contained in this suite diff --git a/docs/interfaces/TreeOptions.md b/docs/interfaces/TreeOptions.md new file mode 100644 index 0000000..990e47b --- /dev/null +++ b/docs/interfaces/TreeOptions.md @@ -0,0 +1,21 @@ +[**CTRF v0.0.16**](../README.md) + +*** + +[CTRF](../README.md) / TreeOptions + +# Interface: TreeOptions + +Defined in: src/methods/tree-hierarchical-structure.ts:36 + +Options for controlling tree structure creation + +## Properties + +### includeSummary? + +> `optional` **includeSummary**: `boolean` + +Defined in: src/methods/tree-hierarchical-structure.ts:38 + +Whether to include summary statistics aggregation (default: true) diff --git a/docs/interfaces/ValidationResult.md b/docs/interfaces/ValidationResult.md index fa30028..8da8b87 100644 --- a/docs/interfaces/ValidationResult.md +++ b/docs/interfaces/ValidationResult.md @@ -1,4 +1,4 @@ -[**CTRF v0.0.15**](../README.md) +[**CTRF v0.0.16**](../README.md) *** @@ -6,7 +6,7 @@ # Interface: ValidationResult -Defined in: src/methods/validate-schema.ts:17 +Defined in: [src/methods/validate-schema.ts:17](https://github.com/ctrf-io/ctrf-core-js/blob/main/src/methods/validate-schema.ts#L17) Interface for validation result @@ -16,7 +16,7 @@ Interface for validation result > `optional` **errors**: `string`[] -Defined in: src/methods/validate-schema.ts:19 +Defined in: [src/methods/validate-schema.ts:19](https://github.com/ctrf-io/ctrf-core-js/blob/main/src/methods/validate-schema.ts#L19) *** @@ -24,4 +24,4 @@ Defined in: src/methods/validate-schema.ts:19 > **valid**: `boolean` -Defined in: src/methods/validate-schema.ts:18 +Defined in: [src/methods/validate-schema.ts:18](https://github.com/ctrf-io/ctrf-core-js/blob/main/src/methods/validate-schema.ts#L18) diff --git a/docs/type-aliases/TestStatus.md b/docs/type-aliases/TestStatus.md index 61f9283..4f41fa7 100644 --- a/docs/type-aliases/TestStatus.md +++ b/docs/type-aliases/TestStatus.md @@ -1,4 +1,4 @@ -[**CTRF v0.0.15**](../README.md) +[**CTRF v0.0.16**](../README.md) *** diff --git a/docs/type-aliases/TreeTest.md b/docs/type-aliases/TreeTest.md new file mode 100644 index 0000000..4a153c8 --- /dev/null +++ b/docs/type-aliases/TreeTest.md @@ -0,0 +1,21 @@ +[**CTRF v0.0.16**](../README.md) + +*** + +[CTRF](../README.md) / TreeTest + +# Type Alias: TreeTest + +> **TreeTest** = [`Test`](../interfaces/Test.md) & `object` + +Defined in: src/methods/tree-hierarchical-structure.ts:7 + +Tree test extends CTRF Test with a nodeType field for tree traversal + +## Type declaration + +### nodeType + +> **nodeType**: `"test"` + +Node type identifier - always "test" for tree traversal diff --git a/src/index.ts b/src/index.ts index 5787259..268a09b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,6 +14,19 @@ export { isValidCtrfReport, type ValidationResult, } from './methods/validate-schema.js' +export { + organizeTestsBySuite, + traverseTree, + findSuiteByName, + findTestByName, + flattenTree, + getAllTests, + getSuiteStats, + type TreeNode, + type TreeTest, + type TreeOptions, + type TestTree, +} from './methods/tree-hierarchical-structure.js' export type { Report, diff --git a/src/methods/tree-hierarchical-structure.test.ts b/src/methods/tree-hierarchical-structure.test.ts new file mode 100644 index 0000000..610fd30 --- /dev/null +++ b/src/methods/tree-hierarchical-structure.test.ts @@ -0,0 +1,507 @@ +import { describe, it, expect } from 'vitest' +import { + organizeTestsBySuite, + traverseTree, + findSuiteByName, + findTestByName, + flattenTree, + getAllTests, + getSuiteStats, + type TreeNode, + type TreeTest, + type TreeOptions, +} from './tree-hierarchical-structure.js' +import type { Test } from '../../types/ctrf.js' + +describe('Tree Structure Functions', () => { + const createTest = ( + name: string, + status: 'passed' | 'failed' | 'skipped' | 'pending' | 'other', + duration: number, + suite?: string[], + flaky?: boolean, + id?: string + ): Test => ({ + id, + name, + status, + duration, + suite, + flaky, + }) + + describe('organizeTestsBySuite', () => { + it('should create a tree structure from tests with array suite paths', () => { + const tests: Test[] = [ + createTest( + 'should login successfully', + 'passed', + 150, + ['Authentication', 'Login'], + false, + 'test-1' + ), + createTest( + 'should logout successfully', + 'passed', + 100, + ['Authentication', 'Logout'], + false, + 'test-2' + ), + createTest( + 'should handle invalid credentials', + 'failed', + 200, + ['Authentication', 'Login'], + false, + 'test-3' + ), + createTest( + 'should validate user permissions', + 'passed', + 120, + ['Authorization', 'Permissions'], + false, + 'test-4' + ), + ] + + const tree = organizeTestsBySuite(tests) + + expect(tree.roots).toHaveLength(2) + + const authSuite = tree.roots.find(r => r.name === 'Authentication') + expect(authSuite).toBeDefined() + expect(authSuite?.suites).toHaveLength(2) // Login, Logout + expect(authSuite?.summary!.tests).toBe(3) + expect(authSuite?.summary!.passed).toBe(2) + expect(authSuite?.summary!.failed).toBe(1) + expect(authSuite?.summary!.duration).toBe(450) + expect(authSuite?.status).toBe('failed') // Has failed tests + expect(authSuite?.duration).toBe(450) + + const loginSuite = authSuite?.suites?.find(s => s.name === 'Login') + expect(loginSuite).toBeDefined() + expect(loginSuite?.tests).toHaveLength(2) + expect(loginSuite?.summary!.tests).toBe(2) + expect(loginSuite?.summary!.passed).toBe(1) + expect(loginSuite?.summary!.failed).toBe(1) + + const authzSuite = tree.roots.find(r => r.name === 'Authorization') + expect(authzSuite).toBeDefined() + expect(authzSuite?.suites).toHaveLength(1) // Permissions + expect(authzSuite?.summary!.tests).toBe(1) + expect(authzSuite?.summary!.passed).toBe(1) + expect(authzSuite?.status).toBe('passed') // All tests passed + }) + + it('should handle tests with array suite paths', () => { + const tests: Test[] = [ + createTest('test1', 'passed', 100, ['Suite1', 'SubSuite1']), + createTest('test2', 'failed', 200, ['Suite1', 'SubSuite2']), + createTest('test3', 'passed', 150, ['Suite2']), + ] + + const tree = organizeTestsBySuite(tests) + + expect(tree.roots).toHaveLength(2) + + const suite1 = tree.roots.find(r => r.name === 'Suite1') + expect(suite1?.suites).toHaveLength(2) + + const suite2 = tree.roots.find(r => r.name === 'Suite2') + expect(suite2?.tests).toHaveLength(1) + }) + + it('should handle tests without suite paths', () => { + const tests: Test[] = [ + createTest( + 'standalone test 1', + 'passed', + 100, + undefined, + false, + 'test-1' + ), + createTest( + 'standalone test 2', + 'failed', + 200, + undefined, + false, + 'test-2' + ), + createTest('suite test', 'passed', 150, ['Suite1'], false, 'test-3'), + ] + + const tree = organizeTestsBySuite(tests) + + expect(tree.roots).toHaveLength(3) // 2 standalone tests + "Suite1" + + const standalone1 = tree.roots.find(r => r.name === 'standalone test 1') + expect(standalone1).toBeDefined() + expect(standalone1?.tests).toHaveLength(1) + expect(standalone1?.tests[0].name).toBe('standalone test 1') + + const standalone2 = tree.roots.find(r => r.name === 'standalone test 2') + expect(standalone2).toBeDefined() + expect(standalone2?.tests[0].name).toBe('standalone test 2') + + const suite1 = tree.roots.find(r => r.name === 'Suite1') + expect(suite1).toBeDefined() + expect(suite1?.tests).toHaveLength(1) + }) + + it('should handle flaky test statistics correctly', () => { + const tests: Test[] = [ + createTest('flaky test', 'passed', 100, ['Suite1'], true, 'test-1'), + createTest('normal test', 'passed', 100, ['Suite1'], false, 'test-2'), + createTest('another flaky', 'failed', 200, ['Suite1'], true, 'test-3'), + ] + + const tree = organizeTestsBySuite(tests) + + const suite1 = tree.roots.find(r => r.name === 'Suite1') + expect(suite1?.summary!.tests).toBe(3) + expect(suite1?.summary!.flaky).toBe(2) // Two tests marked as flaky + expect(suite1?.summary!.passed).toBe(2) + expect(suite1?.summary!.failed).toBe(1) + + expect(tree.summary!.tests).toBe(3) + expect(tree.summary!.flaky).toBe(2) // Two flaky tests total + }) + + it('should handle flaky tests with retries correctly', () => { + const tests: Test[] = [ + { + ...createTest( + 'retry test', + 'passed', + 100, + ['Suite1'], + false, + 'test-1' + ), + retries: 2, + }, + { + ...createTest( + 'failed retry test', + 'failed', + 100, + ['Suite1'], + false, + 'test-2' + ), + retries: 1, + }, + createTest('normal test', 'passed', 100, ['Suite1'], false, 'test-3'), + ] + + const tree = organizeTestsBySuite(tests) + + const suite1 = tree.roots.find(r => r.name === 'Suite1') + expect(suite1?.summary!.tests).toBe(3) + expect(suite1?.summary!.flaky).toBe(1) // Only the passed test with retries + expect(suite1?.summary!.passed).toBe(2) + expect(suite1?.summary!.failed).toBe(1) + + expect(tree.summary!.flaky).toBe(1) + }) + + it('should handle malformed suite paths gracefully', () => { + const tests: Test[] = [ + createTest('test1', 'passed', 100, [], false, 'test-1'), + createTest('test2', 'passed', 100, ['', ''], false, 'test-2'), + createTest('test3', 'passed', 100, ['Valid', 'Suite'], false, 'test-3'), + ] + + const tree = organizeTestsBySuite(tests) + + expect(tree.roots).toHaveLength(3) // test1 + test2 + "Valid" + + const validSuite = tree.roots.find(r => r.name === 'Valid') + expect(validSuite).toBeDefined() + + const test1Node = tree.roots.find(r => r.name === 'test1') + expect(test1Node).toBeDefined() + + const test2Node = tree.roots.find(r => r.name === 'test2') + expect(test2Node).toBeDefined() + }) + + it('should handle empty test array', () => { + const tree = organizeTestsBySuite([]) + + expect(tree.roots).toHaveLength(0) + expect(tree.summary!.tests).toBe(0) + }) + + it('should disable summary when includeSummary is false', () => { + const tests: Test[] = [ + createTest('test1', 'passed', 100, ['Suite1'], false, 'test-1'), + createTest('test2', 'failed', 200, ['Suite1'], false, 'test-2'), + ] + + const options: TreeOptions = { includeSummary: false } + const tree = organizeTestsBySuite(tests, options) + + expect(tree.summary).toBeUndefined() + + const suite1 = tree.roots.find(r => r.name === 'Suite1') + expect(suite1).toBeDefined() + + expect(suite1?.summary).toBeUndefined() + + expect(suite1?.status).toBe('other') + expect(suite1?.duration).toBe(0) + }) + + it('should calculate overall statistics correctly', () => { + const tests: Test[] = [ + createTest('test1', 'passed', 100, ['Suite1'], false, 'test-1'), + createTest('test2', 'failed', 200, ['Suite1'], false, 'test-2'), + createTest('test3', 'skipped', 50, ['Suite2'], false, 'test-3'), + createTest('test4', 'pending', 0, ['Suite2'], false, 'test-4'), + createTest('test5', 'other', 25, ['Suite3'], false, 'test-5'), + ] + + const tree = organizeTestsBySuite(tests) + + expect(tree.summary).toBeDefined() + expect(tree.summary!.tests).toBe(5) + expect(tree.summary!.passed).toBe(1) + expect(tree.summary!.failed).toBe(1) + expect(tree.summary!.skipped).toBe(1) + expect(tree.summary!.pending).toBe(1) + expect(tree.summary!.other).toBe(1) + expect(tree.summary!.duration).toBe(375) + }) + + it('should handle deep nesting correctly', () => { + const tests: Test[] = [ + createTest('deep test', 'passed', 100, [ + 'Level1', + 'Level2', + 'Level3', + 'Level4', + 'Level5', + ]), + ] + + const tree = organizeTestsBySuite(tests) + + expect(tree.roots).toHaveLength(1) + + let current = tree.roots[0] + expect(current.name).toBe('Level1') + + for (let i = 2; i <= 5; i++) { + expect(current.suites).toHaveLength(1) + current = current.suites![0] + expect(current.name).toBe(`Level${i}`) + } + + expect(current.tests).toHaveLength(1) + expect(current.tests![0].name).toBe('deep test') + }) + }) + + describe('traverseTree', () => { + it('should traverse all nodes in the correct order', () => { + const tests: Test[] = [ + createTest('test1', 'passed', 100, ['Suite1', 'SubSuite1']), + createTest('test2', 'passed', 100, ['Suite1', 'SubSuite2']), + createTest('test3', 'passed', 100, ['Suite2']), + ] + + const tree = organizeTestsBySuite(tests) + const visitedNodes: Array<{ name: string; depth: number }> = [] + + traverseTree(tree.roots, (node, depth) => { + visitedNodes.push({ name: node.name, depth }) + }) + + expect(visitedNodes).toContainEqual({ name: 'Suite1', depth: 0 }) + expect(visitedNodes).toContainEqual({ name: 'SubSuite1', depth: 1 }) + expect(visitedNodes).toContainEqual({ name: 'test1', depth: 2 }) + expect(visitedNodes).toContainEqual({ name: 'Suite2', depth: 0 }) + expect(visitedNodes).toContainEqual({ name: 'test3', depth: 1 }) + }) + }) + + describe('findSuiteByName', () => { + it('should find suite by name in root', () => { + const tests: Test[] = [ + createTest('test1', 'passed', 100, ['Suite1'], false, 'test-1'), + createTest('test2', 'passed', 100, ['Suite2'], false, 'test-2'), + ] + + const tree = organizeTestsBySuite(tests) + const found = findSuiteByName(tree.roots, 'Suite1') + + expect(found).toBeDefined() + expect(found?.name).toBe('Suite1') + }) + + it('should find nested suite by name', () => { + const tests: Test[] = [ + createTest( + 'test1', + 'passed', + 100, + ['Parent', 'Child', 'Grandchild'], + false, + 'test-1' + ), + ] + + const tree = organizeTestsBySuite(tests) + const found = findSuiteByName(tree.roots, 'Grandchild') + + expect(found).toBeDefined() + expect(found?.name).toBe('Grandchild') + }) + + it('should return undefined for non-existent suite', () => { + const tests: Test[] = [ + createTest('test1', 'passed', 100, ['Suite1'], false, 'test-1'), + ] + + const tree = organizeTestsBySuite(tests) + const found = findSuiteByName(tree.roots, 'NonExistent') + + expect(found).toBeUndefined() + }) + }) + + describe('findTestByName', () => { + it('should find test by name in suite', () => { + const tests: Test[] = [ + createTest('test1', 'passed', 100, ['Suite1'], false, 'test-1'), + createTest('test2', 'passed', 100, ['Suite1'], false, 'test-2'), + ] + + const tree = organizeTestsBySuite(tests) + const found = findTestByName(tree.roots, 'test1') + + expect(found).toBeDefined() + expect(found?.name).toBe('test1') + }) + + it('should find test by name in nested suite', () => { + const tests: Test[] = [ + createTest( + 'nested-test', + 'passed', + 100, + ['Parent', 'Child'], + false, + 'nested-test' + ), + ] + + const tree = organizeTestsBySuite(tests) + const found = findTestByName(tree.roots, 'nested-test') + + expect(found).toBeDefined() + expect(found?.name).toBe('nested-test') + }) + + it('should return undefined for non-existent test', () => { + const tests: Test[] = [ + createTest('test1', 'passed', 100, ['Suite1'], false, 'test-1'), + ] + + const tree = organizeTestsBySuite(tests) + const found = findTestByName(tree.roots, 'non-existent-test') + + expect(found).toBeUndefined() + }) + }) + + describe('flattenTree', () => { + it('should flatten tree structure with correct depth information', () => { + const tests: Test[] = [ + createTest('test1', 'passed', 100, ['Suite1', 'SubSuite1']), + createTest('test2', 'passed', 100, ['Suite1']), + createTest('standalone', 'passed', 100), + ] + + const tree = organizeTestsBySuite(tests) + const flattened = flattenTree(tree.roots) + + expect(flattened.length).toBeGreaterThan(0) + + const suite1 = flattened.find(item => item.node.name === 'Suite1') + expect(suite1?.depth).toBe(0) + + const subSuite1 = flattened.find(item => item.node.name === 'SubSuite1') + expect(subSuite1?.depth).toBe(1) + + const test1 = flattened.find(item => item.node.name === 'test1') + expect(test1?.depth).toBe(2) + + const standalone = flattened.find(item => item.node.name === 'standalone') + expect(standalone?.depth).toBe(0) + }) + }) + + describe('Real-world test data compatibility', () => { + it('should work with CTRF array suite format', () => { + const tests: Test[] = [ + createTest( + 'run-insights utility functions isTestFlaky should return true for explicitly flaky tests', + 'passed', + 1, + [ + 'run-insights.test.ts', + 'run-insights utility functions', + 'isTestFlaky', + ] + ), + createTest( + 'enrichReportWithInsights - Main API basic functionality should enrich a report with run-level insights when no previous reports', + 'passed', + 1, + [ + 'run-insights.test.ts', + 'enrichReportWithInsights - Main API', + 'basic functionality', + ] + ), + createTest( + 'read-reports readSingleReport should read and parse a valid CTRF report file', + 'passed', + 1, + ['read-reports.test.ts', 'read-reports', 'readSingleReport'] + ), + ] + + const tree = organizeTestsBySuite(tests) + + expect(tree.roots).toHaveLength(2) // run-insights.test.ts and read-reports.test.ts + + const runInsightsFile = tree.roots.find( + r => r.name === 'run-insights.test.ts' + ) + expect(runInsightsFile?.suites).toHaveLength(2) // "run-insights utility functions" and "enrichReportWithInsights - Main API" + + const utilityFunctions = runInsightsFile?.suites?.find( + s => s.name === 'run-insights utility functions' + ) + expect(utilityFunctions?.suites).toHaveLength(1) // "isTestFlaky" + + const isTestFlaky = utilityFunctions?.suites?.find( + s => s.name === 'isTestFlaky' + ) + expect(isTestFlaky?.tests).toHaveLength(1) + + const readReportsFile = tree.roots.find( + r => r.name === 'read-reports.test.ts' + ) + expect(readReportsFile?.suites).toHaveLength(1) // "read-reports" + }) + }) +}) diff --git a/src/methods/tree-hierarchical-structure.ts b/src/methods/tree-hierarchical-structure.ts new file mode 100644 index 0000000..fc70bde --- /dev/null +++ b/src/methods/tree-hierarchical-structure.ts @@ -0,0 +1,440 @@ +import type { Test, TestStatus, Summary } from '../../types/ctrf.js' +import { isTestFlaky } from './run-insights.js' + +/** + * Tree test extends CTRF Test with a nodeType field for tree traversal + */ +export type TreeTest = Test & { + /** Node type identifier - always "test" for tree traversal */ + nodeType: 'test' +} + +/** + * Represents a tree node (suite) that can contain tests and child suites + * Following the CTRF Suite Tree schema specification + */ +export interface TreeNode { + /** The name of this suite */ + name: string + /** The status of this suite (derived from child test results) */ + status: TestStatus + /** Total duration of all tests in this suite and children */ + duration: number + /** Aggregated statistics for this suite (only present when includeSummary is true) */ + summary?: Summary + /** Tests directly contained in this suite */ + tests: TreeTest[] + /** Child suites contained within this suite */ + suites: TreeNode[] + /** Additional properties */ + extra?: Record +} + +/** + * Options for controlling tree structure creation + */ +export interface TreeOptions { + /** Whether to include summary statistics aggregation (default: true) */ + includeSummary?: boolean +} + +/** + * Result of converting tests to tree structure + */ +export interface TestTree { + /** Root nodes of the tree (top-level suites) */ + roots: TreeNode[] + /** Overall statistics for all tests (only present when includeSummary is true) */ + summary?: Summary +} + +/** + * Organizes CTRF tests into a hierarchical tree structure based on the suite property. + * + * The function handles array format (['suite1', 'suite2', 'suite3']) for the suite property + * as defined in the CTRF schema. The output follows the CTRF Suite Tree schema specification. + * + * @param tests - Array of CTRF test objects + * @param options - Options for controlling tree creation + * @returns TestTree object containing the hierarchical structure and statistics + * + * @example + * ```typescript + * import { organizeTestsBySuite } from 'ctrf-js-common' + * + * const tests = [ + * { + * name: 'should login successfully', + * status: 'passed', + * duration: 150, + * suite: ['Authentication', 'Login'] + * }, + * { + * name: 'should logout successfully', + * status: 'passed', + * duration: 100, + * suite: ['Authentication', 'Logout'] + * } + * ] + * + * const tree = organizeTestsBySuite(tests) + * + * // For structure-only without summary statistics: + * // const tree = organizeTestsBySuite(tests, { includeSummary: false }) + * + * // Convert to JSON for machine consumption + * const treeJson = JSON.stringify(tree, null, 2) + * + * console.log(tree.roots[0].name) // 'Authentication' + * console.log(tree.roots[0].suites.length) // 2 (Login, Logout) + * ``` + */ +export function organizeTestsBySuite( + tests: Test[], + options: TreeOptions = {} +): TestTree { + const { includeSummary = true } = options + + const nodeMap = new Map() + const rootNodes = new Map() + + const createEmptySummary = (): Summary => ({ + tests: 0, + passed: 0, + failed: 0, + skipped: 0, + pending: 0, + other: 0, + flaky: 0, + start: 0, + stop: 0, + duration: 0, + }) + + const createTreeTest = (test: Test): TreeTest => ({ + ...test, + nodeType: 'test', + id: test.id || crypto.randomUUID(), + }) + + const parseSuitePath = (suite: string[] | undefined): string[] => { + if (!suite) return [] + if (Array.isArray(suite)) { + return suite.filter(s => s && s.trim().length > 0) + } + return [] + } + + const calculateSuiteStatus = (summary: Summary): TestStatus => { + if (summary.tests === 0) return 'other' + if (summary.failed > 0) return 'failed' + if (summary.pending > 0) return 'pending' + if (summary.skipped === summary.tests) return 'skipped' + if (summary.passed === summary.tests) return 'passed' + return 'other' + } + + const getOrCreateSuite = (path: string[]): TreeNode => { + const fullPath = path.join('/') + + if (nodeMap.has(fullPath)) { + return nodeMap.get(fullPath)! + } + + const name = path[path.length - 1] + const node: TreeNode = { + name, + status: 'other', + duration: 0, + tests: [], + suites: [], + } + + if (includeSummary) { + node.summary = createEmptySummary() + } + + nodeMap.set(fullPath, node) + + if (path.length === 1) { + rootNodes.set(name, node) + } else { + const parentPath = path.slice(0, -1) + const parent = getOrCreateSuite(parentPath) + parent.suites.push(node) + } + + return node + } + + for (const test of tests) { + const suitePath = parseSuitePath(test.suite) + const treeTest = createTreeTest(test) + + if (suitePath.length === 0) { + const testNode: TreeNode = { + name: treeTest.name, + status: treeTest.status, + duration: treeTest.duration, + tests: [treeTest], + suites: [], + } + + if (includeSummary) { + testNode.summary = createEmptySummary() + } + + rootNodes.set(treeTest.name, testNode) + nodeMap.set(treeTest.name, testNode) + } else { + const parentSuite = getOrCreateSuite(suitePath) + parentSuite.tests.push(treeTest) + } + } + + if (includeSummary) { + const aggregateStats = (node: TreeNode): void => { + if (!node.summary) { + node.summary = createEmptySummary() + } + + for (const test of node.tests) { + node.summary.tests++ + node.summary.duration = (node.summary.duration || 0) + test.duration + + if (isTestFlaky(test)) { + node.summary.flaky = (node.summary.flaky || 0) + 1 + } + + switch (test.status) { + case 'passed': + node.summary.passed++ + break + case 'failed': + node.summary.failed++ + break + case 'skipped': + node.summary.skipped++ + break + case 'pending': + node.summary.pending++ + break + case 'other': + node.summary.other++ + break + } + } + + for (const suite of node.suites) { + aggregateStats(suite) + + if (suite.summary) { + node.summary.tests += suite.summary.tests + node.summary.passed += suite.summary.passed + node.summary.failed += suite.summary.failed + node.summary.skipped += suite.summary.skipped + node.summary.pending += suite.summary.pending + node.summary.other += suite.summary.other + node.summary.flaky = + (node.summary.flaky || 0) + (suite.summary.flaky || 0) + node.summary.duration = + (node.summary.duration || 0) + (suite.summary.duration || 0) + } + } + + node.duration = node.summary.duration || 0 + node.status = calculateSuiteStatus(node.summary) + } + + for (const rootNode of rootNodes.values()) { + aggregateStats(rootNode) + } + } else { + const setDefaultValues = (node: TreeNode): void => { + node.status = 'other' + node.duration = 0 + + for (const suite of node.suites) { + setDefaultValues(suite) + } + } + + for (const rootNode of rootNodes.values()) { + setDefaultValues(rootNode) + } + } + + if (includeSummary) { + const overallSummary = createEmptySummary() + for (const rootNode of rootNodes.values()) { + if (rootNode.summary) { + overallSummary.tests += rootNode.summary.tests + overallSummary.passed += rootNode.summary.passed + overallSummary.failed += rootNode.summary.failed + overallSummary.skipped += rootNode.summary.skipped + overallSummary.pending += rootNode.summary.pending + overallSummary.other += rootNode.summary.other + overallSummary.flaky = + (overallSummary.flaky || 0) + (rootNode.summary.flaky || 0) + overallSummary.duration = + (overallSummary.duration || 0) + (rootNode.summary.duration || 0) + } + } + + return { + roots: Array.from(rootNodes.values()), + summary: overallSummary, + } + } else { + return { + roots: Array.from(rootNodes.values()), + } + } +} + +/** + * Utility function to traverse the tree and apply a function to each node + * + * @param nodes - Array of tree nodes to traverse + * @param callback - Function to call for each node (suites and tests) + * @param depth - Current depth in the tree (starts at 0) + */ +export function traverseTree( + nodes: TreeNode[], + callback: ( + node: TreeNode | TreeTest, + depth: number, + nodeType: 'suite' | 'test' + ) => void, + depth: number = 0 +): void { + for (const node of nodes) { + callback(node, depth, 'suite') + + if (node.suites.length > 0) { + traverseTree(node.suites, callback, depth + 1) + } + + for (const test of node.tests) { + callback(test, depth + 1, 'test') + } + } +} + +/** + * Utility function to find a suite by name in the tree + * + * @param nodes - Array of tree nodes to search + * @param name - Name of the suite to find + * @returns The found suite node or undefined + */ +export function findSuiteByName( + nodes: TreeNode[], + name: string +): TreeNode | undefined { + for (const node of nodes) { + if (node.name === name) { + return node + } + + const found = findSuiteByName(node.suites, name) + if (found) return found + } + + return undefined +} + +/** + * Utility function to find a test by name in the tree + * + * @param nodes - Array of tree nodes to search + * @param name - Name of the test to find + * @returns The found test or undefined + */ +export function findTestByName( + nodes: TreeNode[], + name: string +): TreeTest | undefined { + for (const node of nodes) { + for (const test of node.tests) { + if (test.name === name) { + return test + } + } + + const found = findTestByName(node.suites, name) + if (found) return found + } + + return undefined +} + +/** + * Utility function to convert tree to a flat array with indentation information + * Useful for displaying the tree in a linear format + * + * @param nodes - Array of tree nodes to flatten + * @returns Array of objects containing node, depth, and nodeType information + */ +export function flattenTree(nodes: TreeNode[]): Array<{ + node: TreeNode | TreeTest + depth: number + nodeType: 'suite' | 'test' +}> { + const result: Array<{ + node: TreeNode | TreeTest + depth: number + nodeType: 'suite' | 'test' + }> = [] + + traverseTree(nodes, (node, depth, nodeType) => { + result.push({ node, depth, nodeType }) + }) + + return result +} + +/** + * Utility function to get all tests from the tree structure as a flat array + * + * @param nodes - Array of tree nodes to extract tests from + * @returns Array of all tests in the tree + */ +export function getAllTests(nodes: TreeNode[]): TreeTest[] { + const tests: TreeTest[] = [] + + traverseTree(nodes, (node, depth, nodeType) => { + if (nodeType === 'test') { + tests.push(node as TreeTest) + } + }) + + return tests +} + +/** + * Utility function to get statistics for a specific suite path + * + * @param nodes - Array of tree nodes to search + * @param suitePath - Array representing the path to the suite + * @returns Summary statistics for the suite or undefined if not found + */ +export function getSuiteStats( + nodes: TreeNode[], + suitePath: string[] +): Summary | undefined { + let current = nodes + + for (const suiteName of suitePath) { + const found = current.find(node => node.name === suiteName) + if (!found) return undefined + + if (suitePath.indexOf(suiteName) === suitePath.length - 1) { + return found.summary + } + + current = found.suites + } + + return undefined +}