@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,262 @@
1
+ /**
2
+ * fluxo: outros
3
+ * Justificativa: ajuste visual do footer runtime (marquee 1 linha), fora dos
4
+ * fluxos de negócio do catálogo canônico.
5
+ * Issue: app-community#630
6
+ *
7
+ * Passos:
8
+ * 1. Abrir shell Manager com mensagem curta no footer
9
+ * 2. Capturar footer (1 linha, estático)
10
+ * 3. Abrir shell Manager com mensagem longa
11
+ * 4. Capturar footer (1 linha + overflow/marquee)
12
+ */
13
+ const {expect, test} = require('playwright/test');
14
+ const packageJson = require('../../../../../../../package.json');
15
+ const {API_ORIGIN} = require('../../../../../../../src/tests/browser/apiOrigin');
16
+
17
+ const APP_VERSION = packageJson?.version || '1.0.0';
18
+
19
+ const CORS_HEADERS = {
20
+ 'access-control-allow-origin': '*',
21
+ 'access-control-allow-headers':
22
+ 'API-TOKEN, APP-DOMAIN, DEVICE, ACCEPT, CONTENT-TYPE, X-Requested-With',
23
+ 'access-control-allow-methods': 'GET,POST,PUT,PATCH,DELETE,OPTIONS',
24
+ };
25
+
26
+ const jsonHeaders = () => ({
27
+ ...CORS_HEADERS,
28
+ 'content-type': 'application/ld+json; charset=utf-8',
29
+ });
30
+
31
+ const textHeaders = () => ({
32
+ ...CORS_HEADERS,
33
+ 'content-type': 'text/css; charset=utf-8',
34
+ });
35
+
36
+ const collection = member => ({
37
+ member,
38
+ 'hydra:member': member,
39
+ totalItems: member.length,
40
+ 'hydra:totalItems': member.length,
41
+ summary: {},
42
+ });
43
+
44
+ const LONG_FOOTER =
45
+ 'Aviso operacional: fila de produção atrasada — conferir pedidos abertos, estoque crítico e dispositivos KDS offline antes de liberar o próximo turno de atendimento no salão e delivery';
46
+
47
+ const createCompany = footerText => ({
48
+ id: 3,
49
+ name: 'Controle Online',
50
+ alias: 'CONTROLE ONLINE',
51
+ panel_enabled: true,
52
+ enabled: true,
53
+ commercial_enabled: true,
54
+ theme: {
55
+ colors: {
56
+ primary: '#0EA5E9',
57
+ footerBackground: '#0F172A',
58
+ footerBorder: '#1E293B',
59
+ footerText: '#E2E8F0',
60
+ footerLink: '#38BDF8',
61
+ success: '#22C55E',
62
+ warning: '#F59E0B',
63
+ error: '#EF4444',
64
+ },
65
+ },
66
+ configs: {
67
+ 'device-runtime-footer-text': footerText,
68
+ },
69
+ });
70
+
71
+ const mockApi = async (page, footerText) => {
72
+ const company = createCompany(footerText);
73
+
74
+ await page.route(`${API_ORIGIN}/**`, async route => {
75
+ const request = route.request();
76
+ const url = new URL(request.url());
77
+ const pathname = url.pathname.replace(/^\/+/, '');
78
+ const method = request.method().toUpperCase();
79
+
80
+ if (method === 'OPTIONS') {
81
+ return route.fulfill({status: 204, headers: CORS_HEADERS, body: ''});
82
+ }
83
+
84
+ if (pathname === 'themes-colors.css') {
85
+ return route.fulfill({
86
+ status: 200,
87
+ headers: textHeaders(),
88
+ body: ':root { --primary: #0ea5e9; --footer-text: #e2e8f0; }',
89
+ });
90
+ }
91
+
92
+ if (pathname === 'runtime/ip') {
93
+ return route.fulfill({
94
+ status: 200,
95
+ headers: jsonHeaders(),
96
+ body: JSON.stringify({ip: '127.0.0.1'}),
97
+ });
98
+ }
99
+
100
+ if (pathname === 'menus-people') {
101
+ return route.fulfill({
102
+ status: 200,
103
+ headers: jsonHeaders(),
104
+ body: JSON.stringify({modules: {}}),
105
+ });
106
+ }
107
+
108
+ if (
109
+ pathname === 'people/company/default' ||
110
+ pathname === 'people/companies/my'
111
+ ) {
112
+ return route.fulfill({
113
+ status: 200,
114
+ headers: jsonHeaders(),
115
+ body: JSON.stringify(
116
+ pathname === 'people/company/default' ? company : collection([company]),
117
+ ),
118
+ });
119
+ }
120
+
121
+ if (
122
+ pathname === 'people' ||
123
+ pathname === 'devices' ||
124
+ pathname === 'device_configs'
125
+ ) {
126
+ return route.fulfill({
127
+ status: 200,
128
+ headers: jsonHeaders(),
129
+ body: JSON.stringify(collection([])),
130
+ });
131
+ }
132
+
133
+ return route.fulfill({
134
+ status: 200,
135
+ headers: jsonHeaders(),
136
+ body: JSON.stringify(collection([])),
137
+ });
138
+ });
139
+
140
+ await page.addInitScript(
141
+ ({appVersion}) => {
142
+ window.localStorage.setItem(
143
+ 'session',
144
+ JSON.stringify({
145
+ id: 7,
146
+ people: '/people/7',
147
+ api_key: 'test-api-key',
148
+ active: 1,
149
+ mycompany: 3,
150
+ roles: ['ROLE_SUPER'],
151
+ phone: '(11) 97114-0832',
152
+ email: 'qa@controleonline.com',
153
+ }),
154
+ );
155
+ window.localStorage.setItem('config', JSON.stringify({language: 'pt-br'}));
156
+ window.localStorage.setItem('app-type', 'MANAGER');
157
+ window.localStorage.setItem(
158
+ 'device',
159
+ JSON.stringify({
160
+ id: 'web-manager',
161
+ device: 'web-manager',
162
+ type: 'WEB',
163
+ appName: 'Browser Manager',
164
+ appVersion,
165
+ buildNumber: appVersion,
166
+ systemName: 'web',
167
+ systemVersion: 'web',
168
+ deviceType: 'web',
169
+ metadata: {},
170
+ }),
171
+ );
172
+ },
173
+ {appVersion: APP_VERSION},
174
+ );
175
+ };
176
+
177
+ async function assertSingleLineFooter(page, label) {
178
+ const footer = page.getByTestId('runtime-info-footer');
179
+ await expect(footer, `footer visible (${label})`).toBeVisible({timeout: 15000});
180
+
181
+ const box = await footer.boundingBox();
182
+ expect(box, `footer box (${label})`).toBeTruthy();
183
+ // One visual line of content + chrome: keep height bounded (not stacked multi-line body)
184
+ expect(box.height, `footer height single-line-ish (${label})`).toBeLessThan(120);
185
+
186
+ const marquee = page.getByTestId('runtime-footer-marquee-text');
187
+ if (await marquee.count()) {
188
+ await expect(marquee.first()).toBeVisible();
189
+ }
190
+
191
+ return footer;
192
+ }
193
+
194
+ test.describe('runtime footer marquee (#630) — fluxo: outros', () => {
195
+ test('short message: footer one line and static', async ({page}, testInfo) => {
196
+ const consoleErrors = [];
197
+ page.on('console', message => {
198
+ if (message.type() === 'error') consoleErrors.push(message.text());
199
+ });
200
+ page.on('pageerror', error => {
201
+ consoleErrors.push(String(error?.message || error));
202
+ });
203
+
204
+ await mockApi(page, 'OK');
205
+ await page.goto('/devices-index?store=device_config');
206
+ await page.waitForTimeout(2500);
207
+
208
+ const footer = await assertSingleLineFooter(page, 'short');
209
+ await footer.screenshot({
210
+ path: testInfo.outputPath('step-short-footer.png'),
211
+ });
212
+ await page.screenshot({
213
+ path: testInfo.outputPath('step-short-full.png'),
214
+ fullPage: true,
215
+ });
216
+
217
+ const relevant = consoleErrors.filter(text =>
218
+ /TypeError|Cannot read|undefined is not|RuntimeInfoFooter|Marquee/i.test(
219
+ text,
220
+ ),
221
+ );
222
+ expect(relevant, JSON.stringify(relevant)).toEqual([]);
223
+ });
224
+
225
+ test('long message: footer stays one line and remains readable', async ({
226
+ page,
227
+ }, testInfo) => {
228
+ const consoleErrors = [];
229
+ page.on('console', message => {
230
+ if (message.type() === 'error') consoleErrors.push(message.text());
231
+ });
232
+ page.on('pageerror', error => {
233
+ consoleErrors.push(String(error?.message || error));
234
+ });
235
+
236
+ await mockApi(page, LONG_FOOTER);
237
+ await page.goto('/devices-index?store=device_config');
238
+ await page.waitForTimeout(2500);
239
+
240
+ const footer = await assertSingleLineFooter(page, 'long');
241
+ await footer.screenshot({
242
+ path: testInfo.outputPath('step-long-footer.png'),
243
+ });
244
+ await page.screenshot({
245
+ path: testInfo.outputPath('step-long-full.png'),
246
+ fullPage: true,
247
+ });
248
+
249
+ // Wait a bit so marquee animation can start if overflow applies
250
+ await page.waitForTimeout(2000);
251
+ await footer.screenshot({
252
+ path: testInfo.outputPath('step-long-footer-after-hold.png'),
253
+ });
254
+
255
+ const relevant = consoleErrors.filter(text =>
256
+ /TypeError|Cannot read|undefined is not|RuntimeInfoFooter|Marquee/i.test(
257
+ text,
258
+ ),
259
+ );
260
+ expect(relevant, JSON.stringify(relevant)).toEqual([]);
261
+ });
262
+ });
@@ -0,0 +1,236 @@
1
+ const {expect, test} = require('playwright/test');
2
+ const packageJson = require('../../../../../../../package.json');
3
+ const {API_ORIGIN} = require('../../../../../../../src/tests/browser/apiOrigin');
4
+
5
+ const APP_VERSION = packageJson?.version || '1.0.0';
6
+
7
+ const CORS_HEADERS = {
8
+ 'access-control-allow-origin': '*',
9
+ 'access-control-allow-headers':
10
+ 'API-TOKEN, APP-DOMAIN, DEVICE, ACCEPT, CONTENT-TYPE, X-Requested-With',
11
+ 'access-control-allow-methods': 'GET,POST,PUT,PATCH,DELETE,OPTIONS',
12
+ };
13
+
14
+ const jsonHeaders = () => ({
15
+ ...CORS_HEADERS,
16
+ 'content-type': 'application/ld+json; charset=utf-8',
17
+ });
18
+
19
+ const textHeaders = () => ({
20
+ ...CORS_HEADERS,
21
+ 'content-type': 'text/css; charset=utf-8',
22
+ });
23
+
24
+ const collection = member => ({
25
+ member,
26
+ 'hydra:member': member,
27
+ totalItems: member.length,
28
+ 'hydra:totalItems': member.length,
29
+ summary: {},
30
+ });
31
+
32
+ const createCompany = () => ({
33
+ id: 3,
34
+ name: 'Controle Online',
35
+ alias: 'CONTROLE ONLINE',
36
+ panel_enabled: true,
37
+ enabled: true,
38
+ commercial_enabled: true,
39
+ theme: {
40
+ colors: {
41
+ primary: '#0EA5E9',
42
+ secondary: '#F97316',
43
+ buttonBackground: '#0EA5E9',
44
+ buttonBorder: '#0284C7',
45
+ buttonText: '#FFFFFF',
46
+ buttonTextSecondary: '#64748B',
47
+ cardBackground: '#FFFFFF',
48
+ cardBorder: '#D8E0EA',
49
+ modalOverlay: 'rgba(15, 23, 42, 0.45)',
50
+ modalBackground: '#FFFFFF',
51
+ modalBorder: '#D8E0EA',
52
+ modalHeaderText: '#0F172A',
53
+ modalText: '#334155',
54
+ toastDangerBackground: '#FEE2E2',
55
+ toastDangerBorder: '#FECACA',
56
+ toastDangerIcon: '#DC2626',
57
+ toastDangerText: '#991B1B',
58
+ },
59
+ },
60
+ configs: {},
61
+ });
62
+
63
+ const mockApi = async page => {
64
+ const company = createCompany();
65
+
66
+ await page.route(`${API_ORIGIN}/**`, async route => {
67
+ const request = route.request();
68
+ const url = new URL(request.url());
69
+ const pathname = url.pathname.replace(/^\/+/, '');
70
+ const method = request.method().toUpperCase();
71
+
72
+ if (method === 'OPTIONS') {
73
+ return route.fulfill({status: 204, headers: CORS_HEADERS, body: ''});
74
+ }
75
+
76
+ if (pathname === 'themes-colors.css') {
77
+ return route.fulfill({
78
+ status: 200,
79
+ headers: textHeaders(),
80
+ body: ':root { --primary: #0ea5e9; --modal-overlay: rgba(15,23,42,0.45); }',
81
+ });
82
+ }
83
+
84
+ if (pathname === 'runtime/ip') {
85
+ return route.fulfill({
86
+ status: 200,
87
+ headers: jsonHeaders(),
88
+ body: JSON.stringify({ip: '127.0.0.1'}),
89
+ });
90
+ }
91
+
92
+ if (
93
+ pathname === 'people/company/default' ||
94
+ pathname === 'people' ||
95
+ pathname === 'devices' ||
96
+ pathname === 'device_configs' ||
97
+ pathname === 'menus-people'
98
+ ) {
99
+ return route.fulfill({
100
+ status: 200,
101
+ headers: jsonHeaders(),
102
+ body: JSON.stringify(collection([company])),
103
+ });
104
+ }
105
+
106
+ return route.fulfill({
107
+ status: 200,
108
+ headers: jsonHeaders(),
109
+ body: JSON.stringify(collection([])),
110
+ });
111
+ });
112
+
113
+ await page.addInitScript(
114
+ ({appVersion}) => {
115
+ window.localStorage.setItem(
116
+ 'session',
117
+ JSON.stringify({
118
+ id: 7,
119
+ people: '/people/7',
120
+ api_key: 'test-api-key',
121
+ active: 1,
122
+ mycompany: 3,
123
+ roles: ['ROLE_SUPER'],
124
+ phone: '(11) 97114-0832',
125
+ email: 'qa@controleonline.com',
126
+ }),
127
+ );
128
+ window.localStorage.setItem('config', JSON.stringify({language: 'pt-br'}));
129
+ window.localStorage.setItem('app-type', 'MANAGER');
130
+ window.localStorage.setItem(
131
+ 'device',
132
+ JSON.stringify({
133
+ id: 'web-manager',
134
+ device: 'web-manager',
135
+ type: 'WEB',
136
+ appName: 'Browser Manager',
137
+ appVersion,
138
+ buildNumber: appVersion,
139
+ systemName: 'web',
140
+ systemVersion: 'web',
141
+ deviceType: 'web',
142
+ metadata: {},
143
+ }),
144
+ );
145
+ },
146
+ {appVersion: APP_VERSION},
147
+ );
148
+ };
149
+
150
+ test.describe('theme ConfirmModal + SystemErrorToast (#362)', () => {
151
+ test('manager shell loads with theme tokens and no console errors on shared UI', async ({
152
+ page,
153
+ }) => {
154
+ const consoleErrors = [];
155
+ page.on('console', message => {
156
+ if (message.type() === 'error') {
157
+ consoleErrors.push(message.text());
158
+ }
159
+ });
160
+ page.on('pageerror', error => {
161
+ consoleErrors.push(String(error?.message || error));
162
+ });
163
+
164
+ await mockApi(page);
165
+ await page.goto('/devices-index?store=device_config');
166
+
167
+ // Page should settle without theme/runtime crashes
168
+ await page.waitForTimeout(2000);
169
+
170
+ // Soft presence check — devices index or empty state or nav shell
171
+ const bodyText = await page.locator('body').innerText();
172
+ expect(bodyText.length).toBeGreaterThan(0);
173
+
174
+ // No React / theme token resolution crashes
175
+ const relevant = consoleErrors.filter(
176
+ text =>
177
+ /theme|TypeError|Cannot read|undefined is not|StyleSheet|modalOverlay|toastDanger/i.test(
178
+ text,
179
+ ),
180
+ );
181
+ expect(
182
+ relevant,
183
+ `Unexpected theme/runtime console errors: ${JSON.stringify(relevant)}`,
184
+ ).toEqual([]);
185
+ });
186
+
187
+ test('SystemErrorToast path accepts theme palette without throwing when error is published', async ({
188
+ page,
189
+ }) => {
190
+ const consoleErrors = [];
191
+ page.on('console', message => {
192
+ if (message.type() === 'error') {
193
+ consoleErrors.push(message.text());
194
+ }
195
+ });
196
+ page.on('pageerror', error => {
197
+ consoleErrors.push(String(error?.message || error));
198
+ });
199
+
200
+ await mockApi(page);
201
+ await page.goto('/devices-index?store=device_config');
202
+ await page.waitForTimeout(1500);
203
+
204
+ // Attempt to publish a system error through any global channel the app may expose
205
+ await page.evaluate(() => {
206
+ try {
207
+ // MessageService / systemErrorChannel may be reachable via window hooks in some builds
208
+ if (typeof window.__publishSystemError === 'function') {
209
+ window.__publishSystemError('QA theme toast probe #362');
210
+ return;
211
+ }
212
+ // Fallback: dispatch a CustomEvent some runtimes listen to
213
+ window.dispatchEvent(
214
+ new CustomEvent('controleonline:system-error', {
215
+ detail: {message: 'QA theme toast probe #362'},
216
+ }),
217
+ );
218
+ } catch (e) {
219
+ // Swallow — presence of throw is asserted via consoleErrors
220
+ console.error(String(e));
221
+ }
222
+ });
223
+
224
+ await page.waitForTimeout(800);
225
+
226
+ const relevant = consoleErrors.filter(text =>
227
+ /theme|modalOverlay|toastDanger|Cannot read properties of undefined \(reading 'colors'\)/i.test(
228
+ text,
229
+ ),
230
+ );
231
+ expect(
232
+ relevant,
233
+ `Theme token errors after system-error probe: ${JSON.stringify(relevant)}`,
234
+ ).toEqual([]);
235
+ });
236
+ });
@@ -0,0 +1,171 @@
1
+ const assert = require('node:assert/strict');
2
+ const {test, before, after, describe} = require('node:test');
3
+ const Module = require('module');
4
+ const path = require('path');
5
+ const React = require('react');
6
+
7
+ const originalLoad = Module._load;
8
+ let lastLoopConfig = null;
9
+ let animationStarts = 0;
10
+
11
+ before(() => {
12
+ Module._load = function mockLoad(request, parent, isMain) {
13
+ if (request === 'react-native') {
14
+ function AnimatedValue(v) {
15
+ this._value = v;
16
+ }
17
+ AnimatedValue.prototype.setValue = function (v) {
18
+ this._value = v;
19
+ };
20
+ AnimatedValue.prototype.stopAnimation = function () {};
21
+
22
+ const timing = (value, config) => ({
23
+ _type: 'timing',
24
+ value,
25
+ config,
26
+ start: cb => {
27
+ animationStarts += 1;
28
+ if (typeof cb === 'function') cb({finished: true});
29
+ },
30
+ stop: () => {},
31
+ });
32
+
33
+ return {
34
+ Animated: {
35
+ Value: AnimatedValue,
36
+ View: props =>
37
+ React.createElement('AnimatedView', props, props.children),
38
+ Text: props =>
39
+ React.createElement('AnimatedText', props, props.children),
40
+ timing,
41
+ sequence: items => ({
42
+ _type: 'sequence',
43
+ items,
44
+ start: cb => {
45
+ animationStarts += 1;
46
+ if (typeof cb === 'function') cb({finished: true});
47
+ },
48
+ stop: () => {},
49
+ }),
50
+ loop: anim => {
51
+ lastLoopConfig = anim;
52
+ return {
53
+ _type: 'loop',
54
+ anim,
55
+ start: cb => {
56
+ animationStarts += 1;
57
+ if (typeof cb === 'function') cb({finished: true});
58
+ },
59
+ stop: () => {},
60
+ };
61
+ },
62
+ delay: ms => ({_type: 'delay', ms, start: () => {}, stop: () => {}}),
63
+ },
64
+ Text: props => React.createElement('Text', props, props.children),
65
+ View: props => React.createElement('View', props, props.children),
66
+ StyleSheet: {create: styles => styles},
67
+ };
68
+ }
69
+ return originalLoad.call(this, request, parent, isMain);
70
+ };
71
+ });
72
+
73
+ after(() => {
74
+ Module._load = originalLoad;
75
+ });
76
+
77
+ const loadComponent = () => {
78
+ const componentPath = path.resolve(
79
+ __dirname,
80
+ '../../../react/components/RuntimeFooterMarqueeText.js',
81
+ );
82
+ delete require.cache[componentPath];
83
+ return require(componentPath);
84
+ };
85
+
86
+ test('RuntimeFooterMarqueeText exports a React component', () => {
87
+ const mod = loadComponent();
88
+ assert.equal(typeof mod, 'function');
89
+ assert.equal(typeof mod.default, 'function');
90
+ });
91
+
92
+ test('short text stays single-line without duplicate marquee copy', async () => {
93
+ const RuntimeFooterMarqueeText = loadComponent();
94
+ const renderer = require('react-test-renderer');
95
+
96
+ animationStarts = 0;
97
+ lastLoopConfig = null;
98
+
99
+ let tree;
100
+ renderer.act(() => {
101
+ tree = renderer.create(
102
+ React.createElement(RuntimeFooterMarqueeText, {
103
+ text: 'v1.2.3 • PDV',
104
+ color: '#111',
105
+ style: {fontSize: 10},
106
+ }),
107
+ );
108
+ });
109
+
110
+ const root = tree.root.findByProps({testID: 'runtime-footer-marquee-text'});
111
+ renderer.act(() => {
112
+ root.props.onLayout({nativeEvent: {layout: {width: 400, height: 12}}});
113
+ });
114
+
115
+ const textNodes = tree.root.findAllByType('Text');
116
+ assert.equal(textNodes.length, 1);
117
+ assert.equal(textNodes[0].props.numberOfLines, 1);
118
+ assert.equal(textNodes[0].props.children, 'v1.2.3 • PDV');
119
+
120
+ renderer.act(() => {
121
+ textNodes[0].props.onLayout({
122
+ nativeEvent: {layout: {width: 80, height: 12}},
123
+ });
124
+ });
125
+
126
+ // Still short: content 80 < container 400 → no duplicate
127
+ assert.equal(tree.root.findAllByType('Text').length, 1);
128
+ assert.equal(lastLoopConfig, null);
129
+ });
130
+
131
+ test('long text enables marquee duplicate and starts loop animation', async () => {
132
+ const RuntimeFooterMarqueeText = loadComponent();
133
+ const renderer = require('react-test-renderer');
134
+
135
+ animationStarts = 0;
136
+ lastLoopConfig = null;
137
+
138
+ const longText =
139
+ 'Mensagem muito longa do rodape runtime para validar deslizamento horizontal continuo no Manager e PDV';
140
+
141
+ let tree;
142
+ renderer.act(() => {
143
+ tree = renderer.create(
144
+ React.createElement(RuntimeFooterMarqueeText, {
145
+ text: longText,
146
+ color: '#222',
147
+ style: {fontSize: 10},
148
+ }),
149
+ );
150
+ });
151
+
152
+ const root = tree.root.findByProps({testID: 'runtime-footer-marquee-text'});
153
+ renderer.act(() => {
154
+ root.props.onLayout({nativeEvent: {layout: {width: 120, height: 12}}});
155
+ });
156
+
157
+ const firstText = tree.root.findAllByType('Text')[0];
158
+ renderer.act(() => {
159
+ firstText.props.onLayout({
160
+ nativeEvent: {layout: {width: 480, height: 12}},
161
+ });
162
+ });
163
+
164
+ const texts = tree.root.findAllByType('Text');
165
+ assert.equal(texts.length, 2, 'duplicate copy for seamless loop');
166
+ assert.equal(texts[0].props.children, longText);
167
+ assert.equal(texts[1].props.children, longText);
168
+ assert.equal(texts[0].props.numberOfLines, 1);
169
+ assert.ok(lastLoopConfig, 'Animated.loop should start when overflowing');
170
+ assert.ok(animationStarts >= 1);
171
+ });
@@ -4,6 +4,7 @@ const {jest} = require('@jest/globals');
4
4
 
5
5
  jest.mock('react-native', () => ({
6
6
  Image: props => React.createElement('Image', props),
7
+ Platform: {OS: 'ios'},
7
8
  StyleSheet: {
8
9
  create: styles => styles,
9
10
  },
@@ -33,20 +34,23 @@ describe('UserAvatar', () => {
33
34
  expect(tree.root.findByType('Text').props.children).toBe('CT');
34
35
  });
35
36
 
36
- it('uses gravatar as fallback when enabled', () => {
37
+ it('uses gravatar as fallback when enabled (native path)', async () => {
37
38
  let tree;
38
39
 
39
- renderer.act(() => {
40
+ await renderer.act(async () => {
40
41
  tree = renderer.create(
41
42
  React.createElement(UserAvatar, {
42
43
  email: 'client@example.com',
43
44
  name: 'Client Test',
44
45
  }),
45
46
  );
47
+ // flush useEffect
48
+ await Promise.resolve();
46
49
  });
47
50
 
48
51
  const image = tree.root.findByType('Image');
49
52
 
50
53
  expect(image.props.source.uri).toContain('gravatar.com/avatar/');
54
+ expect(image.props.source.uri).toContain('d=404');
51
55
  });
52
56
  });