@controleonline/ui-common 1.2.80 → 1.2.82

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 (90) hide show
  1. package/AGENTS.md +8 -0
  2. package/package.json +9 -3
  3. package/src/api/fetch.js +7 -7
  4. package/src/react/components/AddImportModal.js +6 -2
  5. package/src/react/components/BackgroundRuntimeBridge.js +201 -215
  6. package/src/react/components/ConfirmModal.js +44 -9
  7. package/src/react/components/ConfirmModal.styles.js +55 -41
  8. package/src/react/components/CopyDeviceConfigModal.js +193 -0
  9. package/src/react/components/CopyDeviceConfigModal.styles.js +123 -0
  10. package/src/react/components/DefaultProvider.native.js +22 -0
  11. package/src/react/components/DefaultProvider.web.js +25 -1
  12. package/src/react/components/EntityLogBranch.js +482 -0
  13. package/src/react/components/EntityLogCards.js +369 -0
  14. package/src/react/components/EntityLogContent.js +113 -794
  15. package/src/react/components/LinkedOrderProductsTab.js +3 -3
  16. package/src/react/components/PrintService.js +130 -409
  17. package/src/react/components/RemoteCheckoutService.js +2 -2
  18. package/src/react/components/RuntimeBottomNavigationBar.js +141 -146
  19. package/src/react/components/RuntimeFooterMarqueeText.js +137 -0
  20. package/src/react/components/RuntimeInfoFooter.js +12 -15
  21. package/src/react/components/SystemErrorToast.js +49 -29
  22. package/src/react/components/SystemErrorToast.styles.js +43 -38
  23. package/src/react/components/UserAvatar.js +120 -28
  24. package/src/react/config/deviceConfigBootstrap.js +330 -304
  25. package/src/react/hooks/usePrintSpoolEffects.js +161 -0
  26. package/src/react/pages/DeviceDetailPage.js +6 -2882
  27. package/src/react/pages/Devices/detail/DeviceDetailAlertsCommandsSection.js +233 -0
  28. package/src/react/pages/Devices/detail/DeviceDetailHeader.js +138 -0
  29. package/src/react/pages/Devices/detail/DeviceDetailMovementSections.js +167 -0
  30. package/src/react/pages/Devices/detail/DeviceDetailOrdersPrintSection.js +216 -0
  31. package/src/react/pages/Devices/detail/DeviceDetailPaymentSection.js +44 -0
  32. package/src/react/pages/Devices/detail/DeviceDetailPdvConfigSection.js +390 -0
  33. package/src/react/pages/Devices/detail/DeviceDetailRenderers.js +135 -0
  34. package/src/react/pages/Devices/detail/DeviceDetailScreen.js +495 -0
  35. package/src/react/pages/Devices/detail/OptionButtonChip.js +65 -0
  36. package/src/react/pages/Devices/detail/deviceDetailConstants.js +36 -0
  37. package/src/react/pages/Devices/detail/deviceDetailHelpers.js +70 -0
  38. package/src/react/pages/Devices/detail/useDeviceDetailActions.js +483 -0
  39. package/src/react/pages/Devices/detail/useDeviceDetailCopyConfig.js +87 -0
  40. package/src/react/pages/Devices/detail/useDeviceDetailLoaders.js +327 -0
  41. package/src/react/pages/Devices/detail/useDeviceDetailSaves.js +429 -0
  42. package/src/react/pages/Devices/detail/useDeviceDetailStateA.js +489 -0
  43. package/src/react/pages/Devices/detail/useDeviceDetailStateB.js +98 -0
  44. package/src/react/pages/Devices/deviceTypes/index.js +2 -0
  45. package/src/react/pages/Devices/deviceTypes/pdv.js +11 -0
  46. package/src/react/pages/Imports.js +3 -2
  47. package/src/react/pages/IntegrationConfigFields.js +126 -0
  48. package/src/react/pages/IntegrationConfigPage.js +122 -591
  49. package/src/react/pages/IntegrationConfigPage.utils.js +95 -0
  50. package/src/react/print/usePrintButtonController.js +9 -1
  51. package/src/react/services/Getnet/Checkout.js +81 -0
  52. package/src/react/services/Getnet/Getnet.js +77 -0
  53. package/src/react/services/paymentGatewayExecution.js +20 -0
  54. package/src/react/utils/bottomNavigationToolbar.js +161 -0
  55. package/src/react/utils/commercialDocumentOrders.js +18 -6
  56. package/src/react/utils/copyDeviceConfigs.js +198 -0
  57. package/src/react/utils/fileUrl.js +43 -0
  58. package/src/react/utils/importStatus.js +19 -10
  59. package/src/react/utils/logSettings.js +7 -7
  60. package/src/react/utils/maintenanceSettings.js +16 -0
  61. package/src/react/utils/paymentDevices.js +6 -0
  62. package/src/react/utils/printManagedPrinter.js +45 -0
  63. package/src/react/utils/printRouting.js +63 -0
  64. package/src/react/utils/printSpoolUtils.js +119 -0
  65. package/src/react/utils/printerDevices.js +368 -368
  66. package/src/react/utils/runtimeFooter.js +10 -19
  67. package/src/react/utils/shopFranchises.js +186 -65
  68. package/src/react/utils/systemErrorMessage.js +71 -67
  69. package/src/store/device_config/customActions.js +119 -30
  70. package/src/tests/browser/manager/bottom-navigation-role-toolbar.spec.js +353 -0
  71. package/src/tests/browser/manager/device-detail-alias-save.spec.js +345 -0
  72. package/src/tests/browser/manager/device-detail-copy-config.spec.js +365 -0
  73. package/src/tests/browser/manager/runtime-footer-marquee.spec.js +262 -0
  74. package/src/tests/browser/manager/theme-confirm-toast.spec.js +236 -0
  75. package/src/tests/react/components/RuntimeFooterMarqueeText.test.js +171 -0
  76. package/src/tests/react/components/UserAvatar.test.js +6 -2
  77. package/src/tests/react/components/themeAwareModals.test.mjs +60 -0
  78. package/src/tests/react/config/deviceConfigBootstrap.test.js +104 -66
  79. package/src/tests/react/pages/deviceDetailDelete.test.js +78 -0
  80. package/src/tests/react/pages/receitaFederalIntegrationContract.test.mjs +48 -0
  81. package/src/tests/react/utils/bottomNavigationToolbar.test.js +222 -0
  82. package/src/tests/react/utils/copyDeviceConfigs.test.mjs +127 -0
  83. package/src/tests/react/utils/fileUrl.test.js +38 -0
  84. package/src/tests/react/utils/importStatus.test.js +26 -9
  85. package/src/tests/react/utils/passwordPolicy.test.js +27 -0
  86. package/src/tests/react/utils/printRouting.test.mjs +86 -0
  87. package/src/tests/react/utils/printSpoolUtils.test.mjs +52 -0
  88. package/src/tests/react/utils/runtimeFooter.test.js +7 -0
  89. package/src/tests/react/utils/shopFranchises.test.js +121 -115
  90. package/src/tests/react/utils/systemErrorMessage.test.js +32 -59
