@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,345 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smoke browser: Manager /device-detail alias save updates header without refresh.
|
|
3
|
+
* fluxo: manager-devices
|
|
4
|
+
* Refs: app-community#382
|
|
5
|
+
*
|
|
6
|
+
* Criteria:
|
|
7
|
+
* - Edit device name → save → displayed alias in header becomes the new value without page reload.
|
|
8
|
+
* - Modules under Devices/detail remain ≤ 500 lines (absolute limit).
|
|
9
|
+
*/
|
|
10
|
+
const { expect, test } = require('playwright/test');
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const packageJson = require('../../../../../../../package.json');
|
|
14
|
+
const { API_ORIGIN } = require('../../../../../../../src/tests/browser/apiOrigin');
|
|
15
|
+
|
|
16
|
+
const APP_VERSION = packageJson?.version || '1.0.0';
|
|
17
|
+
const CURRENT_DEVICE_ID = 'web-7';
|
|
18
|
+
const DEVICE_ENTITY_ID = 396;
|
|
19
|
+
const INITIAL_ALIAS = 'Caixa atual';
|
|
20
|
+
const NEXT_ALIAS = 'Caixa Renomeado #382';
|
|
21
|
+
|
|
22
|
+
const CORS_HEADERS = {
|
|
23
|
+
'access-control-allow-origin': '*',
|
|
24
|
+
'access-control-allow-headers':
|
|
25
|
+
'API-TOKEN, APP-DOMAIN, DEVICE, ACCEPT, CONTENT-TYPE, X-Requested-With',
|
|
26
|
+
'access-control-allow-methods': 'GET,POST,PUT,PATCH,DELETE,OPTIONS',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const jsonHeaders = () => ({
|
|
30
|
+
...CORS_HEADERS,
|
|
31
|
+
'content-type': 'application/ld+json; charset=utf-8',
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const collection = (member) => ({
|
|
35
|
+
member,
|
|
36
|
+
'hydra:member': member,
|
|
37
|
+
totalItems: member.length,
|
|
38
|
+
'hydra:totalItems': member.length,
|
|
39
|
+
summary: {},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const currentDevice = {
|
|
43
|
+
'@id': `/devices/${DEVICE_ENTITY_ID}`,
|
|
44
|
+
'@type': 'Device',
|
|
45
|
+
id: DEVICE_ENTITY_ID,
|
|
46
|
+
device: CURRENT_DEVICE_ID,
|
|
47
|
+
alias: INITIAL_ALIAS,
|
|
48
|
+
metadata: {
|
|
49
|
+
runtime: 'web',
|
|
50
|
+
network: { publicIp: '127.0.0.1' },
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const createDeviceConfig = ({ id, type, device = currentDevice }) => ({
|
|
55
|
+
'@id': `/device_configs/${id}`,
|
|
56
|
+
'@type': 'DeviceConfig',
|
|
57
|
+
id,
|
|
58
|
+
type,
|
|
59
|
+
people: '/people/3',
|
|
60
|
+
device,
|
|
61
|
+
configs: JSON.stringify({
|
|
62
|
+
'config-version': APP_VERSION,
|
|
63
|
+
'pos-gateway': 'infinite-pay',
|
|
64
|
+
}),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const detailDir = path.join(__dirname, '../../../react/pages/Devices/detail');
|
|
68
|
+
|
|
69
|
+
const MODULES_MAX_500 = [
|
|
70
|
+
'DeviceDetailScreen.js',
|
|
71
|
+
'DeviceDetailHeader.js',
|
|
72
|
+
'DeviceDetailRenderers.js',
|
|
73
|
+
'DeviceDetailPdvConfigSection.js',
|
|
74
|
+
'DeviceDetailOrdersPrintSection.js',
|
|
75
|
+
'DeviceDetailAlertsCommandsSection.js',
|
|
76
|
+
'DeviceDetailMovementSections.js',
|
|
77
|
+
'DeviceDetailPaymentSection.js',
|
|
78
|
+
'deviceDetailConstants.js',
|
|
79
|
+
'deviceDetailHelpers.js',
|
|
80
|
+
'OptionButtonChip.js',
|
|
81
|
+
'useDeviceDetailStateA.js',
|
|
82
|
+
'useDeviceDetailStateB.js',
|
|
83
|
+
'useDeviceDetailLoaders.js',
|
|
84
|
+
'useDeviceDetailActions.js',
|
|
85
|
+
'useDeviceDetailSaves.js',
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
const mockDevicesApi = async (page) => {
|
|
89
|
+
const company = {
|
|
90
|
+
id: 3,
|
|
91
|
+
name: 'Teste',
|
|
92
|
+
alias: 'TESTE',
|
|
93
|
+
panel_enabled: true,
|
|
94
|
+
enabled: true,
|
|
95
|
+
commercial_enabled: true,
|
|
96
|
+
theme: {
|
|
97
|
+
colors: {
|
|
98
|
+
primary: '#0EA5E9',
|
|
99
|
+
cardBackground: '#FFFFFF',
|
|
100
|
+
cardBorder: '#D8E0EA',
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const managerConfig = createDeviceConfig({ id: 487, type: 'MANAGER' });
|
|
106
|
+
const deviceConfigs = [managerConfig];
|
|
107
|
+
/** @type {Array<{id:number,alias:string}>} */
|
|
108
|
+
const savedAliasPuts = [];
|
|
109
|
+
|
|
110
|
+
// Live mutable alias for GET after PUT
|
|
111
|
+
let liveAlias = INITIAL_ALIAS;
|
|
112
|
+
|
|
113
|
+
await page.route(`${API_ORIGIN}/**`, async (route) => {
|
|
114
|
+
const request = route.request();
|
|
115
|
+
const url = new URL(request.url());
|
|
116
|
+
const pathname = url.pathname.replace(/^\/+/, '');
|
|
117
|
+
const method = request.method().toUpperCase();
|
|
118
|
+
|
|
119
|
+
if (method === 'OPTIONS') {
|
|
120
|
+
return route.fulfill({ status: 204, headers: CORS_HEADERS, body: '' });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (pathname === 'themes-colors.css') {
|
|
124
|
+
return route.fulfill({
|
|
125
|
+
status: 200,
|
|
126
|
+
headers: { ...CORS_HEADERS, 'content-type': 'text/css; charset=utf-8' },
|
|
127
|
+
body: ':root { --primary: #0ea5e9; }',
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (pathname === 'runtime/ip') {
|
|
132
|
+
return route.fulfill({
|
|
133
|
+
status: 200,
|
|
134
|
+
headers: jsonHeaders(),
|
|
135
|
+
body: JSON.stringify({ ip: '127.0.0.1' }),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (pathname === 'menus-people') {
|
|
140
|
+
return route.fulfill({
|
|
141
|
+
status: 200,
|
|
142
|
+
headers: jsonHeaders(),
|
|
143
|
+
body: JSON.stringify({ modules: {} }),
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (pathname === 'people/companies/my') {
|
|
148
|
+
return route.fulfill({
|
|
149
|
+
status: 200,
|
|
150
|
+
headers: jsonHeaders(),
|
|
151
|
+
body: JSON.stringify(collection([company])),
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (pathname === 'people/company/default') {
|
|
156
|
+
return route.fulfill({
|
|
157
|
+
status: 200,
|
|
158
|
+
headers: jsonHeaders(),
|
|
159
|
+
body: JSON.stringify(company),
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (pathname === 'configs/discovery-configs') {
|
|
164
|
+
return route.fulfill({
|
|
165
|
+
status: 200,
|
|
166
|
+
headers: jsonHeaders(),
|
|
167
|
+
body: JSON.stringify({ configs: {} }),
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (pathname === 'devices' && method === 'GET') {
|
|
172
|
+
return route.fulfill({
|
|
173
|
+
status: 200,
|
|
174
|
+
headers: jsonHeaders(),
|
|
175
|
+
body: JSON.stringify(
|
|
176
|
+
collection([{ ...currentDevice, alias: liveAlias }]),
|
|
177
|
+
),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (
|
|
182
|
+
(pathname === `devices/${DEVICE_ENTITY_ID}` ||
|
|
183
|
+
pathname === `devices/${CURRENT_DEVICE_ID}`) &&
|
|
184
|
+
method === 'GET'
|
|
185
|
+
) {
|
|
186
|
+
return route.fulfill({
|
|
187
|
+
status: 200,
|
|
188
|
+
headers: jsonHeaders(),
|
|
189
|
+
body: JSON.stringify({ ...currentDevice, alias: liveAlias }),
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Alias save: PUT /devices/{id}
|
|
194
|
+
if (
|
|
195
|
+
(pathname === `devices/${DEVICE_ENTITY_ID}` ||
|
|
196
|
+
pathname === `devices/${CURRENT_DEVICE_ID}`) &&
|
|
197
|
+
(method === 'PUT' || method === 'PATCH')
|
|
198
|
+
) {
|
|
199
|
+
let body = {};
|
|
200
|
+
try {
|
|
201
|
+
body = request.postDataJSON() || {};
|
|
202
|
+
} catch {
|
|
203
|
+
body = {};
|
|
204
|
+
}
|
|
205
|
+
const next = String(body.alias || body.name || '').trim() || liveAlias;
|
|
206
|
+
liveAlias = next;
|
|
207
|
+
savedAliasPuts.push({ id: DEVICE_ENTITY_ID, alias: next });
|
|
208
|
+
return route.fulfill({
|
|
209
|
+
status: 200,
|
|
210
|
+
headers: jsonHeaders(),
|
|
211
|
+
body: JSON.stringify({ ...currentDevice, alias: next }),
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (pathname === 'devices' && method === 'POST') {
|
|
216
|
+
return route.fulfill({
|
|
217
|
+
status: 200,
|
|
218
|
+
headers: jsonHeaders(),
|
|
219
|
+
body: JSON.stringify({ ...currentDevice, alias: liveAlias }),
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (pathname === 'device_configs' && method === 'GET') {
|
|
224
|
+
return route.fulfill({
|
|
225
|
+
status: 200,
|
|
226
|
+
headers: jsonHeaders(),
|
|
227
|
+
body: JSON.stringify(collection(deviceConfigs)),
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (pathname.startsWith('device_configs/') && method === 'GET') {
|
|
232
|
+
const id = Number(pathname.split('/')[1]);
|
|
233
|
+
const found = deviceConfigs.find((c) => c.id === id) || managerConfig;
|
|
234
|
+
return route.fulfill({
|
|
235
|
+
status: 200,
|
|
236
|
+
headers: jsonHeaders(),
|
|
237
|
+
body: JSON.stringify({
|
|
238
|
+
...found,
|
|
239
|
+
device: { ...found.device, alias: liveAlias },
|
|
240
|
+
}),
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Fallback: empty collection / 200
|
|
245
|
+
return route.fulfill({
|
|
246
|
+
status: 200,
|
|
247
|
+
headers: jsonHeaders(),
|
|
248
|
+
body: JSON.stringify(collection([])),
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
await page.addInitScript(
|
|
253
|
+
({ appVersion }) => {
|
|
254
|
+
localStorage.setItem('token', 'smoke-token-382');
|
|
255
|
+
localStorage.setItem('config', JSON.stringify({ language: 'pt-br' }));
|
|
256
|
+
localStorage.setItem(
|
|
257
|
+
'device',
|
|
258
|
+
JSON.stringify({
|
|
259
|
+
id: 'web-7',
|
|
260
|
+
device: 'web-7',
|
|
261
|
+
type: 'MANAGER',
|
|
262
|
+
appName: 'Browser Manager',
|
|
263
|
+
appVersion,
|
|
264
|
+
buildNumber: appVersion,
|
|
265
|
+
systemName: 'web',
|
|
266
|
+
systemVersion: 'web',
|
|
267
|
+
deviceType: 'web',
|
|
268
|
+
metadata: { runtime: 'web' },
|
|
269
|
+
}),
|
|
270
|
+
);
|
|
271
|
+
},
|
|
272
|
+
{ appVersion: APP_VERSION },
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
return { savedAliasPuts, getLiveAlias: () => liveAlias };
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
test.describe('device-detail alias save (browser smoke #382)', () => {
|
|
279
|
+
test('all detail modules respect absolute 500-line limit', async () => {
|
|
280
|
+
for (const name of MODULES_MAX_500) {
|
|
281
|
+
const f = path.join(detailDir, name);
|
|
282
|
+
expect(fs.existsSync(f), `missing ${name}`).toBe(true);
|
|
283
|
+
const lines = fs.readFileSync(f, 'utf8').split(/\r?\n/).length;
|
|
284
|
+
expect(lines, `${name} has ${lines} lines (max 500)`).toBeLessThanOrEqual(
|
|
285
|
+
500,
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test('deviceAliasSync helper exists for save path', async () => {
|
|
291
|
+
const helper = path.join(
|
|
292
|
+
__dirname,
|
|
293
|
+
'../../../react/utils/deviceAliasSync.js',
|
|
294
|
+
);
|
|
295
|
+
expect(fs.existsSync(helper)).toBe(true);
|
|
296
|
+
expect(fs.readFileSync(helper, 'utf8')).toMatch(/buildDeviceAliasStoreUpdates/);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test('edit alias → save → header shows new value without refresh', async ({
|
|
300
|
+
page,
|
|
301
|
+
}) => {
|
|
302
|
+
const api = await mockDevicesApi(page);
|
|
303
|
+
|
|
304
|
+
await page.goto('/devices-index?store=device_config');
|
|
305
|
+
|
|
306
|
+
// Open device detail (MANAGER profile of current device)
|
|
307
|
+
await page.getByTestId('device-config-487').click();
|
|
308
|
+
await expect(page).toHaveURL(/device-detail/);
|
|
309
|
+
|
|
310
|
+
// Header must show initial alias
|
|
311
|
+
await expect(page.getByText(INITIAL_ALIAS, { exact: true })).toBeVisible({
|
|
312
|
+
timeout: 15000,
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// Header shows initial alias via testID
|
|
316
|
+
await expect(page.getByTestId('device-alias-text')).toHaveText(INITIAL_ALIAS, {
|
|
317
|
+
timeout: 15000,
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// Start edit via testID
|
|
321
|
+
await page.getByTestId('device-alias-edit').click();
|
|
322
|
+
|
|
323
|
+
// Fill input (testID on RN TextInput maps to data-testid on web)
|
|
324
|
+
const aliasInput = page.getByTestId('device-alias-input');
|
|
325
|
+
await expect(aliasInput).toBeVisible({ timeout: 10000 });
|
|
326
|
+
await aliasInput.fill(NEXT_ALIAS);
|
|
327
|
+
|
|
328
|
+
// Save via testID
|
|
329
|
+
await page.getByTestId('device-alias-save').click();
|
|
330
|
+
|
|
331
|
+
// Wait for PUT
|
|
332
|
+
await expect
|
|
333
|
+
.poll(() => api.savedAliasPuts.length, { timeout: 15000 })
|
|
334
|
+
.toBeGreaterThanOrEqual(1);
|
|
335
|
+
expect(api.savedAliasPuts[api.savedAliasPuts.length - 1].alias).toBe(
|
|
336
|
+
NEXT_ALIAS,
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
// CRITICAL acceptance: header shows NEW alias without page reload
|
|
340
|
+
await expect(page.getByText(NEXT_ALIAS, { exact: true })).toBeVisible({
|
|
341
|
+
timeout: 10000,
|
|
342
|
+
});
|
|
343
|
+
await expect(page).toHaveURL(/device-detail/);
|
|
344
|
+
});
|
|
345
|
+
});
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smoke browser: Manager /device-detail copy configs from another device.
|
|
3
|
+
* fluxo: manager-devices
|
|
4
|
+
* Refs: app-community#629
|
|
5
|
+
*
|
|
6
|
+
* Criteria:
|
|
7
|
+
* - Destino abre detalhe → ícone copy → escolhe origem → confirma.
|
|
8
|
+
* - POST /device_configs/add-configs usa identidade do destino (device/people).
|
|
9
|
+
* - Origem não é alterada; chaves copiadas vêm do configs da origem.
|
|
10
|
+
*/
|
|
11
|
+
const { expect, test } = require('playwright/test');
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
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
|
+
const DEST_DEVICE_STRING = 'web-7';
|
|
19
|
+
const DEST_ENTITY_ID = 396;
|
|
20
|
+
const DEST_ALIAS = 'Caixa destino';
|
|
21
|
+
const SRC_DEVICE_STRING = 'src-88';
|
|
22
|
+
const SRC_ENTITY_ID = 410;
|
|
23
|
+
const SRC_ALIAS = 'Caixa origem';
|
|
24
|
+
const OTHER_TENANT_DEVICE = 'other-co';
|
|
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 collection = (member) => ({
|
|
39
|
+
member,
|
|
40
|
+
'hydra:member': member,
|
|
41
|
+
totalItems: member.length,
|
|
42
|
+
'hydra:totalItems': member.length,
|
|
43
|
+
summary: {},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const destDevice = {
|
|
47
|
+
'@id': `/devices/${DEST_ENTITY_ID}`,
|
|
48
|
+
'@type': 'Device',
|
|
49
|
+
id: DEST_ENTITY_ID,
|
|
50
|
+
device: DEST_DEVICE_STRING,
|
|
51
|
+
alias: DEST_ALIAS,
|
|
52
|
+
metadata: { runtime: 'web', network: { publicIp: '127.0.0.1' } },
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const srcDevice = {
|
|
56
|
+
'@id': `/devices/${SRC_ENTITY_ID}`,
|
|
57
|
+
'@type': 'Device',
|
|
58
|
+
id: SRC_ENTITY_ID,
|
|
59
|
+
device: SRC_DEVICE_STRING,
|
|
60
|
+
alias: SRC_ALIAS,
|
|
61
|
+
metadata: { runtime: 'web' },
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const createDeviceConfig = ({ id, type, device, people = '/people/3', configs }) => ({
|
|
65
|
+
'@id': `/device_configs/${id}`,
|
|
66
|
+
'@type': 'DeviceConfig',
|
|
67
|
+
id,
|
|
68
|
+
type,
|
|
69
|
+
people,
|
|
70
|
+
device,
|
|
71
|
+
configs: JSON.stringify(configs),
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const destManager = createDeviceConfig({
|
|
75
|
+
id: 487,
|
|
76
|
+
type: 'MANAGER',
|
|
77
|
+
device: destDevice,
|
|
78
|
+
configs: { 'config-version': APP_VERSION, 'pos-gateway': 'infinite-pay' },
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const srcPdv = createDeviceConfig({
|
|
82
|
+
id: 501,
|
|
83
|
+
type: 'PDV',
|
|
84
|
+
device: srcDevice,
|
|
85
|
+
configs: {
|
|
86
|
+
'pos-operation-mode': 'cashier',
|
|
87
|
+
'pos-gateway': 'cielo',
|
|
88
|
+
'printer-enabled': '1',
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const srcPrint = createDeviceConfig({
|
|
93
|
+
id: 502,
|
|
94
|
+
type: 'PRINT',
|
|
95
|
+
device: srcDevice,
|
|
96
|
+
configs: { 'print-network-port': '9100' },
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const otherTenant = createDeviceConfig({
|
|
100
|
+
id: 777,
|
|
101
|
+
type: 'PDV',
|
|
102
|
+
people: '/people/99',
|
|
103
|
+
device: {
|
|
104
|
+
'@id': '/devices/999',
|
|
105
|
+
id: 999,
|
|
106
|
+
device: OTHER_TENANT_DEVICE,
|
|
107
|
+
alias: 'Outra empresa',
|
|
108
|
+
},
|
|
109
|
+
configs: { 'pos-gateway': 'getnet' },
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const mockCopyApi = async (page) => {
|
|
113
|
+
const company = {
|
|
114
|
+
id: 3,
|
|
115
|
+
name: 'Teste',
|
|
116
|
+
alias: 'TESTE',
|
|
117
|
+
panel_enabled: true,
|
|
118
|
+
enabled: true,
|
|
119
|
+
commercial_enabled: true,
|
|
120
|
+
theme: {
|
|
121
|
+
colors: {
|
|
122
|
+
primary: '#0EA5E9',
|
|
123
|
+
cardBackground: '#FFFFFF',
|
|
124
|
+
cardBorder: '#D8E0EA',
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const companyConfigs = [destManager, srcPdv, srcPrint];
|
|
130
|
+
/** @type {Array<object>} */
|
|
131
|
+
const addConfigPosts = [];
|
|
132
|
+
|
|
133
|
+
await page.route(`${API_ORIGIN}/**`, async (route) => {
|
|
134
|
+
const request = route.request();
|
|
135
|
+
const url = new URL(request.url());
|
|
136
|
+
const pathname = url.pathname.replace(/^\/+/, '');
|
|
137
|
+
const method = request.method().toUpperCase();
|
|
138
|
+
|
|
139
|
+
if (method === 'OPTIONS') {
|
|
140
|
+
return route.fulfill({ status: 204, headers: CORS_HEADERS, body: '' });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (pathname === 'themes-colors.css') {
|
|
144
|
+
return route.fulfill({
|
|
145
|
+
status: 200,
|
|
146
|
+
headers: { ...CORS_HEADERS, 'content-type': 'text/css; charset=utf-8' },
|
|
147
|
+
body: ':root { --primary: #0ea5e9; }',
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (pathname === 'runtime/ip') {
|
|
152
|
+
return route.fulfill({
|
|
153
|
+
status: 200,
|
|
154
|
+
headers: jsonHeaders(),
|
|
155
|
+
body: JSON.stringify({ ip: '127.0.0.1' }),
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (pathname === 'menus-people') {
|
|
160
|
+
return route.fulfill({
|
|
161
|
+
status: 200,
|
|
162
|
+
headers: jsonHeaders(),
|
|
163
|
+
body: JSON.stringify({ modules: {} }),
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (pathname === 'people/companies/my') {
|
|
168
|
+
return route.fulfill({
|
|
169
|
+
status: 200,
|
|
170
|
+
headers: jsonHeaders(),
|
|
171
|
+
body: JSON.stringify(collection([company])),
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (pathname === 'people/company/default') {
|
|
176
|
+
return route.fulfill({
|
|
177
|
+
status: 200,
|
|
178
|
+
headers: jsonHeaders(),
|
|
179
|
+
body: JSON.stringify(company),
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (pathname === 'configs/discovery-configs') {
|
|
184
|
+
return route.fulfill({
|
|
185
|
+
status: 200,
|
|
186
|
+
headers: jsonHeaders(),
|
|
187
|
+
body: JSON.stringify({ configs: {} }),
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (pathname === 'devices' && method === 'GET') {
|
|
192
|
+
return route.fulfill({
|
|
193
|
+
status: 200,
|
|
194
|
+
headers: jsonHeaders(),
|
|
195
|
+
body: JSON.stringify(collection([destDevice, srcDevice])),
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (
|
|
200
|
+
(pathname === `devices/${DEST_ENTITY_ID}` ||
|
|
201
|
+
pathname === `devices/${DEST_DEVICE_STRING}`) &&
|
|
202
|
+
method === 'GET'
|
|
203
|
+
) {
|
|
204
|
+
return route.fulfill({
|
|
205
|
+
status: 200,
|
|
206
|
+
headers: jsonHeaders(),
|
|
207
|
+
body: JSON.stringify(destDevice),
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (pathname === 'device_configs' && method === 'GET') {
|
|
212
|
+
const people = String(url.searchParams.get('people') || '');
|
|
213
|
+
const rows =
|
|
214
|
+
people.includes('/people/3') || !people
|
|
215
|
+
? companyConfigs
|
|
216
|
+
: [];
|
|
217
|
+
return route.fulfill({
|
|
218
|
+
status: 200,
|
|
219
|
+
headers: jsonHeaders(),
|
|
220
|
+
body: JSON.stringify(collection([...rows, otherTenant])),
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (pathname === 'device_configs/add-configs' && method === 'POST') {
|
|
225
|
+
let body = {};
|
|
226
|
+
try {
|
|
227
|
+
body = request.postDataJSON() || {};
|
|
228
|
+
} catch {
|
|
229
|
+
body = {};
|
|
230
|
+
}
|
|
231
|
+
addConfigPosts.push(body);
|
|
232
|
+
return route.fulfill({
|
|
233
|
+
status: 200,
|
|
234
|
+
headers: jsonHeaders(),
|
|
235
|
+
body: JSON.stringify({
|
|
236
|
+
...destManager,
|
|
237
|
+
type: body.type || 'PDV',
|
|
238
|
+
device: destDevice,
|
|
239
|
+
configs: body.configs || destManager.configs,
|
|
240
|
+
}),
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (pathname.startsWith('device_configs/') && method === 'GET') {
|
|
245
|
+
const id = Number(pathname.split('/')[1]);
|
|
246
|
+
const found =
|
|
247
|
+
[...companyConfigs, otherTenant].find((c) => c.id === id) || destManager;
|
|
248
|
+
return route.fulfill({
|
|
249
|
+
status: 200,
|
|
250
|
+
headers: jsonHeaders(),
|
|
251
|
+
body: JSON.stringify(found),
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return route.fulfill({
|
|
256
|
+
status: 200,
|
|
257
|
+
headers: jsonHeaders(),
|
|
258
|
+
body: JSON.stringify(collection([])),
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
await page.addInitScript(
|
|
263
|
+
({ appVersion }) => {
|
|
264
|
+
localStorage.setItem('token', 'smoke-token-629');
|
|
265
|
+
localStorage.setItem('config', JSON.stringify({ language: 'pt-br' }));
|
|
266
|
+
localStorage.setItem(
|
|
267
|
+
'device',
|
|
268
|
+
JSON.stringify({
|
|
269
|
+
id: 'web-7',
|
|
270
|
+
device: 'web-7',
|
|
271
|
+
type: 'MANAGER',
|
|
272
|
+
appName: 'Browser Manager',
|
|
273
|
+
appVersion,
|
|
274
|
+
buildNumber: appVersion,
|
|
275
|
+
systemName: 'web',
|
|
276
|
+
systemVersion: 'web',
|
|
277
|
+
deviceType: 'web',
|
|
278
|
+
metadata: { runtime: 'web' },
|
|
279
|
+
}),
|
|
280
|
+
);
|
|
281
|
+
},
|
|
282
|
+
{ appVersion: APP_VERSION },
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
return { addConfigPosts };
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
test.describe('device-detail copy config (browser smoke #629)', () => {
|
|
289
|
+
test('copy modal modules stay under 500 lines', async () => {
|
|
290
|
+
const files = [
|
|
291
|
+
path.join(__dirname, '../../../react/components/CopyDeviceConfigModal.js'),
|
|
292
|
+
path.join(__dirname, '../../../react/utils/copyDeviceConfigs.js'),
|
|
293
|
+
path.join(
|
|
294
|
+
__dirname,
|
|
295
|
+
'../../../react/pages/Devices/detail/useDeviceDetailCopyConfig.js',
|
|
296
|
+
),
|
|
297
|
+
];
|
|
298
|
+
for (const f of files) {
|
|
299
|
+
expect(fs.existsSync(f), `missing ${f}`).toBe(true);
|
|
300
|
+
const lines = fs.readFileSync(f, 'utf8').split(/\r?\n/).length;
|
|
301
|
+
expect(lines, `${f} has ${lines} lines`).toBeLessThanOrEqual(500);
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test('copy from source device posts destination identity only', async ({
|
|
306
|
+
page,
|
|
307
|
+
}) => {
|
|
308
|
+
const api = await mockCopyApi(page);
|
|
309
|
+
|
|
310
|
+
await page.goto('/devices-index?store=device_config');
|
|
311
|
+
await page.getByTestId('device-config-487').click();
|
|
312
|
+
await expect(page).toHaveURL(/device-detail/);
|
|
313
|
+
await expect(page.getByTestId('device-alias-text')).toHaveText(DEST_ALIAS, {
|
|
314
|
+
timeout: 15000,
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
await page.getByTestId('device-detail-copy-config-btn').click();
|
|
318
|
+
await expect(page.getByTestId('copy-device-config-modal')).toBeVisible({
|
|
319
|
+
timeout: 15000,
|
|
320
|
+
});
|
|
321
|
+
await expect(page.getByTestId('copy-device-config-hint')).toContainText(
|
|
322
|
+
DEST_ALIAS,
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
await expect(
|
|
326
|
+
page.getByTestId(`copy-device-config-option-${SRC_DEVICE_STRING}`),
|
|
327
|
+
).toBeVisible({ timeout: 10000 });
|
|
328
|
+
await expect(
|
|
329
|
+
page.getByTestId(`copy-device-config-option-${DEST_DEVICE_STRING}`),
|
|
330
|
+
).toHaveCount(0);
|
|
331
|
+
await expect(
|
|
332
|
+
page.getByTestId(`copy-device-config-option-${OTHER_TENANT_DEVICE}`),
|
|
333
|
+
).toHaveCount(0);
|
|
334
|
+
|
|
335
|
+
await page
|
|
336
|
+
.getByTestId(`copy-device-config-option-${SRC_DEVICE_STRING}`)
|
|
337
|
+
.click();
|
|
338
|
+
await expect(page.getByTestId('copy-device-config-preview')).toBeVisible();
|
|
339
|
+
await expect(page.getByTestId('copy-device-config-preview')).toContainText(
|
|
340
|
+
'PDV',
|
|
341
|
+
);
|
|
342
|
+
await expect(page.getByTestId('copy-device-config-preview')).toContainText(
|
|
343
|
+
'pos-gateway',
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
await page.getByTestId('copy-device-config-confirm').click();
|
|
347
|
+
|
|
348
|
+
await expect
|
|
349
|
+
.poll(() => api.addConfigPosts.length, { timeout: 15000 })
|
|
350
|
+
.toBeGreaterThanOrEqual(1);
|
|
351
|
+
|
|
352
|
+
const devicesPosted = api.addConfigPosts.map((p) => String(p.device || ''));
|
|
353
|
+
expect(devicesPosted.every((d) => d === DEST_DEVICE_STRING)).toBe(true);
|
|
354
|
+
expect(devicesPosted).not.toContain(SRC_DEVICE_STRING);
|
|
355
|
+
|
|
356
|
+
const peoplePosted = api.addConfigPosts.map((p) => String(p.people || ''));
|
|
357
|
+
expect(peoplePosted.every((p) => p === '/people/3')).toBe(true);
|
|
358
|
+
|
|
359
|
+
const types = api.addConfigPosts.map((p) => String(p.type || '').toUpperCase());
|
|
360
|
+
expect(types).toEqual(expect.arrayContaining(['PDV', 'PRINT']));
|
|
361
|
+
|
|
362
|
+
await expect(page).toHaveURL(/device-detail/);
|
|
363
|
+
await expect(page.getByTestId('device-alias-text')).toHaveText(DEST_ALIAS);
|
|
364
|
+
});
|
|
365
|
+
});
|