@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
@@ -0,0 +1,41 @@
1
+ import React from 'react';
2
+ import { screen } from '@testing-library/react';
3
+
4
+ import { render } from '../utils';
5
+ import { initI18n } from '../../../src/i18n';
6
+
7
+ import { ThemeSelectorCallout } from '../../../src/components/ThemeSelectorCallout';
8
+ import { mockResizeObserver } from '../../utils/utils';
9
+ import { initIcons } from '@sap-ux/ui-components';
10
+
11
+ beforeAll(() => {
12
+ mockResizeObserver();
13
+ initI18n();
14
+ initIcons();
15
+ });
16
+
17
+ test('renders theme selector callout', () => {
18
+ render(<ThemeSelectorCallout />);
19
+ screen.getByRole('button').click();
20
+ const themeCalloutContent = screen.getAllByRole('button');
21
+ expect(themeCalloutContent).toHaveLength(4);
22
+ });
23
+
24
+ test('check selected theme', () => {
25
+ localStorage.setItem('theme', 'light');
26
+ render(<ThemeSelectorCallout />);
27
+ screen.getByRole('button').click();
28
+ const themeCalloutContent = screen.getAllByRole('button', { pressed: true });
29
+ const pressedButton = themeCalloutContent.find((button) => button.getAttribute('aria-pressed') === 'true');
30
+ expect(pressedButton?.getAttribute('id')).toStrictEqual('theme-light-rect');
31
+ });
32
+
33
+ test('change theme to light', () => {
34
+ render(<ThemeSelectorCallout />);
35
+ screen.getByRole('button').click();
36
+ screen.getByTitle('Light').click();
37
+ const themeCalloutContent = screen.getAllByRole('button', { pressed: true });
38
+ const pressedButton = themeCalloutContent.find((button) => button.getAttribute('aria-pressed') === 'true');
39
+ expect(pressedButton?.getAttribute('id')).toStrictEqual('theme-light-rect');
40
+ expect(localStorage.getItem('theme')).toStrictEqual('light');
41
+ });
@@ -0,0 +1,116 @@
1
+ import * as common from '@sap-ux-private/control-property-editor-common';
2
+ import { communicationMiddleware } from '../../src/middleware';
3
+ jest.mock('../../src/slice', () => {
4
+ return {
5
+ changeProperty: { type: '[ext] property-changed' }
6
+ };
7
+ });
8
+
9
+ describe('communication middleware', () => {
10
+ let messageProcessor: jest.SpyInstance;
11
+ let dispatch: jest.SpyInstance;
12
+ let middleWare: any;
13
+ const sendActionfn = jest.fn();
14
+
15
+ beforeEach(() => {
16
+ messageProcessor = jest.spyOn(common, 'startPostMessageCommunication').mockReturnValue({
17
+ sendAction: sendActionfn,
18
+ dispose: jest.fn()
19
+ });
20
+ dispatch = jest.fn();
21
+ middleWare = communicationMiddleware({ dispatch } as any);
22
+ jest.spyOn(document, 'getElementById').mockReturnValue({
23
+ contentWindow: 'Target'
24
+ } as any);
25
+ });
26
+ afterEach(() => {
27
+ if (messageProcessor) {
28
+ messageProcessor.mockRestore();
29
+ }
30
+ sendActionfn.mockReset();
31
+ });
32
+
33
+ test('property changed in UI5 application', () => {
34
+ const action = common.propertyChanged({
35
+ controlId: 'control1',
36
+ propertyName: 'text',
37
+ newValue: 'new value'
38
+ });
39
+ messageProcessor.mock.calls[0][1](action);
40
+ expect(dispatch).toHaveBeenCalledTimes(1);
41
+ expect(dispatch).toHaveBeenNthCalledWith(1, action);
42
+ });
43
+
44
+ test('control selected in UI5 application', () => {
45
+ const action = common.controlSelected({
46
+ id: 'control1',
47
+ name: 'testing',
48
+ type: 'text',
49
+ properties: []
50
+ });
51
+ messageProcessor.mock.calls[0][1](action);
52
+ expect(dispatch).toHaveBeenCalledTimes(1);
53
+ expect(dispatch).toHaveBeenNthCalledWith(1, action);
54
+ });
55
+
56
+ test('outline changed in UI5 application', () => {
57
+ const action = common.outlineChanged([]);
58
+ messageProcessor.mock.calls[0][1](action);
59
+ expect(dispatch).toHaveBeenCalledTimes(1);
60
+ expect(dispatch).toHaveBeenNthCalledWith(1, action);
61
+ });
62
+
63
+ test('property change failed in UI5 application', () => {
64
+ const action = common.propertyChangeFailed({
65
+ controlId: 'control1',
66
+ propertyName: 'text',
67
+ errorMessage: 'change failed'
68
+ });
69
+ messageProcessor.mock.calls[0][1](action);
70
+ expect(dispatch).toHaveBeenCalledTimes(1);
71
+ expect(dispatch).toHaveBeenNthCalledWith(1, action);
72
+ });
73
+
74
+ test('getTarget', () => {
75
+ expect(messageProcessor.mock.calls[0][0]()).toEqual('Target');
76
+ });
77
+
78
+ test('property change - send action', () => {
79
+ const action = common.propertyChanged({
80
+ controlId: 'control1',
81
+ propertyName: 'text',
82
+ newValue: 'new value'
83
+ });
84
+ const next = jest.fn().mockReturnValue(action);
85
+ const result = middleWare(next)(action);
86
+ expect(result).toMatchInlineSnapshot(`
87
+ Object {
88
+ "payload": Object {
89
+ "controlId": "control1",
90
+ "newValue": "new value",
91
+ "propertyName": "text",
92
+ },
93
+ "type": "[ext] property-changed",
94
+ }
95
+ `);
96
+ expect(sendActionfn).toHaveBeenCalledTimes(1);
97
+ });
98
+
99
+ test('select control - send action', () => {
100
+ const action = common.selectControl('01-02');
101
+ const next = jest.fn().mockReturnValue(action);
102
+ jest.mock('@sap-ux-private/control-property-editor-common', () => {
103
+ return {
104
+ selectControl: { type: '[ext] select-control' }
105
+ };
106
+ });
107
+ const result = middleWare(next)(action);
108
+ expect(result).toMatchInlineSnapshot(`
109
+ Object {
110
+ "payload": "01-02",
111
+ "type": "[ext] select-control",
112
+ }
113
+ `);
114
+ expect(sendActionfn).toHaveBeenCalledTimes(1);
115
+ });
116
+ });
@@ -0,0 +1,261 @@
1
+ import React from 'react';
2
+ import { screen, fireEvent } from '@testing-library/react';
3
+ import { initIcons } from '@sap-ux/ui-components';
4
+
5
+ import { render } from '../../utils';
6
+ import { FilterName } from '../../../../src/slice';
7
+ import type { FilterOptions, ChangesSlice, default as reducer } from '../../../../src/slice';
8
+ import { DeviceType } from '../../../../src/devices';
9
+ import { registerAppIcons } from '../../../../src/icons';
10
+ import { ChangesPanel } from '../../../../src/panels/changes';
11
+ import { initI18n } from '../../../../src/i18n';
12
+ import type { PendingPropertyChange, SavedPropertyChange } from '@sap-ux-private/control-property-editor-common';
13
+
14
+ export type State = ReturnType<typeof reducer>;
15
+
16
+ const getEmptyModel = (): ChangesSlice => {
17
+ const model: ChangesSlice = {
18
+ controls: {} as any,
19
+ pending: [],
20
+ saved: []
21
+ };
22
+ return model;
23
+ };
24
+
25
+ const getModel = (saved = false): ChangesSlice => {
26
+ const model: ChangesSlice = {
27
+ controls: {} as any,
28
+ pending: !saved
29
+ ? ([
30
+ {
31
+ controlId: 'testId1',
32
+ controlName: 'controlName1',
33
+ propertyName: 'testPropertyName1',
34
+ type: 'pending',
35
+ value: 'testValue1',
36
+ isActive: true
37
+ },
38
+ {
39
+ controlId: 'testId1BoolFalse',
40
+ controlName: 'controlNameBoolFalse',
41
+ propertyName: 'testPropertyNameBoolFalse',
42
+ type: 'pending',
43
+ value: false,
44
+ isActive: true
45
+ },
46
+ {
47
+ controlId: 'testId1Exp',
48
+ controlName: 'controlNameExp',
49
+ propertyName: 'testPropertyNameExp',
50
+ type: 'pending',
51
+ value: '{expression}',
52
+ isActive: true
53
+ }
54
+ ] as PendingPropertyChange[])
55
+ : [],
56
+ saved: saved
57
+ ? ([
58
+ {
59
+ controlId: 'testId2',
60
+ controlName: 'controlName2',
61
+ propertyName: 'testPropertyName2',
62
+ type: 'saved',
63
+ value: 'testValue2',
64
+ fileName: 'testFileName',
65
+ kind: 'valid',
66
+ timestamp: new Date('2022-02-09T12:06:53.939Z').getTime()
67
+ },
68
+ {
69
+ controlId: 'testId3',
70
+ controlName: 'controlNameBoolean',
71
+ propertyName: 'testPropertyNameBool',
72
+ type: 'saved',
73
+ value: true,
74
+ fileName: 'testFileNameBool',
75
+ kind: 'valid',
76
+ timestamp: new Date('2022-02-09T12:06:53.939Z').getTime()
77
+ },
78
+ {
79
+ controlId: 'testId4',
80
+ controlName: 'controlNameNumber',
81
+ propertyName: 'testPropertyNameNum',
82
+ type: 'saved',
83
+ value: 2,
84
+ fileName: 'testFileNameNum',
85
+ kind: 'valid',
86
+ timestamp: new Date('2022-02-09T12:06:53.939Z').getTime()
87
+ }
88
+ ] as SavedPropertyChange[])
89
+ : []
90
+ };
91
+ return model;
92
+ };
93
+ const filterInitOptions: FilterOptions[] = [{ name: FilterName.changeSummaryFilterQuery, value: '' }];
94
+ describe('ChangePanel', () => {
95
+ beforeAll(() => {
96
+ initI18n();
97
+ initIcons();
98
+ registerAppIcons();
99
+ });
100
+ test('ChangePanel empty save and pending', () => {
101
+ const model = getEmptyModel();
102
+ const initialState: State = {
103
+ deviceType: DeviceType.Desktop,
104
+ scale: 1,
105
+ outline: {} as any,
106
+ filterQuery: filterInitOptions,
107
+ selectedControl: undefined,
108
+ changes: model,
109
+ icons: []
110
+ };
111
+ render(<ChangesPanel />, { initialState });
112
+
113
+ // check no controls found
114
+ const noControlFound = screen.getByText(/no control changes found/i);
115
+ expect(noControlFound).toBeInTheDocument();
116
+ });
117
+
118
+ test('ChangePanel with unsaved changes', () => {
119
+ const model = getModel();
120
+ const initialState: State = {
121
+ deviceType: DeviceType.Desktop,
122
+ scale: 1,
123
+ outline: {} as any,
124
+ filterQuery: filterInitOptions,
125
+ selectedControl: undefined,
126
+ changes: model,
127
+ icons: []
128
+ };
129
+ render(<ChangesPanel />, { initialState });
130
+
131
+ // check unsaved changes
132
+ const unsavedChangesTitle = screen.getByText(/unsaved changes/i);
133
+ expect(unsavedChangesTitle).toBeInTheDocument();
134
+
135
+ const controlName = screen.getByRole('button', { name: /Control Name1/i });
136
+ expect(controlName).toBeInTheDocument();
137
+
138
+ const propertyName = screen.getByText(/Test Property Name1/i);
139
+ expect(propertyName).toBeInTheDocument();
140
+
141
+ const value = screen.getByText(/testValue1/i);
142
+ expect(value).toBeInTheDocument();
143
+ });
144
+
145
+ test('ChangePanel with saved changes', () => {
146
+ const model = getModel(true);
147
+ const initialState: State = {
148
+ deviceType: DeviceType.Desktop,
149
+ scale: 1,
150
+ outline: {} as any,
151
+ filterQuery: filterInitOptions,
152
+ selectedControl: undefined,
153
+ changes: model,
154
+ icons: []
155
+ };
156
+ render(<ChangesPanel />, { initialState });
157
+
158
+ // check saved changes
159
+ const savedChangesTitle = screen.getByText(/saved changes/i);
160
+ expect(savedChangesTitle).toBeInTheDocument();
161
+
162
+ const controlName1 = screen.getByRole('button', { name: /control name2/i });
163
+ expect(controlName1).toBeInTheDocument();
164
+ fireEvent.click(controlName1);
165
+
166
+ const propertyName1 = screen.getByText(/Test Property Name2/i);
167
+ expect(propertyName1).toBeInTheDocument();
168
+
169
+ const value1 = screen.getByText(/testValue2/i);
170
+ expect(value1).toBeInTheDocument();
171
+
172
+ const deleteButton = screen.getAllByRole('button')[1];
173
+ const iTagAttributes = deleteButton?.children?.item(0)?.children?.item(0)?.attributes;
174
+ const iconName = iTagAttributes?.getNamedItem('data-icon-name')?.value;
175
+
176
+ expect(deleteButton).toBeInTheDocument();
177
+ expect(iconName).toBe('TrashCan');
178
+
179
+ fireEvent.click(deleteButton);
180
+ expect(
181
+ screen.getByText(
182
+ /Are you sure you want to delete the change for this property\? This action cannot be undone\./i
183
+ )
184
+ ).toBeInTheDocument();
185
+
186
+ // first cancel
187
+ const cancelButton = screen.getByRole('button', { name: /^Cancel$/i });
188
+ cancelButton.click();
189
+
190
+ // delete
191
+ fireEvent.click(deleteButton);
192
+ const confirmButton = screen.getByRole('button', { name: /^Delete$/i });
193
+ confirmButton.click();
194
+
195
+ fireEvent.change(screen.getByRole('searchbox'), { target: { value: 'control Name2' } });
196
+
197
+ const controlName2 = screen.getByRole('button', { name: /control name2/i });
198
+ expect(controlName2).toBeInTheDocument();
199
+
200
+ fireEvent.change(screen.getByRole('searchbox'), { target: { value: 'yyyyy' } });
201
+
202
+ expect(screen.queryByText(/Test Property Name1/i)).toStrictEqual(null);
203
+ expect(screen.queryByText(/Test Property Name2/i)).toStrictEqual(null);
204
+ });
205
+
206
+ test('ChangePanel with unknown saved changes', () => {
207
+ const model: ChangesSlice = {
208
+ controls: {} as any,
209
+ pending: [],
210
+ saved: [
211
+ {
212
+ fileName: 'testFileName2',
213
+ type: 'saved',
214
+ kind: 'unknown'
215
+ } as any
216
+ ]
217
+ };
218
+ const initialState: State = {
219
+ deviceType: DeviceType.Desktop,
220
+ scale: 1,
221
+ outline: {} as any,
222
+ filterQuery: filterInitOptions,
223
+ selectedControl: undefined,
224
+ changes: model,
225
+ icons: []
226
+ };
227
+ render(<ChangesPanel />, { initialState });
228
+
229
+ // check unknown changes
230
+ const savedChangesTitle = screen.getByText(/saved changes/i);
231
+ expect(savedChangesTitle).toBeInTheDocument();
232
+
233
+ const title = screen.getByText(/Test File Name2/i);
234
+ expect(title).toBeInTheDocument();
235
+
236
+ const value = screen.getByText(/File: testFileName2/i);
237
+ expect(value).toBeInTheDocument();
238
+
239
+ const deleteButton = screen.getAllByRole('button')[0];
240
+ const iTagAttributes = deleteButton?.children?.item(0)?.children?.item(0)?.attributes;
241
+ const iconName = iTagAttributes?.getNamedItem('data-icon-name')?.value;
242
+ expect(deleteButton).toBeInTheDocument();
243
+ expect(iconName).toBe('TrashCan');
244
+
245
+ fireEvent.click(deleteButton);
246
+ expect(
247
+ screen.getByText(
248
+ /Are you sure you want to delete the change for this property\? This action cannot be undone\./i
249
+ )
250
+ ).toBeInTheDocument();
251
+
252
+ // first cancel
253
+ const cancelButton = screen.getByRole('button', { name: /^Cancel$/i });
254
+ cancelButton.click();
255
+
256
+ // delete
257
+ fireEvent.click(deleteButton);
258
+ const confirmButton = screen.getByRole('button', { name: /^Delete$/i });
259
+ confirmButton.click();
260
+ });
261
+ });
@@ -0,0 +1,40 @@
1
+ import { getFormattedDateAndTime } from '../../../../src/panels/changes/utils';
2
+
3
+ describe('getFormattedDateAndTime', () => {
4
+ let navigatorSpy: jest.SpyInstance;
5
+ jest.spyOn(navigator, 'languages', 'get').mockReturnValue(undefined);
6
+ beforeEach(() => {
7
+ navigatorSpy = jest.spyOn(navigator, 'language', 'get');
8
+ });
9
+ afterEach(() => {
10
+ jest.resetAllMocks();
11
+ });
12
+
13
+ test('should format timestamp correctly in default locale', () => {
14
+ const timestamp = Date.parse('2023-10-01T12:34:56');
15
+ navigatorSpy.mockReturnValue('en-US');
16
+ const result = getFormattedDateAndTime(timestamp);
17
+ expect(result).toBe('12:34 01/10/23');
18
+ });
19
+
20
+ test('should format timestamp correctly in supported locale', () => {
21
+ const timestamp = Date.parse('2023-10-01T12:34:56');
22
+ navigatorSpy.mockReturnValue('fr-FR');
23
+ const result = getFormattedDateAndTime(timestamp);
24
+ expect(result).toBe('12:34 01/10/23');
25
+ });
26
+
27
+ test('should format timestamp correctly when navigator is not defined', () => {
28
+ const timestamp = Date.parse('2023-10-01T12:34:56');
29
+ navigatorSpy.mockReturnValue('');
30
+ const result = getFormattedDateAndTime(timestamp);
31
+ expect(result).toBe('12:34 01/10/23');
32
+ });
33
+
34
+ test('should fall back to default locale if no supported locales found', () => {
35
+ const timestamp = Date.parse('2023-10-01T12:34:56');
36
+ navigatorSpy.mockReturnValue('xx-YY');
37
+ const result = getFormattedDateAndTime(timestamp);
38
+ expect(result).toBe('12:34 01/10/23');
39
+ });
40
+ });