@allurereport/web-awesome 3.1.0 → 3.2.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.
Files changed (87) hide show
  1. package/dist/multi/173.app-d0210ed2e64d38a2ee8e.js +1 -0
  2. package/dist/multi/174.app-d0210ed2e64d38a2ee8e.js +1 -0
  3. package/dist/multi/252.app-d0210ed2e64d38a2ee8e.js +1 -0
  4. package/dist/multi/282.app-d0210ed2e64d38a2ee8e.js +1 -0
  5. package/dist/multi/29.app-d0210ed2e64d38a2ee8e.js +1 -0
  6. package/dist/multi/416.app-d0210ed2e64d38a2ee8e.js +1 -0
  7. package/dist/multi/527.app-d0210ed2e64d38a2ee8e.js +1 -0
  8. package/dist/multi/600.app-d0210ed2e64d38a2ee8e.js +1 -0
  9. package/dist/multi/605.app-d0210ed2e64d38a2ee8e.js +1 -0
  10. package/dist/multi/638.app-d0210ed2e64d38a2ee8e.js +1 -0
  11. package/dist/multi/672.app-d0210ed2e64d38a2ee8e.js +1 -0
  12. package/dist/multi/686.app-d0210ed2e64d38a2ee8e.js +1 -0
  13. package/dist/multi/725.app-d0210ed2e64d38a2ee8e.js +1 -0
  14. package/dist/multi/741.app-d0210ed2e64d38a2ee8e.js +1 -0
  15. package/dist/multi/749.app-d0210ed2e64d38a2ee8e.js +1 -0
  16. package/dist/multi/755.app-d0210ed2e64d38a2ee8e.js +1 -0
  17. package/dist/multi/894.app-d0210ed2e64d38a2ee8e.js +1 -0
  18. package/dist/multi/943.app-d0210ed2e64d38a2ee8e.js +1 -0
  19. package/dist/multi/980.app-d0210ed2e64d38a2ee8e.js +1 -0
  20. package/dist/multi/app-d0210ed2e64d38a2ee8e.js +2 -0
  21. package/dist/multi/manifest.json +21 -21
  22. package/dist/multi/{styles-9e390bad7ce54a807a8e.css → styles-13107bbe6906beabc50f.css} +5 -5
  23. package/dist/single/app-01fed10ad5f9083fd39c.js +2 -0
  24. package/dist/single/manifest.json +1 -1
  25. package/package.json +8 -10
  26. package/src/components/Charts/index.tsx +5 -5
  27. package/src/components/MainReport/index.tsx +52 -47
  28. package/src/components/Metadata/index.tsx +75 -1
  29. package/src/components/Metadata/styles.scss +10 -0
  30. package/src/components/ReportQualityGateResults/index.tsx +77 -19
  31. package/src/components/ReportQualityGateResults/styles.scss +13 -0
  32. package/src/components/TestResult/TrDescription/index.tsx +60 -10
  33. package/src/components/TestResult/TrDescription/styles.scss +4 -0
  34. package/src/components/TestResult/TrError/TrDiff.tsx +2 -6
  35. package/src/components/TestResult/TrError/styles.scss +4 -0
  36. package/src/components/TestResult/TrLinks/index.tsx +4 -4
  37. package/src/components/TestResult/TrOverview.tsx +3 -2
  38. package/src/components/Timeline/index.tsx +2 -5
  39. package/src/locales/az.json +3 -2
  40. package/src/locales/de.json +3 -2
  41. package/src/locales/en.json +3 -2
  42. package/src/locales/es.json +3 -2
  43. package/src/locales/fr.json +3 -2
  44. package/src/locales/he.json +3 -2
  45. package/src/locales/hy.json +3 -2
  46. package/src/locales/it.json +3 -2
  47. package/src/locales/ja.json +3 -2
  48. package/src/locales/ka.json +3 -2
  49. package/src/locales/kr.json +3 -2
  50. package/src/locales/nl.json +3 -2
  51. package/src/locales/pl.json +3 -2
  52. package/src/locales/pt.json +3 -2
  53. package/src/locales/ru.json +3 -2
  54. package/src/locales/sv.json +3 -2
  55. package/src/locales/tr.json +3 -2
  56. package/src/locales/{ua.json → uk.json} +3 -2
  57. package/src/locales/zh.json +3 -2
  58. package/src/stores/locale.ts +69 -37
  59. package/src/stores/qualityGate.ts +2 -2
  60. package/src/stores/timeline.ts +5 -2
  61. package/src/utils/ownerAddress.ts +92 -0
  62. package/src/utils/time.ts +16 -2
  63. package/test/utils/ownerAddress.test.ts +89 -0
  64. package/types.d.ts +1 -1
  65. package/dist/multi/173.app-79c65c7bff941abcbc51.js +0 -1
  66. package/dist/multi/174.app-79c65c7bff941abcbc51.js +0 -1
  67. package/dist/multi/252.app-79c65c7bff941abcbc51.js +0 -1
  68. package/dist/multi/282.app-79c65c7bff941abcbc51.js +0 -1
  69. package/dist/multi/29.app-79c65c7bff941abcbc51.js +0 -1
  70. package/dist/multi/416.app-79c65c7bff941abcbc51.js +0 -1
  71. package/dist/multi/527.app-79c65c7bff941abcbc51.js +0 -1
  72. package/dist/multi/600.app-79c65c7bff941abcbc51.js +0 -1
  73. package/dist/multi/605.app-79c65c7bff941abcbc51.js +0 -1
  74. package/dist/multi/638.app-79c65c7bff941abcbc51.js +0 -1
  75. package/dist/multi/672.app-79c65c7bff941abcbc51.js +0 -1
  76. package/dist/multi/686.app-79c65c7bff941abcbc51.js +0 -1
  77. package/dist/multi/725.app-79c65c7bff941abcbc51.js +0 -1
  78. package/dist/multi/741.app-79c65c7bff941abcbc51.js +0 -1
  79. package/dist/multi/755.app-79c65c7bff941abcbc51.js +0 -1
  80. package/dist/multi/894.app-79c65c7bff941abcbc51.js +0 -1
  81. package/dist/multi/91.app-79c65c7bff941abcbc51.js +0 -1
  82. package/dist/multi/943.app-79c65c7bff941abcbc51.js +0 -1
  83. package/dist/multi/980.app-79c65c7bff941abcbc51.js +0 -1
  84. package/dist/multi/app-79c65c7bff941abcbc51.js +0 -2
  85. package/dist/single/app-3ca67f29d0f1166c08ca.js +0 -2
  86. /package/dist/multi/{app-79c65c7bff941abcbc51.js.LICENSE.txt → app-d0210ed2e64d38a2ee8e.js.LICENSE.txt} +0 -0
  87. /package/dist/single/{app-3ca67f29d0f1166c08ca.js.LICENSE.txt → app-01fed10ad5f9083fd39c.js.LICENSE.txt} +0 -0
