From 30cc3989cec579ae1d1d47844034f053fd349b02 Mon Sep 17 00:00:00 2001 From: Matthew Thomas Date: Fri, 10 Oct 2025 20:24:15 +0100 Subject: [PATCH 1/4] feat: add organizeTestsBySuite --- docs/README.md | 15 +- docs/enumerations/SortOrder.md | 2 +- docs/functions/enrichReportWithInsights.md | 2 +- docs/functions/findSuiteByName.md | 33 ++ docs/functions/findTestByName.md | 33 ++ docs/functions/flattenTree.md | 28 + docs/functions/getAllTests.md | 27 + docs/functions/getSuiteStats.md | 33 ++ docs/functions/isValidCtrfReport.md | 4 +- docs/functions/mergeReports.md | 2 +- docs/functions/organizeTestsBySuite.md | 68 +++ docs/functions/readReportFromFile.md | 2 +- docs/functions/readReportsFromDirectory.md | 2 +- docs/functions/readReportsFromGlobPattern.md | 2 +- docs/functions/sortReportsByTimestamp.md | 2 +- docs/functions/storePreviousResults.md | 2 +- docs/functions/traverseTree.md | 37 ++ docs/functions/validateReport.md | 6 +- docs/functions/validateReportStrict.md | 4 +- docs/interfaces/Attachment.md | 2 +- docs/interfaces/Environment.md | 2 +- docs/interfaces/Insights.md | 2 +- docs/interfaces/InsightsMetric.md | 2 +- docs/interfaces/Report.md | 2 +- docs/interfaces/Results.md | 2 +- docs/interfaces/RetryAttempt.md | 2 +- docs/interfaces/Step.md | 2 +- docs/interfaces/Summary.md | 2 +- docs/interfaces/Test.md | 2 +- docs/interfaces/TestInsights.md | 2 +- docs/interfaces/TestTree.md | 31 ++ docs/interfaces/Tool.md | 2 +- docs/interfaces/TreeNode.md | 82 +++ docs/interfaces/TreeOptions.md | 21 + docs/interfaces/ValidationResult.md | 8 +- docs/type-aliases/TestStatus.md | 2 +- docs/type-aliases/TreeTest.md | 21 + src/index.ts | 13 + .../tree-hierarchical-structure.test.ts | 507 ++++++++++++++++++ src/methods/tree-hierarchical-structure.ts | 440 +++++++++++++++ 40 files changed, 1419 insertions(+), 34 deletions(-) create mode 100644 docs/functions/findSuiteByName.md create mode 100644 docs/functions/findTestByName.md create mode 100644 docs/functions/flattenTree.md create mode 100644 docs/functions/getAllTests.md create mode 100644 docs/functions/getSuiteStats.md create mode 100644 docs/functions/organizeTestsBySuite.md create mode 100644 docs/functions/traverseTree.md create mode 100644 docs/interfaces/TestTree.md create mode 100644 docs/interfaces/TreeNode.md create mode 100644 docs/interfaces/TreeOptions.md create mode 100644 docs/type-aliases/TreeTest.md create mode 100644 src/methods/tree-hierarchical-structure.test.ts create mode 100644 src/methods/tree-hierarchical-structure.ts 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 +} From 4946775242be02b01e7868c7a9c6eb2372bfd6aa Mon Sep 17 00:00:00 2001 From: Matthew Thomas Date: Fri, 10 Oct 2025 20:44:29 +0100 Subject: [PATCH 2/4] docs: update function definitions and add Baseline interface documentation --- docs/README.md | 34 +++++---- docs/functions/findSuiteByName.md | 2 +- docs/functions/findTestByName.md | 2 +- docs/functions/flattenTree.md | 2 +- docs/functions/getAllTests.md | 2 +- docs/functions/getSuiteStats.md | 2 +- docs/functions/organizeTestsBySuite.md | 2 +- docs/functions/traverseTree.md | 2 +- docs/interfaces/Baseline.md | 73 +++++++++++++++++++ docs/interfaces/Report.md | 4 +- .../{Insights.md => RootInsights.md} | 4 +- docs/interfaces/TestTree.md | 6 +- docs/interfaces/TreeNode.md | 16 ++-- docs/interfaces/TreeOptions.md | 4 +- docs/type-aliases/TreeTest.md | 2 +- src/index.ts | 53 ++++++++++++-- src/methods/run-insights.ts | 14 ++-- typedoc.json | 3 +- types/ctrf.d.ts | 4 +- 19 files changed, 175 insertions(+), 56 deletions(-) create mode 100644 docs/interfaces/Baseline.md rename docs/interfaces/{Insights.md => RootInsights.md} (96%) diff --git a/docs/README.md b/docs/README.md index a33081a..a6abedf 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,35 +4,24 @@ # CTRF v0.0.16 -## Enumerations - -- [SortOrder](enumerations/SortOrder.md) - -## Interfaces +## Schema - [Attachment](interfaces/Attachment.md) +- [Baseline](interfaces/Baseline.md) - [Environment](interfaces/Environment.md) -- [Insights](interfaces/Insights.md) - [InsightsMetric](interfaces/InsightsMetric.md) - [Report](interfaces/Report.md) - [Results](interfaces/Results.md) - [RetryAttempt](interfaces/RetryAttempt.md) +- [RootInsights](interfaces/RootInsights.md) - [Step](interfaces/Step.md) - [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 +## Methods - [enrichReportWithInsights](functions/enrichReportWithInsights.md) - [findSuiteByName](functions/findSuiteByName.md) @@ -51,3 +40,18 @@ - [traverseTree](functions/traverseTree.md) - [validateReport](functions/validateReport.md) - [validateReportStrict](functions/validateReportStrict.md) + +## Enumerations + +- [SortOrder](enumerations/SortOrder.md) + +## Interfaces + +- [TestTree](interfaces/TestTree.md) +- [TreeNode](interfaces/TreeNode.md) +- [TreeOptions](interfaces/TreeOptions.md) +- [ValidationResult](interfaces/ValidationResult.md) + +## Type Aliases + +- [TreeTest](type-aliases/TreeTest.md) diff --git a/docs/functions/findSuiteByName.md b/docs/functions/findSuiteByName.md index 99e9a9e..ecc371b 100644 --- a/docs/functions/findSuiteByName.md +++ b/docs/functions/findSuiteByName.md @@ -8,7 +8,7 @@ > **findSuiteByName**(`nodes`, `name`): `undefined` \| [`TreeNode`](../interfaces/TreeNode.md) -Defined in: src/methods/tree-hierarchical-structure.ts:331 +Defined in: [src/methods/tree-hierarchical-structure.ts:331](https://github.com/ctrf-io/ctrf-core-js/blob/main/src/methods/tree-hierarchical-structure.ts#L331) Utility function to find a suite by name in the tree diff --git a/docs/functions/findTestByName.md b/docs/functions/findTestByName.md index edc0cc0..a05dbb4 100644 --- a/docs/functions/findTestByName.md +++ b/docs/functions/findTestByName.md @@ -8,7 +8,7 @@ > **findTestByName**(`nodes`, `name`): `undefined` \| [`TreeTest`](../type-aliases/TreeTest.md) -Defined in: src/methods/tree-hierarchical-structure.ts:354 +Defined in: [src/methods/tree-hierarchical-structure.ts:354](https://github.com/ctrf-io/ctrf-core-js/blob/main/src/methods/tree-hierarchical-structure.ts#L354) Utility function to find a test by name in the tree diff --git a/docs/functions/flattenTree.md b/docs/functions/flattenTree.md index 8103501..611ad33 100644 --- a/docs/functions/flattenTree.md +++ b/docs/functions/flattenTree.md @@ -8,7 +8,7 @@ > **flattenTree**(`nodes`): `object`[] -Defined in: src/methods/tree-hierarchical-structure.ts:379 +Defined in: [src/methods/tree-hierarchical-structure.ts:379](https://github.com/ctrf-io/ctrf-core-js/blob/main/src/methods/tree-hierarchical-structure.ts#L379) Utility function to convert tree to a flat array with indentation information Useful for displaying the tree in a linear format diff --git a/docs/functions/getAllTests.md b/docs/functions/getAllTests.md index 91568a5..18a48ec 100644 --- a/docs/functions/getAllTests.md +++ b/docs/functions/getAllTests.md @@ -8,7 +8,7 @@ > **getAllTests**(`nodes`): [`TreeTest`](../type-aliases/TreeTest.md)[] -Defined in: src/methods/tree-hierarchical-structure.ts:403 +Defined in: [src/methods/tree-hierarchical-structure.ts:403](https://github.com/ctrf-io/ctrf-core-js/blob/main/src/methods/tree-hierarchical-structure.ts#L403) Utility function to get all tests from the tree structure as a flat array diff --git a/docs/functions/getSuiteStats.md b/docs/functions/getSuiteStats.md index 905505a..63fe99e 100644 --- a/docs/functions/getSuiteStats.md +++ b/docs/functions/getSuiteStats.md @@ -8,7 +8,7 @@ > **getSuiteStats**(`nodes`, `suitePath`): `undefined` \| [`Summary`](../interfaces/Summary.md) -Defined in: src/methods/tree-hierarchical-structure.ts:422 +Defined in: [src/methods/tree-hierarchical-structure.ts:422](https://github.com/ctrf-io/ctrf-core-js/blob/main/src/methods/tree-hierarchical-structure.ts#L422) Utility function to get statistics for a specific suite path diff --git a/docs/functions/organizeTestsBySuite.md b/docs/functions/organizeTestsBySuite.md index 5376cd9..8b5916a 100644 --- a/docs/functions/organizeTestsBySuite.md +++ b/docs/functions/organizeTestsBySuite.md @@ -8,7 +8,7 @@ > **organizeTestsBySuite**(`tests`, `options`): [`TestTree`](../interfaces/TestTree.md) -Defined in: src/methods/tree-hierarchical-structure.ts:92 +Defined in: [src/methods/tree-hierarchical-structure.ts:92](https://github.com/ctrf-io/ctrf-core-js/blob/main/src/methods/tree-hierarchical-structure.ts#L92) Organizes CTRF tests into a hierarchical tree structure based on the suite property. diff --git a/docs/functions/traverseTree.md b/docs/functions/traverseTree.md index 23cba20..2aecc3e 100644 --- a/docs/functions/traverseTree.md +++ b/docs/functions/traverseTree.md @@ -8,7 +8,7 @@ > **traverseTree**(`nodes`, `callback`, `depth`): `void` -Defined in: src/methods/tree-hierarchical-structure.ts:302 +Defined in: [src/methods/tree-hierarchical-structure.ts:302](https://github.com/ctrf-io/ctrf-core-js/blob/main/src/methods/tree-hierarchical-structure.ts#L302) Utility function to traverse the tree and apply a function to each node diff --git a/docs/interfaces/Baseline.md b/docs/interfaces/Baseline.md new file mode 100644 index 0000000..3c9f696 --- /dev/null +++ b/docs/interfaces/Baseline.md @@ -0,0 +1,73 @@ +[**CTRF v0.0.16**](../README.md) + +*** + +[CTRF](../README.md) / Baseline + +# Interface: Baseline + +Defined in: [types/ctrf.d.ts:150](https://github.com/ctrf-io/ctrf-core-js/blob/main/types/ctrf.d.ts#L150) + +## Properties + +### buildName? + +> `optional` **buildName**: `string` + +Defined in: [types/ctrf.d.ts:155](https://github.com/ctrf-io/ctrf-core-js/blob/main/types/ctrf.d.ts#L155) + +*** + +### buildNumber? + +> `optional` **buildNumber**: `number` + +Defined in: [types/ctrf.d.ts:156](https://github.com/ctrf-io/ctrf-core-js/blob/main/types/ctrf.d.ts#L156) + +*** + +### buildUrl? + +> `optional` **buildUrl**: `string` + +Defined in: [types/ctrf.d.ts:157](https://github.com/ctrf-io/ctrf-core-js/blob/main/types/ctrf.d.ts#L157) + +*** + +### commit? + +> `optional` **commit**: `string` + +Defined in: [types/ctrf.d.ts:154](https://github.com/ctrf-io/ctrf-core-js/blob/main/types/ctrf.d.ts#L154) + +*** + +### extra? + +> `optional` **extra**: `Record`\<`string`, `unknown`\> + +Defined in: [types/ctrf.d.ts:158](https://github.com/ctrf-io/ctrf-core-js/blob/main/types/ctrf.d.ts#L158) + +*** + +### reportId + +> **reportId**: `string` + +Defined in: [types/ctrf.d.ts:151](https://github.com/ctrf-io/ctrf-core-js/blob/main/types/ctrf.d.ts#L151) + +*** + +### source? + +> `optional` **source**: `string` + +Defined in: [types/ctrf.d.ts:152](https://github.com/ctrf-io/ctrf-core-js/blob/main/types/ctrf.d.ts#L152) + +*** + +### timestamp? + +> `optional` **timestamp**: `string` + +Defined in: [types/ctrf.d.ts:153](https://github.com/ctrf-io/ctrf-core-js/blob/main/types/ctrf.d.ts#L153) diff --git a/docs/interfaces/Report.md b/docs/interfaces/Report.md index dd843e2..0a12065 100644 --- a/docs/interfaces/Report.md +++ b/docs/interfaces/Report.md @@ -12,7 +12,7 @@ Defined in: [types/ctrf.d.ts:1](https://github.com/ctrf-io/ctrf-core-js/blob/mai ### baseline? -> `optional` **baseline**: `Baseline` +> `optional` **baseline**: [`Baseline`](Baseline.md) Defined in: [types/ctrf.d.ts:9](https://github.com/ctrf-io/ctrf-core-js/blob/main/types/ctrf.d.ts#L9) @@ -36,7 +36,7 @@ Defined in: [types/ctrf.d.ts:6](https://github.com/ctrf-io/ctrf-core-js/blob/mai ### insights? -> `optional` **insights**: [`Insights`](Insights.md) +> `optional` **insights**: [`RootInsights`](RootInsights.md) Defined in: [types/ctrf.d.ts:8](https://github.com/ctrf-io/ctrf-core-js/blob/main/types/ctrf.d.ts#L8) diff --git a/docs/interfaces/Insights.md b/docs/interfaces/RootInsights.md similarity index 96% rename from docs/interfaces/Insights.md rename to docs/interfaces/RootInsights.md index a6cb468..2b47768 100644 --- a/docs/interfaces/Insights.md +++ b/docs/interfaces/RootInsights.md @@ -2,9 +2,9 @@ *** -[CTRF](../README.md) / Insights +[CTRF](../README.md) / RootInsights -# Interface: Insights +# Interface: RootInsights Defined in: [types/ctrf.d.ts:123](https://github.com/ctrf-io/ctrf-core-js/blob/main/types/ctrf.d.ts#L123) diff --git a/docs/interfaces/TestTree.md b/docs/interfaces/TestTree.md index 48a85a0..09c6a3c 100644 --- a/docs/interfaces/TestTree.md +++ b/docs/interfaces/TestTree.md @@ -6,7 +6,7 @@ # Interface: TestTree -Defined in: src/methods/tree-hierarchical-structure.ts:44 +Defined in: [src/methods/tree-hierarchical-structure.ts:44](https://github.com/ctrf-io/ctrf-core-js/blob/main/src/methods/tree-hierarchical-structure.ts#L44) Result of converting tests to tree structure @@ -16,7 +16,7 @@ Result of converting tests to tree structure > **roots**: [`TreeNode`](TreeNode.md)[] -Defined in: src/methods/tree-hierarchical-structure.ts:46 +Defined in: [src/methods/tree-hierarchical-structure.ts:46](https://github.com/ctrf-io/ctrf-core-js/blob/main/src/methods/tree-hierarchical-structure.ts#L46) Root nodes of the tree (top-level suites) @@ -26,6 +26,6 @@ Root nodes of the tree (top-level suites) > `optional` **summary**: [`Summary`](Summary.md) -Defined in: src/methods/tree-hierarchical-structure.ts:48 +Defined in: [src/methods/tree-hierarchical-structure.ts:48](https://github.com/ctrf-io/ctrf-core-js/blob/main/src/methods/tree-hierarchical-structure.ts#L48) Overall statistics for all tests (only present when includeSummary is true) diff --git a/docs/interfaces/TreeNode.md b/docs/interfaces/TreeNode.md index 1cc7d5c..d2b74ed 100644 --- a/docs/interfaces/TreeNode.md +++ b/docs/interfaces/TreeNode.md @@ -6,7 +6,7 @@ # Interface: TreeNode -Defined in: src/methods/tree-hierarchical-structure.ts:16 +Defined in: [src/methods/tree-hierarchical-structure.ts:16](https://github.com/ctrf-io/ctrf-core-js/blob/main/src/methods/tree-hierarchical-structure.ts#L16) Represents a tree node (suite) that can contain tests and child suites Following the CTRF Suite Tree schema specification @@ -17,7 +17,7 @@ Following the CTRF Suite Tree schema specification > **duration**: `number` -Defined in: src/methods/tree-hierarchical-structure.ts:22 +Defined in: [src/methods/tree-hierarchical-structure.ts:22](https://github.com/ctrf-io/ctrf-core-js/blob/main/src/methods/tree-hierarchical-structure.ts#L22) Total duration of all tests in this suite and children @@ -27,7 +27,7 @@ Total duration of all tests in this suite and children > `optional` **extra**: `Record`\<`string`, `unknown`\> -Defined in: src/methods/tree-hierarchical-structure.ts:30 +Defined in: [src/methods/tree-hierarchical-structure.ts:30](https://github.com/ctrf-io/ctrf-core-js/blob/main/src/methods/tree-hierarchical-structure.ts#L30) Additional properties @@ -37,7 +37,7 @@ Additional properties > **name**: `string` -Defined in: src/methods/tree-hierarchical-structure.ts:18 +Defined in: [src/methods/tree-hierarchical-structure.ts:18](https://github.com/ctrf-io/ctrf-core-js/blob/main/src/methods/tree-hierarchical-structure.ts#L18) The name of this suite @@ -47,7 +47,7 @@ The name of this suite > **status**: [`TestStatus`](../type-aliases/TestStatus.md) -Defined in: src/methods/tree-hierarchical-structure.ts:20 +Defined in: [src/methods/tree-hierarchical-structure.ts:20](https://github.com/ctrf-io/ctrf-core-js/blob/main/src/methods/tree-hierarchical-structure.ts#L20) The status of this suite (derived from child test results) @@ -57,7 +57,7 @@ The status of this suite (derived from child test results) > **suites**: `TreeNode`[] -Defined in: src/methods/tree-hierarchical-structure.ts:28 +Defined in: [src/methods/tree-hierarchical-structure.ts:28](https://github.com/ctrf-io/ctrf-core-js/blob/main/src/methods/tree-hierarchical-structure.ts#L28) Child suites contained within this suite @@ -67,7 +67,7 @@ Child suites contained within this suite > `optional` **summary**: [`Summary`](Summary.md) -Defined in: src/methods/tree-hierarchical-structure.ts:24 +Defined in: [src/methods/tree-hierarchical-structure.ts:24](https://github.com/ctrf-io/ctrf-core-js/blob/main/src/methods/tree-hierarchical-structure.ts#L24) Aggregated statistics for this suite (only present when includeSummary is true) @@ -77,6 +77,6 @@ Aggregated statistics for this suite (only present when includeSummary is true) > **tests**: [`TreeTest`](../type-aliases/TreeTest.md)[] -Defined in: src/methods/tree-hierarchical-structure.ts:26 +Defined in: [src/methods/tree-hierarchical-structure.ts:26](https://github.com/ctrf-io/ctrf-core-js/blob/main/src/methods/tree-hierarchical-structure.ts#L26) Tests directly contained in this suite diff --git a/docs/interfaces/TreeOptions.md b/docs/interfaces/TreeOptions.md index 990e47b..6399485 100644 --- a/docs/interfaces/TreeOptions.md +++ b/docs/interfaces/TreeOptions.md @@ -6,7 +6,7 @@ # Interface: TreeOptions -Defined in: src/methods/tree-hierarchical-structure.ts:36 +Defined in: [src/methods/tree-hierarchical-structure.ts:36](https://github.com/ctrf-io/ctrf-core-js/blob/main/src/methods/tree-hierarchical-structure.ts#L36) Options for controlling tree structure creation @@ -16,6 +16,6 @@ Options for controlling tree structure creation > `optional` **includeSummary**: `boolean` -Defined in: src/methods/tree-hierarchical-structure.ts:38 +Defined in: [src/methods/tree-hierarchical-structure.ts:38](https://github.com/ctrf-io/ctrf-core-js/blob/main/src/methods/tree-hierarchical-structure.ts#L38) Whether to include summary statistics aggregation (default: true) diff --git a/docs/type-aliases/TreeTest.md b/docs/type-aliases/TreeTest.md index 4a153c8..dd92522 100644 --- a/docs/type-aliases/TreeTest.md +++ b/docs/type-aliases/TreeTest.md @@ -8,7 +8,7 @@ > **TreeTest** = [`Test`](../interfaces/Test.md) & `object` -Defined in: src/methods/tree-hierarchical-structure.ts:7 +Defined in: [src/methods/tree-hierarchical-structure.ts:7](https://github.com/ctrf-io/ctrf-core-js/blob/main/src/methods/tree-hierarchical-structure.ts#L7) Tree test extends CTRF Test with a nodeType field for tree traversal diff --git a/src/index.ts b/src/index.ts index 268a09b..a2edc96 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,19 +1,45 @@ +/** + * @group Methods + */ export { mergeReports } from './methods/merge-reports.js' +/** + * @group Methods + */ export { readReportsFromDirectory } from './methods/read-reports.js' +/** + * @group Methods + */ export { readReportsFromGlobPattern } from './methods/read-reports.js' +/** + * @group Methods + */ export { enrichReportWithInsights } from './methods/run-insights.js' +/** + * @group Methods + */ export { sortReportsByTimestamp, SortOrder, } from './methods/utilities/sort-reports.js' +/** + * @group Methods + */ export { storePreviousResults } from './methods/store-previous-results.js' +/** + * @group Methods + */ export { readReportFromFile } from './methods/read-reports.js' +/** + * @group Methods + */ export { validateReport, validateReportStrict, isValidCtrfReport, - type ValidationResult, } from './methods/validate-schema.js' +/** + * @group Methods + */ export { organizeTestsBySuite, traverseTree, @@ -22,12 +48,11 @@ export { flattenTree, getAllTests, getSuiteStats, - type TreeNode, - type TreeTest, - type TreeOptions, - type TestTree, } from './methods/tree-hierarchical-structure.js' +/** + * @group Schema + */ export type { Report, Results, @@ -38,8 +63,24 @@ export type { Step, Attachment, RetryAttempt, - Insights, + RootInsights, TestInsights, + Baseline, InsightsMetric, TestStatus, } from '../types/ctrf.js' + +/** + * @group Utility Types + */ +export type { + TreeNode, + TreeTest, + TreeOptions, + TestTree, +} from './methods/tree-hierarchical-structure.js' + +/** + * @group Utility Types + */ +export type { ValidationResult } from './methods/validate-schema.js' diff --git a/src/methods/run-insights.ts b/src/methods/run-insights.ts index cdf800e..b820143 100644 --- a/src/methods/run-insights.ts +++ b/src/methods/run-insights.ts @@ -3,7 +3,7 @@ import { Report, Test, TestInsights, - Insights, + RootInsights, } from '../../types/ctrf.js' import { sortReportsByTimestamp } from './utilities/sort-reports.js' @@ -625,13 +625,13 @@ function addTestInsightsWithBaselineToCurrentReport( function calculateReportInsightsBaseline( currentReport: Report, baslineReport: Report -): Insights { +): RootInsights { const currentInsights = currentReport.insights const previousInsights = baslineReport.insights if (!currentInsights || !previousInsights) { console.log('Both reports must have insights populated') - return currentReport.insights as Insights + return currentReport.insights as RootInsights } return { @@ -767,10 +767,10 @@ function getTestsAddedSinceBaseline( * @returns The insights object with testsRemoved added to extra */ function setTestsRemovedToInsights( - insights: Insights, + insights: RootInsights, currentReport: Report, baselineReport: Report -): Insights { +): RootInsights { const removedTests = getTestsRemovedSinceBaseline( currentReport, baselineReport @@ -795,10 +795,10 @@ function setTestsRemovedToInsights( * @returns The insights object with testsAdded added to extra */ function setTestsAddedToInsights( - insights: Insights, + insights: RootInsights, currentReport: Report, baselineReport: Report -): Insights { +): RootInsights { const addedTests = getTestsAddedSinceBaseline(currentReport, baselineReport) return { diff --git a/typedoc.json b/typedoc.json index 633d598..ac4448b 100644 --- a/typedoc.json +++ b/typedoc.json @@ -7,7 +7,8 @@ "excludeExternals": true, "includeVersion": true, "categorizeByGroup": true, - "categoryOrder": ["Methods", "*"], + "categoryOrder": ["Schema", "Utility Types", "Methods", "*"], + "groupOrder": ["Schema", "Utility Types", "Methods", "*"], "readme": "none", "plugin": ["typedoc-plugin-markdown"], "name": "CTRF ", diff --git a/types/ctrf.d.ts b/types/ctrf.d.ts index 5d370c4..102622a 100644 --- a/types/ctrf.d.ts +++ b/types/ctrf.d.ts @@ -5,7 +5,7 @@ export interface Report { timestamp?: string generatedBy?: string results: Results - insights?: Insights + insights?: RootInsights baseline?: Baseline extra?: Record } @@ -120,7 +120,7 @@ export interface RetryAttempt { extra?: Record } -export interface Insights { +export interface RootInsights { runsAnalyzed?: number passRate?: InsightsMetric failRate?: InsightsMetric From 0abe6f63c06d9359c7d193eec8593b353d3de294 Mon Sep 17 00:00:00 2001 From: Matthew Thomas Date: Fri, 10 Oct 2025 22:04:10 +0100 Subject: [PATCH 3/4] feat: enhance README and documentation structure with categorized API references --- README.md | 59 ++++++++++++++++++++++++-- docs/README.md | 29 ++++++++----- package.json | 1 + src/index.ts | 24 +++++------ typedoc.json | 20 ++++++++- update-readme.ts | 107 +++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 213 insertions(+), 27 deletions(-) create mode 100644 update-readme.ts diff --git a/README.md b/README.md index 8d5ca2b..01cc244 100644 --- a/README.md +++ b/README.md @@ -33,9 +33,62 @@ We’d love your feedback, { + if (url.startsWith('http') || url.startsWith('docs/')) { + return match + } + return `](docs/${url})` + }) + + if (['Enumerations', 'Interfaces', 'Type Aliases'].includes(section.name)) { + if (!utilityTypesAdded) { + apiContent += '\n### Utility Types\n\n' + utilityTypesAdded = true + } + const lines = sectionContent.split('\n') + let listItems = lines.filter(line => line.startsWith('- [')) + if (section.name === 'Enumerations' && listItems.length > 0) { + listItems = listItems.map(item => item + ' (enumeration)') + } + if (listItems.length > 0) { + apiContent += listItems.join('\n') + '\n' + } + } else { + sectionContent = sectionContent.replace( + `## ${section.name}`, + `### ${section.newName}` + ) + apiContent += '\n' + sectionContent + '\n' + } + } + + return apiContent.trim() +} + +function updateReadmeWithDocs() { + try { + const originalReadme = fs.readFileSync(README_PATH, 'utf-8') + + const generatedDocs = fs.readFileSync(DOCS_PATH, 'utf-8') + + const apiReferenceContent = extractCategorizedSections(generatedDocs) + + const originalApiReferenceStart = originalReadme.indexOf('## API Reference') + const nextSectionStart = originalReadme.indexOf( + '## ', + originalApiReferenceStart + 1 + ) + + if (originalApiReferenceStart === -1) { + console.error('Could not find "## API Reference" section in README.md') + process.exit(1) + } + + const beforeApiReference = originalReadme.substring( + 0, + originalApiReferenceStart + ) + + const afterApiReference = + nextSectionStart !== -1 ? originalReadme.substring(nextSectionStart) : '' + + const newReadme = + beforeApiReference + + apiReferenceContent + + (afterApiReference ? '\n\n' + afterApiReference : '') + + fs.writeFileSync(README_PATH, newReadme) + + console.log('✅ README.md updated with categorized API documentation') + } catch (error) { + console.error('❌ Error updating README:', error) + process.exit(1) + } +} + +updateReadmeWithDocs() From 284f3510fc7e559b082659ef48da72dc4fed0ec7 Mon Sep 17 00:00:00 2001 From: Matthew Thomas Date: Fri, 10 Oct 2025 22:56:05 +0100 Subject: [PATCH 4/4] feat: add test operations --- README.md | 9 + docs/README.md | 12 ++ docs/functions/findTestById.md | 33 +++ .../functions/generateTestIdFromProperties.md | 39 ++++ docs/functions/getTestId.md | 27 +++ docs/functions/setTestId.md | 27 +++ docs/functions/setTestIdsForReport.md | 27 +++ docs/variables/CTRF_NAMESPACE.md | 15 ++ examples/test-id-example.md | 99 +++++++++ src/index.ts | 11 + src/methods/test-id.test.ts | 191 ++++++++++++++++++ src/methods/test-id.ts | 101 +++++++++ typedoc.json | 2 + update-readme.ts | 4 +- 14 files changed, 596 insertions(+), 1 deletion(-) create mode 100644 docs/functions/findTestById.md create mode 100644 docs/functions/generateTestIdFromProperties.md create mode 100644 docs/functions/getTestId.md create mode 100644 docs/functions/setTestId.md create mode 100644 docs/functions/setTestIdsForReport.md create mode 100644 docs/variables/CTRF_NAMESPACE.md create mode 100644 examples/test-id-example.md create mode 100644 src/methods/test-id.test.ts create mode 100644 src/methods/test-id.ts diff --git a/README.md b/README.md index 01cc244..02c0353 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,14 @@ npm install ctrf - [organizeTestsBySuite](docs/functions/organizeTestsBySuite.md) - [traverseTree](docs/functions/traverseTree.md) +### Test Operations Methods + +- [findTestById](docs/functions/findTestById.md) +- [generateTestIdFromProperties](docs/functions/generateTestIdFromProperties.md) +- [getTestId](docs/functions/getTestId.md) +- [setTestId](docs/functions/setTestId.md) +- [setTestIdsForReport](docs/functions/setTestIdsForReport.md) + ### Utility Types - [SortOrder](docs/enumerations/SortOrder.md) (enumeration) @@ -89,6 +97,7 @@ npm install ctrf - [TreeOptions](docs/interfaces/TreeOptions.md) - [ValidationResult](docs/interfaces/ValidationResult.md) - [TreeTest](docs/type-aliases/TreeTest.md) +- [CTRF\_NAMESPACE](docs/variables/CTRF_NAMESPACE.md) ## TypeScript Types diff --git a/docs/README.md b/docs/README.md index eb91f39..5ce34eb 100644 --- a/docs/README.md +++ b/docs/README.md @@ -50,6 +50,14 @@ - [organizeTestsBySuite](functions/organizeTestsBySuite.md) - [traverseTree](functions/traverseTree.md) +## Test Operations + +- [findTestById](functions/findTestById.md) +- [generateTestIdFromProperties](functions/generateTestIdFromProperties.md) +- [getTestId](functions/getTestId.md) +- [setTestId](functions/setTestId.md) +- [setTestIdsForReport](functions/setTestIdsForReport.md) + ## Enumerations - [SortOrder](enumerations/SortOrder.md) @@ -64,3 +72,7 @@ ## Type Aliases - [TreeTest](type-aliases/TreeTest.md) + +## Variables + +- [CTRF\_NAMESPACE](variables/CTRF_NAMESPACE.md) diff --git a/docs/functions/findTestById.md b/docs/functions/findTestById.md new file mode 100644 index 0000000..65c32ab --- /dev/null +++ b/docs/functions/findTestById.md @@ -0,0 +1,33 @@ +[**CTRF v0.0.16**](../README.md) + +*** + +[CTRF](../README.md) / findTestById + +# Function: findTestById() + +> **findTestById**(`report`, `testId`): `undefined` \| [`Test`](../interfaces/Test.md) + +Defined in: src/methods/test-id.ts:84 + +Finds a test by its ID in a report + +## Parameters + +### report + +[`Report`](../interfaces/Report.md) + +The CTRF report + +### testId + +`string` + +The test ID to search for + +## Returns + +`undefined` \| [`Test`](../interfaces/Test.md) + +The test object if found, undefined otherwise diff --git a/docs/functions/generateTestIdFromProperties.md b/docs/functions/generateTestIdFromProperties.md new file mode 100644 index 0000000..a5ccf83 --- /dev/null +++ b/docs/functions/generateTestIdFromProperties.md @@ -0,0 +1,39 @@ +[**CTRF v0.0.16**](../README.md) + +*** + +[CTRF](../README.md) / generateTestIdFromProperties + +# Function: generateTestIdFromProperties() + +> **generateTestIdFromProperties**(`name`, `suite?`, `filePath?`): `string` + +Defined in: src/methods/test-id.ts:95 + +Generates a new test ID based on test properties (exposed utility) + +## Parameters + +### name + +`string` + +Test name + +### suite? + +`string`[] + +Test suite path + +### filePath? + +`string` + +Test file path + +## Returns + +`string` + +A deterministic UUID v5 string based on the properties diff --git a/docs/functions/getTestId.md b/docs/functions/getTestId.md new file mode 100644 index 0000000..5b72f86 --- /dev/null +++ b/docs/functions/getTestId.md @@ -0,0 +1,27 @@ +[**CTRF v0.0.16**](../README.md) + +*** + +[CTRF](../README.md) / getTestId + +# Function: getTestId() + +> **getTestId**(`test`): `string` + +Defined in: src/methods/test-id.ts:61 + +Gets the test ID from a test object, generating one if it doesn't exist + +## Parameters + +### test + +[`Test`](../interfaces/Test.md) + +The test object to get the ID from + +## Returns + +`string` + +The test ID diff --git a/docs/functions/setTestId.md b/docs/functions/setTestId.md new file mode 100644 index 0000000..a16d36a --- /dev/null +++ b/docs/functions/setTestId.md @@ -0,0 +1,27 @@ +[**CTRF v0.0.16**](../README.md) + +*** + +[CTRF](../README.md) / setTestId + +# Function: setTestId() + +> **setTestId**(`test`): [`Test`](../interfaces/Test.md) + +Defined in: src/methods/test-id.ts:49 + +Sets a test ID for a test object based on its properties + +## Parameters + +### test + +[`Test`](../interfaces/Test.md) + +The test object to add an ID to + +## Returns + +[`Test`](../interfaces/Test.md) + +The test object with the ID set diff --git a/docs/functions/setTestIdsForReport.md b/docs/functions/setTestIdsForReport.md new file mode 100644 index 0000000..c3f70ae --- /dev/null +++ b/docs/functions/setTestIdsForReport.md @@ -0,0 +1,27 @@ +[**CTRF v0.0.16**](../README.md) + +*** + +[CTRF](../README.md) / setTestIdsForReport + +# Function: setTestIdsForReport() + +> **setTestIdsForReport**(`report`): [`Report`](../interfaces/Report.md) + +Defined in: src/methods/test-id.ts:73 + +Sets test IDs for all tests in a report + +## Parameters + +### report + +[`Report`](../interfaces/Report.md) + +The CTRF report + +## Returns + +[`Report`](../interfaces/Report.md) + +The report with test IDs set for all tests diff --git a/docs/variables/CTRF_NAMESPACE.md b/docs/variables/CTRF_NAMESPACE.md new file mode 100644 index 0000000..67b34a7 --- /dev/null +++ b/docs/variables/CTRF_NAMESPACE.md @@ -0,0 +1,15 @@ +[**CTRF v0.0.16**](../README.md) + +*** + +[CTRF](../README.md) / CTRF\_NAMESPACE + +# Variable: CTRF\_NAMESPACE + +> `const` **CTRF\_NAMESPACE**: `"6ba7b810-9dad-11d1-80b4-00c04fd430c8"` = `'6ba7b810-9dad-11d1-80b4-00c04fd430c8'` + +Defined in: src/methods/test-id.ts:11 + +The CTRF namespace UUID used for generating deterministic test IDs. +This namespace ensures that all CTRF test IDs are generated consistently +across different implementations and tools. diff --git a/examples/test-id-example.md b/examples/test-id-example.md new file mode 100644 index 0000000..ec9019d --- /dev/null +++ b/examples/test-id-example.md @@ -0,0 +1,99 @@ +# Test ID Operations Example + +This example demonstrates how to use the new deterministic test ID functionality. + +```typescript +import { + setTestId, + getTestId, + setTestIdsForReport, + findTestById, + generateTestIdFromProperties +} from 'ctrf' +import type { Test, Report } from 'ctrf' + +// Example test object +const test: Test = { + name: 'should authenticate user', + status: 'passed', + duration: 150, + suite: ['auth', 'login'], + filePath: 'src/auth/login.test.ts' +} + +// Set a test ID (generates deterministic UUID based on properties) +setTestId(test) +console.log('Test ID:', test.id) // Always the same UUID for these properties! + +// Get a test ID (generates one if not present) +const testId = getTestId(test) +console.log('Test ID:', testId) // Same as above + +// Generate a test ID from properties - always deterministic! +const customId = generateTestIdFromProperties( + 'my test', + ['suite1', 'suite2'], + 'my-test.ts' +) +console.log('Generated ID:', customId) // Always the same for these inputs + +// Demonstrate deterministic behavior +const sameId = generateTestIdFromProperties( + 'my test', + ['suite1', 'suite2'], + 'my-test.ts' +) +console.log('Same ID?', customId === sameId) // true! + +// Set IDs for all tests in a report +const report: Report = { + reportFormat: 'CTRF', + specVersion: '1.0.0', + results: { + tool: { name: 'vitest' }, + summary: { + tests: 2, + passed: 2, + failed: 0, + skipped: 0, + pending: 0, + other: 0, + start: Date.now(), + stop: Date.now() + 1000 + }, + tests: [ + { + name: 'test 1', + status: 'passed', + duration: 100, + suite: ['unit'], + filePath: 'test1.ts' + }, + { + name: 'test 2', + status: 'passed', + duration: 200, + suite: ['integration'], + filePath: 'test2.ts' + } + ] + } +} + +// Set IDs for all tests +setTestIdsForReport(report) + +// Find a test by its ID +const foundTest = findTestById(report, report.results.tests[0].id!) +console.log('Found test:', foundTest?.name) +``` + +## Key Features + +1. **Deterministic UUIDs**: Same test properties always generate the same UUID +2. **Proper UUID format**: Valid UUIDs that follow the standard format +3. **Non-destructive**: Won't overwrite existing IDs +4. **Property-based**: Uses test name, suite, and filePath for generation +5. **Report-level operations**: Can process entire reports at once +6. **Search functionality**: Find tests by their deterministic IDs +7. **Consistent across runs**: Same test will always have the same ID diff --git a/src/index.ts b/src/index.ts index 046cdf5..8241366 100644 --- a/src/index.ts +++ b/src/index.ts @@ -49,6 +49,17 @@ export { getAllTests, getSuiteStats, } from './methods/tree-hierarchical-structure.js' +/** + * @group Test Operations + */ +export { + setTestId, + getTestId, + setTestIdsForReport, + findTestById, + generateTestIdFromProperties, + CTRF_NAMESPACE, +} from './methods/test-id.js' /** * @group Schema diff --git a/src/methods/test-id.test.ts b/src/methods/test-id.test.ts new file mode 100644 index 0000000..798791b --- /dev/null +++ b/src/methods/test-id.test.ts @@ -0,0 +1,191 @@ +import { describe, it, expect } from 'vitest' +import { + setTestId, + getTestId, + setTestIdsForReport, + findTestById, + generateTestIdFromProperties, + CTRF_NAMESPACE, +} from './test-id.js' +import type { Test, Report } from '../../types/ctrf.js' + +describe('test-id', () => { + const mockTest: Test = { + name: 'should pass', + status: 'passed', + duration: 100, + suite: ['unit', 'auth'], + filePath: 'src/auth.test.ts', + } + + const mockReport: Report = { + reportFormat: 'CTRF', + specVersion: '1.0.0', + results: { + tool: { name: 'vitest' }, + summary: { + tests: 2, + passed: 2, + failed: 0, + skipped: 0, + pending: 0, + other: 0, + start: 1234567890, + stop: 1234567990, + }, + tests: [ + { + name: 'test 1', + status: 'passed', + duration: 50, + suite: ['unit'], + filePath: 'test1.ts', + }, + { + name: 'test 2', + status: 'passed', + duration: 75, + suite: ['integration'], + filePath: 'test2.ts', + }, + ], + }, + } + + describe('setTestId', () => { + it('should add a deterministic UUID to a test without one', () => { + const test = { ...mockTest } + const result = setTestId(test) + + expect(result.id).toBeDefined() + expect(typeof result.id).toBe('string') + expect(result.id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i) + + const test2 = { ...mockTest } + const result2 = setTestId(test2) + expect(result.id).toBe(result2.id) + }) + + it('should not overwrite existing ID', () => { + const test = { ...mockTest, id: 'existing-id' } + const result = setTestId(test) + + expect(result.id).toBe('existing-id') + }) + }) + + describe('getTestId', () => { + it('should return existing ID', () => { + const test = { ...mockTest, id: 'existing-id' } + const result = getTestId(test) + + expect(result).toBe('existing-id') + }) + + it('should generate and return deterministic UUID if none exists', () => { + const test = { ...mockTest } + const result = getTestId(test) + + expect(result).toBeDefined() + expect(typeof result).toBe('string') + expect(result).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i) + expect(test.id).toBe(result) + + const test2 = { ...mockTest } + const result2 = getTestId(test2) + expect(result).toBe(result2) + }) + }) + + describe('setTestIdsForReport', () => { + it('should set IDs for all tests in a report', () => { + const report = JSON.parse(JSON.stringify(mockReport)) + const result = setTestIdsForReport(report) + + expect(result.results.tests).toHaveLength(2) + expect(result.results.tests[0].id).toBeDefined() + expect(result.results.tests[1].id).toBeDefined() + expect(result.results.tests[0].id).not.toBe(result.results.tests[1].id) + }) + + it('should not overwrite existing IDs', () => { + const report = JSON.parse(JSON.stringify(mockReport)) + report.results.tests[0].id = 'existing-id' + + const result = setTestIdsForReport(report) + + expect(result.results.tests[0].id).toBe('existing-id') + expect(result.results.tests[1].id).toBeDefined() + expect(result.results.tests[1].id).not.toBe('existing-id') + }) + }) + + describe('findTestById', () => { + it('should find test by ID', () => { + const report = JSON.parse(JSON.stringify(mockReport)) + report.results.tests[0].id = 'test-id-1' + report.results.tests[1].id = 'test-id-2' + + const result = findTestById(report, 'test-id-1') + + expect(result).toBeDefined() + expect(result?.name).toBe('test 1') + }) + + it('should return undefined for non-existent ID', () => { + const report = JSON.parse(JSON.stringify(mockReport)) + + const result = findTestById(report, 'non-existent-id') + + expect(result).toBeUndefined() + }) + }) + + describe('generateTestIdFromProperties', () => { + it('should generate deterministic UUID from properties', () => { + const result = generateTestIdFromProperties( + 'test name', + ['suite1', 'suite2'], + 'test.ts' + ) + + expect(result).toBeDefined() + expect(typeof result).toBe('string') + expect(result).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i) + + const result2 = generateTestIdFromProperties( + 'test name', + ['suite1', 'suite2'], + 'test.ts' + ) + expect(result).toBe(result2) + }) + + it('should handle missing optional parameters', () => { + const result = generateTestIdFromProperties('test name') + + expect(result).toBeDefined() + expect(typeof result).toBe('string') + expect(result).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i) + }) + + it('should generate different UUIDs for different properties', () => { + const result1 = generateTestIdFromProperties('test1', ['suite1'], 'file1.ts') + const result2 = generateTestIdFromProperties('test2', ['suite2'], 'file2.ts') + + expect(result1).not.toBe(result2) + }) + }) + + describe('CTRF_NAMESPACE', () => { + it('should be a valid UUID', () => { + expect(CTRF_NAMESPACE).toBeDefined() + expect(typeof CTRF_NAMESPACE).toBe('string') + expect(CTRF_NAMESPACE).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i) + }) + + it('should be stable and not change', () => { + expect(CTRF_NAMESPACE).toBe('6ba7b810-9dad-11d1-80b4-00c04fd430c8') + }) + }) +}) \ No newline at end of file diff --git a/src/methods/test-id.ts b/src/methods/test-id.ts new file mode 100644 index 0000000..4dcfc87 --- /dev/null +++ b/src/methods/test-id.ts @@ -0,0 +1,101 @@ +import { createHash } from 'crypto' +import type { Test, Report } from '../../types/ctrf.js' + +/** + * The CTRF namespace UUID used for generating deterministic test IDs. + * This namespace ensures that all CTRF test IDs are generated consistently + * across different implementations and tools. + * + * @public + */ +export const CTRF_NAMESPACE = '6ba7b810-9dad-11d1-80b4-00c04fd430c8' + +/** + * Generates a deterministic UUID v5 based on test properties + * @param name - Test name + * @param suite - Test suite path + * @param filePath - Test file path + * @returns A deterministic UUID v5 string based on the properties + */ +function generateTestId(name: string, suite?: string[], filePath?: string): string { + const suiteString = suite ? suite.join('/') : '' + const identifier = `${name}|${suiteString}|${filePath || ''}` + + const namespaceBytes = CTRF_NAMESPACE.replace(/-/g, '').match(/.{2}/g)!.map(byte => parseInt(byte, 16)) + + const input = Buffer.concat([ + Buffer.from(namespaceBytes), + Buffer.from(identifier, 'utf8') + ]) + + const hash = createHash('sha1').update(input).digest('hex') + + const uuid = [ + hash.substring(0, 8), + hash.substring(8, 12), + '5' + hash.substring(13, 16), + ((parseInt(hash.substring(16, 17), 16) & 0x3) | 0x8).toString(16) + hash.substring(17, 20), + hash.substring(20, 32) + ].join('-') + + return uuid +} + +/** + * Sets a test ID for a test object based on its properties + * @param test - The test object to add an ID to + * @returns The test object with the ID set + */ +export function setTestId(test: Test): Test { + if (!test.id) { + test.id = generateTestId(test.name, test.suite, test.filePath) + } + return test +} + +/** + * Gets the test ID from a test object, generating one if it doesn't exist + * @param test - The test object to get the ID from + * @returns The test ID + */ +export function getTestId(test: Test): string { + if (!test.id) { + test.id = generateTestId(test.name, test.suite, test.filePath) + } + return test.id +} + +/** + * Sets test IDs for all tests in a report + * @param report - The CTRF report + * @returns The report with test IDs set for all tests + */ +export function setTestIdsForReport(report: Report): Report { + report.results.tests.forEach(test => setTestId(test)) + return report +} + +/** + * Finds a test by its ID in a report + * @param report - The CTRF report + * @param testId - The test ID to search for + * @returns The test object if found, undefined otherwise + */ +export function findTestById(report: Report, testId: string): Test | undefined { + return report.results.tests.find(test => test.id === testId) +} + +/** + * Generates a new test ID based on test properties (exposed utility) + * @param name - Test name + * @param suite - Test suite path + * @param filePath - Test file path + * @returns A deterministic UUID v5 string based on the properties + */ +export function generateTestIdFromProperties( + name: string, + suite?: string[], + filePath?: string +): string { + return generateTestId(name, suite, filePath) +} \ No newline at end of file diff --git a/typedoc.json b/typedoc.json index 87335d1..57d701b 100644 --- a/typedoc.json +++ b/typedoc.json @@ -13,6 +13,7 @@ "Report Processing", "Validation", "Tree Operations", + "Test Operations", "Utility Types", "*" ], @@ -22,6 +23,7 @@ "Report Processing", "Validation", "Tree Operations", + "Test Operations", "Utility Types", "*" ], diff --git a/update-readme.ts b/update-readme.ts index 84fccad..fa21d80 100644 --- a/update-readme.ts +++ b/update-readme.ts @@ -13,9 +13,11 @@ function extractCategorizedSections(docsContent: string): string { { name: 'Report Processing', newName: 'Report Processing Methods' }, { name: 'Validation', newName: 'Validation Methods' }, { name: 'Tree Operations', newName: 'Tree Operations Methods' }, + { name: 'Test Operations', newName: 'Test Operations Methods' }, { name: 'Enumerations', newName: 'Utility Types' }, { name: 'Interfaces', newName: 'Utility Types' }, { name: 'Type Aliases', newName: 'Utility Types' }, + { name: 'Variables', newName: 'Utility Types' }, ] let apiContent = '## API Reference\n' @@ -38,7 +40,7 @@ function extractCategorizedSections(docsContent: string): string { return `](docs/${url})` }) - if (['Enumerations', 'Interfaces', 'Type Aliases'].includes(section.name)) { + if (['Enumerations', 'Interfaces', 'Type Aliases', 'Variables'].includes(section.name)) { if (!utilityTypesAdded) { apiContent += '\n### Utility Types\n\n' utilityTypesAdded = true