@allurereport/web-commons 3.17.0 → 3.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -201,6 +201,7 @@ export const attachmentType = (type) => {
201
201
  case "image/jpeg":
202
202
  case "image/jpg":
203
203
  case "image/png":
204
+ case "image/webp":
204
205
  case "image/*":
205
206
  return "image";
206
207
  case "text/xml":
@@ -1,3 +1,4 @@
1
+ import { createHistoryTestResultLookup, } from "@allurereport/core-api";
1
2
  import { createTreeByLabels, md5 } from "@allurereport/plugin-api";
2
3
  import { isChildrenLeavesOnly } from "../chart-utils.js";
3
4
  import { convertTreeDataToTreeMapNode, transformTreeMapNode } from "../treeMapUtils.js";
@@ -35,22 +36,20 @@ const calculateColorValue = (metrics) => {
35
36
  };
36
37
  const isSkipped = (tr) => tr.status === "skipped";
37
38
  const getNewTestResults = (trs, closestHtrs) => {
38
- return trs.filter((tr) => !closestHtrs[tr.historyId]);
39
+ return trs.filter((tr) => !closestHtrs[tr.retryHash]);
39
40
  };
40
- const getRemovedTestResults = (trs, closestHtrs) => {
41
- const historyPointTestResultsAsArray = Object.values(closestHtrs);
42
- const testResultsAsDictionary = Object.fromEntries(trs.map((tr) => [tr.historyId, tr]));
43
- return historyPointTestResultsAsArray.filter((htr) => !testResultsAsDictionary[htr.historyId]);
41
+ const getRemovedTestResults = (historyTestResults, matchedHistoryTestResults) => {
42
+ return historyTestResults.filter((htr) => !matchedHistoryTestResults.has(htr));
44
43
  };
45
44
  const getEnabledTestResults = (trs, closestHtrs) => {
46
45
  return trs.filter((tr) => {
47
- const historyPointTestResult = closestHtrs[tr.historyId];
46
+ const historyPointTestResult = closestHtrs[tr.retryHash];
48
47
  return historyPointTestResult && isSkipped(historyPointTestResult) && !isSkipped(tr);
49
48
  });
50
49
  };
51
50
  const getDisabledTestResults = (trs, closestHtrs) => {
52
51
  return trs.filter((tr) => {
53
- const historyPointTestResult = closestHtrs[tr.historyId];
52
+ const historyPointTestResult = closestHtrs[tr.retryHash];
54
53
  return historyPointTestResult && !isSkipped(historyPointTestResult) && isSkipped(tr);
55
54
  });
56
55
  };
@@ -80,33 +79,33 @@ const calculateSubtreeMetrics = (node) => {
80
79
  }
81
80
  return { totalTests, newCount, deletedCount, disabledCount, enabledCount };
82
81
  };
83
- const createCoverageDiffTreeMap = (trs, closestHtrs) => {
82
+ const createCoverageDiffTreeMap = (trs, closestHtrs, historyTestResults = [], matchedHistoryTestResults = new Set()) => {
84
83
  const newTrs = getNewTestResults(trs, closestHtrs);
85
- const removedHtrs = getRemovedTestResults(trs, closestHtrs);
84
+ const removedHtrs = getRemovedTestResults(historyTestResults, matchedHistoryTestResults);
86
85
  const enabledTrs = getEnabledTestResults(trs, closestHtrs);
87
86
  const disabledTrs = getDisabledTestResults(trs, closestHtrs);
88
- const newTestsById = new Map(newTrs.map((tr) => [tr.historyId, tr]));
89
- const deletedTestsById = new Map(removedHtrs.map((htr) => [htr.historyId, htr]));
90
- const enabledTestsById = new Map(enabledTrs.map((tr) => [tr.historyId, tr]));
91
- const disabledTestsById = new Map(disabledTrs.map((tr) => [tr.historyId, tr]));
87
+ const newTestsById = new Map(newTrs.map((tr) => [tr.retryHash, tr]));
88
+ const deletedTestsById = new Map(removedHtrs.map((htr) => [htr.retryHash, htr]));
89
+ const enabledTestsById = new Map(enabledTrs.map((tr) => [tr.retryHash, tr]));
90
+ const disabledTestsById = new Map(disabledTrs.map((tr) => [tr.retryHash, tr]));
92
91
  const allTests = [...trs, ...removedHtrs];
93
- const getChangeType = (historyId) => {
94
- if (newTestsById.has(historyId)) {
92
+ const getChangeType = (retryHash) => {
93
+ if (newTestsById.has(retryHash)) {
95
94
  return "new";
96
95
  }
97
- if (deletedTestsById.has(historyId)) {
96
+ if (deletedTestsById.has(retryHash)) {
98
97
  return "deleted";
99
98
  }
100
- if (enabledTestsById.has(historyId)) {
99
+ if (enabledTestsById.has(retryHash)) {
101
100
  return "enabled";
102
101
  }
103
- if (disabledTestsById.has(historyId)) {
102
+ if (disabledTestsById.has(retryHash)) {
104
103
  return "disabled";
105
104
  }
106
105
  return "unchanged";
107
106
  };
108
107
  const leafFactoryFnWithMaps = (test) => {
109
- const changeType = getChangeType(test.historyId);
108
+ const changeType = getChangeType(test.retryHash);
110
109
  return {
111
110
  nodeId: test.id,
112
111
  name: test.name,
@@ -169,15 +168,29 @@ const createCoverageDiffTreeMap = (trs, closestHtrs) => {
169
168
  });
170
169
  };
171
170
  export const coverageDiffTreeMapAccessor = {
172
- getTreeMap: ({ testResults, historyDataPoints }) => {
171
+ getTreeMap: ({ testResults, historyDataPoints, allTestResults }) => {
172
+ const lookupHistoryTestResult = createHistoryTestResultLookup(allTestResults ?? testResults);
173
173
  const testsWithBehaviorLabels = filterTestsWithBehaviorLabels(testResults);
174
174
  if (!historyDataPoints || historyDataPoints.length === 0) {
175
175
  return createCoverageDiffTreeMap(testsWithBehaviorLabels, {});
176
176
  }
177
177
  const closestHdp = historyDataPoints[0];
178
- const closestHtrs = closestHdp.testResults;
179
- const closestHtrsWithBehaviorLabels = filterTestsWithBehaviorLabels(Object.values(closestHtrs));
180
- const closestHtrsWithBehaviorLabelsById = Object.fromEntries(closestHtrsWithBehaviorLabels.map((htr) => [htr.historyId, htr]));
181
- return createCoverageDiffTreeMap(testsWithBehaviorLabels, closestHtrsWithBehaviorLabelsById);
178
+ const closestHtrsWithBehaviorLabels = filterTestsWithBehaviorLabels(Object.values(closestHdp.testResults)).filter((tr) => {
179
+ const selected = lookupHistoryTestResult(closestHdp, tr);
180
+ return !selected || selected === tr;
181
+ });
182
+ const selectedHtrsByCurrentRetryHash = {};
183
+ const matchedHistoryTestResults = new Set();
184
+ for (const testResult of testsWithBehaviorLabels) {
185
+ if (!testResult.retryHash) {
186
+ continue;
187
+ }
188
+ const historicalTestResult = lookupHistoryTestResult(closestHdp, testResult);
189
+ if (historicalTestResult) {
190
+ selectedHtrsByCurrentRetryHash[testResult.retryHash] = historicalTestResult;
191
+ matchedHistoryTestResults.add(historicalTestResult);
192
+ }
193
+ }
194
+ return createCoverageDiffTreeMap(testsWithBehaviorLabels, selectedHtrsByCurrentRetryHash, closestHtrsWithBehaviorLabels, matchedHistoryTestResults);
182
195
  },
183
196
  };
@@ -2,6 +2,7 @@ import type { TreeMapDataAccessor, TreeMapNode } from "@allurereport/charts-api"
2
2
  import type { TestResult } from "@allurereport/core-api";
3
3
  type SubtreeMetrics = {
4
4
  totalTests: number;
5
+ eligibleCount: number;
5
6
  passedTests: number;
6
7
  failedTests: number;
7
8
  otherTests: number;
@@ -1,3 +1,4 @@
1
+ import { getSuccessRate, includedInSuccessRate } from "@allurereport/core-api";
1
2
  import { createTreeByLabels, md5 } from "@allurereport/plugin-api";
2
3
  import { isChildrenLeavesOnly } from "../chart-utils.js";
3
4
  import { convertTreeDataToTreeMapNode, transformTreeMapNode } from "../treeMapUtils.js";
@@ -11,6 +12,7 @@ const leafFactoryFn = ({ id, name, status }) => ({
11
12
  const groupFactoryFn = (parentId, groupClassifier) => ({
12
13
  nodeId: md5((parentId ? `${parentId}.` : "") + groupClassifier),
13
14
  name: groupClassifier,
15
+ eligibleCount: 0,
14
16
  value: 0,
15
17
  passedTests: 0,
16
18
  failedTests: 0,
@@ -18,34 +20,36 @@ const groupFactoryFn = (parentId, groupClassifier) => ({
18
20
  });
19
21
  const addLeafToGroupFn = (group, leaf) => {
20
22
  group.value += leaf.value;
23
+ group.eligibleCount += includedInSuccessRate.has(leaf.status) ? 1 : 0;
21
24
  group.passedTests += leaf.status === "passed" ? 1 : 0;
22
25
  group.failedTests += leaf.status === "failed" ? 1 : 0;
23
26
  group.otherTests += leaf?.status && !["passed", "failed"].includes(leaf.status) ? 1 : 0;
24
27
  };
25
- const calculateColorValue = ({ totalTests, passedTests }) => {
26
- return totalTests > 0 ? passedTests / totalTests : 0;
27
- };
28
+ const calculateColorValue = ({ eligibleCount, passedTests }) => getSuccessRate({ passed: passedTests, failed: eligibleCount - passedTests });
28
29
  const calculateSubtreeMetrics = (node) => {
29
30
  if (!node.children || node.children.length === 0) {
30
31
  return {
31
- totalTests: 1,
32
+ totalTests: node.status ? 1 : 0,
33
+ eligibleCount: node.status && includedInSuccessRate.has(node.status) ? 1 : 0,
32
34
  passedTests: node?.status === "passed" ? 1 : 0,
33
35
  failedTests: node?.status === "failed" ? 1 : 0,
34
36
  otherTests: node?.status && !["passed", "failed"].includes(node.status) ? 1 : 0,
35
37
  };
36
38
  }
37
39
  let totalTests = 0;
40
+ let eligibleCount = 0;
38
41
  let passedTests = 0;
39
42
  let failedTests = 0;
40
43
  let otherTests = 0;
41
44
  for (const child of node.children) {
42
45
  const childMetrics = calculateSubtreeMetrics(child);
43
46
  totalTests += childMetrics.totalTests;
47
+ eligibleCount += childMetrics.eligibleCount;
44
48
  passedTests += childMetrics.passedTests;
45
49
  failedTests += childMetrics.failedTests;
46
50
  otherTests += childMetrics.otherTests;
47
51
  }
48
- return { totalTests, passedTests, failedTests, otherTests };
52
+ return { totalTests, eligibleCount, passedTests, failedTests, otherTests };
49
53
  };
50
54
  export const createSuccessRateDistributionTreeMap = (testResults) => {
51
55
  const treeByLabels = createTreeByLabels(testResults, behaviorLabels, leafFactoryFn, groupFactoryFn, addLeafToGroupFn);
@@ -58,6 +62,7 @@ export const createSuccessRateDistributionTreeMap = (testResults) => {
58
62
  const group = node;
59
63
  return {
60
64
  ...baseNode,
65
+ eligibleCount: group.eligibleCount,
61
66
  passedTests: group.passedTests,
62
67
  failedTests: group.failedTests,
63
68
  otherTests: group.otherTests,
@@ -68,6 +73,7 @@ export const createSuccessRateDistributionTreeMap = (testResults) => {
68
73
  return {
69
74
  ...baseNode,
70
75
  status: leaf.status,
76
+ eligibleCount: includedInSuccessRate.has(leaf.status) ? 1 : 0,
71
77
  passedTests: leaf?.status === "passed" ? 1 : 0,
72
78
  failedTests: leaf?.status === "failed" ? 1 : 0,
73
79
  otherTests: leaf?.status && !["passed", "failed"].includes(leaf.status) ? 1 : 0,
@@ -75,6 +81,7 @@ export const createSuccessRateDistributionTreeMap = (testResults) => {
75
81
  }
76
82
  }, () => ({
77
83
  id: "root",
84
+ eligibleCount: 0,
78
85
  passedTests: 0,
79
86
  failedTests: 0,
80
87
  otherTests: 0,
@@ -82,7 +89,7 @@ export const createSuccessRateDistributionTreeMap = (testResults) => {
82
89
  return transformTreeMapNode(convertedTree, (node) => {
83
90
  const subtreeMetrics = calculateSubtreeMetrics(node);
84
91
  const colorValue = calculateColorValue(subtreeMetrics);
85
- const { totalTests, ...restSubtreeMetrics } = subtreeMetrics;
92
+ const { totalTests: _totalTests, ...restSubtreeMetrics } = subtreeMetrics;
86
93
  if (isChildrenLeavesOnly(node)) {
87
94
  const value = node.children?.reduce((acc, child) => {
88
95
  return acc + (child.value ?? 0);
@@ -1,6 +1,5 @@
1
- import { statusesList } from "@allurereport/core-api";
1
+ import { getSuccessRate, statusesList } from "@allurereport/core-api";
2
2
  import { arc, pie } from "d3-shape";
3
- const getPercentage = (value, total) => Math.floor((value / total) * 10000) / 100;
4
3
  const createD3ArcGenerator = () => arc().innerRadius(40).outerRadius(50).cornerRadius(2).padAngle(0.03);
5
4
  const d3Pie = pie()
6
5
  .value((d) => d.count)
@@ -44,7 +43,7 @@ export const getPieChartValues = (stats) => {
44
43
  };
45
44
  })
46
45
  .filter((item) => item !== null);
47
- const percentage = getPercentage(stats.passed ?? 0, stats.total);
46
+ const percentage = Math.floor(getSuccessRate(stats) * 10000) / 100;
48
47
  return {
49
48
  slices,
50
49
  percentage,
@@ -63,7 +63,7 @@ export const generateDurationDynamicsChart = (props) => {
63
63
  ...limitedHistoryPoints,
64
64
  {
65
65
  testResults: testResults.reduce((acc, testResult) => {
66
- acc[testResult.historyId ?? testResult.id] = testResult;
66
+ acc[testResult.retryHash ?? testResult.id] = testResult;
67
67
  return acc;
68
68
  }, {}),
69
69
  uuid: "current",
@@ -1,5 +1,5 @@
1
1
  import type { AllureChartsStoreData, StabilityDistributionChartData, StabilityDistributionChartOptions } from "@allurereport/charts-api";
2
- import type { TestStatus } from "@allurereport/core-api";
2
+ import { type TestStatus } from "@allurereport/core-api";
3
3
  export declare const getStabilityScore: (statuses: TestStatus[], historyDepth: number, stabilizationPeriod: number) => number | undefined;
4
4
  export declare const generateStabilityDistributionChart: (props: {
5
5
  options: StabilityDistributionChartOptions;
@@ -1,4 +1,5 @@
1
1
  import { ChartType } from "@allurereport/charts-api";
2
+ import { createHistoryTestResultLookup, } from "@allurereport/core-api";
2
3
  import { createHashStorage, createMapWithDefault } from "./utils.js";
3
4
  const DEFAULT_LIMIT = 10;
4
5
  const DEFAULT_STABILIZATION_PERIOD = 5;
@@ -79,10 +80,10 @@ export const getStabilityScore = (statuses, historyDepth, stabilizationPeriod) =
79
80
  }
80
81
  return instabilitySumNumerator / denominator;
81
82
  };
82
- const getStatusSequence = (historyDataPoints, tr) => {
83
+ const getStatusSequence = (historyDataPoints, tr, lookupHistoryTestResult) => {
83
84
  let block = [];
84
85
  for (const hdp of historyDataPoints) {
85
- const htr = hdp.testResults[tr.historyId];
86
+ const htr = lookupHistoryTestResult(hdp, tr);
86
87
  if (!htr) {
87
88
  block = [];
88
89
  continue;
@@ -100,6 +101,7 @@ export const generateStabilityDistributionChart = (props) => {
100
101
  const { options, storeData } = props;
101
102
  const { title, limit = DEFAULT_LIMIT, stabilizationPeriod = DEFAULT_STABILIZATION_PERIOD, threshold = DEFAULT_THRESHOLD, skipStatuses: skipStatusesList = NON_SIGNIFICANT_STATUSES, groupBy = DEFAULT_GROUP_BY, groupValues = [], } = options;
102
103
  const { testResults, historyDataPoints } = storeData;
104
+ const lookupHistoryTestResult = createHistoryTestResultLookup(storeData.allTestResults ?? testResults);
103
105
  const effectiveStabilizationPeriod = Math.min(stabilizationPeriod, limit);
104
106
  if (historyDataPoints.length < effectiveStabilizationPeriod) {
105
107
  return {
@@ -120,7 +122,7 @@ export const generateStabilityDistributionChart = (props) => {
120
122
  const hashes = createHashStorage();
121
123
  const skipStatuses = new Set(skipStatusesList);
122
124
  for (const tr of testResults) {
123
- if (!tr.historyId) {
125
+ if (!tr.retryHash) {
124
126
  continue;
125
127
  }
126
128
  if (!isSignificantStatus(tr.status)) {
@@ -133,7 +135,7 @@ export const generateStabilityDistributionChart = (props) => {
133
135
  if (!labelValue || (groupValuesSet.size > 0 && !groupValuesSet.has(labelValue))) {
134
136
  continue;
135
137
  }
136
- const statusSequence = getStatusSequence(limitedHistoryDataPoints, tr);
138
+ const statusSequence = getStatusSequence(limitedHistoryDataPoints, tr, lookupHistoryTestResult);
137
139
  const stabilityScore = getStabilityScore(statusSequence, limit, effectiveStabilizationPeriod);
138
140
  if (stabilityScore === undefined) {
139
141
  continue;
@@ -1,4 +1,5 @@
1
1
  import { ChartType, DEFAULT_CHART_HISTORY_LIMIT } from "@allurereport/charts-api";
2
+ import { createHistoryTestResultLookup } from "@allurereport/core-api";
2
3
  import { limitHistoryDataPoints } from "./chart-utils.js";
3
4
  const createEmptyStats = () => {
4
5
  return STATUSES.reduce((acc, status) => {
@@ -12,6 +13,7 @@ export const generateStatusAgePyramid = (props) => {
12
13
  const { options, storeData } = props;
13
14
  const { limit = DEFAULT_CHART_HISTORY_LIMIT } = options;
14
15
  const { historyDataPoints, testResults } = storeData;
16
+ const lookupHistoryTestResult = createHistoryTestResultLookup(storeData.allTestResults ?? testResults);
15
17
  const currentReportTimestamp = testResults.reduce((acc, testResult) => Math.max(acc, testResult.stop ?? 0), 0);
16
18
  const limitedHistoryPoints = limitHistoryDataPoints(historyDataPoints, limit).sort((a, b) => a.timestamp - b.timestamp);
17
19
  if (limitedHistoryPoints.length === 0) {
@@ -28,59 +30,48 @@ export const generateStatusAgePyramid = (props) => {
28
30
  statuses: STATUSES,
29
31
  };
30
32
  }
31
- const currTrIds = new Set(testResults.map((tr) => tr.historyId ?? tr.id));
32
- const hdps = limitedHistoryPoints.map((datapoint) => ({
33
- ...datapoint,
34
- testResults: Object.values(datapoint.testResults).reduce((acc, testResult) => {
35
- if (!testResult.historyId) {
36
- return acc;
37
- }
38
- const isInCurrentRun = currTrIds.has(testResult.historyId);
39
- if (isInCurrentRun) {
40
- acc[testResult.historyId] = testResult;
33
+ const data = limitedHistoryPoints.map((historyDataPoint, index) => {
34
+ const stats = createEmptyStats();
35
+ for (const testResult of testResults) {
36
+ const historicalTestResult = lookupHistoryTestResult(historyDataPoint, testResult);
37
+ if (!historicalTestResult) {
38
+ continue;
41
39
  }
42
- return acc;
43
- }, {}),
44
- }));
45
- const dataPoints = [
46
- ...hdps.map((hdp) => ({
47
- ...hdp,
48
- ...createEmptyStats(),
49
- })),
50
- {
51
- testResults: testResults.reduce((acc, testResult) => {
52
- acc[testResult.historyId ?? testResult.id] = testResult;
53
- return acc;
54
- }, {}),
55
- uuid: "current",
56
- timestamp: currentReportTimestamp,
57
- ...createEmptyStats(),
58
- },
59
- ];
60
- dataPoints.forEach((dp, index, dps) => {
61
- const { testResults: trs } = dp;
62
- const historyAfter = dps.slice(index, dps.length - 1);
63
- const currentTrs = Object.values(trs);
64
- for (const cTr of currentTrs) {
65
- const currentTrStatus = cTr.status;
40
+ const currentTrStatus = historicalTestResult.status;
66
41
  if (!isFBSUStatus(currentTrStatus)) {
67
42
  continue;
68
43
  }
69
- const historyAfterTrsStatuses = historyAfter.map((hdp) => hdp.testResults[cTr.historyId]?.status ?? undefined);
44
+ const historyAfterTrsStatuses = limitedHistoryPoints
45
+ .slice(index)
46
+ .map((historyAfterPoint) => lookupHistoryTestResult(historyAfterPoint, testResult)?.status);
70
47
  if (historyAfterTrsStatuses.some((status) => status !== currentTrStatus)) {
71
48
  continue;
72
49
  }
73
- dp[currentTrStatus]++;
50
+ stats[currentTrStatus]++;
74
51
  }
52
+ return {
53
+ id: historyDataPoint.uuid,
54
+ timestamp: historyDataPoint.timestamp,
55
+ failed: stats.failed,
56
+ broken: stats.broken,
57
+ skipped: stats.skipped,
58
+ unknown: stats.unknown,
59
+ };
60
+ });
61
+ const currentStats = createEmptyStats();
62
+ for (const testResult of testResults) {
63
+ if (isFBSUStatus(testResult.status)) {
64
+ currentStats[testResult.status]++;
65
+ }
66
+ }
67
+ data.push({
68
+ id: "current",
69
+ timestamp: currentReportTimestamp,
70
+ failed: currentStats.failed,
71
+ broken: currentStats.broken,
72
+ skipped: currentStats.skipped,
73
+ unknown: currentStats.unknown,
75
74
  });
76
- const data = dataPoints.map(({ uuid, timestamp, ...stats }) => ({
77
- id: uuid,
78
- timestamp,
79
- failed: stats.failed ?? 0,
80
- broken: stats.broken ?? 0,
81
- skipped: stats.skipped ?? 0,
82
- unknown: stats.unknown ?? 0,
83
- }));
84
75
  return {
85
76
  type: ChartType.StatusAgePyramid,
86
77
  title: options.title,
@@ -1,5 +1,5 @@
1
1
  import { ChartType, DEFAULT_CHART_HISTORY_LIMIT, } from "@allurereport/charts-api";
2
- import { htrsByTr } from "@allurereport/core-api";
2
+ import { createHistoryTestResultLookup, } from "@allurereport/core-api";
3
3
  import { limitHistoryDataPoints } from "./chart-utils.js";
4
4
  const getLastSignificantStatus = (history = []) => {
5
5
  const significantHtr = [...history]
@@ -31,6 +31,7 @@ export const generateStatusTransitionsChart = (props) => {
31
31
  const { options, storeData } = props;
32
32
  const { limit = DEFAULT_CHART_HISTORY_LIMIT } = options;
33
33
  const { historyDataPoints, testResults } = storeData;
34
+ const lookupHistoryTestResult = createHistoryTestResultLookup(storeData.allTestResults ?? testResults);
34
35
  const limitedHdps = limitHistoryDataPoints(historyDataPoints, limit).sort((a, b) => a.timestamp - b.timestamp);
35
36
  const currentReportTimestamp = testResults.reduce((acc, testResult) => Math.max(acc, testResult.stop ?? 0), 0);
36
37
  if (limitedHdps.length === 0) {
@@ -55,7 +56,7 @@ export const generateStatusTransitionsChart = (props) => {
55
56
  ...hdps,
56
57
  {
57
58
  testResults: testResults.reduce((acc, testResult) => {
58
- acc[testResult.historyId ?? testResult.id] = testResult;
59
+ acc[testResult.retryHash ?? testResult.id] = testResult;
59
60
  return acc;
60
61
  }, {}),
61
62
  uuid: "current",
@@ -73,9 +74,15 @@ export const generateStatusTransitionsChart = (props) => {
73
74
  malfunctioned: 0,
74
75
  };
75
76
  data.push(newDataItem);
76
- const cTrs = Object.values(trs);
77
+ const cTrs = Object.values(trs).filter((tr) => {
78
+ const selected = lookupHistoryTestResult({ testResults: trs }, tr);
79
+ return !selected || selected === tr;
80
+ });
77
81
  for (const cTr of cTrs) {
78
- const htrs = htrsByTr(hpsPriorToCurrent, cTr);
82
+ const htrs = hpsPriorToCurrent.flatMap((historyDataPoint) => {
83
+ const historicalTestResult = lookupHistoryTestResult(historyDataPoint, cTr);
84
+ return historicalTestResult ? [historicalTestResult] : [];
85
+ });
79
86
  const transition = getStatusTransition(cTr, htrs);
80
87
  if (!transition || transition === "new") {
81
88
  continue;
@@ -1,5 +1,5 @@
1
1
  import { ChartType, DEFAULT_CHART_HISTORY_LIMIT } from "@allurereport/charts-api";
2
- import { htrsByTr, statusesList } from "@allurereport/core-api";
2
+ import { createHistoryTestResultLookup, statusesList, } from "@allurereport/core-api";
3
3
  import { limitHistoryDataPoints } from "./chart-utils.js";
4
4
  const createEmptyStats = (statuses) => {
5
5
  return statuses.reduce((acc, status) => {
@@ -13,6 +13,7 @@ export const generateTestBaseGrowthDynamicsChart = (props) => {
13
13
  const { options, storeData } = props;
14
14
  const { limit = DEFAULT_CHART_HISTORY_LIMIT, statuses = DEFAULT_STATUSES } = options;
15
15
  const { historyDataPoints, testResults } = storeData;
16
+ const lookupHistoryTestResult = createHistoryTestResultLookup(storeData.allTestResults ?? testResults);
16
17
  const currentReportTimestamp = testResults.reduce((acc, testResult) => Math.max(acc, testResult.stop ?? 0), 0);
17
18
  const statusList = statuses.length > 0 ? statuses : DEFAULT_STATUSES;
18
19
  const limitedHistoryPoints = limitHistoryDataPoints(historyDataPoints, limit).sort((a, b) => a.timestamp - b.timestamp);
@@ -38,7 +39,7 @@ export const generateTestBaseGrowthDynamicsChart = (props) => {
38
39
  })),
39
40
  {
40
41
  testResults: testResults.reduce((acc, testResult) => {
41
- acc[testResult.historyId ?? testResult.id] = testResult;
42
+ acc[testResult.retryHash ?? testResult.id] = testResult;
42
43
  return acc;
43
44
  }, {}),
44
45
  uuid: "current",
@@ -51,19 +52,28 @@ export const generateTestBaseGrowthDynamicsChart = (props) => {
51
52
  const isLast = index === dataPoints.length - 1;
52
53
  const hpsPriorToCurrent = isFirst ? [earliestHdp] : dataPoints.slice(0, index);
53
54
  const hpsAfterCurrent = dataPoints.slice(index + 1);
54
- const currentTrs = Object.values(trs);
55
+ const currentTrs = Object.values(trs).filter((tr) => {
56
+ const selected = lookupHistoryTestResult({ testResults: trs }, tr);
57
+ return !selected || selected === tr;
58
+ });
55
59
  for (const cTr of currentTrs) {
56
60
  if (!statusList.includes(cTr.status)) {
57
61
  continue;
58
62
  }
59
- const htrsPriortoCurr = htrsByTr(hpsPriorToCurrent, cTr);
63
+ const htrsPriortoCurr = hpsPriorToCurrent.flatMap((historyDataPoint) => {
64
+ const historicalTestResult = lookupHistoryTestResult(historyDataPoint, cTr);
65
+ return historicalTestResult ? [historicalTestResult] : [];
66
+ });
60
67
  if (htrsPriortoCurr.length === 0) {
61
68
  stats[`new:${cTr.status}`]++;
62
69
  }
63
70
  if (isLast) {
64
71
  continue;
65
72
  }
66
- const htrsAfterCurrent = htrsByTr(hpsAfterCurrent, cTr);
73
+ const htrsAfterCurrent = hpsAfterCurrent.flatMap((historyDataPoint) => {
74
+ const historicalTestResult = lookupHistoryTestResult(historyDataPoint, cTr);
75
+ return historicalTestResult ? [historicalTestResult] : [];
76
+ });
67
77
  if (htrsAfterCurrent.length === 0) {
68
78
  stats[`removed:${cTr.status}`]++;
69
79
  }
@@ -1,4 +1,5 @@
1
1
  import { ChartType } from "@allurereport/charts-api";
2
+ import { emptyStatistic, getSuccessRate, getSuccessRateTotal, incrementStatistic, } from "@allurereport/core-api";
2
3
  const DEFAULT_LAYERS = ["unit", "integration", "e2e"];
3
4
  const getPercentage = (value, total) => {
4
5
  if (total === 0 || value === 0) {
@@ -14,13 +15,7 @@ export const generateTestingPyramidChart = (chartOptions, storeData) => {
14
15
  layersMap.set(layer, layer);
15
16
  layersMap.set(layer.toLocaleLowerCase(), layer);
16
17
  });
17
- const statsByLayers = new Map();
18
- layers.forEach((layer) => {
19
- statsByLayers.set(layer, {
20
- passed: 0,
21
- total: 0,
22
- });
23
- });
18
+ const statsByLayers = new Map(layers.map((layer) => [layer, emptyStatistic()]));
24
19
  for (const testResult of testResults) {
25
20
  const trLayer = testResult.labels.find((label) => label.name === "layer")?.value;
26
21
  if (!trLayer) {
@@ -31,10 +26,7 @@ export const generateTestingPyramidChart = (chartOptions, storeData) => {
31
26
  continue;
32
27
  }
33
28
  const data = statsByLayers.get(layer);
34
- data.total++;
35
- if (testResult.status === "passed") {
36
- data.passed++;
37
- }
29
+ incrementStatistic(data, testResult.status);
38
30
  }
39
31
  return {
40
32
  type: ChartType.TestingPyramid,
@@ -44,7 +36,8 @@ export const generateTestingPyramidChart = (chartOptions, storeData) => {
44
36
  return {
45
37
  layer,
46
38
  testCount: data.total,
47
- successRate: getPercentage(data.passed, data.total),
39
+ successRate: Math.floor(getSuccessRate(data) * 10000) / 100,
40
+ eligibleCount: getSuccessRateTotal(data),
48
41
  percentage: getPercentage(data.total, testResults.length),
49
42
  };
50
43
  }),
@@ -1,5 +1,5 @@
1
1
  import { ChartType, } from "@allurereport/charts-api";
2
- import { DEFAULT_ENVIRONMENT, emptyStatistic, getFallbackHistoryId, incrementStatistic, } from "@allurereport/core-api";
2
+ import { DEFAULT_ENVIRONMENT, emptyStatistic, incrementStatistic, } from "@allurereport/core-api";
3
3
  import { generateCurrentStatusChart } from "./generateCurrentStatusChart.js";
4
4
  import { generateDurationDynamicsChart } from "./generateDurationDynamicsChart.js";
5
5
  import { generateDurationsChart } from "./generateDurationsChart.js";
@@ -12,60 +12,6 @@ import { generateTestingPyramidChart } from "./generateTestingPyramidChart.js";
12
12
  import { generateTrSeveritiesChart } from "./generateTrSeveritiesChart.js";
13
13
  import { generateHeatMapChart } from "./heatMap.js";
14
14
  import { generateTreeMapChart } from "./treeMap.js";
15
- const buildFallbackHistoryAliases = (testResults) => {
16
- const aliases = new Map();
17
- const ambiguousAliases = new Set();
18
- for (const testResult of testResults) {
19
- if (!testResult.historyId) {
20
- continue;
21
- }
22
- const fallbackHistoryId = getFallbackHistoryId(testResult);
23
- if (!fallbackHistoryId || fallbackHistoryId === testResult.historyId) {
24
- continue;
25
- }
26
- const existingAlias = aliases.get(fallbackHistoryId);
27
- if (existingAlias && existingAlias !== testResult.historyId) {
28
- aliases.delete(fallbackHistoryId);
29
- ambiguousAliases.add(fallbackHistoryId);
30
- continue;
31
- }
32
- if (!ambiguousAliases.has(fallbackHistoryId)) {
33
- aliases.set(fallbackHistoryId, testResult.historyId);
34
- }
35
- }
36
- return aliases;
37
- };
38
- const normalizeHistoryDataPointsByAliases = (historyDataPoints, aliases) => {
39
- if (aliases.size === 0) {
40
- return historyDataPoints;
41
- }
42
- return historyDataPoints.map((historyDataPoint) => {
43
- let testResults = historyDataPoint.testResults;
44
- for (const [legacyHistoryId, primaryHistoryId] of aliases) {
45
- if (!Object.prototype.hasOwnProperty.call(testResults, legacyHistoryId)) {
46
- continue;
47
- }
48
- if (Object.prototype.hasOwnProperty.call(testResults, primaryHistoryId)) {
49
- continue;
50
- }
51
- if (testResults === historyDataPoint.testResults) {
52
- testResults = { ...testResults };
53
- }
54
- testResults[primaryHistoryId] = {
55
- ...testResults[legacyHistoryId],
56
- historyId: primaryHistoryId,
57
- };
58
- delete testResults[legacyHistoryId];
59
- }
60
- if (testResults === historyDataPoint.testResults) {
61
- return historyDataPoint;
62
- }
63
- return {
64
- ...historyDataPoint,
65
- testResults,
66
- };
67
- });
68
- };
69
15
  const generateChartData = async (props) => {
70
16
  const { envId, chartsOptions, store, generateUuid, filter } = props;
71
17
  const result = {};
@@ -116,14 +62,17 @@ const generateChartData = async (props) => {
116
62
  }
117
63
  return statistic;
118
64
  };
119
- const storeData = await Promise.all([getHistoryDataPoints(), getTrs(), getStatistic()]).then(([historyDataPoints, testResults, statistic]) => {
120
- const fallbackHistoryAliases = buildFallbackHistoryAliases(testResults);
121
- return {
122
- historyDataPoints: normalizeHistoryDataPointsByAliases(historyDataPoints, fallbackHistoryAliases),
123
- testResults,
124
- statistic,
125
- };
126
- });
65
+ const storeData = await Promise.all([
66
+ getHistoryDataPoints(),
67
+ getTrs(),
68
+ getStatistic(),
69
+ store.allTestResults({ includeRetries: true }),
70
+ ]).then(([historyDataPoints, testResults, statistic, allTestResults]) => ({
71
+ historyDataPoints,
72
+ testResults,
73
+ statistic,
74
+ allTestResults,
75
+ }));
127
76
  for (const chartOption of chartsOptions) {
128
77
  const chartId = generateUuid();
129
78
  switch (chartOption.type) {
@@ -0,0 +1 @@
1
+ export declare const copyToClipboard: (text: string) => Promise<boolean>;
@@ -0,0 +1,21 @@
1
+ export const copyToClipboard = async (text) => {
2
+ try {
3
+ if (navigator.clipboard) {
4
+ await navigator.clipboard.writeText(text);
5
+ return true;
6
+ }
7
+ const textarea = document.createElement("textarea");
8
+ textarea.value = text;
9
+ textarea.style.position = "fixed";
10
+ textarea.style.opacity = "0";
11
+ document.body.appendChild(textarea);
12
+ textarea.focus();
13
+ textarea.select();
14
+ document.execCommand("copy");
15
+ document.body.removeChild(textarea);
16
+ return true;
17
+ }
18
+ catch {
19
+ return false;
20
+ }
21
+ };
@@ -44,6 +44,12 @@ const buildAqlFromFieldFilter = (field) => {
44
44
  valueType = "STRING";
45
45
  break;
46
46
  }
47
+ case "null": {
48
+ expressionValue = "null";
49
+ valueType = "NULL";
50
+ operator = "EQ";
51
+ break;
52
+ }
47
53
  default: {
48
54
  const exhaustiveCheck = type;
49
55
  throw new Error(`Unsupported field type: ${String(exhaustiveCheck)}`);
@@ -1,3 +1,3 @@
1
1
  export * from "./builders.js";
2
- export type { ArrayField, BooleanField, Field, FieldFilter, FieldFilterGroup, Filter, LogicalOperator, NumberField, StringField, } from "./model.js";
2
+ export type { ArrayField, BooleanField, Field, FieldFilter, FieldFilterGroup, Filter, LogicalOperator, NullField, NumberField, StringField, } from "./model.js";
3
3
  export { MAX_ARRAY_FIELD_VALUES } from "./model.js";
@@ -27,13 +27,19 @@ export type BooleanField<T extends string = string> = {
27
27
  strict?: never;
28
28
  type: "boolean";
29
29
  };
30
+ export type NullField<T extends string = string> = {
31
+ key: T;
32
+ value: null;
33
+ strict?: never;
34
+ type: "null";
35
+ };
30
36
  export type ArrayField<T extends string = string> = {
31
37
  key: T;
32
38
  value: string[];
33
39
  strict?: boolean;
34
40
  type: "array";
35
41
  };
36
- export type Field<T extends string = string> = StringField<T> | NumberField<T> | BooleanField<T> | ArrayField<T>;
42
+ export type Field<T extends string = string> = StringField<T> | NumberField<T> | BooleanField<T> | ArrayField<T> | NullField<T>;
37
43
  export type LogicalOperator = "AND" | "OR";
38
- export type AqlValueType = "STRING" | "NUMBER" | "BOOLEAN";
44
+ export type AqlValueType = "STRING" | "NUMBER" | "BOOLEAN" | "NULL";
39
45
  export declare const MAX_ARRAY_FIELD_VALUES = 20;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from "./data.js";
2
2
  export * from "./attachments.js";
3
+ export * from "./clipboard.js";
3
4
  export * from "./environments.js";
4
5
  export * from "./i18n.js";
5
6
  export * from "./charts/index.js";
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from "./data.js";
2
2
  export * from "./attachments.js";
3
+ export * from "./clipboard.js";
3
4
  export * from "./environments.js";
4
5
  export * from "./i18n.js";
5
6
  export * from "./charts/index.js";
@@ -12,3 +12,7 @@ export declare const scrollFocusIntoView: (target: HTMLElement, options?: {
12
12
  inset?: number;
13
13
  }) => void;
14
14
  export declare const scrollTreePaneToTop: (anchor?: HTMLElement | null) => void;
15
+ export declare const isFocusInView: (target: HTMLElement, options?: {
16
+ containerAttribute?: string;
17
+ inset?: number;
18
+ }) => boolean;
@@ -80,3 +80,10 @@ export const scrollTreePaneToTop = (anchor) => {
80
80
  }
81
81
  }
82
82
  };
83
+ export const isFocusInView = (target, options) => {
84
+ const containerAttribute = options?.containerAttribute ?? "data-tree-scroll-container";
85
+ const scrollRoot = findFocusScrollRoot(target, containerAttribute);
86
+ const rootRect = scrollRoot ? scrollRoot.getBoundingClientRect() : { top: 0, bottom: window.innerHeight };
87
+ const inset = scrollRoot ? computeStickyInset(scrollRoot, options?.inset ?? 4) : (options?.inset ?? 4);
88
+ return !isOutOfScrollport(target.getBoundingClientRect(), rootRect, inset);
89
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@allurereport/web-commons",
3
- "version": "3.17.0",
3
+ "version": "3.19.0",
4
4
  "description": "Collection of utilities used across the web Allure reports",
5
5
  "keywords": [
6
6
  "allure",
@@ -26,10 +26,10 @@
26
26
  "lint:fix": "oxlint --import-plugin --fix src test features stories"
27
27
  },
28
28
  "dependencies": {
29
- "@allurereport/aql": "3.17.0",
30
- "@allurereport/charts-api": "3.17.0",
31
- "@allurereport/core-api": "3.17.0",
32
- "@allurereport/plugin-api": "3.17.0",
29
+ "@allurereport/aql": "3.19.0",
30
+ "@allurereport/charts-api": "3.19.0",
31
+ "@allurereport/core-api": "3.19.0",
32
+ "@allurereport/plugin-api": "3.19.0",
33
33
  "@preact/signals": "^2.6.1",
34
34
  "@preact/signals-core": "^1.12.2",
35
35
  "ansi-to-html": "^0.7.2",
@@ -37,7 +37,7 @@
37
37
  "d3-scale": "^4.0.2",
38
38
  "d3-shape": "^3.2.0",
39
39
  "dompurify": "^3.4.0",
40
- "nanoid": "^5.1.6"
40
+ "nanoid": "^5.1.16"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/d3-interpolate": "^3.0.4",