@controleonline/ui-common 1.2.87 → 1.2.88
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/.github/workflows/integration-source-gate.yml +68 -0
- package/.github/workflows/manager-worker.yml +7 -156
- package/.github/workflows/publish-npm.yml +73 -27
- package/.github/workflows/pull-request-checks.yml +7 -5
- package/docs/technical/Device-Detail-Alias-UI-Sync.md +141 -0
- package/package.json +13 -8
- package/src/react/components/BottomNavigationBar.js +178 -174
- package/src/react/components/BottomNavigationBar.styles.js +139 -118
- package/src/react/components/DefaultProvider.native.js +427 -421
- package/src/react/components/DefaultProvider.web.js +131 -125
- package/src/react/components/RemoteCheckoutService.js +3 -3
- package/src/react/components/RuntimeFooterMarqueeText.js +88 -36
- package/src/react/components/RuntimeInfoFooter.js +76 -43
- package/src/react/components/RuntimeInfoFooter.styles.js +22 -14
- package/src/react/config/deviceConfigBootstrap.js +3 -3
- package/src/react/pages/Devices/detail/DeviceDetailAlertsCommandsSection.js +4 -22
- package/src/react/pages/Devices/detail/DeviceDetailMovementSections.js +6 -2
- package/src/react/pages/Devices/detail/DeviceDetailOrdersPrintSection.js +4 -0
- package/src/react/pages/Devices/detail/DeviceDetailScreen.js +23 -338
- package/src/react/pages/Devices/detail/createDeviceDetailContext.js +20 -0
- package/src/react/pages/Devices/detail/useDeviceDetailActions.js +11 -36
- package/src/react/pages/Devices/detail/useDeviceDetailLoaders.js +129 -37
- package/src/react/pages/Devices/detail/useDeviceDetailSaves.js +11 -38
- package/src/react/pages/Devices/detail/useDeviceDetailStateA.js +9 -28
- package/src/react/pages/IntegrationConfigFields.js +1 -0
- package/src/react/pages/IntegrationConfigPage.js +101 -45
- package/src/react/pages/IntegrationConfigPage.utils.js +21 -13
- package/src/react/pages/fetchPeopleConfigs.js +21 -0
- package/src/react/pages/integrationsCatalog.js +15 -110
- package/src/react/services/Cielo/Cielo.js +9 -9
- package/src/react/services/Getnet/Getnet.web.js +25 -0
- package/src/react/utils/companyAuthority.js +36 -0
- package/src/react/utils/fileUrl.js +14 -5
- package/src/react/utils/paymentDevices.js +375 -270
- package/src/react/utils/printerDevices.js +2 -2
- package/src/react/utils/runtimeFooter.js +178 -224
- package/src/react/utils/runtimeFooterText.js +160 -0
- package/src/react/utils/runtimeLanguage.js +5 -5
- package/src/react/utils/runtimeZoom.js +29 -5
- package/src/react/utils/shopFranchises.js +167 -12
- package/src/react/utils/storeColumns.js +235 -217
- package/src/react/utils/systemErrorMessage.js +2 -1
- package/src/store/integration/index.js +43 -0
- package/src/tests/browser/manager/device-detail-apply-config-defined.spec.js +31 -0
- package/src/tests/browser/manager/device-detail-composition.spec.js +295 -0
- package/src/tests/browser/manager/device-detail-copy-config.spec.js +11 -1
- package/src/tests/browser/manager/device-detail-pdv-current-company.spec.js +273 -0
- package/src/tests/browser/manager/runtime-footer-rotation.spec.js +397 -0
- package/src/tests/react/components/DefaultProviderAuthority.test.js +28 -0
- package/src/tests/react/components/RuntimeFooterMarqueeText.build.test.js +29 -0
- package/src/tests/react/config/deviceConfigBootstrap.test.js +24 -1
- package/src/tests/react/pages/applyCurrentDeviceConfig.test.js +85 -0
- package/src/tests/react/pages/deviceDetailComposition.test.js +65 -0
- package/src/tests/react/pages/deviceDetailHookDependencies.test.mjs +31 -0
- package/src/tests/react/pages/deviceDetailLoaderDependencies.test.mjs +28 -0
- package/src/tests/react/pages/receitaFederalIntegrationContract.test.mjs +27 -6
- package/src/tests/react/services/Cielo.test.js +2 -2
- package/src/tests/react/utils/companyAuthority.test.js +53 -0
- package/src/tests/react/utils/formatter.test.mjs +6 -0
- package/src/tests/react/utils/paymentDevices.test.js +72 -0
- package/src/tests/react/utils/runtimeFooter.test.js +35 -0
- package/src/tests/react/utils/runtimeFooterText.test.js +88 -0
- package/src/tests/react/utils/runtimeLanguage.test.js +4 -4
- package/src/tests/react/utils/runtimeZoom.test.js +20 -0
- package/src/tests/react/utils/shopFranchises.test.js +32 -13
- package/src/tests/react/utils/translate.test.mjs +6 -6
- package/src/utils/formatter.js +7 -3
- package/src/utils/translate.js +16 -16
- package/src/utils/translatePending.js +6 -6
- 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,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
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const {Linter} = require('eslint');
|
|
4
|
+
const parser = require('@babel/parser');
|
|
5
|
+
const traverse = require('@babel/traverse').default;
|
|
6
|
+
const createContext = require('../../../react/pages/Devices/detail/createDeviceDetailContext').default;
|
|
7
|
+
|
|
8
|
+
const directory = path.resolve(__dirname, '../../../react/pages/Devices/detail');
|
|
9
|
+
const files = fs.readdirSync(directory).filter(file => file.endsWith('.js'));
|
|
10
|
+
|
|
11
|
+
describe('DeviceDetail composition regression (app-community#706)', () => {
|
|
12
|
+
test.each(files)('%s parses and has no unbound runtime or JSX references', file => {
|
|
13
|
+
const source = fs.readFileSync(path.join(directory, file), 'utf8');
|
|
14
|
+
const messages = new Linter().verify(source, {
|
|
15
|
+
parserOptions: {ecmaVersion: 2022, sourceType: 'module', ecmaFeatures: {jsx: true}},
|
|
16
|
+
env: {browser: true, node: true, es2022: true},
|
|
17
|
+
rules: {'no-undef': 'error'},
|
|
18
|
+
});
|
|
19
|
+
expect(messages.map(message => `${message.line}: ${message.message}`)).toEqual([]);
|
|
20
|
+
const unbound = [];
|
|
21
|
+
traverse(parser.parse(source, {sourceType: 'module', plugins: ['jsx']}), {
|
|
22
|
+
JSXOpeningElement(element) {
|
|
23
|
+
const name = element.node.name;
|
|
24
|
+
if (name.type === 'JSXIdentifier' && /^[A-Z]/.test(name.name) &&
|
|
25
|
+
!element.scope.hasBinding(name.name)) unbound.push(name.name);
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
expect(unbound).toEqual([]);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('section context retains actual values and callbacks instead of losing hook outputs', () => {
|
|
32
|
+
const setVisibility = jest.fn();
|
|
33
|
+
const setDelivery = jest.fn();
|
|
34
|
+
const saveDelivery = jest.fn();
|
|
35
|
+
const renderSwitchRow = jest.fn();
|
|
36
|
+
const state = {
|
|
37
|
+
currentCompany: {id: 7}, themeColors: {buttonText: '#fff'},
|
|
38
|
+
brandColors: {primary: '#000'}, runtimeDevice: {id: 403},
|
|
39
|
+
deviceOrderVisibility: 'company', setDeviceOrderVisibility: setVisibility,
|
|
40
|
+
deviceDeliveryEnabled: true, setDeviceDeliveryEnabled: setDelivery,
|
|
41
|
+
savingDeviceDeliverySettings: true, displayAutoPrintProductEnabled: true,
|
|
42
|
+
products: [{quantity: 2}], deviceRuntimeDebugInfoEnabled: true,
|
|
43
|
+
};
|
|
44
|
+
const context = createContext(state, {refreshCurrentConfig: jest.fn()}, {}, {
|
|
45
|
+
saveDeviceDeliverySettings: saveDelivery, renderSwitchRow, inflowTotal: 90,
|
|
46
|
+
});
|
|
47
|
+
expect(context.currentCompany).toBe(state.currentCompany);
|
|
48
|
+
expect(context.runtimeDevice).toBe(state.runtimeDevice);
|
|
49
|
+
expect(context.products).toBe(state.products);
|
|
50
|
+
expect(context.inflowTotal).toBe(90);
|
|
51
|
+
expect(context.palette).toBe(state.brandColors);
|
|
52
|
+
expect(context.orderVisibility).toBe('company');
|
|
53
|
+
expect(context.deliveryEnabled).toBe(true);
|
|
54
|
+
expect(context.savingDeliverySettings).toBe(true);
|
|
55
|
+
expect(context.displayAutoPrintProduct).toBe(true);
|
|
56
|
+
expect(context.runtimeDebugInfoEnabled).toBe(true);
|
|
57
|
+
context.setOrderVisibility('device');
|
|
58
|
+
context.setDeliveryEnabled(false);
|
|
59
|
+
context.saveDeviceDeliverySettings({deviceDeliveryEnabled: false});
|
|
60
|
+
expect(setVisibility).toHaveBeenCalledWith('device');
|
|
61
|
+
expect(setDelivery).toHaveBeenCalledWith(false);
|
|
62
|
+
expect(saveDelivery).toHaveBeenCalledWith({deviceDeliveryEnabled: false});
|
|
63
|
+
expect(context.renderSwitchRow).toBe(renderSwitchRow);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import {readFileSync} from 'node:fs';
|
|
3
|
+
import test from 'node:test';
|
|
4
|
+
|
|
5
|
+
const hookFiles = [
|
|
6
|
+
'useDeviceDetailActions.js',
|
|
7
|
+
'useDeviceDetailSaves.js',
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
for (const file of hookFiles) {
|
|
11
|
+
test(`${file} receives currentCompany from deps`, () => {
|
|
12
|
+
const source = readFileSync(
|
|
13
|
+
new URL(`../../../react/pages/Devices/detail/${file}`, import.meta.url),
|
|
14
|
+
'utf8',
|
|
15
|
+
);
|
|
16
|
+
const dependencies = source.match(
|
|
17
|
+
/export default function [^(]+\(deps\)[\s\S]*?const \{([\s\S]*?)\}\s*=\s*deps;/,
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
assert.ok(dependencies, `${file} dependency block not found`);
|
|
21
|
+
const block = dependencies[1];
|
|
22
|
+
const hits = block.match(/(?:^|,)\s*currentCompany\s*(?:,|$)/gm) || [];
|
|
23
|
+
|
|
24
|
+
assert.equal(
|
|
25
|
+
hits.length,
|
|
26
|
+
1,
|
|
27
|
+
`${file} must destructure currentCompany exactly once`,
|
|
28
|
+
);
|
|
29
|
+
assert.match(source, /currentCompany\?\.id/);
|
|
30
|
+
});
|
|
31
|
+
}
|