@@ -0,0 +1,60 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import {readFileSync} from 'node:fs';
4
+ import {fileURLToPath} from 'node:url';
5
+ import {dirname, join} from 'node:path';
6
+
7
+ const root = join(dirname(fileURLToPath(import.meta.url)), '../../..');
8
+
9
+ const FORBIDDEN_COLOR = /#[0-9a-fA-F]{3,8}\b|\brgba?\s*\(|\bwhite\b|\bblack\b/i;
10
+
11
+ function loadSource(rel) {
12
+ return readFileSync(join(root, rel), 'utf8');
13
+ }
14
+
15
+ test('ConfirmModal.js sources colors only from themeStore getters.colors', () => {
16
+ const src = loadSource('react/components/ConfirmModal.js');
17
+ assert.match(src, /themeStore\.getters\.colors|themeColors/);
18
+ assert.match(src, /modalOverlay|modalBackground|buttonBackground/);
19
+ assert.doesNotMatch(
20
+ src.replace(/transparent/g, ''),
21
+ FORBIDDEN_COLOR,
22
+ 'ConfirmModal.js must not hardcode colors',
23
+ );
24
+ });
25
+
26
+ test('ConfirmModal.styles.js createStyles uses only palette tokens', () => {
27
+ const src = loadSource('react/components/ConfirmModal.styles.js');
28
+ assert.match(src, /palette\.modalOverlay/);
29
+ assert.match(src, /palette\.modalBackground/);
30
+ assert.match(src, /palette\.buttonBackground/);
31
+ assert.match(src, /palette\.buttonText/);
32
+ // strip comments and the word transparent (Modal prop, not a color literal in styles)
33
+ const stripped = src
34
+ .replace(/\/\*[\s\S]*?\*\//g, '')
35
+ .replace(/\/\/.*$/gm, '')
36
+ .replace(/transparent/gi, '');
37
+ assert.doesNotMatch(stripped, FORBIDDEN_COLOR);
38
+ });
39
+
40
+ test('SystemErrorToast.js sources colors only from themeStore getters.colors', () => {
41
+ const src = loadSource('react/components/SystemErrorToast.js');
42
+ assert.match(src, /themeStore\.getters\.colors|themeColors/);
43
+ assert.match(src, /toastDangerBackground|toastDangerText/);
44
+ assert.doesNotMatch(
45
+ src.replace(/transparent/g, ''),
46
+ FORBIDDEN_COLOR,
47
+ 'SystemErrorToast.js must not hardcode colors',
48
+ );
49
+ });
50
+
51
+ test('SystemErrorToast.styles.js createStyles uses only palette tokens', () => {
52
+ const src = loadSource('react/components/SystemErrorToast.styles.js');
53
+ assert.match(src, /palette\.toastDangerBackground/);
54
+ assert.match(src, /palette\.toastDangerText/);
55
+ const stripped = src
56
+ .replace(/\/\*[\s\S]*?\*\//g, '')
57
+ .replace(/\/\/.*$/gm, '')
58
+ .replace(/transparent/gi, '');
59
+ assert.doesNotMatch(stripped, FORBIDDEN_COLOR);
60
+ });
@@ -1,10 +1,10 @@
1
- const {jest} = require('@jest/globals')
2
-
3
- jest.mock('../../../react/utils/screenMetrics', () => ({
4
- appendScreenMetrics: configs => configs,
5
- hasScreenMetricsChanges: () => false,
6
- }))
7
-
1
+ const {jest} = require('@jest/globals')
2
+
3
+ jest.mock('../../../react/utils/screenMetrics', () => ({
4
+ appendScreenMetrics: configs => configs,
5
+ hasScreenMetricsChanges: () => false,
6
+ }))
7
+
8
8
  const {
9
9
  buildProviderManagedDeviceConfigs,
10
10
  DEVICE_ANDROID_KIOSK_ENABLED_CONFIG_KEY,
@@ -36,22 +36,24 @@ const {
36
36
  resolvePosCashManagementMode,
37
37
  resolvePosOperationMode,
38
38
  resolvePosPrintMode,
39
+ POS_LOCAL_CHARGE_ENABLED_CONFIG_KEY,
40
+ isPosLocalChargeEnabled,
39
41
  resolvePosCheckOrderType,
40
42
  shouldEnableAndroidKioskMode,
41
43
  shouldEnableAndroidLauncherMode,
42
44
  shouldUsePosCashRegisterLifecycle,
43
45
  isPosTotemMode,
44
46
  } = require('../../../react/config/deviceConfigBootstrap')
45
-
46
- const {describe, expect, it} = global
47
-
48
- describe('deviceConfigBootstrap POS operation helpers', () => {
47
+
48
+ const {describe, expect, it} = global
49
+
50
+ describe('deviceConfigBootstrap POS operation helpers', () => {
49
51
  it('normalizes balcão as counter and treats counter as self service', () => {
50
52
  const configs = {
51
53
  [POS_OPERATION_MODE_CONFIG_KEY]: 'balcao',
52
54
  }
53
-
54
- expect(resolvePosOperationMode(configs)).toBe('counter')
55
+
56
+ expect(resolvePosOperationMode(configs)).toBe('counter')
55
57
  expect(isPosCounterMode(configs)).toBe(true)
56
58
  expect(isPosSelfServiceMode(configs)).toBe(true)
57
59
  })
@@ -65,52 +67,52 @@ describe('deviceConfigBootstrap POS operation helpers', () => {
65
67
  expect(isPosSingleItemMode(configs)).toBe(true)
66
68
  expect(isPosSelfServiceMode(configs)).toBe(false)
67
69
  })
68
-
69
- it('keeps counter auto print disabled by default and allows explicit enablement', () => {
70
- const counterConfigs = {
71
- [POS_OPERATION_MODE_CONFIG_KEY]: 'counter',
72
- }
73
-
74
- expect(isPosAutoPrintEnabled(counterConfigs)).toBe(false)
75
-
76
- expect(
77
- isPosAutoPrintEnabled({
78
- ...counterConfigs,
79
- [POS_AUTO_PRINT_ENABLED_CONFIG_KEY]: '1',
80
- }),
81
- ).toBe(true)
82
- })
83
-
84
- it('defaults non-counter modes to auto print enabled', () => {
85
- expect(
86
- isPosAutoPrintEnabled({
87
- [POS_OPERATION_MODE_CONFIG_KEY]: 'cashier',
88
- }),
89
- ).toBe(true)
90
- })
91
-
92
- it('resolves counter daily close mode and skips cash register lifecycle', () => {
93
- const configs = {
94
- [POS_OPERATION_MODE_CONFIG_KEY]: 'counter',
95
- [POS_CASH_MANAGEMENT_MODE_CONFIG_KEY]: 'daily',
96
- }
97
-
98
- expect(resolvePosCashManagementMode(configs)).toBe('daily')
99
- expect(shouldUsePosCashRegisterLifecycle(configs)).toBe(false)
100
- expect(isPosCashRegisterOpen(configs)).toBe(true)
101
- expect(isPosCashRegisterClosed(configs)).toBe(false)
102
- })
103
-
70
+
71
+ it('keeps counter auto print disabled by default and allows explicit enablement', () => {
72
+ const counterConfigs = {
73
+ [POS_OPERATION_MODE_CONFIG_KEY]: 'counter',
74
+ }
75
+
76
+ expect(isPosAutoPrintEnabled(counterConfigs)).toBe(false)
77
+
78
+ expect(
79
+ isPosAutoPrintEnabled({
80
+ ...counterConfigs,
81
+ [POS_AUTO_PRINT_ENABLED_CONFIG_KEY]: '1',
82
+ }),
83
+ ).toBe(true)
84
+ })
85
+
86
+ it('defaults non-counter modes to auto print enabled', () => {
87
+ expect(
88
+ isPosAutoPrintEnabled({
89
+ [POS_OPERATION_MODE_CONFIG_KEY]: 'cashier',
90
+ }),
91
+ ).toBe(true)
92
+ })
93
+
94
+ it('resolves counter daily close mode and skips cash register lifecycle', () => {
95
+ const configs = {
96
+ [POS_OPERATION_MODE_CONFIG_KEY]: 'counter',
97
+ [POS_CASH_MANAGEMENT_MODE_CONFIG_KEY]: 'daily',
98
+ }
99
+
100
+ expect(resolvePosCashManagementMode(configs)).toBe('daily')
101
+ expect(shouldUsePosCashRegisterLifecycle(configs)).toBe(false)
102
+ expect(isPosCashRegisterOpen(configs)).toBe(true)
103
+ expect(isPosCashRegisterClosed(configs)).toBe(false)
104
+ })
105
+
104
106
  it('requires cash register lifecycle for counter with open/close policy', () => {
105
107
  const closedConfigs = {
106
108
  [POS_OPERATION_MODE_CONFIG_KEY]: 'counter',
107
109
  [POS_CASH_MANAGEMENT_MODE_CONFIG_KEY]: 'cash-register',
108
110
  'cash-wallet-closed-id': 1,
109
111
  }
110
-
111
- expect(shouldUsePosCashRegisterLifecycle(closedConfigs)).toBe(true)
112
- expect(isPosCashRegisterClosed(closedConfigs)).toBe(true)
113
-
112
+
113
+ expect(shouldUsePosCashRegisterLifecycle(closedConfigs)).toBe(true)
114
+ expect(isPosCashRegisterClosed(closedConfigs)).toBe(true)
115
+
114
116
  expect(
115
117
  isPosCashRegisterOpen({
116
118
  ...closedConfigs,
@@ -369,16 +371,52 @@ describe('deviceConfigBootstrap POS operation helpers', () => {
369
371
  })
370
372
 
371
373
  it('keeps delivery enabled by default and respects explicit disablement', () => {
372
- expect(isPosDeliveryEnabled({})).toBe(true)
373
- expect(
374
- isPosDeliveryEnabled({
375
- [POS_DELIVERY_ENABLED_CONFIG_KEY]: '0',
376
- }),
377
- ).toBe(false)
378
- expect(
379
- isPosDeliveryEnabled({
380
- [POS_DELIVERY_ENABLED_CONFIG_KEY]: '1',
381
- }),
382
- ).toBe(true)
383
- })
384
- })
374
+ expect(isPosDeliveryEnabled({})).toBe(true)
375
+ expect(
376
+ isPosDeliveryEnabled({
377
+ [POS_DELIVERY_ENABLED_CONFIG_KEY]: '0',
378
+ }),
379
+ ).toBe(false)
380
+ expect(
381
+ isPosDeliveryEnabled({
382
+ [POS_DELIVERY_ENABLED_CONFIG_KEY]: '1',
383
+ }),
384
+ ).toBe(true)
385
+ })
386
+ })
387
+
388
+ describe('deviceConfigBootstrap local charge capability', () => {
389
+ it('defaults waiter to blocked and other modes to allowed when config missing', () => {
390
+ expect(
391
+ isPosLocalChargeEnabled({
392
+ [POS_OPERATION_MODE_CONFIG_KEY]: 'waiter',
393
+ }),
394
+ ).toBe(false)
395
+ expect(
396
+ isPosLocalChargeEnabled({
397
+ [POS_OPERATION_MODE_CONFIG_KEY]: 'cashier',
398
+ }),
399
+ ).toBe(true)
400
+ expect(
401
+ isPosLocalChargeEnabled({
402
+ [POS_OPERATION_MODE_CONFIG_KEY]: 'single-item',
403
+ }),
404
+ ).toBe(true)
405
+ })
406
+
407
+ it('honors explicit pos-local-charge-enabled over mode default', () => {
408
+ expect(
409
+ isPosLocalChargeEnabled({
410
+ [POS_OPERATION_MODE_CONFIG_KEY]: 'waiter',
411
+ [POS_LOCAL_CHARGE_ENABLED_CONFIG_KEY]: '1',
412
+ }),
413
+ ).toBe(true)
414
+ expect(
415
+ isPosLocalChargeEnabled({
416
+ [POS_OPERATION_MODE_CONFIG_KEY]: 'cashier',
417
+ [POS_LOCAL_CHARGE_ENABLED_CONFIG_KEY]: '0',
418
+ }),
419
+ ).toBe(false)
420
+ })
421
+ })
422
+
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Unit coverage for device-detail delete confirmation flow (app-community#383).
3
+ */
4
+
5
+ describe('device-detail delete confirmation (task-383)', () => {
6
+ const buildDeleteFlow = ({ remove, onSuccess, onError }) => {
7
+ let confirmed = false;
8
+ const confirm = (msg, cb) => ({
9
+ confirmNow: () => {
10
+ confirmed = true;
11
+ return Promise.resolve().then(() => cb());
12
+ },
13
+ cancel: () => {
14
+ confirmed = false;
15
+ },
16
+ wasConfirmed: () => confirmed,
17
+ message: msg,
18
+ });
19
+
20
+ const deleteDevice = async ({ deviceId, alias, deviceString }) => {
21
+ if (!deviceId) return { aborted: true };
22
+ const label = String(alias || deviceString || deviceId).trim();
23
+ const message = `Excluir o device "${label}"? Esta ação não pode ser desfeita.`;
24
+ return confirm(message, async () => {
25
+ try {
26
+ await remove(deviceId);
27
+ onSuccess?.();
28
+ } catch (e) {
29
+ onError?.(e);
30
+ }
31
+ });
32
+ };
33
+
34
+ return { deleteDevice };
35
+ };
36
+
37
+ test('does not call remove when confirmation is cancelled', async () => {
38
+ const remove = jest.fn();
39
+ const onSuccess = jest.fn();
40
+ const { deleteDevice } = buildDeleteFlow({ remove, onSuccess });
41
+ const gate = await deleteDevice({ deviceId: '42', alias: 'PDV 1' });
42
+ expect(gate.message).toContain('PDV 1');
43
+ gate.cancel();
44
+ expect(remove).not.toHaveBeenCalled();
45
+ expect(onSuccess).not.toHaveBeenCalled();
46
+ expect(gate.wasConfirmed()).toBe(false);
47
+ });
48
+
49
+ test('calls remove with deviceId after confirmation', async () => {
50
+ const remove = jest.fn().mockResolvedValue(undefined);
51
+ const onSuccess = jest.fn();
52
+ const { deleteDevice } = buildDeleteFlow({ remove, onSuccess });
53
+ const gate = await deleteDevice({ deviceId: '99', alias: 'Kiosk' });
54
+ await gate.confirmNow();
55
+ expect(remove).toHaveBeenCalledWith('99');
56
+ expect(onSuccess).toHaveBeenCalled();
57
+ });
58
+
59
+ test('surfaces error without success when remove rejects', async () => {
60
+ const remove = jest.fn().mockRejectedValue(new Error('fail'));
61
+ const onSuccess = jest.fn();
62
+ const onError = jest.fn();
63
+ const { deleteDevice } = buildDeleteFlow({ remove, onSuccess, onError });
64
+ const gate = await deleteDevice({ deviceId: '7', deviceString: 'dev-7' });
65
+ await gate.confirmNow();
66
+ expect(remove).toHaveBeenCalledWith('7');
67
+ expect(onSuccess).not.toHaveBeenCalled();
68
+ expect(onError).toHaveBeenCalled();
69
+ });
70
+
71
+ test('aborts when deviceId is missing', async () => {
72
+ const remove = jest.fn();
73
+ const { deleteDevice } = buildDeleteFlow({ remove });
74
+ const result = await deleteDevice({ deviceId: null, alias: 'x' });
75
+ expect(result).toEqual({ aborted: true });
76
+ expect(remove).not.toHaveBeenCalled();
77
+ });
78
+ });
@@ -0,0 +1,48 @@
1
+ import assert from 'node:assert/strict';
2
+ import fs from 'node:fs';
3
+ import test from 'node:test';
4
+
5
+ const catalog = fs.readFileSync(new URL('../../../react/pages/integrationsCatalog.js', import.meta.url), 'utf8');
6
+ const page = fs.readFileSync(new URL('../../../react/pages/IntegrationConfigPage.js', import.meta.url), 'utf8');
7
+ const fields = fs.readFileSync(new URL('../../../react/pages/IntegrationConfigFields.js', import.meta.url), 'utf8');
8
+ const utils = fs.readFileSync(new URL('../../../react/pages/IntegrationConfigPage.utils.js', import.meta.url), 'utf8');
9
+
10
+ test('Receita Federal catalog exposes company fiscal configuration', () => {
11
+ assert.match(catalog, /key: 'receita-federal'/);
12
+ assert.match(catalog, /label: 'Receita Federal'/);
13
+ assert.match(catalog, /receita-federal-tax-regime/);
14
+ assert.match(catalog, /receita-federal-ibge-code/);
15
+ assert.match(catalog, /receita-federal-certificate-file/);
16
+ assert.match(catalog, /receita-federal-certificate-password/);
17
+ assert.match(catalog, /secureTextEntry: true/);
18
+ assert.match(catalog, /\.pfx,\.p12,application\/x-pkcs12/);
19
+ });
20
+
21
+ test('Receita Federal route resolves without relying only on route params', () => {
22
+ assert.match(utils, /ReceitaFederalIntegrationPage: 'receita-federal'/);
23
+ assert.match(page, /ROUTE_PROVIDER_MAP\[route\?\.name\]/);
24
+ });
25
+
26
+ test('embedded fiscal configuration keeps reads and writes on the company being edited', () => {
27
+ assert.match(utils, /route\?\.params\?\.companyId/);
28
+ assert.match(utils, /resolveProviderId/);
29
+ assert.match(utils, /resolveFallbackConfigs/);
30
+ assert.match(page, /resolveProviderId\(\{ route, currentCompany \}\)/);
31
+ assert.match(page, /api\.fetch\('\/configs', \{ params: \{ people: providerIri \} \}\)/);
32
+ assert.match(page, /addManyConfigs\(\{ configs, people: providerIri/);
33
+ assert.match(page, /syncConfigValues\(fallbackConfigs\)/);
34
+ });
35
+
36
+ test('certificate picker remains scoped to the active company', () => {
37
+ assert.match(fields, /companyId=\{providerId\}/);
38
+ assert.match(fields, /entityId=\{providerId\}/);
39
+ assert.match(fields, /peopleId: companyId \|\| providerId/);
40
+ assert.match(fields, /company_certificate/);
41
+ });
42
+
43
+ test('IntegrationConfigPage delegates field rendering to a focused component', () => {
44
+ assert.match(page, /<IntegrationConfigFields/);
45
+ assert.ok(page.split('\n').length <= 500, `IntegrationConfigPage.js has ${page.split('\n').length} lines`);
46
+ assert.ok(fields.split('\n').length <= 500, `IntegrationConfigFields.js has ${fields.split('\n').length} lines`);
47
+ assert.ok(utils.split('\n').length <= 500, `IntegrationConfigPage.utils.js has ${utils.split('\n').length} lines`);
48
+ });
@@ -0,0 +1,222 @@
1
+ const assert = require('node:assert/strict')
2
+ const {test} = require('node:test')
3
+
4
+ const {
5
+ SAFE_TOOLBAR_FALLBACK_ROUTES,
6
+ buildSafeToolbarFallbackMenus,
7
+ mapRuntimeMenusToNavItems,
8
+ resolveToolbarRuntimeMenus,
9
+ } = require('../../../react/utils/bottomNavigationToolbar')
10
+
11
+ const managerPreset = {
12
+ items: [
13
+ {
14
+ route: 'HomePage',
15
+ icon: 'home',
16
+ label: {store: 'configs', type: 'toolbar', key: 'home', fallback: 'Início'},
17
+ },
18
+ {
19
+ route: 'CrmIndex',
20
+ icon: 'dollar-sign',
21
+ label: {
22
+ store: 'configs',
23
+ type: 'toolbar',
24
+ key: 'opportunities',
25
+ fallback: 'Oportunidades',
26
+ },
27
+ },
28
+ {
29
+ route: 'ClientsIndex',
30
+ icon: 'users',
31
+ label: {
32
+ store: 'configs',
33
+ type: 'toolbar',
34
+ key: 'customers',
35
+ fallback: 'Clientes',
36
+ },
37
+ },
38
+ {
39
+ route: 'ProfilePage',
40
+ icon: 'user',
41
+ label: {
42
+ store: 'configs',
43
+ type: 'toolbar',
44
+ key: 'profile',
45
+ fallback: 'Perfil',
46
+ },
47
+ },
48
+ ],
49
+ routeAliases: {
50
+ ClientDetails: 'ClientsIndex',
51
+ ProfilePage: 'ProfilePage',
52
+ SettingsPage: 'ProfilePage',
53
+ },
54
+ }
55
+
56
+ test('exposes only HomePage and ProfilePage as safe fallback routes', () => {
57
+ assert.deepEqual(SAFE_TOOLBAR_FALLBACK_ROUTES, ['HomePage', 'ProfilePage'])
58
+ })
59
+
60
+ test('builds safe fallback from managerToolbar without business routes', () => {
61
+ const menus = buildSafeToolbarFallbackMenus(
62
+ managerPreset,
63
+ 'toolbar',
64
+ null,
65
+ 'managerToolbar',
66
+ )
67
+
68
+ assert.equal(menus.length, 1)
69
+ const routes = menus[0].menus.map(item => item.route)
70
+ assert.deepEqual(routes, ['HomePage', 'ProfilePage'])
71
+ assert.equal(routes.includes('ClientsIndex'), false)
72
+ assert.equal(routes.includes('CrmIndex'), false)
73
+ assert.deepEqual(
74
+ menus[0].menus.map(item => item.label),
75
+ ['Início', 'Perfil'],
76
+ )
77
+ })
78
+
79
+ test('returns empty safe fallback when preset has no safe routes', () => {
80
+ const menus = buildSafeToolbarFallbackMenus(
81
+ {items: [{route: 'ClientsIndex', icon: 'users', label: 'Clientes'}]},
82
+ 'toolbar',
83
+ )
84
+ assert.deepEqual(menus, [])
85
+ })
86
+
87
+ test('prefers API toolbar menus over preset when authorized items exist', () => {
88
+ const apiMenus = [
89
+ {
90
+ id: 'toolbar-mod',
91
+ label: 'Toolbar',
92
+ menus: [
93
+ {
94
+ id: 1,
95
+ route: 'HomePage',
96
+ icon: 'home',
97
+ label: 'Início',
98
+ menuType: 'toolbar',
99
+ sortOrder: 10,
100
+ },
101
+ {
102
+ id: 2,
103
+ route: 'ClientsIndex',
104
+ icon: 'users',
105
+ label: 'Clientes',
106
+ menuType: 'toolbar',
107
+ sortOrder: 20,
108
+ },
109
+ ],
110
+ },
111
+ ]
112
+
113
+ const resolved = resolveToolbarRuntimeMenus({
114
+ apiMenus,
115
+ preset: managerPreset,
116
+ presetKey: 'managerToolbar',
117
+ menuType: 'toolbar',
118
+ })
119
+
120
+ const routes = resolved.flatMap(m => m.menus.map(i => i.route))
121
+ assert.deepEqual(routes, ['HomePage', 'ClientsIndex'])
122
+ assert.equal(routes.includes('ProfilePage'), false)
123
+ })
124
+
125
+ test('falls back to safe routes when API menus are empty', () => {
126
+ const resolved = resolveToolbarRuntimeMenus({
127
+ apiMenus: [],
128
+ preset: managerPreset,
129
+ presetKey: 'managerToolbar',
130
+ menuType: 'toolbar',
131
+ })
132
+
133
+ const routes = resolved.flatMap(m => m.menus.map(i => i.route))
134
+ assert.deepEqual(routes, ['HomePage', 'ProfilePage'])
135
+ assert.equal(routes.includes('ClientsIndex'), false)
136
+ assert.equal(routes.includes('CrmIndex'), false)
137
+ })
138
+
139
+ test('falls back to safe routes when API only has non-toolbar menus', () => {
140
+ const apiMenus = [
141
+ {
142
+ id: 'home-mod',
143
+ menus: [
144
+ {
145
+ route: 'ProductsPage',
146
+ label: 'Produtos',
147
+ menuType: 'home',
148
+ sortOrder: 1,
149
+ },
150
+ ],
151
+ },
152
+ ]
153
+
154
+ const resolved = resolveToolbarRuntimeMenus({
155
+ apiMenus,
156
+ preset: managerPreset,
157
+ menuType: 'toolbar',
158
+ })
159
+
160
+ const routes = resolved.flatMap(m => m.menus.map(i => i.route))
161
+ assert.deepEqual(routes, ['HomePage', 'ProfilePage'])
162
+ })
163
+
164
+ test('returns empty when API empty and no preset (no bypass)', () => {
165
+ const resolved = resolveToolbarRuntimeMenus({
166
+ apiMenus: [],
167
+ preset: null,
168
+ menuType: 'toolbar',
169
+ })
170
+ assert.deepEqual(resolved, [])
171
+ })
172
+
173
+ test('maps runtime menus to nav items sorted by sortOrder', () => {
174
+ const runtimeMenus = [
175
+ {
176
+ id: 'm1',
177
+ menus: [
178
+ {
179
+ route: 'ClientsIndex',
180
+ icon: 'users',
181
+ label: 'Clientes',
182
+ menuKey: 'clients',
183
+ menuType: 'toolbar',
184
+ sortOrder: 30,
185
+ },
186
+ {
187
+ route: 'HomePage',
188
+ icon: 'home',
189
+ label: 'Início',
190
+ menuKey: 'home',
191
+ menuType: 'toolbar',
192
+ sortOrder: 10,
193
+ },
194
+ ],
195
+ },
196
+ ]
197
+
198
+ const items = mapRuntimeMenusToNavItems(runtimeMenus, 'toolbar')
199
+ assert.deepEqual(
200
+ items.map(i => i.route),
201
+ ['HomePage', 'ClientsIndex'],
202
+ )
203
+ assert.equal(items[0].menuKey, 'home')
204
+ })
205
+
206
+ test('ignores items without route when mapping', () => {
207
+ const items = mapRuntimeMenusToNavItems(
208
+ [
209
+ {
210
+ menus: [
211
+ {route: '', label: 'Empty', menuType: 'toolbar'},
212
+ {route: 'HomePage', label: 'Início', menuType: 'toolbar', sortOrder: 1},
213
+ ],
214
+ },
215
+ ],
216
+ 'toolbar',
217
+ )
218
+ assert.deepEqual(
219
+ items.map(i => i.route),
220
+ ['HomePage'],
221
+ )
222
+ })