@allurereport/web-awesome 3.8.2 → 3.10.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 (227) hide show
  1. package/README.md +112 -0
  2. package/allurerc-dev.mjs +10 -0
  3. package/dist/multi/121.app-b18cce138691927e8759.js +1 -0
  4. package/dist/multi/173.app-b18cce138691927e8759.js +1 -0
  5. package/dist/multi/174.app-b18cce138691927e8759.js +1 -0
  6. package/dist/multi/252.app-b18cce138691927e8759.js +1 -0
  7. package/dist/multi/282.app-b18cce138691927e8759.js +1 -0
  8. package/dist/multi/29.app-b18cce138691927e8759.js +1 -0
  9. package/dist/multi/310.app-b18cce138691927e8759.js +1 -0
  10. package/dist/multi/416.app-b18cce138691927e8759.js +1 -0
  11. package/dist/multi/507.app-b18cce138691927e8759.js +1 -0
  12. package/dist/multi/527.app-b18cce138691927e8759.js +1 -0
  13. package/dist/multi/600.app-b18cce138691927e8759.js +1 -0
  14. package/dist/multi/605.app-b18cce138691927e8759.js +1 -0
  15. package/dist/multi/638.app-b18cce138691927e8759.js +1 -0
  16. package/dist/multi/672.app-b18cce138691927e8759.js +1 -0
  17. package/dist/multi/686.app-b18cce138691927e8759.js +1 -0
  18. package/dist/multi/725.app-b18cce138691927e8759.js +1 -0
  19. package/dist/multi/741.app-b18cce138691927e8759.js +1 -0
  20. package/dist/multi/749.app-b18cce138691927e8759.js +1 -0
  21. package/dist/multi/755.app-b18cce138691927e8759.js +1 -0
  22. package/dist/multi/779.app-b18cce138691927e8759.js +1 -0
  23. package/dist/multi/894.app-b18cce138691927e8759.js +1 -0
  24. package/dist/multi/943.app-b18cce138691927e8759.js +1 -0
  25. package/dist/multi/980.app-b18cce138691927e8759.js +1 -0
  26. package/dist/multi/app-b18cce138691927e8759.js +2 -0
  27. package/dist/multi/manifest.json +26 -23
  28. package/dist/multi/styles-212da6c68fa0beb4c6c5.css +1 -0
  29. package/dist/multi/styles-5c882b14b6f3112e40c4.css +1 -0
  30. package/dist/multi/styles-a4f65de86208f79dd2be.css +58 -0
  31. package/dist/single/app-733f473da7b51f98876d.js +2 -0
  32. package/dist/single/manifest.json +1 -1
  33. package/package.json +19 -14
  34. package/src/assets/scss/_common.scss +2 -2
  35. package/src/assets/scss/index.scss +8 -6
  36. package/src/components/BaseLayout/index.tsx +14 -2
  37. package/src/components/BaseLayout/styles.scss +5 -5
  38. package/src/components/Categories/CategoryHeaderItem/styles.scss +2 -2
  39. package/src/components/Categories/CategoryTreeItem/styles.scss +2 -2
  40. package/src/components/Categories/GroupTreeItem/styles.scss +4 -5
  41. package/src/components/Categories/HistoryTreeItem/styles.scss +2 -2
  42. package/src/components/Categories/LabelTreeItem/styles.scss +2 -2
  43. package/src/components/Categories/MessageTreeItem/index.tsx +1 -1
  44. package/src/components/Categories/MessageTreeItem/styles.scss +18 -18
  45. package/src/components/Categories/sticky.ts +1 -1
  46. package/src/components/Footer/FooterVersion.tsx +5 -10
  47. package/src/components/Footer/index.tsx +7 -1
  48. package/src/components/Footer/styles.scss +8 -2
  49. package/src/components/Header/CiInfo/index.tsx +17 -13
  50. package/src/components/Header/CiInfo/styles.scss +1 -1
  51. package/src/components/Header/styles.scss +2 -2
  52. package/src/components/HeaderControls/index.tsx +1 -3
  53. package/src/components/HotkeysProvider/index.tsx +556 -0
  54. package/src/components/KeyboardShortcuts/index.tsx +73 -0
  55. package/src/components/KeyboardShortcuts/shortcutsConfig.ts +91 -0
  56. package/src/components/KeyboardShortcuts/styles.scss +69 -0
  57. package/src/components/MainReport/index.tsx +89 -72
  58. package/src/components/MainReport/styles.scss +20 -5
  59. package/src/components/Metadata/index.tsx +27 -6
  60. package/src/components/Metadata/styles.scss +21 -9
  61. package/src/components/MetadataButton/index.tsx +2 -0
  62. package/src/components/MetadataButton/styles.scss +1 -1
  63. package/src/components/NavTabs/styles.scss +8 -8
  64. package/src/components/ReportBody/styles.scss +3 -4
  65. package/src/components/ReportCategories/styles.scss +1 -1
  66. package/src/components/ReportFilters/styles.scss +1 -1
  67. package/src/components/ReportGlobalAttachments/styles.scss +1 -1
  68. package/src/components/ReportGlobalErrors/styles.scss +1 -1
  69. package/src/components/ReportHeader/index.tsx +25 -13
  70. package/src/components/ReportHeader/styles.scss +2 -2
  71. package/src/components/ReportMetadata/index.tsx +44 -15
  72. package/src/components/ReportMetadata/styles.scss +6 -6
  73. package/src/components/ReportQualityGateResults/styles.scss +2 -2
  74. package/src/components/ReportSearch/index.tsx +1 -5
  75. package/src/components/ReportTabs/styles.scss +9 -9
  76. package/src/components/SectionSwitcher/index.tsx +87 -10
  77. package/src/components/SideBySide/index.tsx +20 -2
  78. package/src/components/SideBySide/styles.scss +9 -1
  79. package/src/components/SplitLayout/index.tsx +11 -2
  80. package/src/components/SplitLayout/styles.scss +23 -4
  81. package/src/components/TestResult/TestStepsEmpty/styles.scss +1 -1
  82. package/src/components/TestResult/TrDescription/styles.scss +1 -1
  83. package/src/components/TestResult/TrDropdown/index.tsx +2 -2
  84. package/src/components/TestResult/TrDropdown/styles.scss +1 -1
  85. package/src/components/TestResult/TrEmpty/styles.scss +1 -1
  86. package/src/components/TestResult/TrEnvironmentItem/styles.scss +4 -4
  87. package/src/components/TestResult/TrError/index.tsx +32 -7
  88. package/src/components/TestResult/TrError/styles.scss +23 -23
  89. package/src/components/TestResult/TrHeader/styles.scss +2 -2
  90. package/src/components/TestResult/TrHistory/styles.scss +6 -6
  91. package/src/components/TestResult/TrInfo/styles.scss +8 -8
  92. package/src/components/TestResult/TrLinks/index.tsx +2 -2
  93. package/src/components/TestResult/TrLinks/styles.scss +2 -2
  94. package/src/components/TestResult/TrMetadata/index.tsx +1 -1
  95. package/src/components/TestResult/TrMetadata/styles.scss +1 -1
  96. package/src/components/TestResult/TrNavigation/index.tsx +1 -1
  97. package/src/components/TestResult/TrNavigation/styles.scss +2 -2
  98. package/src/components/TestResult/TrOverview.tsx +2 -0
  99. package/src/components/TestResult/TrParameters/index.tsx +1 -1
  100. package/src/components/TestResult/TrParameters/styles.scss +1 -1
  101. package/src/components/TestResult/TrPrevStatuses/styles.scss +8 -8
  102. package/src/components/TestResult/TrPwTraces/styles.scss +1 -1
  103. package/src/components/TestResult/TrRetriesView/TrRetriesItem.tsx +27 -1
  104. package/src/components/TestResult/TrRetriesView/styles.scss +20 -10
  105. package/src/components/TestResult/TrSetup/index.tsx +10 -4
  106. package/src/components/TestResult/TrSeverity/styles.scss +7 -7
  107. package/src/components/TestResult/TrStatus/styles.scss +2 -35
  108. package/src/components/TestResult/TrSteps/TrAttachment.tsx +79 -43
  109. package/src/components/TestResult/TrSteps/TrAttachmentInfo.tsx +44 -17
  110. package/src/components/TestResult/TrSteps/TrBodyItems.tsx +5 -2
  111. package/src/components/TestResult/TrSteps/TrErrorStep.tsx +3 -0
  112. package/src/components/TestResult/TrSteps/TrStep.tsx +15 -6
  113. package/src/components/TestResult/TrSteps/TrStepHeader.tsx +8 -5
  114. package/src/components/TestResult/TrSteps/index.tsx +7 -5
  115. package/src/components/TestResult/TrSteps/stepTreeExpansion.ts +27 -9
  116. package/src/components/TestResult/TrSteps/styles.scss +80 -20
  117. package/src/components/TestResult/TrTeardown/index.tsx +10 -4
  118. package/src/components/TestResult/bodyItems.ts +1 -1
  119. package/src/components/TestResult/index.tsx +8 -2
  120. package/src/components/TestResult/styles.scss +10 -1
  121. package/src/components/TestResult/trOverviewFocus.scss +4 -0
  122. package/src/components/Timeline/styles.scss +6 -6
  123. package/src/components/Tree/index.tsx +79 -5
  124. package/src/components/Tree/styles.scss +55 -35
  125. package/src/hooks/useTestResultOverviewFocusScroll.ts +23 -0
  126. package/src/index.html +30 -33
  127. package/src/index.tsx +12 -6
  128. package/src/locales/ar.json +62 -1
  129. package/src/locales/az.json +62 -1
  130. package/src/locales/de.json +62 -1
  131. package/src/locales/en.json +62 -1
  132. package/src/locales/es.json +62 -1
  133. package/src/locales/fr.json +62 -1
  134. package/src/locales/he.json +62 -1
  135. package/src/locales/hy.json +62 -1
  136. package/src/locales/it.json +62 -1
  137. package/src/locales/ja.json +62 -1
  138. package/src/locales/ka.json +62 -1
  139. package/src/locales/kr.json +62 -1
  140. package/src/locales/nl.json +62 -1
  141. package/src/locales/pl.json +62 -1
  142. package/src/locales/pt.json +62 -1
  143. package/src/locales/ru.json +62 -1
  144. package/src/locales/sv.json +62 -1
  145. package/src/locales/tr.json +62 -1
  146. package/src/locales/uk.json +62 -1
  147. package/src/locales/zh-TW.json +62 -1
  148. package/src/locales/zh.json +62 -1
  149. package/src/stores/keyboard.ts +371 -0
  150. package/src/stores/keyboardActions.ts +769 -0
  151. package/src/stores/locale.ts +5 -2
  152. package/src/stores/reportEnvSections.ts +6 -0
  153. package/src/stores/reportRootTabs.ts +95 -0
  154. package/src/stores/search.ts +147 -0
  155. package/src/stores/testResultOverviewNav.ts +119 -0
  156. package/src/stores/testResultTabs.ts +62 -0
  157. package/src/stores/timeline.ts +1 -1
  158. package/src/stores/tree.ts +42 -4
  159. package/src/stores/treeFilters/store.ts +3 -36
  160. package/src/stores/treeSort.ts +7 -1
  161. package/src/styles/_pane-active.scss +8 -0
  162. package/src/styles.scss +1 -1
  163. package/src/utils/atSeparator.ts +4 -0
  164. package/src/utils/flattenTestResultOverview.ts +182 -0
  165. package/src/utils/time.ts +2 -1
  166. package/src/utils/trOverviewFocus.ts +18 -0
  167. package/src/utils/treeFilters.ts +15 -4
  168. package/test/components/EnvironmentPicker.test.tsx +21 -3
  169. package/test/components/Footer.test.tsx +26 -0
  170. package/test/components/Header/CiInfo.test.tsx +56 -0
  171. package/test/components/Header.test.tsx +8 -0
  172. package/test/components/HeaderControls.test.tsx +28 -0
  173. package/test/components/ReportGlobals.test.tsx +9 -1
  174. package/test/components/ReportHeader.test.tsx +77 -0
  175. package/test/components/ReportMetadata.test.tsx +131 -0
  176. package/test/components/TestResult/PwTraceButton.test.tsx +8 -0
  177. package/test/components/TestResult/TrErrorStep.test.tsx +8 -0
  178. package/test/components/TestResult/TrOverview.test.tsx +30 -10
  179. package/test/components/TestResult/TrRetriesItem.test.tsx +163 -0
  180. package/test/components/TestResult/TrSteps.test.tsx +108 -0
  181. package/test/components/TestResult/bodyItems.test.ts +9 -1
  182. package/test/components/TestResult/openPwTraceInNewTab.test.ts +8 -0
  183. package/test/components/TestResult/stepTreeExpansion.test.ts +10 -2
  184. package/test/components/Timeline.test.tsx +15 -7
  185. package/test/stores/keyboard/keyboardActions.test.ts +615 -0
  186. package/test/stores/search.test.ts +143 -0
  187. package/test/stores/treeFilters/actions.test.ts +8 -0
  188. package/test/stores/treeSort.test.ts +58 -0
  189. package/test/utils/flattenTestResultOverview.test.ts +57 -0
  190. package/test/utils/ownerAddress.test.ts +9 -1
  191. package/test/utils/time.test.ts +52 -0
  192. package/test/utils/treeFilters.test.ts +113 -1
  193. package/types.d.ts +39 -0
  194. package/webpack.config.js +12 -7
  195. package/CONTRIBUTING.md +0 -34
  196. package/dist/multi/173.app-f008fb8342025f2b1ace.js +0 -1
  197. package/dist/multi/174.app-f008fb8342025f2b1ace.js +0 -1
  198. package/dist/multi/252.app-f008fb8342025f2b1ace.js +0 -1
  199. package/dist/multi/282.app-f008fb8342025f2b1ace.js +0 -1
  200. package/dist/multi/29.app-f008fb8342025f2b1ace.js +0 -1
  201. package/dist/multi/310.app-f008fb8342025f2b1ace.js +0 -1
  202. package/dist/multi/416.app-f008fb8342025f2b1ace.js +0 -1
  203. package/dist/multi/507.app-f008fb8342025f2b1ace.js +0 -1
  204. package/dist/multi/527.app-f008fb8342025f2b1ace.js +0 -1
  205. package/dist/multi/600.app-f008fb8342025f2b1ace.js +0 -1
  206. package/dist/multi/605.app-f008fb8342025f2b1ace.js +0 -1
  207. package/dist/multi/638.app-f008fb8342025f2b1ace.js +0 -1
  208. package/dist/multi/672.app-f008fb8342025f2b1ace.js +0 -1
  209. package/dist/multi/686.app-f008fb8342025f2b1ace.js +0 -1
  210. package/dist/multi/725.app-f008fb8342025f2b1ace.js +0 -1
  211. package/dist/multi/741.app-f008fb8342025f2b1ace.js +0 -1
  212. package/dist/multi/749.app-f008fb8342025f2b1ace.js +0 -1
  213. package/dist/multi/755.app-f008fb8342025f2b1ace.js +0 -1
  214. package/dist/multi/894.app-f008fb8342025f2b1ace.js +0 -1
  215. package/dist/multi/943.app-f008fb8342025f2b1ace.js +0 -1
  216. package/dist/multi/980.app-f008fb8342025f2b1ace.js +0 -1
  217. package/dist/multi/app-f008fb8342025f2b1ace.js +0 -2
  218. package/dist/multi/styles-9f7a23a0c8b79fa76981.css +0 -58
  219. package/dist/single/app-07332238da9897064301.js +0 -2
  220. package/src/assets/scss/day.scss +0 -53
  221. package/src/assets/scss/fonts.scss +0 -3
  222. package/src/assets/scss/night.scss +0 -63
  223. package/src/assets/scss/palette.scss +0 -393
  224. package/src/assets/scss/theme.scss +0 -330
  225. package/src/assets/scss/vars.scss +0 -11
  226. /package/dist/multi/{app-f008fb8342025f2b1ace.js.LICENSE.txt → app-b18cce138691927e8759.js.LICENSE.txt} +0 -0
  227. /package/dist/single/{app-07332238da9897064301.js.LICENSE.txt → app-733f473da7b51f98876d.js.LICENSE.txt} +0 -0
