@allurereport/web-awesome 3.6.0 → 3.6.1

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 (62) hide show
  1. package/dist/multi/app-1c928f385beb78e2c2e8.js +2 -0
  2. package/dist/multi/{app-15cc636581486c011867.js.LICENSE.txt → app-1c928f385beb78e2c2e8.js.LICENSE.txt} +1 -1
  3. package/dist/multi/manifest.json +23 -23
  4. package/dist/multi/{styles-fd12e72f7e024e668bb4.css → styles-3515d3bdd45651cd4e66.css} +8 -8
  5. package/dist/single/app-16786ab64ac3e094685f.js +2 -0
  6. package/dist/single/{app-bbd6e33664f6d94cfaac.js.LICENSE.txt → app-16786ab64ac3e094685f.js.LICENSE.txt} +1 -1
  7. package/dist/single/manifest.json +1 -1
  8. package/package.json +6 -6
  9. package/src/components/Header/styles.scss +2 -1
  10. package/src/components/SectionSwitcher/styles.scss +1 -1
  11. package/src/components/SideBySide/index.tsx +1 -1
  12. package/src/components/SideBySide/styles.scss +6 -5
  13. package/src/components/TestResult/TrDropdown/index.tsx +7 -1
  14. package/src/components/TestResult/TrDropdown/styles.scss +7 -0
  15. package/src/components/TestResult/TrHeader/styles.scss +3 -0
  16. package/src/components/TestResult/TrRetriesView/TrRetriesItem.tsx +4 -3
  17. package/src/components/TestResult/TrRetriesView/index.tsx +2 -2
  18. package/src/components/TestResult/TrSetup/index.tsx +6 -16
  19. package/src/components/TestResult/TrSteps/TrAttachment.tsx +3 -3
  20. package/src/components/TestResult/TrSteps/TrErrorStep.tsx +9 -3
  21. package/src/components/TestResult/TrSteps/TrStep.tsx +113 -8
  22. package/src/components/TestResult/TrSteps/TrStepHeader.tsx +3 -0
  23. package/src/components/TestResult/TrSteps/index.tsx +90 -5
  24. package/src/components/TestResult/TrSteps/stepTreeExpansion.ts +101 -0
  25. package/src/components/TestResult/TrSteps/styles.scss +12 -0
  26. package/src/components/TestResult/TrTeardown/index.tsx +6 -16
  27. package/src/components/TestResult/bodyItems.ts +26 -1
  28. package/src/components/Tree/index.tsx +7 -2
  29. package/src/components/Tree/styles.scss +1 -1
  30. package/src/index.tsx +13 -2
  31. package/src/stores/env.ts +6 -3
  32. package/src/stores/envInfo.ts +2 -2
  33. package/src/stores/sections.ts +3 -1
  34. package/src/stores/stats.ts +3 -3
  35. package/src/stores/tree.ts +40 -8
  36. package/test/components/TestResult/bodyItems.test.ts +25 -2
  37. package/test/components/TestResult/stepTreeExpansion.test.ts +179 -0
  38. package/types.d.ts +2 -0
  39. package/webpack.config.js +15 -3
  40. package/dist/multi/app-15cc636581486c011867.js +0 -2
  41. package/dist/single/app-bbd6e33664f6d94cfaac.js +0 -2
  42. /package/dist/multi/{173.app-15cc636581486c011867.js → 173.app-1c928f385beb78e2c2e8.js} +0 -0
  43. /package/dist/multi/{174.app-15cc636581486c011867.js → 174.app-1c928f385beb78e2c2e8.js} +0 -0
  44. /package/dist/multi/{252.app-15cc636581486c011867.js → 252.app-1c928f385beb78e2c2e8.js} +0 -0
  45. /package/dist/multi/{282.app-15cc636581486c011867.js → 282.app-1c928f385beb78e2c2e8.js} +0 -0
  46. /package/dist/multi/{29.app-15cc636581486c011867.js → 29.app-1c928f385beb78e2c2e8.js} +0 -0
  47. /package/dist/multi/{310.app-15cc636581486c011867.js → 310.app-1c928f385beb78e2c2e8.js} +0 -0
  48. /package/dist/multi/{416.app-15cc636581486c011867.js → 416.app-1c928f385beb78e2c2e8.js} +0 -0
  49. /package/dist/multi/{507.app-15cc636581486c011867.js → 507.app-1c928f385beb78e2c2e8.js} +0 -0
  50. /package/dist/multi/{527.app-15cc636581486c011867.js → 527.app-1c928f385beb78e2c2e8.js} +0 -0
  51. /package/dist/multi/{600.app-15cc636581486c011867.js → 600.app-1c928f385beb78e2c2e8.js} +0 -0
  52. /package/dist/multi/{605.app-15cc636581486c011867.js → 605.app-1c928f385beb78e2c2e8.js} +0 -0
  53. /package/dist/multi/{638.app-15cc636581486c011867.js → 638.app-1c928f385beb78e2c2e8.js} +0 -0
  54. /package/dist/multi/{672.app-15cc636581486c011867.js → 672.app-1c928f385beb78e2c2e8.js} +0 -0
  55. /package/dist/multi/{686.app-15cc636581486c011867.js → 686.app-1c928f385beb78e2c2e8.js} +0 -0
  56. /package/dist/multi/{725.app-15cc636581486c011867.js → 725.app-1c928f385beb78e2c2e8.js} +0 -0
  57. /package/dist/multi/{741.app-15cc636581486c011867.js → 741.app-1c928f385beb78e2c2e8.js} +0 -0
  58. /package/dist/multi/{749.app-15cc636581486c011867.js → 749.app-1c928f385beb78e2c2e8.js} +0 -0
  59. /package/dist/multi/{755.app-15cc636581486c011867.js → 755.app-1c928f385beb78e2c2e8.js} +0 -0
  60. /package/dist/multi/{894.app-15cc636581486c011867.js → 894.app-1c928f385beb78e2c2e8.js} +0 -0
  61. /package/dist/multi/{943.app-15cc636581486c011867.js → 943.app-1c928f385beb78e2c2e8.js} +0 -0
  62. /package/dist/multi/{980.app-15cc636581486c011867.js → 980.app-1c928f385beb78e2c2e8.js} +0 -0
