@controleonline/ui-common 1.2.71 → 1.2.76

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 (150) hide show
  1. package/.github/actions/workers/manager/action.yml +147 -0
  2. package/.github/actions/workers/qa/action.yml +44 -0
  3. package/.github/actions/workers/security/action.yml +44 -0
  4. package/.github/actions/workers/technical-documenter/action.yml +51 -0
  5. package/.github/workflows/manager-worker.yml +168 -0
  6. package/.github/workflows/publish-npm.yml +45 -0
  7. package/.gitmodules +3 -0
  8. package/AGENTS.md +6 -0
  9. package/README.md +14 -0
  10. package/package.json +3 -2
  11. package/src/api/fetch.js +1 -1
  12. package/src/api/index.js +92 -22
  13. package/src/react/components/AddImportModal.js +94 -110
  14. package/src/react/components/BottomNavigationBar.config.js +211 -231
  15. package/src/react/components/CategoryForm.js +52 -35
  16. package/src/react/components/DefaultProvider.native.js +131 -95
  17. package/src/react/components/DefaultProvider.web.js +148 -74
  18. package/src/react/components/NetworkPrinterPreviewModal.js +249 -0
  19. package/src/react/components/NetworkPrinterPreviewModal.styles.js +159 -0
  20. package/src/react/components/RuntimeInfoFooter.js +164 -104
  21. package/src/react/components/RuntimeInfoFooter.styles.js +3 -0
  22. package/src/react/components/UnifiedPaymentBar.js +7 -4
  23. package/src/react/components/UserAvatar.js +160 -0
  24. package/src/react/components/integrations/IntegrationHero/index.js +48 -0
  25. package/src/react/components/integrations/IntegrationHero/styles.js +57 -0
  26. package/src/react/components/integrations/IntegrationTabs/index.js +89 -0
  27. package/src/react/components/integrations/IntegrationTabs/styles.js +44 -0
  28. package/src/react/config/deviceConfigBootstrap.js +5 -3
  29. package/src/react/pages/Connections.js +229 -0
  30. package/src/react/pages/Connections.styles.js +222 -0
  31. package/src/react/pages/DeviceDetailPage.js +2865 -0
  32. package/src/react/pages/DeviceDetailPage.styles.js +308 -0
  33. package/src/react/pages/Devices/currentDevice.js +194 -0
  34. package/src/react/pages/Devices/deviceTypes/DeviceGroupCard.js +257 -0
  35. package/src/react/pages/Devices/deviceTypes/all.js +23 -0
  36. package/src/react/pages/Devices/deviceTypes/device.js +25 -0
  37. package/src/react/pages/Devices/deviceTypes/deviceListHelpers.js +178 -0
  38. package/src/react/pages/Devices/deviceTypes/display.js +26 -0
  39. package/src/react/pages/Devices/deviceTypes/index.js +145 -0
  40. package/src/react/pages/Devices/deviceTypes/ipCamera.js +35 -0
  41. package/src/react/pages/Devices/deviceTypes/pdv.js +88 -0
  42. package/src/react/pages/Devices/deviceTypes/printer.js +37 -0
  43. package/src/react/pages/Devices/deviceTypes/shared.js +453 -0
  44. package/src/react/pages/Devices/deviceTypes/useDeviceNetworkConnectivity.js +145 -0
  45. package/src/react/pages/Devices.js +183 -0
  46. package/src/react/pages/Devices.styles.js +353 -0
  47. package/src/react/pages/Food99IntegrationPage/components/Food99CatalogTab.js +219 -0
  48. package/src/react/pages/Food99IntegrationPage/components/Food99OverviewTab.js +134 -0
  49. package/src/react/pages/Food99IntegrationPage/components/Food99PreviewModal.js +173 -0
  50. package/src/react/pages/Food99IntegrationPage/components/Food99ProductCard.js +1 -0
  51. package/src/react/pages/Food99IntegrationPage/components/Food99QuickWalletModal.js +181 -0
  52. package/src/react/pages/Food99IntegrationPage/components/Food99SettingsTab.js +306 -0
  53. package/src/react/pages/Food99IntegrationPage/components/Food99StoreTab.js +417 -0
  54. package/src/react/pages/Food99IntegrationPage/index.js +1530 -0
  55. package/src/react/pages/Food99IntegrationPage/styles.js +792 -0
  56. package/src/react/pages/Food99IntegrationPage/utils.js +155 -0
  57. package/src/react/pages/Food99IntegrationPage.js +1 -0
  58. package/src/react/pages/Food99IntegrationPage.styles.js +1 -0
  59. package/src/react/pages/GenericLogPage.js +42 -167
  60. package/src/react/pages/IFoodIntegrationPage/components/IFoodCatalogTab.js +8 -0
  61. package/src/react/pages/IFoodIntegrationPage/components/IFoodOperationsTab.js +420 -0
  62. package/src/react/pages/IFoodIntegrationPage/components/IFoodOverviewTab.js +118 -0
  63. package/src/react/pages/IFoodIntegrationPage/components/IFoodPreviewModal.js +103 -0
  64. package/src/react/pages/IFoodIntegrationPage/components/IFoodProductCard.js +1 -0
  65. package/src/react/pages/IFoodIntegrationPage/components/IFoodStoreTab.js +150 -0
  66. package/src/react/pages/IFoodIntegrationPage/index.js +1298 -0
  67. package/src/react/pages/IFoodIntegrationPage/styles.js +764 -0
  68. package/src/react/pages/IFoodIntegrationPage/utils.js +91 -0
  69. package/src/react/pages/IFoodIntegrationPage.js +1 -0
  70. package/src/react/pages/IFoodIntegrationPage.styles.js +1 -0
  71. package/src/react/pages/Imports.js +214 -166
  72. package/src/react/pages/IntegrationConfigPage.js +744 -0
  73. package/src/react/pages/IntegrationConfigPage.styles.js +269 -0
  74. package/src/react/pages/Integrations.js +299 -0
  75. package/src/react/pages/Integrations.styles.js +130 -0
  76. package/src/react/pages/ManagerOrderNotificationsPage.js +642 -0
  77. package/src/react/pages/MarketplaceIntegrationPage/components/MarketplaceProductCard.js +205 -0
  78. package/src/react/pages/MarketplaceIntegrationPage/components/RemoteMarketplaceMenuTab.js +335 -0
  79. package/src/react/pages/MarketplaceIntegrationPage/index.js +60 -0
  80. package/src/react/pages/MarketplaceIntegrationPage/utils.js +137 -0
  81. package/src/react/pages/MercadoLivreIntegrationPage.js +538 -0
  82. package/src/react/pages/PrinterDeviceDetailPage.js +1916 -0
  83. package/src/react/pages/PrinterDeviceDetailPage.styles.js +223 -0
  84. package/src/react/pages/PrinterDeviceFormPage.js +521 -0
  85. package/src/react/pages/PrinterDeviceFormPage.styles.js +159 -0
  86. package/src/react/pages/WhatsAppConnectionPage.js +459 -0
  87. package/src/react/pages/WhatsAppConnectionPage.styles.js +284 -0
  88. package/src/react/pages/integrationsCatalog.js +399 -0
  89. package/src/react/services/Cielo/Cielo.js +193 -193
  90. package/src/react/utils/categoryContexts.js +46 -0
  91. package/src/react/utils/entityDisplay.js +222 -217
  92. package/src/react/utils/fileUrl.js +39 -20
  93. package/src/react/utils/integrationPage.js +16 -0
  94. package/src/react/utils/runtimeFooter.js +40 -22
  95. package/src/react/utils/runtimeLanguage.js +50 -9
  96. package/src/react/utils/runtimeMenu.js +191 -131
  97. package/src/react/utils/runtimeZoom.js +43 -0
  98. package/src/react/utils/shopConfig.js +27 -15
  99. package/src/react/utils/shopFranchises.js +126 -1
  100. package/src/react/utils/storeColumns.js +23 -4
  101. package/src/react/utils/userAvatar.js +104 -0
  102. package/src/store/categories/index.js +48 -20
  103. package/src/store/device_config/index.js +62 -21
  104. package/src/store/entity_log/index.js +22 -1
  105. package/src/store/imports/customActions.js +51 -0
  106. package/src/store/imports/index.js +89 -88
  107. package/src/store/status/index.js +11 -11
  108. package/src/store/themes/index.js +22 -0
  109. package/src/store/timezones/index.js +3 -1
  110. package/src/tests/browser/manager/connections.spec.js +143 -0
  111. package/src/tests/browser/manager/devices-current.spec.js +343 -0
  112. package/src/tests/browser/manager/mercado-livre-integration.spec.js +268 -0
  113. package/src/tests/react/api/deviceHeaders.test.js +114 -0
  114. package/src/tests/react/components/AddImportModal.test.js +166 -0
  115. package/src/tests/react/components/BottomNavigationBar.config.test.js +20 -0
  116. package/src/tests/react/components/UserAvatar.test.js +52 -0
  117. package/src/tests/react/pages/DevicePickerStyles.test.js +47 -0
  118. package/src/tests/react/pages/Devices/deviceListHelpers.test.js +39 -0
  119. package/src/tests/react/pages/DevicesCurrentDevice.test.js +198 -0
  120. package/src/tests/react/pages/connectionsHelpers.test.js +34 -0
  121. package/src/tests/react/pages/mercadoLivreIntegrationPageContract.test.mjs +16 -0
  122. package/src/tests/react/services/Cielo.test.js +190 -190
  123. package/src/tests/react/store/categoriesOrdering.test.js +26 -0
  124. package/src/tests/react/utils/appDomain.test.js +62 -0
  125. package/src/tests/react/utils/entityDisplay.test.js +2 -2
  126. package/src/tests/react/utils/fileUrl.test.js +3 -3
  127. package/src/tests/react/utils/formatter.test.mjs +33 -0
  128. package/src/tests/react/utils/runtimeFooter.test.js +29 -4
  129. package/src/tests/react/utils/runtimeLanguage.test.js +79 -9
  130. package/src/tests/react/utils/runtimeMenu.test.js +198 -134
  131. package/src/tests/react/utils/runtimeZoom.test.js +38 -0
  132. package/src/tests/react/utils/shopConfig.test.js +10 -9
  133. package/src/tests/react/utils/shopFranchises.test.js +43 -0
  134. package/src/tests/react/utils/translate.discovery.test.mjs +170 -0
  135. package/src/tests/react/utils/translate.test.helpers.mjs +152 -0
  136. package/src/tests/react/utils/translate.test.mjs +141 -349
  137. package/src/tests/react/utils/userAvatar.test.js +63 -0
  138. package/src/utils/appDomain.js +15 -4
  139. package/src/utils/formatter.js +104 -16
  140. package/src/utils/integrationConfigs.js +80 -80
  141. package/src/utils/translate.js +222 -438
  142. package/src/utils/translateDiscovery.js +96 -0
  143. package/src/utils/translateNormalize.js +33 -0
  144. package/src/utils/translatePending.js +92 -0
  145. package/src/utils/translateStorage.js +142 -0
  146. package/.github/agents/developer.agent.md +0 -30
  147. package/.github/agents/devops.agent.md +0 -30
  148. package/.github/agents/qa.agent.md +0 -30
  149. package/.github/agents/security.agent.md +0 -30
  150. package/.github/workflows/npmjs.yml +0 -35
