@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.
Files changed (91) hide show
  1. package/AGENTS.md +8 -0
  2. package/package.json +9 -3
  3. package/src/api/fetch.js +7 -7
  4. package/src/react/components/AddImportModal.js +6 -2
  5. package/src/react/components/BackgroundRuntimeBridge.js +201 -215
  6. package/src/react/components/ConfirmModal.js +44 -9
  7. package/src/react/components/ConfirmModal.styles.js +55 -41
  8. package/src/react/components/CopyDeviceConfigModal.js +193 -0
  9. package/src/react/components/CopyDeviceConfigModal.styles.js +123 -0
  10. package/src/react/components/DefaultProvider.native.js +22 -0
  11. package/src/react/components/DefaultProvider.web.js +25 -1
  12. package/src/react/components/EntityLogBranch.js +482 -0
  13. package/src/react/components/EntityLogCards.js +369 -0
  14. package/src/react/components/EntityLogContent.js +113 -794
  15. package/src/react/components/LinkedOrderProductsTab.js +3 -3
  16. package/src/react/components/PrintService.js +130 -409
  17. package/src/react/components/RemoteCheckoutService.js +2 -2
  18. package/src/react/components/RuntimeBottomNavigationBar.js +141 -146
  19. package/src/react/components/RuntimeFooterMarqueeText.js +137 -0
  20. package/src/react/components/RuntimeInfoFooter.js +12 -15
  21. package/src/react/components/SystemErrorToast.js +49 -29
  22. package/src/react/components/SystemErrorToast.styles.js +43 -38
  23. package/src/react/components/UserAvatar.js +120 -28
  24. package/src/react/config/deviceConfigBootstrap.js +330 -304
  25. package/src/react/hooks/usePrintSpoolEffects.js +161 -0
  26. package/src/react/pages/DeviceDetailPage.js +6 -2882
  27. package/src/react/pages/Devices/detail/DeviceDetailAlertsCommandsSection.js +233 -0
  28. package/src/react/pages/Devices/detail/DeviceDetailHeader.js +138 -0
  29. package/src/react/pages/Devices/detail/DeviceDetailMovementSections.js +167 -0
  30. package/src/react/pages/Devices/detail/DeviceDetailOrdersPrintSection.js +216 -0
  31. package/src/react/pages/Devices/detail/DeviceDetailPaymentSection.js +40 -0
  32. package/src/react/pages/Devices/detail/DeviceDetailPdvConfigSection.js +390 -0
  33. package/src/react/pages/Devices/detail/DeviceDetailRenderers.js +135 -0
  34. package/src/react/pages/Devices/detail/DeviceDetailScreen.js +493 -0
  35. package/src/react/pages/Devices/detail/OptionButtonChip.js +65 -0
  36. package/src/react/pages/Devices/detail/deviceDetailConstants.js +36 -0
  37. package/src/react/pages/Devices/detail/deviceDetailHelpers.js +70 -0
  38. package/src/react/pages/Devices/detail/useDeviceDetailActions.js +483 -0
  39. package/src/react/pages/Devices/detail/useDeviceDetailCopyConfig.js +87 -0
  40. package/src/react/pages/Devices/detail/useDeviceDetailLoaders.js +327 -0
  41. package/src/react/pages/Devices/detail/useDeviceDetailSaves.js +429 -0
  42. package/src/react/pages/Devices/detail/useDeviceDetailStateA.js +484 -0
  43. package/src/react/pages/Devices/detail/useDeviceDetailStateB.js +71 -0
  44. package/src/react/pages/Devices/deviceTypes/index.js +2 -0
  45. package/src/react/pages/Devices/deviceTypes/pdv.js +11 -0
  46. package/src/react/pages/Imports.js +3 -2
  47. package/src/react/pages/IntegrationConfigFields.js +126 -0
  48. package/src/react/pages/IntegrationConfigPage.js +122 -591
  49. package/src/react/pages/IntegrationConfigPage.utils.js +95 -0
  50. package/src/react/print/usePrintButtonController.js +9 -1
  51. package/src/react/services/Getnet/Checkout.js +81 -0
  52. package/src/react/services/Getnet/Getnet.js +77 -0
  53. package/src/react/services/paymentGatewayExecution.js +20 -0
  54. package/src/react/utils/bottomNavigationToolbar.js +161 -0
  55. package/src/react/utils/commercialDocumentOrders.js +18 -6
  56. package/src/react/utils/copyDeviceConfigs.js +198 -0
  57. package/src/react/utils/fileUrl.js +43 -0
  58. package/src/react/utils/importStatus.js +19 -10
  59. package/src/react/utils/logSettings.js +7 -7
  60. package/src/react/utils/maintenanceSettings.js +16 -0
  61. package/src/react/utils/passwordPolicy.js +73 -0
  62. package/src/react/utils/paymentDevices.js +6 -0
  63. package/src/react/utils/printManagedPrinter.js +45 -0
  64. package/src/react/utils/printRouting.js +63 -0
  65. package/src/react/utils/printSpoolUtils.js +119 -0
  66. package/src/react/utils/printerDevices.js +368 -368
  67. package/src/react/utils/runtimeFooter.js +6 -1
  68. package/src/react/utils/shopFranchises.js +186 -65
  69. package/src/react/utils/systemErrorMessage.js +71 -67
  70. package/src/store/device_config/customActions.js +119 -30
  71. package/src/tests/browser/manager/bottom-navigation-role-toolbar.spec.js +353 -0
  72. package/src/tests/browser/manager/device-detail-alias-save.spec.js +345 -0
  73. package/src/tests/browser/manager/device-detail-copy-config.spec.js +365 -0
  74. package/src/tests/browser/manager/runtime-footer-marquee.spec.js +262 -0
  75. package/src/tests/browser/manager/theme-confirm-toast.spec.js +236 -0
  76. package/src/tests/react/components/RuntimeFooterMarqueeText.test.js +171 -0
  77. package/src/tests/react/components/UserAvatar.test.js +6 -2
  78. package/src/tests/react/components/themeAwareModals.test.mjs +60 -0
  79. package/src/tests/react/config/deviceConfigBootstrap.test.js +104 -66
  80. package/src/tests/react/pages/deviceDetailDelete.test.js +78 -0
  81. package/src/tests/react/pages/receitaFederalIntegrationContract.test.mjs +48 -0
  82. package/src/tests/react/utils/bottomNavigationToolbar.test.js +222 -0
  83. package/src/tests/react/utils/copyDeviceConfigs.test.mjs +127 -0
  84. package/src/tests/react/utils/fileUrl.test.js +38 -0
  85. package/src/tests/react/utils/importStatus.test.js +27 -10
  86. package/src/tests/react/utils/passwordPolicy.test.js +27 -0
  87. package/src/tests/react/utils/printRouting.test.mjs +86 -0
  88. package/src/tests/react/utils/printSpoolUtils.test.mjs +52 -0
  89. package/src/tests/react/utils/runtimeFooter.test.js +10 -8
  90. package/src/tests/react/utils/shopFranchises.test.js +121 -115
  91. package/src/tests/react/utils/systemErrorMessage.test.js +32 -59
