@sap-ux/control-property-editor 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/.eslintignore +1 -0
  2. package/.eslintrc.js +16 -0
  3. package/CHANGELOG.md +7 -0
  4. package/LICENSE +201 -0
  5. package/README.md +16 -0
  6. package/dist/app.css +2 -0
  7. package/dist/app.css.map +7 -0
  8. package/dist/app.js +347 -0
  9. package/dist/app.js.map +7 -0
  10. package/esbuild.js +25 -0
  11. package/jest.config.js +20 -0
  12. package/package.json +68 -0
  13. package/src/App.scss +57 -0
  14. package/src/App.tsx +136 -0
  15. package/src/Workarounds.scss +79 -0
  16. package/src/actions.ts +3 -0
  17. package/src/components/AppLogo.module.scss +8 -0
  18. package/src/components/AppLogo.tsx +75 -0
  19. package/src/components/ChangeIndicator.tsx +80 -0
  20. package/src/components/Separator.tsx +32 -0
  21. package/src/components/ThemeSelectorCallout.scss +48 -0
  22. package/src/components/ThemeSelectorCallout.tsx +125 -0
  23. package/src/components/ToolBar.scss +39 -0
  24. package/src/components/ToolBar.tsx +26 -0
  25. package/src/components/index.ts +4 -0
  26. package/src/devices.ts +18 -0
  27. package/src/global.d.ts +4 -0
  28. package/src/i18n/i18n.json +68 -0
  29. package/src/i18n.ts +25 -0
  30. package/src/icons.tsx +198 -0
  31. package/src/index.css +1288 -0
  32. package/src/index.tsx +47 -0
  33. package/src/middleware.ts +54 -0
  34. package/src/panels/LeftPanel.scss +17 -0
  35. package/src/panels/LeftPanel.tsx +48 -0
  36. package/src/panels/changes/ChangeStack.module.scss +3 -0
  37. package/src/panels/changes/ChangeStack.tsx +219 -0
  38. package/src/panels/changes/ChangeStackHeader.tsx +43 -0
  39. package/src/panels/changes/ChangesPanel.module.scss +18 -0
  40. package/src/panels/changes/ChangesPanel.tsx +90 -0
  41. package/src/panels/changes/ControlGroup.module.scss +17 -0
  42. package/src/panels/changes/ControlGroup.tsx +61 -0
  43. package/src/panels/changes/PropertyChange.module.scss +24 -0
  44. package/src/panels/changes/PropertyChange.tsx +159 -0
  45. package/src/panels/changes/UnknownChange.module.scss +46 -0
  46. package/src/panels/changes/UnknownChange.tsx +96 -0
  47. package/src/panels/changes/index.tsx +3 -0
  48. package/src/panels/changes/utils.ts +36 -0
  49. package/src/panels/index.ts +2 -0
  50. package/src/panels/outline/Funnel.tsx +64 -0
  51. package/src/panels/outline/NoControlFound.tsx +45 -0
  52. package/src/panels/outline/OutlinePanel.scss +98 -0
  53. package/src/panels/outline/OutlinePanel.tsx +38 -0
  54. package/src/panels/outline/Tree.tsx +393 -0
  55. package/src/panels/outline/index.ts +1 -0
  56. package/src/panels/outline/utils.ts +154 -0
  57. package/src/panels/properties/Clipboard.tsx +44 -0
  58. package/src/panels/properties/DeviceSelector.tsx +40 -0
  59. package/src/panels/properties/DeviceToggle.tsx +39 -0
  60. package/src/panels/properties/DropdownEditor.tsx +80 -0
  61. package/src/panels/properties/Funnel.tsx +64 -0
  62. package/src/panels/properties/HeaderField.tsx +150 -0
  63. package/src/panels/properties/IconValueHelp.tsx +203 -0
  64. package/src/panels/properties/InputTypeSelector.tsx +20 -0
  65. package/src/panels/properties/InputTypeToggle.module.scss +4 -0
  66. package/src/panels/properties/InputTypeToggle.tsx +79 -0
  67. package/src/panels/properties/InputTypeWrapper.tsx +259 -0
  68. package/src/panels/properties/NoControlSelected.tsx +38 -0
  69. package/src/panels/properties/Properties.scss +102 -0
  70. package/src/panels/properties/PropertiesList.tsx +162 -0
  71. package/src/panels/properties/PropertiesPanel.tsx +30 -0
  72. package/src/panels/properties/PropertyDocumentation.module.scss +81 -0
  73. package/src/panels/properties/PropertyDocumentation.tsx +174 -0
  74. package/src/panels/properties/SapUiIcon.scss +109 -0
  75. package/src/panels/properties/StringEditor.tsx +122 -0
  76. package/src/panels/properties/ViewChanger.module.scss +5 -0
  77. package/src/panels/properties/ViewChanger.tsx +143 -0
  78. package/src/panels/properties/constants.ts +2 -0
  79. package/src/panels/properties/index.tsx +1 -0
  80. package/src/panels/properties/propertyValuesCache.ts +39 -0
  81. package/src/panels/properties/types.ts +49 -0
  82. package/src/slice.ts +216 -0
  83. package/src/store.ts +19 -0
  84. package/src/use-local-storage.ts +40 -0
  85. package/src/use-window-size.ts +39 -0
  86. package/src/variables.scss +2 -0
  87. package/test/unit/App.test.tsx +207 -0
  88. package/test/unit/appIndex.test.ts +23 -0
  89. package/test/unit/components/ChangeIndicator.test.tsx +120 -0
  90. package/test/unit/components/ThemeSelector.test.tsx +41 -0
  91. package/test/unit/middleware.test.ts +116 -0
  92. package/test/unit/panels/changes/ChangesPanel.test.tsx +261 -0
  93. package/test/unit/panels/changes/utils.test.ts +40 -0
  94. package/test/unit/panels/outline/OutlinePanel.test.tsx +353 -0
  95. package/test/unit/panels/outline/__snapshots__/utils.test.ts.snap +36 -0
  96. package/test/unit/panels/outline/utils.test.ts +83 -0
  97. package/test/unit/panels/properties/Clipboard.test.tsx +18 -0
  98. package/test/unit/panels/properties/DropdownEditor.test.tsx +62 -0
  99. package/test/unit/panels/properties/Funnel.test.tsx +34 -0
  100. package/test/unit/panels/properties/HeaderField.test.tsx +36 -0
  101. package/test/unit/panels/properties/IconValueHelp.test.tsx +60 -0
  102. package/test/unit/panels/properties/InputTypeToggle.test.tsx +126 -0
  103. package/test/unit/panels/properties/InputTypeWrapper.test.tsx +430 -0
  104. package/test/unit/panels/properties/PropertyDocumentation.test.tsx +131 -0
  105. package/test/unit/panels/properties/StringEditor.test.tsx +107 -0
  106. package/test/unit/panels/properties/ViewChanger.test.tsx +190 -0
  107. package/test/unit/panels/properties/propertyValuesCache.test.ts +23 -0
  108. package/test/unit/slice.test.ts +268 -0
  109. package/test/unit/utils.tsx +67 -0
  110. package/test/utils/utils.tsx +25 -0
  111. package/tsconfig.eslint.json +4 -0
  112. package/tsconfig.json +39 -0
