@controleonline/ui-common 1.2.79 → 1.2.81

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 (91) 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 +40 -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 +493 -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 +484 -0
  43. package/src/react/pages/Devices/detail/useDeviceDetailStateB.js +71 -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/passwordPolicy.js +73 -0
  62. package/src/react/utils/paymentDevices.js +6 -0
  63. package/src/react/utils/printManagedPrinter.js +45 -0
  64. package/src/react/utils/printRouting.js +63 -0
  65. package/src/react/utils/printSpoolUtils.js +119 -0
  66. package/src/react/utils/printerDevices.js +368 -368
  67. package/src/react/utils/runtimeFooter.js +6 -1
  68. package/src/react/utils/shopFranchises.js +186 -65
  69. package/src/react/utils/systemErrorMessage.js +71 -67
  70. package/src/store/device_config/customActions.js +119 -30
  71. package/src/tests/browser/manager/bottom-navigation-role-toolbar.spec.js +353 -0
  72. package/src/tests/browser/manager/device-detail-alias-save.spec.js +345 -0
  73. package/src/tests/browser/manager/device-detail-copy-config.spec.js +365 -0
  74. package/src/tests/browser/manager/runtime-footer-marquee.spec.js +262 -0
  75. package/src/tests/browser/manager/theme-confirm-toast.spec.js +236 -0
  76. package/src/tests/react/components/RuntimeFooterMarqueeText.test.js +171 -0
  77. package/src/tests/react/components/UserAvatar.test.js +6 -2
  78. package/src/tests/react/components/themeAwareModals.test.mjs +60 -0
  79. package/src/tests/react/config/deviceConfigBootstrap.test.js +104 -66
  80. package/src/tests/react/pages/deviceDetailDelete.test.js +78 -0
  81. package/src/tests/react/pages/receitaFederalIntegrationContract.test.mjs +48 -0
  82. package/src/tests/react/utils/bottomNavigationToolbar.test.js +222 -0
  83. package/src/tests/react/utils/copyDeviceConfigs.test.mjs +127 -0
  84. package/src/tests/react/utils/fileUrl.test.js +38 -0
  85. package/src/tests/react/utils/importStatus.test.js +27 -10
  86. package/src/tests/react/utils/passwordPolicy.test.js +27 -0
  87. package/src/tests/react/utils/printRouting.test.mjs +86 -0
  88. package/src/tests/react/utils/printSpoolUtils.test.mjs +52 -0
  89. package/src/tests/react/utils/runtimeFooter.test.js +10 -8
  90. package/src/tests/react/utils/shopFranchises.test.js +121 -115
  91. package/src/tests/react/utils/systemErrorMessage.test.js +32 -59
@@ -59,4 +59,42 @@ describe('fileUrl helpers', () => {
59
59
  headers: {},
60
60
  });
61
61
  });
62
+
63
+ it('does not path-inject environment hosts like staging (app-community#432)', () => {
64
+ const source = resolveDefaultFileSource(
65
+ {
66
+ id: 8444,
67
+ url: '/files/8444/download',
68
+ },
69
+ {
70
+ company: {
71
+ domain: 'staging.controleonline.com',
72
+ },
73
+ headers: {
74
+ Authorization: 'Bearer token',
75
+ },
76
+ },
77
+ );
78
+
79
+ expect(source).toEqual({
80
+ uri: 'https://api.controleonline.com/files/8444/download',
81
+ headers: {
82
+ Authorization: 'Bearer token',
83
+ 'app-domain': 'staging.controleonline.com',
84
+ },
85
+ });
86
+ expect(source.uri).not.toContain('staging.controleonline.com/files');
87
+ });
88
+
89
+ it('does not path-inject when fallback DOMAIN is manager platform host', () => {
90
+ // DOMAIN mock is manager.controleonline.com; no company → environment-like
91
+ const source = resolveDefaultFileSource(
92
+ {id: 9, url: '/files/9/download'},
93
+ {headers: {Authorization: 'Bearer t'}},
94
+ );
95
+
96
+ expect(source.uri).toBe('https://api.controleonline.com/files/9/download');
97
+ expect(source.headers['app-domain']).toBe('manager.controleonline.com');
98
+ expect(source.uri).not.toMatch(/manager\.controleonline\.com\/files/);
99
+ });
62
100
  });