@@ -1,4 +1,4 @@
1
- /*! @license DOMPurify 3.3.3 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.3.3/LICENSE */
1
+ /*! @license DOMPurify 3.4.0 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.4.0/LICENSE */
2
2
 
3
3
  /**
4
4
  * Prism: Lightweight, robust, elegant syntax highlighting
@@ -1,3 +1,3 @@
1
1
  {
2
- "main.js": "app-bbd6e33664f6d94cfaac.js"
2
+ "main.js": "app-16786ab64ac3e094685f.js"
3
3
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@allurereport/web-awesome",
3
- "version": "3.6.0",
3
+ "version": "3.6.1",
4
4
  "description": "The static files for Allure Awesome Report",
5
5
  "keywords": [
6
6
  "allure",
@@ -27,11 +27,11 @@
27
27
  "lint:fix": "oxlint --import-plugin --fix src test features stories"
28
28
  },
29
29
  "dependencies": {
30
- "@allurereport/charts-api": "3.6.0",
31
- "@allurereport/core-api": "3.6.0",
32
- "@allurereport/plugin-api": "3.6.0",
33
- "@allurereport/web-commons": "3.6.0",
34
- "@allurereport/web-components": "3.6.0",
30
+ "@allurereport/charts-api": "3.6.1",
31
+ "@allurereport/core-api": "3.6.1",
32
+ "@allurereport/plugin-api": "3.6.1",
33
+ "@allurereport/web-commons": "3.6.1",
34
+ "@allurereport/web-components": "3.6.1",
35
35
  "@preact/signals": "^2.6.1",
36
36
  "clsx": "^2.1.1",
37
37
  "d3-shape": "^3.2.0",
@@ -1,7 +1,7 @@
1
1
  .above {
2
- margin-bottom: 12px;
3
2
  display: flex;
4
3
  width: 100%;
4
+ min-width: 0;
5
5
  justify-content: space-between;
6
6
  align-items: center;
7
7
  gap: 12px;
@@ -28,6 +28,7 @@
28
28
 
29
29
  .right {
30
30
  margin-left: auto;
31
+ flex: none;
31
32
  display: flex;
32
33
  gap: 4px;
33
34
  align-items: center;
@@ -1,4 +1,4 @@
1
1
  .layout {
2
2
  flex: 1 1 auto;
3
- overflow: hidden;
3
+ height: 85vh;
4
4
  }
@@ -15,7 +15,7 @@ const SideBySide = ({ left, right }: { left: JSX.Element; right: JSX.Element })
15
15
 
16
16
  const splitter = Split([`.${styles["side-left"]}`, `.${styles["side-right"]}`], {
17
17
  sizes,
18
- gutterSize: 7,
18
+ gutterSize: 4,
19
19
  gutter: (): HTMLElement => {
20
20
  const gutter = document.createElement("div");
21
21
  gutter.className = `${styles.gutter}`;
@@ -1,7 +1,6 @@
1
1
  .side {
2
2
  width: 100%;
3
3
  margin: 0;
4
- overflow: hidden;
5
4
  display: flex;
6
5
  max-width: 1920px;
7
6
  justify-content: space-between;
@@ -17,15 +16,17 @@
17
16
 
18
17
  .side-left {
19
18
  margin-right: auto;
20
- transition: width 50ms;
21
- will-change: width;
19
+ border-radius: 12px 0 0 12px;
20
+ box-shadow: var(--shadow-small);
21
+ overflow: hidden;
22
22
  }
23
23
 
24
24
  .side-right {
25
25
  flex: 1 1 auto;
26
26
  margin-left: auto;
27
- transition: width 200ms;
28
- will-change: width, height;
27
+ border-radius: 0 12px 12px 0;
28
+ box-shadow: var(--shadow-small);
29
+ overflow: hidden;
29
30
  }
30
31
 
31
32
  [dir="ltr"] {
@@ -11,8 +11,9 @@ export const TrDropdown: FunctionalComponent<{
11
11
  title: string;
12
12
  icon: string;
13
13
  counter: number;
14
+ actions?: preact.ComponentChildren;
14
15
  className?: ClassValue;
15
- }> = ({ isOpened, setIsOpen, title, icon, counter, className }) => {
16
+ }> = ({ isOpened, setIsOpen, title, icon, counter, actions, className }) => {
16
17
  return (
17
18
  <div className={clsx(styles["test-result-dropdown"], className)} onClick={() => setIsOpen(!isOpened)}>
18
19
  <ArrowButton isOpened={isOpened} icon={allureIcons.arrowsChevronDown} />
@@ -20,6 +21,11 @@ export const TrDropdown: FunctionalComponent<{
20
21
  <SvgIcon id={icon} />
21
22
  <Text bold>{title}</Text>
22
23
  <Counter count={counter} size="s" />
24
+ {actions ? (
25
+ <div className={styles["test-result-dropdown-actions"]} onClick={(event) => event.stopPropagation()}>
26
+ {actions}
27
+ </div>
28
+ ) : null}
23
29
  </div>
24
30
  </div>
25
31
  );
@@ -32,3 +32,10 @@
32
32
  background: var(--bg-control-flat-medium);
33
33
  }
34
34
  }
35
+
36
+ .test-result-dropdown-actions {
37
+ margin-left: auto;
38
+ display: flex;
39
+ align-items: center;
40
+ gap: 4px;
41
+ }
@@ -1,6 +1,8 @@
1
1
  .test-result-breadcrumbs {
2
2
  margin-right: auto;
3
3
  display: flex;
4
+ flex: 1 1 auto;
5
+ min-width: 0;
4
6
  gap: 4px;
5
7
  overflow: hidden;
6
8
  white-space: nowrap;
@@ -16,6 +18,7 @@
16
18
 
17
19
  .test-result-breadcrumb {
18
20
  display: flex;
21
+ min-width: 0;
19
22
  gap: 4px;
20
23
  color: var(--on-text-secondary);
21
24
  white-space: nowrap;
@@ -28,11 +28,12 @@ export const TrRetriesItem: FunctionalComponent<TrRetriesItemProps> = ({ testRes
28
28
  const retryTitle = convertedStop ? `${retryTitlePrefix} – ${convertedStop}` : retryTitlePrefix;
29
29
 
30
30
  const formattedDuration = typeof duration === "number" ? formatDuration(duration) : undefined;
31
+ const hasErrorDetails = Boolean(error?.trace || error?.message);
31
32
 
32
33
  return (
33
34
  <div data-testid="test-result-retries-item">
34
35
  <div className={styles["test-result-retries-item-header"]} onClick={() => setIsOpen(!isOpened)}>
35
- {Boolean(error.trace || error.message) && (
36
+ {hasErrorDetails && (
36
37
  <ArrowButton
37
38
  data-testid="test-result-retries-item-arrow-button"
38
39
  isOpened={isOpened}
@@ -61,9 +62,9 @@ export const TrRetriesItem: FunctionalComponent<TrRetriesItemProps> = ({ testRes
61
62
  </div>
62
63
  </div>
63
64
  </div>
64
- {isOpened && (error.message || error.trace) && (
65
+ {isOpened && hasErrorDetails && (
65
66
  <div className={styles["test-result-retries-item-content"]}>
66
- <TrError {...error} status={status} />
67
+ <TrError {...(error ?? {})} status={status} />
67
68
  </div>
68
69
  )}
69
70
  </div>
@@ -10,13 +10,13 @@ import * as styles from "./styles.scss";
10
10
  export const TrRetriesView: FunctionalComponent<{
11
11
  testResult: AwesomeTestResult;
12
12
  }> = ({ testResult }) => {
13
- const { retries } = testResult ?? {};
13
+ const retries = testResult?.retries ?? [];
14
14
  const { t } = useI18n("empty");
15
15
 
16
16
  return (
17
17
  <div className={styles["test-result-retries"]}>
18
18
  {retries.length ? (
19
- retries?.map((item, key) => (
19
+ retries.map((item, key) => (
20
20
  <TrRetriesItem
21
21
  testResultItem={item as unknown as AwesomeTestResult}
22
22
  key={key}
@@ -3,21 +3,14 @@ import type { FunctionalComponent } from "preact";
3
3
  import { useState } from "preact/hooks";
4
4
  import type { AwesomeTestResult } from "types";
5
5
 
6
+ import { fixtureResultToTrStepItem } from "@/components/TestResult/bodyItems";
6
7
  import { TrDropdown } from "@/components/TestResult/TrDropdown";
7
- import { TrAttachment } from "@/components/TestResult/TrSteps/TrAttachment";
8
8
  import { TrStep } from "@/components/TestResult/TrSteps/TrStep";
9
9
  import { useI18n } from "@/stores/locale";
10
10
  import { collapsedTrees, toggleTree } from "@/stores/tree";
11
11
 
12
12
  import * as styles from "@/components/TestResult/TrSteps/styles.scss";
13
13
 
14
- const typeMap = {
15
- before: TrStep,
16
- after: TrStep,
17
- step: TrStep,
18
- attachment: TrAttachment,
19
- };
20
-
21
14
  export type TrSetupProps = {
22
15
  setup: AwesomeTestResult["setup"];
23
16
  id?: string;
@@ -45,14 +38,11 @@ export const TrSetup: FunctionalComponent<TrSetupProps> = ({ setup, id }) => {
45
38
  />
46
39
  {isOpened && (
47
40
  <div className={styles["test-result-steps-root"]}>
48
- {setup?.map((item, key) => {
49
- const StepComponent = typeMap[item.type];
50
- return StepComponent ? (
51
- // FIXME: use proper type in the StepComponent component
52
- // @ts-ignore
53
- <StepComponent item={item} stepIndex={key + 1} key={key} className={styles["test-result-step-root"]} />
54
- ) : null;
55
- })}
41
+ {setup?.map((fixture, key) => (
42
+ <div className={styles["test-result-step-root"]} key={fixture.id}>
43
+ <TrStep item={fixtureResultToTrStepItem(fixture)} stepIndex={key + 1} />
44
+ </div>
45
+ ))}
56
46
  </div>
57
47
  )}
58
48
  </div>
@@ -8,7 +8,7 @@ import { useState } from "preact/hooks";
8
8
  import { TrAttachmentInfo } from "@/components/TestResult/TrSteps/TrAttachmentInfo";
9
9
  import { useI18n } from "@/stores";
10
10
  import { openModal } from "@/stores/modal";
11
- import { collapsedTrees, toggleTree } from "@/stores/tree";
11
+ import { isTreeOpened, toggleTree } from "@/stores/tree";
12
12
 
13
13
  import * as styles from "@/components/TestResult/TrSteps/styles.scss";
14
14
 
@@ -62,7 +62,7 @@ export const TrAttachment: FunctionComponent<{
62
62
  className?: string;
63
63
  }> = ({ item, stepIndex }) => {
64
64
  const attachmentTreeId = item.link?.id !== null ? `attachment-${item.link.id}` : null;
65
- const isOpened = !collapsedTrees.value.has(attachmentTreeId);
65
+ const isOpened = attachmentTreeId !== null ? isTreeOpened(attachmentTreeId, false) : false;
66
66
  const [showPreview, setShowPreview] = useState(false);
67
67
  const [highlightCode, setHighlightCode] = useState(true);
68
68
  const { t: tAttachments } = useI18n("attachments");
@@ -100,7 +100,7 @@ export const TrAttachment: FunctionComponent<{
100
100
  onClick={(e) => {
101
101
  e.stopPropagation();
102
102
  if (attachmentTreeId !== null) {
103
- toggleTree(attachmentTreeId);
103
+ toggleTree(attachmentTreeId, false);
104
104
  }
105
105
  }}
106
106
  >
@@ -3,8 +3,12 @@ import type { FunctionComponent } from "preact";
3
3
 
4
4
  import { hasTestLevelErrorContent, type TestLevelErrorItem } from "@/components/TestResult/bodyItems";
5
5
  import { TrError } from "@/components/TestResult/TrError";
6
+ import {
7
+ getStepTreeExpansionPolicy,
8
+ isOpenByDefaultForPolicy,
9
+ } from "@/components/TestResult/TrSteps/stepTreeExpansion";
6
10
  import { TrStepHeader } from "@/components/TestResult/TrSteps/TrStepHeader";
7
- import { collapsedTrees, toggleTree } from "@/stores/tree";
11
+ import { isTreeOpened, toggleTree } from "@/stores/tree";
8
12
 
9
13
  import * as styles from "@/components/TestResult/TrSteps/styles.scss";
10
14
 
@@ -14,8 +18,10 @@ export type TrErrorStepProps = {
14
18
  };
15
19
 
16
20
  export const TrErrorStep: FunctionComponent<TrErrorStepProps> = ({ item, stepIndex }) => {
17
- const isOpened = !collapsedTrees.value.has(item.id);
18
21
  const hasContent = hasTestLevelErrorContent(item.error);
22
+ const policy = getStepTreeExpansionPolicy();
23
+ const openedByDefault = isOpenByDefaultForPolicy(policy, item.status === "failed" || item.status === "broken");
24
+ const isOpened = isTreeOpened(item.id, openedByDefault);
19
25
 
20
26
  return (
21
27
  <div data-testid="test-result-step" className={styles["test-result-step"]}>
@@ -25,7 +31,7 @@ export const TrErrorStep: FunctionComponent<TrErrorStepProps> = ({ item, stepInd
25
31
  stepIndex={stepIndex}
26
32
  isOpened={isOpened}
27
33
  hasContent={hasContent}
28
- onToggle={() => toggleTree(item.id)}
34
+ onToggle={() => toggleTree(item.id, openedByDefault)}
29
35
  />
30
36
  {isOpened && hasContent && (
31
37
  <div
@@ -1,16 +1,36 @@
1
+ import { IconButton, allureIcons } from "@allurereport/web-components";
1
2
  import type { FunctionComponent } from "preact";
3
+ import { useState } from "preact/hooks";
2
4
 
3
5
  import { MetadataList } from "@/components/Metadata";
4
6
  import { type MetadataItem } from "@/components/ReportMetadata";
5
- import type { TrStepItem } from "@/components/TestResult/bodyItems";
7
+ import { hasErrorDiff, type TrStepItem } from "@/components/TestResult/bodyItems";
6
8
  import { TrError } from "@/components/TestResult/TrError";
9
+ import {
10
+ collectExpandableStepNodes,
11
+ hasStepContent,
12
+ getStepTreeExpansionPolicy,
13
+ getNextSubtreeToggleState,
14
+ getSubtreeToggleIcon,
15
+ isSubtreeFirstLevelOnlyOpened,
16
+ isStepOpenedByDefault,
17
+ type SubtreeNode,
18
+ type SubtreeToggleState,
19
+ } from "@/components/TestResult/TrSteps/stepTreeExpansion";
7
20
  import { TrBodyItems } from "@/components/TestResult/TrSteps/TrBodyItems";
8
21
  import { TrStepHeader } from "@/components/TestResult/TrSteps/TrStepHeader";
9
22
  import { TrStepInfo } from "@/components/TestResult/TrSteps/TrStepInfo";
10
- import { collapsedTrees, toggleTree } from "@/stores/tree";
23
+ import { isTreeOpened, setTreeOpened, toggleTree } from "@/stores/tree";
11
24
 
12
25
  import * as styles from "@/components/TestResult/TrSteps/styles.scss";
13
26
 
27
+ const iconBySubtreeState = {
28
+ "single-down": allureIcons.lineArrowsChevronDown,
29
+ "single-up": allureIcons.lineArrowsChevronUp,
30
+ "double-down": allureIcons.lineArrowsChevronDownDouble,
31
+ "double-up": allureIcons.lineArrowsChevronUpDouble,
32
+ } as const;
33
+
14
34
  export const TrStepParameters = (props: { parameters: TrStepItem["item"]["parameters"] }) => {
15
35
  const { parameters } = props;
16
36
 
@@ -23,14 +43,22 @@ export const TrStepParameters = (props: { parameters: TrStepItem["item"]["parame
23
43
 
24
44
  export const TrStepsContent = (props: { item: TrStepItem }) => {
25
45
  const { item: stepData, bodyItems, suppressInlineError } = props.item;
46
+ const inlineError = {
47
+ message: stepData.message ?? stepData.error?.message,
48
+ trace: stepData.trace ?? stepData.error?.trace,
49
+ actual: stepData.error?.actual,
50
+ expected: stepData.error?.expected,
51
+ };
26
52
  const hasInlineError = Boolean(
27
- (stepData.message || stepData.trace) && !stepData.hasSimilarErrorInSubSteps && !suppressInlineError,
53
+ (inlineError.message || inlineError.trace || hasErrorDiff(inlineError)) &&
54
+ !stepData.hasSimilarErrorInSubSteps &&
55
+ !suppressInlineError,
28
56
  );
29
57
 
30
58
  return (
31
59
  <div data-testid={"test-result-step-content"} className={styles["test-result-step-content"]}>
32
60
  {Boolean(stepData.parameters?.length) && <TrStepParameters parameters={stepData.parameters} />}
33
- {hasInlineError && <TrError {...stepData} />}
61
+ {hasInlineError && <TrError {...inlineError} status={stepData.status} />}
34
62
  {Boolean(bodyItems.length) && <TrBodyItems bodyItems={bodyItems} />}
35
63
  </div>
36
64
  );
@@ -41,11 +69,76 @@ export const TrStep: FunctionComponent<{
41
69
  stepIndex?: number;
42
70
  }> = ({ item, stepIndex }) => {
43
71
  const { item: stepData, bodyItems, suppressInlineError } = item;
72
+ const inlineError = {
73
+ message: stepData.message ?? stepData.error?.message,
74
+ trace: stepData.trace ?? stepData.error?.trace,
75
+ actual: stepData.error?.actual,
76
+ expected: stepData.error?.expected,
77
+ };
44
78
  const hasInlineError = Boolean(
45
- (stepData.message || stepData.trace) && !stepData.hasSimilarErrorInSubSteps && !suppressInlineError,
79
+ (inlineError.message || inlineError.trace || hasErrorDiff(inlineError)) &&
80
+ !stepData.hasSimilarErrorInSubSteps &&
81
+ !suppressInlineError,
82
+ );
83
+ const policy = getStepTreeExpansionPolicy();
84
+ const hasContent = hasStepContent(item);
85
+ const openedByDefault = isStepOpenedByDefault(policy, stepData.status, bodyItems);
86
+ const isOpened = isTreeOpened(stepData.stepId, openedByDefault);
87
+ const expandableDescendantNodes = collectExpandableStepNodes(bodyItems, policy);
88
+ const hasExpandableDescendants = expandableDescendantNodes.length > 0;
89
+ const subtreeNodes: SubtreeNode[] = hasExpandableDescendants
90
+ ? [
91
+ { id: stepData.stepId, openedByDefault, isRoot: true },
92
+ ...expandableDescendantNodes.map((node) => ({ ...node, isRoot: false })),
93
+ ]
94
+ : [];
95
+ const [lastSubtreeToggle, setLastSubtreeToggle] = useState<SubtreeToggleState | null>(null);
96
+ const isRootSubtreeOpened = isTreeOpened(stepData.stepId, openedByDefault);
97
+ const isSubtreeCollapsedAll = !isRootSubtreeOpened;
98
+ const isSubtreeFirstLevelOnly = isSubtreeFirstLevelOnlyOpened(
99
+ stepData.stepId,
100
+ openedByDefault,
101
+ subtreeNodes,
102
+ isTreeOpened,
46
103
  );
47
- const hasContent = Boolean(bodyItems.length || stepData.parameters?.length || hasInlineError);
48
- const isOpened = !collapsedTrees.value.has(stepData.stepId);
104
+ const isSubtreeExpandedAll =
105
+ hasExpandableDescendants && subtreeNodes.every((node) => isTreeOpened(node.id, node.openedByDefault));
106
+ const hasOnlyLeafResults = hasExpandableDescendants && subtreeNodes.every((node) => node.isRoot);
107
+ const subtreeToggleIcon =
108
+ iconBySubtreeState[
109
+ getSubtreeToggleIcon({
110
+ hasOnlyLeafResults,
111
+ isSubtreeCollapsedAll,
112
+ isSubtreeFirstLevelOnly,
113
+ })
114
+ ];
115
+
116
+ const setSubtreeState = (state: SubtreeToggleState) => {
117
+ subtreeNodes.forEach((node) => {
118
+ const shouldOpenSubtree = state === "all" ? true : state === "first" ? node.isRoot : false;
119
+ setTreeOpened(node.id, shouldOpenSubtree, node.openedByDefault);
120
+ });
121
+ };
122
+
123
+ const toggleSubtree = (event: MouseEvent) => {
124
+ event.stopPropagation();
125
+ const nextState = getNextSubtreeToggleState({
126
+ hasOnlyLeafResults,
127
+ isSubtreeCollapsedAll,
128
+ isSubtreeFirstLevelOnly,
129
+ isSubtreeExpandedAll,
130
+ lastSubtreeToggle,
131
+ });
132
+ setSubtreeState(nextState);
133
+ if (nextState !== "first") {
134
+ setLastSubtreeToggle(nextState);
135
+ }
136
+ };
137
+
138
+ const toggleStep = () => {
139
+ setLastSubtreeToggle(null);
140
+ toggleTree(stepData.stepId, openedByDefault);
141
+ };
49
142
 
50
143
  return (
51
144
  <div data-testid={"test-result-step"} className={styles["test-result-step"]}>
@@ -55,7 +148,19 @@ export const TrStep: FunctionComponent<{
55
148
  stepIndex={stepIndex}
56
149
  isOpened={isOpened}
57
150
  hasContent={hasContent}
58
- onToggle={() => toggleTree(stepData.stepId)}
151
+ onToggle={toggleStep}
152
+ subtreeToggle={
153
+ hasExpandableDescendants ? (
154
+ <IconButton
155
+ style="ghost"
156
+ size="xs"
157
+ icon={subtreeToggleIcon}
158
+ onClick={toggleSubtree}
159
+ data-testid="test-result-step-subtree-toggle"
160
+ className={styles["test-result-step-subtree-toggle"]}
161
+ />
162
+ ) : null
163
+ }
59
164
  extra={<TrStepInfo item={stepData} />}
60
165
  />
61
166
  {hasContent && isOpened && <TrStepsContent item={item} />}
@@ -12,6 +12,7 @@ export type TrStepHeaderProps = {
12
12
  hasContent: boolean;
13
13
  onToggle: () => void;
14
14
  extra?: preact.ComponentChildren;
15
+ subtreeToggle?: preact.ComponentChildren;
15
16
  };
16
17
 
17
18
  export const TrStepHeader: FunctionComponent<TrStepHeaderProps> = ({
@@ -22,6 +23,7 @@ export const TrStepHeader: FunctionComponent<TrStepHeaderProps> = ({
22
23
  hasContent,
23
24
  onToggle,
24
25
  extra,
26
+ subtreeToggle,
25
27
  }) => (
26
28
  <div
27
29
  data-testid="test-result-step-header"
@@ -45,6 +47,7 @@ export const TrStepHeader: FunctionComponent<TrStepHeaderProps> = ({
45
47
  <Text data-testid="test-result-step-title" className={styles["test-result-header-text"]}>
46
48
  {title}
47
49
  </Text>
50
+ {subtreeToggle}
48
51
  {extra}
49
52
  </div>
50
53
  );
@@ -1,22 +1,96 @@
1
- import { allureIcons } from "@allurereport/web-components";
1
+ import { IconButton, allureIcons } from "@allurereport/web-components";
2
2
  import type { FunctionalComponent } from "preact";
3
+ import { useState } from "preact/hooks";
3
4
 
4
5
  import type { TrBodyItem } from "@/components/TestResult/bodyItems";
5
6
  import { TrDropdown } from "@/components/TestResult/TrDropdown";
7
+ import {
8
+ collectExpandableStepNodes,
9
+ getNextSubtreeToggleState,
10
+ getSubtreeToggleIcon,
11
+ getStepTreeExpansionPolicy,
12
+ hasFailedStepContext,
13
+ isSubtreeFirstLevelOnlyOpened,
14
+ isOpenByDefaultForPolicy,
15
+ type SubtreeNode,
16
+ type SubtreeToggleState,
17
+ } from "@/components/TestResult/TrSteps/stepTreeExpansion";
6
18
  import { TrBodyItems } from "@/components/TestResult/TrSteps/TrBodyItems";
7
19
  import { useI18n } from "@/stores/locale";
8
- import { collapsedTrees, toggleTree } from "@/stores/tree";
20
+ import { isTreeOpened, setTreeOpened, toggleTree } from "@/stores/tree";
9
21
 
10
22
  import * as styles from "./styles.scss";
11
23
 
24
+ const iconBySubtreeState = {
25
+ "single-down": allureIcons.lineArrowsChevronDown,
26
+ "single-up": allureIcons.lineArrowsChevronUp,
27
+ "double-down": allureIcons.lineArrowsChevronDownDouble,
28
+ "double-up": allureIcons.lineArrowsChevronUpDouble,
29
+ } as const;
30
+
12
31
  export type TrStepsProps = {
13
32
  bodyItems: TrBodyItem[];
14
33
  id?: string;
15
34
  };
16
35
 
17
36
  export const TrSteps: FunctionalComponent<TrStepsProps> = ({ bodyItems, id }) => {
18
- const stepsId = id !== null ? `${id}-steps` : null;
19
- const isOpened = !collapsedTrees.value.has(stepsId);
37
+ const stepsId = typeof id === "string" ? `${id}-steps` : null;
38
+ const policy = getStepTreeExpansionPolicy();
39
+ const openedByDefault = isOpenByDefaultForPolicy(policy, hasFailedStepContext(bodyItems));
40
+ const isOpened = stepsId !== null ? isTreeOpened(stepsId, openedByDefault) : openedByDefault;
41
+ const expandableTreeNodes = collectExpandableStepNodes(bodyItems, policy);
42
+ const hasChildren = stepsId !== null && bodyItems.length > 0;
43
+ const subtreeNodes: SubtreeNode[] = hasChildren
44
+ ? [
45
+ { id: stepsId, openedByDefault, isRoot: true },
46
+ ...expandableTreeNodes.map((node) => ({ ...node, isRoot: false })),
47
+ ]
48
+ : [];
49
+ const [lastSubtreeToggle, setLastSubtreeToggle] = useState<SubtreeToggleState | null>(null);
50
+ const isRootSubtreeOpened = stepsId !== null ? isTreeOpened(stepsId, openedByDefault) : false;
51
+ const isSubtreeCollapsedAll = !isRootSubtreeOpened;
52
+ const isSubtreeFirstLevelOnly =
53
+ stepsId !== null ? isSubtreeFirstLevelOnlyOpened(stepsId, openedByDefault, subtreeNodes, isTreeOpened) : false;
54
+ const isSubtreeExpandedAll = hasChildren && subtreeNodes.every((node) => isTreeOpened(node.id, node.openedByDefault));
55
+ const hasOnlyLeafResults = hasChildren && subtreeNodes.every((node) => node.isRoot);
56
+ const subtreeToggleIcon =
57
+ iconBySubtreeState[
58
+ getSubtreeToggleIcon({
59
+ hasOnlyLeafResults,
60
+ isSubtreeCollapsedAll,
61
+ isSubtreeFirstLevelOnly,
62
+ })
63
+ ];
64
+
65
+ const setSubtreeState = (state: SubtreeToggleState) => {
66
+ subtreeNodes.forEach((node) => {
67
+ const shouldOpenSubtree = state === "all" ? true : state === "first" ? node.isRoot : false;
68
+ setTreeOpened(node.id, shouldOpenSubtree, node.openedByDefault);
69
+ });
70
+ };
71
+
72
+ const handleToggleSubtree = (event: MouseEvent) => {
73
+ event.stopPropagation();
74
+ const nextState = getNextSubtreeToggleState({
75
+ hasOnlyLeafResults,
76
+ isSubtreeCollapsedAll,
77
+ isSubtreeFirstLevelOnly,
78
+ isSubtreeExpandedAll,
79
+ lastSubtreeToggle,
80
+ });
81
+ setSubtreeState(nextState);
82
+ if (nextState !== "first") {
83
+ setLastSubtreeToggle(nextState);
84
+ }
85
+ };
86
+
87
+ const toggleRoot = () => {
88
+ if (stepsId === null) {
89
+ return;
90
+ }
91
+ setLastSubtreeToggle(null);
92
+ toggleTree(stepsId, openedByDefault);
93
+ };
20
94
 
21
95
  const { t } = useI18n("execution");
22
96
  return (
@@ -24,9 +98,20 @@ export const TrSteps: FunctionalComponent<TrStepsProps> = ({ bodyItems, id }) =>
24
98
  <TrDropdown
25
99
  icon={allureIcons.lineHelpersPlayCircle}
26
100
  isOpened={isOpened}
27
- setIsOpen={() => stepsId !== null && toggleTree(stepsId)}
101
+ setIsOpen={toggleRoot}
28
102
  counter={bodyItems.length}
29
103
  title={t("body")}
104
+ actions={
105
+ hasChildren ? (
106
+ <IconButton
107
+ style="ghost"
108
+ size="xs"
109
+ icon={subtreeToggleIcon}
110
+ onClick={handleToggleSubtree}
111
+ data-testid="test-result-steps-subtree-toggle"
112
+ />
113
+ ) : null
114
+ }
30
115
  />
31
116
  {isOpened && (
32
117
  <div data-testid="test-result-steps-root" className={styles["test-result-steps-root"]}>