@@ -0,0 +1,182 @@
1
+ import type { FlatTreeNode } from "@allurereport/web-commons";
2
+
3
+ import type { TrBodyItem } from "@/components/TestResult/bodyItems";
4
+ import { hasTestLevelErrorContent } from "@/components/TestResult/bodyItems";
5
+ import {
6
+ getStepTreeExpansionPolicy,
7
+ hasStepContent,
8
+ isStepOpenedByDefault,
9
+ type StepTreeExpansion,
10
+ } from "@/components/TestResult/TrSteps/stepTreeExpansion";
11
+
12
+ export type FlattenTestResultOverviewInput = {
13
+ testResultId: string;
14
+ hasSetup: boolean;
15
+ setupBodyItems: TrBodyItem[];
16
+ bodyItems: TrBodyItem[];
17
+ hasTeardown: boolean;
18
+ teardownBodyItems: TrBodyItem[];
19
+ isGroupOpened: (id: string, openedByDefault: boolean) => boolean;
20
+ stepExpansionPolicy?: StepTreeExpansion;
21
+ };
22
+
23
+ const sectionId = (testResultId: string, section: string) => `${testResultId}-${section}`;
24
+
25
+ const pushSection = (
26
+ result: FlatTreeNode[],
27
+ options: {
28
+ id: string;
29
+ depth: number;
30
+ parentId?: string;
31
+ openedByDefault?: boolean;
32
+ isGroupOpened: FlattenTestResultOverviewInput["isGroupOpened"];
33
+ },
34
+ ) => {
35
+ const openedByDefault = options.openedByDefault ?? true;
36
+ const isExpanded = options.isGroupOpened(options.id, openedByDefault);
37
+
38
+ result.push({
39
+ kind: "group",
40
+ id: options.id,
41
+ nodeId: options.id,
42
+ depth: options.depth,
43
+ parentId: options.parentId,
44
+ hasChildren: true,
45
+ isExpanded,
46
+ openedByDefault,
47
+ });
48
+ };
49
+
50
+ const flattenBodyItems = (
51
+ bodyItems: TrBodyItem[],
52
+ depth: number,
53
+ parentId: string,
54
+ policy: StepTreeExpansion,
55
+ isGroupOpened: FlattenTestResultOverviewInput["isGroupOpened"],
56
+ ): FlatTreeNode[] => {
57
+ const result: FlatTreeNode[] = [];
58
+
59
+ for (const bodyItem of bodyItems) {
60
+ if (bodyItem.type === "step") {
61
+ const stepId = bodyItem.item.stepId;
62
+ const openedByDefault = isStepOpenedByDefault(policy, bodyItem.item.status, bodyItem.bodyItems);
63
+ const hasContent = hasStepContent(bodyItem);
64
+ const isExpanded = isGroupOpened(stepId, openedByDefault);
65
+
66
+ result.push({
67
+ kind: hasContent ? "group" : "leaf",
68
+ id: stepId,
69
+ nodeId: stepId,
70
+ depth,
71
+ parentId,
72
+ hasChildren: hasContent,
73
+ isExpanded: hasContent ? isExpanded : undefined,
74
+ openedByDefault,
75
+ });
76
+
77
+ if (hasContent && isExpanded) {
78
+ result.push(...flattenBodyItems(bodyItem.bodyItems, depth + 1, stepId, policy, isGroupOpened));
79
+ }
80
+
81
+ continue;
82
+ }
83
+
84
+ if (bodyItem.type === "error" && hasTestLevelErrorContent(bodyItem.error)) {
85
+ const openedByDefault = policy !== "collapsed";
86
+ const isExpanded = isGroupOpened(bodyItem.id, openedByDefault);
87
+
88
+ result.push({
89
+ kind: "group",
90
+ id: bodyItem.id,
91
+ nodeId: bodyItem.id,
92
+ depth,
93
+ parentId,
94
+ hasChildren: true,
95
+ isExpanded,
96
+ openedByDefault,
97
+ });
98
+ continue;
99
+ }
100
+
101
+ if (bodyItem.type === "attachment") {
102
+ const attachmentTreeId = `attachment-${bodyItem.link.id}`;
103
+
104
+ result.push({
105
+ kind: "leaf",
106
+ id: bodyItem.link.id,
107
+ nodeId: attachmentTreeId,
108
+ depth,
109
+ parentId,
110
+ openedByDefault: false,
111
+ });
112
+ }
113
+ }
114
+
115
+ return result;
116
+ };
117
+
118
+ export const flattenTestResultOverview = (input: FlattenTestResultOverviewInput): FlatTreeNode[] => {
119
+ const { testResultId, hasSetup, setupBodyItems, bodyItems, hasTeardown, teardownBodyItems, isGroupOpened } = input;
120
+ const policy = input.stepExpansionPolicy ?? getStepTreeExpansionPolicy();
121
+ const result: FlatTreeNode[] = [];
122
+ let lastSectionId: string | undefined;
123
+
124
+ const appendSection = (section: string, openedByDefault = true) => {
125
+ const id = sectionId(testResultId, section);
126
+ pushSection(result, {
127
+ id,
128
+ depth: 0,
129
+ parentId: lastSectionId,
130
+ openedByDefault,
131
+ isGroupOpened,
132
+ });
133
+ lastSectionId = id;
134
+ return id;
135
+ };
136
+
137
+ if (hasSetup) {
138
+ const setupId = appendSection("setup");
139
+
140
+ if (isGroupOpened(setupId, true) && setupBodyItems.length > 0) {
141
+ const setupSteps = flattenBodyItems(setupBodyItems, 1, setupId, policy, isGroupOpened);
142
+ result.push(...setupSteps);
143
+
144
+ if (setupSteps.length > 0) {
145
+ lastSectionId = setupSteps[setupSteps.length - 1]!.id;
146
+ }
147
+ }
148
+ }
149
+
150
+ if (bodyItems.length > 0) {
151
+ const stepsId = sectionId(testResultId, "steps");
152
+ const stepsOpenedByDefault = policy !== "collapsed";
153
+ const stepsExpanded = isGroupOpened(stepsId, stepsOpenedByDefault);
154
+
155
+ pushSection(result, {
156
+ id: stepsId,
157
+ depth: 0,
158
+ parentId: lastSectionId,
159
+ openedByDefault: stepsOpenedByDefault,
160
+ isGroupOpened,
161
+ });
162
+ lastSectionId = stepsId;
163
+
164
+ if (stepsExpanded) {
165
+ const stepNodes = flattenBodyItems(bodyItems, 1, stepsId, policy, isGroupOpened);
166
+ result.push(...stepNodes);
167
+ if (stepNodes.length > 0) {
168
+ lastSectionId = stepNodes[stepNodes.length - 1]!.id;
169
+ }
170
+ }
171
+ }
172
+
173
+ if (hasTeardown) {
174
+ const teardownId = appendSection("teardown");
175
+
176
+ if (isGroupOpened(teardownId, true) && teardownBodyItems.length > 0) {
177
+ result.push(...flattenBodyItems(teardownBodyItems, 1, teardownId, policy, isGroupOpened));
178
+ }
179
+ }
180
+
181
+ return result;
182
+ };
package/src/utils/time.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { getLocaleDateTimeOverride } from "@allurereport/web-commons";
2
2
 
