@controleonline/ui-common 1.2.87 → 1.2.89

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 (77) hide show
  1. package/.github/workflows/integration-source-gate.yml +68 -0
  2. package/.github/workflows/manager-worker.yml +7 -156
  3. package/.github/workflows/publish-npm.yml +73 -27
  4. package/.github/workflows/pull-request-checks.yml +7 -5
  5. package/docs/technical/Device-Detail-Alias-UI-Sync.md +141 -0
  6. package/package.json +13 -8
  7. package/src/api/localDB.js +86 -131
  8. package/src/react/components/BottomNavigationBar.js +178 -174
  9. package/src/react/components/BottomNavigationBar.styles.js +139 -118
  10. package/src/react/components/DefaultProvider.native.js +427 -421
  11. package/src/react/components/DefaultProvider.web.js +131 -125
  12. package/src/react/components/RemoteCheckoutService.js +3 -3
  13. package/src/react/components/RuntimeFooterMarqueeText.js +88 -36
  14. package/src/react/components/RuntimeInfoFooter.js +76 -43
  15. package/src/react/components/RuntimeInfoFooter.styles.js +22 -14
  16. package/src/react/config/deviceConfigBootstrap.js +71 -233
  17. package/src/react/config/deviceConfigValues.js +238 -0
  18. package/src/react/pages/Devices/detail/DeviceChargePermissionSection.js +32 -0
  19. package/src/react/pages/Devices/detail/DeviceDetailAlertsCommandsSection.js +4 -22
  20. package/src/react/pages/Devices/detail/DeviceDetailMovementSections.js +6 -2
  21. package/src/react/pages/Devices/detail/DeviceDetailOrdersPrintSection.js +4 -0
  22. package/src/react/pages/Devices/detail/DeviceDetailPdvConfigSection.js +7 -0
  23. package/src/react/pages/Devices/detail/DeviceDetailScreen.js +24 -335
  24. package/src/react/pages/Devices/detail/createDeviceDetailContext.js +20 -0
  25. package/src/react/pages/Devices/detail/deviceDetailHelpers.js +2 -11
  26. package/src/react/pages/Devices/detail/useDeviceDetailActions.js +11 -36
  27. package/src/react/pages/Devices/detail/useDeviceDetailLoaders.js +129 -37
  28. package/src/react/pages/Devices/detail/useDeviceDetailSaves.js +11 -38
  29. package/src/react/pages/Devices/detail/useDeviceDetailStateA.js +9 -28
  30. package/src/react/pages/IntegrationConfigFields.js +1 -0
  31. package/src/react/pages/IntegrationConfigPage.js +101 -45
  32. package/src/react/pages/IntegrationConfigPage.utils.js +21 -13
  33. package/src/react/pages/fetchPeopleConfigs.js +21 -0
  34. package/src/react/pages/integrationsCatalog.js +15 -110
  35. package/src/react/services/Cielo/Cielo.js +9 -9
  36. package/src/react/services/Getnet/Getnet.web.js +25 -0
  37. package/src/react/utils/companyAuthority.js +36 -0
  38. package/src/react/utils/confirmAction.js +12 -0
  39. package/src/react/utils/fileUrl.js +14 -5
  40. package/src/react/utils/paymentDevices.js +375 -270
  41. package/src/react/utils/printerDevices.js +2 -2
  42. package/src/react/utils/runtimeFooter.js +178 -224
  43. package/src/react/utils/runtimeFooterText.js +160 -0
  44. package/src/react/utils/runtimeLanguage.js +5 -5
  45. package/src/react/utils/runtimeZoom.js +29 -5
  46. package/src/react/utils/shopFranchises.js +167 -12
  47. package/src/react/utils/storeColumns.js +235 -217
  48. package/src/react/utils/systemErrorMessage.js +2 -1
  49. package/src/store/integration/index.js +43 -0
  50. package/src/tests/browser/manager/device-detail-apply-config-defined.spec.js +31 -0
  51. package/src/tests/browser/manager/device-detail-composition.spec.js +295 -0
  52. package/src/tests/browser/manager/device-detail-copy-config.spec.js +11 -1
  53. package/src/tests/browser/manager/device-detail-pdv-current-company.spec.js +273 -0
  54. package/src/tests/browser/manager/runtime-footer-rotation.spec.js +397 -0
  55. package/src/tests/react/components/DefaultProviderAuthority.test.js +28 -0
  56. package/src/tests/react/components/RuntimeFooterMarqueeText.build.test.js +29 -0
  57. package/src/tests/react/config/deviceConfigBootstrap.test.js +24 -1
  58. package/src/tests/react/pages/DeviceChargePermissionSection.test.js +42 -0
  59. package/src/tests/react/pages/applyCurrentDeviceConfig.test.js +85 -0
  60. package/src/tests/react/pages/deviceDetailComposition.test.js +65 -0
  61. package/src/tests/react/pages/deviceDetailHookDependencies.test.mjs +31 -0
  62. package/src/tests/react/pages/deviceDetailLoaderDependencies.test.mjs +28 -0
  63. package/src/tests/react/pages/receitaFederalIntegrationContract.test.mjs +27 -6
  64. package/src/tests/react/services/Cielo.test.js +2 -2
  65. package/src/tests/react/utils/companyAuthority.test.js +53 -0
  66. package/src/tests/react/utils/formatter.test.mjs +6 -0
  67. package/src/tests/react/utils/paymentDevices.test.js +72 -0
  68. package/src/tests/react/utils/runtimeFooter.test.js +35 -0
  69. package/src/tests/react/utils/runtimeFooterText.test.js +88 -0
  70. package/src/tests/react/utils/runtimeLanguage.test.js +4 -4
  71. package/src/tests/react/utils/runtimeZoom.test.js +20 -0
  72. package/src/tests/react/utils/shopFranchises.test.js +32 -13
  73. package/src/tests/react/utils/translate.test.mjs +6 -6
  74. package/src/utils/formatter.js +7 -3
  75. package/src/utils/translate.js +16 -16
  76. package/src/utils/translatePending.js +6 -6
  77. package/src/utils/translateStorage.js +2 -2
