@eeacms/volto-eea-website-theme 3.7.0 → 3.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +19 -2
  2. package/package.json +3 -1
  3. package/src/actions/index.js +1 -0
  4. package/src/actions/navigation.js +24 -0
  5. package/src/actions/print.js +9 -1
  6. package/src/components/manage/Blocks/ContextNavigation/variations/Accordion.jsx +42 -35
  7. package/src/components/manage/Blocks/LayoutSettings/LayoutSettingsEdit.test.jsx +383 -0
  8. package/src/components/manage/Blocks/Title/variations/WebReportPage.test.jsx +232 -0
  9. package/src/components/theme/Banner/View.jsx +11 -92
  10. package/src/components/theme/PrintLoader/PrintLoader.jsx +56 -0
  11. package/src/components/theme/PrintLoader/PrintLoader.test.jsx +91 -0
  12. package/src/components/theme/PrintLoader/style.less +12 -0
  13. package/src/components/theme/WebReport/WebReportSectionView.test.jsx +462 -0
  14. package/src/components/theme/Widgets/ImageViewWidget.test.jsx +26 -0
  15. package/src/components/theme/Widgets/NavigationBehaviorWidget.jsx +601 -0
  16. package/src/components/theme/Widgets/NavigationBehaviorWidget.test.jsx +507 -0
  17. package/src/components/theme/Widgets/SimpleArrayWidget.jsx +183 -0
  18. package/src/components/theme/Widgets/SimpleArrayWidget.test.jsx +283 -0
  19. package/src/constants/ActionTypes.js +2 -0
  20. package/src/customizations/volto/components/manage/History/History.diff +207 -0
  21. package/src/customizations/volto/components/manage/History/History.jsx +444 -0
  22. package/src/customizations/volto/components/theme/Comments/Comments.jsx +9 -2
  23. package/src/customizations/volto/components/theme/Comments/Comments.test.jsx +4 -4
  24. package/src/customizations/volto/components/theme/Comments/comments.less +16 -0
  25. package/src/customizations/volto/components/theme/Header/Header.jsx +60 -1
  26. package/src/customizations/volto/components/theme/View/DefaultView.jsx +42 -33
  27. package/src/customizations/volto/helpers/Html/Html.jsx +212 -0
  28. package/src/customizations/volto/helpers/Html/Readme.md +1 -0
  29. package/src/customizations/volto/server.jsx +375 -0
  30. package/src/helpers/loadLazyImages.js +11 -0
  31. package/src/helpers/loadLazyImages.test.js +22 -0
  32. package/src/helpers/setupPrintView.js +134 -0
  33. package/src/helpers/setupPrintView.test.js +49 -0
  34. package/src/index.js +11 -1
  35. package/src/index.test.js +6 -0
  36. package/src/middleware/voltoCustom.test.js +282 -0
  37. package/src/reducers/index.js +2 -1
  38. package/src/reducers/navigation/navigation.js +47 -0
  39. package/src/reducers/navigation/navigation.test.js +348 -0
  40. package/src/reducers/navigation.js +55 -0
  41. package/src/reducers/print.js +18 -8
  42. package/src/reducers/print.test.js +117 -0
package/CHANGELOG.md CHANGED
@@ -4,11 +4,28 @@ All notable changes to this project will be documented in this file. Dates are d
4
4
 
5
5
  Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
6
6
 
