@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
@@ -25,6 +25,7 @@ const namespaces = [
25
25
  "ui",
26
26
  "welcome",
27
27
  "controls",
28
+ "shortcuts",
28
29
  "errors",
29
30
  "split",
30
31
  "modal",
@@ -0,0 +1,6 @@
1
+ import { currentEnvironment } from "@/stores/env";
2
+
3
+ export type ReportEnvSection = "variables" | "metadata";
4
+
5
+ export const getReportEnvSectionId = (section: ReportEnvSection, envId = currentEnvironment.value ?? "default") =>
6
+ `report-${envId}-${section}`;
@@ -0,0 +1,95 @@
1
+ import { categoriesStore } from "@/stores/categories";
2
+ import { currentEnvironment } from "@/stores/env";
3
+ import { globalsStore } from "@/stores/globals";
4
+ import {
5
+ navigateToPlainTestResult,
6
+ navigateToRoot,
7
+ navigateToRootTabRoot,
8
+ navigateToRootTabTestResult,
9
+ rootTabRoute,
10
+ } from "@/stores/router";
11
+ import { currentTrId, trCurrentTab } from "@/stores/testResult";
12
+
13
+ export const REPORT_ROOT_TAB = {
14
+ Results: "results",
15
+ Categories: "categories",
16
+ QualityGate: "qualityGate",
17
+ GlobalAttachments: "globalAttachments",
18
+ GlobalErrors: "globalErrors",
19
+ } as const;
20
+
21
+ export type ReportRootTabId = (typeof REPORT_ROOT_TAB)[keyof typeof REPORT_ROOT_TAB];
22
+
23
+ const REPORT_ROOT_TAB_ORDER: ReportRootTabId[] = [
24
+ REPORT_ROOT_TAB.Results,
25
+ REPORT_ROOT_TAB.Categories,
26
+ REPORT_ROOT_TAB.QualityGate,
27
+ REPORT_ROOT_TAB.GlobalAttachments,
28
+ REPORT_ROOT_TAB.GlobalErrors,
29
+ ];
30
+
31
+ export const getAvailableReportRootTabs = (): ReportRootTabId[] => {
32
+ const tabs: ReportRootTabId[] = [REPORT_ROOT_TAB.Results];
33
+ const categories = categoriesStore.value.data;
34
+
35
+ if (categories?.roots?.length) {
36
+ tabs.push(REPORT_ROOT_TAB.Categories);
37
+ }
38
+
39
+ tabs.push(REPORT_ROOT_TAB.QualityGate, REPORT_ROOT_TAB.GlobalAttachments, REPORT_ROOT_TAB.GlobalErrors);
40
+
41
+ return tabs;
42
+ };
43
+
44
+ export const getCurrentReportRootTab = (): ReportRootTabId => {
45
+ if (rootTabRoute.value.matches) {
46
+ const rootTab = rootTabRoute.value.params.rootTab as ReportRootTabId;
47
+
48
+ if (REPORT_ROOT_TAB_ORDER.includes(rootTab)) {
49
+ return rootTab;
50
+ }
51
+ }
52
+
53
+ return REPORT_ROOT_TAB.Results;
54
+ };
55
+
56
+ export const navigateToReportRootTab = (tab: ReportRootTabId) => {
57
+ if (!getAvailableReportRootTabs().includes(tab)) {
58
+ return;
59
+ }
60
+
61
+ const testResultId = currentTrId.value;
62
+ const trTab = trCurrentTab.value;
63
+
64
+ if (tab === REPORT_ROOT_TAB.Results) {
65
+ if (testResultId) {
66
+ navigateToPlainTestResult({ testResultId, tab: trTab });
67
+ } else {
68
+ navigateToRoot();
69
+ }
70
+ return;
71
+ }
72
+
73
+ if (testResultId) {
74
+ navigateToRootTabTestResult({ rootTab: tab, testResultId, tab: trTab });
75
+ return;
76
+ }
77
+
78
+ navigateToRootTabRoot({ rootTab: tab });
79
+ };
80
+
81
+ export const cycleReportRootTab = (direction: "next" | "prev") => {
82
+ const available = getAvailableReportRootTabs();
83
+ const current = getCurrentReportRootTab();
84
+ const index = available.indexOf(current);
85
+
86
+ if (index < 0) {
87
+ navigateToReportRootTab(available[0] ?? REPORT_ROOT_TAB.Results);
88
+ return;
89
+ }
90
+
91
+ const nextIndex =
92
+ direction === "next" ? (index + 1) % available.length : (index - 1 + available.length) % available.length;
93
+
94
+ navigateToReportRootTab(available[nextIndex]!);
95
+ };
@@ -0,0 +1,147 @@
1
+ import { ReportFetchError, errorMessageFromUnknown, fetchReportJsonData } from "@allurereport/web-commons";
2
+ import { signal } from "@preact/signals";
3
+ import MiniSearch from "minisearch";
4
+ import type { AwesomeSearchDocument } from "types";
5
+
6
+ import type { StoreSignalState } from "@/stores/types";
7
+
8
+ const SEARCH_FIELDS: (keyof AwesomeSearchDocument)[] = [
9
+ "id",
10
+ "name",
11
+ "fullName",
12
+ "owner",
13
+ "tags",
14
+ "labels",
15
+ "links",
16
+ "categories",
17
+ "parameters",
18
+ "statusMessage",
19
+ "historyId",
20
+ ];
21
+
22
+ const STORE_FIELDS: (keyof AwesomeSearchDocument)[] = ["nodeId", "name"];
23
+
24
+ export const createSearchIndex = (documents: AwesomeSearchDocument[]) => {
25
+ const searchIndex = new MiniSearch<AwesomeSearchDocument>({
26
+ fields: SEARCH_FIELDS,
27
+ storeFields: STORE_FIELDS,
28
+ searchOptions: {
29
+ combineWith: "AND",
30
+ prefix: true,
31
+ fuzzy: (term) => (term.length > 3 ? 0.2 : false),
32
+ maxFuzzy: 2,
33
+ boost: {
34
+ name: 4,
35
+ fullName: 3,
36
+ owner: 3,
37
+ tags: 2,
38
+ labels: 2,
39
+ links: 1.5,
40
+ categories: 1,
41
+ parameters: 1,
42
+ statusMessage: 0.75,
43
+ historyId: 0.5,
44
+ },
45
+ },
46
+ });
47
+
48
+ searchIndex.addAll(documents);
49
+
50
+ return searchIndex;
51
+ };
52
+
53
+ export const searchNodeIds = (searchIndex: MiniSearch<AwesomeSearchDocument>, query: string) => {
54
+ const normalizedQuery = query.trim();
55
+
56
+ if (!normalizedQuery) {
57
+ return new Set<string>();
58
+ }
59
+
60
+ return new Set(searchIndex.search(normalizedQuery).map(({ nodeId }) => nodeId));
61
+ };
62
+
63
+ export const searchIndexesStore = signal<StoreSignalState<Record<string, MiniSearch<AwesomeSearchDocument>>>>({
64
+ loading: false,
65
+ error: undefined,
66
+ data: {},
67
+ });
68
+
69
+ const loadingSearchEnvIds = new Set<string>();
70
+ const failedSearchEnvIds = new Set<string>();
71
+
72
+ const searchIndexPath = (env: string) => `widgets/${env}/search-index.json`;
73
+ const isMissingSearchIndexError = (error: unknown) =>
74
+ error instanceof ReportFetchError && error.response.status === 404;
75
+
76
+ export const resetSearchIndexes = () => {
77
+ loadingSearchEnvIds.clear();
78
+ failedSearchEnvIds.clear();
79
+ searchIndexesStore.value = {
80
+ loading: false,
81
+ error: undefined,
82
+ data: {},
83
+ };
84
+ };
85
+
86
+ export const fetchEnvSearchIndexes = async (envs: string[]) => {
87
+ const currentData = searchIndexesStore.peek().data ?? {};
88
+ const envsToFetch = envs.filter(
89
+ (env) => !currentData[env] && !loadingSearchEnvIds.has(env) && !failedSearchEnvIds.has(env),
90
+ );
91
+
92
+ if (envsToFetch.length === 0) {
93
+ return;
94
+ }
95
+
96
+ envsToFetch.forEach((env) => loadingSearchEnvIds.add(env));
97
+
98
+ searchIndexesStore.value = {
99
+ ...searchIndexesStore.peek(),
100
+ loading: true,
101
+ error: undefined,
102
+ };
103
+
104
+ try {
105
+ const documentsByEnv = await Promise.allSettled(
106
+ envsToFetch.map(async (env) => ({
107
+ env,
108
+ documents: await fetchReportJsonData<AwesomeSearchDocument[]>(searchIndexPath(env), { bustCache: true }),
109
+ })),
110
+ );
111
+ const loadedSearchIndexes: Record<string, MiniSearch<AwesomeSearchDocument>> = {};
112
+ let error: string | undefined;
113
+
114
+ for (const [index, result] of documentsByEnv.entries()) {
115
+ if (result.status === "fulfilled") {
116
+ loadedSearchIndexes[result.value.env] = createSearchIndex(result.value.documents);
117
+ continue;
118
+ }
119
+
120
+ if (!error) {
121
+ error = errorMessageFromUnknown(result.reason);
122
+ }
123
+
124
+ if (isMissingSearchIndexError(result.reason)) {
125
+ failedSearchEnvIds.add(envsToFetch[index]);
126
+ }
127
+ }
128
+
129
+ searchIndexesStore.value = {
130
+ data: {
131
+ ...(searchIndexesStore.peek().data ?? {}),
132
+ ...loadedSearchIndexes,
133
+ },
134
+ loading: false,
135
+ error,
136
+ };
137
+ } catch (e) {
138
+ // Retry malformed/transient responses; only missing search indexes are permanently cached.
139
+ searchIndexesStore.value = {
140
+ ...searchIndexesStore.peek(),
141
+ error: errorMessageFromUnknown(e),
142
+ loading: false,
143
+ };
144
+ } finally {
145
+ envsToFetch.forEach((env) => loadingSearchEnvIds.delete(env));
146
+ }
147
+ };
@@ -0,0 +1,119 @@
1
+ import { moveFocus, type FlatTreeNode, type MoveDirection, type MoveFocusResult } from "@allurereport/web-commons";
2
+ import { computed, effect, signal } from "@preact/signals";
3
+
4
+ import { fixtureResultToTrStepItem, getBodyItems } from "@/components/TestResult/bodyItems";
5
+ import { isTestResultHotkeysContext } from "@/stores/keyboard";
6
+ import { currentTrId, trCurrentTab } from "@/stores/testResult";
7
+ import { testResultStore } from "@/stores/testResults";
8
+ import { collapsedTrees, isTreeOpened, setTreeOpened, toggleTree } from "@/stores/tree";
9
+ import { flattenTestResultOverview } from "@/utils/flattenTestResultOverview";
10
+ export const testResultFocusId = signal<string | undefined>(undefined);
11
+
12
+ export const isTestResultOverviewNavigationContext = (): boolean =>
13
+ isTestResultHotkeysContext() && trCurrentTab.value === "overview";
14
+
15
+ const buildFlatOverview = (): FlatTreeNode[] => {
16
+ const testResultId = currentTrId.value;
17
+
18
+ if (!testResultId) {
19
+ return [];
20
+ }
21
+
22
+ const testResult = testResultStore.value.data?.[testResultId];
23
+
24
+ if (!testResult) {
25
+ return [];
26
+ }
27
+
28
+ collapsedTrees.value;
29
+
30
+ const bodyItems = getBodyItems(testResult, "");
31
+ const setupBodyItems = (testResult.setup ?? []).map((fixture) => fixtureResultToTrStepItem(fixture));
32
+ const teardownBodyItems = (testResult.teardown ?? []).map((fixture) => fixtureResultToTrStepItem(fixture));
33
+
34
+ return flattenTestResultOverview({
35
+ testResultId,
36
+ hasSetup: setupBodyItems.length > 0,
37
+ setupBodyItems,
38
+ bodyItems,
39
+ hasTeardown: teardownBodyItems.length > 0,
40
+ teardownBodyItems,
41
+ isGroupOpened: (id, openedByDefault) => isTreeOpened(id, openedByDefault),
42
+ });
43
+ };
44
+
45
+ export const flatTestResultOverview = computed(() => buildFlatOverview());
46
+
47
+ export const getFlatTestResultNode = (id: string | undefined) =>
48
+ flatTestResultOverview.value.find((node) => node.id === id);
49
+
50
+ export const moveTestResultFocus = (direction: MoveDirection): MoveFocusResult =>
51
+ moveFocus(flatTestResultOverview.value, testResultFocusId.value, direction);
52
+
53
+ export const setTestResultFocusId = (id: string | undefined) => {
54
+ testResultFocusId.value = id;
55
+ };
56
+
57
+ export const ensureTestResultFocusId = () => {
58
+ const flat = flatTestResultOverview.value;
59
+
60
+ if (flat.length === 0) {
61
+ testResultFocusId.value = undefined;
62
+ return;
63
+ }
64
+
65
+ const currentId = testResultFocusId.value;
66
+ const currentExists = currentId ? flat.some((node) => node.id === currentId) : false;
67
+
68
+ if (currentExists) {
69
+ return;
70
+ }
71
+
72
+ testResultFocusId.value = flat[0]?.id;
73
+ };
74
+
75
+ effect(() => {
76
+ if (!isTestResultOverviewNavigationContext()) {
77
+ return;
78
+ }
79
+
80
+ flatTestResultOverview.value;
81
+ ensureTestResultFocusId();
82
+ });
83
+
84
+ effect(() => {
85
+ currentTrId.value;
86
+ trCurrentTab.value;
87
+
88
+ if (trCurrentTab.value !== "overview") {
89
+ testResultFocusId.value = undefined;
90
+ }
91
+ });
92
+
93
+ const resolveOpenedByDefault = (node: FlatTreeNode) => node.openedByDefault ?? true;
94
+
95
+ export const applyTestResultFocusMove = (result: MoveFocusResult) => {
96
+ const node = result.nextId ? getFlatTestResultNode(result.nextId) : undefined;
97
+
98
+ if (result.collapse && node?.nodeId) {
99
+ setTreeOpened(node.nodeId, false, resolveOpenedByDefault(node));
100
+ }
101
+
102
+ if (result.expand && node?.nodeId) {
103
+ setTreeOpened(node.nodeId, true, resolveOpenedByDefault(node));
104
+ }
105
+
106
+ if (result.nextId) {
107
+ setTestResultFocusId(result.nextId);
108
+ }
109
+ };
110
+
111
+ export const toggleTestResultFocusNode = () => {
112
+ const node = getFlatTestResultNode(testResultFocusId.value);
113
+
114
+ if (!node?.nodeId) {
115
+ return;
116
+ }
117
+
118
+ toggleTree(node.nodeId, resolveOpenedByDefault(node));
119
+ };
@@ -0,0 +1,62 @@
1
+ import { navigateToTestResultTab } from "@/stores/router";
2
+ import { currentTrId, trCurrentTab } from "@/stores/testResult";
3
+
4
+ export const TEST_RESULT_TAB = {
5
+ Overview: "overview",
6
+ History: "history",
7
+ Retries: "retries",
8
+ Attachments: "attachments",
9
+ Environments: "environments",
10
+ } as const;
11
+
12
+ export type TestResultTabId = (typeof TEST_RESULT_TAB)[keyof typeof TEST_RESULT_TAB];
13
+
14
+ const TEST_RESULT_TAB_ORDER: TestResultTabId[] = [
15
+ TEST_RESULT_TAB.Overview,
16
+ TEST_RESULT_TAB.History,
17
+ TEST_RESULT_TAB.Retries,
18
+ TEST_RESULT_TAB.Attachments,
19
+ TEST_RESULT_TAB.Environments,
20
+ ];
21
+
22
+ export const getCurrentTestResultTab = (): TestResultTabId => {
23
+ const tab = trCurrentTab.value as TestResultTabId;
24
+
25
+ if (TEST_RESULT_TAB_ORDER.includes(tab)) {
26
+ return tab;
27
+ }
28
+
29
+ return TEST_RESULT_TAB.Overview;
30
+ };
31
+
32
+ export const navigateToTestResultTabById = (tab: TestResultTabId) => {
33
+ const testResultId = currentTrId.value;
34
+
35
+ if (!testResultId) {
36
+ return;
37
+ }
38
+
39
+ if (tab === TEST_RESULT_TAB.Overview) {
40
+ navigateToTestResultTab({ testResultId, tab: "" });
41
+ return;
42
+ }
43
+
44
+ navigateToTestResultTab({ testResultId, tab });
45
+ };
46
+
47
+ export const cycleTestResultTab = (direction: "next" | "prev") => {
48
+ const testResultId = currentTrId.value;
49
+
50
+ if (!testResultId) {
51
+ return;
52
+ }
53
+
54
+ const current = getCurrentTestResultTab();
55
+ const index = TEST_RESULT_TAB_ORDER.indexOf(current);
56
+ const nextIndex =
57
+ direction === "next"
58
+ ? (index + 1) % TEST_RESULT_TAB_ORDER.length
59
+ : (index - 1 + TEST_RESULT_TAB_ORDER.length) % TEST_RESULT_TAB_ORDER.length;
60
+
61
+ navigateToTestResultTabById(TEST_RESULT_TAB_ORDER[nextIndex]!);
62
+ };
@@ -6,7 +6,7 @@ import type { StoreSignalState } from "@/stores/types";
6
6
 
7
7
  export type TimlineTr = Pick<
8
8
  TestResult,
9
- "id" | "name" | "status" | "flaky" | "hidden" | "environment" | "start" | "duration"
9
+ "id" | "name" | "status" | "flaky" | "isRetry" | "environment" | "start" | "duration"
10
10
  > & {
11
11
  environmentName?: string;
12
12
  host: string;
@@ -7,7 +7,9 @@ import type { StoreSignalState } from "@/stores/types";
7
7
  import { loadFromLocalStorage } from "@/utils/loadFromLocalStorage";
8
8
  import { createRecursiveTree, isRecursiveTreeEmpty } from "@/utils/treeFilters";
9
9
 
10
- import { treeFilters } from "./treeFilters/store";
10
+ import { currentEnvironment } from "./env";
11
+ import { fetchEnvSearchIndexes, searchIndexesStore, searchNodeIds } from "./search";
12
+ import { treeNonQueryFilters, treeQueryFilterValue } from "./treeFilters/store";
11
13
  import { sortBy } from "./treeSort";
12
14
 
13
15
  export const treeStore = signal<StoreSignalState<Record<string, AwesomeTree>>>({
@@ -117,13 +119,48 @@ const treeEntries = computed(() => (treeStore.value.data ? Object.entries(treeSt
117
119
  const alwaysTruePredicate = () => true;
118
120
 
119
121
  const filterPredicate = computed(() => {
120
- if (treeFilters.value.length === 0) {
122
+ if (treeNonQueryFilters.value.length === 0) {
121
123
  return alwaysTruePredicate;
122
124
  }
123
125
 
124
- return buildFilterPredicate(treeFilters.value);
126
+ return buildFilterPredicate(treeNonQueryFilters.value);
125
127
  });
126
128
 
129
+ effect(() => {
130
+ if (!treeQueryFilterValue.value?.trim()) {
131
+ return;
132
+ }
133
+
134
+ const treeEnvIds = Object.keys(treeStore.value.data ?? {});
135
+ const envsToFetch = currentEnvironment.value ? [currentEnvironment.value] : treeEnvIds;
136
+
137
+ if (envsToFetch.length === 0) {
138
+ return;
139
+ }
140
+
141
+ fetchEnvSearchIndexes(envsToFetch);
142
+ });
143
+
144
+ const searchFilterPredicate = (env: string) => {
145
+ const query = treeQueryFilterValue.value?.trim();
146
+
147
+ if (!query) {
148
+ return alwaysTruePredicate;
149
+ }
150
+
151
+ const searchIndex = searchIndexesStore.value.data?.[env];
152
+
153
+ if (!searchIndex) {
154
+ fetchEnvSearchIndexes([env]);
155
+
156
+ return alwaysTruePredicate;
157
+ }
158
+
159
+ const matchingNodeIds = searchNodeIds(searchIndex, query);
160
+
161
+ return (leaf: { nodeId: string }) => matchingNodeIds.has(leaf.nodeId);
162
+ };
163
+
127
164
  export const filteredTree = computed(() => {
128
165
  return treeEntries.value.reduce(
129
166
  (acc, [key, value]) => {
@@ -132,12 +169,13 @@ export const filteredTree = computed(() => {
132
169
  }
133
170
 
134
171
  const { root, leavesById, groupsById } = value;
172
+ const envSearchFilterPredicate = searchFilterPredicate(key);
135
173
 
136
174
  const tree = createRecursiveTree({
137
175
  group: root as AwesomeTreeGroup,
138
176
  leavesById,
139
177
  groupsById,
140
- filterPredicate: filterPredicate.value,
178
+ filterPredicate: (leaf) => filterPredicate.value(leaf) && envSearchFilterPredicate(leaf),
141
179
  sortBy: sortBy.value,
142
180
  });
143
181
 
@@ -114,38 +114,9 @@ const treeStatusFilter = computed<AwesomeStringFieldFilter>(() => ({
114
114
  },
115
115
  }));
116
116
 
117
- export const treeQueryFilter = computed<AwesomeFilterGroupSimple>(() => {
118
- return {
119
- type: "group",
120
- logicalOperator: "AND",
121
- value: [
122
- {
123
- type: "field",
124
- logicalOperator: "OR",
125
- value: {
126
- key: "name",
127
- value: urlQueryFilter.value,
128
- type: "string",
129
- strict: false,
130
- },
131
- },
132
- {
133
- type: "field",
134
- logicalOperator: "OR",
135
- value: {
136
- key: "id",
137
- value: urlQueryFilter.value,
138
- type: "string",
139
- strict: false,
140
- },
141
- },
142
- ],
143
- };
144
- });
117
+ export const treeQueryFilterValue = computed(() => urlQueryFilter.value);
145
118
 
146
- export const treeQueryFilterValue = computed(() => treeQueryFilter.value.value[0].value.value as string);
147
-
148
- export const setTreeQueryFilter = (query: string) => {
119
+ export const setTreeQueryFilter = (query?: string) => {
149
120
  setQueryFilter(query);
150
121
  };
151
122
 
@@ -217,13 +188,9 @@ export const treeQuickFilters = computed<AwesomeFilter[]>(() => [
217
188
  treeCategoriesFilter.value,
218
189
  ]);
219
190
 
220
- export const treeFilters = computed(() => {
191
+ export const treeNonQueryFilters = computed(() => {
221
192
  const filters: AwesomeFilter[] = [];
222
193
 
223
- if (treeQueryFilterValue.value) {
224
- filters.push(treeQueryFilter.value);
225
- }
226
-
227
194
  const hasBothRetryAndFlaky = urlRetryFilter.value && urlFlakyFilter.value;
228
195
 
229
196
  if (hasBothRetryAndFlaky) {
@@ -0,0 +1,8 @@
1
+ // border-top on the white pane card (visible above header/content; inset shadow is covered by children).
2
+ @mixin split-pane-indicator {
3
+ border-top: 3px solid transparent;
4
+ }
5
+
6
+ @mixin split-pane-indicator-active {
7
+ border-top-color: var(--bg-support-aldebaran);
8
+ }
package/src/styles.scss CHANGED
@@ -39,7 +39,7 @@
39
39
  display: flex;
40
40
  align-items: center;
41
41
  justify-content: center;
42
- background: var(--bg-base-secondary);
42
+ background: var(--color-bg-canvas);
43
43
  background-blend-mode: saturation;
44
44
  flex-direction: column;
45
45
  gap: 16px;