@@ -0,0 +1,397 @@
1
+ /**
2
+ * fluxo: outros
3
+ * Justificativa: jornada visual do RuntimeInfoFooter (rotação 1 linha ↔
4
+ * device/versão em 4s), fora dos fluxos de negócio do catálogo canônico.
5
+ * Issue: app-community#384
6
+ *
7
+ * Passos / prints:
8
+ * 1. 1 linha configurada — frame da linha isolada
9
+ * 2. após ~4s — frame do primaryText (device + versão)
10
+ * 3. 0 linhas — somente primaryText
11
+ * 4. 2+ linhas — primeira linha, depois próxima entrada
12
+ */
13
+ const {expect, test} = require('playwright/test');
14
+ const packageJson = require('../../../../../../../package.json');
15
+ const {API_ORIGIN} = require('../../../../../../../src/tests/browser/apiOrigin');
16
+ const {writeManifest} = require('../../../../../../../modules/controleonline/ui-tests/src/tests/helpers/smokeEvidence');
17
+ const API_ORIGINS = [API_ORIGIN, 'https://api.controleonline.com'].filter(
18
+ (origin, index, origins) => origins.indexOf(origin) === index,
19
+ );
20
+
21
+ const APP_VERSION = packageJson?.version || '1.0.0';
22
+ const LINE_ONE = 'Linha isolada do rodapé';
23
+ const LINE_TWO = 'Segunda linha do rodapé';
24
+ const PRIMARY_HINT = /Browser Manager|web-manager|web\s*•\s*ADMIN|1\./i;
25
+
26
+ const CORS_HEADERS = {
27
+ 'access-control-allow-origin': '*',
28
+ 'access-control-allow-headers':
29
+ 'API-TOKEN, APP-DOMAIN, DEVICE, ACCEPT, CONTENT-TYPE, X-Requested-With',
30
+ 'access-control-allow-methods': 'GET,POST,PUT,PATCH,DELETE,OPTIONS',
31
+ };
32
+
33
+ const jsonHeaders = () => ({
34
+ ...CORS_HEADERS,
35
+ 'content-type': 'application/ld+json; charset=utf-8',
36
+ });
37
+
38
+ const textHeaders = () => ({
39
+ ...CORS_HEADERS,
40
+ 'content-type': 'text/css; charset=utf-8',
41
+ });
42
+
43
+ const collection = member => ({
44
+ member,
45
+ 'hydra:member': member,
46
+ totalItems: member.length,
47
+ 'hydra:totalItems': member.length,
48
+ summary: {},
49
+ });
50
+
51
+ const createCompany = footerText => ({
52
+ id: 3,
53
+ name: 'Controle Online',
54
+ alias: 'CONTROLE ONLINE',
55
+ panel_enabled: true,
56
+ enabled: true,
57
+ commercial_enabled: true,
58
+ theme: {
59
+ colors: {
60
+ primary: '#0EA5E9',
61
+ footerBackground: '#0F172A',
62
+ footerBorder: '#1E293B',
63
+ footerText: '#E2E8F0',
64
+ footerLink: '#38BDF8',
65
+ success: '#22C55E',
66
+ warning: '#F59E0B',
67
+ error: '#EF4444',
68
+ },
69
+ },
70
+ configs: footerText
71
+ ? {'device-runtime-footer-text': footerText}
72
+ : {},
73
+ });
74
+
75
+ const mockApi = async (page, footerText) => {
76
+ const company = createCompany(footerText);
77
+
78
+ for (const apiOrigin of API_ORIGINS) {
79
+ await page.route(`${apiOrigin}/**`, async route => {
80
+ const request = route.request();
81
+ const url = new URL(request.url());
82
+ const pathname = url.pathname.replace(/^\/+/, '');
83
+ const method = request.method().toUpperCase();
84
+
85
+ if (method === 'OPTIONS') {
86
+ return route.fulfill({status: 204, headers: CORS_HEADERS, body: ''});
87
+ }
88
+
89
+ if (pathname === 'themes-colors.css') {
90
+ return route.fulfill({
91
+ status: 200,
92
+ headers: textHeaders(),
93
+ body: ':root { --primary: #0ea5e9; --footer-text: #e2e8f0; }',
94
+ });
95
+ }
96
+
97
+ if (pathname === 'runtime/ip') {
98
+ return route.fulfill({
99
+ status: 200,
100
+ headers: jsonHeaders(),
101
+ body: JSON.stringify({ip: '127.0.0.1'}),
102
+ });
103
+ }
104
+
105
+ if (pathname === 'menus-people') {
106
+ return route.fulfill({
107
+ status: 200,
108
+ headers: jsonHeaders(),
109
+ body: JSON.stringify({modules: {}}),
110
+ });
111
+ }
112
+
113
+ if (
114
+ pathname === 'people/company/default' ||
115
+ pathname === 'people/companies/my'
116
+ ) {
117
+ return route.fulfill({
118
+ status: 200,
119
+ headers: jsonHeaders(),
120
+ body: JSON.stringify(
121
+ pathname === 'people/company/default' ? company : collection([company]),
122
+ ),
123
+ });
124
+ }
125
+
126
+ if (
127
+ pathname === 'people' ||
128
+ pathname === 'devices' ||
129
+ pathname === 'device_configs'
130
+ ) {
131
+ return route.fulfill({
132
+ status: 200,
133
+ headers: jsonHeaders(),
134
+ body: JSON.stringify(collection([])),
135
+ });
136
+ }
137
+
138
+ return route.fulfill({
139
+ status: 200,
140
+ headers: jsonHeaders(),
141
+ body: JSON.stringify(collection([])),
142
+ });
143
+ });
144
+ }
145
+
146
+ await page.addInitScript(
147
+ ({appVersion}) => {
148
+ window.localStorage.setItem(
149
+ 'session',
150
+ JSON.stringify({
151
+ id: 7,
152
+ people: '/people/7',
153
+ api_key: 'test-api-key',
154
+ active: 1,
155
+ mycompany: 3,
156
+ roles: ['ROLE_SUPER'],
157
+ phone: '(11) 97114-0832',
158
+ email: 'qa@controleonline.com',
159
+ }),
160
+ );
161
+ window.localStorage.setItem('config', JSON.stringify({language: 'pt-br'}));
162
+ window.localStorage.setItem('app-type', 'MANAGER');
163
+ window.localStorage.setItem(
164
+ 'device',
165
+ JSON.stringify({
166
+ id: 'web-manager',
167
+ device: 'web-manager',
168
+ type: 'WEB',
169
+ appName: 'Browser Manager',
170
+ appVersion,
171
+ buildNumber: appVersion,
172
+ systemName: 'web',
173
+ systemVersion: 'web',
174
+ deviceType: 'web',
175
+ metadata: {},
176
+ }),
177
+ );
178
+ },
179
+ {appVersion: APP_VERSION},
180
+ );
181
+ };
182
+
183
+ async function openFooter(page, footerText) {
184
+ await mockApi(page, footerText);
185
+ await page.goto('/devices-index?store=device_config');
186
+ const footer = page.getByTestId('runtime-info-footer');
187
+ await expect(footer).toBeVisible({timeout: 15000});
188
+ const label = page.getByTestId('runtime-footer-primary-text');
189
+ await expect(label.first()).toBeVisible({timeout: 15000});
190
+ return {footer, label: label.first()};
191
+ }
192
+
193
+ async function openAuthenticatedFooter(page, testInfo) {
194
+ const sessionJson = String(process.env.PLAYWRIGHT_SESSION_JSON || '').trim();
195
+ if (!sessionJson) {
196
+ test.skip(true, 'PLAYWRIGHT_SESSION_JSON is required for the live staging journey.');
197
+ }
198
+
199
+ const consoleErrors = [];
200
+ const requestFailures = [];
201
+ page.on('console', message => {
202
+ if (message.type() === 'error') {
203
+ consoleErrors.push(message.text());
204
+ }
205
+ });
206
+ page.on('requestfailed', request => {
207
+ requestFailures.push(`${request.method()} ${request.url()}: ${request.failure()?.errorText || 'failed'}`);
208
+ });
209
+
210
+ await page.addInitScript(({session}) => {
211
+ localStorage.setItem('session', session);
212
+ localStorage.setItem('config', JSON.stringify({language: 'pt-br'}));
213
+ // Staging publishes the authenticated web bundle as ADMIN.
214
+ localStorage.setItem('app-type', 'ADMIN');
215
+ }, {session: sessionJson});
216
+
217
+ await page.goto('/devices-index?store=device_config');
218
+ const footer = page.getByTestId('runtime-info-footer');
219
+ const label = page.getByTestId('runtime-footer-primary-text').first();
220
+ await expect(footer).toBeVisible({timeout: 30000});
221
+ await expect(label).toBeVisible({timeout: 30000});
222
+
223
+ return {footer, label, consoleErrors, requestFailures, evidenceDir: testInfo.outputDir};
224
+ }
225
+
226
+ test.describe('runtime footer rotation (#384) — fluxo: outros', () => {
227
+ test('1 line: isolated line then device/version without concat', async (
228
+ {page},
229
+ testInfo,
230
+ ) => {
231
+ const {footer, label} = await openFooter(page, LINE_ONE);
232
+ await page.waitForTimeout(800);
233
+
234
+ const firstText = (await label.innerText()).trim();
235
+ expect(firstText).toContain(LINE_ONE);
236
+ expect(firstText).not.toMatch(/ • /);
237
+
238
+ await footer.screenshot({
239
+ path: testInfo.outputPath('01-one-line-isolated.png'),
240
+ });
241
+ await page.screenshot({
242
+ path: testInfo.outputPath('01-one-line-isolated-full.png'),
243
+ fullPage: true,
244
+ });
245
+
246
+ await expect
247
+ .poll(async () => (await label.innerText()).trim(), {
248
+ timeout: 7000,
249
+ intervals: [400, 800, 1200],
250
+ })
251
+ .not.toBe(firstText);
252
+
253
+ const secondText = (await label.innerText()).trim();
254
+ expect(secondText).not.toContain(LINE_ONE);
255
+ expect(secondText).not.toContain(`${LINE_ONE} • `);
256
+ expect(secondText).toMatch(PRIMARY_HINT);
257
+
258
+ await footer.screenshot({
259
+ path: testInfo.outputPath('02-device-version.png'),
260
+ });
261
+ await page.screenshot({
262
+ path: testInfo.outputPath('02-device-version-full.png'),
263
+ fullPage: true,
264
+ });
265
+ });
266
+
267
+ test('0 lines: only primaryText', async ({page}, testInfo) => {
268
+ const {footer, label} = await openFooter(page, '');
269
+ await page.waitForTimeout(800);
270
+
271
+ const text = (await label.innerText()).trim();
272
+ expect(text.length).toBeGreaterThan(0);
273
+ expect(text).not.toContain(LINE_ONE);
274
+ expect(text).toMatch(PRIMARY_HINT);
275
+
276
+ await footer.screenshot({
277
+ path: testInfo.outputPath('03-zero-lines-primary.png'),
278
+ });
279
+ await page.screenshot({
280
+ path: testInfo.outputPath('03-zero-lines-primary-full.png'),
281
+ fullPage: true,
282
+ });
283
+ });
284
+
285
+ test('2+ lines: rotates between configured lines and primary', async (
286
+ {page},
287
+ testInfo,
288
+ ) => {
289
+ const {footer, label} = await openFooter(
290
+ page,
291
+ `${LINE_ONE}\n${LINE_TWO}`,
292
+ );
293
+ await page.waitForTimeout(800);
294
+
295
+ const firstText = (await label.innerText()).trim();
296
+ expect([LINE_ONE, LINE_TWO].some(line => firstText.includes(line))).toBe(
297
+ true,
298
+ );
299
+ expect(firstText).not.toMatch(/ • /);
300
+
301
+ await footer.screenshot({
302
+ path: testInfo.outputPath('04-two-lines-first.png'),
303
+ });
304
+
305
+ await expect
306
+ .poll(async () => (await label.innerText()).trim(), {
307
+ timeout: 7000,
308
+ intervals: [400, 800, 1200],
309
+ })
310
+ .not.toBe(firstText);
311
+
312
+ const secondText = (await label.innerText()).trim();
313
+ expect(secondText).not.toContain(`${LINE_ONE} • `);
314
+ expect(secondText).not.toContain(`${LINE_TWO} • `);
315
+ expect(secondText.length).toBeGreaterThan(0);
316
+
317
+ await footer.screenshot({
318
+ path: testInfo.outputPath('05-two-lines-next.png'),
319
+ });
320
+ await page.screenshot({
321
+ path: testInfo.outputPath('05-two-lines-next-full.png'),
322
+ fullPage: true,
323
+ });
324
+ });
325
+
326
+ test('authenticated staging journey: captures the real runtime footer cycle', async ({
327
+ page,
328
+ }, testInfo) => {
329
+ testInfo.annotations.push({
330
+ type: 'fluxo',
331
+ description: 'outros',
332
+ });
333
+
334
+ const {footer, label, consoleErrors, requestFailures, evidenceDir} =
335
+ await openAuthenticatedFooter(page, testInfo);
336
+ const observedTexts = [];
337
+
338
+ const captureAuthenticatedStep = async stepName => {
339
+ const text = (await label.innerText()).trim();
340
+ expect(text).not.toBe('');
341
+ expect(text).not.toMatch(/ • /);
342
+ observedTexts.push(text);
343
+ await footer.screenshot({path: testInfo.outputPath(`${stepName}.png`)});
344
+ await page.screenshot({
345
+ path: testInfo.outputPath(`${stepName}-full.png`),
346
+ fullPage: true,
347
+ });
348
+ };
349
+
350
+ await page.waitForTimeout(800);
351
+ await captureAuthenticatedStep('01-authenticated-footer-initial');
352
+ await expect
353
+ .poll(async () => (await label.innerText()).trim(), {
354
+ timeout: 7000,
355
+ intervals: [400, 800, 1200],
356
+ })
357
+ .not.toBe(observedTexts[0]);
358
+ await captureAuthenticatedStep('02-authenticated-footer-after-rotation');
359
+
360
+ for (const stepName of [
361
+ '03-authenticated-footer-next',
362
+ '04-authenticated-footer-next',
363
+ '05-authenticated-footer-next',
364
+ ]) {
365
+ await page.waitForTimeout(4200);
366
+ await captureAuthenticatedStep(stepName);
367
+ }
368
+
369
+ expect(new Set(observedTexts).size).toBeGreaterThan(1);
370
+ expect(consoleErrors).toEqual([]);
371
+ expect(requestFailures).toEqual([]);
372
+ writeManifest(evidenceDir, {
373
+ flowchartIds: [],
374
+ fluxo: 'outros',
375
+ steps: [
376
+ '01-authenticated-footer-initial',
377
+ '02-authenticated-footer-after-rotation',
378
+ '03-authenticated-footer-next',
379
+ '04-authenticated-footer-next',
380
+ '05-authenticated-footer-next',
381
+ ],
382
+ prints: [
383
+ '01-authenticated-footer-initial',
384
+ '02-authenticated-footer-after-rotation',
385
+ '03-authenticated-footer-next',
386
+ '04-authenticated-footer-next',
387
+ '05-authenticated-footer-next',
388
+ ],
389
+ missingPrints: [],
390
+ authenticated: true,
391
+ rotationIntervalMs: 4000,
392
+ observedTexts,
393
+ consoleErrors,
394
+ requestFailures,
395
+ });
396
+ });
397
+ });
@@ -0,0 +1,28 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import {describe, expect, it} from '@jest/globals';
4
+
5
+ const providerFiles = [
6
+ 'DefaultProvider.web.js',
7
+ 'DefaultProvider.native.js',
8
+ ];
9
+
10
+ describe('DefaultProvider company authority guard', () => {
11
+ it.each(providerFiles)(
12
+ 'uses the shared authority helper in %s',
13
+ providerFile => {
14
+ const source = fs.readFileSync(
15
+ path.resolve(__dirname, '../../../react/components', providerFile),
16
+ 'utf8',
17
+ );
18
+
19
+ expect(source).not.toContain('isTenantAdministrativeAuthority');
20
+ expect(source).toMatch(
21
+ /import\s*\{\s*canAdministerCompany\s*\}\s*from\s*['"]@controleonline\/ui-common\/src\/react\/utils\/companyAuthority['"]/,
22
+ );
23
+ expect(source).toMatch(
24
+ /canAdministerCompany\(\{\s*company:\s*currentCompany,\s*mainCompany,\s*user\s*\}\)/,
25
+ );
26
+ },
27
+ );
28
+ });
@@ -0,0 +1,29 @@
1
+ const assert = require('node:assert/strict');
2
+ const {test} = require('node:test');
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const babel = require('@babel/core');
6
+
7
+ // Run with the host app's Expo dependencies, just like the production build.
8
+ const filename = path.resolve(__dirname, '../../../react/components/RuntimeFooterMarqueeText.js');
9
+ const source = fs.readFileSync(filename, 'utf8');
10
+
11
+ for (const platform of ['web', 'android', 'ios']) {
12
+ test(`runtime footer compiles with the production Expo preset (${platform})`, () => {
13
+ const result = babel.transformSync(source, {
14
+ filename,
15
+ babelrc: false,
16
+ configFile: false,
17
+ presets: [require.resolve('babel-preset-expo')],
18
+ envName: 'production',
19
+ caller: {
20
+ name: 'metro', bundler: 'metro', platform,
21
+ isDev: false, isServer: false, supportsStaticESM: true,
22
+ },
23
+ });
24
+ assert.ok(result.code.length > 0);
25
+ if (platform === 'web') {
26
+ assert.match(result.code, /react-native-web/);
27
+ }
28
+ });
29
+ }
@@ -6,6 +6,7 @@ jest.mock('../../../react/utils/screenMetrics', () => ({
6
6
  }))
7
7
 
8
8
  const {
9
+ buildDefaultDeviceConfigs,
9
10
  buildProviderManagedDeviceConfigs,
10
11
  DEVICE_ANDROID_KIOSK_ENABLED_CONFIG_KEY,
11
12
  DEVICE_ANDROID_LAUNCHER_ENABLED_CONFIG_KEY,
@@ -18,6 +19,7 @@ const {
18
19
  POS_CASH_MANAGEMENT_MODE_CONFIG_KEY,
19
20
  POS_DELIVERY_ENABLED_CONFIG_KEY,
20
21
  POS_OPERATION_MODE_CONFIG_KEY,
22
+ POS_OPERATION_MODE_DEFAULT,
21
23
  POS_CHECK_ORDER_TYPE_CONFIG_KEY,
22
24
  POS_CHECK_ORDER_TYPE_STAMP,
23
25
  isPosSingleItemMode,
@@ -48,6 +50,28 @@ const {
48
50
  const {describe, expect, it} = global
49
51
 
50
52
  describe('deviceConfigBootstrap POS operation helpers', () => {
53
+ it('builds new device configs with the counter operation mode by default', () => {
54
+ const {nextConfigs, needsUpdate} = buildDefaultDeviceConfigs({
55
+ configs: {},
56
+ appVersion: '1.3.74',
57
+ deviceInfo: {manufacturer: 'Google', isEmulator: true},
58
+ })
59
+
60
+ expect(POS_OPERATION_MODE_DEFAULT).toBe('counter')
61
+ expect(nextConfigs[POS_OPERATION_MODE_CONFIG_KEY]).toBe('counter')
62
+ expect(needsUpdate).toBe(true)
63
+ })
64
+
65
+ it('preserves an explicitly configured operation mode', () => {
66
+ const {nextConfigs} = buildDefaultDeviceConfigs({
67
+ configs: {[POS_OPERATION_MODE_CONFIG_KEY]: 'waiter'},
68
+ appVersion: '1.3.74',
69
+ deviceInfo: {manufacturer: 'Google', isEmulator: true},
70
+ })
71
+
72
+ expect(nextConfigs[POS_OPERATION_MODE_CONFIG_KEY]).toBe('waiter')
73
+ })
74
+
51
75
  it('normalizes balcão as counter and treats counter as self service', () => {
52
76
  const configs = {
53
77
  [POS_OPERATION_MODE_CONFIG_KEY]: 'balcao',
@@ -419,4 +443,3 @@ describe('deviceConfigBootstrap local charge capability', () => {
419
443
  ).toBe(false)
420
444
  })
421
445
  })
422
-
@@ -0,0 +1,42 @@
1
+ const React = require('react')
2
+ const renderer = require('react-test-renderer')
3
+ const {jest} = require('@jest/globals')
4
+ global.IS_REACT_ACT_ENVIRONMENT = true
5
+ jest.mock('react-native', () => ({Text: 'Text', View: 'View', Platform: {OS: 'web', select: x => x.web}, StyleSheet: {create: x => x}}))
6
+ jest.mock('../../../react/utils/screenMetrics', () => ({appendScreenMetrics: x => x, hasScreenMetricsChanges: () => false}))
7
+ const Section = require('../../../react/pages/Devices/detail/DeviceChargePermissionSection').default
8
+ const {isOrderChargeEnabled, isPosChargeEntryEnabled} = require('../../../react/config/deviceConfigBootstrap')
9
+ let tree, props
10
+ beforeEach(() => {
11
+ jest.clearAllMocks()
12
+ props = {configs: {'order-charge-enabled': false, 'pos-gateway': 'infinite-pay', 'payment-type-ids': [11]},
13
+ actionsRef: {current: {deviceConfigActions: {addDeviceConfigs: jest.fn(async () => {})}}},
14
+ currentCompany: {id: 3}, deviceString: 'web-10', deviceType: 'PDV', refreshCurrentConfig: jest.fn(),
15
+ showSystemError: jest.fn(), brandColors: {textSecondary: '#555'}, renderSwitchRow: value => React.createElement('Switch', value)}
16
+ })
17
+ afterEach(async () => {if (tree) await renderer.act(async () => tree.unmount()); tree = null})
18
+ it('saves the existing protected financial permission using the current device and company', async () => {
19
+ await renderer.act(async () => {tree = renderer.create(React.createElement(Section, props))})
20
+ await renderer.act(async () => tree.root.findByType('Switch').props.onValueChange(true))
21
+ expect(props.actionsRef.current.deviceConfigActions.addDeviceConfigs).toHaveBeenCalledWith({device: 'web-10', people: '/people/3', type: 'PDV', configs: '{"order-charge-enabled":true}'})
22
+ expect(props.refreshCurrentConfig).toHaveBeenCalledTimes(1)
23
+ expect(props.configs['pos-gateway']).toBe('infinite-pay')
24
+ expect(props.configs['payment-type-ids']).toEqual([11])
25
+ })
26
+ it('preserves the current permission and reports a rejected administrative write', async () => {
27
+ props.actionsRef.current.deviceConfigActions.addDeviceConfigs.mockRejectedValue(new Error('Acesso negado'))
28
+ await renderer.act(async () => {tree = renderer.create(React.createElement(Section, props))})
29
+ await renderer.act(async () => tree.root.findByType('Switch').props.onValueChange(true))
30
+ expect(props.refreshCurrentConfig).not.toHaveBeenCalled()
31
+ expect(tree.root.findByType('Switch').props.value).toBe(false)
32
+ expect(props.showSystemError).toHaveBeenCalledWith(expect.any(Error), expect.any(String))
33
+ })
34
+ it('honors an explicit canonical denial without erasing legacy config decisions', () => {
35
+ const configs = {'pos-operation-mode': 'waiter', 'pos-local-charge-enabled': true, 'order-charge-enabled': false}
36
+ expect(isOrderChargeEnabled(configs)).toBe(false)
37
+ expect(isPosChargeEntryEnabled(configs)).toBe(false)
38
+ expect(isPosChargeEntryEnabled({...configs, 'order-charge-enabled': true})).toBe(true)
39
+ expect(isOrderChargeEnabled('{"order-charge-enabled":"on"}')).toBe(true)
40
+ expect(isOrderChargeEnabled({})).toBe(false)
41
+ expect(configs['pos-local-charge-enabled']).toBe(true)
42
+ })
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Unit coverage for DeviceDetail applyCurrentDeviceConfig (app-community#706).
3
+ */
4
+ const { describe, test } = require('node:test');
5
+ const assert = require('node:assert/strict');
6
+ const path = require('node:path');
7
+ const { spawnSync } = require('node:child_process');
8
+
9
+ const loaderPath = path.resolve(
10
+ __dirname,
11
+ '../../../react/pages/Devices/detail/useDeviceDetailLoaders.js',
12
+ );
13
+
14
+ const normalizeEntityId = value => {
15
+ if (value == null || value === '') return '';
16
+ const raw = String(value);
17
+ const parts = raw.split('/');
18
+ return parts[parts.length - 1];
19
+ };
20
+
21
+ const findCurrentDeviceConfig = (scopedItems, context = {}) => {
22
+ const currentDeviceString = String(context.deviceString || '').trim();
23
+ const currentDeviceType = String(context.deviceType || '')
24
+ .trim()
25
+ .toUpperCase();
26
+ const currentDeviceConfigId = normalizeEntityId(context.deviceId);
27
+ return (scopedItems || []).find(d => {
28
+ const currentConfigType = String(d?.type || d?.device?.type || '')
29
+ .trim()
30
+ .toUpperCase();
31
+ const nextDeviceId = normalizeEntityId(
32
+ d?.device?.id ||
33
+ d?.device?.['@id'] ||
34
+ d?.deviceId ||
35
+ d?.device?.deviceId,
36
+ );
37
+ if (currentDeviceConfigId && nextDeviceId === currentDeviceConfigId) {
38
+ return true;
39
+ }
40
+ return (
41
+ d?.device?.device === currentDeviceString &&
42
+ currentConfigType === currentDeviceType
43
+ );
44
+ });
45
+ };
46
+
47
+ describe('applyCurrentDeviceConfig contract (task-706)', () => {
48
+ test('production loader parses without duplicate bindings', () => {
49
+ const result = spawnSync(process.execPath, ['--check', loaderPath], {
50
+ encoding: 'utf8',
51
+ });
52
+
53
+ assert.equal(result.status, 0, result.stderr || result.stdout);
54
+ });
55
+
56
+ test('selects config by device id even when type/string differ', () => {
57
+ const items = [
58
+ { type: 'DISPLAY', device: { id: '1', device: 'aaa' } },
59
+ { type: 'PDV', device: { id: '/devices/403', device: 'other' } },
60
+ ];
61
+ const found = findCurrentDeviceConfig(items, {
62
+ deviceId: '403',
63
+ deviceString: 'pdv-x',
64
+ deviceType: 'PDV',
65
+ });
66
+ assert.equal(found?.device?.id, '/devices/403');
67
+ });
68
+
69
+ test('falls back to device string + type when id is missing', () => {
70
+ const items = [
71
+ { type: 'PDV', device: { device: 'device:403' } },
72
+ { type: 'DISPLAY', device: { device: 'device:403' } },
73
+ ];
74
+ const found = findCurrentDeviceConfig(items, {
75
+ deviceString: 'device:403',
76
+ deviceType: 'PDV',
77
+ });
78
+ assert.equal(found?.type, 'PDV');
79
+ });
80
+
81
+ test('returns undefined when no match so loader resets local state', () => {
82
+ const found = findCurrentDeviceConfig([], { deviceId: '403' });
83
+ assert.equal(found, undefined);
84
+ });
85
+ });