@controleonline/ui-common 1.2.80 → 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.
- 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 +40 -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 +493 -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 +484 -0
- package/src/react/pages/Devices/detail/useDeviceDetailStateB.js +71 -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,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
|
});
|