@allurereport/web-awesome 3.8.2 → 3.9.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 (206) hide show
  1. package/README.md +112 -0
  2. package/allurerc-dev.mjs +10 -0
  3. package/dist/multi/121.app-d36b0855e3e7a53eeee9.js +1 -0
  4. package/dist/multi/173.app-d36b0855e3e7a53eeee9.js +1 -0
  5. package/dist/multi/174.app-d36b0855e3e7a53eeee9.js +1 -0
  6. package/dist/multi/252.app-d36b0855e3e7a53eeee9.js +1 -0
  7. package/dist/multi/282.app-d36b0855e3e7a53eeee9.js +1 -0
  8. package/dist/multi/29.app-d36b0855e3e7a53eeee9.js +1 -0
  9. package/dist/multi/310.app-d36b0855e3e7a53eeee9.js +1 -0
  10. package/dist/multi/{416.app-f008fb8342025f2b1ace.js → 416.app-d36b0855e3e7a53eeee9.js} +1 -1
  11. package/dist/multi/{507.app-f008fb8342025f2b1ace.js → 507.app-d36b0855e3e7a53eeee9.js} +1 -1
  12. package/dist/multi/527.app-d36b0855e3e7a53eeee9.js +1 -0
  13. package/dist/multi/600.app-d36b0855e3e7a53eeee9.js +1 -0
  14. package/dist/multi/605.app-d36b0855e3e7a53eeee9.js +1 -0
  15. package/dist/multi/638.app-d36b0855e3e7a53eeee9.js +1 -0
  16. package/dist/multi/672.app-d36b0855e3e7a53eeee9.js +1 -0
  17. package/dist/multi/686.app-d36b0855e3e7a53eeee9.js +1 -0
  18. package/dist/multi/725.app-d36b0855e3e7a53eeee9.js +1 -0
  19. package/dist/multi/741.app-d36b0855e3e7a53eeee9.js +1 -0
  20. package/dist/multi/749.app-d36b0855e3e7a53eeee9.js +1 -0
  21. package/dist/multi/755.app-d36b0855e3e7a53eeee9.js +1 -0
  22. package/dist/multi/779.app-d36b0855e3e7a53eeee9.js +1 -0
  23. package/dist/multi/{894.app-f008fb8342025f2b1ace.js → 894.app-d36b0855e3e7a53eeee9.js} +1 -1
  24. package/dist/multi/943.app-d36b0855e3e7a53eeee9.js +1 -0
  25. package/dist/multi/980.app-d36b0855e3e7a53eeee9.js +1 -0
  26. package/dist/multi/app-d36b0855e3e7a53eeee9.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-468416ffee9a9dea6cae.css +58 -0
  30. package/dist/multi/styles-5c882b14b6f3112e40c4.css +1 -0
  31. package/dist/single/app-62171f5f51b5954a787c.js +2 -0
  32. package/dist/single/manifest.json +1 -1
  33. package/package.json +12 -7
  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/styles.scss +2 -2
  47. package/src/components/Header/CiInfo/styles.scss +1 -1
  48. package/src/components/Header/styles.scss +2 -2
  49. package/src/components/HotkeysProvider/index.tsx +556 -0
  50. package/src/components/KeyboardShortcuts/index.tsx +73 -0
  51. package/src/components/KeyboardShortcuts/shortcutsConfig.ts +91 -0
  52. package/src/components/KeyboardShortcuts/styles.scss +69 -0
  53. package/src/components/MainReport/index.tsx +89 -72
  54. package/src/components/MainReport/styles.scss +40 -4
  55. package/src/components/Metadata/styles.scss +9 -9
  56. package/src/components/MetadataButton/index.tsx +2 -0
  57. package/src/components/MetadataButton/styles.scss +1 -1
  58. package/src/components/NavTabs/styles.scss +8 -8
  59. package/src/components/ReportBody/index.tsx +11 -2
  60. package/src/components/ReportBody/styles.scss +24 -4
  61. package/src/components/ReportCategories/styles.scss +1 -1
  62. package/src/components/ReportFilters/styles.scss +1 -1
  63. package/src/components/ReportGlobalAttachments/styles.scss +1 -1
  64. package/src/components/ReportGlobalErrors/styles.scss +1 -1
  65. package/src/components/ReportHeader/styles.scss +2 -2
  66. package/src/components/ReportMetadata/index.tsx +9 -11
  67. package/src/components/ReportMetadata/styles.scss +6 -6
  68. package/src/components/ReportQualityGateResults/styles.scss +2 -2
  69. package/src/components/ReportSearch/index.tsx +1 -5
  70. package/src/components/ReportTabs/styles.scss +9 -9
  71. package/src/components/SectionSwitcher/index.tsx +87 -10
  72. package/src/components/SideBySide/index.tsx +20 -2
  73. package/src/components/SideBySide/styles.scss +9 -1
  74. package/src/components/SplitLayout/index.tsx +10 -1
  75. package/src/components/SplitLayout/styles.scss +20 -4
  76. package/src/components/TestResult/TestStepsEmpty/styles.scss +1 -1
  77. package/src/components/TestResult/TrDescription/styles.scss +1 -1
  78. package/src/components/TestResult/TrDropdown/index.tsx +2 -2
  79. package/src/components/TestResult/TrDropdown/styles.scss +1 -1
  80. package/src/components/TestResult/TrEmpty/styles.scss +1 -1
  81. package/src/components/TestResult/TrEnvironmentItem/styles.scss +4 -4
  82. package/src/components/TestResult/TrError/index.tsx +32 -7
  83. package/src/components/TestResult/TrError/styles.scss +23 -23
  84. package/src/components/TestResult/TrHeader/styles.scss +2 -2
  85. package/src/components/TestResult/TrHistory/styles.scss +6 -6
  86. package/src/components/TestResult/TrInfo/styles.scss +8 -8
  87. package/src/components/TestResult/TrLinks/index.tsx +2 -2
  88. package/src/components/TestResult/TrLinks/styles.scss +2 -2
  89. package/src/components/TestResult/TrMetadata/index.tsx +1 -1
  90. package/src/components/TestResult/TrMetadata/styles.scss +1 -1
  91. package/src/components/TestResult/TrNavigation/index.tsx +1 -1
  92. package/src/components/TestResult/TrNavigation/styles.scss +2 -2
  93. package/src/components/TestResult/TrOverview.tsx +2 -0
  94. package/src/components/TestResult/TrParameters/index.tsx +1 -1
  95. package/src/components/TestResult/TrParameters/styles.scss +1 -1
  96. package/src/components/TestResult/TrPrevStatuses/styles.scss +8 -8
  97. package/src/components/TestResult/TrPwTraces/styles.scss +1 -1
  98. package/src/components/TestResult/TrRetriesView/styles.scss +3 -3
  99. package/src/components/TestResult/TrSetup/index.tsx +9 -3
  100. package/src/components/TestResult/TrSeverity/styles.scss +7 -7
  101. package/src/components/TestResult/TrStatus/styles.scss +2 -35
  102. package/src/components/TestResult/TrSteps/TrAttachment.tsx +79 -43
  103. package/src/components/TestResult/TrSteps/TrAttachmentInfo.tsx +44 -17
  104. package/src/components/TestResult/TrSteps/TrErrorStep.tsx +3 -0
  105. package/src/components/TestResult/TrSteps/TrStep.tsx +9 -4
  106. package/src/components/TestResult/TrSteps/TrStepHeader.tsx +8 -5
  107. package/src/components/TestResult/TrSteps/index.tsx +7 -4
  108. package/src/components/TestResult/TrSteps/stepTreeExpansion.ts +27 -9
  109. package/src/components/TestResult/TrSteps/styles.scss +80 -20
  110. package/src/components/TestResult/TrTeardown/index.tsx +9 -3
  111. package/src/components/TestResult/bodyItems.ts +1 -1
  112. package/src/components/TestResult/index.tsx +8 -2
  113. package/src/components/TestResult/styles.scss +10 -1
  114. package/src/components/TestResult/trOverviewFocus.scss +4 -0
  115. package/src/components/Timeline/styles.scss +6 -6
  116. package/src/components/Tree/index.tsx +54 -5
  117. package/src/components/Tree/styles.scss +55 -35
  118. package/src/hooks/useTestResultOverviewFocusScroll.ts +23 -0
  119. package/src/index.html +30 -33
  120. package/src/index.tsx +12 -6
  121. package/src/locales/ar.json +61 -1
  122. package/src/locales/az.json +61 -1
  123. package/src/locales/de.json +61 -1
  124. package/src/locales/en.json +61 -1
  125. package/src/locales/es.json +61 -1
  126. package/src/locales/fr.json +61 -1
  127. package/src/locales/he.json +61 -1
  128. package/src/locales/hy.json +61 -1
  129. package/src/locales/it.json +61 -1
  130. package/src/locales/ja.json +61 -1
  131. package/src/locales/ka.json +61 -1
  132. package/src/locales/kr.json +61 -1
  133. package/src/locales/nl.json +61 -1
  134. package/src/locales/pl.json +61 -1
  135. package/src/locales/pt.json +61 -1
  136. package/src/locales/ru.json +61 -1
  137. package/src/locales/sv.json +61 -1
  138. package/src/locales/tr.json +61 -1
  139. package/src/locales/uk.json +61 -1
  140. package/src/locales/zh-TW.json +61 -1
  141. package/src/locales/zh.json +61 -1
  142. package/src/stores/keyboard.ts +371 -0
  143. package/src/stores/keyboardActions.ts +769 -0
  144. package/src/stores/locale.ts +1 -0
  145. package/src/stores/reportEnvSections.ts +6 -0
  146. package/src/stores/reportRootTabs.ts +95 -0
  147. package/src/stores/search.ts +147 -0
  148. package/src/stores/testResultOverviewNav.ts +119 -0
  149. package/src/stores/testResultTabs.ts +62 -0
  150. package/src/stores/timeline.ts +1 -1
  151. package/src/stores/tree.ts +42 -4
  152. package/src/stores/treeFilters/store.ts +3 -36
  153. package/src/styles/_pane-active.scss +8 -0
  154. package/src/styles.scss +1 -1
  155. package/src/utils/flattenTestResultOverview.ts +182 -0
  156. package/src/utils/trOverviewFocus.ts +18 -0
  157. package/test/components/EnvironmentPicker.test.tsx +21 -3
  158. package/test/components/Header/CiInfo.test.tsx +8 -0
  159. package/test/components/Header.test.tsx +8 -0
  160. package/test/components/ReportGlobals.test.tsx +9 -1
  161. package/test/components/TestResult/PwTraceButton.test.tsx +8 -0
  162. package/test/components/TestResult/TrErrorStep.test.tsx +8 -0
  163. package/test/components/TestResult/TrOverview.test.tsx +30 -10
  164. package/test/components/TestResult/TrSteps.test.tsx +73 -0
  165. package/test/components/TestResult/bodyItems.test.ts +9 -1
  166. package/test/components/TestResult/openPwTraceInNewTab.test.ts +8 -0
  167. package/test/components/TestResult/stepTreeExpansion.test.ts +10 -2
  168. package/test/components/Timeline.test.tsx +15 -7
  169. package/test/stores/keyboard/keyboardActions.test.ts +615 -0
  170. package/test/stores/search.test.ts +143 -0
  171. package/test/stores/treeFilters/actions.test.ts +8 -0
  172. package/test/utils/flattenTestResultOverview.test.ts +57 -0
  173. package/test/utils/ownerAddress.test.ts +9 -1
  174. package/test/utils/treeFilters.test.ts +9 -1
  175. package/types.d.ts +17 -0
  176. package/webpack.config.js +3 -0
  177. package/CONTRIBUTING.md +0 -34
  178. package/dist/multi/173.app-f008fb8342025f2b1ace.js +0 -1
  179. package/dist/multi/174.app-f008fb8342025f2b1ace.js +0 -1
  180. package/dist/multi/252.app-f008fb8342025f2b1ace.js +0 -1
  181. package/dist/multi/282.app-f008fb8342025f2b1ace.js +0 -1
  182. package/dist/multi/29.app-f008fb8342025f2b1ace.js +0 -1
  183. package/dist/multi/310.app-f008fb8342025f2b1ace.js +0 -1
  184. package/dist/multi/527.app-f008fb8342025f2b1ace.js +0 -1
  185. package/dist/multi/600.app-f008fb8342025f2b1ace.js +0 -1
  186. package/dist/multi/605.app-f008fb8342025f2b1ace.js +0 -1
  187. package/dist/multi/638.app-f008fb8342025f2b1ace.js +0 -1
  188. package/dist/multi/672.app-f008fb8342025f2b1ace.js +0 -1
  189. package/dist/multi/686.app-f008fb8342025f2b1ace.js +0 -1
  190. package/dist/multi/725.app-f008fb8342025f2b1ace.js +0 -1
  191. package/dist/multi/741.app-f008fb8342025f2b1ace.js +0 -1
  192. package/dist/multi/749.app-f008fb8342025f2b1ace.js +0 -1
  193. package/dist/multi/755.app-f008fb8342025f2b1ace.js +0 -1
  194. package/dist/multi/943.app-f008fb8342025f2b1ace.js +0 -1
  195. package/dist/multi/980.app-f008fb8342025f2b1ace.js +0 -1
  196. package/dist/multi/app-f008fb8342025f2b1ace.js +0 -2
  197. package/dist/multi/styles-9f7a23a0c8b79fa76981.css +0 -58
  198. package/dist/single/app-07332238da9897064301.js +0 -2
  199. package/src/assets/scss/day.scss +0 -53
  200. package/src/assets/scss/fonts.scss +0 -3
  201. package/src/assets/scss/night.scss +0 -63
  202. package/src/assets/scss/palette.scss +0 -393
  203. package/src/assets/scss/theme.scss +0 -330
  204. package/src/assets/scss/vars.scss +0 -11
  205. /package/dist/multi/{app-f008fb8342025f2b1ace.js.LICENSE.txt → app-d36b0855e3e7a53eeee9.js.LICENSE.txt} +0 -0
  206. /package/dist/single/{app-07332238da9897064301.js.LICENSE.txt → app-62171f5f51b5954a787c.js.LICENSE.txt} +0 -0
