@controleonline/ui-common 1.2.78 → 1.2.79

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@controleonline/ui-common",
3
- "version": "1.2.78",
3
+ "version": "1.2.79",
4
4
  "author": "ControleOnline",
5
5
  "description": "ControleOnline Common Quasar UI Component",
6
6
  "license": "MIT",
@@ -64,6 +64,7 @@ import {
64
64
  resolvePosOperationMode,
65
65
  resolvePosPrintMode,
66
66
  } from '@controleonline/ui-common/src/react/config/deviceConfigBootstrap';
67
+ import { buildDeviceAliasStoreUpdates } from '@controleonline/ui-common/src/react/utils/deviceAliasSync';
67
68
 
68
69
  import {
69
70
  filterDeviceConfigsByCompany,
@@ -496,11 +497,16 @@ const DeviceDetailPage = () => {
496
497
  const [savingAlias, setSavingAlias] = useState(false);
497
498
  const [removingDevice, setRemovingDevice] = useState(false);
498
499
  const aliasInputRef = useRef(null);
500
+ const skipAliasSyncFromStoreRef = useRef(false);
499
501
 
500
502
  useEffect(() => {
501
503
  if (editingAlias) {
502
504
  return;
503
505
  }
506
+ if (skipAliasSyncFromStoreRef.current) {
507
+ skipAliasSyncFromStoreRef.current = false;
508
+ return;
509
+ }
504
510
 
505
511
  setAlias(initialAlias || '');
506
512
  setAliasInput(initialAlias || '');
@@ -1060,6 +1066,19 @@ const DeviceDetailPage = () => {
1060
1066
  alias: trimmed,
1061
1067
  });
1062
1068
  const nextAlias = String(savedDevice?.alias || trimmed).trim();
1069
+ const { mergedDevice, nextDeviceConfig } = buildDeviceAliasStoreUpdates({
1070
+ deviceId,
1071
+ nextAlias,
1072
+ runtimeDevice,
1073
+ runtimeDeviceConfig,
1074
+ savedDevice,
1075
+ normalizeEntityId,
1076
+ });
1077
+ actionsRef.current.deviceActions.setItem?.(mergedDevice);
1078
+ if (nextDeviceConfig && actionsRef.current.deviceConfigActions?.setItem) {
1079
+ actionsRef.current.deviceConfigActions.setItem(nextDeviceConfig);
1080
+ }
1081
+ skipAliasSyncFromStoreRef.current = true;
1063
1082
  setAlias(nextAlias);
1064
1083
  setAliasInput(nextAlias);
1065
1084
  setEditingAlias(false);
@@ -1075,28 +1094,25 @@ const DeviceDetailPage = () => {
1075
1094
  if (!deviceId || removingDevice) {
1076
1095
  return;
1077
1096
  }
1078
- Alert.alert(
1079
- 'Excluir device',
1080
- `Tem certeza que deseja excluir o device "${alias || deviceString || deviceId}"? Esta ação não pode ser desfeita.`,
1081
- [
1082
- { text: 'Cancelar', style: 'cancel' },
1083
- {
1084
- text: 'Excluir',
1085
- style: 'destructive',
1086
- onPress: async () => {
1087
- setRemovingDevice(true);
1088
- try {
1089
- await actionsRef.current.deviceActions.remove(deviceId);
1090
- navigation.navigate('DevicesIndex');
1091
- } catch (error) {
1092
- showSystemError(error, 'Não foi possível excluir o device.');
1093
- } finally {
1094
- setRemovingDevice(false);
1095
- }
1096
- },
1097
- },
1098
- ],
1099
- );
1097
+ const label = String(alias || deviceString || deviceId).trim();
1098
+ const message = `Tem certeza que deseja excluir o device "${label}"? Esta ação não pode ser desfeita.`;
1099
+ // Use web-safe confirm() (window.confirm on web; Alert.alert on native).
1100
+ // Alert.alert alone is a no-op on Manager web — root cause of "click does nothing".
1101
+ confirm(message, async () => {
1102
+ setRemovingDevice(true);
1103
+ try {
1104
+ await actionsRef.current.deviceActions.remove(deviceId);
1105
+ if (navigation?.canGoBack?.()) {
1106
+ navigation.goBack();
1107
+ } else {
1108
+ navigation.navigate('DevicesIndex');
1109
+ }
1110
+ } catch (error) {
1111
+ showSystemError(error, 'Não foi possível excluir o device.');
1112
+ } finally {
1113
+ setRemovingDevice(false);
1114
+ }
1115
+ });
1100
1116
  }, [deviceId, removingDevice, alias, deviceString, navigation, showSystemError]);
1101
1117
 
1102
1118
  const saveDevicePaymentTarget = useCallback(async (override = {}) => {
@@ -1934,6 +1950,7 @@ const DeviceDetailPage = () => {
1934
1950
  activeOpacity={0.8}
1935
1951
  disabled={removingDevice || savingAlias}
1936
1952
  accessibilityLabel="Excluir device"
1953
+ testID="device-detail-delete-btn"
1937
1954
  >
1938
1955
  <Icon
1939
1956
  name={removingDevice ? 'loader' : 'trash-2'}
@@ -26,8 +26,44 @@ import {
26
26
  } from './deviceListHelpers';
27
27
  import styles from '../../Devices.styles';
28
28
 
29
+ /**
30
+ * Normalize DefaultTable card `item` into a device group shape.
31
+ * DefaultTable can hand us either a grouped object ({device, deviceConfigs})
32
+ * or a flat device_config entity from the store (no deviceConfigs array).
33
+ * Accessing `.deviceConfigs[0]` on a flat item throws TypeError (app-community#627 KDS).
34
+ */
35
+ const normalizeDeviceGroup = item => {
36
+ if (!item || typeof item !== 'object') {
37
+ return null;
38
+ }
39
+
40
+ if (Array.isArray(item.deviceConfigs)) {
41
+ return item;
42
+ }
43
+
44
+ // Flat device_config row from store — promote to single-config group.
45
+ if (item.device != null || item.type != null || item.id != null) {
46
+ return {
47
+ key:
48
+ item.key ||
49
+ item['@id'] ||
50
+ (item.id != null ? `config:${item.id}` : undefined),
51
+ device: item.device && typeof item.device === 'object' ? item.device : {},
52
+ deviceConfigs: [item],
53
+ primaryConfig: item,
54
+ alias:
55
+ item.alias ||
56
+ item.device?.alias ||
57
+ item.device?.device ||
58
+ undefined,
59
+ };
60
+ }
61
+
62
+ return null;
63
+ };
64
+
29
65
  export default function DeviceGroupCard({
30
- item: deviceGroup,
66
+ item,
31
67
  brandColors,
32
68
  creatingPdv,
33
69
  goToDetail,
@@ -38,6 +74,13 @@ export default function DeviceGroupCard({
38
74
  runtimeDeviceType,
39
75
  showCurrentPdvSetup,
40
76
  }) {
77
+ const deviceGroup = normalizeDeviceGroup(item);
78
+ if (!deviceGroup) {
79
+ return null;
80
+ }
81
+ const deviceConfigs = Array.isArray(deviceGroup.deviceConfigs)
82
+ ? deviceGroup.deviceConfigs
83
+ : [];
41
84
  const isCurrentDevice = isCurrentDeviceGroup({
42
85
  deviceGroup,
43
86
  runtimeDeviceIdentifier,
@@ -52,7 +95,8 @@ export default function DeviceGroupCard({
52
95
  const primaryConfig =
53
96
  sessionConfig ||
54
97
  filteredTypeConfig ||
55
- deviceGroup.deviceConfigs[0] ||
98
+ deviceGroup.primaryConfig ||
99
+ deviceConfigs[0] ||
56
100
  null;
57
101
  const normalizedType = primaryConfig
58
102
  ? getDeviceConfigType(primaryConfig)
@@ -150,7 +194,7 @@ export default function DeviceGroupCard({
150
194
  </Text>
151
195
 
152
196
  <View style={styles.deviceConfigRow}>
153
- {deviceGroup.deviceConfigs.map(deviceConfig => {
197
+ {deviceConfigs.map(deviceConfig => {
154
198
  const configType = getDeviceConfigType(deviceConfig);
155
199
  const configAccent = getDeviceTypeAccent(configType);
156
200
  const isSessionConfig =
@@ -138,6 +138,12 @@ export const getDeviceListIdentifier = deviceConfig =>
138
138
  deviceConfig,
139
139
  }).runtimeDetail || String(deviceConfig?.device?.device || '').trim();
140
140
 
141
+ /**
142
+ * Build list params for device_configs collection.
143
+ * Always send a single scalar `type` when filtering (API SearchFilter exact).
144
+ * Callers with multiple queryTypes must request each type separately and merge.
145
+ * Empty queryTypes = no type filter (All).
146
+ */
141
147
  export const buildDeviceListParams = ({
142
148
  companyId,
143
149
  page,
@@ -151,17 +157,43 @@ export const buildDeviceListParams = ({
151
157
  'order[id]': 'DESC',
152
158
  };
153
159
 
154
- if (Array.isArray(queryTypes) && queryTypes.length === 1) {
155
- params.type = queryTypes[0];
156
- }
160
+ const types = Array.isArray(queryTypes)
161
+ ? queryTypes.map(t => String(t || '').trim()).filter(Boolean)
162
+ : [];
157
163
 
158
- if (Array.isArray(queryTypes) && queryTypes.length > 1) {
159
- params.type = queryTypes;
164
+ if (types.length === 1) {
165
+ params.type = types[0];
160
166
  }
161
167
 
168
+ // Multi-type: do NOT set params.type as array — API exact filter is unreliable
169
+ // with type[]= on some platforms; callers must fetch each type and merge.
170
+
162
171
  return params;
163
172
  };
164
173
 
174
+ /**
175
+ * Expand queryTypes into one or more single-type param sets for sequential fetch.
176
+ * Empty → one set without type (All). Single → one set. Multiple → one set per type.
177
+ */
178
+ export const expandDeviceListParamSets = ({
179
+ companyId,
180
+ page,
181
+ pageSize = PAGE_SIZE,
182
+ queryTypes = [],
183
+ }) => {
184
+ const types = Array.isArray(queryTypes)
185
+ ? queryTypes.map(t => String(t || '').trim()).filter(Boolean)
186
+ : [];
187
+
188
+ if (types.length === 0) {
189
+ return [buildDeviceListParams({companyId, page, pageSize, queryTypes: []})];
190
+ }
191
+
192
+ return types.map(type =>
193
+ buildDeviceListParams({companyId, page, pageSize, queryTypes: [type]}),
194
+ );
195
+ };
196
+
165
197
  export {
166
198
  parseConfigsObject,
167
199
  getDeviceConfigType,
@@ -55,7 +55,7 @@ import {
55
55
  getPosOperationModeLabel,
56
56
  getDeviceDetailRoute,
57
57
  getDeviceListIdentifier,
58
- buildDeviceListParams,
58
+ expandDeviceListParamSets,
59
59
  isPdvPrinterEnabled,
60
60
  } from './deviceListHelpers';
61
61
 
@@ -205,40 +205,45 @@ export const createDeviceTypeTab = ({
205
205
 
206
206
  try {
207
207
  const requestPageSize = Math.max(pageSize, API_PAGE_SIZE);
208
- let page = 1;
209
208
  let loadedItems = [];
210
209
  let reportedTotal = 0;
211
210
 
212
- while (true) {
213
- const pageItems = await deviceConfigStore.actions.getItems(
214
- buildDeviceListParams({
215
- companyId,
211
+ const paramSets = expandDeviceListParamSets({
212
+ companyId,
213
+ page: 1,
214
+ pageSize: requestPageSize,
215
+ queryTypes,
216
+ });
217
+
218
+ for (const baseParams of paramSets) {
219
+ let page = 1;
220
+ while (true) {
221
+ const pageItems = await deviceConfigStore.actions.getItems({
222
+ ...baseParams,
216
223
  page,
217
- pageSize: requestPageSize,
218
- queryTypes,
219
- }),
220
- );
221
- const previousLength = loadedItems.length;
222
- loadedItems = mergeDeviceConfigs(loadedItems, pageItems);
223
- reportedTotal = Math.max(
224
- reportedTotal,
225
- Number(
226
- deviceConfigStore.getters.totalItems ||
227
- loadedItems.length ||
228
- 0,
229
- ),
230
- );
231
-
232
- if (
233
- !Array.isArray(pageItems) ||
234
- pageItems.length === 0 ||
235
- loadedItems.length >= reportedTotal ||
236
- loadedItems.length === previousLength
237
- ) {
238
- break;
224
+ });
225
+ const previousLength = loadedItems.length;
226
+ loadedItems = mergeDeviceConfigs(loadedItems, pageItems);
227
+ reportedTotal = Math.max(
228
+ reportedTotal,
229
+ Number(
230
+ deviceConfigStore.getters.totalItems ||
231
+ loadedItems.length ||
232
+ 0,
233
+ ),
234
+ );
235
+
236
+ if (
237
+ !Array.isArray(pageItems) ||
238
+ pageItems.length === 0 ||
239
+ loadedItems.length >= reportedTotal ||
240
+ loadedItems.length === previousLength
241
+ ) {
242
+ break;
243
+ }
244
+
245
+ page += 1;
239
246
  }
240
-
241
- page += 1;
242
247
  }
243
248
 
244
249
  setDeviceConfigs(loadedItems);
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Build device + device_config store payloads after a successful alias save.
3
+ * Keeps derived initialAlias in sync so UI does not snap back to the old name.
4
+ */
5
+ export function buildDeviceAliasStoreUpdates({
6
+ deviceId,
7
+ nextAlias,
8
+ runtimeDevice,
9
+ runtimeDeviceConfig,
10
+ savedDevice,
11
+ normalizeEntityId,
12
+ }) {
13
+ const id = normalizeEntityId(deviceId);
14
+ const alias = String(nextAlias || '').trim();
15
+
16
+ const baseDevice =
17
+ runtimeDevice &&
18
+ normalizeEntityId(runtimeDevice?.id || runtimeDevice?.['@id']) === id
19
+ ? runtimeDevice
20
+ : {};
21
+
22
+ const mergedDevice = {
23
+ ...baseDevice,
24
+ ...(savedDevice && typeof savedDevice === 'object' ? savedDevice : {}),
25
+ id,
26
+ alias,
27
+ };
28
+
29
+ let nextDeviceConfig = null;
30
+ if (runtimeDeviceConfig && typeof runtimeDeviceConfig === 'object') {
31
+ const nestedDevice =
32
+ runtimeDeviceConfig.device && typeof runtimeDeviceConfig.device === 'object'
33
+ ? runtimeDeviceConfig.device
34
+ : {};
35
+ nextDeviceConfig = {
36
+ ...runtimeDeviceConfig,
37
+ device: {
38
+ ...nestedDevice,
39
+ id: nestedDevice.id || id,
40
+ alias,
41
+ },
42
+ };
43
+ }
44
+
45
+ return { mergedDevice, nextDeviceConfig };
46
+ }
@@ -0,0 +1,322 @@
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
+ const CURRENT_DEVICE_ID = 'web-7';
7
+
8
+ const CORS_HEADERS = {
9
+ 'access-control-allow-origin': '*',
10
+ 'access-control-allow-headers':
11
+ 'API-TOKEN, APP-DOMAIN, DEVICE, ACCEPT, CONTENT-TYPE, X-Requested-With',
12
+ 'access-control-allow-methods': 'GET,POST,PUT,PATCH,DELETE,OPTIONS',
13
+ };
14
+
15
+ const jsonHeaders = () => ({
16
+ ...CORS_HEADERS,
17
+ 'content-type': 'application/ld+json; charset=utf-8',
18
+ });
19
+
20
+ const collection = member => ({
21
+ member,
22
+ 'hydra:member': member,
23
+ totalItems: member.length,
24
+ 'hydra:totalItems': member.length,
25
+ summary: {},
26
+ });
27
+
28
+ const currentDevice = {
29
+ '@id': '/devices/396',
30
+ '@type': 'Device',
31
+ id: 396,
32
+ device: CURRENT_DEVICE_ID,
33
+ alias: 'Caixa atual',
34
+ metadata: {
35
+ runtime: 'web',
36
+ network: {publicIp: '127.0.0.1'},
37
+ },
38
+ };
39
+
40
+ const printerDevice = {
41
+ '@id': '/devices/501',
42
+ '@type': 'Device',
43
+ id: 501,
44
+ device: 'printer-1',
45
+ alias: 'Impressora cozinha',
46
+ metadata: {runtime: 'network'},
47
+ };
48
+
49
+ const displayDevice = {
50
+ '@id': '/devices/502',
51
+ '@type': 'Device',
52
+ id: 502,
53
+ device: 'kds-1',
54
+ alias: 'KDS salão',
55
+ metadata: {runtime: 'network'},
56
+ };
57
+
58
+ const createDeviceConfig = ({id, type, device = currentDevice, alias}) => ({
59
+ '@id': `/device_configs/${id}`,
60
+ '@type': 'DeviceConfig',
61
+ id,
62
+ type,
63
+ people: '/people/3',
64
+ device: alias
65
+ ? {...device, alias}
66
+ : device,
67
+ configs: JSON.stringify({
68
+ 'config-version': APP_VERSION,
69
+ 'pos-gateway': 'infinite-pay',
70
+ }),
71
+ });
72
+
73
+ /**
74
+ * Smoke for app-community#381:
75
+ * type filters beyond All/PDV must not throw and must request scalar `type`
76
+ * (or omit type for All). Multi-type filters expand to sequential single-type GETs.
77
+ */
78
+ const mockDevicesTypeFilterApi = async page => {
79
+ const company = {
80
+ id: 3,
81
+ name: 'Teste',
82
+ alias: 'TESTE',
83
+ panel_enabled: true,
84
+ enabled: true,
85
+ commercial_enabled: true,
86
+ theme: {
87
+ colors: {
88
+ primary: '#0EA5E9',
89
+ cardBackground: '#FFFFFF',
90
+ cardBorder: '#D8E0EA',
91
+ cardSelectedBackground: '#E8F8FD',
92
+ cardSelectedBorder: '#0284C7',
93
+ cardSelectedText: '#0F172A',
94
+ badgeSelectedBackground: '#CDEFFA',
95
+ badgeSelectedText: '#075985',
96
+ },
97
+ },
98
+ };
99
+
100
+ const allConfigs = [
101
+ createDeviceConfig({id: 487, type: 'MANAGER'}),
102
+ createDeviceConfig({id: 488, type: 'PDV'}),
103
+ createDeviceConfig({
104
+ id: 501,
105
+ type: 'PRINTER',
106
+ device: printerDevice,
107
+ }),
108
+ createDeviceConfig({
109
+ id: 502,
110
+ type: 'DISPLAY',
111
+ device: displayDevice,
112
+ }),
113
+ createDeviceConfig({id: 503, type: 'DEVICE'}),
114
+ ];
115
+
116
+ const typeRequests = [];
117
+
118
+ await page.route(`${API_ORIGIN}/**`, async route => {
119
+ const request = route.request();
120
+ const url = new URL(request.url());
121
+ const pathname = url.pathname.replace(/^\/+/, '');
122
+ const method = request.method().toUpperCase();
123
+
124
+ if (method === 'OPTIONS') {
125
+ return route.fulfill({status: 204, headers: CORS_HEADERS, body: ''});
126
+ }
127
+
128
+ if (pathname === 'themes-colors.css') {
129
+ return route.fulfill({
130
+ status: 200,
131
+ headers: {...CORS_HEADERS, 'content-type': 'text/css; charset=utf-8'},
132
+ body: ':root { --primary: #0ea5e9; }',
133
+ });
134
+ }
135
+
136
+ if (pathname === 'runtime/ip') {
137
+ return route.fulfill({
138
+ status: 200,
139
+ headers: jsonHeaders(),
140
+ body: JSON.stringify({ip: '127.0.0.1'}),
141
+ });
142
+ }
143
+
144
+ if (pathname === 'menus-people') {
145
+ return route.fulfill({
146
+ status: 200,
147
+ headers: jsonHeaders(),
148
+ body: JSON.stringify({modules: {}}),
149
+ });
150
+ }
151
+
152
+ if (pathname === 'people/companies/my') {
153
+ return route.fulfill({
154
+ status: 200,
155
+ headers: jsonHeaders(),
156
+ body: JSON.stringify(collection([company])),
157
+ });
158
+ }
159
+
160
+ if (pathname === 'people/company/default') {
161
+ return route.fulfill({
162
+ status: 200,
163
+ headers: jsonHeaders(),
164
+ body: JSON.stringify(company),
165
+ });
166
+ }
167
+
168
+ if (pathname === 'configs/discovery-configs') {
169
+ return route.fulfill({
170
+ status: 200,
171
+ headers: jsonHeaders(),
172
+ body: JSON.stringify({configs: {}}),
173
+ });
174
+ }
175
+
176
+ if (pathname === 'devices' && method === 'GET') {
177
+ return route.fulfill({
178
+ status: 200,
179
+ headers: jsonHeaders(),
180
+ body: JSON.stringify(
181
+ collection([currentDevice, printerDevice, displayDevice]),
182
+ ),
183
+ });
184
+ }
185
+
186
+ if (pathname === 'devices' && method === 'POST') {
187
+ return route.fulfill({
188
+ status: 200,
189
+ headers: jsonHeaders(),
190
+ body: JSON.stringify(currentDevice),
191
+ });
192
+ }
193
+
194
+ if (pathname === 'device_configs' && method === 'GET') {
195
+ const requestedType = url.searchParams.get('type');
196
+ // API Platform array notation would be type[] — must NOT appear for this fix
197
+ const hasArrayType =
198
+ url.searchParams.has('type[]') ||
199
+ [...url.searchParams.keys()].some(k => k === 'type[]' || k.startsWith('type['));
200
+
201
+ typeRequests.push({
202
+ type: requestedType,
203
+ hasArrayType,
204
+ search: url.search,
205
+ });
206
+
207
+ if (hasArrayType) {
208
+ // Simulate the previous failure mode (bad request / empty / error)
209
+ return route.fulfill({
210
+ status: 400,
211
+ headers: jsonHeaders(),
212
+ body: JSON.stringify({
213
+ 'hydra:description': 'Invalid type filter array notation',
214
+ }),
215
+ });
216
+ }
217
+
218
+ const filtered = requestedType
219
+ ? allConfigs.filter(c => String(c.type).toUpperCase() === String(requestedType).toUpperCase())
220
+ : allConfigs;
221
+
222
+ return route.fulfill({
223
+ status: 200,
224
+ headers: jsonHeaders(),
225
+ body: JSON.stringify(collection(filtered)),
226
+ });
227
+ }
228
+
229
+ return route.fulfill({
230
+ status: 200,
231
+ headers: jsonHeaders(),
232
+ body: JSON.stringify(collection([])),
233
+ });
234
+ });
235
+
236
+ return {typeRequests};
237
+ };
238
+
239
+ test.describe('Manager devices-index type filters (device_config) #381', () => {
240
+ test('All, PDV and non-PDV types load without error (scalar type only)', async ({
241
+ page,
242
+ }) => {
243
+ const api = await mockDevicesTypeFilterApi(page);
244
+ const consoleErrors = [];
245
+ page.on('pageerror', err => consoleErrors.push(String(err)));
246
+ page.on('console', msg => {
247
+ if (msg.type() === 'error') {
248
+ consoleErrors.push(msg.text());
249
+ }
250
+ });
251
+
252
+ await page.goto('/devices-index?store=device_config');
253
+
254
+ // Initial All (or default) should render without crash
255
+ await expect(page.locator('body')).toBeVisible();
256
+ await expect.poll(() => api.typeRequests.length).toBeGreaterThan(0);
257
+
258
+ // Click filter chips by visible labels used in UI
259
+ const filterLabels = ['Todos', 'PDVs', 'KDS', 'Impressoras', 'Devices'];
260
+ for (const label of filterLabels) {
261
+ const chip = page.getByText(label, {exact: true}).first();
262
+ if (await chip.count()) {
263
+ await chip.click();
264
+ // Give list a moment to re-fetch
265
+ await page.waitForTimeout(400);
266
+ }
267
+ }
268
+
269
+ // No array-type requests (the bug)
270
+ const arrayRequests = api.typeRequests.filter(r => r.hasArrayType);
271
+ expect(arrayRequests).toEqual([]);
272
+
273
+ // At least one request without type (All) and one with scalar PDV / PRINTER / DISPLAY
274
+ const typesSeen = new Set(
275
+ api.typeRequests.map(r => r.type).filter(Boolean),
276
+ );
277
+ // PDV or PRINTER or DISPLAY should have been requested as scalar
278
+ const hasScalarNonAll = [...typesSeen].some(t =>
279
+ ['PDV', 'PRINTER', 'PRINT', 'DISPLAY', 'DEVICE', 'IP_CAMERA'].includes(
280
+ String(t).toUpperCase(),
281
+ ),
282
+ );
283
+ expect(hasScalarNonAll || api.typeRequests.some(r => !r.type)).toBe(true);
284
+
285
+ // Page still healthy
286
+ await expect(page.locator('body')).toBeVisible();
287
+ const critical = consoleErrors.filter(
288
+ e =>
289
+ !/favicon|ResizeObserver|Download the React DevTools/i.test(e) &&
290
+ /TypeError|ReferenceError|Cannot read|hydra:description|Invalid type/i.test(
291
+ e,
292
+ ),
293
+ );
294
+ expect(critical).toEqual([]);
295
+ });
296
+
297
+ test('switching All ↔ Printer ↔ All stays stable', async ({page}) => {
298
+ const api = await mockDevicesTypeFilterApi(page);
299
+
300
+ await page.goto('/devices-index?store=device_config');
301
+ await expect.poll(() => api.typeRequests.length).toBeGreaterThan(0);
302
+
303
+ const todos = page.getByText('Todos', {exact: true}).first();
304
+ const impressoras = page.getByText('Impressoras', {exact: true}).first();
305
+
306
+ if (await impressoras.count()) {
307
+ await impressoras.click();
308
+ await page.waitForTimeout(350);
309
+ }
310
+ if (await todos.count()) {
311
+ await todos.click();
312
+ await page.waitForTimeout(350);
313
+ }
314
+ if (await impressoras.count()) {
315
+ await impressoras.click();
316
+ await page.waitForTimeout(350);
317
+ }
318
+
319
+ expect(api.typeRequests.every(r => !r.hasArrayType)).toBe(true);
320
+ await expect(page.locator('body')).toBeVisible();
321
+ });
322
+ });
@@ -3,6 +3,7 @@
3
3
  const {
4
4
  mergeDeviceConfigs,
5
5
  buildDeviceListParams,
6
+ expandDeviceListParamSets,
6
7
  PAGE_SIZE,
7
8
  } = require('../../../../react/pages/Devices/deviceTypes/deviceListHelpers');
8
9
 
@@ -27,13 +28,57 @@ describe('deviceListHelpers', () => {
27
28
  expect(params.itemsPerPage).toBe(PAGE_SIZE);
28
29
  });
29
30
 
30
- it('buildDeviceListParams accepts multiple types', () => {
31
+ it('buildDeviceListParams omits type for empty queryTypes (All)', () => {
32
+ const params = buildDeviceListParams({
33
+ companyId: 1,
34
+ page: 1,
35
+ queryTypes: [],
36
+ });
37
+ expect(params.type).toBeUndefined();
38
+ expect(params.people).toBe('/people/1');
39
+ });
40
+
41
+ it('buildDeviceListParams does not set array type for multi (caller expands)', () => {
31
42
  const params = buildDeviceListParams({
32
43
  companyId: 1,
33
44
  page: 2,
34
- queryTypes: ['PDV', 'PRINTER'],
45
+ queryTypes: ['PRINT', 'PRINTER'],
35
46
  });
36
- expect(params.type).toEqual(['PDV', 'PRINTER']);
47
+ expect(params.type).toBeUndefined();
37
48
  expect(params.page).toBe(2);
38
49
  });
50
+
51
+ it('expandDeviceListParamSets returns one set without type for All', () => {
52
+ const sets = expandDeviceListParamSets({
53
+ companyId: 3,
54
+ page: 1,
55
+ pageSize: 50,
56
+ queryTypes: [],
57
+ });
58
+ expect(sets).toHaveLength(1);
59
+ expect(sets[0].type).toBeUndefined();
60
+ expect(sets[0].people).toBe('/people/3');
61
+ });
62
+
63
+ it('expandDeviceListParamSets returns one set per type for multi', () => {
64
+ const sets = expandDeviceListParamSets({
65
+ companyId: 5,
66
+ page: 1,
67
+ queryTypes: ['PRINT', 'PRINTER'],
68
+ });
69
+ expect(sets).toHaveLength(2);
70
+ expect(sets[0].type).toBe('PRINT');
71
+ expect(sets[1].type).toBe('PRINTER');
72
+ expect(sets[0].people).toBe('/people/5');
73
+ });
74
+
75
+ it('expandDeviceListParamSets single type stays one set', () => {
76
+ const sets = expandDeviceListParamSets({
77
+ companyId: 2,
78
+ page: 1,
79
+ queryTypes: ['DISPLAY'],
80
+ });
81
+ expect(sets).toHaveLength(1);
82
+ expect(sets[0].type).toBe('DISPLAY');
83
+ });
39
84
  });
@@ -0,0 +1,48 @@
1
+ const test = require('node:test');
2
+ const assert = require('node:assert/strict');
3
+ const {
4
+ buildDeviceAliasStoreUpdates,
5
+ } = require('../../../react/utils/deviceAliasSync.js');
6
+
7
+ const normalizeEntityId = value => {
8
+ if (value == null || value === '') return '';
9
+ const raw = String(value);
10
+ const match = raw.match(/(\d+)\s*$/);
11
+ return match ? match[1] : raw.replace(/\D/g, '') || raw;
12
+ };
13
+
14
+ test('buildDeviceAliasStoreUpdates merges alias into device and nested config', () => {
15
+ const { mergedDevice, nextDeviceConfig } = buildDeviceAliasStoreUpdates({
16
+ deviceId: 42,
17
+ nextAlias: 'Caixa 01',
18
+ runtimeDevice: { id: 42, alias: 'Antigo', device: 'uuid-1' },
19
+ runtimeDeviceConfig: {
20
+ id: 7,
21
+ device: { id: 42, alias: 'Antigo', device: 'uuid-1' },
22
+ configs: {},
23
+ },
24
+ savedDevice: { id: 42, alias: 'Caixa 01' },
25
+ normalizeEntityId,
26
+ });
27
+
28
+ assert.equal(mergedDevice.alias, 'Caixa 01');
29
+ assert.equal(mergedDevice.id, '42');
30
+ assert.equal(mergedDevice.device, 'uuid-1');
31
+ assert.equal(nextDeviceConfig.device.alias, 'Caixa 01');
32
+ assert.equal(nextDeviceConfig.id, 7);
33
+ });
34
+
35
+ test('buildDeviceAliasStoreUpdates works without runtimeDeviceConfig', () => {
36
+ const { mergedDevice, nextDeviceConfig } = buildDeviceAliasStoreUpdates({
37
+ deviceId: '/devices/9',
38
+ nextAlias: 'Novo',
39
+ runtimeDevice: null,
40
+ runtimeDeviceConfig: null,
41
+ savedDevice: { '@id': '/devices/9', alias: 'Novo' },
42
+ normalizeEntityId,
43
+ });
44
+
45
+ assert.equal(mergedDevice.alias, 'Novo');
46
+ assert.equal(mergedDevice.id, '9');
47
+ assert.equal(nextDeviceConfig, null);
48
+ });