@allurereport/web-awesome 3.9.0 → 3.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/dist/multi/173.app-b18cce138691927e8759.js +1 -0
  2. package/dist/multi/174.app-b18cce138691927e8759.js +1 -0
  3. package/dist/multi/252.app-b18cce138691927e8759.js +1 -0
  4. package/dist/multi/282.app-b18cce138691927e8759.js +1 -0
  5. package/dist/multi/29.app-b18cce138691927e8759.js +1 -0
  6. package/dist/multi/310.app-b18cce138691927e8759.js +1 -0
  7. package/dist/multi/416.app-b18cce138691927e8759.js +1 -0
  8. package/dist/multi/507.app-b18cce138691927e8759.js +1 -0
  9. package/dist/multi/527.app-b18cce138691927e8759.js +1 -0
  10. package/dist/multi/600.app-b18cce138691927e8759.js +1 -0
  11. package/dist/multi/605.app-b18cce138691927e8759.js +1 -0
  12. package/dist/multi/638.app-b18cce138691927e8759.js +1 -0
  13. package/dist/multi/672.app-b18cce138691927e8759.js +1 -0
  14. package/dist/multi/686.app-b18cce138691927e8759.js +1 -0
  15. package/dist/multi/725.app-b18cce138691927e8759.js +1 -0
  16. package/dist/multi/741.app-b18cce138691927e8759.js +1 -0
  17. package/dist/multi/749.app-b18cce138691927e8759.js +1 -0
  18. package/dist/multi/755.app-b18cce138691927e8759.js +1 -0
  19. package/dist/multi/894.app-b18cce138691927e8759.js +1 -0
  20. package/dist/multi/943.app-b18cce138691927e8759.js +1 -0
  21. package/dist/multi/980.app-b18cce138691927e8759.js +1 -0
  22. package/dist/multi/app-b18cce138691927e8759.js +2 -0
  23. package/dist/multi/manifest.json +25 -25
  24. package/dist/multi/{styles-468416ffee9a9dea6cae.css → styles-a4f65de86208f79dd2be.css} +8 -8
  25. package/dist/single/app-733f473da7b51f98876d.js +2 -0
  26. package/dist/single/manifest.json +1 -1
  27. package/package.json +14 -14
  28. package/src/components/Footer/FooterVersion.tsx +5 -10
  29. package/src/components/Footer/index.tsx +7 -1
  30. package/src/components/Footer/styles.scss +6 -0
  31. package/src/components/Header/CiInfo/index.tsx +17 -13
  32. package/src/components/HeaderControls/index.tsx +1 -3
  33. package/src/components/KeyboardShortcuts/styles.scss +5 -5
  34. package/src/components/MainReport/styles.scss +0 -21
  35. package/src/components/Metadata/index.tsx +27 -6
  36. package/src/components/Metadata/styles.scss +12 -0
  37. package/src/components/ReportBody/index.tsx +2 -11
  38. package/src/components/ReportBody/styles.scss +0 -21
  39. package/src/components/ReportHeader/index.tsx +25 -13
  40. package/src/components/ReportMetadata/index.tsx +35 -4
  41. package/src/components/SplitLayout/index.tsx +1 -1
  42. package/src/components/SplitLayout/styles.scss +4 -1
  43. package/src/components/TestResult/TrRetriesView/TrRetriesItem.tsx +27 -1
  44. package/src/components/TestResult/TrRetriesView/styles.scss +17 -7
  45. package/src/components/TestResult/TrSetup/index.tsx +1 -1
  46. package/src/components/TestResult/TrSteps/TrBodyItems.tsx +5 -2
  47. package/src/components/TestResult/TrSteps/TrStep.tsx +6 -2
  48. package/src/components/TestResult/TrSteps/index.tsx +2 -3
  49. package/src/components/TestResult/TrTeardown/index.tsx +1 -1
  50. package/src/components/Tree/index.tsx +26 -1
  51. package/src/locales/ar.json +1 -0
  52. package/src/locales/az.json +1 -0
  53. package/src/locales/de.json +1 -0
  54. package/src/locales/en.json +1 -0
  55. package/src/locales/es.json +1 -0
  56. package/src/locales/fr.json +1 -0
  57. package/src/locales/he.json +1 -0
  58. package/src/locales/hy.json +1 -0
  59. package/src/locales/it.json +1 -0
  60. package/src/locales/ja.json +1 -0
  61. package/src/locales/ka.json +1 -0
  62. package/src/locales/kr.json +1 -0
  63. package/src/locales/nl.json +1 -0
  64. package/src/locales/pl.json +1 -0
  65. package/src/locales/pt.json +1 -0
  66. package/src/locales/ru.json +1 -0
  67. package/src/locales/sv.json +1 -0
  68. package/src/locales/tr.json +1 -0
  69. package/src/locales/uk.json +1 -0
  70. package/src/locales/zh-TW.json +1 -0
  71. package/src/locales/zh.json +1 -0
  72. package/src/stores/locale.ts +4 -2
  73. package/src/stores/treeSort.ts +7 -1
  74. package/src/utils/atSeparator.ts +4 -0
  75. package/src/utils/time.ts +2 -1
  76. package/src/utils/treeFilters.ts +15 -4
  77. package/test/components/Footer.test.tsx +26 -0
  78. package/test/components/Header/CiInfo.test.tsx +48 -0
  79. package/test/components/HeaderControls.test.tsx +28 -0
  80. package/test/components/ReportHeader.test.tsx +77 -0
  81. package/test/components/ReportMetadata.test.tsx +131 -0
  82. package/test/components/TestResult/TrRetriesItem.test.tsx +163 -0
  83. package/test/components/TestResult/TrSteps.test.tsx +45 -10
  84. package/test/stores/treeSort.test.ts +58 -0
  85. package/test/utils/time.test.ts +52 -0
  86. package/test/utils/treeFilters.test.ts +104 -0
  87. package/types.d.ts +22 -0
  88. package/webpack.config.js +9 -7
  89. package/dist/multi/173.app-d36b0855e3e7a53eeee9.js +0 -1
  90. package/dist/multi/174.app-d36b0855e3e7a53eeee9.js +0 -1
  91. package/dist/multi/252.app-d36b0855e3e7a53eeee9.js +0 -1
  92. package/dist/multi/282.app-d36b0855e3e7a53eeee9.js +0 -1
  93. package/dist/multi/29.app-d36b0855e3e7a53eeee9.js +0 -1
  94. package/dist/multi/310.app-d36b0855e3e7a53eeee9.js +0 -1
  95. package/dist/multi/416.app-d36b0855e3e7a53eeee9.js +0 -1
  96. package/dist/multi/507.app-d36b0855e3e7a53eeee9.js +0 -1
  97. package/dist/multi/527.app-d36b0855e3e7a53eeee9.js +0 -1
  98. package/dist/multi/600.app-d36b0855e3e7a53eeee9.js +0 -1
  99. package/dist/multi/605.app-d36b0855e3e7a53eeee9.js +0 -1
  100. package/dist/multi/638.app-d36b0855e3e7a53eeee9.js +0 -1
  101. package/dist/multi/672.app-d36b0855e3e7a53eeee9.js +0 -1
  102. package/dist/multi/686.app-d36b0855e3e7a53eeee9.js +0 -1
  103. package/dist/multi/725.app-d36b0855e3e7a53eeee9.js +0 -1
  104. package/dist/multi/741.app-d36b0855e3e7a53eeee9.js +0 -1
  105. package/dist/multi/749.app-d36b0855e3e7a53eeee9.js +0 -1
  106. package/dist/multi/755.app-d36b0855e3e7a53eeee9.js +0 -1
  107. package/dist/multi/894.app-d36b0855e3e7a53eeee9.js +0 -1
  108. package/dist/multi/943.app-d36b0855e3e7a53eeee9.js +0 -1
  109. package/dist/multi/980.app-d36b0855e3e7a53eeee9.js +0 -1
  110. package/dist/multi/app-d36b0855e3e7a53eeee9.js +0 -2
  111. package/dist/single/app-62171f5f51b5954a787c.js +0 -2
  112. /package/dist/multi/{121.app-d36b0855e3e7a53eeee9.js → 121.app-b18cce138691927e8759.js} +0 -0
  113. /package/dist/multi/{779.app-d36b0855e3e7a53eeee9.js → 779.app-b18cce138691927e8759.js} +0 -0
  114. /package/dist/multi/{app-d36b0855e3e7a53eeee9.js.LICENSE.txt → app-b18cce138691927e8759.js.LICENSE.txt} +0 -0
  115. /package/dist/single/{app-62171f5f51b5954a787c.js.LICENSE.txt → app-733f473da7b51f98876d.js.LICENSE.txt} +0 -0