3
3
  import { currentLocale, currentLocaleIso, useI18n } from "@/stores/locale";
4
+ import { ensureAtSeparator } from "@/utils/atSeparator";
4
5
 
5
6
  const defaultOptions: Intl.DateTimeFormatOptions = {
6
7
  month: "numeric",
@@ -29,5 +30,5 @@ export const timestampToDate = (timestamp: number, options = defaultOptions) =>
29
30
  return formatted.replace(",", "");
30
31
  }
31
32
 
32
- return formatted.replace(",", ` ${t("at")}`);
33
+ return ensureAtSeparator(formatted, t("at"));
33
34
  };
@@ -0,0 +1,18 @@
1
+ import clsx from "clsx";
2
+
3
+ import { testResultFocusId } from "@/stores/testResultOverviewNav";
4
+
5
+ import * as styles from "@/components/TestResult/trOverviewFocus.scss";
6
+
7
+ export const isTrOverviewFocused = (id: string | null | undefined) => Boolean(id && testResultFocusId.value === id);
8
+
9
+ /** Focus ring for a section/step header row only (pass as `className` on the header control). */
10
+ export const trOverviewHeaderFocusClass = (id: string | null | undefined, className?: string) =>
11
+ clsx(className, isTrOverviewFocused(id) && styles["tr-overview-focused"]);
12
+
13
+ export const trOverviewFocusAttrs = (id: string | null | undefined) =>
14
+ id
15
+ ? {
16
+ "data-tr-focus-id": id,
17
+ }
18
+ : {};
@@ -1,4 +1,4 @@
1
- import type { Comparator, DefaultTreeGroup, Statistic, TestStatus, TreeLeaf } from "@allurereport/core-api";
1
+ import type { Comparator, Statistic, TestStatus, TreeLeaf } from "@allurereport/core-api";
2
2
  import {
3
3
  alphabetically,
4
4
  andThen,
@@ -38,16 +38,18 @@ const leafComparatorByTreeSortBy = (sortBy: SortBy = "status,asc"): Comparator<T
38
38
  }
