@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.
- package/AGENTS.md +8 -0
- package/package.json +9 -3
- package/src/api/fetch.js +7 -7
- package/src/react/components/AddImportModal.js +6 -2
- package/src/react/components/BackgroundRuntimeBridge.js +201 -215
- package/src/react/components/ConfirmModal.js +44 -9
- package/src/react/components/ConfirmModal.styles.js +55 -41
- package/src/react/components/CopyDeviceConfigModal.js +193 -0
- package/src/react/components/CopyDeviceConfigModal.styles.js +123 -0
- package/src/react/components/DefaultProvider.native.js +22 -0
- package/src/react/components/DefaultProvider.web.js +25 -1
- package/src/react/components/EntityLogBranch.js +482 -0
- package/src/react/components/EntityLogCards.js +369 -0
- package/src/react/components/EntityLogContent.js +113 -794
- package/src/react/components/LinkedOrderProductsTab.js +3 -3
- package/src/react/components/PrintService.js +130 -409
- package/src/react/components/RemoteCheckoutService.js +2 -2
- package/src/react/components/RuntimeBottomNavigationBar.js +141 -146
- package/src/react/components/RuntimeFooterMarqueeText.js +137 -0
- package/src/react/components/RuntimeInfoFooter.js +12 -15
- package/src/react/components/SystemErrorToast.js +49 -29
- package/src/react/components/SystemErrorToast.styles.js +43 -38
- package/src/react/components/UserAvatar.js +120 -28
- package/src/react/config/deviceConfigBootstrap.js +330 -304
- package/src/react/hooks/usePrintSpoolEffects.js +161 -0
- package/src/react/pages/DeviceDetailPage.js +6 -2882
- package/src/react/pages/Devices/detail/DeviceDetailAlertsCommandsSection.js +233 -0
- package/src/react/pages/Devices/detail/DeviceDetailHeader.js +138 -0
- package/src/react/pages/Devices/detail/DeviceDetailMovementSections.js +167 -0
- package/src/react/pages/Devices/detail/DeviceDetailOrdersPrintSection.js +216 -0
- package/src/react/pages/Devices/detail/DeviceDetailPaymentSection.js +44 -0
- package/src/react/pages/Devices/detail/DeviceDetailPdvConfigSection.js +390 -0
- package/src/react/pages/Devices/detail/DeviceDetailRenderers.js +135 -0
- package/src/react/pages/Devices/detail/DeviceDetailScreen.js +495 -0
- package/src/react/pages/Devices/detail/OptionButtonChip.js +65 -0
- package/src/react/pages/Devices/detail/deviceDetailConstants.js +36 -0
- package/src/react/pages/Devices/detail/deviceDetailHelpers.js +70 -0
- package/src/react/pages/Devices/detail/useDeviceDetailActions.js +483 -0
- package/src/react/pages/Devices/detail/useDeviceDetailCopyConfig.js +87 -0
- package/src/react/pages/Devices/detail/useDeviceDetailLoaders.js +327 -0
- package/src/react/pages/Devices/detail/useDeviceDetailSaves.js +429 -0
- package/src/react/pages/Devices/detail/useDeviceDetailStateA.js +489 -0
- package/src/react/pages/Devices/detail/useDeviceDetailStateB.js +98 -0
- package/src/react/pages/Devices/deviceTypes/index.js +2 -0
- package/src/react/pages/Devices/deviceTypes/pdv.js +11 -0
- package/src/react/pages/Imports.js +3 -2
- package/src/react/pages/IntegrationConfigFields.js +126 -0
- package/src/react/pages/IntegrationConfigPage.js +122 -591
- package/src/react/pages/IntegrationConfigPage.utils.js +95 -0
- package/src/react/print/usePrintButtonController.js +9 -1
- package/src/react/services/Getnet/Checkout.js +81 -0
- package/src/react/services/Getnet/Getnet.js +77 -0
- package/src/react/services/paymentGatewayExecution.js +20 -0
- package/src/react/utils/bottomNavigationToolbar.js +161 -0
- package/src/react/utils/commercialDocumentOrders.js +18 -6
- package/src/react/utils/copyDeviceConfigs.js +198 -0
- package/src/react/utils/fileUrl.js +43 -0
- package/src/react/utils/importStatus.js +19 -10
- package/src/react/utils/logSettings.js +7 -7
- package/src/react/utils/maintenanceSettings.js +16 -0
- package/src/react/utils/paymentDevices.js +6 -0
- package/src/react/utils/printManagedPrinter.js +45 -0
- package/src/react/utils/printRouting.js +63 -0
- package/src/react/utils/printSpoolUtils.js +119 -0
- package/src/react/utils/printerDevices.js +368 -368
- package/src/react/utils/runtimeFooter.js +10 -19
- package/src/react/utils/shopFranchises.js +186 -65
- package/src/react/utils/systemErrorMessage.js +71 -67
- package/src/store/device_config/customActions.js +119 -30
- package/src/tests/browser/manager/bottom-navigation-role-toolbar.spec.js +353 -0
- package/src/tests/browser/manager/device-detail-alias-save.spec.js +345 -0
- package/src/tests/browser/manager/device-detail-copy-config.spec.js +365 -0
- package/src/tests/browser/manager/runtime-footer-marquee.spec.js +262 -0
- package/src/tests/browser/manager/theme-confirm-toast.spec.js +236 -0
- package/src/tests/react/components/RuntimeFooterMarqueeText.test.js +171 -0
- package/src/tests/react/components/UserAvatar.test.js +6 -2
- package/src/tests/react/components/themeAwareModals.test.mjs +60 -0
- package/src/tests/react/config/deviceConfigBootstrap.test.js +104 -66
- package/src/tests/react/pages/deviceDetailDelete.test.js +78 -0
- package/src/tests/react/pages/receitaFederalIntegrationContract.test.mjs +48 -0
- package/src/tests/react/utils/bottomNavigationToolbar.test.js +222 -0
- package/src/tests/react/utils/copyDeviceConfigs.test.mjs +127 -0
- package/src/tests/react/utils/fileUrl.test.js +38 -0
- package/src/tests/react/utils/importStatus.test.js +26 -9
- package/src/tests/react/utils/passwordPolicy.test.js +27 -0
- package/src/tests/react/utils/printRouting.test.mjs +86 -0
- package/src/tests/react/utils/printSpoolUtils.test.mjs +52 -0
- package/src/tests/react/utils/runtimeFooter.test.js +7 -0
- package/src/tests/react/utils/shopFranchises.test.js +121 -115
- 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
|
+
})
|