@@ -0,0 +1,28 @@
1
+ import { render, screen } from "@testing-library/preact";
2
+ import { describe, expect, it, vi } from "vitest";
3
+
4
+ import { HeaderControls } from "@/components/HeaderControls";
5
+
6
+ vi.mock("@/components/EnvironmentPicker", () => ({
7
+ EnvironmentPicker: () => <div data-testid="environment-picker" />,
8
+ }));
9
+
10
+ vi.mock("@/components/ToggleLayout", () => ({
11
+ default: () => <div data-testid="toggle-layout" />,
12
+ }));
13
+
14
+ vi.mock("@allurereport/web-components", () => ({
15
+ ThemeButton: () => <button data-testid="theme-button" type="button" />,
16
+ LanguagePicker: () => <button data-testid="language-picker" type="button" />,
17
+ }));
18
+
19
+ describe("components > HeaderControls", () => {
20
+ it("should keep frequent controls in header and omit language picker", () => {
21
+ render(<HeaderControls />);
22
+
23
+ expect(screen.getByTestId("environment-picker")).toBeInTheDocument();
24
+ expect(screen.getByTestId("toggle-layout")).toBeInTheDocument();
25
+ expect(screen.getByTestId("theme-button")).toBeInTheDocument();
26
+ expect(screen.queryByTestId("language-picker")).not.toBeInTheDocument();
27
+ });
28
+ });
@@ -0,0 +1,77 @@
1
+ import { getReportOptions } from "@allurereport/web-commons";
2
+ import { render, screen } from "@testing-library/preact";
3
+ import { type Mock, beforeEach, describe, expect, it, vi } from "vitest";
4
+
5
+ import { ReportHeader } from "@/components/ReportHeader";
6
+
7
+ vi.mock("@allurereport/web-commons", async (importOriginal) => ({
8
+ ...(await importOriginal()),
9
+ getReportOptions: vi.fn(),
10
+ }));
11
+
12
+ vi.mock("@allurereport/web-components", () => ({
13
+ Heading: (props: { children: string }) => <h2>{props.children}</h2>,
14
+ Loadable: (props: { renderData: (data: unknown) => unknown }) => props.renderData({}),
15
+ Text: (props: { "children": unknown; "data-testid"?: string }) => (
16
+ <span data-testid={props["data-testid"]}>{props.children}</span>
17
+ ),
18
+ TooltipWrapper: (props: { children: unknown }) => props.children,
19
+ }));
20
+
21
+ vi.mock("@/components/ReportHeader/ReportHeaderLogo", () => ({
22
+ ReportHeaderLogo: () => <div data-testid="report-logo" />,
23
+ }));
24
+
25
+ vi.mock("@/components/ReportHeader/ReportHeaderPie", () => ({
26
+ ReportHeaderPie: () => <div data-testid="report-pie" />,
27
+ }));
28
+
29
+ vi.mock("@/components/TestResult/TrStatus", () => ({
30
+ TrStatus: () => <div data-testid="tr-status" />,
31
+ }));
32
+
33
+ vi.mock("@/stores", () => ({
34
+ useI18n: () => ({
35
+ t: (key: string, params: Record<string, unknown>) => `${key}: ${params.formattedCreatedAt}`,
36
+ }),
37
+ }));
38
+
39
+ vi.mock("@/utils/time", () => ({
40
+ timestampToDate: (value: number, options?: Intl.DateTimeFormatOptions) =>
41
+ `${options?.month === "long" ? "long" : "default"}:${value}`,
42
+ }));
43
+
44
+ beforeEach(() => {
45
+ vi.clearAllMocks();
46
+ });
47
+
48
+ describe("components > ReportHeader", () => {
49
+ it("should render launch start time and duration when run summary is available", () => {
50
+ (getReportOptions as Mock).mockReturnValue({
51
+ reportName: "Wrike report",
52
+ createdAt: 42,
53
+ runSummary: {
54
+ start: 1000,
55
+ stop: 2500,
56
+ duration: 1500,
57
+ },
58
+ });
59
+
60
+ render(<ReportHeader />);
61
+
62
+ expect(screen.getByTestId("report-data")).toHaveTextContent("long:1000");
63
+ expect(screen.getByTestId("report-data")).not.toHaveTextContent("long:2500");
64
+ expect(screen.getByTestId("report-data")).not.toHaveTextContent("long:42");
65
+ });
66
+
67
+ it("should fall back to generated time when run summary is missing", () => {
68
+ (getReportOptions as Mock).mockReturnValue({
69
+ reportName: "Wrike report",
70
+ createdAt: 10,
71
+ });
72
+
73
+ render(<ReportHeader />);
74
+
75
+ expect(screen.getByTestId("report-data")).toHaveTextContent("long:10");
76
+ });
77
+ });
@@ -0,0 +1,131 @@
1
+ import { getReportOptions } from "@allurereport/web-commons";
2
+ import { cleanup, render, screen } from "@testing-library/preact";
3
+ import { type Mock, beforeEach, describe, expect, it, vi } from "vitest";
4
+
5
+ import { ReportMetadata } from "@/components/ReportMetadata";
6
+
7
+ vi.mock("@allurereport/web-commons", async (importOriginal) => ({
8
+ ...(await importOriginal()),
9
+ getReportOptions: vi.fn(),
10
+ }));
11
+
12
+ vi.mock("@allurereport/web-components", () => {
13
+ const Menu = Object.assign(
14
+ (props: { children: unknown; menuTrigger: (props: { onClick: () => void }) => unknown }) => (
15
+ <>
16
+ {props.menuTrigger({ onClick: vi.fn() })}
17
+ {props.children}
18
+ </>
19
+ ),
20
+ {
21
+ Section: (props: { children: unknown }) => <>{props.children}</>,
22
+ },
23
+ );
24
+
25
+ return {
26
+ ArrowButton: () => <span />,
27
+ Button: (props: { text?: string; onClick?: () => void }) => <button onClick={props.onClick}>{props.text}</button>,
28
+ ButtonLink: (props: { href: string; text?: string }) => <a href={props.href}>{props.text}</a>,
29
+ Counter: (props: { count: number }) => <span>{props.count}</span>,
30
+ Loadable: (props: {
31
+ source: { value?: { data?: unknown } };
32
+ transformData?: (data: unknown) => unknown;
33
+ renderData: (data: unknown) => unknown;
34
+ }) =>
35
+ props.renderData(props.transformData ? props.transformData(props.source.value?.data) : props.source.value?.data),
36
+ Menu,
37
+ SvgIcon: () => <span />,
38
+ Text: (props: { children: unknown }) => <span>{props.children}</span>,
39
+ TooltipWrapper: (props: { children: unknown }) => props.children,
40
+ allureIcons: {
41
+ lineGeneralCopy3: "copy",
42
+ lineGeneralLinkExternal: "external",
43
+ },
44
+ useElementTruncation: () => ({ ref: { current: null }, isTruncated: false }),
45
+ };
46
+ });
47
+
48
+ vi.mock("@/stores", () => ({
49
+ reportStatsStore: { value: { data: undefined } },
50
+ statsByEnvStore: { value: { data: {} } },
51
+ useI18n: () => ({
52
+ t: (key: string) => key,
53
+ }),
54
+ }));
55
+
56
+ vi.mock("@/stores/env", () => ({
57
+ currentEnvironment: { value: undefined },
58
+ }));
59
+
60
+ vi.mock("@/stores/envInfo", () => ({
61
+ envInfoStore: { value: { data: [] } },
62
+ }));
63
+
64
+ vi.mock("@/stores/tree", () => ({
65
+ collapsedTrees: { value: new Set() },
66
+ toggleTree: vi.fn(),
67
+ }));
68
+
69
+ vi.mock("@/stores/variables", () => ({
70
+ fetchVariables: vi.fn(),
71
+ variables: { value: { data: { default: {} } } },
72
+ }));
73
+
74
+ beforeEach(() => {
75
+ vi.clearAllMocks();
76
+ cleanup();
77
+ });
78
+
79
+ describe("components > ReportMetadata", () => {
80
+ it("should render executor metadata as a clickable build link", () => {
81
+ (getReportOptions as Mock).mockReturnValue({
82
+ executor: {
83
+ name: "TeamCity",
84
+ buildName: "Wrike #123",
85
+ url: "https://teamcity.example",
86
+ reportUrl: "https://teamcity.example/report/123",
87
+ buildUrl: "https://teamcity.example/build/123",
88
+ },
89
+ });
90
+
91
+ render(<ReportMetadata />);
92
+
93
+ expect(screen.getByText("executor")).toBeInTheDocument();
94
+ expect(screen.getByRole("link", { name: "TeamCity · Wrike #123" })).toHaveAttribute(
95
+ "href",
96
+ "https://teamcity.example/build/123",
97
+ );
98
+ });
99
+
100
+ it("should fall back to report url when executor build url is missing", () => {
101
+ (getReportOptions as Mock).mockReturnValue({
102
+ executor: {
103
+ name: "TeamCity",
104
+ buildName: "Wrike #123",
105
+ reportUrl: "https://teamcity.example/report/123",
106
+ },
107
+ });
108
+
109
+ render(<ReportMetadata />);
110
+
111
+ expect(screen.getByRole("link", { name: "TeamCity · Wrike #123" })).toHaveAttribute(
112
+ "href",
113
+ "https://teamcity.example/report/123",
114
+ );
115
+ });
116
+
117
+ it("should render executor metadata as plain text when url is unsafe", () => {
118
+ (getReportOptions as Mock).mockReturnValue({
119
+ executor: {
120
+ name: "TeamCity",
121
+ buildName: "Wrike #123",
122
+ buildUrl: "javascript:alert(1)",
123
+ },
124
+ });
125
+
126
+ render(<ReportMetadata />);
127
+
128
+ expect(screen.queryByRole("link", { name: "TeamCity · Wrike #123" })).not.toBeInTheDocument();
129
+ expect(screen.getByTestId("metadata-item")).toHaveTextContent("TeamCity · Wrike #123");
130
+ });
131
+ });
@@ -0,0 +1,163 @@
1
+ import { fireEvent, render, screen } from "@testing-library/preact";
2
+ import { beforeEach, describe, expect, it, vi } from "vitest";
3
+
4
+ import { TrRetriesItem } from "@/components/TestResult/TrRetriesView/TrRetriesItem";
5
+
6
+ import type { AwesomeTestResult } from "../../../types";
7
+
8
+ vi.mock("@allurereport/web-components", () => ({
9
+ ArrowButton: () => <button data-testid="test-result-retries-item-arrow-button" type="button" />,
10
+ IconButton: (props: { "onClick": () => void; "data-testid"?: string }) => (
11
+ <button data-testid={props["data-testid"]} type="button" onClick={props.onClick} />
12
+ ),
13
+ Text: (props: { "children": unknown; "data-testid"?: string }) => (
14
+ <span data-testid={props["data-testid"]}>{props.children}</span>
15
+ ),
16
+ TreeItemIcon: () => <span data-testid="retry-status" />,
17
+ allureIcons: {
18
+ lineArrowsChevronDown: "chevron",
19
+ lineGeneralLinkExternal: "external",
20
+ },
21
+ }));
22
+
23
+ vi.mock("@/components/TestResult/TrError", () => ({
24
+ TrError: (props: { message?: string; trace?: string; actual?: string; expected?: string }) => (
25
+ <div data-testid="test-result-error">{props.message || props.trace || `${props.actual} ${props.expected}`}</div>
26
+ ),
27
+ }));
28
+
29
+ vi.mock("@/stores/locale", () => ({
30
+ useI18n: (namespace: string) =>
31
+ namespace === "controls"
32
+ ? {
33
+ t: (key: string) => (key === "comparison" ? "Comparison" : key),
34
+ }
35
+ : {
36
+ t: (_key: string, params: { attempt: number; total: number }) =>
37
+ `Attempt ${params.attempt} of ${params.total}`,
38
+ },
39
+ }));
40
+
41
+ vi.mock("@/stores/router", () => ({
42
+ navigateToTestResult: vi.fn(),
43
+ }));
44
+
45
+ vi.mock("@/utils/time", () => ({
46
+ timestampToDate: (value: number) => `date:${value}`,
47
+ }));
48
+
49
+ const makeRetry = (overrides: Partial<AwesomeTestResult> = {}): AwesomeTestResult =>
50
+ ({
51
+ id: "retry-id",
52
+ name: "retry",
53
+ status: "failed",
54
+ fullName: "retry.fullName",
55
+ flaky: false,
56
+ muted: false,
57
+ known: false,
58
+ isRetry: true,
59
+ labels: [],
60
+ groupedLabels: {},
61
+ parameters: [],
62
+ links: [],
63
+ steps: [],
64
+ error: undefined,
65
+ environment: "default",
66
+ setup: [],
67
+ teardown: [],
68
+ history: [],
69
+ retries: [],
70
+ breadcrumbs: [],
71
+ titlePath: [],
72
+ attachments: [],
73
+ ...overrides,
74
+ }) as AwesomeTestResult;
75
+
76
+ beforeEach(() => {
77
+ vi.clearAllMocks();
78
+ });
79
+
80
+ describe("components > TestResult > TrRetriesItem", () => {
81
+ it("should render retry error message preview without expanding details", () => {
82
+ render(
83
+ <TrRetriesItem
84
+ attempt={1}
85
+ totalAttempts={2}
86
+ testResultItem={makeRetry({
87
+ stop: 1000,
88
+ duration: 500,
89
+ error: {
90
+ message: "Expected true to be false",
91
+ trace: "stack trace",
92
+ },
93
+ })}
94
+ />,
95
+ );
96
+
97
+ expect(screen.getByTestId("test-result-retries-item-error-preview")).toHaveTextContent("Expected true to be false");
98
+ expect(screen.queryByTestId("test-result-error")).not.toBeInTheDocument();
99
+
100
+ fireEvent.click(screen.getByTestId("test-result-retries-item-arrow-button"));
101
+
102
+ expect(screen.getByTestId("test-result-error")).toHaveTextContent("Expected true to be false");
103
+ });
104
+
105
+ it("should use first meaningful trace line when message is missing", () => {
106
+ render(
107
+ <TrRetriesItem
108
+ attempt={1}
109
+ totalAttempts={2}
110
+ testResultItem={makeRetry({
111
+ error: {
112
+ trace: "\n\nAssertionError: timeout\n at test.ts:1",
113
+ },
114
+ })}
115
+ />,
116
+ );
117
+
118
+ expect(screen.getByTestId("test-result-retries-item-error-preview")).toHaveTextContent("AssertionError: timeout");
119
+ });
120
+
121
+ it("should use trace preview when message is only whitespace", () => {
122
+ render(
123
+ <TrRetriesItem
124
+ attempt={1}
125
+ totalAttempts={2}
126
+ testResultItem={makeRetry({
127
+ error: {
128
+ message: " ",
129
+ trace: "\nAssertionError: retry failed",
130
+ },
131
+ })}
132
+ />,
133
+ );
134
+
135
+ expect(screen.getByTestId("test-result-retries-item-arrow-button")).toBeInTheDocument();
136
+ expect(screen.getByTestId("test-result-retries-item-error-preview")).toHaveTextContent(
137
+ "AssertionError: retry failed",
138
+ );
139
+ });
140
+
141
+ it("should render diff-only retry preview and keep details expandable", () => {
142
+ render(
143
+ <TrRetriesItem
144
+ attempt={1}
145
+ totalAttempts={2}
146
+ testResultItem={makeRetry({
147
+ error: {
148
+ actual: "actual value",
149
+ expected: "expected value",
150
+ },
151
+ })}
152
+ />,
153
+ );
154
+
155
+ expect(screen.getByTestId("test-result-retries-item-arrow-button")).toBeInTheDocument();
156
+ expect(screen.getByTestId("test-result-retries-item-error-preview")).toHaveTextContent("Comparison");
157
+ expect(screen.queryByTestId("test-result-error")).not.toBeInTheDocument();
158
+
159
+ fireEvent.click(screen.getByTestId("test-result-retries-item-arrow-button"));
160
+
161
+ expect(screen.getByTestId("test-result-error")).toHaveTextContent("actual value expected value");
162
+ });
163
+ });
@@ -19,9 +19,25 @@ vi.hoisted(() => {
19
19
 
20
20
  import type { TrBodyItem } from "@/components/TestResult/bodyItems";
21
21
  import { TrSteps } from "@/components/TestResult/TrSteps";
22
+ import { TrStep } from "@/components/TestResult/TrSteps/TrStep";
22
23
  import { collapsedTrees, expandedTrees } from "@/stores/tree";
23
24
 
24
- const passedStep = {
25
+ const nestedPassedStep = {
26
+ type: "step",
27
+ item: {
28
+ stepId: "nested-passed-step",
29
+ name: "nested passed step",
30
+ status: "passed",
31
+ parameters: [],
32
+ message: "",
33
+ trace: "",
34
+ hasSimilarErrorInSubSteps: false,
35
+ },
36
+ suppressInlineError: false,
37
+ bodyItems: [],
38
+ } satisfies TrBodyItem;
39
+
40
+ const passedStepWithContent = {
25
41
  type: "step",
26
42
  item: {
27
43
  stepId: "passed-step",
@@ -33,10 +49,10 @@ const passedStep = {
33
49
  hasSimilarErrorInSubSteps: false,
34
50
  },
35
51
  suppressInlineError: false,
36
- bodyItems: [],
52
+ bodyItems: [nestedPassedStep],
37
53
  } satisfies TrBodyItem;
38
54
 
39
- const failedStep = {
55
+ const failedStepWithContent = {
40
56
  type: "step",
41
57
  item: {
42
58
  stepId: "failed-step",
@@ -48,7 +64,7 @@ const failedStep = {
48
64
  hasSimilarErrorInSubSteps: false,
49
65
  },
50
66
  suppressInlineError: false,
51
- bodyItems: [],
67
+ bodyItems: [nestedPassedStep],
52
68
  } satisfies TrBodyItem;
53
69
 
54
70
  describe("components > TestResult > TrSteps", () => {
@@ -59,15 +75,34 @@ describe("components > TestResult > TrSteps", () => {
59
75
  globalThis.allureReportOptions = { stepTreeExpansion: "expand_failed_only" } as any;
60
76
  });
61
77
 
62
- it("collapses passed-only root steps by default with expand_failed_only", () => {
63
- const view = render(<TrSteps id="passed-test" bodyItems={[passedStep]} />);
78
+ it("always shows the steps root container regardless of step status", () => {
79
+ const view = render(<TrSteps id="test" bodyItems={[passedStepWithContent]} />);
64
80
 
65
- expect(view.queryByTestId("test-result-steps-root")).not.toBeInTheDocument();
81
+ expect(view.getByTestId("test-result-steps-root")).toBeInTheDocument();
66
82
  });
67
83
 
68
- it("opens root steps by default when expand_failed_only finds failed context", () => {
69
- const view = render(<TrSteps id="failed-test" bodyItems={[passedStep, failedStep]} />);
84
+ it("opens top-level passed steps by default even with expand_failed_only", () => {
85
+ const view = render(<TrStep item={passedStepWithContent} stepIndex={1} isTopLevel={true} />);
70
86
 
71
- expect(view.getByTestId("test-result-steps-root")).toBeInTheDocument();
87
+ expect(view.getByTestId("test-result-step-content")).toBeInTheDocument();
88
+ });
89
+
90
+ it("collapses top-level steps when policy is collapsed", () => {
91
+ globalThis.allureReportOptions = { stepTreeExpansion: "collapsed" } as any;
92
+ const view = render(<TrStep item={passedStepWithContent} stepIndex={1} isTopLevel={true} />);
93
+
94
+ expect(view.queryByTestId("test-result-step-content")).not.toBeInTheDocument();
95
+ });
96
+
97
+ it("opens top-level failed steps by default with expand_failed_only", () => {
98
+ const view = render(<TrSteps id="failed-test" bodyItems={[failedStepWithContent]} />);
99
+
100
+ expect(view.getByTestId("test-result-step-content")).toBeInTheDocument();
101
+ });
102
+
103
+ it("collapses nested passed steps by default with expand_failed_only", () => {
104
+ const view = render(<TrStep item={passedStepWithContent} stepIndex={1} isTopLevel={true} />);
105
+
106
+ expect(view.queryAllByTestId("test-result-step-content")).toHaveLength(1);
72
107
  });
73
108
  });
@@ -0,0 +1,58 @@
1
+ import { epic, feature, label, story } from "allure-js-commons";
2
+ import { beforeEach, describe, expect, it, vi } from "vitest";
3
+
4
+ const STORAGE_KEY = "ALLURE_REPORT_SORT_BY";
5
+
6
+ beforeEach(async () => {
7
+ await epic("coverage");
8
+ await feature("sort");
9
+ await story("treeSort");
10
+ await label("coverage", "sort");
11
+ });
12
+
13
+ describe("stores > treeSort", () => {
14
+ beforeEach(() => {
15
+ vi.resetModules();
16
+ localStorage.clear();
17
+ delete (globalThis as any).allureReportOptions;
18
+ });
19
+
20
+ it("defaults to order,asc when no config and no localStorage value", async () => {
21
+ const { sortBy } = await import("../../src/stores/treeSort.js");
22
+
23
+ expect(sortBy.value).toBe("order,asc");
24
+ });
25
+
26
+ it("uses defaultSortBy from reportOptions when localStorage is empty", async () => {
27
+ (globalThis as any).allureReportOptions = { defaultSortBy: "name,asc" };
28
+
29
+ const { sortBy } = await import("../../src/stores/treeSort.js");
30
+
31
+ expect(sortBy.value).toBe("name,asc");
32
+ });
33
+
34
+ it("is case-insensitive for defaultSortBy", async () => {
35
+ (globalThis as any).allureReportOptions = { defaultSortBy: "Name,ASC" };
36
+
37
+ const { sortBy } = await import("../../src/stores/treeSort.js");
38
+
39
+ expect(sortBy.value).toBe("name,asc");
40
+ });
41
+
42
+ it("ignores invalid defaultSortBy and falls back to order,asc", async () => {
43
+ (globalThis as any).allureReportOptions = { defaultSortBy: "invalid,value" };
44
+
45
+ const { sortBy } = await import("../../src/stores/treeSort.js");
46
+
47
+ expect(sortBy.value).toBe("order,asc");
48
+ });
49
+
50
+ it("localStorage takes priority over defaultSortBy from reportOptions", async () => {
51
+ localStorage.setItem(STORAGE_KEY, "duration,desc");
52
+ (globalThis as any).allureReportOptions = { defaultSortBy: "name,asc" };
53
+
54
+ const { sortBy } = await import("../../src/stores/treeSort.js");
55
+
56
+ expect(sortBy.value).toBe("duration,desc");
57
+ });
58
+ });
@@ -0,0 +1,52 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+
3
+ import { timestampToDate } from "@/utils/time";
4
+
5
+ const numericDateTimeOptions: Intl.DateTimeFormatOptions = {
6
+ month: "numeric",
7
+ day: "numeric",
8
+ year: "numeric",
9
+ hour: "numeric",
10
+ minute: "numeric",
11
+ second: "numeric",
12
+ hour12: false,
13
+ };
14
+
15
+ vi.mock("@/stores/locale", () => ({
16
+ currentLocale: { value: "en" },
17
+ currentLocaleIso: { value: "en-US" },
18
+ useI18n: () => ({
19
+ t: (key: string) => (key === "at" ? "at" : key),
20
+ }),
21
+ }));
22
+
23
+ vi.mock("@allurereport/web-commons", async (importOriginal) => ({
24
+ ...(await importOriginal()),
25
+ getLocaleDateTimeOverride: () => undefined,
26
+ }));
27
+
28
+ describe("utils > timestampToDate", () => {
29
+ it("should keep Intl-provided at separator for long English dates", () => {
30
+ const timestamp = 1527776360360;
31
+ const options: Intl.DateTimeFormatOptions = {
32
+ month: "long",
33
+ day: "numeric",
34
+ year: "numeric",
35
+ hour: "numeric",
36
+ minute: "numeric",
37
+ second: "numeric",
38
+ };
39
+ const intlFormatted = new Intl.DateTimeFormat("en-US", options).format(new Date(timestamp));
40
+ const formatted = timestampToDate(timestamp, options);
41
+
42
+ expect(formatted).toBe(intlFormatted);
43
+ expect(formatted).not.toContain("May 31 at 2018 at");
44
+ });
45
+
46
+ it("should add at separator when Intl returns comma-separated date and time", () => {
47
+ const timestamp = 1779887536194;
48
+ const intlFormatted = new Intl.DateTimeFormat("en-US", numericDateTimeOptions).format(new Date(timestamp));
49
+
50
+ expect(timestampToDate(timestamp, numericDateTimeOptions)).toBe(intlFormatted.replace(",", " at"));
51
+ });
52
+ });