39
39
  };
40
40
 
41
- const groupComparatorByTreeSortBy = (sortBy: SortBy = "status,asc"): Comparator<DefaultTreeGroup> => {
42
- const typedCompareBy = compareBy<DefaultTreeGroup>;
41
+ const groupComparatorByTreeSortBy = (sortBy: SortBy = "status,asc"): Comparator<AwesomeRecursiveTree> => {
42
+ const typedCompareBy = compareBy<AwesomeRecursiveTree>;
43
43
  switch (sortBy) {
44
44
  case "name,desc":
45
45
  case "name,asc":
46
46
  return typedCompareBy("name", alphabetically());
47
47
  case "order,desc":
48
48
  case "order,asc":
49
+ return typedCompareBy("groupOrder", ordinal());
49
50
  case "duration,desc":
50
51
  case "duration,asc":
52
+ return typedCompareBy("duration", ordinal());
51
53
  case "status,desc":
52
54
  case "status,asc":
53
55
  return typedCompareBy("statistic", byStatistic());
@@ -76,7 +78,7 @@ export const leafComparator = (sortBy: SortBy = "status,asc"): Comparator<TreeLe
76
78
  return withDirection(cmp, sortBy);
77
79
  };
78
80
 
79
- export const groupComparator = (sortBy: SortBy = "status,asc"): Comparator<DefaultTreeGroup> => {
81
+ export const groupComparator = (sortBy: SortBy = "status,asc"): Comparator<AwesomeRecursiveTree> => {
80
82
  const cmp = groupComparatorByTreeSortBy(sortBy);
81
83
 
82
84
  return withDirection(cmp, sortBy);
@@ -150,11 +152,20 @@ export const createRecursiveTree = (payload: {
150
152
  incrementStatistic(statistic, status);
151
153
  });
152
154
 
155
+ const duration =
156
+ leaves.reduce((acc, leaf) => acc + (leaf.duration ?? 0), 0) + trees.reduce((acc, rt) => acc + rt.duration, 0);
157
+
158
+ const leafMinOrder = leaves.reduce((acc, leaf) => Math.min(acc, leaf.groupOrder ?? Infinity), Infinity);
159
+ const treeMinOrder = trees.reduce((acc, rt) => Math.min(acc, rt.groupOrder), Infinity);
160
+ const groupOrder = Math.min(leafMinOrder, treeMinOrder);
161
+
153
162
  return {
154
163
  ...group,
155
164
  statistic,
156
165
  leaves,
157
166
  trees: trees.sort(groupComparator(sortBy)),
167
+ duration,
168
+ groupOrder: isFinite(groupOrder) ? groupOrder : 0,
158
169
  };
159
170
  };
160
171
 
@@ -1,6 +1,14 @@
1
- import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/preact";
1
+ import { act, cleanup, fireEvent, render, screen, waitFor } from "@testing-library/preact";
2
+ import { epic, feature, label, story } from "allure-js-commons";
2
3
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
4
 
5
+ beforeEach(async () => {
6
+ await epic("coverage");
7
+ await feature("ui-components");
8
+ await story("EnvironmentPicker");
9
+ await label("coverage", "ui-components");
10
+ });
11
+
4
12
  import { currentEnvironment, environmentsStore } from "@/stores/env";
5
13
 
6
14
  const longEnvironmentName = "я".repeat(64);
@@ -65,6 +73,7 @@ describe("components > EnvironmentPicker", () => {
65
73
  let restoreElementSizeMocks: () => void;
66
74
 
67
75
  beforeEach(() => {
76
+ vi.useFakeTimers();
68
77
  vi.stubGlobal(
69
78
  "matchMedia",
70
79
  vi.fn().mockImplementation(() => ({
@@ -96,6 +105,8 @@ describe("components > EnvironmentPicker", () => {
96
105
  restoreElementSizeMocks();
97
106
  vi.unstubAllGlobals();
98
107
  cleanup();
108
+ vi.runOnlyPendingTimers();
109
+ vi.useRealTimers();
99
110
  });
100
111
 
101
112
  it("should show tooltip on hover after resize truncates selected value", async () => {
@@ -108,14 +119,21 @@ describe("components > EnvironmentPicker", () => {
108
119
 
109
120
  expect(queryTooltip()).not.toBeInTheDocument();
110
121
 
111
- setNarrowViewport(true);
112
- await waitFor(() => {
122
+ await act(async () => {
123
+ setNarrowViewport(true);
113
124
  fireEvent(window, new Event("resize"));
125
+ vi.advanceTimersByTime(16);
126
+ });
127
+
128
+ await waitFor(() => {
114
129
  expect(getTooltipTrigger().childElementCount).toBe(2);
115
130
  });
116
131
 
117
132
  const tooltipTrigger = getTooltipTrigger();
118
133
  fireEvent.mouseEnter(tooltipTrigger);
134
+ await act(async () => {
135
+ vi.advanceTimersByTime(200);
136
+ });
119
137
  await waitFor(() => {
120
138
  expect(queryTooltip()).toBeInTheDocument();
121
139
  });
@@ -0,0 +1,26 @@
1
+ import { render, screen } from "@testing-library/preact";
2
+ import { describe, expect, it, vi } from "vitest";
3
+
4
+ import { Footer } from "@/components/Footer";
5
+
6
+ vi.mock("@/components/Footer/FooterLogo", () => ({
7
+ FooterLogo: () => <div data-testid="footer-logo" />,
8
+ }));
9
+
10
+ vi.mock("@allurereport/web-components", () => ({
11
+ LanguagePicker: () => <div data-testid="footer-language-picker" />,
12
+ }));
13
+
14
+ vi.mock("@/components/Footer/FooterVersion", () => ({
15
+ FooterVersion: () => <div data-testid="footer-version">Generated May 10, 2026 Ver: 3.8.2</div>,
16
+ }));
17
+
18
+ describe("components > Footer", () => {
19
+ it("should render language picker near generated time and version", () => {
20
+ render(<Footer />);
21
+
22
+ expect(screen.getByTestId("footer-logo")).toBeInTheDocument();
23
+ expect(screen.getByTestId("footer-language-picker")).toBeInTheDocument();
24
+ expect(screen.getByTestId("footer-version")).toHaveTextContent("Generated");
25
+ });
26
+ });
@@ -1,9 +1,17 @@
1
1
  import { CiType } from "@allurereport/core-api";
2
2
  import { getReportOptions } from "@allurereport/web-commons";
3
3
  import { cleanup, render, screen } from "@testing-library/preact";
4
+ import { epic, feature, label, story } from "allure-js-commons";
4
5
  import { h } from "preact";
5
6
  import { type Mock, beforeEach, describe, expect, it, vi } from "vitest";
6
7
 
8
+ beforeEach(async () => {
9
+ await epic("coverage");
10
+ await feature("ui-components");
11
+ await story("CiInfo");
12
+ await label("coverage", "ui-components");
13
+ });
14
+
7
15
  import { CiInfo } from "@/components/Header/CiInfo";
8
16
 
9
17
  const fixtures = {
@@ -189,4 +197,52 @@ describe("components > Header > CiInfo", () => {
189
197
 
190
198
  expect(screen.getByRole("link")).toHaveTextContent(fixtures.jobRunName);
191
199
  });
200
+
201
+ it("should not render executor metadata in the header", () => {
202
+ (getReportOptions as Mock).mockReturnValueOnce({
203
+ executor: {
204
+ name: "TeamCity",
205
+ type: "teamcity",
206
+ buildName: "Wrike #123",
207
+ buildUrl: "https://teamcity.example/build/123",
208
+ },
209
+ });
210
+
211
+ render(<CiInfo />);
212
+
213
+ expect(screen.queryByRole("link")).not.toBeInTheDocument();
214
+ expect(screen.queryByText("TeamCity · Wrike #123")).not.toBeInTheDocument();
215
+ });
216
+
217
+ it("should render ci label as plain text when ci has a name but no link", () => {
218
+ (getReportOptions as Mock).mockReturnValueOnce({
219
+ ci: {
220
+ jobName: "Nightly Build",
221
+ },
222
+ });
223
+
224
+ render(<CiInfo />);
225
+
226
+ expect(screen.queryByRole("link")).not.toBeInTheDocument();
227
+ expect(screen.getByText("Nightly Build")).toBeInTheDocument();
228
+ });
229
+
230
+ it("should ignore executor metadata when ci has no link", () => {
231
+ (getReportOptions as Mock).mockReturnValueOnce({
232
+ ci: {
233
+ jobName: "Nightly Build",
234
+ },
235
+ executor: {
236
+ name: "TeamCity",
237
+ buildName: "Wrike #123",
238
+ buildUrl: "https://teamcity.example/build/123",
239
+ },
240
+ });
241
+
242
+ render(<CiInfo />);
243
+
244
+ expect(screen.queryByRole("link")).not.toBeInTheDocument();
245
+ expect(screen.getByText("Nightly Build")).toBeInTheDocument();
246
+ expect(screen.queryByText("TeamCity · Wrike #123")).not.toBeInTheDocument();
247
+ });
192
248
  });
@@ -1,8 +1,16 @@
1
1
  import * as webCommons from "@allurereport/web-commons";
2
2
  import { signal } from "@preact/signals";
3
3
  import { cleanup, render, screen } from "@testing-library/preact";
4
+ import { epic, feature, label, story } from "allure-js-commons";
4
5
  import { beforeEach, describe, expect, it, vi } from "vitest";
5
6
 
7
+ beforeEach(async () => {
8
+ await epic("coverage");
9
+ await feature("ui-components");
10
+ await story("Header");
11
+ await label("coverage", "ui-components");
12
+ });
13
+
6
14
  import { Header } from "@/components/Header";
7
15
  import { CiInfo } from "@/components/Header/CiInfo";
8
16
  import type * as routerModule from "@/stores/router";
@@ -0,0 +1,28 @@
1
+ import { render, screen } from "@testing-library/preact";
2
+ import { describe, expect, it, vi } from "vitest";
3
+
4
+ import { HeaderControls } from "@/components/HeaderControls";
5
+
6
+ vi.mock("@/components/EnvironmentPicker", () => ({
7
+ EnvironmentPicker: () => <div data-testid="environment-picker" />,
8
+ }));
9
+
10
+ vi.mock("@/components/ToggleLayout", () => ({
11
+ default: () => <div data-testid="toggle-layout" />,
12
+ }));
13
+
14
+ vi.mock("@allurereport/web-components", () => ({
15
+ ThemeButton: () => <button data-testid="theme-button" type="button" />,
16
+ LanguagePicker: () => <button data-testid="language-picker" type="button" />,
17
+ }));
18
+
19
+ describe("components > HeaderControls", () => {
20
+ it("should keep frequent controls in header and omit language picker", () => {
21
+ render(<HeaderControls />);
22
+
23
+ expect(screen.getByTestId("environment-picker")).toBeInTheDocument();
24
+ expect(screen.getByTestId("toggle-layout")).toBeInTheDocument();
25
+ expect(screen.getByTestId("theme-button")).toBeInTheDocument();
26
+ expect(screen.queryByTestId("language-picker")).not.toBeInTheDocument();
27
+ });
28
+ });
@@ -1,6 +1,14 @@
1
1
  import { signal } from "@preact/signals";
2
2
  import { cleanup, render, screen } from "@testing-library/preact";
3
- import { afterEach, describe, expect, it, vi } from "vitest";
3
+ import { epic, feature, label, story } from "allure-js-commons";
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
+
6
+ beforeEach(async () => {
7
+ await epic("coverage");
8
+ await feature("ui-components");
9
+ await story("ReportGlobals");
10
+ await label("coverage", "ui-components");
11
+ });
4
12
 
5
13
  const environmentNames: Record<string, string> = {
6
14
  prod_env: "Prod",
@@ -0,0 +1,77 @@
1
+ import { getReportOptions } from "@allurereport/web-commons";
2
+ import { render, screen } from "@testing-library/preact";
3
+ import { type Mock, beforeEach, describe, expect, it, vi } from "vitest";
4
+
5
+ import { ReportHeader } from "@/components/ReportHeader";
6
+
7
+ vi.mock("@allurereport/web-commons", async (importOriginal) => ({
8
+ ...(await importOriginal()),
9
+ getReportOptions: vi.fn(),
10
+ }));
11
+
12
+ vi.mock("@allurereport/web-components", () => ({
13
+ Heading: (props: { children: string }) => <h2>{props.children}</h2>,
14
+ Loadable: (props: { renderData: (data: unknown) => unknown }) => props.renderData({}),
15
+ Text: (props: { "children": unknown; "data-testid"?: string }) => (
16
+ <span data-testid={props["data-testid"]}>{props.children}</span>
17
+ ),
18
+ TooltipWrapper: (props: { children: unknown }) => props.children,
19
+ }));
20
+
21
+ vi.mock("@/components/ReportHeader/ReportHeaderLogo", () => ({
22
+ ReportHeaderLogo: () => <div data-testid="report-logo" />,
23
+ }));
24
+
25
+ vi.mock("@/components/ReportHeader/ReportHeaderPie", () => ({
26
+ ReportHeaderPie: () => <div data-testid="report-pie" />,
27
+ }));
28
+
29
+ vi.mock("@/components/TestResult/TrStatus", () => ({
30
+ TrStatus: () => <div data-testid="tr-status" />,
31
+ }));
32
+
33
+ vi.mock("@/stores", () => ({
34
+ useI18n: () => ({
35
+ t: (key: string, params: Record<string, unknown>) => `${key}: ${params.formattedCreatedAt}`,
36
+ }),
37
+ }));
38
+
39
+ vi.mock("@/utils/time", () => ({
40
+ timestampToDate: (value: number, options?: Intl.DateTimeFormatOptions) =>
41
+ `${options?.month === "long" ? "long" : "default"}:${value}`,
42
+ }));
43
+
44
+ beforeEach(() => {
45
+ vi.clearAllMocks();
46
+ });
47
+
48
+ describe("components > ReportHeader", () => {
49
+ it("should render launch start time and duration when run summary is available", () => {
50
+ (getReportOptions as Mock).mockReturnValue({
51
+ reportName: "Wrike report",
52
+ createdAt: 42,
53
+ runSummary: {
54
+ start: 1000,
55
+ stop: 2500,
56
+ duration: 1500,
57
+ },
58
+ });
59
+
60
+ render(<ReportHeader />);
61
+
62
+ expect(screen.getByTestId("report-data")).toHaveTextContent("long:1000");
63
+ expect(screen.getByTestId("report-data")).not.toHaveTextContent("long:2500");
64
+ expect(screen.getByTestId("report-data")).not.toHaveTextContent("long:42");
65
+ });
66
+
67
+ it("should fall back to generated time when run summary is missing", () => {
68
+ (getReportOptions as Mock).mockReturnValue({
69
+ reportName: "Wrike report",
70
+ createdAt: 10,
71
+ });
72
+
73
+ render(<ReportHeader />);
74
+
75
+ expect(screen.getByTestId("report-data")).toHaveTextContent("long:10");
76
+ });
77
+ });