7
- ### [3.7.0](https://github.com/eea/volto-eea-website-theme/compare/3.6.3...3.7.0) - 6 June 2025
7
+ ### [3.9.0](https://github.com/eea/volto-eea-website-theme/compare/3.8.0...3.9.0) - 20 August 2025
8
+
9
+ #### :rocket: New Features
10
+
11
+ - feat: Provide full versioning for indicators and easy access to previous versions - refs #289335 [dobri1408 - [`6cdeb1d`](https://github.com/eea/volto-eea-website-theme/commit/6cdeb1d56b20d63a5c8c3fc5fc9432620053ba89)]
12
+ - feat: Control Navigation Settings TTW - refs #288509 [dobri1408 - [`0e82ddb`](https://github.com/eea/volto-eea-website-theme/commit/0e82ddb7369be52a63a3ac15e22a18940933a1f4)]
13
+
14
+ #### :hammer_and_wrench: Others
15
+
16
+ - Release 3.9.0 [Alin Voinea - [`b19fa21`](https://github.com/eea/volto-eea-website-theme/commit/b19fa2173b646ad105cf2eb2d70aeef480a76356)]
17
+ ### [3.8.0](https://github.com/eea/volto-eea-website-theme/compare/3.7.0...3.8.0) - 7 July 2025
18
+
19
+ #### :rocket: New Features
20
+
21
+ - feat(side-nav): allow content to be inserted before instead of the default append [David Ichim - [`6e184d2`](https://github.com/eea/volto-eea-website-theme/commit/6e184d2dfff0ee693b2d27527fa538dce8bdede7)]
8
22
 
9
23
  #### :hammer_and_wrench: Others
10
24
 
11
- - Release 3.7.0 [Alin Voinea - [`b4c7cc5`](https://github.com/eea/volto-eea-website-theme/commit/b4c7cc5218a5ed04d2a53c75d2dfdac7c7959bd5)]
25
+ - Update package.json [David Ichim - [`af355dd`](https://github.com/eea/volto-eea-website-theme/commit/af355dda36692e048779abc80d968096a0cc3c49)]
26
+ - add dependency on volto-anchors since we have a require which we catch [David Ichim - [`135db9f`](https://github.com/eea/volto-eea-website-theme/commit/135db9fc4d50ef535ef80a499ddd3c9d87dabac6)]
27
+ ### [3.7.0](https://github.com/eea/volto-eea-website-theme/compare/3.6.3...3.7.0) - 6 June 2025
28
+
12
29
  ### [3.6.3](https://github.com/eea/volto-eea-website-theme/compare/3.6.2...3.6.3) - 2 June 2025
13
30
 
14
31
  #### :bug: Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eeacms/volto-eea-website-theme",
3
- "version": "3.7.0",
3
+ "version": "3.9.0",
4
4
  "description": "@eeacms/volto-eea-website-theme: Volto add-on",
5
5
  "main": "src/index.js",
6
6
  "author": "European Environment Agency: IDM2 A-Team",
@@ -14,6 +14,7 @@
14
14
  "react"
15
15
  ],
16
16
  "addons": [
17
+ "@eeacms/volto-anchors",
17
18
  "@eeacms/volto-block-toc",
18
19
  "@eeacms/volto-group-block",
19
20
  "@eeacms/volto-eea-design-system",
@@ -24,6 +25,7 @@
24
25
  "url": "git@github.com:eea/volto-eea-website-theme.git"
25
26
  },
26
27
  "dependencies": {
28
+ "@eeacms/volto-anchors": "*",
27
29
  "@eeacms/volto-block-style": "*",
28
30
  "@eeacms/volto-block-toc": "*",
29
31
  "@eeacms/volto-eea-design-system": "*",
@@ -1 +1,2 @@
1
1
  export * from './print';
2
+ export * from './navigation';
@@ -0,0 +1,24 @@
1
+ import { flattenToAppURL } from '@plone/volto/helpers';
2
+ import { GET_NAVIGATION_SETTINGS } from '../constants/ActionTypes';
3
+
4
+ export const getNavigationSettings = (url = '') => {
5
+ let cleanedUrl = typeof url === 'string' ? url : '';
6
+
7
+ if (
8
+ cleanedUrl &&
9
+ typeof cleanedUrl === 'string' &&
10
+ cleanedUrl.endsWith('/edit')
11
+ ) {
12
+ cleanedUrl = cleanedUrl.slice(0, -'/edit'.length);
13
+ }
14
+
15
+ const apiPath = flattenToAppURL(cleanedUrl);
16
+
17
+ return {
18
+ type: GET_NAVIGATION_SETTINGS,
19
+ request: {
20
+ op: 'get',
21
+ path: `${apiPath}/@inherit?expand.inherit.behaviors=eea.enhanced_navigation`,
22
+ },
23
+ };
24
+ };
@@ -3,7 +3,10 @@
3
3
  * @module actions/print
4
4
  */
5
5
 
6
- import { SET_ISPRINT } from '@eeacms/volto-eea-website-theme/constants/ActionTypes';
6
+ import {
7
+ SET_ISPRINT,
8
+ SET_PRINT_LOADING,
9
+ } from '@eeacms/volto-eea-website-theme/constants/ActionTypes';
7
10
 
8
11
  export const setIsPrint = (data) => {
9
12
  return {
@@ -11,3 +14,8 @@ export const setIsPrint = (data) => {
11
14
  payload: data,
12
15
  };
13
16
  };
17
+
18
+ export const setPrintLoading = (isLoading) => ({
19
+ type: SET_PRINT_LOADING,
20
+ payload: isLoading,
21
+ });
@@ -8,7 +8,7 @@ import { Accordion, Icon } from 'semantic-ui-react';
8
8
 
9
9
  import Slugger from 'github-slugger';
10
10
 
11
- import { UniversalLink, MaybeWrap } from '@plone/volto/components';
11
+ import { UniversalLink } from '@plone/volto/components';
12
12
  import { withContentNavigation } from '@plone/volto/components/theme/Navigation/withContentNavigation';
13
13
  import withEEASideMenu from '@eeacms/volto-block-toc/hocs/withEEASideMenu';
14
14
  import { flattenToAppURL } from '@plone/volto/helpers';
@@ -24,10 +24,12 @@ const AccordionNavigation = ({
24
24
  navigation = {},
25
25
  device,
26
26
  isMenuOpenOnOutsideClick,
27
+ hasWideContent,
27
28
  }) => {
28
29
  const { items = [], title, has_custom_name } = navigation;
29
30
  const intl = useIntl();
30
- const navOpen = ['mobile', 'tablet'].includes(device) ? false : true;
31
+ const navOpen =
32
+ ['mobile', 'tablet'].includes(device) || hasWideContent ? false : true;
31
33
  const [isNavOpen, setIsNavOpen] = React.useState(navOpen);
32
34
  const [activeItems, setActiveItems] = React.useState({});
33
35
  const contextNavigationListRef = React.useRef(null);
@@ -42,6 +44,10 @@ const AccordionNavigation = ({
42
44
  if (isMenuOpenOnOutsideClick === false) setIsNavOpen(false);
43
45
  }, [isMenuOpenOnOutsideClick]);
44
46
 
47
+ React.useEffect(() => {
48
+ setIsNavOpen(navOpen);
49
+ }, [navOpen]);
50
+
45
51
  React.useEffect(() => {
46
52
  if (!navOpen) {
47
53
  const handleOutsideClick = (event) => {
@@ -72,7 +78,7 @@ const AccordionNavigation = ({
72
78
  [onClickSummary],
73
79
  );
74
80
 
75
- const renderItems = ({ item, level = 0 }) => {
81
+ const renderItems = ({ item, level = 0, index }) => {
76
82
  const {
77
83
  title,
78
84
  href,
@@ -83,6 +89,7 @@ const AccordionNavigation = ({
83
89
  } = item;
84
90
  const hasChildItems = childItems && childItems.length > 0;
85
91
  const normalizedTitle = Slugger.slug(title);
92
+ const firstItem = index === 0;
86
93
 
87
94
  const checkIfActive = () => {
88
95
  return activeItems[href] !== undefined ? activeItems[href] : is_in_path;
@@ -126,7 +133,7 @@ const AccordionNavigation = ({
126
133
  aria-labelledby={`accordion-title-${normalizedTitle}`}
127
134
  role="region"
128
135
  >
129
- <ul className="accordion-list">
136
+ <ul className={`accordion-list ${'level-' + level}`}>
130
137
  {childItems.map((child) =>
131
138
  renderItems({ item: child, level: level + 1 }),
132
139
  )}
@@ -139,8 +146,10 @@ const AccordionNavigation = ({
139
146
  className={cx(`title-link contenttype-${type}`, {
140
147
  current: is_current,
141
148
  in_path: is_in_path,
149
+ navigation_home: firstItem,
142
150
  })}
143
151
  >
152
+ {firstItem && <Icon className={'ri-home-2-line'} />}
144
153
  {title}
145
154
  </UniversalLink>
146
155
  )}
@@ -159,31 +168,21 @@ const AccordionNavigation = ({
159
168
  onKeyDown={onKeyDownSummary}
160
169
  ref={summaryRef}
161
170
  >
162
- <MaybeWrap
163
- condition={!navOpen}
164
- className="ui container d-flex flex-items-center"
165
- >
166
- {has_custom_name
167
- ? title
168
- : intl.formatMessage(messages.navigation)}
169
- <Icon
170
- className={
171
- isNavOpen ? 'ri-arrow-up-s-line' : 'ri-arrow-down-s-line'
172
- }
173
- />
174
- </MaybeWrap>
171
+ {has_custom_name ? title : intl.formatMessage(messages.navigation)}
172
+ <Icon
173
+ className={
174
+ isNavOpen ? 'ri-arrow-up-s-line' : 'ri-arrow-down-s-line'
175
+ }
176
+ />
175
177
  </summary>
176
- <MaybeWrap
177
- condition={!navOpen}
178
- className="ui container d-flex flex-items-center"
178
+ <ul
179
+ className="context-navigation-list accordion-list"
180
+ ref={contextNavigationListRef}
179
181
  >
180
- <ul
181
- className="context-navigation-list accordion-list"
182
- ref={contextNavigationListRef}
183
- >
184
- {items.map((item) => renderItems({ item }))}
185
- </ul>
186
- </MaybeWrap>
182
+ {items.map((item, index) =>
183
+ renderItems({ item, level: 0, index: index }),
184
+ )}
185
+ </ul>
187
186
  </details>
188
187
  </nav>
189
188
  </>
@@ -209,17 +208,25 @@ AccordionNavigation.propTypes = {
209
208
  has_custom_name: PropTypes.bool,
210
209
  title: PropTypes.string,
211
210
  }),
211
+ /**
212
+ * Provided by withEEASideMenu HOC to indicate if page has wide content
213
+ */
214
+ hasWideContent: PropTypes.bool,
212
215
  };
213
216
 
214
217
  export default compose(
215
218
  withRouter,
216
219
  withContentNavigation,
217
- (WrappedComponent) => (props) =>
218
- withEEASideMenu(WrappedComponent)({
219
- ...props,
220
- targetParent: '.eea.header ',
221
- fixedVisibilitySwitchTarget: '.main.bar',
222
- insertBeforeOnMobile: '.banner',
223
- shouldRender: props.navigation?.items?.length > 0,
224
- }),
220
+ (WrappedComponent) => (props) => {
221
+ const Enhanced = withEEASideMenu(WrappedComponent);
222
+ return (
223
+ <Enhanced
224
+ {...props}
225
+ targetParent=".eea.header"
226
+ fixedVisibilitySwitchTarget=".main.bar"
227
+ insertBeforeOnMobile=".banner"
228
+ shouldRender={Boolean(props.navigation?.items?.length)}
229
+ />
230
+ );
231
+ },
225
232
  )(AccordionNavigation);
@@ -0,0 +1,383 @@
1
+ import React from 'react';
2
+ import { render, screen, fireEvent } from '@testing-library/react';
3
+ import LayoutSettingsEdit from './LayoutSettingsEdit';
4
+ import { EditSchema } from './schema';
5
+
6
+ // Add jest-dom matchers
7
+ import '@testing-library/jest-dom';
8
+
9
+ // Mock dependencies
10
+ jest.mock('./schema', () => ({
11
+ EditSchema: jest.fn(),
12
+ }));
13
+
14
+ jest.mock('@plone/volto/components', () => ({
15
+ BlockDataForm: jest.fn(({ title, schema, formData, onChangeField }) => (
16
+ <div data-testid="block-data-form">
17
+ <div data-testid="form-title">{title}</div>
18
+ <div data-testid="form-schema">{JSON.stringify(schema)}</div>
19
+ <div data-testid="form-data">{JSON.stringify(formData)}</div>
20
+ <button
21
+ data-testid="change-field-button"
22
+ onClick={() => onChangeField('test-field', 'test-value')}
23
+ >
24
+ Change Field
25
+ </button>
26
+ </div>
27
+ )),
28
+ SidebarPortal: jest.fn(({ selected, children }) => (
29
+ <div data-testid="sidebar-portal" data-selected={selected}>
30
+ {children}
31
+ </div>
32
+ )),
33
+ }));
34
+
35
+ jest.mock('./LayoutSettingsView', () => ({
36
+ __esModule: true,
37
+ default: jest.fn((props) => (
38
+ <div data-testid="layout-settings-view" data-props={JSON.stringify(props)}>
39
+ Layout Settings View
40
+ </div>
41
+ )),
42
+ }));
43
+
44
+ const mockLayoutSettingsView = require('./LayoutSettingsView').default;
45
+
46
+ const { BlockDataForm, SidebarPortal } = require('@plone/volto/components');
47
+
48
+ describe('LayoutSettingsEdit', () => {
49
+ const mockSchema = {
50
+ title: 'Page layout settings',
51
+ fieldsets: [
52
+ {
53
+ id: 'default',
54
+ title: 'Default',
55
+ fields: ['layout_size', 'body_class'],
56
+ },
57
+ ],
58
+ properties: {
59
+ layout_size: {
60
+ widget: 'style_align',
61
+ title: 'Layout size',
62
+ },
63
+ body_class: {
64
+ title: 'Body class',
65
+ widget: 'creatable_select',
66
+ },
67
+ },
68
+ };
69
+
70
+ const defaultProps = {
71
+ selected: true,
72
+ block: 'block-id',
73
+ data: {
74
+ layout_size: 'container_view',
75
+ body_class: 'homepage',
76
+ },
77
+ onChangeBlock: jest.fn(),
78
+ };
79
+
80
+ beforeEach(() => {
81
+ EditSchema.mockReturnValue(mockSchema);
82
+ jest.clearAllMocks();
83
+ mockLayoutSettingsView.mockClear();
84
+ });
85
+
86
+ it('renders without crashing', () => {
87
+ const { container } = render(<LayoutSettingsEdit {...defaultProps} />);
88
+ expect(container).toBeTruthy();
89
+ });
90
+
91
+ it('renders the page title', () => {
92
+ render(<LayoutSettingsEdit {...defaultProps} />);
93
+ expect(screen.getByRole('heading', { level: 3 })).toHaveTextContent(
94
+ 'Page layout settings',
95
+ );
96
+ });
97
+
98
+ it('renders LayoutSettingsView with correct props', () => {
99
+ render(<LayoutSettingsEdit {...defaultProps} />);
100
+
101
+ expect(mockLayoutSettingsView).toHaveBeenCalledWith(defaultProps, {});
102
+ expect(screen.getByTestId('layout-settings-view')).toBeInTheDocument();
103
+ });
104
+
105
+ it('renders SidebarPortal with correct selected prop', () => {
106
+ render(<LayoutSettingsEdit {...defaultProps} />);
107
+
108
+ expect(SidebarPortal).toHaveBeenCalledWith(
109
+ expect.objectContaining({
110
+ selected: true,
111
+ }),
112
+ {},
113
+ );
114
+ expect(screen.getByTestId('sidebar-portal')).toHaveAttribute(
115
+ 'data-selected',
116
+ 'true',
117
+ );
118
+ });
119
+
120
+ it('renders SidebarPortal with selected false', () => {
121
+ const props = { ...defaultProps, selected: false };
122
+ render(<LayoutSettingsEdit {...props} />);
123
+
124
+ expect(SidebarPortal).toHaveBeenCalledWith(
125
+ expect.objectContaining({
126
+ selected: false,
127
+ }),
128
+ {},
129
+ );
130
+ expect(screen.getByTestId('sidebar-portal')).toHaveAttribute(
131
+ 'data-selected',
132
+ 'false',
133
+ );
134
+ });
135
+
136
+ it('renders BlockDataForm when selected is true', () => {
137
+ render(<LayoutSettingsEdit {...defaultProps} />);
138
+
139
+ expect(BlockDataForm).toHaveBeenCalledWith(
140
+ expect.objectContaining({
141
+ title: mockSchema.title,
142
+ schema: mockSchema,
143
+ formData: defaultProps.data,
144
+ onChangeField: expect.any(Function),
145
+ }),
146
+ {},
147
+ );
148
+ expect(screen.getByTestId('block-data-form')).toBeInTheDocument();
149
+ });
150
+
151
+ it('does not render BlockDataForm when selected is false', () => {
152
+ const props = { ...defaultProps, selected: false };
153
+ render(<LayoutSettingsEdit {...props} />);
154
+
155
+ expect(BlockDataForm).not.toHaveBeenCalled();
156
+ expect(screen.queryByTestId('block-data-form')).not.toBeInTheDocument();
157
+ });
158
+
159
+ it('calls EditSchema to get schema', () => {
160
+ render(<LayoutSettingsEdit {...defaultProps} />);
161
+
162
+ expect(EditSchema).toHaveBeenCalled();
163
+ });
164
+
165
+ it('passes correct title to BlockDataForm', () => {
166
+ render(<LayoutSettingsEdit {...defaultProps} />);
167
+
168
+ expect(screen.getByTestId('form-title')).toHaveTextContent(
169
+ 'Page layout settings',
170
+ );
171
+ });
172
+
173
+ it('passes correct schema to BlockDataForm', () => {
174
+ render(<LayoutSettingsEdit {...defaultProps} />);
175
+
176
+ expect(screen.getByTestId('form-schema')).toHaveTextContent(
177
+ JSON.stringify(mockSchema),
178
+ );
179
+ });
180
+
181
+ it('passes correct formData to BlockDataForm', () => {
182
+ render(<LayoutSettingsEdit {...defaultProps} />);
183
+
184
+ expect(screen.getByTestId('form-data')).toHaveTextContent(
185
+ JSON.stringify(defaultProps.data),
186
+ );
187
+ });
188
+
189
+ it('handles onChangeField correctly', () => {
190
+ render(<LayoutSettingsEdit {...defaultProps} />);
191
+
192
+ const changeButton = screen.getByTestId('change-field-button');
193
+ fireEvent.click(changeButton);
194
+
195
+ expect(defaultProps.onChangeBlock).toHaveBeenCalledWith('block-id', {
196
+ ...defaultProps.data,
197
+ 'test-field': 'test-value',
198
+ });
199
+ });
200
+
201
+ it('preserves existing data when changing field', () => {
202
+ const propsWithData = {
203
+ ...defaultProps,
204
+ data: {
205
+ layout_size: 'wide_view',
206
+ body_class: 'homepage-inverse',
207
+ existing_field: 'existing_value',
208
+ },
209
+ };
210
+
211
+ render(<LayoutSettingsEdit {...propsWithData} />);
212
+
213
+ const changeButton = screen.getByTestId('change-field-button');
214
+ fireEvent.click(changeButton);
215
+
216
+ expect(defaultProps.onChangeBlock).toHaveBeenCalledWith('block-id', {
217
+ layout_size: 'wide_view',
218
+ body_class: 'homepage-inverse',
219
+ existing_field: 'existing_value',
220
+ 'test-field': 'test-value',
221
+ });
222
+ });
223
+
224
+ it('handles empty data object', () => {
225
+ const propsWithEmptyData = {
226
+ ...defaultProps,
227
+ data: {},
228
+ };
229
+
230
+ render(<LayoutSettingsEdit {...propsWithEmptyData} />);
231
+
232
+ const changeButton = screen.getByTestId('change-field-button');
233
+ fireEvent.click(changeButton);
234
+
235
+ expect(defaultProps.onChangeBlock).toHaveBeenCalledWith('block-id', {
236
+ 'test-field': 'test-value',
237
+ });
238
+ });
239
+
240
+ it('handles null data', () => {
241
+ const propsWithNullData = {
242
+ ...defaultProps,
243
+ data: null,
244
+ };
245
+
246
+ render(<LayoutSettingsEdit {...propsWithNullData} />);
247
+
248
+ const changeButton = screen.getByTestId('change-field-button');
249
+ fireEvent.click(changeButton);
250
+
251
+ expect(defaultProps.onChangeBlock).toHaveBeenCalledWith('block-id', {
252
+ 'test-field': 'test-value',
253
+ });
254
+ });
255
+
256
+ it('handles undefined data', () => {
257
+ const propsWithUndefinedData = {
258
+ ...defaultProps,
259
+ data: undefined,
260
+ };
261
+
262
+ render(<LayoutSettingsEdit {...propsWithUndefinedData} />);
263
+
264
+ const changeButton = screen.getByTestId('change-field-button');
265
+ fireEvent.click(changeButton);
266
+
267
+ expect(defaultProps.onChangeBlock).toHaveBeenCalledWith('block-id', {
268
+ 'test-field': 'test-value',
269
+ });
270
+ });
271
+
272
+ it('works with different block IDs', () => {
273
+ const propsWithDifferentBlock = {
274
+ ...defaultProps,
275
+ block: 'different-block-id',
276
+ };
277
+
278
+ render(<LayoutSettingsEdit {...propsWithDifferentBlock} />);
279
+
280
+ const changeButton = screen.getByTestId('change-field-button');
281
+ fireEvent.click(changeButton);
282
+
283
+ expect(defaultProps.onChangeBlock).toHaveBeenCalledWith(
284
+ 'different-block-id',
285
+ {
286
+ ...defaultProps.data,
287
+ 'test-field': 'test-value',
288
+ },
289
+ );
290
+ });
291
+
292
+ it('handles different schema configurations', () => {
293
+ const customSchema = {
294
+ title: 'Custom Layout Settings',
295
+ fieldsets: [
296
+ {
297
+ id: 'advanced',
298
+ title: 'Advanced',
299
+ fields: ['custom_field'],
300
+ },
301
+ ],
302
+ properties: {
303
+ custom_field: {
304
+ title: 'Custom Field',
305
+ widget: 'text',
306
+ },
307
+ },
308
+ };
309
+
310
+ EditSchema.mockReturnValue(customSchema);
311
+
312
+ render(<LayoutSettingsEdit {...defaultProps} />);
313
+
314
+ expect(screen.getByTestId('form-title')).toHaveTextContent(
315
+ 'Custom Layout Settings',
316
+ );
317
+ expect(screen.getByTestId('form-schema')).toHaveTextContent(
318
+ JSON.stringify(customSchema),
319
+ );
320
+ });
321
+
322
+ it('passes all props to LayoutSettingsView', () => {
323
+ const customProps = {
324
+ ...defaultProps,
325
+ customProp: 'custom-value',
326
+ anotherProp: { nested: 'object' },
327
+ };
328
+
329
+ render(<LayoutSettingsEdit {...customProps} />);
330
+
331
+ expect(mockLayoutSettingsView).toHaveBeenCalledWith(customProps, {});
332
+ });
333
+
334
+ it('renders all components in correct structure', () => {
335
+ const { container } = render(<LayoutSettingsEdit {...defaultProps} />);
336
+
337
+ // Check that h3 is rendered
338
+ expect(container.querySelector('h3')).toHaveTextContent(
339
+ 'Page layout settings',
340
+ );
341
+
342
+ // Check that LayoutSettingsView is rendered
343
+ expect(screen.getByTestId('layout-settings-view')).toBeInTheDocument();
344
+
345
+ // Check that SidebarPortal is rendered
346
+ expect(screen.getByTestId('sidebar-portal')).toBeInTheDocument();
347
+
348
+ // Check that BlockDataForm is rendered inside SidebarPortal when selected
349
+ expect(screen.getByTestId('block-data-form')).toBeInTheDocument();
350
+ });
351
+
352
+ it('handles re-renders correctly', () => {
353
+ const { rerender } = render(<LayoutSettingsEdit {...defaultProps} />);
354
+
355
+ expect(EditSchema).toHaveBeenCalledTimes(1);
356
+
357
+ // Re-render with different props
358
+ const newProps = { ...defaultProps, selected: false };
359
+ rerender(<LayoutSettingsEdit {...newProps} />);
360
+
361
+ expect(EditSchema).toHaveBeenCalledTimes(2);
362
+ expect(screen.queryByTestId('block-data-form')).not.toBeInTheDocument();
363
+ });
364
+
365
+ it('handles onChangeBlock function prop correctly', () => {
366
+ const mockOnChangeBlock = jest.fn();
367
+ const propsWithMockFunction = {
368
+ ...defaultProps,
369
+ onChangeBlock: mockOnChangeBlock,
370
+ };
371
+
372
+ render(<LayoutSettingsEdit {...propsWithMockFunction} />);
373
+
374
+ const changeButton = screen.getByTestId('change-field-button');
375
+ fireEvent.click(changeButton);
376
+
377
+ expect(mockOnChangeBlock).toHaveBeenCalledWith('block-id', {
378
+ ...defaultProps.data,
379
+ 'test-field': 'test-value',
380
+ });
381
+ expect(defaultProps.onChangeBlock).not.toHaveBeenCalled();
382
+ });
383
+ });