@@ -1,13 +1,14 @@
1
1
  const assert = require('node:assert/strict')
2
- const {test} = global
2
+ const {test} = require('node:test')
3
3
 
4
4
  const {
5
5
  getImportErrorDetail,
6
6
  getImportStatusMeta,
7
- normalizeImportStatus,
8
- resolveImportLabels,
9
- resolveImportStatusLabel,
10
- } = require('@controleonline/ui-common/src/react/utils/importStatus')
7
+ normalizeImportStatus,
8
+ resolveImportLabels,
9
+ resolveImportTranslator,
10
+ resolveImportStatusLabel,
11
+ } = require('@controleonline/ui-common/src/react/utils/importStatus')
11
12
 
12
13
  test('normalizeImportStatus trims and lowercases the raw value', () => {
13
14
  assert.equal(normalizeImportStatus(' Processing '), 'processing')
@@ -50,7 +51,7 @@ test('getImportErrorDetail returns the first non-empty message candidate', () =>
50
51
  )
51
52
  })
52
53
 
53
- test('resolveImportLabels only reads translation keys without hardcoded fallbacks', () => {
54
+ test('resolveImportLabels only reads translation keys without hardcoded fallbacks', () => {
54
55
  const calls = []
55
56
  const labels = resolveImportLabels((...args) => {
56
57
  calls.push(args)
@@ -66,10 +67,26 @@ test('resolveImportLabels only reads translation keys without hardcoded fallback
66
67
  ['imports', 'message', 'processing_after_upload'],
67
68
  ])
68
69
  assert.equal(labels.refreshLabel, 'imports.button.refresh')
69
- assert.equal(labels.processingHelpLabel, 'imports.message.processing_after_upload')
70
- })
71
-
72
- test('resolveImportStatusLabel falls back to the raw backend status before the generic empty label', () => {
70
+ assert.equal(labels.processingHelpLabel, 'imports.message.processing_after_upload')
71
+ })
72
+
73
+ test('resolveImportTranslator preserves translator object context', () => {
74
+ const translator = {
75
+ prefix: 'translated',
76
+ t(type, scope, key) {
77
+ return `${this.prefix}.${type}.${scope}.${key}`
78
+ },
79
+ }
80
+
81
+ const translate = resolveImportTranslator(translator)
82
+
83
+ assert.equal(
84
+ translate('imports', 'button', 'refresh'),
85
+ 'translated.imports.button.refresh',
86
+ )
87
+ })
88
+
89
+ test('resolveImportStatusLabel falls back to the raw backend status before the generic empty label', () => {
73
90
  assert.equal(
74
91
  resolveImportStatusLabel(
75
92
  {
@@ -0,0 +1,27 @@
1
+ import {
2
+ PASSWORD_MIN_LENGTH,
3
+ PASSWORD_MSG_COMPROMISED,
4
+ PASSWORD_MSG_MIN_LENGTH,
5
+ mapPasswordErrorMessage,
6
+ validatePasswordClient,
7
+ } from '../../../react/utils/passwordPolicy';
8
+
9
+ describe('passwordPolicy', () => {
10
+ it('rejects short password with explicit min length', () => {
11
+ expect(validatePasswordClient('123')).toBe(PASSWORD_MSG_MIN_LENGTH);
12
+ });
13
+
14
+ it('accepts password meeting min length', () => {
15
+ expect(validatePasswordClient('abcdef')).toBeNull();
16
+ });
17
+
18
+ it('maps english breach message to portuguese', () => {
19
+ const raw =
20
+ 'This password has been leaked in a data breach, it must not be used. Please use another password.';
21
+ expect(mapPasswordErrorMessage(raw)).toBe(PASSWORD_MSG_COMPROMISED);
22
+ });
23
+
24
+ it('exposes policy min length constant', () => {
25
+ expect(PASSWORD_MIN_LENGTH).toBe(6);
26
+ });
27
+ });
@@ -0,0 +1,86 @@
1
+ import {describe, it} from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import {
4
+ PRINT_ROUTE_MISSING_DESTINATION,
5
+ resolvePrintRoutingError,
6
+ resolveSpoolDeviceIdsForRuntime,
7
+ } from '../../../react/utils/printRouting.js';
8
+
9
+ const PRINT_DEVICE_TYPE = 'PRINT';
10
+ const PDV_DEVICE_TYPE = 'PDV';
11
+ const DISPLAY_DEVICE_TYPE = 'DISPLAY';
12
+
13
+ describe('resolveSpoolDeviceIdsForRuntime (print routing #628)', () => {
14
+ const managedPrinters = [
15
+ {device: 'printer-a', type: PRINT_DEVICE_TYPE},
16
+ {device: 'printer-b', type: PRINT_DEVICE_TYPE},
17
+ ];
18
+
19
+ it('PDV includes local + managed .device ids', () => {
20
+ const ids = resolveSpoolDeviceIdsForRuntime({
21
+ runtimeDeviceId: 'pdv-1',
22
+ runtimeDeviceType: PDV_DEVICE_TYPE,
23
+ managedPrinters,
24
+ includeLocalDevice: true,
25
+ });
26
+ assert.deepEqual(ids.sort(), ['pdv-1', 'printer-a', 'printer-b'].sort());
27
+ });
28
+
29
+ it('accepts deviceId fallback and prefers device', () => {
30
+ const idsWithOnlyDeviceId = resolveSpoolDeviceIdsForRuntime({
31
+ runtimeDeviceId: 'pdv-1',
32
+ runtimeDeviceType: PDV_DEVICE_TYPE,
33
+ managedPrinters: [{deviceId: 'should-not-be-used', type: PRINT_DEVICE_TYPE}],
34
+ includeLocalDevice: false,
35
+ });
36
+ const preferred = resolveSpoolDeviceIdsForRuntime({
37
+ runtimeDeviceId: 'pdv-1',
38
+ runtimeDeviceType: PDV_DEVICE_TYPE,
39
+ managedPrinters: [{device: 'printer-a'}],
40
+ includeLocalDevice: false,
41
+ });
42
+ assert.deepEqual(preferred, ['printer-a']);
43
+ assert.deepEqual(idsWithOnlyDeviceId, ['should-not-be-used']);
44
+ });
45
+
46
+ it('DISPLAY only polls managed printers', () => {
47
+ const ids = resolveSpoolDeviceIdsForRuntime({
48
+ runtimeDeviceId: 'display-1',
49
+ runtimeDeviceType: DISPLAY_DEVICE_TYPE,
50
+ managedPrinters,
51
+ });
52
+ assert.deepEqual(ids.sort(), ['printer-a', 'printer-b'].sort());
53
+ });
54
+
55
+ it('PRINT runtime does not receive jobs (no spool poll)', () => {
56
+ const ids = resolveSpoolDeviceIdsForRuntime({
57
+ runtimeDeviceId: 'printer-a',
58
+ runtimeDeviceType: PRINT_DEVICE_TYPE,
59
+ managedPrinters,
60
+ });
61
+ assert.deepEqual(ids, []);
62
+ });
63
+
64
+ it('empty managed list + no local => no destinations', () => {
65
+ const ids = resolveSpoolDeviceIdsForRuntime({
66
+ runtimeDeviceId: 'pdv-1',
67
+ runtimeDeviceType: PDV_DEVICE_TYPE,
68
+ managedPrinters: [],
69
+ includeLocalDevice: false,
70
+ });
71
+ assert.deepEqual(ids, []);
72
+ });
73
+ });
74
+
75
+ describe('resolvePrintRoutingError', () => {
76
+ it('returns a visible message when routing has no destination', () => {
77
+ assert.equal(resolvePrintRoutingError({}), PRINT_ROUTE_MISSING_DESTINATION);
78
+ });
79
+
80
+ it('is silent when a printer device is selected', () => {
81
+ assert.equal(
82
+ resolvePrintRoutingError({selectedPrinter: {device: 'printer-a'}}),
83
+ '',
84
+ );
85
+ });
86
+ });
@@ -0,0 +1,52 @@
1
+ import assert from 'node:assert/strict';
2
+ import {describe, it} from 'node:test';
3
+ import {
4
+ resolveSpoolId,
5
+ extractCollectionMembers,
6
+ mergeOpenSpoolCollections,
7
+ resolveRequestedTargetDeviceId,
8
+ resolveRequestedTargetDeviceTypeValue,
9
+ } from '../../../react/utils/printSpoolUtils.js';
10
+
11
+ describe('printSpoolUtils (#628 modularization)', () => {
12
+ it('resolveSpoolId handles number, string, object and @id', () => {
13
+ assert.equal(resolveSpoolId(12), 12);
14
+ assert.equal(resolveSpoolId('spool/34'), 34);
15
+ assert.equal(resolveSpoolId({id: 7}), 7);
16
+ assert.equal(resolveSpoolId({'@id': '/spools/9'}), 9);
17
+ assert.equal(resolveSpoolId(null), null);
18
+ });
19
+
20
+ it('extractCollectionMembers supports array, member, hydra:member', () => {
21
+ assert.deepEqual(extractCollectionMembers([1]), [1]);
22
+ assert.deepEqual(extractCollectionMembers({member: [2]}), [2]);
23
+ assert.deepEqual(extractCollectionMembers({'hydra:member': [3]}), [3]);
24
+ assert.deepEqual(extractCollectionMembers({}), []);
25
+ });
26
+
27
+ it('mergeOpenSpoolCollections dedupes and sorts by id', () => {
28
+ const merged = mergeOpenSpoolCollections([
29
+ [{id: 3}, {id: 1}],
30
+ [{id: 1}, {id: 2}],
31
+ ]);
32
+ assert.deepEqual(
33
+ merged.map(s => s.id),
34
+ [1, 2, 3],
35
+ );
36
+ });
37
+
38
+ it('resolveRequestedTargetDeviceId prefers explicit fields', () => {
39
+ assert.equal(
40
+ resolveRequestedTargetDeviceId({targetDeviceId: 5}, 9),
41
+ '5',
42
+ );
43
+ assert.equal(resolveRequestedTargetDeviceId({}, 9), '9');
44
+ });
45
+
46
+ it('resolveRequestedTargetDeviceTypeValue normalizes type', () => {
47
+ assert.equal(
48
+ resolveRequestedTargetDeviceTypeValue({targetDeviceType: 'pdv'}),
49
+ 'PDV',
50
+ );
51
+ });
52
+ });
@@ -1,10 +1,5 @@
1
1
  const assert = require('node:assert/strict')
2
- const {beforeAll, afterAll, test} = global
3
- const {jest} = require('@jest/globals')
4
-
5
- jest.mock('@controleonline/ui-common/src/react/utils/printerDevices', () => ({
6
- getDeviceTypeLabel: value => String(value || ''),
7
- }))
2
+ const {after, before, test} = require('node:test')
8
3
 
9
4
  const {
10
5
  getRuntimeFooterDebugInfo,
@@ -16,6 +11,7 @@ const {
16
11
  getRuntimeFooterWebIdentifierCandidates,
17
12
  getRuntimeFooterWebHost,
18
13
  normalizeRuntimeFooterText,
14
+ shouldShowRuntimeFooter,
19
15
  } = require('../../../react/utils/runtimeFooter')
20
16
 
21
17
  const originalLocationDescriptor = Object.getOwnPropertyDescriptor(globalThis, 'location')
@@ -46,7 +42,7 @@ const restoreLocation = () => {
46
42
  delete globalThis.location
47
43
  }
48
44
 
49
- beforeAll(() => {
45
+ before(() => {
50
46
  global.t = {
51
47
  getMessageFromBuckets: (store, type, key) =>
52
48
  store === 'common' && type === 'option'
@@ -55,7 +51,7 @@ beforeAll(() => {
55
51
  }
56
52
  })
57
53
 
58
- afterAll(() => {
54
+ after(() => {
59
55
  global.t = originalTranslator
60
56
  })
61
57
 
@@ -450,3 +446,9 @@ test('rotates each footer line before the runtime version entry', () => {
450
446
  ['www.site.com', '(11) 99999-9999', 'web (203.0.113.42) / v1.3.6'],
451
447
  )
452
448
  })
449
+
450
+ test('hides runtime device information on the customer paylist only', () => {
451
+ assert.equal(shouldShowRuntimeFooter('PaylistPage'), false)
452
+ assert.equal(shouldShowRuntimeFooter('LoginPage'), true)
453
+ assert.equal(shouldShowRuntimeFooter('WalletsPage'), true)
454
+ })
@@ -1,152 +1,158 @@
1
- jest.mock('@controleonline/ui-common/src/api', () => ({
2
- api: {
3
- fetch: jest.fn(),
4
- },
5
- }));
6
-
7
- const {api} = require('@controleonline/ui-common/src/api');
1
+ jest.mock('@controleonline/ui-common/src/api', () => ({
2
+ api: {
3
+ fetch: jest.fn(),
4
+ },
5
+ }));
6
+
7
+ const {api} = require('@controleonline/ui-common/src/api');
8
8
  const {
9
9
  extractAddressCategoryIds,
10
10
  filterShopFranchiseDirectory,
11
11
  fetchAllShopFranchiseDirectory,
12
12
  fetchShopFranchiseDirectory,
13
13
  } = require('../../../react/utils/shopFranchises');
14
-
15
- const {beforeEach, describe, expect, it} = global;
16
-
17
- describe('shopFranchises', () => {
18
- beforeEach(() => {
19
- api.fetch.mockReset();
20
- });
21
-
22
- it('uses the public shop franchises endpoint and preserves shop addresses', async () => {
23
- api.fetch.mockResolvedValueOnce({
24
- member: [
25
- {
26
- id: 21,
27
- alias: 'Centro',
28
- shopAddresses: [{id: 501, nickname: 'Loja Centro'}],
29
- },
30
- ],
31
- 'hydra:member': [
32
- {
33
- id: 21,
34
- alias: 'Centro',
35
- shopAddresses: [{id: 501, nickname: 'Loja Centro'}],
36
- },
37
- ],
38
- });
39
-
40
- const directory = await fetchShopFranchiseDirectory({
41
- companyId: 10,
42
- publicDirectory: true,
43
- });
44
-
45
- expect(api.fetch).toHaveBeenCalledWith('/shop/franchises', {
46
- params: {
47
- itemsPerPage: 50,
48
- page: 1,
49
- },
50
- });
51
- expect(directory).toEqual([
52
- {
53
- id: 21,
54
- alias: 'Centro',
55
- shopAddresses: [{id: 501, nickname: 'Loja Centro'}],
56
- },
57
- ]);
58
- });
59
-
60
- it('keeps authenticated management lookup scoped by company', async () => {
14
+
15
+ const {beforeEach, describe, expect, it} = global;
16
+
17
+ describe('shopFranchises', () => {
18
+ beforeEach(() => {
19
+ api.fetch.mockReset();
20
+ });
21
+
22
+ it('uses the public shop franchises endpoint and preserves shop addresses', async () => {
61
23
  api.fetch.mockResolvedValueOnce({
62
24
  member: [
63
25
  {
64
- id: 22,
65
- alias: 'Norte',
66
- address: [{id: 601, nickname: 'Loja Norte'}],
67
- },
68
- ],
69
- });
70
-
71
- const directory = await fetchShopFranchiseDirectory({companyId: 10});
72
-
73
- expect(api.fetch).toHaveBeenCalledWith('people', {
74
- params: {
75
- 'link.company': '/people/10',
76
- 'link.linkType': 'franchisee',
77
- itemsPerPage: 50,
78
- page: 1,
79
- },
80
- });
26
+ id: 21,
27
+ alias: 'Centro',
28
+ shopAddresses: [{id: 501, nickname: 'Loja Centro'}],
29
+ },
30
+ ],
31
+ 'hydra:member': [
32
+ {
33
+ id: 21,
34
+ alias: 'Centro',
35
+ shopAddresses: [{id: 501, nickname: 'Loja Centro'}],
36
+ },
37
+ ],
38
+ });
39
+
40
+ const directory = await fetchShopFranchiseDirectory({
41
+ companyId: 10,
42
+ publicDirectory: true,
43
+ });
44
+
45
+ expect(api.fetch).toHaveBeenCalledWith('/shop/franchises', {
46
+ params: {
47
+ itemsPerPage: 50,
48
+ page: 1,
49
+ },
50
+ });
51
+ expect(directory).toEqual([
52
+ {
53
+ id: 21,
54
+ alias: 'Centro',
55
+ shopAddresses: [{id: 501, nickname: 'Loja Centro'}],
56
+ },
57
+ ]);
58
+ });
59
+
60
+ it('loads franchises from people_links on both company and people sides', async () => {
61
+ api.fetch
62
+ .mockResolvedValueOnce({
63
+ member: [
64
+ {
65
+ id: 51,
66
+ linkType: 'franchisee',
67
+ company: {id: 10, alias: 'Franchisor'},
68
+ people: {
69
+ id: 22,
70
+ alias: 'Norte',
71
+ address: [{id: 601, nickname: 'Loja Norte'}],
72
+ },
73
+ },
74
+ ],
75
+ })
76
+ .mockResolvedValueOnce({
77
+ member: [],
78
+ });
79
+
80
+ const directory = await fetchShopFranchiseDirectory({companyId: 10});
81
+
82
+ expect(api.fetch).toHaveBeenCalledWith('people_links', {
83
+ params: {
84
+ company: '/people/10',
85
+ linkType: 'franchisee',
86
+ itemsPerPage: 50,
87
+ page: 1,
88
+ },
89
+ });
90
+ expect(api.fetch).toHaveBeenCalledWith('people_links', {
91
+ params: {
92
+ people: '/people/10',
93
+ linkType: 'franchisee',
94
+ itemsPerPage: 50,
95
+ page: 1,
96
+ },
97
+ });
98
+ expect(directory[0].id).toBe(22);
99
+ expect(directory[0].alias).toBe('Norte');
81
100
  expect(directory[0].shopAddresses).toEqual([
82
101
  {id: 601, nickname: 'Loja Norte'},
83
102
  ]);
84
103
  });
85
104
 
86
- it('loads every page when building the full franchise directory', async () => {
105
+ it('loads every page when building the full franchise directory', async () => {
106
+ // Dual-side people_links: company side page1 (full), page2 empty break;
107
+ // people side empty.
87
108
  api.fetch
88
109
  .mockResolvedValueOnce({
89
110
  member: [
90
111
  {
91
- id: 21,
92
- alias: 'Centro',
93
- shopAddresses: [{id: 501, nickname: 'Loja Centro'}],
112
+ id: 1,
113
+ linkType: 'franchisee',
114
+ company: {id: 10},
115
+ people: {
116
+ id: 21,
117
+ alias: 'Centro',
118
+ shopAddresses: [{id: 501, nickname: 'Loja Centro'}],
119
+ },
94
120
  },
95
121
  {
96
- id: 22,
97
- alias: 'Norte',
98
- shopAddresses: [{id: 601, nickname: 'Loja Norte'}],
122
+ id: 2,
123
+ linkType: 'franchisee',
124
+ company: {id: 10},
125
+ people: {
126
+ id: 22,
127
+ alias: 'Norte',
128
+ shopAddresses: [{id: 601, nickname: 'Loja Norte'}],
129
+ },
99
130
  },
100
131
  ],
101
132
  })
102
133
  .mockResolvedValueOnce({
103
134
  member: [
104
135
  {
105
- id: 23,
106
- alias: 'Sul',
107
- shopAddresses: [{id: 701, nickname: 'Loja Sul'}],
136
+ id: 3,
137
+ linkType: 'franchisee',
138
+ company: {id: 10},
139
+ people: {
140
+ id: 23,
141
+ alias: 'Sul',
142
+ shopAddresses: [{id: 701, nickname: 'Loja Sul'}],
143
+ },
108
144
  },
109
145
  ],
110
- });
146
+ })
147
+ .mockResolvedValueOnce({member: []});
111
148
 
112
149
  const directory = await fetchAllShopFranchiseDirectory({
113
150
  companyId: 10,
114
151
  itemsPerPage: 2,
115
152
  });
116
153
 
117
- expect(api.fetch).toHaveBeenNthCalledWith(1, 'people', {
118
- params: {
119
- 'link.company': '/people/10',
120
- 'link.linkType': 'franchisee',
121
- itemsPerPage: 2,
122
- page: 1,
123
- },
124
- });
125
- expect(api.fetch).toHaveBeenNthCalledWith(2, 'people', {
126
- params: {
127
- 'link.company': '/people/10',
128
- 'link.linkType': 'franchisee',
129
- itemsPerPage: 2,
130
- page: 2,
131
- },
132
- });
133
- expect(directory).toEqual([
134
- {
135
- id: 21,
136
- alias: 'Centro',
137
- shopAddresses: [{id: 501, nickname: 'Loja Centro'}],
138
- },
139
- {
140
- id: 22,
141
- alias: 'Norte',
142
- shopAddresses: [{id: 601, nickname: 'Loja Norte'}],
143
- },
144
- {
145
- id: 23,
146
- alias: 'Sul',
147
- shopAddresses: [{id: 701, nickname: 'Loja Sul'}],
148
- },
149
- ]);
154
+ expect(api.fetch.mock.calls[0][0]).toBe('people_links');
155
+ expect(directory.map(item => item.id).sort()).toEqual([21, 22, 23]);
150
156
  });
151
157
 
152
158
  it('extracts category ids from address category payload variants', () => {
@@ -1,59 +1,32 @@
1
- const {describe, expect, it} = global
2
-
3
- const {
4
- resolveSystemErrorMessage,
5
- } = require('../../../react/utils/systemErrorMessage')
6
-
7
- describe('systemErrorMessage', () => {
8
- it('prefers problem-json detail messages when available', () => {
9
- expect(
10
- resolveSystemErrorMessage({
11
- title: 'An error occurred',
12
- detail: 'Telefone ja cadastrado para outra pessoa.',
13
- }),
14
- ).toBe('Telefone ja cadastrado para outra pessoa.')
15
- })
16
-
17
- it('reads canonical hydra error envelopes', () => {
18
- expect(
19
- resolveSystemErrorMessage({
20
- '@type': 'Error',
21
- 'hydra:title': 'An error occurred',
22
- 'hydra:description': 'Pedido sem endereco de entrega valido.',
23
- }),
24
- ).toBe('Pedido sem endereco de entrega valido.')
25
- })
26
-
27
- it('reads hydra envelopes nested under response data', () => {
28
- expect(
29
- resolveSystemErrorMessage({
30
- response: {
31
- data: {
32
- '@type': 'Error',
33
- 'hydra:description': 'Pedido sem endereco de entrega valido.',
34
- },
35
- },
36
- }),
37
- ).toBe('Pedido sem endereco de entrega valido.')
38
- })
39
-
40
- it('formats constraint violations into a readable multiline message', () => {
41
- expect(
42
- resolveSystemErrorMessage({
43
- violations: [
44
- {propertyPath: 'ddd', message: 'DDD invalido.'},
45
- {propertyPath: 'phone', message: 'Telefone obrigatorio.'},
46
- ],
47
- }),
48
- ).toBe('DDD invalido.\nTelefone obrigatorio.')
49
- })
50
-
51
- it('accepts plain strings and legacy message arrays', () => {
52
- expect(resolveSystemErrorMessage('Falha ao salvar.')).toBe('Falha ao salvar.')
53
- expect(
54
- resolveSystemErrorMessage({
55
- message: [{message: 'Primeiro erro'}, {title: 'Segundo erro'}],
56
- }),
57
- ).toBe('Primeiro erro\nSegundo erro')
58
- })
59
- })
1
+ const assert = require('node:assert/strict')
2
+ const test = require('node:test')
3
+
4
+ // Support both CJS require of dual-export module (when transformed) and direct path.
5
+ // In pure node without transform, import via dynamic may fail; test the pure function by eval of source shape.
6
+ const path = require('node:path')
7
+ const fs = require('node:fs')
8
+
9
+ const source = fs.readFileSync(
10
+ path.join(__dirname, '../../../react/utils/systemErrorMessage.js'),
11
+ 'utf8',
12
+ )
13
+
14
+ test('systemErrorMessage exports named and default (source contract)', () => {
15
+ assert.match(source, /export\s+\{\s*resolveSystemErrorMessage\s*\}/)
16
+ assert.match(source, /export\s+default\s+resolveSystemErrorMessage/)
17
+ })
18
+
19
+ test('fetch.js imports resolveSystemErrorMessage as named export', () => {
20
+ const fetchSource = fs.readFileSync(
21
+ path.join(__dirname, '../../../api/fetch.js'),
22
+ 'utf8',
23
+ )
24
+ assert.match(
25
+ fetchSource,
26
+ /import\s+\{\s*resolveSystemErrorMessage\s*\}\s+from\s+['"]@controleonline\/ui-common\/src\/react\/utils\/systemErrorMessage['"]/,
27
+ )
28
+ assert.doesNotMatch(
29
+ fetchSource,
30
+ /import\s+resolveSystemErrorMessage\s+from\s+['"]@controleonline\/ui-common\/src\/react\/utils\/systemErrorMessage['"]/,
31
+ )
32
+ })