@griddo/ax 11.12.0 → 11.12.1-rc.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 (100) hide show
  1. package/config/jest/componentsMock.js +7 -5
  2. package/package.json +2 -2
  3. package/src/__tests__/components/Browser/Browser.test.tsx +438 -87
  4. package/src/__tests__/components/Browser/Browser.utils.test.ts +55 -0
  5. package/src/__tests__/components/ConfigPanel/ConfigPanel.test.tsx +1 -3
  6. package/src/__tests__/components/Fields/Button/Button.test.tsx +29 -27
  7. package/src/__tests__/components/HeadingsPreviewModal/ErrorsBanner/ErrorItem/ErrorItem.test.tsx +158 -0
  8. package/src/__tests__/components/HeadingsPreviewModal/ErrorsBanner/ErrorsBanner.test.tsx +90 -0
  9. package/src/__tests__/components/HeadingsPreviewModal/HeadingsPreviewModal.test.tsx +178 -0
  10. package/src/__tests__/components/HeadingsPreviewModal/HeadingsPreviewModal.utils.test.tsx +150 -0
  11. package/src/__tests__/components/KeywordsPreviewModal/KeywordItem/KeywordItem.test.tsx +91 -0
  12. package/src/__tests__/components/KeywordsPreviewModal/KeywordsPreviewModal.test.tsx +122 -0
  13. package/src/__tests__/components/KeywordsPreviewModal/KeywordsPreviewModal.utils.test.ts +15 -0
  14. package/src/__tests__/components/KeywordsPreviewModal/atoms.test.tsx +101 -0
  15. package/src/__tests__/components/ResizePanel/ResizePanel.test.tsx +1 -1
  16. package/src/__tests__/modules/FramePreview/FramePreview.test.tsx +318 -0
  17. package/src/__tests__/modules/FramePreview/FramePreview.utils.test.ts +242 -0
  18. package/src/__tests__/modules/FramePreview/HeadingsOverlay/HeadingsOverlay.test.tsx +185 -0
  19. package/src/components/Browser/index.tsx +294 -149
  20. package/src/components/Browser/style.tsx +75 -6
  21. package/src/components/Browser/utils.tsx +13 -0
  22. package/src/components/Button/index.tsx +2 -1
  23. package/src/components/ConfigPanel/Form/ConnectedField/PageConnectedField/Field/index.tsx +2 -4
  24. package/src/components/Fields/AsyncSelect/style.tsx +13 -0
  25. package/src/components/Fields/FieldGroup/index.tsx +5 -2
  26. package/src/components/Fields/FieldGroup/style.tsx +32 -7
  27. package/src/components/Fields/HeadingField/index.tsx +22 -22
  28. package/src/components/Fields/HiddenField/style.tsx +1 -1
  29. package/src/components/Fields/NumberField/index.tsx +15 -16
  30. package/src/components/Fields/NumberField/style.tsx +2 -0
  31. package/src/components/Fields/ReferenceField/index.tsx +1 -1
  32. package/src/components/Fields/SEOPreview/index.tsx +36 -0
  33. package/src/components/Fields/SEOPreview/style.tsx +24 -0
  34. package/src/components/Fields/Select/index.tsx +5 -1
  35. package/src/components/Fields/Select/style.tsx +56 -0
  36. package/src/components/Fields/SummaryButton/index.tsx +18 -9
  37. package/src/components/Fields/SummaryButton/style.tsx +1 -2
  38. package/src/components/Fields/TagsField/index.tsx +8 -9
  39. package/src/components/Fields/UrlField/index.tsx +26 -27
  40. package/src/components/Fields/index.tsx +2 -0
  41. package/src/components/FloatingNote/index.tsx +35 -0
  42. package/src/components/FloatingNote/style.tsx +26 -0
  43. package/src/components/FloatingPanel/index.tsx +5 -2
  44. package/src/components/FloatingPanel/style.tsx +2 -1
  45. package/src/components/HeadingsPreviewModal/ErrorsBanner/ErrorItem/index.tsx +85 -0
  46. package/src/components/HeadingsPreviewModal/ErrorsBanner/ErrorItem/style.tsx +80 -0
  47. package/src/components/HeadingsPreviewModal/ErrorsBanner/index.tsx +57 -0
  48. package/src/components/HeadingsPreviewModal/ErrorsBanner/style.tsx +82 -0
  49. package/src/components/HeadingsPreviewModal/HeadingItem/index.tsx +71 -0
  50. package/src/components/HeadingsPreviewModal/HeadingItem/style.tsx +77 -0
  51. package/src/components/HeadingsPreviewModal/index.tsx +148 -0
  52. package/src/components/HeadingsPreviewModal/style.tsx +82 -0
  53. package/src/components/HeadingsPreviewModal/utils.tsx +329 -0
  54. package/src/components/Icon/index.tsx +1 -2
  55. package/src/components/IconAction/index.tsx +1 -1
  56. package/src/components/KeywordsPreviewModal/KeywordItem/index.tsx +46 -0
  57. package/src/components/KeywordsPreviewModal/KeywordItem/style.tsx +64 -0
  58. package/src/components/KeywordsPreviewModal/atoms.tsx +96 -0
  59. package/src/components/KeywordsPreviewModal/index.tsx +99 -0
  60. package/src/components/KeywordsPreviewModal/style.tsx +87 -0
  61. package/src/components/KeywordsPreviewModal/utils.tsx +22 -0
  62. package/src/components/MainWrapper/AppBar/index.tsx +8 -1
  63. package/src/components/MainWrapper/index.tsx +7 -1
  64. package/src/components/Notification/index.tsx +2 -2
  65. package/src/components/PageFinder/index.tsx +1 -1
  66. package/src/components/ResizePanel/index.tsx +4 -3
  67. package/src/components/ResizePanel/style.tsx +1 -1
  68. package/src/components/SearchField/style.tsx +2 -2
  69. package/src/components/SideModal/index.tsx +2 -1
  70. package/src/components/Tabs/index.tsx +13 -4
  71. package/src/components/Tabs/style.tsx +7 -8
  72. package/src/components/Toast/index.tsx +4 -2
  73. package/src/components/Tooltip/index.tsx +4 -3
  74. package/src/components/index.tsx +8 -0
  75. package/src/forms/fields.tsx +70 -68
  76. package/src/hooks/forms.tsx +22 -1
  77. package/src/hooks/index.tsx +13 -3
  78. package/src/hooks/modals.tsx +103 -15
  79. package/src/hooks/users.tsx +25 -8
  80. package/src/modules/Forms/atoms.tsx +2 -2
  81. package/src/modules/FramePreview/HeadingsOverlay/index.tsx +116 -0
  82. package/src/modules/FramePreview/HeadingsOverlay/style.tsx +34 -0
  83. package/src/modules/FramePreview/index.tsx +55 -16
  84. package/src/modules/FramePreview/style.tsx +34 -2
  85. package/src/modules/FramePreview/utils.tsx +140 -0
  86. package/src/modules/GlobalEditor/Editor/index.tsx +37 -3
  87. package/src/modules/GlobalEditor/PageBrowser/index.tsx +19 -2
  88. package/src/modules/GlobalEditor/Preview/index.tsx +0 -2
  89. package/src/modules/GlobalEditor/Preview/style.tsx +1 -1
  90. package/src/modules/GlobalEditor/index.tsx +119 -57
  91. package/src/modules/PageEditor/Editor/index.tsx +33 -2
  92. package/src/modules/PageEditor/PageBrowser/index.tsx +20 -2
  93. package/src/modules/PageEditor/Preview/index.tsx +0 -2
  94. package/src/modules/PageEditor/Preview/style.tsx +1 -1
  95. package/src/modules/PageEditor/atoms.tsx +1 -1
  96. package/src/modules/PageEditor/index.tsx +130 -66
  97. package/src/modules/PublicPreview/index.tsx +5 -2
  98. package/src/schemas/pages/GlobalPage.ts +87 -70
  99. package/src/schemas/pages/Page.ts +87 -70
  100. package/src/types/index.tsx +12 -0