@@ -0,0 +1,114 @@
1
+ const {afterAll, afterEach, beforeEach, describe, expect, it} = global;
2
+ const {jest} = require('@jest/globals');
3
+
4
+ jest.mock('react-native', () => ({
5
+ Platform: {
6
+ OS: 'web',
7
+ select: options => options.web || options.default || options.ios || options.android,
8
+ },
9
+ }));
10
+
11
+ const originalLocalStorage = global.localStorage;
12
+
13
+ global.localStorage = {
14
+ clear: jest.fn(),
15
+ getItem: jest.fn(() => '{}'),
16
+ removeItem: jest.fn(),
17
+ setItem: jest.fn(),
18
+ };
19
+
20
+ const {api} = require('@controleonline/ui-common/src/api');
21
+
22
+ describe('device request headers', () => {
23
+ const originalFetch = global.fetch;
24
+ const originalGetToken = api.getToken;
25
+
26
+ beforeEach(() => {
27
+ global.fetch = jest.fn().mockResolvedValue({
28
+ ok: true,
29
+ status: 200,
30
+ statusText: 'OK',
31
+ text: async () => JSON.stringify({'hydra:member': []}),
32
+ });
33
+ api.getToken = jest.fn().mockResolvedValue('session-token');
34
+ });
35
+
36
+ afterEach(() => {
37
+ global.fetch = originalFetch;
38
+ api.getToken = originalGetToken;
39
+ jest.restoreAllMocks();
40
+ });
41
+
42
+ afterAll(() => {
43
+ global.localStorage = originalLocalStorage;
44
+ });
45
+
46
+ it('sends the operational type from the same runtime device as the identifier', async () => {
47
+ global.localStorage.getItem.mockImplementation(key => {
48
+ if (key === 'device') {
49
+ return JSON.stringify({id: 'web-31477', type: 'PDV'});
50
+ }
51
+
52
+ return '{}';
53
+ });
54
+
55
+ await api.fetch('/product-showcases/catalog', {
56
+ params: {type: ['service', 'product']},
57
+ });
58
+
59
+ const [, options] = global.fetch.mock.calls[0];
60
+ expect(options.headers.get('DEVICE')).toBe('web-31477');
61
+ expect(options.headers.get('DEVICE-TYPE')).toBe('PDV');
62
+ });
63
+
64
+ it('keeps master device identifier and type paired when it is selected', async () => {
65
+ global.localStorage.getItem.mockImplementation(key => {
66
+ if (key === 'device') {
67
+ return JSON.stringify({id: 'web-31477', type: 'MANAGER'});
68
+ }
69
+
70
+ if (key === 'master-device') {
71
+ return JSON.stringify({id: 'terminal-cielo-01', type: 'PDV'});
72
+ }
73
+
74
+ return '{}';
75
+ });
76
+
77
+ await api.fetch('/product-showcases/catalog');
78
+
79
+ const [, options] = global.fetch.mock.calls[0];
80
+ expect(options.headers.get('DEVICE')).toBe('terminal-cielo-01');
81
+ expect(options.headers.get('DEVICE-TYPE')).toBe('PDV');
82
+ });
83
+
84
+ it('does not guess a device type when the selected device has none', async () => {
85
+ global.localStorage.getItem.mockImplementation(key =>
86
+ key === 'device' ? JSON.stringify({id: 'legacy-device'}) : '{}',
87
+ );
88
+
89
+ await api.fetch('/product-showcases/catalog');
90
+
91
+ const [, options] = global.fetch.mock.calls[0];
92
+ expect(options.headers.get('DEVICE')).toBe('legacy-device');
93
+ expect(options.headers.get('DEVICE-TYPE')).toBeNull();
94
+ });
95
+
96
+ it('keeps the app domain in the header and removes it from query params', async () => {
97
+ await api.fetch('/people/company/default?app-domain=loja.exemplo.com.br&existing=1', {
98
+ params: {
99
+ 'app-domain': 'loja.exemplo.com.br',
100
+ AppDomain: 'loja.exemplo.com.br',
101
+ q: 'shop',
102
+ },
103
+ });
104
+
105
+ const [url, options] = global.fetch.mock.calls[0];
106
+ const requestUrl = new URL(String(url));
107
+
108
+ expect(options.headers.get('App-Domain')).toBeTruthy();
109
+ expect(requestUrl.searchParams.has('app-domain')).toBe(false);
110
+ expect(requestUrl.searchParams.has('AppDomain')).toBe(false);
111
+ expect(requestUrl.searchParams.get('existing')).toBe('1');
112
+ expect(requestUrl.searchParams.get('q')).toBe('shop');
113
+ });
114
+ });
@@ -0,0 +1,166 @@
1
+ const React = require('react');
2
+ const renderer = require('react-test-renderer');
3
+ const {jest} = require('@jest/globals');
4
+
5
+ const {describe, expect, it, beforeEach} = global;
6
+
7
+ global.IS_REACT_ACT_ENVIRONMENT = true;
8
+
9
+ const importActions = {
10
+ uploadImportFile: jest.fn(),
11
+ };
12
+
13
+ jest.mock('@store', () => ({
14
+ useStore: jest.fn(name => {
15
+ if (name === 'people') {
16
+ return {
17
+ getters: {
18
+ currentCompany: {id: 7},
19
+ },
20
+ actions: {},
21
+ };
22
+ }
23
+
24
+ if (name === 'imports') {
25
+ return {
26
+ getters: {},
27
+ actions: importActions,
28
+ };
29
+ }
30
+
31
+ return {
32
+ getters: {},
33
+ actions: {},
34
+ };
35
+ }),
36
+ }));
37
+
38
+ jest.mock('@controleonline/ui-common/src/react/components/AnimatedModal', () =>
39
+ props => (props.visible ? React.createElement('modal', props, props.children) : null),
40
+ );
41
+
42
+ jest.mock('@controleonline/ui-common/src/react/components/MessageService', () => ({
43
+ useMessage: () => ({
44
+ showError: jest.fn(),
45
+ showSuccess: jest.fn(),
46
+ }),
47
+ }));
48
+
49
+ jest.mock('react-native-vector-icons/MaterialIcons', () => props =>
50
+ React.createElement('icon', props, props.children),
51
+ {virtual: true},
52
+ );
53
+
54
+ jest.mock('react-native-vector-icons/Feather', () => props =>
55
+ React.createElement('icon', props, props.children),
56
+ {virtual: true},
57
+ );
58
+
59
+ jest.mock('@controleonline/ui-default/src/react/components/upload/DefaultUpload', () => {
60
+ const React = require('react');
61
+
62
+ return props => {
63
+ const trigger =
64
+ typeof props.renderTrigger === 'function'
65
+ ? props.renderTrigger({
66
+ openManager: jest.fn(),
67
+ uploading: false,
68
+ })
69
+ : null;
70
+
71
+ return React.createElement(
72
+ 'defaultupload',
73
+ props,
74
+ trigger,
75
+ React.createElement(
76
+ 'touchableopacity',
77
+ {
78
+ onPress: () =>
79
+ props.onUploadFile({
80
+ file: {
81
+ name: 'import.csv',
82
+ uri: 'file:///import.csv',
83
+ mimeType: 'text/csv',
84
+ },
85
+ }),
86
+ },
87
+ React.createElement('text', null, 'Enviar via gerenciador'),
88
+ ),
89
+ );
90
+ };
91
+ });
92
+
93
+ jest.mock('react-native', () => ({
94
+ ActivityIndicator: props => React.createElement('activityindicator', props, props.children),
95
+ Platform: {OS: 'web'},
96
+ StyleSheet: {
97
+ create: value => value,
98
+ },
99
+ ScrollView: props => React.createElement('scrollview', props, props.children),
100
+ Text: props => React.createElement('text', props, props.children),
101
+ TouchableOpacity: props => React.createElement('touchableopacity', props, props.children),
102
+ View: props => React.createElement('view', props, props.children),
103
+ }));
104
+
105
+ const AddImportModal = require('@controleonline/ui-common/src/react/components/AddImportModal').default;
106
+
107
+ const collectText = node =>
108
+ node
109
+ .findAllByType('text')
110
+ .map(textNode => {
111
+ const children = textNode.props.children;
112
+ if (Array.isArray(children)) {
113
+ return children.flat(Infinity).join('');
114
+ }
115
+ return children || '';
116
+ })
117
+ .join('');
118
+
119
+ const findButtonByLabel = (root, label) =>
120
+ root.findAllByType('touchableopacity').find(button => collectText(button).includes(label));
121
+
122
+ const findButtonByExactLabel = (root, label) =>
123
+ root.findAllByType('touchableopacity').find(button => collectText(button) === label);
124
+
125
+ describe('AddImportModal', () => {
126
+ beforeEach(() => {
127
+ jest.clearAllMocks();
128
+ global.t = {
129
+ t: (_scope, _kind, key) => key,
130
+ };
131
+
132
+ global.document = undefined;
133
+ });
134
+
135
+ it('usa a action do store imports para enviar o arquivo', async () => {
136
+ importActions.uploadImportFile.mockResolvedValue({});
137
+ let tree;
138
+
139
+ await renderer.act(async () => {
140
+ tree = renderer.create(
141
+ React.createElement(AddImportModal, {
142
+ visible: true,
143
+ onClose: jest.fn(),
144
+ onSuccess: jest.fn(),
145
+ context: {context: 'product'},
146
+ }),
147
+ );
148
+ });
149
+
150
+ const selectButton = findButtonByLabel(tree.root, 'select_file');
151
+ expect(selectButton).toBeTruthy();
152
+
153
+ const importButton = findButtonByExactLabel(tree.root, 'Enviar via gerenciador');
154
+ expect(importButton).toBeTruthy();
155
+
156
+ await renderer.act(async () => {
157
+ await importButton.props.onPress();
158
+ });
159
+
160
+ expect(importActions.uploadImportFile).toHaveBeenCalledWith({
161
+ file: expect.objectContaining({name: 'import.csv'}),
162
+ importType: 'product',
163
+ peopleId: 7,
164
+ });
165
+ });
166
+ });
@@ -0,0 +1,20 @@
1
+ import {
2
+ resolveBottomNavigationItems,
3
+ BOTTOM_NAVIGATION_PRESETS,
4
+ } from '../../../react/components/BottomNavigationBar.config';
5
+
6
+ describe('BottomNavigationBar.config', () => {
7
+ it('uses Portuguese fallbacks when translate is missing', () => {
8
+ const items = resolveBottomNavigationItems(
9
+ BOTTOM_NAVIGATION_PRESETS.crmToolbar.items,
10
+ null,
11
+ );
12
+ const labels = items.map(i => i.label);
13
+ expect(labels).toContain('Início');
14
+ expect(labels).toContain('Clientes');
15
+ expect(labels).toContain('Perfil');
16
+ expect(labels).not.toContain('Home');
17
+ expect(labels).not.toContain('Clients');
18
+ expect(labels).not.toContain('Profile');
19
+ });
20
+ });
@@ -0,0 +1,52 @@
1
+ const React = require('react');
2
+ const renderer = require('react-test-renderer');
3
+ const {jest} = require('@jest/globals');
4
+
5
+ jest.mock('react-native', () => ({
6
+ Image: props => React.createElement('Image', props),
7
+ StyleSheet: {
8
+ create: styles => styles,
9
+ },
10
+ Text: props => React.createElement('Text', props, props.children),
11
+ View: props => React.createElement('View', props, props.children),
12
+ }));
13
+
14
+ const UserAvatar = require('../../../react/components/UserAvatar').default;
15
+
16
+ const {describe, expect, it} = global;
17
+
18
+ describe('UserAvatar', () => {
19
+ it('renders initials without requesting gravatar when disabled', () => {
20
+ let tree;
21
+
22
+ renderer.act(() => {
23
+ tree = renderer.create(
24
+ React.createElement(UserAvatar, {
25
+ email: 'client@example.com',
26
+ name: 'Client Test',
27
+ useGravatar: false,
28
+ }),
29
+ );
30
+ });
31
+
32
+ expect(tree.root.findAllByType('Image')).toHaveLength(0);
33
+ expect(tree.root.findByType('Text').props.children).toBe('CT');
34
+ });
35
+
36
+ it('uses gravatar as fallback when enabled', () => {
37
+ let tree;
38
+
39
+ renderer.act(() => {
40
+ tree = renderer.create(
41
+ React.createElement(UserAvatar, {
42
+ email: 'client@example.com',
43
+ name: 'Client Test',
44
+ }),
45
+ );
46
+ });
47
+
48
+ const image = tree.root.findByType('Image');
49
+
50
+ expect(image.props.source.uri).toContain('gravatar.com/avatar/');
51
+ });
52
+ });
@@ -0,0 +1,47 @@
1
+ const assert = require('node:assert/strict');
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+ const {test} = require('@jest/globals');
5
+
6
+ const readFixture = relativePath =>
7
+ fs.readFileSync(path.resolve(__dirname, relativePath), 'utf8');
8
+
9
+ test('device detail display pickers keep explicit native styling hooks', () => {
10
+ const source = readFixture('../../../react/pages/DeviceDetailPage.js');
11
+ const styles = readFixture('../../../react/pages/DeviceDetailPage.styles.js');
12
+
13
+ assert.match(
14
+ source,
15
+ /selectedValue=\{linkedDisplayId \|\| ''\}[\s\S]*style=\{styles\.picker\}[\s\S]*dropdownIconColor="#64748B"/,
16
+ );
17
+ assert.match(
18
+ source,
19
+ /selectedValue=\{displayPrinterId \|\| ''\}[\s\S]*style=\{styles\.picker\}[\s\S]*dropdownIconColor="#64748B"/,
20
+ );
21
+ assert.match(
22
+ source,
23
+ /selectedValue=\{devicePaymentTarget \|\| ''\}[\s\S]*style=\{styles\.picker\}[\s\S]*dropdownIconColor="#64748B"/,
24
+ );
25
+ assert.match(
26
+ styles,
27
+ /picker:\s*\{[\s\S]*minHeight:\s*44,[\s\S]*color:\s*'#0F172A'[\s\S]*backgroundColor:\s*'#F8FAFC'/,
28
+ );
29
+ });
30
+
31
+ test('printer device detail pickers keep explicit native styling hooks', () => {
32
+ const source = readFixture('../../../react/pages/PrinterDeviceDetailPage.js');
33
+ const styles = readFixture('../../../react/pages/PrinterDeviceDetailPage.styles.js');
34
+
35
+ assert.match(
36
+ source,
37
+ /selectedValue=\{protocol \|\| DEFAULT_NETWORK_CAMERA_PROTOCOL\}[\s\S]*style=\{styles\.picker\}[\s\S]*dropdownIconColor="#64748B"/,
38
+ );
39
+ assert.match(
40
+ source,
41
+ /selectedValue=\{managerDeviceId \|\| ''\}[\s\S]*style=\{styles\.picker\}[\s\S]*dropdownIconColor="#64748B"/,
42
+ );
43
+ assert.match(
44
+ styles,
45
+ /picker:\s*\{[\s\S]*minHeight:\s*44,[\s\S]*color:\s*'#0F172A'[\s\S]*backgroundColor:\s*'#F8FAFC'/,
46
+ );
47
+ });
@@ -0,0 +1,39 @@
1
+ /* global describe, expect, it */
2
+
3
+ const {
4
+ mergeDeviceConfigs,
5
+ buildDeviceListParams,
6
+ PAGE_SIZE,
7
+ } = require('../../../../react/pages/Devices/deviceTypes/deviceListHelpers');
8
+
9
+ describe('deviceListHelpers', () => {
10
+ it('mergeDeviceConfigs dedupes by id', () => {
11
+ const merged = mergeDeviceConfigs(
12
+ [{id: 1, type: 'PDV'}, {id: 2, type: 'PRINTER'}],
13
+ [{id: 2, type: 'PRINTER'}, {id: 3, type: 'DISPLAY'}],
14
+ );
15
+ expect(merged.map(item => item.id)).toEqual([1, 2, 3]);
16
+ });
17
+
18
+ it('buildDeviceListParams sets people and single type', () => {
19
+ const params = buildDeviceListParams({
20
+ companyId: 9,
21
+ page: 1,
22
+ pageSize: PAGE_SIZE,
23
+ queryTypes: ['PDV'],
24
+ });
25
+ expect(params.people).toBe('/people/9');
26
+ expect(params.type).toBe('PDV');
27
+ expect(params.itemsPerPage).toBe(PAGE_SIZE);
28
+ });
29
+
30
+ it('buildDeviceListParams accepts multiple types', () => {
31
+ const params = buildDeviceListParams({
32
+ companyId: 1,
33
+ page: 2,
34
+ queryTypes: ['PDV', 'PRINTER'],
35
+ });
36
+ expect(params.type).toEqual(['PDV', 'PRINTER']);
37
+ expect(params.page).toBe(2);
38
+ });
39
+ });
@@ -0,0 +1,198 @@
1
+ /* global describe, expect, it, jest */
2
+
3
+ jest.mock(
4
+ '@controleonline/ui-common/src/react/utils/printerDevices',
5
+ () => ({
6
+ PDV_DEVICE_TYPE: 'PDV',
7
+ normalizeDeviceType: value =>
8
+ String(value || '')
9
+ .trim()
10
+ .toUpperCase(),
11
+ }),
12
+ );
13
+
14
+ jest.mock(
15
+ '@controleonline/ui-common/src/react/utils/paymentDevices',
16
+ () => ({
17
+ normalizeDeviceId: value => String(value || '').trim(),
18
+ normalizeEntityId: value =>
19
+ String(value || '')
20
+ .replace(/\D+/g, '')
21
+ .trim(),
22
+ }),
23
+ );
24
+
25
+ const {
26
+ findDeviceConfigByType,
27
+ filterDeviceConfigsByTypes,
28
+ getRuntimeDeviceIdentifier,
29
+ groupDeviceConfigs,
30
+ hasCurrentPdvConfig,
31
+ isCurrentDeviceConfig,
32
+ isCurrentDeviceGroup,
33
+ prioritizeCurrentDeviceGroups,
34
+ prioritizeCurrentDeviceConfigs,
35
+ readStoredRuntimeDevice,
36
+ } = require('../../../react/pages/Devices/currentDevice');
37
+
38
+ const currentManager = {
39
+ id: 487,
40
+ type: 'MANAGER',
41
+ device: {
42
+ id: 396,
43
+ device: '0d1bea46f3d93f07',
44
+ alias: 'Caixa',
45
+ },
46
+ };
47
+
48
+ const currentPdv = {
49
+ id: 488,
50
+ type: 'PDV',
51
+ device: {
52
+ id: 396,
53
+ device: '0d1bea46f3d93f07',
54
+ alias: 'Caixa',
55
+ },
56
+ };
57
+
58
+ const remotePdv = {
59
+ id: 410,
60
+ type: 'PDV',
61
+ device: {
62
+ id: 380,
63
+ device: '61378bea41ff1712',
64
+ alias: 'Outro caixa',
65
+ },
66
+ };
67
+
68
+ describe('current device profiles', () => {
69
+ it('reads the runtime identity from persisted device storage', () => {
70
+ const storage = {
71
+ getItem: jest.fn(() =>
72
+ JSON.stringify({
73
+ id: '0d1bea46f3d93f07',
74
+ type: 'MANAGER',
75
+ }),
76
+ ),
77
+ };
78
+
79
+ const runtimeDevice = readStoredRuntimeDevice(storage);
80
+
81
+ expect(getRuntimeDeviceIdentifier(runtimeDevice)).toBe(
82
+ '0d1bea46f3d93f07',
83
+ );
84
+ });
85
+
86
+ it('does not fail when persisted device storage is invalid', () => {
87
+ const storage = {
88
+ getItem: jest.fn(() => '{invalid'),
89
+ };
90
+
91
+ expect(readStoredRuntimeDevice(storage)).toEqual({});
92
+ });
93
+
94
+ it('recognizes every operational profile of the same physical device', () => {
95
+ const runtimeDeviceIdentifier = '0d1bea46f3d93f07';
96
+
97
+ expect(
98
+ isCurrentDeviceConfig({
99
+ deviceConfig: currentManager,
100
+ runtimeDeviceIdentifier,
101
+ }),
102
+ ).toBe(true);
103
+ expect(
104
+ isCurrentDeviceConfig({
105
+ deviceConfig: currentPdv,
106
+ runtimeDeviceIdentifier,
107
+ }),
108
+ ).toBe(true);
109
+ expect(
110
+ isCurrentDeviceConfig({
111
+ deviceConfig: remotePdv,
112
+ runtimeDeviceIdentifier,
113
+ }),
114
+ ).toBe(false);
115
+ });
116
+
117
+ it('pins all current profiles before remote devices without changing their order', () => {
118
+ expect(
119
+ prioritizeCurrentDeviceConfigs(
120
+ [remotePdv, currentManager, currentPdv],
121
+ '0d1bea46f3d93f07',
122
+ ).map(item => item.id),
123
+ ).toEqual([487, 488, 410]);
124
+ });
125
+
126
+ it('keeps only current PDV profiles when the PDV filter is active', () => {
127
+ expect(
128
+ filterDeviceConfigsByTypes(
129
+ [currentManager, currentPdv],
130
+ ['PDV'],
131
+ ).map(item => item.id),
132
+ ).toEqual([488]);
133
+ });
134
+
135
+ it('offers PDV setup only until the current physical device has a PDV profile', () => {
136
+ expect(
137
+ hasCurrentPdvConfig(
138
+ [currentManager, remotePdv],
139
+ '0d1bea46f3d93f07',
140
+ ),
141
+ ).toBe(false);
142
+ expect(
143
+ hasCurrentPdvConfig(
144
+ [currentManager, currentPdv, remotePdv],
145
+ '0d1bea46f3d93f07',
146
+ ),
147
+ ).toBe(true);
148
+ });
149
+
150
+ it('groups every configuration of the same Device into one card', () => {
151
+ const groups = groupDeviceConfigs([
152
+ remotePdv,
153
+ currentManager,
154
+ currentPdv,
155
+ ]);
156
+
157
+ expect(groups).toHaveLength(2);
158
+ expect(groups.find(group => group.key === 'device:396').deviceConfigs)
159
+ .toEqual([currentManager, currentPdv]);
160
+ });
161
+
162
+ it('pins the current Device once and resolves the session configuration by type', () => {
163
+ const groups = prioritizeCurrentDeviceGroups(
164
+ groupDeviceConfigs([remotePdv, currentManager, currentPdv]),
165
+ '0d1bea46f3d93f07',
166
+ );
167
+
168
+ expect(groups[0].key).toBe('device:396');
169
+ expect(
170
+ isCurrentDeviceGroup({
171
+ deviceGroup: groups[0],
172
+ runtimeDeviceIdentifier: '0d1bea46f3d93f07',
173
+ }),
174
+ ).toBe(true);
175
+ expect(findDeviceConfigByType(groups[0], 'PDV')).toBe(currentPdv);
176
+ });
177
+
178
+ it('creates a synthetic current Device group when it has no configuration yet', () => {
179
+ const groups = groupDeviceConfigs([remotePdv], {
180
+ includeRuntimeDevice: true,
181
+ runtimeDevice: {
182
+ id: 'new-device',
183
+ alias: 'Nova maquininha',
184
+ type: 'MANAGER',
185
+ },
186
+ });
187
+
188
+ expect(groups).toHaveLength(2);
189
+ expect(groups[1]).toMatchObject({
190
+ key: 'identifier:new-device',
191
+ device: {
192
+ device: 'new-device',
193
+ alias: 'Nova maquininha',
194
+ },
195
+ deviceConfigs: [],
196
+ });
197
+ });
198
+ });
@@ -0,0 +1,34 @@
1
+ /* global describe, expect, it */
2
+
3
+ describe('connections phone/status helpers (migration #288)', () => {
4
+ const formatPhoneLabel = phone => {
5
+ if (!phone) return 'Nao informado';
6
+ if (typeof phone === 'string') return phone;
7
+ const ddd = String(phone?.ddd || '').trim();
8
+ const digits = String(phone?.phone || '').replace(/\D/g, '');
9
+ if (!ddd && !digits) return 'Nao informado';
10
+ if (!digits) return `(${ddd})`;
11
+ const lastFour = digits.slice(-4);
12
+ const firstPart = digits.slice(0, -4);
13
+ if (!firstPart) return ddd ? `(${ddd}) ${lastFour}` : lastFour;
14
+ return ddd ? `(${ddd}) ${firstPart}-${lastFour}` : `${firstPart}-${lastFour}`;
15
+ };
16
+
17
+ const formatStatusLabel = status => {
18
+ if (!status) return '—';
19
+ if (typeof status === 'string') return status;
20
+ return status?.status || status?.name || '—';
21
+ };
22
+
23
+ it('formats phone objects', () => {
24
+ expect(formatPhoneLabel({ddd: '11', phone: '988887777'})).toBe('(11) 98888-7777');
25
+ });
26
+
27
+ it('handles missing phone', () => {
28
+ expect(formatPhoneLabel(null)).toBe('Nao informado');
29
+ });
30
+
31
+ it('formats status objects', () => {
32
+ expect(formatStatusLabel({status: 'connected'})).toBe('connected');
33
+ });
34
+ });
@@ -0,0 +1,16 @@
1
+ import assert from 'node:assert/strict';
2
+ import { readFileSync } from 'node:fs';
3
+ import { resolve } from 'node:path';
4
+ import test from 'node:test';
5
+
6
+ const pagePath = resolve('modules/controleonline/ui-common/src/react/pages/MercadoLivreIntegrationPage.js');
7
+ const pageSource = readFileSync(pagePath, 'utf8');
8
+
9
+ test('MercadoLivreIntegrationPage keeps the backend contract for connect and import actions', () => {
10
+ assert.match(pageSource, /\/marketplace\/integrations\/mercadolivre\/authorization-page/);
11
+ assert.match(pageSource, /\/marketplace\/integrations\/mercadolivre\/products\/import/);
12
+ assert.match(pageSource, /provider_id/);
13
+ assert.match(pageSource, /showcase_id/);
14
+ assert.doesNotMatch(pageSource, /callback_url/);
15
+ assert.doesNotMatch(pageSource, /mercadolivre\/oauth\/return/);
16
+ });