@allurereport/web-awesome 3.0.1 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/multi/173.app-d0210ed2e64d38a2ee8e.js +1 -0
- package/dist/multi/174.app-d0210ed2e64d38a2ee8e.js +1 -0
- package/dist/multi/252.app-d0210ed2e64d38a2ee8e.js +1 -0
- package/dist/multi/282.app-d0210ed2e64d38a2ee8e.js +1 -0
- package/dist/multi/29.app-d0210ed2e64d38a2ee8e.js +1 -0
- package/dist/multi/416.app-d0210ed2e64d38a2ee8e.js +1 -0
- package/dist/multi/527.app-d0210ed2e64d38a2ee8e.js +1 -0
- package/dist/multi/600.app-d0210ed2e64d38a2ee8e.js +1 -0
- package/dist/multi/605.app-d0210ed2e64d38a2ee8e.js +1 -0
- package/dist/multi/638.app-d0210ed2e64d38a2ee8e.js +1 -0
- package/dist/multi/672.app-d0210ed2e64d38a2ee8e.js +1 -0
- package/dist/multi/686.app-d0210ed2e64d38a2ee8e.js +1 -0
- package/dist/multi/725.app-d0210ed2e64d38a2ee8e.js +1 -0
- package/dist/multi/741.app-d0210ed2e64d38a2ee8e.js +1 -0
- package/dist/multi/749.app-d0210ed2e64d38a2ee8e.js +1 -0
- package/dist/multi/755.app-d0210ed2e64d38a2ee8e.js +1 -0
- package/dist/multi/894.app-d0210ed2e64d38a2ee8e.js +1 -0
- package/dist/multi/943.app-d0210ed2e64d38a2ee8e.js +1 -0
- package/dist/multi/980.app-d0210ed2e64d38a2ee8e.js +1 -0
- package/dist/multi/app-d0210ed2e64d38a2ee8e.js +2 -0
- package/dist/multi/{app-bae2a0fe5738d77cd976.js.LICENSE.txt → app-d0210ed2e64d38a2ee8e.js.LICENSE.txt} +7 -0
- package/dist/multi/manifest.json +21 -21
- package/dist/multi/styles-13107bbe6906beabc50f.css +49 -0
- package/dist/single/app-01fed10ad5f9083fd39c.js +2 -0
- package/dist/single/{app-996d3b5869f8fc942b66.js.LICENSE.txt → app-01fed10ad5f9083fd39c.js.LICENSE.txt} +7 -0
- package/dist/single/manifest.json +1 -1
- package/package.json +9 -11
- package/src/assets/scss/vars.scss +3 -0
- package/src/components/BaseLayout/index.tsx +25 -21
- package/src/components/BaseLayout/styles.scss +1 -0
- package/src/components/Charts/index.tsx +10 -7
- package/src/components/Footer/FooterVersion.tsx +14 -8
- package/src/components/Header/index.tsx +9 -7
- package/src/components/HeaderControls/index.tsx +5 -2
- package/src/components/MainReport/index.tsx +52 -47
- package/src/components/MainReport/styles.scss +1 -0
- package/src/components/Metadata/index.tsx +98 -7
- package/src/components/Metadata/styles.scss +10 -0
- package/src/components/ReportBody/HeaderActions.tsx +4 -13
- package/src/components/ReportBody/SortBy.tsx +27 -13
- package/src/components/ReportBody/index.tsx +1 -1
- package/src/components/ReportBody/styles.scss +4 -1
- package/src/components/ReportFilters/BaseFilters.tsx +345 -0
- package/src/components/ReportFilters/RetryFlaky.tsx +29 -0
- package/src/components/ReportFilters/TagsFilter.tsx +41 -0
- package/src/components/ReportFilters/TransitionFilter.tsx +49 -0
- package/src/components/ReportFilters/index.tsx +44 -0
- package/src/components/ReportFilters/styles.scss +55 -0
- package/src/components/ReportQualityGateResults/index.tsx +77 -19
- package/src/components/ReportQualityGateResults/styles.scss +13 -0
- package/src/components/ReportSearch/index.tsx +29 -0
- package/src/components/ReportTabs/index.tsx +1 -1
- package/src/components/SectionPicker/index.tsx +1 -1
- package/src/components/SplitLayout/index.tsx +7 -3
- package/src/components/TestResult/TrDescription/index.tsx +60 -10
- package/src/components/TestResult/TrDescription/styles.scss +4 -0
- package/src/components/TestResult/TrEnvironmentItem/index.tsx +2 -2
- package/src/components/TestResult/TrError/TrDiff.tsx +2 -6
- package/src/components/TestResult/TrError/styles.scss +4 -0
- package/src/components/TestResult/TrHeader/TrBreadcrumbs.tsx +2 -2
- package/src/components/TestResult/TrHistory/TrHistoryItem.tsx +38 -7
- package/src/components/TestResult/TrHistory/index.tsx +18 -8
- package/src/components/TestResult/TrHistory/styles.scss +4 -7
- package/src/components/TestResult/TrInfo/styles.scss +1 -0
- package/src/components/TestResult/TrLinks/index.tsx +4 -4
- package/src/components/TestResult/TrNavigation/index.tsx +109 -68
- package/src/components/TestResult/TrNavigation/styles.scss +15 -25
- package/src/components/TestResult/TrOverview.tsx +3 -2
- package/src/components/TestResult/TrPwTraces/PwTraceButton.tsx +1 -8
- package/src/components/TestResult/TrRetriesView/TrRetriesItem.tsx +2 -3
- package/src/components/TestResult/TrRetriesView/index.tsx +4 -3
- package/src/components/TestResult/TrSteps/TrAttachment.tsx +5 -3
- package/src/components/TestResult/TrSteps/TrAttachmentInfo.tsx +10 -3
- package/src/components/TestResult/TrTabs/index.tsx +7 -23
- package/src/components/TestResult/index.tsx +9 -4
- package/src/components/TestResult/styles.scss +1 -0
- package/src/components/Timeline/index.tsx +2 -5
- package/src/components/Tree/index.tsx +14 -9
- package/src/index.html +19 -18
- package/src/index.tsx +20 -27
- package/src/locales/az.json +42 -13
- package/src/locales/de.json +42 -13
- package/src/locales/en.json +42 -13
- package/src/locales/es.json +42 -13
- package/src/locales/fr.json +42 -13
- package/src/locales/he.json +42 -13
- package/src/locales/hy.json +42 -13
- package/src/locales/it.json +42 -13
- package/src/locales/ja.json +42 -13
- package/src/locales/ka.json +42 -13
- package/src/locales/kr.json +42 -13
- package/src/locales/nl.json +42 -13
- package/src/locales/pl.json +42 -13
- package/src/locales/pt.json +42 -13
- package/src/locales/ru.json +42 -13
- package/src/locales/sv.json +42 -13
- package/src/locales/tr.json +42 -13
- package/src/locales/{ua.json → uk.json} +42 -13
- package/src/locales/zh.json +42 -13
- package/src/stores/chart.ts +2 -2
- package/src/stores/env.ts +6 -6
- package/src/stores/envInfo.ts +2 -2
- package/src/stores/globals.ts +1 -1
- package/src/stores/index.ts +0 -1
- package/src/stores/layout.ts +20 -11
- package/src/stores/locale.ts +71 -38
- package/src/stores/qualityGate.ts +4 -4
- package/src/stores/router.ts +25 -91
- package/src/stores/sections.ts +32 -45
- package/src/stores/stats.ts +4 -4
- package/src/stores/testResult.ts +5 -0
- package/src/stores/testResults.ts +7 -5
- package/src/stores/timeline.ts +5 -2
- package/src/stores/tree.ts +20 -13
- package/src/stores/treeFilters/actions.ts +48 -52
- package/src/stores/treeFilters/constants.ts +11 -5
- package/src/stores/treeFilters/model.ts +51 -0
- package/src/stores/treeFilters/store.ts +260 -60
- package/src/stores/treeFilters/utils.ts +132 -0
- package/src/stores/treeSort.ts +71 -0
- package/src/stores/variables.ts +3 -3
- package/src/utils/ownerAddress.ts +92 -0
- package/src/utils/time.ts +16 -2
- package/src/utils/treeFilters.ts +48 -66
- package/test/components/Header.test.tsx +49 -58
- package/test/utils/ownerAddress.test.ts +89 -0
- package/test/utils/treeFilters.test.ts +18 -321
- package/types.d.ts +3 -2
- package/dist/multi/173.app-bae2a0fe5738d77cd976.js +0 -1
- package/dist/multi/174.app-bae2a0fe5738d77cd976.js +0 -1
- package/dist/multi/252.app-bae2a0fe5738d77cd976.js +0 -1
- package/dist/multi/282.app-bae2a0fe5738d77cd976.js +0 -1
- package/dist/multi/29.app-bae2a0fe5738d77cd976.js +0 -1
- package/dist/multi/416.app-bae2a0fe5738d77cd976.js +0 -1
- package/dist/multi/527.app-bae2a0fe5738d77cd976.js +0 -1
- package/dist/multi/600.app-bae2a0fe5738d77cd976.js +0 -1
- package/dist/multi/605.app-bae2a0fe5738d77cd976.js +0 -1
- package/dist/multi/638.app-bae2a0fe5738d77cd976.js +0 -1
- package/dist/multi/672.app-bae2a0fe5738d77cd976.js +0 -1
- package/dist/multi/686.app-bae2a0fe5738d77cd976.js +0 -1
- package/dist/multi/725.app-bae2a0fe5738d77cd976.js +0 -1
- package/dist/multi/741.app-bae2a0fe5738d77cd976.js +0 -1
- package/dist/multi/755.app-bae2a0fe5738d77cd976.js +0 -1
- package/dist/multi/894.app-bae2a0fe5738d77cd976.js +0 -1
- package/dist/multi/91.app-bae2a0fe5738d77cd976.js +0 -1
- package/dist/multi/943.app-bae2a0fe5738d77cd976.js +0 -1
- package/dist/multi/980.app-bae2a0fe5738d77cd976.js +0 -1
- package/dist/multi/app-bae2a0fe5738d77cd976.js +0 -2
- package/dist/multi/styles-bbf68b2ba63c38b53c38.css +0 -48
- package/dist/single/app-996d3b5869f8fc942b66.js +0 -2
- package/src/components/ReportBody/Filters.tsx +0 -122
- package/src/stores/theme.ts +0 -30
- package/src/stores/treeFilters/index.ts +0 -3
- package/src/stores/treeFilters/types.ts +0 -12
- package/test/stores/treeFilters.test.ts +0 -302
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { getParamValue, hasParam, setParams } from "@allurereport/web-commons";
|
|
2
|
+
import { computed, effect } from "@preact/signals";
|
|
3
|
+
|
|
4
|
+
export type SortByDirection = "asc" | "desc";
|
|
5
|
+
export type SortByField = "order" | "duration" | "status" | "name";
|
|
6
|
+
export type SortBy = `${SortByField},${SortByDirection}`;
|
|
7
|
+
|
|
8
|
+
const DEFAULT_SORT_BY: SortBy = "order,asc";
|
|
9
|
+
|
|
10
|
+
export const DIRECTIONS: SortByDirection[] = ["asc", "desc"];
|
|
11
|
+
|
|
12
|
+
export const SORT_BY_STORAGE_KEY = "ALLURE_REPORT_SORT_BY";
|
|
13
|
+
export const SORT_BY_FIELDS: SortByField[] = ["order", "duration", "status", "name"];
|
|
14
|
+
|
|
15
|
+
const SORT_BY_PARAM = "sortBy";
|
|
16
|
+
|
|
17
|
+
const hasSortByParam = computed(() => hasParam(SORT_BY_PARAM));
|
|
18
|
+
|
|
19
|
+
export const setSortBy = (sortByValue: SortBy) => {
|
|
20
|
+
if (hasSortByParam.peek()) {
|
|
21
|
+
setParams({
|
|
22
|
+
key: SORT_BY_PARAM,
|
|
23
|
+
value: undefined,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (typeof window === "undefined") {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
localStorage.setItem(SORT_BY_STORAGE_KEY, sortByValue);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const validateSortBy = (sortByValue: string): sortByValue is SortBy => {
|
|
35
|
+
const parts = sortByValue.split(",");
|
|
36
|
+
if (parts.length !== 2) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
const [field, direction] = parts;
|
|
40
|
+
|
|
41
|
+
return SORT_BY_FIELDS.includes(field as SortByField) && DIRECTIONS.includes(direction as SortByDirection);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const sortBy = computed<SortBy>(() => {
|
|
45
|
+
const urlSortBy = getParamValue(SORT_BY_PARAM) ?? undefined;
|
|
46
|
+
|
|
47
|
+
// SortBy from URL is taking precedence over the storage value
|
|
48
|
+
if (urlSortBy && validateSortBy(urlSortBy.toLowerCase())) {
|
|
49
|
+
return urlSortBy.toLowerCase() as SortBy;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (typeof window === "undefined") {
|
|
53
|
+
return DEFAULT_SORT_BY;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const storageSortBy = localStorage.getItem(SORT_BY_STORAGE_KEY);
|
|
57
|
+
|
|
58
|
+
if (storageSortBy && validateSortBy(storageSortBy.toLowerCase())) {
|
|
59
|
+
return storageSortBy.toLowerCase() as SortBy;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return DEFAULT_SORT_BY;
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
effect(() => {
|
|
66
|
+
if (typeof window === "undefined") {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
localStorage.setItem(SORT_BY_STORAGE_KEY, sortBy.value);
|
|
71
|
+
});
|
package/src/stores/variables.ts
CHANGED
|
@@ -12,7 +12,7 @@ export const variables = signal<StoreSignalState<Record<string, Variables>>>({
|
|
|
12
12
|
|
|
13
13
|
export const fetchVariables = async (env: string = "default") => {
|
|
14
14
|
variables.value = {
|
|
15
|
-
...variables.
|
|
15
|
+
...variables.peek(),
|
|
16
16
|
loading: true,
|
|
17
17
|
error: undefined,
|
|
18
18
|
};
|
|
@@ -24,7 +24,7 @@ export const fetchVariables = async (env: string = "default") => {
|
|
|
24
24
|
|
|
25
25
|
variables.value = {
|
|
26
26
|
data: {
|
|
27
|
-
...variables.
|
|
27
|
+
...variables.peek().data,
|
|
28
28
|
[env]: res,
|
|
29
29
|
},
|
|
30
30
|
error: undefined,
|
|
@@ -32,7 +32,7 @@ export const fetchVariables = async (env: string = "default") => {
|
|
|
32
32
|
};
|
|
33
33
|
} catch (e) {
|
|
34
34
|
variables.value = {
|
|
35
|
-
...variables.
|
|
35
|
+
...variables.peek(),
|
|
36
36
|
error: e.message,
|
|
37
37
|
loading: false,
|
|
38
38
|
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parses owner string per RFC 2822 (Internet Message Format).
|
|
3
|
+
* Supports: "Display Name <email@example.com>", "Display Name <https://url>",
|
|
4
|
+
* plain "email@example.com", plain "https://url", or plain text.
|
|
5
|
+
*/
|
|
6
|
+
const RFC2822_ADDRESS = /^([^<>]+)\s+<\s*(\S*)\s*>$/;
|
|
7
|
+
|
|
8
|
+
type UrlOwnerAddress = {
|
|
9
|
+
displayName: string;
|
|
10
|
+
url: string;
|
|
11
|
+
type: "url";
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type EmailOwnerAddress = {
|
|
15
|
+
displayName: string;
|
|
16
|
+
email: string;
|
|
17
|
+
type: "email";
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type OwnerAddress =
|
|
21
|
+
| UrlOwnerAddress
|
|
22
|
+
| EmailOwnerAddress
|
|
23
|
+
| {
|
|
24
|
+
displayName: string;
|
|
25
|
+
type: "none";
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const isValidUrl = (value: string): boolean => {
|
|
29
|
+
try {
|
|
30
|
+
const u = new URL(value);
|
|
31
|
+
return u.protocol === "http:" || u.protocol === "https:";
|
|
32
|
+
} catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Simple email validation: local@domain.tld, domain part must not start with dot.
|
|
39
|
+
* Rejects invalid forms like "user@.example.com".
|
|
40
|
+
*/
|
|
41
|
+
const isValidEmail = (value: string): boolean => {
|
|
42
|
+
const simpleEmail = /^[^\s@]+@[^\s.@][^\s@]*\.[^\s@]+$/;
|
|
43
|
+
return simpleEmail.test(value);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Quick check to avoid regex on plain text (no @, :, or <).
|
|
48
|
+
*/
|
|
49
|
+
const isLikelyAddress = (input: string): boolean => {
|
|
50
|
+
return input.includes("@") || input.includes(":") || input.includes("<");
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Parses a single owner value into display name and optional link (mailto or URL).
|
|
55
|
+
* Returns null for null/empty input.
|
|
56
|
+
*/
|
|
57
|
+
export const parseOwnerAddress = (maybeAddress: string): OwnerAddress => {
|
|
58
|
+
if (!maybeAddress || maybeAddress.trim() === "") {
|
|
59
|
+
return { displayName: "", type: "none" };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!isLikelyAddress(maybeAddress)) {
|
|
63
|
+
return { displayName: maybeAddress, type: "none" };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let displayName = maybeAddress;
|
|
67
|
+
let urlOrEmail = maybeAddress;
|
|
68
|
+
|
|
69
|
+
const match = maybeAddress.match(RFC2822_ADDRESS);
|
|
70
|
+
if (match) {
|
|
71
|
+
displayName = match[1].trim();
|
|
72
|
+
urlOrEmail = match[2];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// e.g.: John Doe <>
|
|
76
|
+
if (urlOrEmail === "") {
|
|
77
|
+
return { displayName, type: "none" };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// e.g.: John Doe <https://example.com> or plain https://...
|
|
81
|
+
if (isValidUrl(urlOrEmail)) {
|
|
82
|
+
return { displayName, url: urlOrEmail, type: "url" };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// e.g.: John Doe <mail@example.com> or plain mail@...
|
|
86
|
+
if (isValidEmail(urlOrEmail)) {
|
|
87
|
+
return { displayName, email: urlOrEmail, type: "email" };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Non-compliant: treat as plain text
|
|
91
|
+
return { displayName: maybeAddress, type: "none" };
|
|
92
|
+
};
|
package/src/utils/time.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getLocaleDateTimeOverride } from "@allurereport/web-commons";
|
|
2
|
+
import { currentLocale, currentLocaleIso, useI18n } from "@/stores/locale";
|
|
2
3
|
|
|
3
4
|
const defaultOptions: Intl.DateTimeFormatOptions = {
|
|
4
5
|
month: "numeric",
|
|
@@ -12,7 +13,20 @@ const defaultOptions: Intl.DateTimeFormatOptions = {
|
|
|
12
13
|
|
|
13
14
|
export const timestampToDate = (timestamp: number, options = defaultOptions) => {
|
|
14
15
|
const date = new Date(timestamp);
|
|
16
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
15
17
|
const { t } = useI18n("ui");
|
|
16
18
|
|
|
17
|
-
|
|
19
|
+
const kind = options.second ? "dateTime" : options.hour || options.minute ? "dateTimeNoSeconds" : "date";
|
|
20
|
+
const override = getLocaleDateTimeOverride(currentLocale.value, kind);
|
|
21
|
+
const formatter = new Intl.DateTimeFormat(override?.locale ?? (currentLocaleIso.value as string), {
|
|
22
|
+
...options,
|
|
23
|
+
...(override?.options ?? {}),
|
|
24
|
+
});
|
|
25
|
+
const formatted = formatter.format(date);
|
|
26
|
+
|
|
27
|
+
if (override?.includeAtSeparator === false || override?.stripComma) {
|
|
28
|
+
return formatted.replace(",", "");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return formatted.replace(",", ` ${t("at")}`);
|
|
18
32
|
};
|
package/src/utils/treeFilters.ts
CHANGED
|
@@ -11,103 +11,79 @@ import {
|
|
|
11
11
|
ordinal,
|
|
12
12
|
reverse,
|
|
13
13
|
} from "@allurereport/core-api";
|
|
14
|
-
import type {
|
|
14
|
+
import type { SortBy } from "@/stores/treeSort";
|
|
15
15
|
import type { AwesomeRecursiveTree, AwesomeTree, AwesomeTreeGroup, AwesomeTreeLeaf } from "../../types";
|
|
16
16
|
|
|
17
|
-
const
|
|
18
|
-
return name.toLocaleLowerCase().includes(query.toLocaleLowerCase());
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
const matchesNodeId = (nodeId: string, query: string) => {
|
|
22
|
-
return nodeId.toLowerCase() === query.toLocaleLowerCase();
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
export const isIncluded = (leaf: TreeLeaf<AwesomeTreeLeaf>, filterOptions: TreeFiltersState) => {
|
|
26
|
-
const queryMatched =
|
|
27
|
-
!filterOptions?.query ||
|
|
28
|
-
matchesName(leaf.name, filterOptions.query) ||
|
|
29
|
-
matchesNodeId(leaf.nodeId, filterOptions.query);
|
|
30
|
-
|
|
31
|
-
const statusMatched =
|
|
32
|
-
!filterOptions?.status || filterOptions?.status === "total" || leaf.status === filterOptions.status;
|
|
33
|
-
const flakyMatched = !filterOptions?.filter?.flaky || leaf.flaky;
|
|
34
|
-
const retryMatched = !filterOptions?.filter?.retry || leaf.retry;
|
|
35
|
-
const newMatched = !filterOptions?.filter?.new || leaf.transition === "new";
|
|
36
|
-
const fixedMatched = !filterOptions?.filter?.fixed || leaf.transition === "fixed";
|
|
37
|
-
const regressedMatched = !filterOptions?.filter?.regressed || leaf.transition === "regressed";
|
|
38
|
-
const malfunctionedMatched = !filterOptions?.filter?.malfunctioned || leaf.transition === "malfunctioned";
|
|
39
|
-
|
|
40
|
-
return [
|
|
41
|
-
queryMatched,
|
|
42
|
-
statusMatched,
|
|
43
|
-
flakyMatched,
|
|
44
|
-
retryMatched,
|
|
45
|
-
newMatched,
|
|
46
|
-
fixedMatched,
|
|
47
|
-
regressedMatched,
|
|
48
|
-
malfunctionedMatched,
|
|
49
|
-
].every(Boolean);
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
const leafComparatorByTreeSortBy = (sortBy: TreeSortBy = "status"): Comparator<TreeLeaf<AwesomeTreeLeaf>> => {
|
|
17
|
+
const leafComparatorByTreeSortBy = (sortBy: SortBy = "status,asc"): Comparator<TreeLeaf<AwesomeTreeLeaf>> => {
|
|
53
18
|
const typedCompareBy = compareBy<TreeLeaf<AwesomeTreeLeaf>>;
|
|
54
19
|
switch (sortBy) {
|
|
55
|
-
case "order":
|
|
20
|
+
case "order,asc":
|
|
21
|
+
case "order,desc":
|
|
56
22
|
return typedCompareBy("groupOrder", ordinal());
|
|
57
|
-
case "duration":
|
|
23
|
+
case "duration,asc":
|
|
24
|
+
case "duration,desc":
|
|
58
25
|
return typedCompareBy("duration", ordinal());
|
|
59
|
-
case "
|
|
26
|
+
case "name,asc":
|
|
27
|
+
case "name,desc":
|
|
60
28
|
return typedCompareBy("name", alphabetically());
|
|
61
|
-
case "status":
|
|
29
|
+
case "status,asc":
|
|
30
|
+
case "status,desc":
|
|
62
31
|
return typedCompareBy("status", byStatus());
|
|
63
32
|
default:
|
|
64
33
|
// eslint-disable-next-line no-console
|
|
65
|
-
console.
|
|
34
|
+
console.warn(`unsupported comparator ${sortBy as string}`);
|
|
66
35
|
return () => 0;
|
|
67
36
|
}
|
|
68
37
|
};
|
|
69
38
|
|
|
70
|
-
const groupComparatorByTreeSortBy = (sortBy:
|
|
39
|
+
const groupComparatorByTreeSortBy = (sortBy: SortBy = "status,asc"): Comparator<DefaultTreeGroup> => {
|
|
71
40
|
const typedCompareBy = compareBy<DefaultTreeGroup>;
|
|
72
41
|
switch (sortBy) {
|
|
73
|
-
case "
|
|
42
|
+
case "name,desc":
|
|
43
|
+
case "name,asc":
|
|
74
44
|
return typedCompareBy("name", alphabetically());
|
|
75
|
-
case "order":
|
|
76
|
-
case "
|
|
77
|
-
case "
|
|
45
|
+
case "order,desc":
|
|
46
|
+
case "order,asc":
|
|
47
|
+
case "duration,desc":
|
|
48
|
+
case "duration,asc":
|
|
49
|
+
case "status,desc":
|
|
50
|
+
case "status,asc":
|
|
78
51
|
return typedCompareBy("statistic", byStatistic());
|
|
79
52
|
default:
|
|
80
53
|
// eslint-disable-next-line no-console
|
|
81
|
-
console.
|
|
54
|
+
console.warn(`unsupported comparator ${sortBy as string}`);
|
|
82
55
|
return () => 0;
|
|
83
56
|
}
|
|
84
57
|
};
|
|
85
58
|
|
|
86
|
-
export const leafComparator = (
|
|
87
|
-
const cmp = leafComparatorByTreeSortBy(
|
|
88
|
-
const directional =
|
|
59
|
+
export const leafComparator = (sortBy: SortBy = "status,asc"): Comparator<TreeLeaf<AwesomeTreeLeaf>> => {
|
|
60
|
+
const cmp = leafComparatorByTreeSortBy(sortBy);
|
|
61
|
+
const directional = sortBy.split(",")[1] === "asc" ? cmp : reverse(cmp);
|
|
89
62
|
// apply fallback sorting by name
|
|
90
63
|
return andThen([directional, compareBy("name", alphabetically())]);
|
|
91
64
|
};
|
|
92
65
|
|
|
93
|
-
export const groupComparator = (
|
|
94
|
-
const cmp = groupComparatorByTreeSortBy(
|
|
95
|
-
const directional =
|
|
66
|
+
export const groupComparator = (sortBy: SortBy = "status,asc"): Comparator<DefaultTreeGroup> => {
|
|
67
|
+
const cmp = groupComparatorByTreeSortBy(sortBy);
|
|
68
|
+
const directional = sortBy.split(",")[1] === "asc" ? cmp : reverse(cmp);
|
|
96
69
|
// apply fallback sorting by name
|
|
97
70
|
return andThen([directional, compareBy("name", alphabetically())]);
|
|
98
71
|
};
|
|
99
72
|
|
|
100
73
|
export const filterLeaves = (
|
|
101
|
-
|
|
74
|
+
leafIds: string[] = [],
|
|
102
75
|
leavesById: AwesomeTree["leavesById"],
|
|
103
|
-
|
|
76
|
+
filterPredicate: (item: AwesomeTreeLeaf) => boolean,
|
|
77
|
+
sortBy: SortBy = "status,asc",
|
|
104
78
|
) => {
|
|
105
|
-
|
|
106
|
-
.map((leafId) => leavesById[leafId])
|
|
107
|
-
.filter((leaf: TreeLeaf<AwesomeTreeLeaf>) => isIncluded(leaf, filterOptions));
|
|
79
|
+
let leaves = [...leafIds].map((leafId) => leavesById[leafId]);
|
|
108
80
|
|
|
109
|
-
|
|
110
|
-
|
|
81
|
+
if (filterPredicate) {
|
|
82
|
+
leaves = leaves.filter(filterPredicate);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const comparator = leafComparator(sortBy);
|
|
86
|
+
return leaves.sort(comparator);
|
|
111
87
|
};
|
|
112
88
|
|
|
113
89
|
/**
|
|
@@ -119,12 +95,14 @@ export const createRecursiveTree = (payload: {
|
|
|
119
95
|
group: AwesomeTreeGroup;
|
|
120
96
|
groupsById: AwesomeTree["groupsById"];
|
|
121
97
|
leavesById: AwesomeTree["leavesById"];
|
|
122
|
-
|
|
98
|
+
filterPredicate: (item: AwesomeTreeLeaf) => boolean;
|
|
99
|
+
sortBy: SortBy;
|
|
123
100
|
}): AwesomeRecursiveTree => {
|
|
124
|
-
const { group, groupsById, leavesById,
|
|
101
|
+
const { group, groupsById, leavesById, filterPredicate, sortBy } = payload;
|
|
125
102
|
const groupLeaves: string[] = group.leaves ?? [];
|
|
126
103
|
|
|
127
|
-
const leaves = filterLeaves(groupLeaves, leavesById,
|
|
104
|
+
const leaves = filterLeaves(groupLeaves, leavesById, filterPredicate, sortBy);
|
|
105
|
+
|
|
128
106
|
const trees =
|
|
129
107
|
group.groups
|
|
130
108
|
?.map((groupId) =>
|
|
@@ -132,20 +110,24 @@ export const createRecursiveTree = (payload: {
|
|
|
132
110
|
group: groupsById[groupId],
|
|
133
111
|
groupsById,
|
|
134
112
|
leavesById,
|
|
135
|
-
|
|
113
|
+
filterPredicate,
|
|
114
|
+
sortBy,
|
|
136
115
|
}),
|
|
137
116
|
)
|
|
138
117
|
?.filter((rt) => !isRecursiveTreeEmpty(rt)) ?? [];
|
|
139
118
|
|
|
140
119
|
const statistic: Statistic = emptyStatistic();
|
|
120
|
+
|
|
141
121
|
trees.forEach((rt: AwesomeRecursiveTree) => {
|
|
142
122
|
if (rt.statistic) {
|
|
143
123
|
const additional: Statistic = rt.statistic;
|
|
124
|
+
|
|
144
125
|
mergeStatistic(statistic, additional);
|
|
145
126
|
}
|
|
146
127
|
});
|
|
147
128
|
leaves.forEach((leaf) => {
|
|
148
129
|
const status: TestStatus = leaf.status;
|
|
130
|
+
|
|
149
131
|
incrementStatistic(statistic, status);
|
|
150
132
|
});
|
|
151
133
|
|
|
@@ -153,7 +135,7 @@ export const createRecursiveTree = (payload: {
|
|
|
153
135
|
...group,
|
|
154
136
|
statistic,
|
|
155
137
|
leaves,
|
|
156
|
-
trees: trees.sort(groupComparator(
|
|
138
|
+
trees: trees.sort(groupComparator(sortBy)),
|
|
157
139
|
};
|
|
158
140
|
};
|
|
159
141
|
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as webCommons from "@allurereport/web-commons";
|
|
2
|
+
import { signal } from "@preact/signals";
|
|
2
3
|
import { cleanup, render, screen } from "@testing-library/preact";
|
|
3
|
-
import {
|
|
4
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
5
|
import { Header } from "@/components/Header";
|
|
5
6
|
import { CiInfo } from "@/components/Header/CiInfo";
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
7
|
+
import type * as routerModule from "@/stores/router";
|
|
8
|
+
import { testResultStore } from "@/stores/testResults";
|
|
8
9
|
|
|
9
10
|
const fixtures = {
|
|
10
11
|
ci: {
|
|
@@ -12,64 +13,60 @@ const fixtures = {
|
|
|
12
13
|
},
|
|
13
14
|
};
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}));
|
|
18
|
-
vi.mock("@/stores/router", async () => {
|
|
19
|
-
const { signal } = await import("@preact/signals");
|
|
16
|
+
// Create a controllable route signal
|
|
17
|
+
const mockRouteParams = signal<{ testResultId?: string; tab?: string }>({});
|
|
20
18
|
|
|
21
|
-
|
|
22
|
-
route: signal({}),
|
|
23
|
-
};
|
|
24
|
-
});
|
|
25
|
-
vi.mock("@/stores/sections", async () => {
|
|
26
|
-
const { signal } = await import("@preact/signals");
|
|
27
|
-
|
|
28
|
-
return {
|
|
29
|
-
availableSections: signal([]),
|
|
30
|
-
};
|
|
31
|
-
});
|
|
19
|
+
// Mock UI components to simplify tests
|
|
32
20
|
vi.mock("@/components/HeaderControls", () => ({
|
|
33
|
-
HeaderControls: () => <div data-testid="header-controls"
|
|
21
|
+
HeaderControls: () => <div data-testid="header-controls" />,
|
|
34
22
|
}));
|
|
23
|
+
|
|
35
24
|
vi.mock("@/components/SectionPicker", () => ({
|
|
36
|
-
SectionPicker: () => <div data-testid="section-picker"
|
|
25
|
+
SectionPicker: () => <div data-testid="section-picker" />,
|
|
37
26
|
}));
|
|
27
|
+
|
|
38
28
|
vi.mock("@/components/TestResult/TrHeader/TrBreadcrumbs", () => ({
|
|
39
|
-
TrBreadcrumbs: () => <div data-testid="breadcrumbs"
|
|
29
|
+
TrBreadcrumbs: () => <div data-testid="breadcrumbs" />,
|
|
40
30
|
}));
|
|
31
|
+
|
|
41
32
|
vi.mock("@/components/Header/CiInfo", () => ({
|
|
42
|
-
|
|
43
|
-
CiInfo: vi.fn().mockImplementation(() => <div data-testid="ci-info"></div>),
|
|
33
|
+
CiInfo: vi.fn().mockImplementation(() => <div data-testid="ci-info" />),
|
|
44
34
|
}));
|
|
45
35
|
|
|
36
|
+
// Mock router module with controllable testResultRoute
|
|
37
|
+
vi.mock("@/stores/router", async (importOriginal) => {
|
|
38
|
+
const actual = await importOriginal<typeof routerModule>();
|
|
39
|
+
const { computed: computedFn } = await import("@preact/signals");
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
...actual,
|
|
43
|
+
testResultRoute: computedFn(() => {
|
|
44
|
+
const params = mockRouteParams.value;
|
|
45
|
+
const hasTestResultId = params.testResultId !== undefined;
|
|
46
|
+
return {
|
|
47
|
+
matches: hasTestResultId,
|
|
48
|
+
params: params || {},
|
|
49
|
+
};
|
|
50
|
+
}),
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
|
|
46
54
|
beforeEach(() => {
|
|
47
55
|
vi.clearAllMocks();
|
|
48
56
|
cleanup();
|
|
49
|
-
|
|
50
|
-
|
|
57
|
+
mockRouteParams.value = {};
|
|
58
|
+
testResultStore.value = {
|
|
59
|
+
loading: false,
|
|
60
|
+
error: undefined,
|
|
61
|
+
data: undefined,
|
|
62
|
+
};
|
|
63
|
+
vi.spyOn(webCommons, "getReportOptions").mockReturnValue({});
|
|
51
64
|
});
|
|
52
65
|
|
|
53
66
|
describe("components > Header", () => {
|
|
54
|
-
it("should render sections picker when there are sections available", () => {
|
|
55
|
-
availableSections.value = ["section1", "section2"];
|
|
56
|
-
|
|
57
|
-
render(<Header />);
|
|
58
|
-
|
|
59
|
-
expect(screen.getByTestId("section-picker")).toBeInTheDocument();
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it("shouldn't render sections picker when there are no sections available", () => {
|
|
63
|
-
render(<Header />);
|
|
64
|
-
|
|
65
|
-
expect(screen.queryByTestId("section-picker")).not.toBeInTheDocument();
|
|
66
|
-
});
|
|
67
|
-
|
|
68
67
|
it("should render ci info only when testResultId route parameter doesn't exists and ci report option is available", () => {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
};
|
|
72
|
-
(getReportOptions as Mock).mockReturnValue({
|
|
68
|
+
mockRouteParams.value = {};
|
|
69
|
+
vi.spyOn(webCommons, "getReportOptions").mockReturnValue({
|
|
73
70
|
ci: fixtures.ci,
|
|
74
71
|
});
|
|
75
72
|
|
|
@@ -80,12 +77,10 @@ describe("components > Header", () => {
|
|
|
80
77
|
});
|
|
81
78
|
|
|
82
79
|
it("shouldn't render ci info when testResultId route parameter exists", () => {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
testResultId: "1",
|
|
86
|
-
},
|
|
80
|
+
mockRouteParams.value = {
|
|
81
|
+
testResultId: "1",
|
|
87
82
|
};
|
|
88
|
-
(getReportOptions
|
|
83
|
+
vi.spyOn(webCommons, "getReportOptions").mockReturnValue({
|
|
89
84
|
ci: fixtures.ci,
|
|
90
85
|
});
|
|
91
86
|
|
|
@@ -96,12 +91,10 @@ describe("components > Header", () => {
|
|
|
96
91
|
});
|
|
97
92
|
|
|
98
93
|
it("should render breadcrumbs when testResultId route parameter exists", () => {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
testResultId: "1",
|
|
102
|
-
},
|
|
94
|
+
mockRouteParams.value = {
|
|
95
|
+
testResultId: "1",
|
|
103
96
|
};
|
|
104
|
-
(getReportOptions
|
|
97
|
+
vi.spyOn(webCommons, "getReportOptions").mockReturnValue({
|
|
105
98
|
ci: fixtures.ci,
|
|
106
99
|
});
|
|
107
100
|
|
|
@@ -111,9 +104,7 @@ describe("components > Header", () => {
|
|
|
111
104
|
});
|
|
112
105
|
|
|
113
106
|
it("shouldn't render breadcrumbs when testResultId route parameter doesn't exists", () => {
|
|
114
|
-
|
|
115
|
-
params: {},
|
|
116
|
-
};
|
|
107
|
+
mockRouteParams.value = {};
|
|
117
108
|
|
|
118
109
|
render(<Header />);
|
|
119
110
|
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { parseOwnerAddress } from "../../src/utils/ownerAddress.js";
|
|
3
|
+
|
|
4
|
+
describe("utils > ownerAddress", () => {
|
|
5
|
+
describe("parseOwnerAddress", () => {
|
|
6
|
+
it("returns none type with empty displayName for null input", () => {
|
|
7
|
+
const result = parseOwnerAddress(null as unknown as string);
|
|
8
|
+
expect(result).toEqual({ displayName: "", type: "none" });
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("returns none type with empty displayName for empty input", () => {
|
|
12
|
+
const result = parseOwnerAddress("");
|
|
13
|
+
expect(result).toEqual({ displayName: "", type: "none" });
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("returns none type for whitespace-only input", () => {
|
|
17
|
+
const result = parseOwnerAddress(" ");
|
|
18
|
+
expect(result).toEqual({ displayName: "", type: "none" });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("parses RFC 2822 string with email", () => {
|
|
22
|
+
const input = "John Doe < john.doe@example.com >";
|
|
23
|
+
const result = parseOwnerAddress(input);
|
|
24
|
+
expect(result.displayName).toBe("John Doe");
|
|
25
|
+
expect(result.type).toBe("email");
|
|
26
|
+
expect("email" in result && result.email).toBe("john.doe@example.com");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("parses RFC 2822 string with URL", () => {
|
|
30
|
+
const input = "John Doe <https://github.com/@john.doe>";
|
|
31
|
+
const result = parseOwnerAddress(input);
|
|
32
|
+
expect(result.displayName).toBe("John Doe");
|
|
33
|
+
expect(result.type).toBe("url");
|
|
34
|
+
expect("url" in result && result.url).toBe("https://github.com/@john.doe");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("returns only displayName (type none) for empty RFC 2822 address", () => {
|
|
38
|
+
const result = parseOwnerAddress("John Doe <>");
|
|
39
|
+
expect(result.displayName).toBe("John Doe");
|
|
40
|
+
expect(result.type).toBe("none");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("returns displayName for plain text input", () => {
|
|
44
|
+
const result = parseOwnerAddress("John Doe");
|
|
45
|
+
expect(result.displayName).toBe("John Doe");
|
|
46
|
+
expect(result.type).toBe("none");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("returns email type and mailto for plain email", () => {
|
|
50
|
+
const email = "john.doe@example.com";
|
|
51
|
+
const result = parseOwnerAddress(email);
|
|
52
|
+
expect(result.displayName).toBe(email);
|
|
53
|
+
expect(result.type).toBe("email");
|
|
54
|
+
expect("email" in result && result.email).toBe(email);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("returns url type for plain valid URL", () => {
|
|
58
|
+
const url = "https://github.com/john.doe";
|
|
59
|
+
const result = parseOwnerAddress(url);
|
|
60
|
+
expect(result.displayName).toBe(url);
|
|
61
|
+
expect(result.type).toBe("url");
|
|
62
|
+
expect("url" in result && result.url).toBe(url);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("returns only displayName (type none) for invalid URL", () => {
|
|
66
|
+
const result = parseOwnerAddress("htp:/www.example.com/page");
|
|
67
|
+
expect(result.displayName).toBe("htp:/www.example.com/page");
|
|
68
|
+
expect(result.type).toBe("none");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("returns only displayName (type none) for invalid email", () => {
|
|
72
|
+
const result = parseOwnerAddress("user@.example.com");
|
|
73
|
+
expect(result.displayName).toBe("user@.example.com");
|
|
74
|
+
expect(result.type).toBe("none");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("returns invalid RFC 2822 address unchanged as plain text", () => {
|
|
78
|
+
const result = parseOwnerAddress("John Doe <john@@doe>");
|
|
79
|
+
expect(result.displayName).toBe("John Doe <john@@doe>");
|
|
80
|
+
expect(result.type).toBe("none");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("accepts http URL", () => {
|
|
84
|
+
const result = parseOwnerAddress("http://example.com");
|
|
85
|
+
expect(result.type).toBe("url");
|
|
86
|
+
expect("url" in result && result.url).toBe("http://example.com");
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
});
|