@@ -0,0 +1,353 @@
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 collection = member => ({
20
+ member,
21
+ 'hydra:member': member,
22
+ totalItems: member.length,
23
+ 'hydra:totalItems': member.length,
24
+ summary: {},
25
+ });
26
+
27
+ const companyA = {
28
+ id: 3,
29
+ name: 'Controle Online',
30
+ alias: 'CONTROLE',
31
+ panel_enabled: true,
32
+ enabled: true,
33
+ commercial_enabled: true,
34
+ theme: {colors: {primary: '#0EA5E9', success: '#10b981'}},
35
+ };
36
+
37
+ const companyB = {
38
+ id: 9,
39
+ name: 'Gyros',
40
+ alias: 'GYROS',
41
+ panel_enabled: true,
42
+ enabled: true,
43
+ commercial_enabled: true,
44
+ theme: {colors: {primary: '#16A34A', success: '#10b981'}},
45
+ };
46
+
47
+ const fullToolbarMenus = {
48
+ modules: {
49
+ 1: {
50
+ id: 1,
51
+ label: 'Toolbar',
52
+ icon: 'home',
53
+ sortOrder: 10,
54
+ menus: [
55
+ {
56
+ id: 11,
57
+ menuKey: 'home',
58
+ label: 'Início',
59
+ route: 'HomePage',
60
+ icon: 'home',
61
+ menuType: 'toolbar',
62
+ sortOrder: 10,
63
+ },
64
+ {
65
+ id: 12,
66
+ menuKey: 'opportunities',
67
+ label: 'Oportunidades',
68
+ route: 'CrmIndex',
69
+ icon: 'dollar-sign',
70
+ menuType: 'toolbar',
71
+ sortOrder: 20,
72
+ },
73
+ {
74
+ id: 13,
75
+ menuKey: 'customers',
76
+ label: 'Clientes',
77
+ route: 'ClientsIndex',
78
+ icon: 'users',
79
+ menuType: 'toolbar',
80
+ sortOrder: 30,
81
+ },
82
+ {
83
+ id: 14,
84
+ menuKey: 'profile',
85
+ label: 'Perfil',
86
+ route: 'ProfilePage',
87
+ icon: 'user',
88
+ menuType: 'toolbar',
89
+ sortOrder: 40,
90
+ },
91
+ ],
92
+ },
93
+ },
94
+ };
95
+
96
+ const limitedToolbarMenus = {
97
+ modules: {
98
+ 1: {
99
+ id: 1,
100
+ label: 'Toolbar',
101
+ icon: 'home',
102
+ sortOrder: 10,
103
+ menus: [
104
+ {
105
+ id: 21,
106
+ menuKey: 'home',
107
+ label: 'Início',
108
+ route: 'HomePage',
109
+ icon: 'home',
110
+ menuType: 'toolbar',
111
+ sortOrder: 10,
112
+ },
113
+ {
114
+ id: 24,
115
+ menuKey: 'profile',
116
+ label: 'Perfil',
117
+ route: 'ProfilePage',
118
+ icon: 'user',
119
+ menuType: 'toolbar',
120
+ sortOrder: 40,
121
+ },
122
+ ],
123
+ },
124
+ },
125
+ };
126
+
127
+ const installSession = async (page, {companyId = 3} = {}) => {
128
+ await page.addInitScript(
129
+ ({appVersion, companyId}) => {
130
+ const set = (k, v) => {
131
+ try {
132
+ localStorage.setItem(k, v);
133
+ } catch {}
134
+ };
135
+ set(
136
+ 'session',
137
+ JSON.stringify({
138
+ id: 7,
139
+ people: '/people/7',
140
+ api_key: 'test-api-key',
141
+ active: 1,
142
+ mycompany: companyId,
143
+ roles: ['ROLE_ADMIN'],
144
+ }),
145
+ );
146
+ set('config', JSON.stringify({language: 'pt-br'}));
147
+ set('app-type', 'MANAGER');
148
+ set(
149
+ 'device',
150
+ JSON.stringify({
151
+ id: 'web-manager',
152
+ device: 'web-manager',
153
+ type: 'WEB',
154
+ appName: 'Browser Manager',
155
+ appVersion,
156
+ buildNumber: appVersion,
157
+ systemName: 'web',
158
+ systemVersion: 'web',
159
+ deviceType: 'web',
160
+ metadata: {},
161
+ }),
162
+ );
163
+ },
164
+ {appVersion: APP_VERSION, companyId},
165
+ );
166
+ };
167
+
168
+ const mockManagerApi = async (page, {toolbarByCompany}) => {
169
+ await page.route(`${API_ORIGIN}/**`, async route => {
170
+ const request = route.request();
171
+ const url = new URL(request.url());
172
+ const pathname = url.pathname.replace(/^\/+/, '');
173
+ const method = request.method().toUpperCase();
174
+
175
+ if (method === 'OPTIONS') {
176
+ return route.fulfill({status: 204, headers: CORS_HEADERS, body: ''});
177
+ }
178
+
179
+ if (pathname === 'menus-people') {
180
+ const companyId = String(url.searchParams.get('myCompany') || '');
181
+ const menuType = String(url.searchParams.get('menuType') || '');
182
+ const payload =
183
+ menuType === 'toolbar'
184
+ ? toolbarByCompany[companyId] || {modules: {}}
185
+ : {modules: {}};
186
+ return route.fulfill({
187
+ status: 200,
188
+ headers: jsonHeaders(),
189
+ body: JSON.stringify({
190
+ response: {
191
+ data: payload,
192
+ count: 1,
193
+ error: '',
194
+ success: true,
195
+ },
196
+ }),
197
+ });
198
+ }
199
+
200
+ if (
201
+ pathname === 'people/companies/my' ||
202
+ pathname === 'people/company/default'
203
+ ) {
204
+ const list = [companyA, companyB];
205
+ return route.fulfill({
206
+ status: 200,
207
+ headers: jsonHeaders(),
208
+ body: JSON.stringify(
209
+ pathname === 'people/companies/my' ? collection(list) : companyA,
210
+ ),
211
+ });
212
+ }
213
+
214
+ if (pathname.startsWith('people/') || pathname === 'companies') {
215
+ return route.fulfill({
216
+ status: 200,
217
+ headers: jsonHeaders(),
218
+ body: JSON.stringify(companyA),
219
+ });
220
+ }
221
+
222
+ if (pathname === 'configs/discovery-configs') {
223
+ return route.fulfill({
224
+ status: 200,
225
+ headers: jsonHeaders(),
226
+ body: JSON.stringify({configs: {}}),
227
+ });
228
+ }
229
+
230
+ if (pathname === 'devices' || pathname.startsWith('device')) {
231
+ return route.fulfill({
232
+ status: 200,
233
+ headers: jsonHeaders(),
234
+ body: JSON.stringify(collection([])),
235
+ });
236
+ }
237
+
238
+ return route.fulfill({
239
+ status: 200,
240
+ headers: jsonHeaders(),
241
+ body: JSON.stringify(collection([])),
242
+ });
243
+ });
244
+ };
245
+
246
+ test.describe('MANAGER bottom navigation role toolbar', () => {
247
+ test.use({viewport: {width: 390, height: 844}});
248
+
249
+ test('full role sees authorized toolbar routes including Clientes', async ({
250
+ page,
251
+ }) => {
252
+ await mockManagerApi(page, {
253
+ toolbarByCompany: {
254
+ '3': fullToolbarMenus,
255
+ '9': limitedToolbarMenus,
256
+ },
257
+ });
258
+ await installSession(page, {companyId: 3});
259
+ await page.goto('/');
260
+
261
+ const nav = page.getByTestId('bottom-navigation');
262
+ await expect(nav).toBeVisible({timeout: 20000});
263
+ await expect(nav.getByText('Início')).toBeVisible();
264
+ await expect(nav.getByText('Oportunidades')).toBeVisible();
265
+ await expect(nav.getByText('Clientes')).toBeVisible();
266
+ await expect(nav.getByText('Perfil')).toBeVisible();
267
+ });
268
+
269
+ test('limited role does not expose business routes; keeps Início and Perfil', async ({
270
+ page,
271
+ }) => {
272
+ await mockManagerApi(page, {
273
+ toolbarByCompany: {
274
+ '3': limitedToolbarMenus,
275
+ },
276
+ });
277
+ await installSession(page, {companyId: 3});
278
+ await page.goto('/');
279
+
280
+ const nav = page.getByTestId('bottom-navigation');
281
+ await expect(nav).toBeVisible({timeout: 20000});
282
+ await expect(nav.getByText('Início')).toBeVisible();
283
+ await expect(nav.getByText('Perfil')).toBeVisible();
284
+ await expect(nav.getByText('Clientes')).toHaveCount(0);
285
+ await expect(nav.getByText('Oportunidades')).toHaveCount(0);
286
+ });
287
+
288
+ test('API failure keeps safe fallback without business routes', async ({
289
+ page,
290
+ }) => {
291
+ await page.route(`${API_ORIGIN}/**`, async route => {
292
+ const request = route.request();
293
+ const url = new URL(request.url());
294
+ const pathname = url.pathname.replace(/^\/+/, '');
295
+ const method = request.method().toUpperCase();
296
+
297
+ if (method === 'OPTIONS') {
298
+ return route.fulfill({status: 204, headers: CORS_HEADERS, body: ''});
299
+ }
300
+
301
+ if (pathname === 'menus-people') {
302
+ return route.fulfill({
303
+ status: 500,
304
+ headers: jsonHeaders(),
305
+ body: JSON.stringify({
306
+ response: {data: [], count: 0, error: 'fail', success: false},
307
+ }),
308
+ });
309
+ }
310
+
311
+ if (
312
+ pathname === 'people/companies/my' ||
313
+ pathname === 'people/company/default' ||
314
+ pathname.startsWith('people/') ||
315
+ pathname === 'companies'
316
+ ) {
317
+ return route.fulfill({
318
+ status: 200,
319
+ headers: jsonHeaders(),
320
+ body: JSON.stringify(
321
+ pathname === 'people/companies/my'
322
+ ? collection([companyA])
323
+ : companyA,
324
+ ),
325
+ });
326
+ }
327
+
328
+ if (pathname === 'configs/discovery-configs') {
329
+ return route.fulfill({
330
+ status: 200,
331
+ headers: jsonHeaders(),
332
+ body: JSON.stringify({configs: {}}),
333
+ });
334
+ }
335
+
336
+ return route.fulfill({
337
+ status: 200,
338
+ headers: jsonHeaders(),
339
+ body: JSON.stringify(collection([])),
340
+ });
341
+ });
342
+
343
+ await installSession(page, {companyId: 3});
344
+ await page.goto('/');
345
+
346
+ const nav = page.getByTestId('bottom-navigation');
347
+ await expect(nav).toBeVisible({timeout: 20000});
348
+ await expect(nav.getByText('Início')).toBeVisible();
349
+ await expect(nav.getByText('Perfil')).toBeVisible();
350
+ await expect(nav.getByText('Clientes')).toHaveCount(0);
351
+ await expect(nav.getByText('Oportunidades')).toHaveCount(0);
352
+ });
353
+ });
@@ -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
+ });