@@ -1,23 +1,22 @@
1
- import React from "react";
2
-
3
- import { ThemeProvider } from "styled-components";
1
+ import { cleanup, fireEvent, render, screen } from "@testing-library/react";
4
2
  import { mock } from "jest-mock-extended";
5
- import { render, screen, cleanup, fireEvent } from "@testing-library/react";
3
+ import { ThemeProvider } from "styled-components";
6
4
  import "@testing-library/jest-dom";
7
5
 
8
- import Button, { IButtonProps } from "@ax/components/Button";
6
+ import Button, { type IButtonProps } from "@ax/components/Button";
9
7
  import { parseTheme } from "@ax/helpers";
10
8
  import globalTheme from "@ax/themes/theme.json";
11
9
 
12
10
  afterEach(cleanup);
13
11
 
14
- const defaultProps = mock<IButtonProps>();
12
+ const createDefaultProps = (): IButtonProps => mock<IButtonProps>();
15
13
 
16
14
  describe("Button component rendering", () => {
17
15
  it("should render the component with the default button and no icon", () => {
16
+ const props = createDefaultProps();
18
17
  render(
19
18
  <ThemeProvider theme={parseTheme(globalTheme)}>
20
- <Button {...defaultProps} />
19
+ <Button {...props} />
21
20
  </ThemeProvider>,
22
21
  );
23
22
 
@@ -28,12 +27,13 @@ describe("Button component rendering", () => {
28
27
  });
29
28
 
30
29
  it("should render the component with the text button and an icon", () => {
31
- defaultProps.buttonStyle = "text";
32
- defaultProps.icon = "delete";
30
+ const props = createDefaultProps();
31
+ props.buttonStyle = "text";
32
+ props.icon = "delete";
33
33
 
34
34
  render(
35
35
  <ThemeProvider theme={parseTheme(globalTheme)}>
36
- <Button {...defaultProps} />
36
+ <Button {...props} />
37
37
  </ThemeProvider>,
38
38
  );
39
39
 
@@ -44,10 +44,11 @@ describe("Button component rendering", () => {
44
44
  });
45
45
 
46
46
  it("should render the component with the line button", () => {
47
- defaultProps.buttonStyle = "line";
47
+ const props = createDefaultProps();
48
+ props.buttonStyle = "line";
48
49
  render(
49
50
  <ThemeProvider theme={parseTheme(globalTheme)}>
50
- <Button {...defaultProps} />
51
+ <Button {...props} />
51
52
  </ThemeProvider>,
52
53
  );
53
54
 
@@ -56,10 +57,12 @@ describe("Button component rendering", () => {
56
57
  });
57
58
 
58
59
  it("should render the component with a className", () => {
59
- defaultProps.className = "my-class";
60
+ const props = createDefaultProps();
61
+ props.className = "my-class";
62
+ props.buttonStyle = "line";
60
63
  render(
61
64
  <ThemeProvider theme={parseTheme(globalTheme)}>
62
- <Button {...defaultProps} />
65
+ <Button {...props} />
63
66
  </ThemeProvider>,
64
67
  );
65
68
 
@@ -69,10 +72,11 @@ describe("Button component rendering", () => {
69
72
  });
70
73
 
71
74
  it("should render the children", () => {
72
- defaultProps.children = "el texto";
75
+ const props = createDefaultProps();
76
+ props.children = "el texto";
73
77
  render(
74
78
  <ThemeProvider theme={parseTheme(globalTheme)}>
75
- <Button {...defaultProps} />
79
+ <Button {...props} />
76
80
  </ThemeProvider>,
77
81
  );
78
82
 
@@ -80,13 +84,12 @@ describe("Button component rendering", () => {
80
84
  });
81
85
 
82
86
  it("should trigger the onClick action", () => {
83
- defaultProps.buttonStyle = "lineInverse";
84
- const handleOnClickMock = defaultProps.onClick as jest.MockedFunction<
85
- (e: React.MouseEvent<HTMLButtonElement>) => void
86
- >;
87
+ const props = createDefaultProps();
88
+ props.buttonStyle = "lineInverse";
89
+ const handleOnClickMock = props.onClick as jest.MockedFunction<(e: React.MouseEvent<HTMLButtonElement>) => void>;
87
90
  render(
88
91
  <ThemeProvider theme={parseTheme(globalTheme)}>
89
- <Button {...defaultProps} />
92
+ <Button {...props} />
90
93
  </ThemeProvider>,
91
94
  );
92
95
 
@@ -97,14 +100,13 @@ describe("Button component rendering", () => {
97
100
  });
98
101
 
99
102
  it("should not trigger the onClick action if the button is disabled", () => {
100
- defaultProps.buttonStyle = "lineInverse";
101
- defaultProps.disabled = true;
102
- const handleOnClickMock = defaultProps.onClick as jest.MockedFunction<
103
- (e: React.MouseEvent<HTMLButtonElement>) => void
104
- >;
103
+ const props = createDefaultProps();
104
+ props.buttonStyle = "lineInverse";
105
+ props.disabled = true;
106
+ const handleOnClickMock = props.onClick as jest.MockedFunction<(e: React.MouseEvent<HTMLButtonElement>) => void>;
105
107
  render(
106
108
  <ThemeProvider theme={parseTheme(globalTheme)}>
107
- <Button {...defaultProps} />
109
+ <Button {...props} />
108
110
  </ThemeProvider>,
109
111
  );
110
112
 
@@ -0,0 +1,158 @@
1
+ import React from "react";
2
+
3
+ import { ThemeProvider } from "styled-components";
4
+ import { render, cleanup, screen, fireEvent } from "@testing-library/react";
5
+ import { parseTheme } from "@ax/helpers";
6
+ import "@testing-library/jest-dom";
7
+
8
+ import ErrorItem from "@ax/components/HeadingsPreviewModal/ErrorsBanner/ErrorItem";
9
+ import type { IHeadingError } from "@ax/components/HeadingsPreviewModal/utils";
10
+ import globalTheme from "@ax/themes/theme.json";
11
+
12
+ afterEach(cleanup);
13
+
14
+ const mockError: IHeadingError = {
15
+ message: "Check heading structure for SEO compliance",
16
+ description: "Ensure headings are properly nested",
17
+ headingIds: [1, 2, 3],
18
+ };
19
+
20
+ const defaultProps = {
21
+ error: mockError,
22
+ onSelectHeading: jest.fn(() => jest.fn()),
23
+ };
24
+
25
+ const renderComponent = (props = defaultProps) =>
26
+ render(
27
+ <ThemeProvider theme={parseTheme(globalTheme)}>
28
+ <ErrorItem {...props} />
29
+ </ThemeProvider>,
30
+ );
31
+
32
+ describe("ErrorItem component rendering", () => {
33
+ it("should render the error message", () => {
34
+ renderComponent();
35
+ expect(screen.getByText("Check heading structure for SEO compliance")).toBeTruthy();
36
+ });
37
+
38
+ it("should show heading counter text", () => {
39
+ renderComponent();
40
+ expect(screen.getByText("3 headings")).toBeTruthy();
41
+ });
42
+
43
+ it("should show the open toggle button when headingIds are present", () => {
44
+ renderComponent();
45
+ const buttons = screen.getAllByRole("button");
46
+ // toggle (index 0) + close (index 1) + prev (index 2) + next (index 3)
47
+ expect(buttons.length).toBe(4);
48
+ });
49
+
50
+ it("should not show the open toggle button when headingIds is empty", () => {
51
+ const props = {
52
+ ...defaultProps,
53
+ error: { ...mockError, headingIds: [] },
54
+ };
55
+ renderComponent(props);
56
+ const buttons = screen.getAllByRole("button");
57
+ // only close (index 0) + prev (index 1) + next (index 2) — no toggle
58
+ expect(buttons.length).toBe(3);
59
+ });
60
+
61
+ it("should not render when dismissed", () => {
62
+ renderComponent();
63
+ expect(screen.getByText("Check heading structure for SEO compliance")).toBeTruthy();
64
+
65
+ const buttons = screen.getAllByRole("button");
66
+ // Close button is at index 1 (after toggle)
67
+ fireEvent.click(buttons[1]);
68
+
69
+ expect(screen.queryByText("Check heading structure for SEO compliance")).toBeNull();
70
+ });
71
+ });
72
+
73
+ describe("ErrorItem component events", () => {
74
+ it("should call onSelectHeading when the open button is clicked", () => {
75
+ const onSelectHeadingMock = jest.fn(() => jest.fn());
76
+ renderComponent({ ...defaultProps, onSelectHeading: onSelectHeadingMock });
77
+
78
+ const buttons = screen.getAllByRole("button");
79
+ // Toggle open is at index 0
80
+ fireEvent.click(buttons[0]);
81
+
82
+ expect(onSelectHeadingMock).toBeCalledWith(mockError.headingIds[0]);
83
+ });
84
+
85
+ it("should update counter to '1 of 3 headings' after opening", () => {
86
+ renderComponent();
87
+
88
+ const buttons = screen.getAllByRole("button");
89
+ fireEvent.click(buttons[0]);
90
+
91
+ expect(screen.getByText("1 of 3 headings")).toBeTruthy();
92
+ });
93
+
94
+ it("should call onSelectHeading with first heading ID when next button is clicked", () => {
95
+ const onSelectHeadingMock = jest.fn(() => jest.fn());
96
+ renderComponent({ ...defaultProps, onSelectHeading: onSelectHeadingMock });
97
+
98
+ const buttons = screen.getAllByRole("button");
99
+ // Next button is at index 3 (toggle, close, prev, next)
100
+ fireEvent.click(buttons[3]);
101
+
102
+ expect(onSelectHeadingMock).toBeCalledWith(mockError.headingIds[0]);
103
+ });
104
+
105
+ it("should update counter to '2 of 3 headings' after two next clicks", () => {
106
+ renderComponent();
107
+
108
+ const buttons = screen.getAllByRole("button");
109
+ fireEvent.click(buttons[3]);
110
+ fireEvent.click(buttons[3]);
111
+
112
+ expect(screen.getByText("2 of 3 headings")).toBeTruthy();
113
+ });
114
+
115
+ it("should call onSelectHeading with previous heading ID when prev button is clicked", () => {
116
+ const onSelectHeadingMock = jest.fn(() => jest.fn());
117
+ renderComponent({ ...defaultProps, onSelectHeading: onSelectHeadingMock });
118
+
119
+ const buttons = screen.getAllByRole("button");
120
+ // Advance to index 1, then go back
121
+ fireEvent.click(buttons[3]);
122
+ fireEvent.click(buttons[3]);
123
+
124
+ jest.clearAllMocks();
125
+ fireEvent.click(buttons[2]);
126
+
127
+ expect(onSelectHeadingMock).toBeCalledWith(mockError.headingIds[0]);
128
+ });
129
+
130
+ it("should not navigate prev when already at the first heading", () => {
131
+ const onSelectHeadingMock = jest.fn(() => jest.fn());
132
+ renderComponent({ ...defaultProps, onSelectHeading: onSelectHeadingMock });
133
+
134
+ const buttons = screen.getAllByRole("button");
135
+ // Open sets currentIndex to 0, then try to go prev
136
+ fireEvent.click(buttons[0]);
137
+ jest.clearAllMocks();
138
+ fireEvent.click(buttons[2]);
139
+
140
+ expect(onSelectHeadingMock).not.toBeCalled();
141
+ });
142
+
143
+ it("should not navigate next when already at the last heading", () => {
144
+ const onSelectHeadingMock = jest.fn(() => jest.fn());
145
+ renderComponent({ ...defaultProps, onSelectHeading: onSelectHeadingMock });
146
+
147
+ const buttons = screen.getAllByRole("button");
148
+ // Click next 3 times to reach the end (headingIds has 3 items, indices 0-2)
149
+ fireEvent.click(buttons[3]);
150
+ fireEvent.click(buttons[3]);
151
+ fireEvent.click(buttons[3]);
152
+ jest.clearAllMocks();
153
+
154
+ // Fourth click should be a no-op
155
+ fireEvent.click(buttons[3]);
156
+ expect(onSelectHeadingMock).not.toBeCalled();
157
+ });
158
+ });
@@ -0,0 +1,90 @@
1
+ import React from "react";
2
+
3
+ import { ThemeProvider } from "styled-components";
4
+ import { render, cleanup, screen, fireEvent } from "@testing-library/react";
5
+ import { parseTheme } from "@ax/helpers";
6
+ import "@testing-library/jest-dom";
7
+
8
+ import ErrorsBanner from "@ax/components/HeadingsPreviewModal/ErrorsBanner";
9
+ import type { IHeadingError } from "@ax/components/HeadingsPreviewModal/utils";
10
+ import globalTheme from "@ax/themes/theme.json";
11
+
12
+ afterEach(cleanup);
13
+
14
+ const mockError: IHeadingError = {
15
+ message: "No H1 in this page",
16
+ description: "Add an H1 tag",
17
+ headingIds: [],
18
+ };
19
+
20
+ const defaultProps = {
21
+ errors: [mockError],
22
+ onSelectHeading: jest.fn(() => jest.fn()),
23
+ isOpen: false,
24
+ setIsOpen: jest.fn(),
25
+ resetKey: 0,
26
+ };
27
+
28
+ const renderComponent = (props = defaultProps) =>
29
+ render(
30
+ <ThemeProvider theme={parseTheme(globalTheme)}>
31
+ <ErrorsBanner {...props} />
32
+ </ThemeProvider>,
33
+ );
34
+
35
+ describe("ErrorsBanner component rendering", () => {
36
+ it("should render the SEO Alerts header", () => {
37
+ renderComponent();
38
+ expect(screen.getByText("SEO Alerts")).toBeTruthy();
39
+ });
40
+
41
+ it("should render the error message from the errors list", () => {
42
+ renderComponent();
43
+ expect(screen.getByText("No H1 in this page")).toBeTruthy();
44
+ });
45
+
46
+ it("should render multiple errors", () => {
47
+ const props = {
48
+ ...defaultProps,
49
+ errors: [
50
+ { message: "Error one", description: "Desc", headingIds: [] },
51
+ { message: "Error two", description: "Desc", headingIds: [] },
52
+ ],
53
+ };
54
+ renderComponent(props);
55
+ expect(screen.getByText("Error one")).toBeTruthy();
56
+ expect(screen.getByText("Error two")).toBeTruthy();
57
+ });
58
+
59
+ it("should not render when dismissed", () => {
60
+ renderComponent();
61
+ expect(screen.getByText("SEO Alerts")).toBeTruthy();
62
+
63
+ const buttons = screen.getAllByRole("button");
64
+ // Close button is the second button in the header (after toggle)
65
+ fireEvent.click(buttons[1]);
66
+
67
+ expect(screen.queryByText("SEO Alerts")).toBeNull();
68
+ });
69
+ });
70
+
71
+ describe("ErrorsBanner component events", () => {
72
+ it("should call setIsOpen with true when toggle is clicked and isOpen is false", () => {
73
+ const setIsOpenMock = jest.fn();
74
+ renderComponent({ ...defaultProps, isOpen: false, setIsOpen: setIsOpenMock });
75
+
76
+ const buttons = screen.getAllByRole("button");
77
+ // Toggle is the first button in the header
78
+ fireEvent.click(buttons[0]);
79
+ expect(setIsOpenMock).toBeCalledWith(true);
80
+ });
81
+
82
+ it("should call setIsOpen with false when toggle is clicked and isOpen is true", () => {
83
+ const setIsOpenMock = jest.fn();
84
+ renderComponent({ ...defaultProps, isOpen: true, setIsOpen: setIsOpenMock });
85
+
86
+ const buttons = screen.getAllByRole("button");
87
+ fireEvent.click(buttons[0]);
88
+ expect(setIsOpenMock).toBeCalledWith(false);
89
+ });
90
+ });
@@ -0,0 +1,178 @@
1
+ import React from "react";
2
+
3
+ import { ThemeProvider } from "styled-components";
4
+ import { render, cleanup, screen, fireEvent } from "@testing-library/react";
5
+ import { parseTheme } from "@ax/helpers";
6
+ import "@testing-library/jest-dom";
7
+
8
+ import type { HeadingNode } from "@ax/types";
9
+ import HeadingsPreviewModal from "@ax/components/HeadingsPreviewModal";
10
+ import * as headingsUtils from "@ax/components/HeadingsPreviewModal/utils";
11
+ import globalTheme from "@ax/themes/theme.json";
12
+
13
+ jest.mock("@ax/components/HeadingsPreviewModal/utils", () => ({
14
+ ...jest.requireActual("@ax/components/HeadingsPreviewModal/utils"),
15
+ parseHeadingsTree: jest.fn().mockReturnValue([]),
16
+ }));
17
+
18
+ afterEach(cleanup);
19
+
20
+ const createHeadingNode = (
21
+ tag: HeadingNode["tag"],
22
+ text: string,
23
+ level: HeadingNode["level"],
24
+ children: HeadingNode[] = [],
25
+ ): HeadingNode => ({
26
+ tag,
27
+ text,
28
+ level,
29
+ isHidden: false,
30
+ children,
31
+ });
32
+
33
+ const mockHeadings: HeadingNode[] = [
34
+ createHeadingNode("h1", "Main Title", 1, [createHeadingNode("h2", "Subtitle", 2)]),
35
+ ];
36
+
37
+ const browserRef = { current: document.createElement("div") } as React.RefObject<HTMLDivElement>;
38
+
39
+ const defaultProps = {
40
+ isOpen: false,
41
+ browserRef: { current: null } as React.RefObject<HTMLDivElement>,
42
+ toggleModal: jest.fn(),
43
+ headingsFilter: "all" as const,
44
+ setHeadingsFilter: jest.fn(),
45
+ };
46
+
47
+ const renderComponent = (props = defaultProps) =>
48
+ render(
49
+ <ThemeProvider theme={parseTheme(globalTheme)}>
50
+ <HeadingsPreviewModal {...props} />
51
+ </ThemeProvider>,
52
+ );
53
+
54
+ describe("HeadingsPreviewModal component rendering", () => {
55
+ it("should not render the headings content when closed", () => {
56
+ renderComponent();
57
+
58
+ expect(screen.queryByText("Show headings:")).toBeNull();
59
+ expect(screen.queryByTestId("wrapper")).toBeNull();
60
+ });
61
+
62
+ it("should render the empty state when open with no headings", () => {
63
+ renderComponent({ ...defaultProps, isOpen: true });
64
+
65
+ const floatingPanel = screen.getByTestId("floating-panel");
66
+ expect(floatingPanel).toBeTruthy();
67
+
68
+ const emptyState = screen.getByTestId("wrapper");
69
+ expect(emptyState).toBeTruthy();
70
+ expect(emptyState).toHaveTextContent("There are no headings available to display");
71
+ });
72
+
73
+ it("should render heading items when open with headings", () => {
74
+ (headingsUtils.parseHeadingsTree as jest.Mock).mockReturnValueOnce(mockHeadings);
75
+
76
+ renderComponent({ ...defaultProps, isOpen: true, browserRef });
77
+
78
+ const floatingPanel = screen.getByTestId("floating-panel");
79
+ expect(floatingPanel).toBeTruthy();
80
+
81
+ const allButtons = screen.getAllByRole("button");
82
+ const headingItems = allButtons.filter((el) => el.hasAttribute("data-heading-id"));
83
+ expect(headingItems.length).toBeGreaterThan(0);
84
+ });
85
+
86
+ it("should render filter buttons when open with headings", () => {
87
+ (headingsUtils.parseHeadingsTree as jest.Mock).mockReturnValueOnce(mockHeadings);
88
+
89
+ renderComponent({ ...defaultProps, isOpen: true, browserRef });
90
+
91
+ const filterText = screen.getByText("Show headings:");
92
+ expect(filterText).toBeTruthy();
93
+
94
+ const allFilter = screen.getByText("all");
95
+ expect(allFilter).toBeTruthy();
96
+
97
+ const h1Texts = screen.getAllByText("h1");
98
+ const h1FilterButton = h1Texts.find((el) => el.tagName === "BUTTON");
99
+ expect(h1FilterButton).toBeTruthy();
100
+
101
+ const h2Texts = screen.getAllByText("h2");
102
+ const h2FilterButton = h2Texts.find((el) => el.tagName === "BUTTON");
103
+ expect(h2FilterButton).toBeTruthy();
104
+ });
105
+
106
+ it("should render the errors banner when there are SEO errors", () => {
107
+ const headingsWithoutH1: HeadingNode[] = [createHeadingNode("h2", "Subtitle", 2)];
108
+ (headingsUtils.parseHeadingsTree as jest.Mock).mockReturnValueOnce(headingsWithoutH1);
109
+
110
+ renderComponent({ ...defaultProps, isOpen: true, browserRef });
111
+
112
+ const seoAlerts = screen.getByText("SEO Alerts");
113
+ expect(seoAlerts).toBeTruthy();
114
+ });
115
+
116
+ it("should not render the errors banner when there are no SEO errors", () => {
117
+ (headingsUtils.parseHeadingsTree as jest.Mock).mockReturnValueOnce(mockHeadings);
118
+
119
+ renderComponent({ ...defaultProps, isOpen: true, browserRef });
120
+
121
+ const seoAlerts = screen.queryByText("SEO Alerts");
122
+ expect(seoAlerts).toBeNull();
123
+ });
124
+ });
125
+
126
+ describe("HeadingsPreviewModal component events", () => {
127
+ it("should call toggleModal when close button is clicked", () => {
128
+ const toggleModalMock = jest.fn();
129
+
130
+ renderComponent({ ...defaultProps, isOpen: true, toggleModal: toggleModalMock });
131
+
132
+ const closeButton = screen.getByTestId("icon-action-component");
133
+ fireEvent.click(closeButton);
134
+ expect(toggleModalMock).toBeCalled();
135
+ });
136
+
137
+ it("should call setHeadingsFilter when a filter button is clicked", () => {
138
+ const setHeadingsFilterMock = jest.fn();
139
+ (headingsUtils.parseHeadingsTree as jest.Mock).mockReturnValueOnce(mockHeadings);
140
+
141
+ renderComponent({ ...defaultProps, isOpen: true, browserRef, setHeadingsFilter: setHeadingsFilterMock });
142
+
143
+ const h1Texts = screen.getAllByText("h1");
144
+ const h1FilterButton = h1Texts.find((el) => el.tagName === "BUTTON") as HTMLElement;
145
+ expect(h1FilterButton).toBeTruthy();
146
+ fireEvent.click(h1FilterButton);
147
+ expect(setHeadingsFilterMock).toBeCalledWith("h1");
148
+ });
149
+
150
+ it("should select a heading item when clicked", () => {
151
+ (headingsUtils.parseHeadingsTree as jest.Mock).mockReturnValueOnce(mockHeadings);
152
+
153
+ renderComponent({ ...defaultProps, isOpen: true, browserRef });
154
+
155
+ const allButtons = screen.getAllByRole("button");
156
+ const headingItems = allButtons.filter((el) => el.hasAttribute("data-heading-id"));
157
+ expect(headingItems.length).toBeGreaterThan(0);
158
+
159
+ fireEvent.click(headingItems[0]);
160
+
161
+ expect(headingItems[0]).toBeTruthy();
162
+ });
163
+
164
+ it("should deselect a heading item when clicked again", () => {
165
+ (headingsUtils.parseHeadingsTree as jest.Mock).mockReturnValueOnce(mockHeadings);
166
+
167
+ renderComponent({ ...defaultProps, isOpen: true, browserRef });
168
+
169
+ const allButtons = screen.getAllByRole("button");
170
+ const headingItems = allButtons.filter((el) => el.hasAttribute("data-heading-id"));
171
+ expect(headingItems.length).toBeGreaterThan(0);
172
+
173
+ fireEvent.click(headingItems[0]);
174
+ fireEvent.click(headingItems[0]);
175
+
176
+ expect(headingItems[0]).toBeTruthy();
177
+ });
178
+ });