@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.
- 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/passwordPolicy.js +73 -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 +6 -1
- 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 +27 -10
- 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 +10 -8
- package/src/tests/react/utils/shopFranchises.test.js +121 -115
- package/src/tests/react/utils/systemErrorMessage.test.js +32 -59
|
@@ -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
|
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {readFileSync} from 'node:fs';
|
|
4
|
+
import {fileURLToPath} from 'node:url';
|
|
5
|
+
import {dirname, join} from 'node:path';
|
|
6
|
+
|
|
7
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), '../../..');
|
|
8
|
+
|
|
9
|
+
const FORBIDDEN_COLOR = /#[0-9a-fA-F]{3,8}\b|\brgba?\s*\(|\bwhite\b|\bblack\b/i;
|
|
10
|
+
|
|
11
|
+
function loadSource(rel) {
|
|
12
|
+
return readFileSync(join(root, rel), 'utf8');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
test('ConfirmModal.js sources colors only from themeStore getters.colors', () => {
|
|
16
|
+
const src = loadSource('react/components/ConfirmModal.js');
|
|
17
|
+
assert.match(src, /themeStore\.getters\.colors|themeColors/);
|
|
18
|
+
assert.match(src, /modalOverlay|modalBackground|buttonBackground/);
|
|
19
|
+
assert.doesNotMatch(
|
|
20
|
+
src.replace(/transparent/g, ''),
|
|
21
|
+
FORBIDDEN_COLOR,
|
|
22
|
+
'ConfirmModal.js must not hardcode colors',
|
|
23
|
+
);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('ConfirmModal.styles.js createStyles uses only palette tokens', () => {
|
|
27
|
+
const src = loadSource('react/components/ConfirmModal.styles.js');
|
|
28
|
+
assert.match(src, /palette\.modalOverlay/);
|
|
29
|
+
assert.match(src, /palette\.modalBackground/);
|
|
30
|
+
assert.match(src, /palette\.buttonBackground/);
|
|
31
|
+
assert.match(src, /palette\.buttonText/);
|
|
32
|
+
// strip comments and the word transparent (Modal prop, not a color literal in styles)
|
|
33
|
+
const stripped = src
|
|
34
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
35
|
+
.replace(/\/\/.*$/gm, '')
|
|
36
|
+
.replace(/transparent/gi, '');
|
|
37
|
+
assert.doesNotMatch(stripped, FORBIDDEN_COLOR);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('SystemErrorToast.js sources colors only from themeStore getters.colors', () => {
|
|
41
|
+
const src = loadSource('react/components/SystemErrorToast.js');
|
|
42
|
+
assert.match(src, /themeStore\.getters\.colors|themeColors/);
|
|
43
|
+
assert.match(src, /toastDangerBackground|toastDangerText/);
|
|
44
|
+
assert.doesNotMatch(
|
|
45
|
+
src.replace(/transparent/g, ''),
|
|
46
|
+
FORBIDDEN_COLOR,
|
|
47
|
+
'SystemErrorToast.js must not hardcode colors',
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('SystemErrorToast.styles.js createStyles uses only palette tokens', () => {
|
|
52
|
+
const src = loadSource('react/components/SystemErrorToast.styles.js');
|
|
53
|
+
assert.match(src, /palette\.toastDangerBackground/);
|
|
54
|
+
assert.match(src, /palette\.toastDangerText/);
|
|
55
|
+
const stripped = src
|
|
56
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
57
|
+
.replace(/\/\/.*$/gm, '')
|
|
58
|
+
.replace(/transparent/gi, '');
|
|
59
|
+
assert.doesNotMatch(stripped, FORBIDDEN_COLOR);
|
|
60
|
+
});
|