@allurereport/web-commons 3.20.1 → 3.22.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.
@@ -31,8 +31,9 @@ export declare const fetchAttachment: (id: string, ext: string, contentType?: st
31
31
  export declare const blobAttachment: (id: string, ext: string, contentType: string) => Promise<Blob>;
32
32
  export declare const downloadAttachment: (id: string, ext: string, contentType: string) => Promise<void>;
33
33
  export declare const openAttachmentInNewTab: (id: string, ext: string, contentType: string) => Promise<void>;
34
- export type AttachmentType = "css" | "json" | "image" | "svg" | "code" | "text" | "markdown" | "html" | "table" | "video" | "uri" | "archive" | "image-diff" | "http";
35
- export declare const PREVIEWABLE_CONTENT_TYPES: readonly ["text/html", "text/csv", "text/markdown", "text/tab-separated-values", "text/uri-list"];
34
+ export type AttachmentType = "css" | "json" | "image" | "svg" | "code" | "text" | "markdown" | "html" | "table" | "video" | "uri" | "archive" | "image-diff" | "http" | "mermaid";
35
+ export declare const MERMAID_CONTENT_TYPE = "application/vnd.allure.diagrams.mermaid";
36
+ export declare const PREVIEWABLE_CONTENT_TYPES: readonly ["application/vnd.allure.diagrams.mermaid", "text/html", "text/csv", "text/markdown", "text/tab-separated-values", "text/uri-list"];
36
37
  export declare const isPreviewableContentType: (type?: string) => boolean;
37
38
  export declare const extname: (str: string) => string | undefined;
38
39
  export declare const isSyntaxHighlightSupported: (payload?: {
@@ -1,8 +1,9 @@
1
+ import { normalizeAttachmentContentType } from "./contentTypes.js";
1
2
  import { fetchReportAttachment } from "./data.js";
2
3
  const PLAYWRIGHT_TRACE_CONTENT_TYPE = "application/vnd.allure.playwright-trace";
3
4
  const PLAYWRIGHT_TRACE_DOWNLOAD_CONTENT_TYPE = "application/zip";
4
5
  const downloadContentType = (contentType) => {
5
- return contentType.split(";")[0].trim().toLowerCase() === PLAYWRIGHT_TRACE_CONTENT_TYPE
6
+ return normalizeAttachmentContentType(contentType) === PLAYWRIGHT_TRACE_CONTENT_TYPE
6
7
  ? PLAYWRIGHT_TRACE_DOWNLOAD_CONTENT_TYPE
7
8
  : contentType;
8
9
  };
@@ -30,6 +31,7 @@ export const fetchAttachment = async (id, ext, contentType) => {
30
31
  case "code":
31
32
  case "html":
32
33
  case "markdown":
34
+ case "mermaid":
33
35
  case "table":
34
36
  case "text": {
35
37
  const text = await response.text();
@@ -77,14 +79,16 @@ export const openAttachmentInNewTab = async (id, ext, contentType) => {
77
79
  const linkUrl = URL.createObjectURL(blob);
78
80
  globalThis.open(linkUrl, "_blank");
79
81
  };
82
+ export const MERMAID_CONTENT_TYPE = "application/vnd.allure.diagrams.mermaid";
80
83
  export const PREVIEWABLE_CONTENT_TYPES = [
84
+ MERMAID_CONTENT_TYPE,
81
85
  "text/html",
82
86
  "text/csv",
83
87
  "text/markdown",
84
88
  "text/tab-separated-values",
85
89
  "text/uri-list",
86
90
  ];
87
- export const isPreviewableContentType = (type) => !!type && PREVIEWABLE_CONTENT_TYPES.includes(type);
91
+ export const isPreviewableContentType = (type) => !!type && PREVIEWABLE_CONTENT_TYPES.includes(normalizeAttachmentContentType(type));
88
92
  const HIGHLIGHT_EXTS = new Set([
89
93
  "js",
90
94
  "mjs",
@@ -193,7 +197,7 @@ export const isSyntaxHighlightSupported = (payload) => {
193
197
  return !!ext && HIGHLIGHT_EXTS.has(ext);
194
198
  };
195
199
  export const attachmentType = (type) => {
196
- const normalizedType = type?.split(";")[0].trim().toLowerCase();
200
+ const normalizedType = normalizeAttachmentContentType(type);
197
201
  switch (normalizedType) {
198
202
  case "image/bmp":
199
203
  case "image/gif":
@@ -263,6 +267,8 @@ export const attachmentType = (type) => {
263
267
  return "image-diff";
264
268
  case "application/vnd.allure.http+json":
265
269
  return "http";
270
+ case MERMAID_CONTENT_TYPE:
271
+ return "mermaid";
266
272
  default:
267
273
  return null;
268
274
  }
@@ -0,0 +1,2 @@
1
+ import { type ChartOptions } from "@allurereport/charts-api";
2
+ export declare const validateChartsOptions: (charts: ChartOptions[]) => void;
@@ -0,0 +1,15 @@
1
+ import { ChartType } from "@allurereport/charts-api";
2
+ export const validateChartsOptions = (charts) => {
3
+ for (const options of charts) {
4
+ if (options.type !== ChartType.StabilityDistribution) {
5
+ continue;
6
+ }
7
+ const { limit, stabilizationPeriod } = options;
8
+ if (limit !== undefined && (!Number.isInteger(limit) || limit < 0)) {
9
+ throw new RangeError("stabilityDistribution.limit must be a non-negative integer");
10
+ }
11
+ if (stabilizationPeriod !== undefined && (!Number.isInteger(stabilizationPeriod) || stabilizationPeriod < 1)) {
12
+ throw new RangeError("stabilityDistribution.stabilizationPeriod must be a positive integer");
13
+ }
14
+ }
15
+ };
@@ -43,7 +43,8 @@ const getStats = (testResults) => {
43
43
  export const generateDurationDynamicsChart = (props) => {
44
44
  const { options, storeData } = props;
45
45
  const { title, limit = DEFAULT_CHART_HISTORY_LIMIT } = options;
46
- const { historyDataPoints, testResults } = storeData;
46
+ const { historyDataPoints } = storeData;
47
+ const testResults = storeData.allTestResults ?? storeData.testResults;
47
48
  const limitedHistoryPoints = limitHistoryDataPoints(historyDataPoints, limit - 1).sort((a, b) => a.timestamp - b.timestamp);
48
49
  const currentReportTimestamp = testResults.reduce((acc, testResult) => Math.max(acc, testResult.stop ?? 0), 0);
49
50
  if (limitedHistoryPoints.length === 0) {
@@ -63,7 +64,7 @@ export const generateDurationDynamicsChart = (props) => {
63
64
  ...limitedHistoryPoints,
64
65
  {
65
66
  testResults: testResults.reduce((acc, testResult) => {
66
- acc[testResult.retryHash ?? testResult.id] = testResult;
67
+ acc[testResult.id] = testResult;
67
68
  return acc;
68
69
  }, {}),
69
70
  uuid: "current",
@@ -51,7 +51,7 @@ const getBucketsFromDurations = (durations) => {
51
51
  export const generateDurationsChart = (props) => {
52
52
  const { options, storeData } = props;
53
53
  const { groupBy = BY_NONE, title } = options;
54
- const { testResults } = storeData;
54
+ const testResults = storeData.allTestResults ?? storeData.testResults;
55
55
  const enrichedTrs = enrichAndFilterTrs(testResults, groupBy);
56
56
  const durations = getDurations(enrichedTrs);
57
57
  const buckets = durations.size > 0 ? getBucketsFromDurations(Array.from(durations)) : [];
@@ -1,6 +1,4 @@
1
1
  import type { AllureChartsStoreData, StabilityDistributionChartData, StabilityDistributionChartOptions } from "@allurereport/charts-api";
2
- import { type TestStatus } from "@allurereport/core-api";
3
- export declare const getStabilityScore: (statuses: TestStatus[], historyDepth: number, stabilizationPeriod: number) => number | undefined;
4
2
  export declare const generateStabilityDistributionChart: (props: {
5
3
  options: StabilityDistributionChartOptions;
6
4
  storeData: AllureChartsStoreData;
@@ -1,158 +1,63 @@
1
1
  import { ChartType } from "@allurereport/charts-api";
2
- import { createHistoryTestResultLookup, } from "@allurereport/core-api";
3
- import { createHashStorage, createMapWithDefault } from "./utils.js";
4
- const DEFAULT_LIMIT = 10;
5
- const DEFAULT_STABILIZATION_PERIOD = 5;
2
+ import { createHistoryTestResultLookup, getTestFlakiness } from "@allurereport/core-api";
3
+ import { createHashStorage } from "./utils.js";
6
4
  const DEFAULT_THRESHOLD = 90;
7
5
  const DEFAULT_GROUP_BY = "feature";
8
6
  const CUSTOM_LABEL_NAME_PREFIX = "label-name:";
9
7
  const SIGNIFICANT_STATUSES = new Set(["passed", "failed", "broken"]);
10
- const isSignificantStatus = (status) => SIGNIFICANT_STATUSES.has(status);
11
- const getTrLabelValue = (testResult, labelName) => {
12
- return testResult.labels?.find((label) => label.name === labelName)?.value;
13
- };
14
8
  const NON_SIGNIFICANT_STATUSES = ["unknown", "skipped"];
15
- const findLatestConsecutiveSameBlock = (statuses, sequenceLength, blockSize) => {
16
- for (let start = sequenceLength - blockSize; start >= 0; start--) {
17
- const first = statuses[start];
18
- let allSame = true;
19
- for (let k = 1; k < blockSize; k++) {
20
- if (statuses[start + k] !== first) {
21
- allSame = false;
22
- break;
23
- }
24
- }
25
- if (allSame) {
26
- return { endIndex: start + blockSize - 1, status: first };
27
- }
28
- }
29
- return null;
30
- };
31
- const computeTransitionsInRange = (statuses, firstEdge, lastEdge) => {
32
- let transitionCount = 0;
33
- let firstTransitionIdx = 0;
34
- let secondTransitionIdx = 0;
35
- let instabilitySumNumerator = 0;
36
- for (let i = firstEdge; i <= lastEdge; i++) {
37
- if (statuses[i - 1] === statuses[i]) {
38
- continue;
39
- }
40
- transitionCount++;
41
- if (transitionCount === 1) {
42
- firstTransitionIdx = i;
43
- }
44
- else if (transitionCount === 2) {
45
- secondTransitionIdx = i;
46
- }
47
- instabilitySumNumerator += i;
48
- }
49
- return { transitionCount, firstTransitionIdx, secondTransitionIdx, instabilitySumNumerator };
50
- };
51
- const isRecoveryTolerancePattern = (statuses, firstTransitionIdx, secondTransitionIdx) => {
52
- const s0 = statuses[firstTransitionIdx - 1];
53
- const s1 = statuses[firstTransitionIdx];
54
- const s2 = statuses[secondTransitionIdx - 1];
55
- const s3 = statuses[secondTransitionIdx];
56
- return ((s0 === "passed" && s1 === "broken" && s2 === "broken" && s3 === "passed") ||
57
- (s0 === "passed" && s1 === "failed" && s2 === "failed" && s3 === "passed"));
58
- };
59
- export const getStabilityScore = (statuses, historyDepth, stabilizationPeriod) => {
60
- const effectiveSequenceLength = Math.min(statuses.length, historyDepth);
61
- if (effectiveSequenceLength === 0) {
62
- return undefined;
63
- }
64
- if (effectiveSequenceLength === 1) {
65
- return 1;
66
- }
67
- const stabilBlock = findLatestConsecutiveSameBlock(statuses, effectiveSequenceLength, stabilizationPeriod);
68
- if (stabilBlock !== null && stabilBlock.endIndex === effectiveSequenceLength - 1) {
69
- return 1;
70
- }
71
- const firstEdgeAfterStabilBlock = stabilBlock !== null ? stabilBlock.endIndex + 1 : 1;
72
- const lastEdge = effectiveSequenceLength - 1;
73
- const denominator = ((effectiveSequenceLength - 1) * effectiveSequenceLength) / 2;
74
- const { transitionCount, firstTransitionIdx, secondTransitionIdx, instabilitySumNumerator } = computeTransitionsInRange(statuses, firstEdgeAfterStabilBlock, lastEdge);
75
- if (transitionCount === 1) {
76
- return 1;
77
- }
78
- if (transitionCount === 2 && isRecoveryTolerancePattern(statuses, firstTransitionIdx, secondTransitionIdx)) {
79
- return 1;
80
- }
81
- return instabilitySumNumerator / denominator;
82
- };
83
- const getStatusSequence = (historyDataPoints, tr, lookupHistoryTestResult) => {
84
- let block = [];
85
- for (const hdp of historyDataPoints) {
86
- const htr = lookupHistoryTestResult(hdp, tr);
87
- if (!htr) {
88
- block = [];
89
- continue;
90
- }
91
- if (isSignificantStatus(htr.status)) {
92
- block.push(htr.status);
93
- }
94
- }
95
- if (isSignificantStatus(tr.status)) {
96
- block.push(tr.status);
97
- }
98
- return block;
99
- };
100
9
  export const generateStabilityDistributionChart = (props) => {
101
10
  const { options, storeData } = props;
102
- const { title, limit = DEFAULT_LIMIT, stabilizationPeriod = DEFAULT_STABILIZATION_PERIOD, threshold = DEFAULT_THRESHOLD, skipStatuses: skipStatusesList = NON_SIGNIFICANT_STATUSES, groupBy = DEFAULT_GROUP_BY, groupValues = [], } = options;
103
- const { testResults, historyDataPoints } = storeData;
104
- const lookupHistoryTestResult = createHistoryTestResultLookup(storeData.allTestResults ?? testResults);
105
- const effectiveStabilizationPeriod = Math.min(stabilizationPeriod, limit);
106
- if (historyDataPoints.length < effectiveStabilizationPeriod) {
107
- return {
108
- data: [],
109
- keys: {},
110
- type: ChartType.StabilityDistribution,
111
- title,
112
- threshold,
113
- };
11
+ const { title, limit, stabilizationPeriod, threshold = DEFAULT_THRESHOLD, skipStatuses: skipStatusesList = NON_SIGNIFICANT_STATUSES, groupBy = DEFAULT_GROUP_BY, groupValues = [], } = options;
12
+ const chart = {
13
+ data: [],
14
+ keys: {},
15
+ type: ChartType.StabilityDistribution,
16
+ title,
17
+ threshold,
18
+ };
19
+ if (limit === 0) {
20
+ return chart;
114
21
  }
115
- const limitedHistoryDataPoints = historyDataPoints.slice(0, limit).sort((a, b) => a.timestamp - b.timestamp);
22
+ const { testResults, historyDataPoints } = storeData;
23
+ const lookup = createHistoryTestResultLookup(storeData.allTestResults ?? testResults);
24
+ const history = [...historyDataPoints].sort((a, b) => b.timestamp - a.timestamp);
116
25
  const labelName = groupBy.startsWith(CUSTOM_LABEL_NAME_PREFIX)
117
26
  ? groupBy.slice(CUSTOM_LABEL_NAME_PREFIX.length)
118
27
  : groupBy;
119
28
  const groupValuesSet = new Set(groupValues ?? []);
120
- const stabilityScoresByGroup = createMapWithDefault([]);
121
- const keys = {};
29
+ const groups = new Map();
122
30
  const hashes = createHashStorage();
123
31
  const skipStatuses = new Set(skipStatusesList);
124
32
  for (const tr of testResults) {
125
- if (!tr.retryHash) {
33
+ if (!SIGNIFICANT_STATUSES.has(tr.status) || skipStatuses.has(tr.status)) {
126
34
  continue;
127
35
  }
128
- if (!isSignificantStatus(tr.status)) {
36
+ const labelValue = tr.labels?.find((label) => label.name === labelName)?.value;
37
+ if (!labelValue || (groupValuesSet.size > 0 && !groupValuesSet.has(labelValue))) {
129
38
  continue;
130
39
  }
131
- if (skipStatuses.has(tr.status)) {
40
+ const flaky = tr.sourceMetadata?.reportedFlaky === true
41
+ ? true
42
+ : getTestFlakiness(tr, history.map((point) => lookup(point, tr)), { historyDepth: limit, stabilizationPeriod });
43
+ if (flaky === undefined) {
132
44
  continue;
133
45
  }
134
- const labelValue = getTrLabelValue(tr, labelName);
135
- if (!labelValue || (groupValuesSet.size > 0 && !groupValuesSet.has(labelValue))) {
136
- continue;
46
+ const id = hashes.get(labelValue);
47
+ let counts = groups.get(id);
48
+ if (!counts) {
49
+ counts = { stable: 0, assessed: 0 };
50
+ groups.set(id, counts);
51
+ chart.keys[id] = labelValue;
137
52
  }
138
- const statusSequence = getStatusSequence(limitedHistoryDataPoints, tr, lookupHistoryTestResult);
139
- const stabilityScore = getStabilityScore(statusSequence, limit, effectiveStabilizationPeriod);
140
- if (stabilityScore === undefined) {
141
- continue;
53
+ counts.assessed++;
54
+ if (!flaky) {
55
+ counts.stable++;
142
56
  }
143
- const labelValueHash = hashes.get(labelValue);
144
- keys[labelValueHash] = labelValue;
145
- stabilityScoresByGroup.get(labelValueHash).push(stabilityScore);
146
57
  }
147
- return {
148
- data: stabilityScoresByGroup.entries.map(([id, scores]) => {
149
- const avg = scores.length > 0 ? scores.reduce((a, b) => a + b, 0) / scores.length : 0;
150
- const stabilityRate = Math.floor(avg * 10000) / 100;
151
- return { id, stabilityRate };
152
- }),
153
- keys,
154
- type: ChartType.StabilityDistribution,
155
- title,
156
- threshold,
157
- };
58
+ chart.data = [...groups].map(([id, counts]) => ({
59
+ id,
60
+ stabilityRate: Math.floor((counts.stable * 10000) / counts.assessed) / 100,
61
+ }));
62
+ return chart;
158
63
  };
@@ -28,6 +28,19 @@ const generateChartData = async (props) => {
28
28
  }
29
29
  return trs;
30
30
  };
31
+ const getAllTrs = async () => {
32
+ let trs = [];
33
+ if (envId) {
34
+ trs = await store.testResultsByEnvironmentId(envId, { includeRetries: true });
35
+ }
36
+ else {
37
+ trs = await store.allTestResults({ includeRetries: true });
38
+ }
39
+ if (filter) {
40
+ trs = trs.filter(filter);
41
+ }
42
+ return trs;
43
+ };
31
44
  const getHistoryDataPoints = async () => {
32
45
  let historyDataPoints = [];
33
46
  if (envId) {
@@ -43,7 +56,7 @@ const generateChartData = async (props) => {
43
56
  try {
44
57
  return filter(tr);
45
58
  }
46
- catch (error) {
59
+ catch {
47
60
  return false;
48
61
  }
49
62
  });
@@ -66,7 +79,7 @@ const generateChartData = async (props) => {
66
79
  getHistoryDataPoints(),
67
80
  getTrs(),
68
81
  getStatistic(),
69
- store.allTestResults({ includeRetries: true }),
82
+ getAllTrs(),
70
83
  ]).then(([historyDataPoints, testResults, statistic, allTestResults]) => ({
71
84
  historyDataPoints,
72
85
  testResults,
@@ -1,4 +1,5 @@
1
1
  export * from "./utils.js";
2
+ export * from "./config.js";
2
3
  export type * from "./types.js";
3
4
  export * from "./colors.js";
4
5
  export * from "./chart-utils.js";
@@ -1,4 +1,5 @@
1
1
  export * from "./utils.js";
2
+ export * from "./config.js";
2
3
  export * from "./colors.js";
3
4
  export * from "./chart-utils.js";
4
5
  export * from "./line.js";
@@ -0,0 +1,5 @@
1
+ export declare function normalizeAttachmentContentType(type: string): string;
2
+ export declare function normalizeAttachmentContentType(type?: null | undefined): undefined;
3
+ export declare function normalizeAttachmentContentType(type?: string | null): string | undefined;
4
+ export declare const isJsonAttachmentContentType: (type: string) => boolean;
5
+ export declare const isHtmlAttachmentContentType: (type: string) => boolean;
@@ -0,0 +1,11 @@
1
+ export function normalizeAttachmentContentType(type) {
2
+ return type?.split(";")[0].trim().toLowerCase();
3
+ }
4
+ export const isJsonAttachmentContentType = (type) => {
5
+ const normalized = normalizeAttachmentContentType(type);
6
+ return normalized === "application/json" || normalized === "text/json" || normalized.endsWith("+json");
7
+ };
8
+ export const isHtmlAttachmentContentType = (type) => {
9
+ const normalized = normalizeAttachmentContentType(type);
10
+ return normalized === "text/html" || normalized === "application/xhtml+xml";
11
+ };
@@ -0,0 +1,79 @@
1
+ export declare const HTTP_EXCHANGE_REDACTED_VALUE = "__ALLURE_REDACTED__";
2
+ export type HttpPair = {
3
+ name: string;
4
+ value: string;
5
+ masked: boolean;
6
+ };
7
+ export type HttpCookie = HttpPair & {
8
+ domain: string;
9
+ expires: string;
10
+ httpOnly: boolean;
11
+ maxAge?: number;
12
+ path: string;
13
+ sameSite: string;
14
+ secure: boolean;
15
+ };
16
+ export type HttpStream = {
17
+ type: string;
18
+ complete: string;
19
+ chunkCount?: number;
20
+ };
21
+ export type HttpBodyPart = {
22
+ name: string;
23
+ fileName: string;
24
+ headers: HttpPair[];
25
+ contentType: string;
26
+ encoding: string;
27
+ value: string;
28
+ hasValue: boolean;
29
+ size?: number;
30
+ truncated: boolean;
31
+ };
32
+ export type HttpBody = {
33
+ contentType: string;
34
+ encoding: string;
35
+ value: string;
36
+ hasValue: boolean;
37
+ size?: number;
38
+ truncated: boolean;
39
+ form: HttpPair[];
40
+ parts: HttpBodyPart[];
41
+ stream: HttpStream | null;
42
+ };
43
+ export type HttpRequest = {
44
+ method: string;
45
+ url: string;
46
+ httpVersion: string;
47
+ headers: HttpPair[];
48
+ cookies: HttpCookie[];
49
+ query: HttpPair[];
50
+ body: HttpBody | null;
51
+ trailers: HttpPair[];
52
+ };
53
+ export type HttpInformationalResponse = {
54
+ status: string;
55
+ reason: string;
56
+ headers: HttpPair[];
57
+ };
58
+ export type HttpResponse = {
59
+ status: string;
60
+ reason: string;
61
+ httpVersion: string;
62
+ headers: HttpPair[];
63
+ cookies: HttpCookie[];
64
+ body: HttpBody | null;
65
+ trailers: HttpPair[];
66
+ informationalResponses: HttpInformationalResponse[];
67
+ };
68
+ export type HttpError = {
69
+ name: string;
70
+ message: string;
71
+ stack: string;
72
+ };
73
+ export type NormalizedHttpPayload = {
74
+ request: HttpRequest | null;
75
+ response: HttpResponse | null;
76
+ error: HttpError | null;
77
+ durationMs: string;
78
+ };
79
+ export declare const normalizeHttpExchangePayload: (payload: unknown) => NormalizedHttpPayload;
@@ -0,0 +1,160 @@
1
+ export const HTTP_EXCHANGE_REDACTED_VALUE = "__ALLURE_REDACTED__";
2
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
3
+ const toOptionalString = (value) => value === null || typeof value === "undefined" ? "" : String(value);
4
+ const toFiniteNumber = (value) => {
5
+ const num = typeof value === "number" ? value : typeof value === "string" && value.trim() ? Number(value) : NaN;
6
+ return Number.isFinite(num) ? num : undefined;
7
+ };
8
+ const headerValue = (headers, name) => {
9
+ const normalizedName = name.toLowerCase();
10
+ return headers.find((header) => header.name.toLowerCase() === normalizedName)?.value ?? "";
11
+ };
12
+ const normalizeArray = (value, normalize) => Array.isArray(value) ? value.map(normalize).filter((item) => item !== null) : [];
13
+ const normalizePair = (value) => {
14
+ if (!isRecord(value)) {
15
+ return null;
16
+ }
17
+ const name = toOptionalString(value.name).trim();
18
+ const pairValue = toOptionalString(value.value);
19
+ const masked = pairValue === HTTP_EXCHANGE_REDACTED_VALUE;
20
+ if (!name && !pairValue) {
21
+ return null;
22
+ }
23
+ return { name, value: pairValue, masked };
24
+ };
25
+ const normalizePairs = (value) => normalizeArray(value, normalizePair);
26
+ const normalizeCookie = (value) => {
27
+ if (!isRecord(value)) {
28
+ return null;
29
+ }
30
+ const pair = normalizePair(value);
31
+ if (!pair) {
32
+ return null;
33
+ }
34
+ return {
35
+ ...pair,
36
+ domain: toOptionalString(value.domain).trim(),
37
+ expires: toOptionalString(value.expires).trim(),
38
+ httpOnly: value.httpOnly === true,
39
+ maxAge: toFiniteNumber(value.maxAge),
40
+ path: toOptionalString(value.path).trim(),
41
+ sameSite: toOptionalString(value.sameSite).trim(),
42
+ secure: value.secure === true,
43
+ };
44
+ };
45
+ const normalizeCookies = (value) => normalizeArray(value, normalizeCookie);
46
+ const normalizeStream = (value) => {
47
+ if (!isRecord(value)) {
48
+ return null;
49
+ }
50
+ return {
51
+ type: toOptionalString(value.type).trim(),
52
+ complete: typeof value.complete === "boolean" ? String(value.complete) : "",
53
+ chunkCount: toFiniteNumber(value.chunkCount),
54
+ };
55
+ };
56
+ const normalizeBodyPart = (value) => {
57
+ if (!isRecord(value)) {
58
+ return null;
59
+ }
60
+ const headers = normalizePairs(value.headers);
61
+ const hasValue = typeof value.value !== "undefined" && value.value !== null;
62
+ return {
63
+ name: toOptionalString(value.name).trim(),
64
+ fileName: toOptionalString(value.fileName).trim(),
65
+ headers,
66
+ contentType: toOptionalString(value.contentType).trim() || headerValue(headers, "content-type"),
67
+ encoding: toOptionalString(value.encoding).trim().toLowerCase() || "utf8",
68
+ value: hasValue ? String(value.value) : "",
69
+ hasValue,
70
+ size: toFiniteNumber(value.size),
71
+ truncated: value.truncated === true,
72
+ };
73
+ };
74
+ const normalizeBodyParts = (value) => normalizeArray(value, normalizeBodyPart);
75
+ const normalizeBody = (value, fallbackContentType = "") => {
76
+ if (!isRecord(value)) {
77
+ return null;
78
+ }
79
+ const hasValue = typeof value.value !== "undefined" && value.value !== null;
80
+ return {
81
+ contentType: toOptionalString(value.contentType).trim() || fallbackContentType,
82
+ encoding: toOptionalString(value.encoding).trim().toLowerCase() || "utf8",
83
+ value: hasValue ? String(value.value) : "",
84
+ hasValue,
85
+ size: toFiniteNumber(value.size),
86
+ truncated: value.truncated === true,
87
+ form: normalizePairs(value.form),
88
+ parts: normalizeBodyParts(value.parts),
89
+ stream: normalizeStream(value.stream),
90
+ };
91
+ };
92
+ const normalizeRequest = (value) => {
93
+ if (!isRecord(value)) {
94
+ return null;
95
+ }
96
+ const headers = normalizePairs(value.headers);
97
+ return {
98
+ method: toOptionalString(value.method).trim().toUpperCase() || "GET",
99
+ url: toOptionalString(value.url).trim(),
100
+ httpVersion: toOptionalString(value.httpVersion).trim(),
101
+ headers,
102
+ cookies: normalizeCookies(value.cookies),
103
+ query: normalizePairs(value.query),
104
+ body: normalizeBody(value.body, headerValue(headers, "content-type")),
105
+ trailers: normalizePairs(value.trailers),
106
+ };
107
+ };
108
+ const normalizeInformationalResponse = (value) => {
109
+ if (!isRecord(value)) {
110
+ return null;
111
+ }
112
+ return {
113
+ status: toOptionalString(value.status).trim(),
114
+ reason: toOptionalString(value.statusText).trim(),
115
+ headers: normalizePairs(value.headers),
116
+ };
117
+ };
118
+ const normalizeInformationalResponses = (value) => normalizeArray(value, normalizeInformationalResponse);
119
+ const normalizeResponse = (value) => {
120
+ if (!isRecord(value)) {
121
+ return null;
122
+ }
123
+ const headers = normalizePairs(value.headers);
124
+ return {
125
+ status: toOptionalString(value.status).trim(),
126
+ reason: toOptionalString(value.statusText).trim(),
127
+ httpVersion: toOptionalString(value.httpVersion).trim(),
128
+ headers,
129
+ cookies: normalizeCookies(value.cookies),
130
+ body: normalizeBody(value.body, headerValue(headers, "content-type")),
131
+ trailers: normalizePairs(value.trailers),
132
+ informationalResponses: normalizeInformationalResponses(value.informationalResponses),
133
+ };
134
+ };
135
+ const normalizeError = (value) => {
136
+ if (!isRecord(value)) {
137
+ return null;
138
+ }
139
+ const name = toOptionalString(value.name).trim();
140
+ const message = toOptionalString(value.message).trim();
141
+ const stack = toOptionalString(value.stack).trim();
142
+ if (!name && !message && !stack) {
143
+ return null;
144
+ }
145
+ return { name, message, stack };
146
+ };
147
+ export const normalizeHttpExchangePayload = (payload) => {
148
+ if (!isRecord(payload)) {
149
+ return { request: null, response: null, error: null, durationMs: "" };
150
+ }
151
+ const start = toFiniteNumber(payload.start);
152
+ const stop = toFiniteNumber(payload.stop);
153
+ const durationMs = start !== undefined && stop !== undefined && stop >= start ? `${stop - start} ms` : "";
154
+ return {
155
+ request: normalizeRequest(payload.request),
156
+ response: normalizeResponse(payload.response),
157
+ error: normalizeError(payload.error),
158
+ durationMs,
159
+ };
160
+ };
package/dist/index.d.ts CHANGED
@@ -19,3 +19,6 @@ export * from "./treeSubtreeToggle.js";
19
19
  export * from "./subtreeExpansion.js";
20
20
  export * from "./hotkeys/index.js";
21
21
  export * from "./treeNavigation/index.js";
22
+ export * from "./platform.js";
23
+ export * from "./contentTypes.js";
24
+ export * from "./httpAttachments.js";
package/dist/index.js CHANGED
@@ -19,3 +19,6 @@ export * from "./treeSubtreeToggle.js";
19
19
  export * from "./subtreeExpansion.js";
20
20
  export * from "./hotkeys/index.js";
21
21
  export * from "./treeNavigation/index.js";
22
+ export * from "./platform.js";
23
+ export * from "./contentTypes.js";
24
+ export * from "./httpAttachments.js";
@@ -0,0 +1 @@
1
+ export declare const testPlatform: (re: RegExp) => boolean;
@@ -0,0 +1,8 @@
1
+ export const testPlatform = (re) => {
2
+ if (typeof window === "undefined") {
3
+ return false;
4
+ }
5
+ const navigator = window?.navigator;
6
+ const platform = navigator?.userAgentData?.platform ?? navigator?.platform;
7
+ return platform ? re.test(platform) : false;
8
+ };
@@ -14,6 +14,3 @@ export declare const currentUrl: import("@preact/signals-core").ReadonlySignal<{
14
14
  export declare const getParamValue: (key: string) => string | null;
15
15
  export declare const getParamValues: (key: string) => string[];
16
16
  export declare const hasParam: (key: string) => boolean;
17
- export declare const getParamValueComputed: (key: string) => import("@preact/signals-core").ReadonlySignal<string | null>;
18
- export declare const getParamValuesComputed: (key: string) => import("@preact/signals-core").ReadonlySignal<string[]>;
19
- export declare const hasParamComputed: (key: string) => import("@preact/signals-core").ReadonlySignal<boolean>;
@@ -40,6 +40,3 @@ export const currentUrl = computed(() => {
40
40
  export const getParamValue = (key) => urlSearchParams.value.get(key);
41
41
  export const getParamValues = (key) => urlSearchParams.value.getAll(key);
42
42
  export const hasParam = (key) => urlSearchParams.value.has(key);
43
- export const getParamValueComputed = (key) => computed(() => getParamValue(key));
44
- export const getParamValuesComputed = (key) => computed(() => getParamValues(key));
45
- export const hasParamComputed = (key) => computed(() => hasParam(key));
@@ -7,19 +7,18 @@ export type SubtreeNodeState = {
7
7
  };
8
8
  export declare const isSubtreeFirstLevelOnlyOpened: (rootId: string, rootOpenedByDefault: boolean, subtreeNodes: SubtreeNodeState[], isNodeOpened: (id: string, openedByDefault: boolean) => boolean) => boolean;
9
9
  export declare const isSubtreeExpandedAll: (subtreeNodes: SubtreeNodeState[], isNodeOpened: (id: string, openedByDefault: boolean) => boolean) => boolean;
10
- export declare const getNextSubtreeToggleState: (payload: {
10
+ export type SubtreeTogglePayload = {
11
11
  hasOnlyLeafResults: boolean;
12
12
  isSubtreeCollapsedAll: boolean;
13
13
  isSubtreeFirstLevelOnly: boolean;
14
14
  isSubtreeExpandedAll: boolean;
15
15
  lastSubtreeToggle: SubtreeToggleState | null;
16
- }) => SubtreeToggleState;
16
+ };
17
+ export declare const getNextSubtreeToggleState: (payload: SubtreeTogglePayload) => SubtreeToggleState;
18
+ export type SubtreeToggleAction = "expand-first-level" | "collapse-to-first-level" | "expand-all" | "collapse-all";
19
+ export declare const getSubtreeToggleAction: (payload: SubtreeTogglePayload) => SubtreeToggleAction;
17
20
  export declare const resolveNextSubtreeToggleState: (subtreeNodes: SubtreeNodeState[], isNodeOpened: (id: string, openedByDefault: boolean) => boolean, lastSubtreeToggle: SubtreeToggleState | null) => {
18
21
  nextState: SubtreeToggleState;
19
22
  nextLastToggle: SubtreeToggleState | null;
20
23
  };
21
- export declare const getSubtreeToggleIcon: (payload: {
22
- hasOnlyLeafResults: boolean;
23
- isSubtreeCollapsedAll: boolean;
24
- isSubtreeFirstLevelOnly: boolean;
25
- }) => SubtreeToggleIconState;
24
+ export declare const getSubtreeToggleIcon: (payload: SubtreeTogglePayload) => SubtreeToggleIconState;
@@ -21,6 +21,16 @@ export const getNextSubtreeToggleState = (payload) => {
21
21
  }
22
22
  return "all";
23
23
  };
24
+ export const getSubtreeToggleAction = (payload) => {
25
+ const nextState = getNextSubtreeToggleState(payload);
26
+ if (nextState === "all") {
27
+ return "expand-all";
28
+ }
29
+ if (nextState === "none") {
30
+ return "collapse-all";
31
+ }
32
+ return payload.isSubtreeCollapsedAll ? "expand-first-level" : "collapse-to-first-level";
33
+ };
24
34
  export const resolveNextSubtreeToggleState = (subtreeNodes, isNodeOpened, lastSubtreeToggle) => {
25
35
  const root = subtreeNodes.find((node) => node.isRoot);
26
36
  if (!root || subtreeNodes.length === 0) {
@@ -43,15 +53,14 @@ export const resolveNextSubtreeToggleState = (subtreeNodes, isNodeOpened, lastSu
43
53
  };
44
54
  };
45
55
  export const getSubtreeToggleIcon = (payload) => {
46
- const { hasOnlyLeafResults, isSubtreeCollapsedAll, isSubtreeFirstLevelOnly } = payload;
47
- if (hasOnlyLeafResults) {
48
- return isSubtreeCollapsedAll ? "single-down" : "single-up";
49
- }
50
- if (isSubtreeCollapsedAll) {
51
- return "single-down";
52
- }
53
- if (isSubtreeFirstLevelOnly) {
54
- return "double-down";
56
+ switch (getSubtreeToggleAction(payload)) {
57
+ case "expand-first-level":
58
+ return "single-down";
59
+ case "expand-all":
60
+ return payload.hasOnlyLeafResults ? "single-down" : "double-down";
61
+ case "collapse-to-first-level":
62
+ return "double-up";
63
+ case "collapse-all":
64
+ return "single-up";
55
65
  }
56
- return "double-up";
57
66
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@allurereport/web-commons",
3
- "version": "3.20.1",
3
+ "version": "3.22.0",
4
4
  "description": "Collection of utilities used across the web Allure reports",
5
5
  "keywords": [
6
6
  "allure",
@@ -21,28 +21,31 @@
21
21
  "scripts": {
22
22
  "build": "run clean && tsc --project ./tsconfig.json",
23
23
  "clean": "rimraf ./dist",
24
- "test": "vitest run",
25
- "lint": "oxlint --import-plugin src test features stories",
26
- "lint:fix": "oxlint --import-plugin --fix src test features stories"
24
+ "test": "rimraf out && vp test run",
25
+ "test:unit": "yarn test",
26
+ "lint": "vp lint --import-plugin src test features stories",
27
+ "lint:fix": "vp lint --import-plugin --fix src test features stories"
27
28
  },
28
29
  "dependencies": {
29
- "@allurereport/aql": "3.20.1",
30
- "@allurereport/charts-api": "3.20.1",
31
- "@allurereport/core-api": "3.20.1",
32
- "@allurereport/plugin-api": "3.20.1",
30
+ "@allurereport/aql": "3.22.0",
31
+ "@allurereport/charts-api": "3.22.0",
32
+ "@allurereport/core-api": "3.22.0",
33
+ "@allurereport/plugin-api": "3.22.0",
33
34
  "@preact/signals": "^2.6.1",
34
35
  "@preact/signals-core": "^1.12.2",
35
36
  "ansi-to-html": "^0.7.2",
36
37
  "d3-interpolate": "^3.0.1",
37
38
  "d3-scale": "^4.0.2",
38
39
  "d3-shape": "^3.2.0",
39
- "dompurify": "^3.4.0",
40
+ "dompurify": "3.4.16",
40
41
  "nanoid": "^5.1.16"
41
42
  },
42
43
  "devDependencies": {
44
+ "@allurereport/common-config": "3.22.0",
43
45
  "@types/d3-interpolate": "^3.0.4",
44
46
  "@types/d3-scale": "^4.0.9",
45
47
  "@types/d3-shape": "^3.1.8",
48
+ "@vitest/coverage-istanbul": "5.0.4",
46
49
  "@vitest/runner": "^2",
47
50
  "allure-js-commons": "^3",
48
51
  "allure-vitest": "^3",
@@ -50,6 +53,8 @@
50
53
  "rimraf": "^6",
51
54
  "tslib": "^2.7.0",
52
55
  "typescript": "^5",
53
- "vitest": "^4.1.0"
56
+ "vite": "@voidzero-dev/vite-plus-core@1.2.0",
57
+ "vite-plus": "1.2.0",
58
+ "vitest": "5.0.4"
54
59
  }
55
60
  }