package/src/slice.ts ADDED
@@ -0,0 +1,216 @@
1
+ import type { SliceCaseReducers } from '@reduxjs/toolkit';
2
+ import { createSlice, createAction } from '@reduxjs/toolkit';
3
+
4
+ import type {
5
+ Control,
6
+ IconDetails,
7
+ OutlineNode,
8
+ PendingPropertyChange,
9
+ PropertyChange,
10
+ SavedPropertyChange
11
+ } from '@sap-ux-private/control-property-editor-common';
12
+ import {
13
+ changeStackModified,
14
+ controlSelected,
15
+ iconsLoaded,
16
+ outlineChanged,
17
+ propertyChanged,
18
+ propertyChangeFailed
19
+ } from '@sap-ux-private/control-property-editor-common';
20
+ import { DeviceType } from './devices';
21
+
22
+ interface SliceState {
23
+ deviceType: DeviceType;
24
+ scale: number;
25
+
26
+ /**
27
+ * If set to true on resize the preview will be scaled to fit available space
28
+ */
29
+ fitPreview?: boolean;
30
+ selectedControl: Control | undefined;
31
+ outline: OutlineNode[];
32
+ filterQuery: FilterOptions[];
33
+ icons: IconDetails[];
34
+ changes: ChangesSlice;
35
+ }
36
+
37
+ export interface ChangesSlice {
38
+ controls: ControlChanges;
39
+ pending: PendingPropertyChange[];
40
+ saved: SavedPropertyChange[];
41
+ }
42
+ export interface ControlChanges {
43
+ [id: string]: ControlChangeStats;
44
+ }
45
+
46
+ export interface ControlChangeStats {
47
+ controlName: string;
48
+ pending: number;
49
+ saved: number;
50
+ properties: PropertyChanges;
51
+ }
52
+
53
+ export interface PropertyChanges {
54
+ [id: string]: PropertyChangeStats;
55
+ }
56
+ export interface PropertyChangeStats {
57
+ pending: number;
58
+ saved: number;
59
+ lastSavedChange?: SavedPropertyChange;
60
+ lastChange?: PendingPropertyChange;
61
+ }
62
+
63
+ export const enum FilterName {
64
+ focusEditable = 'focus-editable-controls',
65
+ focusCommonlyUsed = 'focus-commonly-used-controls',
66
+ query = 'query',
67
+ changeSummaryFilterQuery = 'change-summary-filter-query',
68
+ showEditableProperties = 'show-editable-properties'
69
+ }
70
+ export interface FilterOptions {
71
+ name: FilterName;
72
+ value: string | boolean;
73
+ }
74
+ const filterInitOptions: FilterOptions[] = [
75
+ { name: FilterName.focusEditable, value: true },
76
+ { name: FilterName.focusCommonlyUsed, value: true },
77
+ { name: FilterName.query, value: '' },
78
+ { name: FilterName.changeSummaryFilterQuery, value: '' },
79
+ { name: FilterName.showEditableProperties, value: true }
80
+ ];
81
+
82
+ export const changeProperty = createAction<PropertyChange>('app/change-property');
83
+ export const changePreviewScale = createAction<number>('app/change-preview-scale');
84
+ export const changePreviewScaleMode = createAction<'fit' | 'fixed'>('app/change-preview-scale-mode');
85
+ export const changeDeviceType = createAction<DeviceType>('app/change-device-type');
86
+ export const filterNodes = createAction<FilterOptions[]>('app/filter-nodes');
87
+
88
+ export const initialState = {
89
+ deviceType: DeviceType.Desktop,
90
+ scale: 1.0,
91
+ selectedControl: undefined,
92
+ outline: [],
93
+ filterQuery: filterInitOptions,
94
+ icons: [],
95
+ changes: {
96
+ controls: {},
97
+ pending: [],
98
+ saved: []
99
+ }
100
+ };
101
+ const slice = createSlice<SliceState, SliceCaseReducers<SliceState>, string>({
102
+ name: 'app',
103
+ initialState,
104
+ reducers: {},
105
+ extraReducers: (builder) =>
106
+ builder
107
+ .addMatcher(outlineChanged.match, (state, action: ReturnType<typeof outlineChanged>): void => {
108
+ state.outline = action.payload;
109
+ })
110
+ .addMatcher(controlSelected.match, (state, action: ReturnType<typeof controlSelected>): void => {
111
+ state.selectedControl = action.payload;
112
+ })
113
+ .addMatcher(changeDeviceType.match, (state, action: ReturnType<typeof changeDeviceType>): void => {
114
+ state.deviceType = action.payload;
115
+ })
116
+ .addMatcher(changePreviewScale.match, (state, action: ReturnType<typeof changePreviewScale>): void => {
117
+ state.scale = action.payload;
118
+ })
119
+ .addMatcher(
120
+ changePreviewScaleMode.match,
121
+ (state, action: ReturnType<typeof changePreviewScaleMode>): void => {
122
+ state.fitPreview = action.payload === 'fit';
123
+ }
124
+ )
125
+ .addMatcher(iconsLoaded.match, (state, action: ReturnType<typeof iconsLoaded>): void => {
126
+ state.icons = action.payload;
127
+ })
128
+ .addMatcher(changeProperty.match, (state, action: ReturnType<typeof changeProperty>): void => {
129
+ if (state.selectedControl?.id === action.payload.controlId) {
130
+ const index = state.selectedControl?.properties.findIndex(
131
+ (property) => property.name === action.payload.propertyName
132
+ );
133
+ if (index !== -1) {
134
+ state.selectedControl.properties[index].value = action.payload.value;
135
+ state.selectedControl.properties[index].errorMessage = '';
136
+ }
137
+ }
138
+ })
139
+ .addMatcher(propertyChanged.match, (state, action: ReturnType<typeof propertyChanged>): void => {
140
+ if (state.selectedControl?.id === action.payload.controlId) {
141
+ const index = state.selectedControl?.properties.findIndex(
142
+ (property) => property.name === action.payload.propertyName
143
+ );
144
+ if (index !== -1) {
145
+ state.selectedControl.properties[index].value = action.payload.newValue;
146
+ }
147
+ }
148
+ })
149
+ .addMatcher(filterNodes.match, (state, action: ReturnType<typeof filterNodes>): void => {
150
+ action.payload.forEach((item) => {
151
+ const stateItem = state.filterQuery.find((filterItem) => filterItem.name === item.name);
152
+ if (stateItem) {
153
+ stateItem.value = item.value;
154
+ }
155
+ });
156
+ })
157
+ .addMatcher(propertyChangeFailed.match, (state, action: ReturnType<typeof propertyChangeFailed>): void => {
158
+ if (state.selectedControl?.id === action.payload.controlId) {
159
+ const index = state.selectedControl?.properties.findIndex(
160
+ (property) => property.name === action.payload.propertyName
161
+ );
162
+ if (index !== -1) {
163
+ state.selectedControl.properties[index].errorMessage = action.payload.errorMessage;
164
+ }
165
+ }
166
+ })
167
+ .addMatcher(changeStackModified.match, (state, action: ReturnType<typeof changeStackModified>): void => {
168
+ state.changes.saved = action.payload.saved;
169
+ state.changes.pending = action.payload.pending;
170
+ state.changes.controls = {};
171
+ for (const change of [...action.payload.pending, ...action.payload.saved].reverse()) {
172
+ const { controlId, propertyName, type, controlName } = change;
173
+ const key = `${controlId}`;
174
+ const control = state.changes.controls[key]
175
+ ? {
176
+ pending: state.changes.controls[key].pending,
177
+ saved: state.changes.controls[key].saved,
178
+ controlName: state.changes.controls[key].controlName,
179
+ properties: state.changes.controls[key].properties
180
+ }
181
+ : {
182
+ pending: 0,
183
+ saved: 0,
184
+ controlName: controlName ?? '',
185
+ properties: {}
186
+ };
187
+ if (type === 'pending') {
188
+ control.pending++;
189
+ } else if (type === 'saved') {
190
+ control.saved++;
191
+ }
192
+ const property = control.properties[propertyName]
193
+ ? {
194
+ pending: control.properties[propertyName].pending,
195
+ saved: control.properties[propertyName].saved,
196
+ lastSavedChange: control.properties[propertyName].lastSavedChange,
197
+ lastChange: control.properties[propertyName].lastChange
198
+ }
199
+ : {
200
+ pending: 0,
201
+ saved: 0
202
+ };
203
+ if (change.type === 'pending') {
204
+ property.pending++;
205
+ property.lastChange = change;
206
+ } else if (change.type === 'saved') {
207
+ property.lastSavedChange = change;
208
+ property.saved++;
209
+ }
210
+ control.properties[propertyName] = property;
211
+ state.changes.controls[key] = control;
212
+ }
213
+ })
214
+ });
215
+
216
+ export default slice.reducer;
package/src/store.ts ADDED
@@ -0,0 +1,19 @@
1
+ import { configureStore, compose, applyMiddleware } from '@reduxjs/toolkit';
2
+ import { useDispatch } from 'react-redux';
3
+ import { communicationMiddleware } from './middleware';
4
+ import reducer from './slice';
5
+
6
+ declare let window: Window & { __REDUX_DEVTOOLS_EXTENSION_COMPOSE__: <R>(data: R) => R };
7
+ const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?? compose;
8
+
9
+ export const store = configureStore({
10
+ reducer,
11
+ devTools: false,
12
+ middleware: [],
13
+ enhancers: [composeEnhancers(applyMiddleware(communicationMiddleware))]
14
+ });
15
+
16
+ export type AppDispatch = typeof store.dispatch;
17
+ export const useAppDispatch = (): AppDispatch => useDispatch<AppDispatch>();
18
+
19
+ export type RootState = ReturnType<typeof store.getState>;
@@ -0,0 +1,40 @@
1
+ import type React from 'react';
2
+ import { useState, useEffect } from 'react';
3
+
4
+ /**
5
+ * Get full control key.
6
+ *
7
+ * @param key string
8
+ * @returns string - key value
9
+ */
10
+ function fullKey(key: string): string {
11
+ return `com.sap.ux.control-property-editor.${key}`;
12
+ }
13
+ /**
14
+ * Use local storage.
15
+ *
16
+ * @param key string
17
+ * @param defaultValue T
18
+ * @returns [value, setValue] [T, React.Dispatch<T>]
19
+ */
20
+ export function useLocalStorage<T>(key: string, defaultValue: T): [T, React.Dispatch<T>] {
21
+ const [value, setValue] = useState(() => {
22
+ const savedValue = localStorage.getItem(fullKey(key));
23
+
24
+ if (!savedValue) {
25
+ return defaultValue;
26
+ }
27
+
28
+ try {
29
+ return JSON.parse(savedValue);
30
+ } catch (e) {
31
+ return defaultValue;
32
+ }
33
+ });
34
+
35
+ useEffect(() => {
36
+ localStorage.setItem(fullKey(key), JSON.stringify(value));
37
+ }, [key, value]);
38
+
39
+ return [value, setValue];
40
+ }
@@ -0,0 +1,39 @@
1
+ import { useState, useEffect } from 'react';
2
+
3
+ import { debounce } from '@sap-ux-private/control-property-editor-common';
4
+
5
+ export interface WindowSize {
6
+ width: number | undefined;
7
+ height: number | undefined;
8
+ }
9
+
10
+ /**
11
+ * Gets state of window size.
12
+ *
13
+ * @returns WindowSize
14
+ */
15
+ export function useWindowSize(): WindowSize {
16
+ const [windowSize, setWindowSize] = useState<WindowSize>({
17
+ width: undefined,
18
+ height: undefined
19
+ });
20
+
21
+ useEffect(() => {
22
+ const handleResize = debounce(() => {
23
+ setWindowSize({
24
+ width: window.innerWidth,
25
+ height: window.innerHeight
26
+ });
27
+ }, 500);
28
+
29
+ window.addEventListener('resize', handleResize);
30
+
31
+ setWindowSize({
32
+ width: window.innerWidth,
33
+ height: window.innerHeight
34
+ });
35
+
36
+ return (): void => window.removeEventListener('resize', handleResize);
37
+ }, []);
38
+ return windowSize;
39
+ }
@@ -0,0 +1,2 @@
1
+ $defaultFontSize: 13px;
2
+ $sectionHeaderFontSize: 11px;
@@ -0,0 +1,207 @@
1
+ import React from 'react';
2
+ import { fireEvent, screen } from '@testing-library/react';
3
+ import { initIcons } from '@sap-ux/ui-components';
4
+
5
+ import { render, mockDomEventListener } from './utils';
6
+ import { initI18n } from '../../src/i18n';
7
+
8
+ import App from '../../src/App';
9
+ import { controlSelected } from '@sap-ux-private/control-property-editor-common';
10
+ import { mockResizeObserver } from '../utils/utils';
11
+ import { InputType } from '../../src/panels/properties/types';
12
+ import { registerAppIcons } from '../../src/icons';
13
+ import { DeviceType } from '../../src/devices';
14
+ import { changePreviewScale, initialState } from '../../src/slice';
15
+
16
+ jest.useFakeTimers({ advanceTimers: true });
17
+ const windowEventListenerMock = mockDomEventListener(window);
18
+ beforeAll(() => {
19
+ mockResizeObserver();
20
+ initI18n();
21
+ registerAppIcons();
22
+ initIcons();
23
+ // JSDom does not implement this and an error was being
24
+ // thrown from jest-axe because of it.
25
+ (window as any).getComputedStyle = jest.fn();
26
+ });
27
+
28
+ test('renders empty properties panel', () => {
29
+ render(<App previewUrl="" />);
30
+ const noControlSelected = screen.getByText(/No control selected/i);
31
+ expect(noControlSelected).toBeInTheDocument();
32
+
33
+ const selectControlText = screen.getByText(/Select a control on the canvas to see and modify its properties/i);
34
+ expect(selectControlText).toBeInTheDocument();
35
+
36
+ const controlSelectIcon = screen.getByTestId('Control-Property-Editor-No-Control-Selected');
37
+ expect(controlSelectIcon).toBeInTheDocument();
38
+ });
39
+
40
+ test('renders properties', () => {
41
+ const { store } = render(<App previewUrl="" />);
42
+ const propNameString = 'activeIcon';
43
+ const propNameDropDown = 'ariaHasPopup';
44
+ const propNameCheckbox = 'visible';
45
+ const propNameCheckboxExpression = 'random';
46
+ const propNameDropDownExpression = 'sync';
47
+ store.dispatch(
48
+ controlSelected({
49
+ id: 'v2flex::sap.suite.ui.generic.template.ListReport.view.ListReport::SEPMRA_C_PD_Product--addEntry',
50
+ type: 'sap.m.Button',
51
+ properties: [
52
+ {
53
+ type: 'string',
54
+ editor: 'input',
55
+ name: propNameString,
56
+ value: '',
57
+ isEnabled: true,
58
+ readableName: ''
59
+ },
60
+ {
61
+ type: 'string',
62
+ editor: 'dropdown',
63
+ name: propNameDropDown,
64
+ value: 'None',
65
+ isEnabled: true,
66
+ readableName: '',
67
+ options: [
68
+ {
69
+ key: 'None',
70
+ text: 'None'
71
+ },
72
+ {
73
+ key: 'Menu',
74
+ text: 'Menu'
75
+ },
76
+ {
77
+ key: 'ListBox',
78
+ text: 'ListBox'
79
+ },
80
+ {
81
+ key: 'Tree',
82
+ text: 'Tree'
83
+ },
84
+ {
85
+ key: 'Grid',
86
+ text: 'Grid'
87
+ },
88
+ {
89
+ key: 'Dialog',
90
+ text: 'Dialog'
91
+ }
92
+ ]
93
+ },
94
+ {
95
+ type: 'boolean',
96
+ editor: 'checkbox',
97
+ name: propNameCheckbox,
98
+ value: true,
99
+ isEnabled: true,
100
+ readableName: 'test check'
101
+ },
102
+ {
103
+ type: 'string',
104
+ editor: 'dropdown',
105
+ options: [],
106
+ name: propNameDropDownExpression,
107
+ value: '{ dropDownDynamicExpression }',
108
+ isEnabled: true,
109
+ readableName: ''
110
+ },
111
+ {
112
+ type: 'boolean',
113
+ editor: 'checkbox',
114
+ name: propNameCheckboxExpression,
115
+ isEnabled: true,
116
+ value: '{ checkBoxDynamicExpression }',
117
+ readableName: 'test check dynamic expression'
118
+ },
119
+ {
120
+ type: 'string',
121
+ editor: 'unknown',
122
+ name: 'unknownprop',
123
+ isEnabled: true,
124
+ value: 'checkBoxDynamicExpression',
125
+ readableName: 'test check dynamic expression'
126
+ }
127
+ ]
128
+ })
129
+ );
130
+ const visiblePropertyLabel = screen.getByTestId(`${propNameCheckbox}--Label`);
131
+ expect(visiblePropertyLabel).toBeInTheDocument();
132
+
133
+ const buttonTrue = screen.getByTestId(`${propNameCheckbox}--InputTypeToggle--${InputType.booleanTrue}`);
134
+ expect(buttonTrue.getAttribute('aria-pressed')).toBe('true');
135
+ expect(buttonTrue).toBeInTheDocument();
136
+
137
+ const buttonFalse = screen.getByTestId(`${propNameCheckbox}--InputTypeToggle--${InputType.booleanFalse}`);
138
+ expect(buttonFalse.getAttribute('aria-pressed')).toBe('false');
139
+ expect(buttonFalse).toBeInTheDocument();
140
+
141
+ const buttonExpression = screen.getByTestId(`${propNameCheckbox}--InputTypeToggle--${InputType.expression}`);
142
+ expect(buttonExpression.getAttribute('aria-pressed')).toBe('false');
143
+ expect(buttonExpression).toBeInTheDocument();
144
+
145
+ const dropdownButtonExp2 = screen.getByTestId(
146
+ `${propNameDropDownExpression}--InputTypeToggle--${InputType.expression}`
147
+ );
148
+ expect(dropdownButtonExp2.getAttribute('aria-pressed')).toBe('true');
149
+ expect(dropdownButtonExp2).toBeInTheDocument();
150
+
151
+ const checkButtonExp2 = screen.getByTestId(
152
+ `${propNameCheckboxExpression}--InputTypeToggle--${InputType.expression}`
153
+ );
154
+ expect(checkButtonExp2.getAttribute('aria-pressed')).toBe('true');
155
+ expect(checkButtonExp2).toBeInTheDocument();
156
+
157
+ let notFoundException = null;
158
+ try {
159
+ screen.getByTestId(`${propNameCheckbox}--StringEditor`);
160
+ } catch (e) {
161
+ notFoundException = e;
162
+ }
163
+ expect(notFoundException).toBeTruthy();
164
+ });
165
+
166
+ test('renders warning dialog', async () => {
167
+ render(<App previewUrl="" />);
168
+ const dialogContent = screen.getByText(
169
+ /The Control Property Editor enables you to change control properties and behavior directly. These changes may not have the desired effect with Fiori elements applications. Please consult documentation to learn which changes are supported./i
170
+ );
171
+ expect(dialogContent).toBeInTheDocument();
172
+ const okButton = screen.getByText(/ok/i);
173
+ expect(okButton).toBeInTheDocument();
174
+ fireEvent.click(okButton);
175
+ });
176
+
177
+ const testCases = [
178
+ {
179
+ deviceType: DeviceType.Desktop,
180
+ expectedScale: 460 / 1200
181
+ },
182
+ {
183
+ deviceType: DeviceType.Tablet,
184
+ expectedScale: 460 / 720
185
+ }
186
+ ];
187
+
188
+ for (const testCase of testCases) {
189
+ test(`Test resize - fitPreview=true, device=${testCase.deviceType}`, async () => {
190
+ const stateTemp = JSON.parse(JSON.stringify(initialState));
191
+ stateTemp.fitPreview = true;
192
+ stateTemp.deviceType = testCase.deviceType;
193
+
194
+ const { dispatch } = render(<App previewUrl="" />, {
195
+ initialState: stateTemp
196
+ });
197
+ await new Promise((resolve) => setTimeout(resolve, 1));
198
+ global.window.innerWidth = 111;
199
+ global.window.innerHeight = 2222;
200
+ dispatch.mockReset();
201
+ jest.spyOn(HTMLElement.prototype, 'clientWidth', 'get').mockImplementation(() => 500);
202
+ windowEventListenerMock.simulateEvent('resize', {});
203
+ // Debounce timeout within resize + within use effect
204
+ jest.advanceTimersByTime(3000);
205
+ expect(dispatch).toBeCalledWith(changePreviewScale(testCase.expectedScale));
206
+ });
207
+ }
@@ -0,0 +1,23 @@
1
+ import { start } from '../../src/index';
2
+ import * as i18n from '../../src/i18n';
3
+ import * as icons from '../../src/icons';
4
+ import * as initIcon from '@sap-ux/ui-components/dist/components/Icons'; // bug in TS 4.6 https://github.com/microsoft/TypeScript/issues/43081
5
+ import ReactDOM from 'react-dom';
6
+
7
+ describe('index', () => {
8
+ const i18nSpy = jest.spyOn(i18n, 'initI18n');
9
+ const iconsSpy = jest.spyOn(icons, 'registerAppIcons');
10
+ // ts-ignore
11
+ const initIconSpy = jest.spyOn(initIcon, 'initIcons');
12
+ const reactSpy = jest.spyOn(ReactDOM, 'render').mockReturnValue();
13
+
14
+ test('start', () => {
15
+ const previewUrl = 'URL';
16
+ const rootElementId = 'root';
17
+ start({ previewUrl, rootElementId });
18
+ expect(i18nSpy).toHaveBeenCalledTimes(1);
19
+ expect(iconsSpy).toHaveBeenCalledTimes(1);
20
+ expect(initIconSpy).toHaveBeenCalledTimes(1);
21
+ expect(reactSpy).toHaveBeenCalledTimes(1);
22
+ });
23
+ });
@@ -0,0 +1,120 @@
1
+ import React from 'react';
2
+
3
+ import { render } from '../utils';
4
+ import { initI18n } from '../../../src/i18n';
5
+
6
+ import { ChangeIndicator } from '../../../src/components/ChangeIndicator';
7
+ import { mockResizeObserver } from '../../utils/utils';
8
+ import { initIcons } from '@sap-ux/ui-components';
9
+
10
+ beforeAll(() => {
11
+ mockResizeObserver();
12
+ initI18n();
13
+ initIcons();
14
+ });
15
+
16
+ describe('ChangeIndicator', () => {
17
+ test('saved changes', () => {
18
+ const { container } = render(<ChangeIndicator id={'change-indicator'} saved={1} pending={0} />);
19
+ expect(container.querySelector('svg')).toMatchInlineSnapshot(`
20
+ <svg
21
+ fill="none"
22
+ height="8"
23
+ id="change-indicator"
24
+ role="img"
25
+ viewBox="0 0 8 8"
26
+ width="8"
27
+ xmlns="http://www.w3.org/2000/svg"
28
+ >
29
+ <title>
30
+ Modified & Saved
31
+ </title>
32
+ <circle
33
+ cx="4"
34
+ cy="4"
35
+ fill="var(--vscode-terminal-ansiGreen)"
36
+ r="4"
37
+ />
38
+ </svg>
39
+ `);
40
+ });
41
+
42
+ test('pending changes', () => {
43
+ const { container } = render(<ChangeIndicator saved={0} pending={2} />);
44
+ expect(container.querySelector('svg')).toMatchInlineSnapshot(`
45
+ <svg
46
+ fill="none"
47
+ height="8"
48
+ role="img"
49
+ viewBox="0 0 8 8"
50
+ width="8"
51
+ xmlns="http://www.w3.org/2000/svg"
52
+ >
53
+ <title>
54
+ Modified & Unsaved
55
+ </title>
56
+ <circle
57
+ cx="4"
58
+ cy="4"
59
+ r="3.5"
60
+ stroke="var(--vscode-terminal-ansiGreen)"
61
+ />
62
+ </svg>
63
+ `);
64
+ });
65
+
66
+ test('pending and saved changes', () => {
67
+ const { container } = render(<ChangeIndicator saved={3} pending={2} />);
68
+ expect(container.querySelector('svg')).toMatchInlineSnapshot(`
69
+ <svg
70
+ fill="none"
71
+ height="8"
72
+ role="img"
73
+ viewBox="0 0 8 8"
74
+ width="8"
75
+ xmlns="http://www.w3.org/2000/svg"
76
+ >
77
+ <title>
78
+ Saved & Unsaved
79
+ </title>
80
+ <circle
81
+ cx="4"
82
+ cy="4"
83
+ r="3.5"
84
+ stroke="var(--vscode-terminal-ansiGreen)"
85
+ />
86
+ <path
87
+ d="M4 8a4 4 0 1 0 0-8v8Z"
88
+ fill="var(--vscode-terminal-ansiGreen)"
89
+ />
90
+ </svg>
91
+ `);
92
+ });
93
+
94
+ test('do not add unknown properties', () => {
95
+ const { container } = render(
96
+ <ChangeIndicator id={'change-indicator'} saved={1} pending={0} {...{ xyz: 'abc ' }} />
97
+ );
98
+ expect(container.querySelector('svg')).toMatchInlineSnapshot(`
99
+ <svg
100
+ fill="none"
101
+ height="8"
102
+ id="change-indicator"
103
+ role="img"
104
+ viewBox="0 0 8 8"
105
+ width="8"
106
+ xmlns="http://www.w3.org/2000/svg"
107
+ >
108
+ <title>
109
+ Modified & Saved
110
+ </title>
111
+ <circle
112
+ cx="4"
113
+ cy="4"
114
+ fill="var(--vscode-terminal-ansiGreen)"
115
+ r="4"
116
+ />
117
+ </svg>
118
+ `);
119
+ });
120
+ });