@@ -1,3 +1,3 @@
1
1
  {
2
- "main.js": "app-3ca67f29d0f1166c08ca.js"
2
+ "main.js": "app-01fed10ad5f9083fd39c.js"
3
3
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@allurereport/web-awesome",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "The static files for Allure Awesome Report",
5
5
  "keywords": [
6
6
  "allure",
@@ -31,19 +31,18 @@
31
31
  "IE 11"
32
32
  ],
33
33
  "dependencies": {
34
- "@allurereport/charts-api": "3.1.0",
35
- "@allurereport/core-api": "3.1.0",
36
- "@allurereport/plugin-api": "3.1.0",
37
- "@allurereport/web-commons": "3.1.0",
38
- "@allurereport/web-components": "3.1.0",
34
+ "@allurereport/charts-api": "3.2.0",
35
+ "@allurereport/core-api": "3.2.0",
36
+ "@allurereport/plugin-api": "3.2.0",
37
+ "@allurereport/web-commons": "3.2.0",
38
+ "@allurereport/web-components": "3.2.0",
39
39
  "@preact/signals": "^2.6.1",
40
40
  "clsx": "^2.1.1",
41
41
  "d3-shape": "^3.2.0",
42
42
  "i18next": "^24.0.2",
43
43
  "md5": "^2.3.0",
44
44
  "preact": "^10.28.2",
45
- "prismjs": "^1.30.0",
46
- "reset.css": "^2.0.2"
45
+ "prismjs": "^1.30.0"
47
46
  },
48
47
  "devDependencies": {
49
48
  "@babel/core": "^7.27.4",
@@ -60,7 +59,6 @@
60
59
  "@testing-library/user-event": "^14.5.1",
61
60
  "@types/babel__core": "^7.20.5",
62
61
  "@types/d3-shape": "^3.1.6",
63
- "@types/diff": "^7",
64
62
  "@types/eslint": "^8.56.11",
65
63
  "@types/md5": "^2.3.5",
66
64
  "@types/node": "^20.17.9",
@@ -74,7 +72,7 @@
74
72
  "babel-loader": "^9.2.1",
75
73
  "babel-plugin-prismjs": "^2.1.0",
76
74
  "css-loader": "^7.1.2",
77
- "diff": "^7.0.0",
75
+ "diff": "^8.0.3",
78
76
  "eslint": "^8.57.0",
79
77
  "eslint-config-preact": "^1.5.0",
80
78
  "eslint-config-prettier": "^9.1.0",
@@ -6,13 +6,13 @@ import {
6
6
  CurrentStatusChartWidget,
7
7
  DurationDynamicsChartWidget,
8
8
  DurationsChartWidget,
9
- FBSUAgePyramidChartWidget,
10
9
  Grid,
11
10
  GridItem,
12
11
  HeatMapWidget,
13
12
  Loadable,
14
13
  PageLoader,
15
14
  StabilityDistributionWidget,
15
+ StatusAgePyramidChartWidget,
16
16
  StatusDynamicsChartWidget,
17
17
  StatusTransitionsChartWidget,
18
18
  TestBaseGrowthDynamicsChartWidget,
@@ -113,15 +113,15 @@ const getChartWidgetByType = (
113
113
  />
114
114
  );
115
115
  }
116
- case ChartType.FBSUAgePyramid: {
117
- const title = chartData.title ?? t("fbsuAgePyramid.title");
116
+ case ChartType.StatusAgePyramid: {
117
+ const title = chartData.title ?? t("statusAgePyramid.title");
118
118
 
119
119
  return (
120
- <FBSUAgePyramidChartWidget
120
+ <StatusAgePyramidChartWidget
121
121
  title={title}
122
122
  data={chartData.data}
123
123
  statuses={chartData.statuses}
124
- i18n={(key, props = {}) => t(`fbsuAgePyramid.${key}`, props)}
124
+ i18n={(key, props = {}) => t(`statusAgePyramid.${key}`, props)}
125
125
  />
126
126
  );
127
127
  }
@@ -6,8 +6,8 @@ import { ReportGlobalAttachments } from "@/components/ReportGlobalAttachments";
6
6
  import { ReportGlobalErrors } from "@/components/ReportGlobalErrors";
7
7
  import { ReportHeader } from "@/components/ReportHeader";
8
8
  import { ReportMetadata } from "@/components/ReportMetadata";
9
- import { reportStatsStore } from "@/stores";
10
- import { useI18n } from "@/stores";
9
+ import { reportStatsStore, useI18n } from "@/stores";
10
+ import { currentEnvironment } from "@/stores/env";
11
11
  import { globalsStore } from "@/stores/globals";
12
12
  import { isSplitMode } from "@/stores/layout";
13
13
  import { qualityGateStore } from "@/stores/qualityGate";
@@ -45,53 +45,58 @@ const MainReport = () => {
45
45
  const { t } = useI18n("tabs");
46
46
 
47
47
  return (
48
- <>
49
- <div className={clsx(styles.content, isSplitMode.value ? styles["scroll-inside"] : "")}>
50
- <ReportHeader />
51
- <div className={styles["main-report-tabs"]}>
52
- <NavTabs initialTab={ReportRootTab.Results}>
53
- <NavTabsList>
54
- <Loadable
55
- source={reportStatsStore}
56
- renderData={(stats) => (
57
- <NavTab id={ReportRootTab.Results}>
58
- {t("results")} <Counter count={stats?.total ?? 0} />
48
+ <div className={clsx(styles.content, isSplitMode.value ? styles["scroll-inside"] : "")}>
49
+ <ReportHeader />
50
+ <div className={styles["main-report-tabs"]}>
51
+ <NavTabs initialTab={ReportRootTab.Results}>
52
+ <NavTabsList>
53
+ <Loadable
54
+ source={reportStatsStore}
55
+ renderData={(stats) => (
56
+ <NavTab id={ReportRootTab.Results}>
57
+ {t("results")} <Counter count={stats?.total ?? 0} />
58
+ </NavTab>
59
+ )}
60
+ />
61
+ <Loadable
62
+ source={qualityGateStore}
63
+ renderData={(results) => {
64
+ const currentEnvResults = currentEnvironment.value
65
+ ? (results[currentEnvironment.value] ?? [])
66
+ : Object.values(results).flatMap((envResults) => envResults);
67
+
68
+ return (
69
+ <NavTab id={ReportRootTab.QualityGate}>
70
+ {t("qualityGates")}{" "}
71
+ <Counter
72
+ status={currentEnvResults.length > 0 ? "failed" : undefined}
73
+ count={currentEnvResults.length}
74
+ />
75
+ </NavTab>
76
+ );
77
+ }}
78
+ />
79
+ <Loadable
80
+ source={globalsStore}
81
+ renderData={({ attachments = [], errors = [] }) => (
82
+ <>
83
+ <NavTab id={ReportRootTab.GlobalAttachments}>
84
+ {t("globalAttachments")} <Counter count={attachments.length} />
59
85
  </NavTab>
60
- )}
61
- />
62
- <Loadable
63
- source={qualityGateStore}
64
- renderData={(results) => (
65
- <>
66
- <NavTab id={ReportRootTab.QualityGate}>
67
- {t("qualityGates")}{" "}
68
- <Counter status={results.length > 0 ? "failed" : undefined} count={results.length} />
69
- </NavTab>
70
- </>
71
- )}
72
- />
73
- <Loadable
74
- source={globalsStore}
75
- renderData={({ attachments = [], errors = [] }) => (
76
- <>
77
- <NavTab id={ReportRootTab.GlobalAttachments}>
78
- {t("globalAttachments")} <Counter count={attachments.length} />
79
- </NavTab>
80
- <NavTab id={ReportRootTab.GlobalErrors}>
81
- {t("globalErrors")}{" "}
82
- <Counter status={errors.length > 0 ? "failed" : undefined} count={errors.length} />
83
- </NavTab>
84
- </>
85
- )}
86
- />
87
- </NavTabsList>
88
- <div className={styles["main-report-tabs-content"]}>
89
- <MainReportContent />
90
- </div>
91
- </NavTabs>
92
- </div>
86
+ <NavTab id={ReportRootTab.GlobalErrors}>
87
+ {t("globalErrors")}{" "}
88
+ <Counter status={errors.length > 0 ? "failed" : undefined} count={errors.length} />
89
+ </NavTab>
90
+ </>
91
+ )}
92
+ />
93
+ </NavTabsList>
94
+ <div className={styles["main-report-tabs-content"]}>
95
+ <MainReportContent />
96
+ </div>
97
+ </NavTabs>
93
98
  </div>
94
- </>
99
+ </div>
95
100
  );
96
101
  };
97
102
  export default MainReport;
@@ -7,6 +7,7 @@ import type { MetadataProps } from "@/components/ReportMetadata";
7
7
  import { useI18n } from "@/stores/locale";
8
8
  import { getTagsFilterUrl } from "@/stores/treeFilters/utils";
9
9
  import { copyToClipboard } from "@/utils/copyToClipboard";
10
+ import { parseOwnerAddress } from "@/utils/ownerAddress";
10
11
  import * as styles from "./styles.scss";
11
12
 
12
13
  export const MetadataList: FunctionalComponent<MetadataProps & { columns?: number }> = ({
@@ -65,6 +66,49 @@ const OpenFilterUrlButton: FunctionalComponent<{ url: string }> = ({ url }) => {
65
66
  );
66
67
  };
67
68
 
69
+ const MAX_URL_LENGTH = 25;
70
+
71
+ const OwnerAction = (props: { ownerValue: string }) => {
72
+ const { t } = useI18n("ui");
73
+ const { ownerValue } = props;
74
+ const ownerAddress = parseOwnerAddress(ownerValue);
75
+
76
+ if (ownerAddress.type === "none") {
77
+ return null;
78
+ }
79
+
80
+ // Don't need to show copy email button here because
81
+ // we already have a button to copy the whole owner value
82
+ if (ownerAddress.type === "email" && ownerAddress.email === ownerValue) {
83
+ return null;
84
+ }
85
+
86
+ if (ownerAddress.type === "email") {
87
+ return (
88
+ <Button
89
+ icon={allureIcons.lineGeneralCopy3}
90
+ style="outline"
91
+ text={t("copy-email")}
92
+ onClick={() => copyToClipboard(ownerAddress.email)}
93
+ />
94
+ );
95
+ }
96
+
97
+ if (ownerAddress.type === "url") {
98
+ const truncatedUrl =
99
+ ownerAddress.url.length > MAX_URL_LENGTH ? `${ownerAddress.url.slice(0, MAX_URL_LENGTH)}...` : ownerAddress.url;
100
+ return (
101
+ <ButtonLink
102
+ href={ownerAddress.url}
103
+ target="_blank"
104
+ style="ghost"
105
+ icon={allureIcons.lineGeneralLinkExternal}
106
+ text={truncatedUrl}
107
+ />
108
+ );
109
+ }
110
+ };
111
+
68
112
  const MetadataTooltip = (props: { value: string; name: string }) => {
69
113
  const { value, name } = props;
70
114
  const { t } = useI18n("ui");
@@ -75,6 +119,7 @@ const MetadataTooltip = (props: { value: string; name: string }) => {
75
119
  <Text>{value}</Text>
76
120
  </div>
77
121
  {name === "tag" && <OpenFilterUrlButton url={getTagsFilterUrl([value])} />}
122
+ {name === "owner" && <OwnerAction ownerValue={value} />}
78
123
  <Button
79
124
  style={"outline"}
80
125
  icon={allureIcons.lineGeneralCopy3}
@@ -85,11 +130,40 @@ const MetadataTooltip = (props: { value: string; name: string }) => {
85
130
  );
86
131
  };
87
132
 
133
+ const MetaDataOwnerLabel: FunctionalComponent<{
134
+ value: string;
135
+ size?: "s" | "m";
136
+ }> = ({ value, size = "s" }) => {
137
+ const ownerAddress = parseOwnerAddress(value);
138
+ const displayName = ownerAddress.displayName ?? value;
139
+
140
+ return (
141
+ <Menu
142
+ size="xl"
143
+ menuTrigger={({ onClick }) => (
144
+ <div className={styles["report-metadata-keyvalue-wrapper"]}>
145
+ <Text type={"ui"} size={size} onClick={onClick} bold className={styles["report-metadata-keyvalue-value"]}>
146
+ {displayName}
147
+ </Text>
148
+ </div>
149
+ )}
150
+ >
151
+ <Menu.Section>
152
+ <MetadataTooltip value={value} name={"owner"} />
153
+ </Menu.Section>
154
+ </Menu>
155
+ );
156
+ };
157
+
88
158
  const MetaDataKeyLabel: FunctionalComponent<{
89
159
  name: string;
90
160
  size?: "s" | "m";
91
161
  value: string;
92
162
  }> = ({ name, size = "s", value }) => {
163
+ if (name === "owner") {
164
+ return <MetaDataOwnerLabel value={value} size={size} />;
165
+ }
166
+
93
167
  return (
94
168
  <Menu
95
169
  size="xl"
@@ -132,7 +206,7 @@ const MetadataKeyValue: FunctionalComponent<{
132
206
  </div>
133
207
  ) : (
134
208
  <div className={styles["report-metadata-values"]} data-testid={"metadata-item-value"}>
135
- <MetaDataKeyLabel value={value} name={title} />
209
+ <MetaDataKeyLabel value={value ?? ""} name={title} />
136
210
  </div>
137
211
  )}
138
212
  </div>
@@ -92,6 +92,16 @@
92
92
  white-space: nowrap;
93
93
  text-overflow: ellipsis;
94
94
  cursor: pointer;
95
+
96
+ &:is(a) {
97
+ color: inherit;
98
+ font-weight: bold;
99
+ text-decoration: none;
100
+
101
+ &:hover {
102
+ text-decoration: underline;
103
+ }
104
+ }
95
105
  }
96
106
 
97
107
  .report-metadata-list {
@@ -1,40 +1,98 @@
1
+ import { DEFAULT_ENVIRONMENT } from "@allurereport/core-api";
2
+ import type { QualityGateValidationResult } from "@allurereport/plugin-api";
1
3
  import { Loadable, SvgIcon, Text, allureIcons } from "@allurereport/web-components";
4
+ import { useState } from "preact/hooks";
5
+ import { MetadataButton } from "@/components/MetadataButton";
2
6
  import { TrError } from "@/components/TestResult/TrError";
3
7
  import { useI18n } from "@/stores";
8
+ import { currentEnvironment } from "@/stores/env";
4
9
  import { qualityGateStore } from "@/stores/qualityGate";
5
10
  import * as styles from "./styles.scss";
6
11
 
12
+ const QualityGateResultsList = ({ results }: { results: QualityGateValidationResult[] }) => (
13
+ <ul className={styles["report-quality-gate-results-list"]} data-testid={"quality-gate-results-section-env-content"}>
14
+ {results.map((result) => (
15
+ <li key={result.rule} data-testid="quality-gate-result">
16
+ <div className={styles["report-quality-gate-result"]}>
17
+ <SvgIcon id={allureIcons.solidXCircle} className={styles["report-quality-gate-result-icon"]} />
18
+ <div className={styles["report-quality-gate-result-content"]}>
19
+ <Text tag="p" size="l" type="ui" bold data-testid="quality-gate-result-rule">
20
+ {result.rule}
21
+ </Text>
22
+ <TrError
23
+ className={styles["report-quality-gate-result-error"]}
24
+ message={result.message}
25
+ data-testid="quality-gate-result-message"
26
+ />
27
+ </div>
28
+ </div>
29
+ </li>
30
+ ))}
31
+ </ul>
32
+ );
33
+
7
34
  export const ReportQualityGateResults = () => {
8
35
  const { t } = useI18n("empty");
36
+ const { t: tEnvironments } = useI18n("environments");
37
+ const [collapsedEnvs, setCollapsedEnvs] = useState<string[]>([]);
9
38
 
10
39
  return (
11
40
  <Loadable
12
41
  source={qualityGateStore}
13
42
  renderData={(results) => {
14
- if (!results.length) {
43
+ if (currentEnvironment.value) {
44
+ const currentEnvResults = results[currentEnvironment.value] ?? [];
45
+
46
+ if (!currentEnvResults.length) {
47
+ return <div className={styles["report-quality-gate-results-empty"]}>{t("no-quality-gate-results")}</div>;
48
+ }
49
+
50
+ return <QualityGateResultsList results={currentEnvResults} />;
51
+ }
52
+
53
+ const entries = Object.entries(results).filter(([, envResults]) => envResults.length > 0);
54
+
55
+ if (!entries.length) {
15
56
  return <div className={styles["report-quality-gate-results-empty"]}>{t("no-quality-gate-results")}</div>;
16
57
  }
17
58
 
59
+ // single default environment
60
+ if (entries.length === 1 && entries[0][0] === DEFAULT_ENVIRONMENT) {
61
+ const currentEnvResults = entries[0][1] ?? [];
62
+
63
+ if (!currentEnvResults.length) {
64
+ return <div className={styles["report-quality-gate-results-empty"]}>{t("no-quality-gate-results")}</div>;
65
+ }
66
+
67
+ return <QualityGateResultsList results={currentEnvResults} />;
68
+ }
69
+
18
70
  return (
19
- <ul className={styles["report-quality-gate-results"]}>
20
- {results.map((result) => (
21
- <li key={result.rule} data-testid="report-quality-gate-result">
22
- <div className={styles["report-quality-gate-result"]}>
23
- <SvgIcon id={allureIcons.solidXCircle} className={styles["report-quality-gate-result-icon"]} />
24
- <div className={styles["report-quality-gate-result-content"]}>
25
- <Text tag="p" size="l" type="ui" bold data-testid="report-quality-gate-result-rule">
26
- {result.rule}
27
- </Text>
28
- <TrError
29
- className={styles["report-quality-gate-result-error"]}
30
- message={result.message}
31
- data-testid="report-quality-gate-result-message"
32
- />
33
- </div>
71
+ <div className={styles["report-quality-gate-results"]}>
72
+ {entries.map(([env, envResults]) => {
73
+ const isOpened = !collapsedEnvs.includes(env);
74
+ const toggleEnv = () => {
75
+ setCollapsedEnvs((prev) => (isOpened ? prev.concat(env) : prev.filter((e) => e !== env)));
76
+ };
77
+
78
+ return (
79
+ <div
80
+ key={env}
81
+ className={styles["report-quality-gate-section"]}
82
+ data-testid={"quality-gate-results-section"}
83
+ >
84
+ <MetadataButton
85
+ isOpened={isOpened}
86
+ setIsOpen={toggleEnv}
87
+ title={`${tEnvironments("environment", { count: 1 })}: "${env}"`}
88
+ counter={envResults.length}
89
+ data-testid={"quality-gate-results-section-env-button"}
90
+ />
91
+ {isOpened && <QualityGateResultsList results={envResults} />}
34
92
  </div>
35
- </li>
36
- ))}
37
- </ul>
93
+ );
94
+ })}
95
+ </div>
38
96
  );
39
97
  }}
40
98
  />
@@ -1,6 +1,11 @@
1
1
  @import "~@allurereport/web-components/mixins.scss";
2
2
 
3
3
  .report-quality-gate-results {
4
+ padding: 12px 0 32px 8px;
5
+ min-height: 320px;
6
+ }
7
+
8
+ .report-quality-gate-results-list {
4
9
  padding: 20px 0;
5
10
 
6
11
  & > li + li {
@@ -42,3 +47,11 @@
42
47
  .report-quality-gate-result-error {
43
48
  margin-top: 8px;
44
49
  }
50
+
51
+ .report-quality-gate-section {
52
+ &:not(:last-child) {
53
+ padding-bottom: 8px;
54
+ margin-bottom: 14px;
55
+ border-bottom: 1px solid var(--on-border-muted);
56
+ }
57
+ }
@@ -1,25 +1,75 @@
1
- import { Text } from "@allurereport/web-components";
1
+ import { proseStyles, resolveCssVarDeclarations, sanitizeIframeHtml, themeStore } from "@allurereport/web-commons";
2
2
  import type { FunctionalComponent } from "preact";
3
- import { useState } from "preact/hooks";
3
+ import { useEffect, useMemo, useState } from "preact/hooks";
4
4
  import type { AwesomeTestResult } from "types";
5
5
  import { MetadataButton } from "@/components/MetadataButton";
6
6
  import * as styles from "./styles.scss";
7
7
 
8
8
  export type TrDescriptionProps = {
9
- description: AwesomeTestResult["description"];
9
+ descriptionHtml: AwesomeTestResult["descriptionHtml"];
10
10
  };
11
11
 
12
- export const TrDescription: FunctionalComponent<TrDescriptionProps> = ({ description }) => {
13
- const [isOpen, setIsOpen] = useState<boolean>(true);
12
+ const MIN_HEIGHT = 120;
13
+
14
+ export const TrDescription: FunctionalComponent<TrDescriptionProps> = ({ descriptionHtml }) => {
15
+ const [isOpen, setIsOpen] = useState(true);
16
+ const [blobUrl, setBlobUrl] = useState("");
17
+ const [height, setHeight] = useState(MIN_HEIGHT);
18
+ const currentTheme = themeStore.value.current;
19
+
20
+ const sanitized = useMemo(() => (descriptionHtml ? sanitizeIframeHtml(descriptionHtml) : ""), [descriptionHtml]);
21
+
22
+ useEffect(() => {
23
+ if (!sanitized) {
24
+ setBlobUrl("");
25
+ return;
26
+ }
27
+
28
+ const iframeThemeVars = resolveCssVarDeclarations(proseStyles);
29
+
30
+ const html = `<!DOCTYPE html>
31
+ <html data-theme="${currentTheme}">
32
+ <head>
33
+ <meta charset="utf-8">
34
+ <style>:root {${iframeThemeVars}}</style>
35
+ <style>${proseStyles}</style>
36
+ </head>
37
+ <body>${sanitized}</body>
38
+ </html>`;
39
+
40
+ const blob = new Blob([html], { type: "text/html" });
41
+ const url = URL.createObjectURL(blob);
42
+ setBlobUrl(url);
43
+
44
+ return () => URL.revokeObjectURL(url);
45
+ }, [currentTheme, sanitized]);
46
+
47
+ const handleLoad = (e: Event) => {
48
+ const iframe = e.currentTarget as HTMLIFrameElement;
49
+ const documentElement = iframe.contentDocument?.documentElement;
50
+ const body = iframe.contentDocument?.body;
51
+ const scrollHeight = Math.max(documentElement?.scrollHeight ?? 0, body?.scrollHeight ?? 0);
52
+ setHeight(Math.max(scrollHeight, MIN_HEIGHT));
53
+ };
14
54
 
15
55
  return (
16
- <div className={styles["test-result-description"]}>
56
+ <div className={styles["test-result-description"]} data-testid="test-result-description">
17
57
  <div className={styles["test-result-description-wrapper"]}>
18
- <MetadataButton title={"Description"} setIsOpen={setIsOpen} isOpened={isOpen} />
58
+ <MetadataButton title="Description" setIsOpen={setIsOpen} isOpened={isOpen} />
19
59
  {isOpen && (
20
- <Text tag={"p"} className={styles["test-result-description-text"]}>
21
- {description}
22
- </Text>
60
+ <div className={styles["test-result-description-text"]}>
61
+ {blobUrl && (
62
+ <iframe
63
+ data-testid="test-result-description-frame"
64
+ src={blobUrl}
65
+ width="100%"
66
+ height={String(height)}
67
+ style={{ border: 0 }}
68
+ sandbox="allow-same-origin"
69
+ onLoad={handleLoad}
70
+ />
71
+ )}
72
+ </div>
23
73
  )}
24
74
  </div>
25
75
  </div>
@@ -9,4 +9,8 @@
9
9
 
10
10
  .test-result-description-text {
11
11
  padding-top: 10px;
12
+
13
+ iframe {
14
+ display: block;
15
+ }
12
16
  }
@@ -1,5 +1,5 @@
1
1
  import { Button, Code, CodeViewer } from "@allurereport/web-components";
2
- import type { BaseOptions, Change } from "diff";
2
+ import type { Change } from "diff";
3
3
  import { diffChars, diffLines, diffWords } from "diff";
4
4
  import { useState } from "preact/hooks";
5
5
  import * as styles from "@/components/TestResult/TrError/styles.scss";
@@ -33,11 +33,7 @@ export const TrDiff = ({ expected, actual }: { expected: string; actual: string
33
33
  };
34
34
  const changeTypeDiff = (type: DiffType = "chars") => {
35
35
  const diffFn = diffFunctions[type];
36
- const result = (diffFn as (oldStr: string, newStr: string, options?: BaseOptions) => Change[])(
37
- expected,
38
- actual,
39
- {},
40
- );
36
+ const result: Change[] = diffFn(expected, actual, {});
41
37
 
42
38
  setDiffType(type);
43
39
  setDiff(result);
@@ -128,6 +128,10 @@
128
128
  &:hover {
129
129
  background: inherit;
130
130
  }
131
+
132
+ pre {
133
+ white-space: pre-wrap;
134
+ }
131
135
  }
132
136
 
133
137
  .diff {
@@ -22,13 +22,13 @@ const linksIconMap: Record<string, string> = {
22
22
  const TrLink: FunctionalComponent<{
23
23
  link: TrLinkProps;
24
24
  }> = ({ link }) => {
25
- const { url, type } = link;
25
+ const { url, name, type } = link;
26
26
 
27
27
  return (
28
- <div className={styles["test-result-link"]}>
28
+ <div className={styles["test-result-link"]} data-testid="test-result-meta-link">
29
29
  <SvgIcon id={linksIconMap[type] ?? allureIcons.lineGeneralLink1} />
30
30
  <Text tag={"a"} href={url} target={"_blank"} size={"m"} className={styles["test-result-link-text"]}>
31
- {url}
31
+ {name || url}
32
32
  </Text>
33
33
  </div>
34
34
  );
@@ -46,7 +46,7 @@ export const TrLinks: FunctionalComponent<TrLinksProps> = ({ links }) => {
46
46
  });
47
47
 
48
48
  return (
49
- <div className={styles["test-result-links"]}>
49
+ <div className={styles["test-result-links"]} data-testid="test-result-meta-links">
50
50
  <div className={styles["test-result-links-wrapper"]}>
51
51
  <MetadataButton isOpened={isOpened} setIsOpen={setIsOpen} counter={links.length} title={t("links")} />
52
52
  {isOpened && <div className={styles["test-result-links-list"]}>{linkMap}</div>}
@@ -17,7 +17,8 @@ export type TrOverviewProps = {
17
17
  };
18
18
 
19
19
  export const TrOverview: FunctionalComponent<TrOverviewProps> = ({ testResult }) => {
20
- const { error, parameters, groupedLabels, links, description, setup, steps, teardown, id, status } = testResult || {};
20
+ const { error, parameters, groupedLabels, links, descriptionHtml, setup, steps, teardown, id, status } =
21
+ testResult || {};
21
22
  const isNoSteps = !setup?.length && !steps.length && !teardown.length;
22
23
  const pwTraces = testResult?.attachments?.filter(
23
24
  (attachment) => attachment.link.contentType === "application/vnd.allure.playwright-trace",
@@ -34,7 +35,7 @@ export const TrOverview: FunctionalComponent<TrOverviewProps> = ({ testResult })
34
35
  {Boolean(parameters?.length) && <TrParameters parameters={parameters} />}
35
36
  {Boolean(groupedLabels && Object.keys(groupedLabels || {})?.length) && <TrMetadata testResult={testResult} />}
36
37
  {Boolean(links?.length) && <TrLinks links={links} />}
37
- {Boolean(description) && <TrDescription description={description} />}
38
+ {Boolean(descriptionHtml) && <TrDescription descriptionHtml={descriptionHtml} />}
38
39
  <div className={styles["test-results"]}>
39
40
  {isNoSteps && <TestStepsEmpty />}
40
41
  {Boolean(setup?.length) && <TrSetup id={id} setup={setup} />}