@controleonline/ui-common 1.2.78 → 1.2.80
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 +1 -1
- package/src/react/pages/DeviceDetailPage.js +39 -22
- package/src/react/pages/Devices/deviceTypes/DeviceGroupCard.js +47 -3
- package/src/react/pages/Devices/deviceTypes/deviceListHelpers.js +37 -5
- package/src/react/pages/Devices/deviceTypes/shared.js +35 -30
- package/src/react/utils/deviceAliasSync.js +46 -0
- package/src/react/utils/passwordPolicy.js +73 -0
- package/src/react/utils/runtimeFooter.js +18 -4
- package/src/tests/browser/manager/devices-type-filters.spec.js +322 -0
- package/src/tests/react/pages/Devices/deviceListHelpers.test.js +48 -3
- package/src/tests/react/utils/deviceAliasSync.test.js +48 -0
- package/src/tests/react/utils/importStatus.test.js +1 -1
- package/src/tests/react/utils/runtimeFooter.test.js +3 -8
package/package.json
CHANGED
|
@@ -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
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
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
|
|
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.
|
|
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
|
-
{
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
160
|
+
const types = Array.isArray(queryTypes)
|
|
161
|
+
? queryTypes.map(t => String(t || '').trim()).filter(Boolean)
|
|
162
|
+
: [];
|
|
157
163
|
|
|
158
|
-
if (
|
|
159
|
-
params.type =
|
|
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
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical password policy shared by create / change / reset flows.
|
|
3
|
+
* Must stay aligned with api-platform-users PasswordPolicyService.
|
|
4
|
+
*/
|
|
5
|
+
export const PASSWORD_MIN_LENGTH = 6;
|
|
6
|
+
|
|
7
|
+
export const PASSWORD_MSG_REQUIRED = 'A senha é obrigatória.';
|
|
8
|
+
export const PASSWORD_MSG_MIN_LENGTH = `A senha precisa ter pelo menos ${PASSWORD_MIN_LENGTH} caracteres.`;
|
|
9
|
+
export const PASSWORD_MSG_COMPROMISED =
|
|
10
|
+
'Esta senha não pode ser usada porque consta em lista de senhas comprometidas (vazamento de dados). Escolha outra senha.';
|
|
11
|
+
export const PASSWORD_MSG_CONFIRM_MISMATCH =
|
|
12
|
+
'A senha e a confirmação devem ser iguais.';
|
|
13
|
+
|
|
14
|
+
export const PASSWORD_HELP_LINES = [
|
|
15
|
+
`Mínimo de ${PASSWORD_MIN_LENGTH} caracteres.`,
|
|
16
|
+
'Não use senhas que já tenham vazado em vazamentos públicos de dados.',
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Client-side validation for length/required/confirm.
|
|
21
|
+
* Does not call breach API — that remains server-side.
|
|
22
|
+
* @returns {string|null} error message or null when valid
|
|
23
|
+
*/
|
|
24
|
+
export function validatePasswordClient(password, confirm) {
|
|
25
|
+
const normalized = String(password ?? '').trim();
|
|
26
|
+
if (!normalized) {
|
|
27
|
+
return PASSWORD_MSG_REQUIRED;
|
|
28
|
+
}
|
|
29
|
+
if (normalized.length < PASSWORD_MIN_LENGTH) {
|
|
30
|
+
return PASSWORD_MSG_MIN_LENGTH;
|
|
31
|
+
}
|
|
32
|
+
if (confirm !== undefined && confirm !== null) {
|
|
33
|
+
const confirmNormalized = String(confirm ?? '').trim();
|
|
34
|
+
if (normalized !== confirmNormalized) {
|
|
35
|
+
return PASSWORD_MSG_CONFIRM_MISMATCH;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Map API / raw error text to product Portuguese copy for known policy violations.
|
|
43
|
+
*/
|
|
44
|
+
export function mapPasswordErrorMessage(raw) {
|
|
45
|
+
const text = String(raw ?? '').trim();
|
|
46
|
+
if (!text) {
|
|
47
|
+
return text;
|
|
48
|
+
}
|
|
49
|
+
if (/leaked|data breach|compromised password|have i been pwned|comprometid/i.test(text)) {
|
|
50
|
+
return PASSWORD_MSG_COMPROMISED;
|
|
51
|
+
}
|
|
52
|
+
const minMatch =
|
|
53
|
+
text.match(/at least\s+(\d+)\s+characters/i) ||
|
|
54
|
+
text.match(/pelo menos\s+(\d+)\s+caracteres/i);
|
|
55
|
+
if (minMatch) {
|
|
56
|
+
return `A senha precisa ter pelo menos ${minMatch[1]} caracteres.`;
|
|
57
|
+
}
|
|
58
|
+
if (/must be identical|não coincidem|must match/i.test(text)) {
|
|
59
|
+
return PASSWORD_MSG_CONFIRM_MISMATCH;
|
|
60
|
+
}
|
|
61
|
+
return text;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export default {
|
|
65
|
+
PASSWORD_MIN_LENGTH,
|
|
66
|
+
PASSWORD_MSG_REQUIRED,
|
|
67
|
+
PASSWORD_MSG_MIN_LENGTH,
|
|
68
|
+
PASSWORD_MSG_COMPROMISED,
|
|
69
|
+
PASSWORD_MSG_CONFIRM_MISMATCH,
|
|
70
|
+
PASSWORD_HELP_LINES,
|
|
71
|
+
validatePasswordClient,
|
|
72
|
+
mapPasswordErrorMessage,
|
|
73
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { getDeviceTypeLabel } from '@controleonline/ui-common/src/react/utils/printerDevices';
|
|
2
|
-
|
|
3
1
|
const DEVICE_RUNTIME_FOOTER_TEXT_CONFIG_KEY = 'device-runtime-footer-text';
|
|
2
|
+
const PRINTER_DEVICE_TYPES = new Set(['PRINT', 'PRINTER']);
|
|
3
|
+
const IP_CAMERA_DEVICE_TYPE = 'IP_CAMERA';
|
|
4
4
|
|
|
5
5
|
const safeTrim = value => String(value || '').trim();
|
|
6
6
|
|
|
@@ -25,6 +25,20 @@ const normalizeComparableText = value =>
|
|
|
25
25
|
.normalize('NFD')
|
|
26
26
|
.replace(/[\u0300-\u036f]/g, '');
|
|
27
27
|
|
|
28
|
+
const getRuntimeFooterDeviceTypeLabel = type => {
|
|
29
|
+
const normalizedType = safeTrim(type).toUpperCase();
|
|
30
|
+
|
|
31
|
+
if (PRINTER_DEVICE_TYPES.has(normalizedType)) {
|
|
32
|
+
return 'Impressora';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (normalizedType === IP_CAMERA_DEVICE_TYPE) {
|
|
36
|
+
return 'Camera IP';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return normalizedType || 'DEVICE';
|
|
40
|
+
};
|
|
41
|
+
|
|
28
42
|
const getRuntimeFooterTranslation = (store, type, key) =>
|
|
29
43
|
safeTrim(globalThis?.t?.getMessageFromBuckets?.(store, type, key));
|
|
30
44
|
|
|
@@ -254,7 +268,7 @@ const getRuntimeFooterOperationalTypeLabel = ({device, deviceConfig} = {}) => {
|
|
|
254
268
|
});
|
|
255
269
|
|
|
256
270
|
return operationalTypeInfo.value
|
|
257
|
-
?
|
|
271
|
+
? getRuntimeFooterDeviceTypeLabel(operationalTypeInfo.value)
|
|
258
272
|
: '';
|
|
259
273
|
};
|
|
260
274
|
|
|
@@ -382,7 +396,7 @@ const getRuntimeFooterDebugInfo = ({device, appVersion, deviceConfig}) => {
|
|
|
382
396
|
deviceConfig,
|
|
383
397
|
});
|
|
384
398
|
const operationalTypeLabel = operationalTypeInfo.value
|
|
385
|
-
?
|
|
399
|
+
? getRuntimeFooterDeviceTypeLabel(operationalTypeInfo.value)
|
|
386
400
|
: '';
|
|
387
401
|
const operationModeLabel =
|
|
388
402
|
operationalTypeLabel === 'PDV'
|
|
@@ -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
|
|
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: ['
|
|
45
|
+
queryTypes: ['PRINT', 'PRINTER'],
|
|
35
46
|
});
|
|
36
|
-
expect(params.type).
|
|
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
|
+
});
|
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
const assert = require('node:assert/strict')
|
|
2
|
-
const {
|
|
3
|
-
const {jest} = require('@jest/globals')
|
|
4
|
-
|
|
5
|
-
jest.mock('@controleonline/ui-common/src/react/utils/printerDevices', () => ({
|
|
6
|
-
getDeviceTypeLabel: value => String(value || ''),
|
|
7
|
-
}))
|
|
2
|
+
const {after, before, test} = require('node:test')
|
|
8
3
|
|
|
9
4
|
const {
|
|
10
5
|
getRuntimeFooterDebugInfo,
|
|
@@ -46,7 +41,7 @@ const restoreLocation = () => {
|
|
|
46
41
|
delete globalThis.location
|
|
47
42
|
}
|
|
48
43
|
|
|
49
|
-
|
|
44
|
+
before(() => {
|
|
50
45
|
global.t = {
|
|
51
46
|
getMessageFromBuckets: (store, type, key) =>
|
|
52
47
|
store === 'common' && type === 'option'
|
|
@@ -55,7 +50,7 @@ beforeAll(() => {
|
|
|
55
50
|
}
|
|
56
51
|
})
|
|
57
52
|
|
|
58
|
-
|
|
53
|
+
after(() => {
|
|
59
54
|
global.t = originalTranslator
|
|
60
55
|
})
|
|
61
56
|
|