@@ -0,0 +1,769 @@
1
+ import type { HotkeyScope, MoveDirection } from "@allurereport/web-commons";
2
+ import {
3
+ applySubtreeToggleState,
4
+ collectExpandableSubtreeNodes,
5
+ getExpandableDescendants,
6
+ hasExpandableTreeChildren,
7
+ resolveNextSubtreeToggleState,
8
+ scrollTreePaneToTop,
9
+ type SubtreeNodeState,
10
+ type SubtreeToggleState,
11
+ } from "@allurereport/web-commons";
12
+ import type { RecursiveTree } from "@allurereport/web-components/global";
13
+ import { computed } from "@preact/signals";
14
+
15
+ import { getBodyItems } from "@/components/TestResult/bodyItems";
16
+ import {
17
+ collectExpandableStepNodes,
18
+ findStepBodyItems,
19
+ getStepTreeExpansionPolicy,
20
+ } from "@/components/TestResult/TrSteps/stepTreeExpansion";
21
+ import { collapsedEnvironments, currentEnvironment, environmentsStore } from "@/stores/env";
22
+ import {
23
+ activePane,
24
+ flatTree,
25
+ focusTestResultPane,
26
+ focusTreePane,
27
+ getFlatTreeNode,
28
+ isReportRootTabsContext,
29
+ isSearchInput,
30
+ isTestResultHotkeysContext,
31
+ isTreeNavigationContext,
32
+ lastSubtreeToggleByScope,
33
+ moveTreeFocus,
34
+ pendingVimKey,
35
+ setTreeFocusId,
36
+ treeFocusId,
37
+ treeScrollPaneToTopPending,
38
+ } from "@/stores/keyboard";
39
+ import { isSplitMode } from "@/stores/layout";
40
+ import { isModalOpen } from "@/stores/modal";
41
+ import { getReportEnvSectionId, type ReportEnvSection } from "@/stores/reportEnvSections";
42
+ import { cycleReportRootTab, navigateToReportRootTab, REPORT_ROOT_TAB } from "@/stores/reportRootTabs";
43
+ import { navigateToRoot, navigateToTestResult, rootTabRoute, testResultRoute } from "@/stores/router";
44
+ import { currentSection } from "@/stores/sections";
45
+ import { currentTrId, trCurrentTab } from "@/stores/testResult";
46
+ import {
47
+ applyTestResultFocusMove,
48
+ getFlatTestResultNode,
49
+ isTestResultOverviewNavigationContext,
50
+ moveTestResultFocus,
51
+ testResultFocusId,
52
+ toggleTestResultFocusNode,
53
+ } from "@/stores/testResultOverviewNav";
54
+ import { testResultNavStore, testResultStore } from "@/stores/testResults";
55
+ import { cycleTestResultTab, navigateToTestResultTabById, TEST_RESULT_TAB } from "@/stores/testResultTabs";
56
+ import { filteredTree, isTreeOpened, setTreeOpened, toggleTree } from "@/stores/tree";
57
+
58
+ const isTestResultRoute = computed(
59
+ () => testResultRoute.value.matches || Boolean(rootTabRoute.value.params.testResultId),
60
+ );
61
+
62
+ export const getHotkeyScope = (): HotkeyScope => {
63
+ if (isSplitMode.value) {
64
+ return activePane.value === "testResult" ? "testResult" : "tree";
65
+ }
66
+
67
+ if (activePane.value === "testResult" && isTestResultHotkeysContext()) {
68
+ return "testResult";
69
+ }
70
+
71
+ if (isReportRootTabsContext()) {
72
+ return "tree";
73
+ }
74
+
75
+ if (isTreeNavigationContext()) {
76
+ return "tree";
77
+ }
78
+
79
+ return "global";
80
+ };
81
+
82
+ export const isHotkeysEnabled = (): boolean => {
83
+ if (isModalOpen.value) {
84
+ return false;
85
+ }
86
+
87
+ if (currentSection.value !== "default") {
88
+ return false;
89
+ }
90
+
91
+ return true;
92
+ };
93
+
94
+ const resolveTreeOpenedByDefault = (node: ReturnType<typeof getFlatTreeNode>) => node?.openedByDefault ?? true;
95
+
96
+ const applyTreeMoveResult = (result: ReturnType<typeof moveTreeFocus>, options?: { scrollPaneToTop?: boolean }) => {
97
+ const node = result.nextId ? getFlatTreeNode(result.nextId) : undefined;
98
+ const openedByDefault = resolveTreeOpenedByDefault(node);
99
+
100
+ if (result.collapse && node) {
101
+ if (node.kind === "env" && node.nodeId && !collapsedEnvironments.value.includes(node.nodeId)) {
102
+ collapsedEnvironments.value = collapsedEnvironments.value.concat(node.nodeId);
103
+ } else if (node.kind === "group") {
104
+ toggleTree(node.id, openedByDefault);
105
+ }
106
+ }
107
+
108
+ if (result.expand && node) {
109
+ if (node.kind === "env" && node.nodeId) {
110
+ collapsedEnvironments.value = collapsedEnvironments.value.filter((envId) => envId !== node.nodeId);
111
+ } else if (node.kind === "group") {
112
+ toggleTree(node.id, openedByDefault);
113
+ }
114
+ }
115
+
116
+ if (result.nextId) {
117
+ setTreeFocusId(result.nextId);
118
+ }
119
+
120
+ if (options?.scrollPaneToTop) {
121
+ treeScrollPaneToTopPending.value = true;
122
+ }
123
+ };
124
+
125
+ export const scrollTreeListToTop = () => {
126
+ if (!isTreeNavigationContext()) {
127
+ return;
128
+ }
129
+
130
+ const anchor = treeFocusId.value ? document.querySelector(`[data-tree-node-id="${treeFocusId.value}"]`) : null;
131
+
132
+ scrollTreePaneToTop(anchor instanceof HTMLElement ? anchor : null);
133
+ };
134
+
135
+ export const applyTreeNavigation = (direction: MoveDirection | "g" | "gg" | "z" | "zt" | "t") => {
136
+ if (!isTreeNavigationContext()) {
137
+ return;
138
+ }
139
+
140
+ if (direction === "t") {
141
+ if (pendingVimKey.value === "z") {
142
+ pendingVimKey.value = null;
143
+ scrollTreeListToTop();
144
+ }
145
+
146
+ return;
147
+ }
148
+
149
+ if (direction === "z") {
150
+ pendingVimKey.value = "z";
151
+ window.setTimeout(() => {
152
+ if (pendingVimKey.value === "z") {
153
+ pendingVimKey.value = null;
154
+ }
155
+ }, 800);
156
+ return;
157
+ }
158
+
159
+ if (direction === "zt") {
160
+ scrollTreeListToTop();
161
+ return;
162
+ }
163
+
164
+ if (direction === "g") {
165
+ if (pendingVimKey.value === "g") {
166
+ pendingVimKey.value = null;
167
+ applyTreeMoveResult(moveTreeFocus("firstLeaf"), { scrollPaneToTop: true });
168
+ return;
169
+ }
170
+
171
+ pendingVimKey.value = "g";
172
+ window.setTimeout(() => {
173
+ if (pendingVimKey.value === "g") {
174
+ pendingVimKey.value = null;
175
+ }
176
+ }, 800);
177
+ return;
178
+ }
179
+
180
+ if (direction === "gg") {
181
+ applyTreeMoveResult(moveTreeFocus("firstLeaf"), { scrollPaneToTop: true });
182
+ return;
183
+ }
184
+
185
+ applyTreeMoveResult(moveTreeFocus(direction), { scrollPaneToTop: direction === "home" });
186
+ };
187
+
188
+ const setFocusedNodeExpanded = (expanded: boolean) => {
189
+ const node = getFlatTreeNode(treeFocusId.value);
190
+
191
+ if (!node?.nodeId) {
192
+ return;
193
+ }
194
+
195
+ if (node.kind === "env") {
196
+ const isOpened = !collapsedEnvironments.value.includes(node.nodeId);
197
+
198
+ if (isOpened === expanded) {
199
+ return;
200
+ }
201
+
202
+ collapsedEnvironments.value = expanded
203
+ ? collapsedEnvironments.value.filter((envId) => envId !== node.nodeId)
204
+ : collapsedEnvironments.value.concat(node.nodeId);
205
+ return;
206
+ }
207
+
208
+ if (node.kind === "group") {
209
+ setTreeOpened(node.id, expanded, resolveTreeOpenedByDefault(node));
210
+ }
211
+ };
212
+
213
+ export const collapseAllChildrenFromFocus = () => {
214
+ if (!isTreeNavigationContext()) {
215
+ return;
216
+ }
217
+
218
+ const focusId = treeFocusId.value;
219
+
220
+ if (!focusId) {
221
+ return;
222
+ }
223
+
224
+ for (const node of getExpandableDescendants(flatTree.value, focusId)) {
225
+ if (!node.nodeId) {
226
+ continue;
227
+ }
228
+
229
+ if (node.kind === "env") {
230
+ if (!collapsedEnvironments.value.includes(node.nodeId)) {
231
+ collapsedEnvironments.value = collapsedEnvironments.value.concat(node.nodeId);
232
+ }
233
+ continue;
234
+ }
235
+
236
+ if (node.kind === "group") {
237
+ const openedByDefault = node.openedByDefault ?? true;
238
+
239
+ if (isTreeOpened(node.id, openedByDefault)) {
240
+ setTreeOpened(node.id, false, openedByDefault);
241
+ }
242
+ }
243
+ }
244
+ };
245
+
246
+ export const expandAllChildrenFromFocus = () => {
247
+ if (!isTreeNavigationContext()) {
248
+ return;
249
+ }
250
+
251
+ const focusId = treeFocusId.value;
252
+
253
+ if (!focusId) {
254
+ return;
255
+ }
256
+
257
+ setFocusedNodeExpanded(true);
258
+
259
+ for (const node of getExpandableDescendants(flatTree.value, focusId)) {
260
+ if (!node.nodeId) {
261
+ continue;
262
+ }
263
+
264
+ if (node.kind === "env") {
265
+ collapsedEnvironments.value = collapsedEnvironments.value.filter((envId) => envId !== node.nodeId);
266
+ continue;
267
+ }
268
+
269
+ if (node.kind === "group") {
270
+ const openedByDefault = node.openedByDefault ?? true;
271
+
272
+ if (!isTreeOpened(node.id, openedByDefault)) {
273
+ setTreeOpened(node.id, true, openedByDefault);
274
+ }
275
+ }
276
+ }
277
+ };
278
+
279
+ const findGroupInTree = (tree: RecursiveTree, targetNodeId: string): RecursiveTree | null => {
280
+ if (tree.nodeId === targetNodeId) {
281
+ return tree;
282
+ }
283
+
284
+ for (const nested of tree.trees) {
285
+ const found = findGroupInTree(nested, targetNodeId);
286
+
287
+ if (found) {
288
+ return found;
289
+ }
290
+ }
291
+
292
+ return null;
293
+ };
294
+
295
+ const resolveTreeEnvId = (focusId: string): string | undefined => {
296
+ const envIds = new Set(environmentsStore.value.data.map((env) => env.id));
297
+ const colonIndex = focusId.indexOf(":");
298
+
299
+ if (colonIndex > 0) {
300
+ const prefix = focusId.slice(0, colonIndex);
301
+
302
+ if (envIds.has(prefix)) {
303
+ return prefix;
304
+ }
305
+ }
306
+
307
+ if (envIds.size === 1) {
308
+ return environmentsStore.value.data[0]?.id;
309
+ }
310
+
311
+ if (currentEnvironment.value && envIds.has(currentEnvironment.value)) {
312
+ return currentEnvironment.value;
313
+ }
314
+
315
+ return Object.keys(filteredTree.value).find((envId) => envIds.has(envId));
316
+ };
317
+
318
+ const getTreeFocusIdPrefix = (envId: string): string | undefined => {
319
+ if (environmentsStore.value.data.length <= 1) {
320
+ return undefined;
321
+ }
322
+
323
+ if (currentEnvironment.value) {
324
+ return undefined;
325
+ }
326
+
327
+ return `${envId}:`;
328
+ };
329
+
330
+ export const setFocusedSubtreeToggleState = (state: SubtreeToggleState) => {
331
+ if (!isTreeNavigationContext()) {
332
+ return;
333
+ }
334
+
335
+ const focusId = treeFocusId.value;
336
+
337
+ if (!focusId) {
338
+ return;
339
+ }
340
+
341
+ const flatNode = getFlatTreeNode(focusId);
342
+
343
+ if (!flatNode || flatNode.kind !== "group" || !flatNode.nodeId) {
344
+ return;
345
+ }
346
+
347
+ const envId = resolveTreeEnvId(focusId);
348
+ const envTree = envId ? filteredTree.value[envId] : undefined;
349
+
350
+ if (!envTree) {
351
+ return;
352
+ }
353
+
354
+ const groupTree = findGroupInTree(envTree, flatNode.nodeId);
355
+
356
+ if (!groupTree || !hasExpandableTreeChildren(groupTree)) {
357
+ return;
358
+ }
359
+
360
+ const focusIdPrefix = envId ? getTreeFocusIdPrefix(envId) : undefined;
361
+ const toScopedId = (nodeId: string) => (focusIdPrefix ? `${focusIdPrefix}${nodeId}` : nodeId);
362
+ const expandableSubtreeNodes = collectExpandableSubtreeNodes(groupTree);
363
+
364
+ applySubtreeToggleState(expandableSubtreeNodes, state, {
365
+ toScopedId,
366
+ isOpened: (scopedId, openedByDefault) => isTreeOpened(scopedId, openedByDefault),
367
+ setOpened: (scopedId, shouldOpen, openedByDefault) => setTreeOpened(scopedId, shouldOpen, openedByDefault),
368
+ });
369
+ };
370
+
371
+ export const collapseFocusedSubtree = () => setFocusedSubtreeToggleState("none");
372
+
373
+ export const expandFocusedSubtree = () => setFocusedSubtreeToggleState("all");
374
+
375
+ export const expandFocusedSubtreeFirstLevel = () => setFocusedSubtreeToggleState("first");
376
+
377
+ const rememberSubtreeToggle = (scopeKey: string, nextLastToggle: SubtreeToggleState | null) => {
378
+ const next = { ...lastSubtreeToggleByScope.value };
379
+
380
+ if (nextLastToggle) {
381
+ next[scopeKey] = nextLastToggle;
382
+ } else {
383
+ delete next[scopeKey];
384
+ }
385
+
386
+ lastSubtreeToggleByScope.value = next;
387
+ };
388
+
389
+ const getRememberedSubtreeToggle = (scopeKey: string): SubtreeToggleState | null =>
390
+ lastSubtreeToggleByScope.value[scopeKey] ?? null;
391
+
392
+ export const cycleFocusedSubtreeToggle = () => {
393
+ if (!isTreeNavigationContext()) {
394
+ return;
395
+ }
396
+
397
+ const focusId = treeFocusId.value;
398
+
399
+ if (!focusId) {
400
+ return;
401
+ }
402
+
403
+ const flatNode = getFlatTreeNode(focusId);
404
+
405
+ if (!flatNode || flatNode.kind !== "group" || !flatNode.nodeId) {
406
+ return;
407
+ }
408
+
409
+ const envId = resolveTreeEnvId(focusId);
410
+ const envTree = envId ? filteredTree.value[envId] : undefined;
411
+
412
+ if (!envTree) {
413
+ return;
414
+ }
415
+
416
+ const groupTree = findGroupInTree(envTree, flatNode.nodeId);
417
+
418
+ if (!groupTree || !hasExpandableTreeChildren(groupTree)) {
419
+ return;
420
+ }
421
+
422
+ const focusIdPrefix = envId ? getTreeFocusIdPrefix(envId) : undefined;
423
+ const toScopedId = (nodeId: string) => (focusIdPrefix ? `${focusIdPrefix}${nodeId}` : nodeId);
424
+ const expandableSubtreeNodes = collectExpandableSubtreeNodes(groupTree);
425
+ const isOpened = (nodeId: string, openedByDefault: boolean) => isTreeOpened(toScopedId(nodeId), openedByDefault);
426
+ const { nextState, nextLastToggle } = resolveNextSubtreeToggleState(
427
+ expandableSubtreeNodes,
428
+ isOpened,
429
+ getRememberedSubtreeToggle(focusId),
430
+ );
431
+
432
+ applySubtreeToggleState(expandableSubtreeNodes, nextState, {
433
+ toScopedId,
434
+ isOpened: (scopedId, openedByDefault) => isTreeOpened(scopedId, openedByDefault),
435
+ setOpened: (scopedId, shouldOpen, openedByDefault) => setTreeOpened(scopedId, shouldOpen, openedByDefault),
436
+ });
437
+ rememberSubtreeToggle(focusId, nextLastToggle);
438
+ };
439
+
440
+ export const cycleFocusedTestResultSubtreeToggle = () => {
441
+ if (!isTestResultOverviewNavigationContext()) {
442
+ return;
443
+ }
444
+
445
+ const focusId = testResultFocusId.value;
446
+
447
+ if (!focusId) {
448
+ return;
449
+ }
450
+
451
+ const flatNode = getFlatTestResultNode(focusId);
452
+
453
+ if (!flatNode?.nodeId || flatNode.kind !== "group") {
454
+ return;
455
+ }
456
+
457
+ const testResultId = currentTrId.value;
458
+ const testResult = testResultId ? testResultStore.value.data?.[testResultId] : undefined;
459
+
460
+ if (!testResult) {
461
+ return;
462
+ }
463
+
464
+ const policy = getStepTreeExpansionPolicy();
465
+ const bodyItems = getBodyItems(testResult, "");
466
+ const stepBodyItems = findStepBodyItems(bodyItems, flatNode.nodeId);
467
+
468
+ if (!stepBodyItems) {
469
+ return;
470
+ }
471
+
472
+ const openedByDefault = flatNode.openedByDefault ?? true;
473
+ const expandableDescendants = collectExpandableStepNodes(stepBodyItems, policy);
474
+
475
+ if (expandableDescendants.length === 0) {
476
+ return;
477
+ }
478
+
479
+ const subtreeNodes: SubtreeNodeState[] = [
480
+ { id: flatNode.nodeId, openedByDefault, isRoot: true },
481
+ ...expandableDescendants.map((node) => ({ ...node, isRoot: false })),
482
+ ];
483
+ const isOpened = (id: string, defaultOpened: boolean) => isTreeOpened(id, defaultOpened);
484
+ const { nextState, nextLastToggle } = resolveNextSubtreeToggleState(
485
+ subtreeNodes,
486
+ isOpened,
487
+ getRememberedSubtreeToggle(focusId),
488
+ );
489
+
490
+ applySubtreeToggleState(subtreeNodes, nextState, {
491
+ toScopedId: (id) => id,
492
+ isOpened,
493
+ setOpened: (id, shouldOpen, defaultOpened) => setTreeOpened(id, shouldOpen, defaultOpened),
494
+ });
495
+ rememberSubtreeToggle(focusId, nextLastToggle);
496
+ };
497
+
498
+ export const openTreeNodeFromFocus = () => {
499
+ if (!isTreeNavigationContext()) {
500
+ return;
501
+ }
502
+
503
+ const node = getFlatTreeNode(treeFocusId.value);
504
+
505
+ if (!node) {
506
+ return;
507
+ }
508
+
509
+ if (node.kind === "leaf" && node.testResultId) {
510
+ openTestResultFromTree();
511
+ return;
512
+ }
513
+
514
+ if (node.kind === "group" || node.kind === "env") {
515
+ toggleTreeNodeFromFocus();
516
+ }
517
+ };
518
+
519
+ export const toggleTreeNodeFromFocus = () => {
520
+ if (!isTreeNavigationContext()) {
521
+ return;
522
+ }
523
+
524
+ const node = getFlatTreeNode(treeFocusId.value);
525
+
526
+ if (!node) {
527
+ return;
528
+ }
529
+
530
+ if (node.kind === "env" && node.nodeId) {
531
+ const isOpened = !collapsedEnvironments.value.includes(node.nodeId);
532
+ collapsedEnvironments.value = isOpened
533
+ ? collapsedEnvironments.value.concat(node.nodeId)
534
+ : collapsedEnvironments.value.filter((envId) => envId !== node.nodeId);
535
+ return;
536
+ }
537
+
538
+ if (node.kind === "group") {
539
+ toggleTree(node.id, resolveTreeOpenedByDefault(node));
540
+ }
541
+ };
542
+
543
+ export const openTestResultFromTree = () => {
544
+ if (!isTreeNavigationContext()) {
545
+ return;
546
+ }
547
+
548
+ const node = getFlatTreeNode(treeFocusId.value);
549
+
550
+ if (!node || node.kind !== "leaf" || !node.testResultId) {
551
+ return;
552
+ }
553
+
554
+ navigateToTestResult({ testResultId: node.testResultId, tab: trCurrentTab.value });
555
+
556
+ if (isSplitMode.value) {
557
+ focusTestResultPane();
558
+ } else {
559
+ focusTestResultPane();
560
+ }
561
+ };
562
+
563
+ export const getSearchInput = (): HTMLInputElement | null =>
564
+ document.querySelector<HTMLInputElement>('[data-testid="search-input"]') ??
565
+ document.querySelector<HTMLInputElement>('input[name="search"]');
566
+
567
+ export const focusSearch = () => {
568
+ const input = getSearchInput();
569
+
570
+ if (!input) {
571
+ return;
572
+ }
573
+
574
+ input.focus();
575
+ input.select();
576
+ };
577
+
578
+ export const blurSearch = () => {
579
+ const active = document.activeElement;
580
+
581
+ if (!isSearchInput(active)) {
582
+ return;
583
+ }
584
+
585
+ active.blur();
586
+ };
587
+
588
+ export const focusTestResultPaneIfOpen = () => {
589
+ if (isSplitMode.value) {
590
+ focusTestResultPane();
591
+ return;
592
+ }
593
+
594
+ if (isTestResultRoute.value || currentTrId.value) {
595
+ focusTestResultPane();
596
+ }
597
+ };
598
+
599
+ /**
600
+ * Returns the ordered list of test result IDs for prev/next navigation.
601
+ *
602
+ * - Split mode: only tests visible in the tree (respects collapsed folders).
603
+ * - Base layout: full nav.json list, matching the pagination widget order.
604
+ */
605
+ const getNavLeafIds = (): string[] => {
606
+ if (isSplitMode.value) {
607
+ return flatTree.value.flatMap((node) => (node.kind === "leaf" && node.testResultId ? [node.testResultId] : []));
608
+ }
609
+
610
+ return testResultNavStore.value.data ?? [];
611
+ };
612
+
613
+ export const goToPrevTestResult = () => {
614
+ if (!isTestResultHotkeysContext()) {
615
+ return;
616
+ }
617
+
618
+ const currentId = currentTrId.value;
619
+
620
+ if (!currentId) {
621
+ return;
622
+ }
623
+
624
+ const leafIds = getNavLeafIds();
625
+ const currentIndex = leafIds.indexOf(currentId);
626
+
627
+ if (currentIndex <= 0) {
628
+ return;
629
+ }
630
+
631
+ const prevId = leafIds[currentIndex - 1];
632
+
633
+ if (!prevId) {
634
+ return;
635
+ }
636
+
637
+ navigateToTestResult({ testResultId: prevId, tab: trCurrentTab.value });
638
+ setTreeFocusId(undefined);
639
+ };
640
+
641
+ export const goToNextTestResult = () => {
642
+ if (!isTestResultHotkeysContext()) {
643
+ return;
644
+ }
645
+
646
+ const currentId = currentTrId.value;
647
+
648
+ if (!currentId) {
649
+ return;
650
+ }
651
+
652
+ const leafIds = getNavLeafIds();
653
+ const currentIndex = leafIds.indexOf(currentId);
654
+
655
+ if (currentIndex === -1 || currentIndex >= leafIds.length - 1) {
656
+ return;
657
+ }
658
+
659
+ const nextId = leafIds[currentIndex + 1];
660
+
661
+ if (!nextId) {
662
+ return;
663
+ }
664
+
665
+ navigateToTestResult({ testResultId: nextId, tab: trCurrentTab.value });
666
+ setTreeFocusId(undefined);
667
+ };
668
+
669
+ export const navigateDownInTestResultPane = () => goToNextTestResult();
670
+
671
+ export const navigateUpInTestResultPane = () => goToPrevTestResult();
672
+
673
+ export const goToTestResultTab = (tab: string) => {
674
+ if (!isTestResultHotkeysContext()) {
675
+ return;
676
+ }
677
+
678
+ navigateToTestResultTabById(tab as (typeof TEST_RESULT_TAB)[keyof typeof TEST_RESULT_TAB]);
679
+ };
680
+
681
+ export const goToReportRootTab = (tab: (typeof REPORT_ROOT_TAB)[keyof typeof REPORT_ROOT_TAB]) => {
682
+ if (!isReportRootTabsContext()) {
683
+ return;
684
+ }
685
+
686
+ navigateToReportRootTab(tab);
687
+ focusTreePane();
688
+ };
689
+
690
+ export const cycleReportRootTabHotkey = (direction: "next" | "prev") => {
691
+ if (!isReportRootTabsContext()) {
692
+ return;
693
+ }
694
+
695
+ cycleReportRootTab(direction);
696
+ focusTreePane();
697
+ };
698
+
699
+ export const cycleTestResultTabHotkey = (direction: "next" | "prev") => {
700
+ if (!isTestResultHotkeysContext()) {
701
+ return;
702
+ }
703
+
704
+ cycleTestResultTab(direction);
705
+ };
706
+
707
+ export const toggleReportEnvSection = (section: ReportEnvSection) => {
708
+ if (!isTreeNavigationContext()) {
709
+ return;
710
+ }
711
+
712
+ toggleTree(getReportEnvSectionId(section));
713
+ };
714
+
715
+ export const toggleMetadataSection = (section: "labels" | "parameters" | "links") => {
716
+ if (!isTestResultHotkeysContext()) {
717
+ return;
718
+ }
719
+
720
+ const testResultId = currentTrId.value;
721
+
722
+ if (!testResultId || trCurrentTab.value !== "overview") {
723
+ return;
724
+ }
725
+
726
+ toggleTree(`${testResultId}-${section}`);
727
+ };
728
+
729
+ export const applyTestResultOverviewNavigation = (direction: MoveDirection) => {
730
+ if (!isTestResultOverviewNavigationContext()) {
731
+ return;
732
+ }
733
+
734
+ applyTestResultFocusMove(moveTestResultFocus(direction));
735
+ };
736
+
737
+ export const toggleTestResultOverviewNode = () => {
738
+ if (!isTestResultOverviewNavigationContext()) {
739
+ return;
740
+ }
741
+
742
+ toggleTestResultFocusNode();
743
+ };
744
+
745
+ export const openTestResultOverviewFromFocus = () => {
746
+ if (!isTestResultOverviewNavigationContext()) {
747
+ return;
748
+ }
749
+
750
+ const node = getFlatTestResultNode(testResultFocusId.value);
751
+
752
+ if (!node) {
753
+ return;
754
+ }
755
+
756
+ if (node.nodeId) {
757
+ toggleTestResultFocusNode();
758
+ }
759
+ };
760
+
761
+ export const handleTestResultEscape = () => {
762
+ if (isSplitMode.value) {
763
+ focusTreePane();
764
+ return;
765
+ }
766
+
767
+ navigateToRoot();
768
+ focusTreePane();
769
+ };