@controleonline/ui-default 1.0.270 → 1.0.272
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/integration-source-gate.yml +68 -0
- package/.github/workflows/manager-worker.yml +19 -0
- package/AGENTS.md +46 -50
- package/docs/technical/Contrato-de-colunas.md +67 -0
- package/docs/technical/Default-Components.md +76 -0
- package/docs/technical/DefaultForm.md +80 -0
- package/docs/technical/DefaultInput-e-DefaultSelect.md +73 -0
- package/docs/technical/DefaultTable.md +203 -0
- package/docs/wiki.md +1 -0
- package/package.json +22 -21
- package/scripts/validate-wiki-pointer.cjs +13 -0
- package/src/react/components/address/DefaultAddress.js +435 -0
- package/src/react/components/address/DefaultAddress.styles.js +133 -0
- package/src/react/components/address/DefaultAddressMapPane.js +55 -0
- package/src/react/components/address/LatLonReadonlyFields.js +51 -0
- package/src/react/components/address/defaultAddressHelpers.js +163 -0
- package/src/react/components/address/defaultAddressStyles.js +136 -0
- package/src/react/components/address/defaultAddressUtils.js +129 -0
- package/src/react/components/extra-data/DefaultExtraData.js +152 -0
- package/src/react/components/extra-data/DefaultExtraData.styles.js +61 -0
- package/src/react/components/files/DefaultFile.js +231 -46
- package/src/react/components/files/DefaultFileColumn.js +123 -0
- package/src/react/components/files/DefaultFileColumn.styles.js +52 -0
- package/src/react/components/files/DefaultFileColumn.utils.js +104 -0
- package/src/react/components/filters/CompactFilterSelector.js +51 -35
- package/src/react/components/filters/DefaultColumnFilter.js +58 -41
- package/src/react/components/filters/DefaultExternalFilters.js +331 -265
- package/src/react/components/form/DefaultForm.js +99 -79
- package/src/react/components/help/DefaultTooltip.js +59 -33
- package/src/react/components/help/DefaultTooltip.styles.js +23 -3
- package/src/react/components/inputs/DefaultColorInput.js +153 -0
- package/src/react/components/inputs/DefaultFeatherIconPicker.js +146 -0
- package/src/react/components/inputs/DefaultFeatherIconPicker.styles.js +113 -0
- package/src/react/components/inputs/DefaultInput.js +69 -5
- package/src/react/components/inputs/DefaultInput.styles.js +26 -0
- package/src/react/components/inputs/DefaultSelect.js +134 -123
- package/src/react/components/inputs/defaultInputUtils.js +69 -43
- package/src/react/components/map/DefaultMap.native.js +73 -73
- package/src/react/components/map/DefaultMap.shared.js +911 -762
- package/src/react/components/map/DefaultMap.web.js +124 -68
- package/src/react/components/table/DefaultColumnMenu.js +109 -108
- package/src/react/components/table/DefaultDebug.js +44 -37
- package/src/react/components/table/DefaultSearchModal.js +9 -1
- package/src/react/components/table/DefaultTable.grouping.js +98 -0
- package/src/react/components/table/DefaultTable.js +213 -107
- package/src/react/components/table/DefaultTable.styles.js +367 -314
- package/src/react/components/table/DefaultTableBody.js +13 -8
- package/src/react/components/table/DefaultTableCards.js +20 -1
- package/src/react/components/table/DefaultTableControls.js +180 -205
- package/src/react/components/table/DefaultTableImportModal.js +108 -0
- package/src/react/components/table/DefaultTableInput.js +1 -0
- package/src/react/components/table/DefaultTableRowActions.js +1 -1
- package/src/react/components/table/DefaultTableRows.js +224 -363
- package/src/react/components/table/DefaultTableSearch.js +28 -5
- package/src/react/components/table/DefaultTableToolbar.js +92 -3
- package/src/react/components/table/useDefaultTablePagination.js +56 -7
- package/src/react/components/table/useDefaultTableStoreSync.js +164 -0
- package/src/react/components/upload/DefaultUpload.js +436 -0
- package/src/react/components/upload/DefaultUpload.styles.js +153 -0
- package/src/react/components/upload/DefaultUploadAttachmentsList.js +138 -0
- package/src/react/components/upload/DefaultUploadManagerModal.js +247 -0
- package/src/react/components/upload/defaultUploadHelpers.js +108 -0
- package/src/react/components/upload/defaultUploadLibrary.js +248 -0
- package/src/react/components/upload/fileUpload.js +93 -0
- package/src/react/hooks/usePostalCodeLookup.js +145 -0
- package/src/react/index.js +6 -0
- package/src/react/services/addressFormUtils.js +196 -0
- package/src/react/services/addressGeo.js +227 -0
- package/src/react/utils/tableVisibleColumnsPreferences.js +29 -12
- package/src/tests/react/components/address/defaultAddressHelpers.test.js +104 -0
- package/src/tests/react/components/files/DefaultFile.test.js +6 -6
- package/src/tests/react/components/form/DefaultForm.test.js +123 -0
- package/src/tests/react/components/inputs/DefaultSelect.test.js +108 -79
- package/src/tests/react/components/table/DefaultTable.grouping.test.js +49 -0
- package/src/tests/react/components/table/DefaultTable.test.js +53 -2
- package/src/tests/react/components/table/DefaultTableControls.filters.test.mjs +21 -0
- package/src/tests/react/components/table/resolveRowBackgroundColor.test.mjs +45 -0
- package/src/tests/react/components/table/useDefaultTableStoreSync.test.js +161 -0
- package/src/tests/react/components/upload/DefaultUpload.test.js +353 -0
- package/src/tests/react/components/upload/defaultUploadLibrary.test.js +82 -0
- package/src/tests/react/components/upload/peopleMediaLibraryPreview.test.mjs +57 -0
- package/src/tests/react/hooks/usePostalCodeLookup.test.js +40 -0
- package/src/tests/react/services/SMOKE-637-cep-hydrate.md +45 -0
- package/src/tests/react/services/addressFormUtils.test.js +176 -0
- package/src/tests/react/services/addressGeoLookup.async.test.js +104 -0
- package/src/tests/react/services/addressGeoLookup.test.js +105 -0
- package/src/tests/react/services/smoke-637/form-cep-12941040.png +0 -0
- package/src/tests/react/services/smoke-637/index.html +132 -0
- package/src/tests/react/utils/tableVisibleColumnsPreferences.test.js +84 -48
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
const mockReact = require('react');
|
|
2
|
+
const renderer = require('react-test-renderer');
|
|
3
|
+
|
|
4
|
+
const {describe, expect, it, beforeEach} = global;
|
|
5
|
+
|
|
6
|
+
global.IS_REACT_ACT_ENVIRONMENT = true;
|
|
7
|
+
|
|
8
|
+
const mockFileActions = {
|
|
9
|
+
getItems: jest.fn(),
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const mockRelationActions = {
|
|
13
|
+
save: jest.fn(),
|
|
14
|
+
remove: jest.fn(),
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
jest.mock('@env', () => ({
|
|
18
|
+
env: {
|
|
19
|
+
API_ENTRYPOINT: 'https://api.controleonline.com',
|
|
20
|
+
DOMAIN: 'manager.controleonline.com',
|
|
21
|
+
},
|
|
22
|
+
}), {virtual: true});
|
|
23
|
+
|
|
24
|
+
jest.mock('@store', () => ({
|
|
25
|
+
useStore: jest.fn(name => {
|
|
26
|
+
if (name === 'file') {
|
|
27
|
+
return {
|
|
28
|
+
actions: mockFileActions,
|
|
29
|
+
getters: {},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (name === 'product_file' || name === 'category_file') {
|
|
34
|
+
return {
|
|
35
|
+
actions: mockRelationActions,
|
|
36
|
+
getters: {},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
actions: {},
|
|
42
|
+
getters: {},
|
|
43
|
+
};
|
|
44
|
+
}),
|
|
45
|
+
}), {virtual: true});
|
|
46
|
+
|
|
47
|
+
jest.mock(
|
|
48
|
+
'@expo/vector-icons',
|
|
49
|
+
() => ({
|
|
50
|
+
MaterialCommunityIcons: props =>
|
|
51
|
+
mockReact.createElement('icon', props, props.children),
|
|
52
|
+
}),
|
|
53
|
+
{virtual: true},
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
jest.mock('react-native-vector-icons/Feather', () => props =>
|
|
57
|
+
mockReact.createElement('feathericon', props, props.children),
|
|
58
|
+
{virtual: true},
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
jest.mock('@controleonline/ui-common/src/react/components/AnimatedModal', () => {
|
|
62
|
+
return props => (props.visible ? mockReact.createElement('modal', props, props.children) : null);
|
|
63
|
+
}, {virtual: true});
|
|
64
|
+
|
|
65
|
+
jest.mock('@controleonline/ui-default/src/react/components/files/DefaultFile', () => {
|
|
66
|
+
return props => mockReact.createElement('defaultfile', props, props.children);
|
|
67
|
+
}, {virtual: true});
|
|
68
|
+
|
|
69
|
+
jest.mock('../../../../react/components/upload/fileUpload', () => ({
|
|
70
|
+
selectFile: jest.fn(),
|
|
71
|
+
uploadFileToApi: jest.fn(),
|
|
72
|
+
toFileIri: jest.fn(),
|
|
73
|
+
extractFileId: value => value?.id || value,
|
|
74
|
+
}), {virtual: true});
|
|
75
|
+
|
|
76
|
+
jest.mock('react-native', () => ({
|
|
77
|
+
ActivityIndicator: props => mockReact.createElement('activityindicator', props, props.children),
|
|
78
|
+
Platform: {OS: 'web'},
|
|
79
|
+
ScrollView: props => mockReact.createElement('scrollview', props, props.children),
|
|
80
|
+
Text: props => mockReact.createElement('text', props, props.children),
|
|
81
|
+
TextInput: props => mockReact.createElement('textinput', props, props.children),
|
|
82
|
+
TouchableOpacity: props => mockReact.createElement('touchableopacity', props, props.children),
|
|
83
|
+
View: props => mockReact.createElement('view', props, props.children),
|
|
84
|
+
}), {virtual: true});
|
|
85
|
+
|
|
86
|
+
const {selectFile, uploadFileToApi, toFileIri} = require('../../../../react/components/upload/fileUpload');
|
|
87
|
+
const DefaultUpload = require('../../../../react/components/upload/DefaultUpload').default;
|
|
88
|
+
|
|
89
|
+
const flush = () => new Promise(resolve => setImmediate(resolve));
|
|
90
|
+
|
|
91
|
+
const collectText = node =>
|
|
92
|
+
node
|
|
93
|
+
.findAllByType('text')
|
|
94
|
+
.map(textNode => {
|
|
95
|
+
const children = textNode.props.children;
|
|
96
|
+
if (Array.isArray(children)) {
|
|
97
|
+
return children.flat(Infinity).join('');
|
|
98
|
+
}
|
|
99
|
+
return children || '';
|
|
100
|
+
})
|
|
101
|
+
.join('');
|
|
102
|
+
|
|
103
|
+
const findButtonByLabel = (root, label) =>
|
|
104
|
+
root.findAllByType('touchableopacity').find(button => collectText(button).includes(label));
|
|
105
|
+
|
|
106
|
+
const findButtonByAccessibilityLabel = (root, label) =>
|
|
107
|
+
root.findAllByType('touchableopacity').find(button => button.props.accessibilityLabel === label);
|
|
108
|
+
|
|
109
|
+
describe('DefaultUpload', () => {
|
|
110
|
+
beforeEach(() => {
|
|
111
|
+
jest.clearAllMocks();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('abre a biblioteca, carrega arquivos e envia um arquivo pelo helper centralizado', async () => {
|
|
115
|
+
mockFileActions.getItems.mockResolvedValue({
|
|
116
|
+
member: [
|
|
117
|
+
{id: 10, fileName: 'Biblioteca 1', context: 'products', fileType: 'image'},
|
|
118
|
+
],
|
|
119
|
+
});
|
|
120
|
+
mockRelationActions.save.mockResolvedValue({id: 42});
|
|
121
|
+
selectFile.mockResolvedValue({
|
|
122
|
+
name: 'nova-imagem.png',
|
|
123
|
+
uri: 'file:///nova-imagem.png',
|
|
124
|
+
mimeType: 'image/png',
|
|
125
|
+
});
|
|
126
|
+
uploadFileToApi.mockResolvedValue({id: 77, fileName: 'nova-imagem.png'});
|
|
127
|
+
toFileIri.mockReturnValue('/files/77');
|
|
128
|
+
|
|
129
|
+
let tree;
|
|
130
|
+
|
|
131
|
+
await renderer.act(async () => {
|
|
132
|
+
tree = renderer.create(
|
|
133
|
+
mockReact.createElement(DefaultUpload, {
|
|
134
|
+
relationStoreName: 'product_file',
|
|
135
|
+
relationField: 'product',
|
|
136
|
+
relationResource: 'products',
|
|
137
|
+
entityId: 12,
|
|
138
|
+
companyId: 3,
|
|
139
|
+
attachments: [{id: 1, file: {id: 1, fileName: 'Anexa'}}],
|
|
140
|
+
context: 'products',
|
|
141
|
+
title: 'Imagens anexas',
|
|
142
|
+
triggerLabel: 'Gerenciar imagens',
|
|
143
|
+
managerTitle: 'Gerenciador de imagens',
|
|
144
|
+
searchPlaceholder: 'Buscar imagem',
|
|
145
|
+
uploadButtonLabel: 'Enviar nova',
|
|
146
|
+
}),
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
await renderer.act(async () => {
|
|
151
|
+
findButtonByLabel(tree.root, 'Gerenciar imagens').props.onPress();
|
|
152
|
+
await flush();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
expect(mockFileActions.getItems).toHaveBeenCalled();
|
|
156
|
+
|
|
157
|
+
const uploadButton = findButtonByLabel(tree.root, 'Enviar nova');
|
|
158
|
+
expect(uploadButton).toBeTruthy();
|
|
159
|
+
|
|
160
|
+
await renderer.act(async () => {
|
|
161
|
+
await uploadButton.props.onPress();
|
|
162
|
+
await flush();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
expect(selectFile).toHaveBeenCalledWith('image/*');
|
|
166
|
+
expect(uploadFileToApi).toHaveBeenCalledWith({
|
|
167
|
+
file: expect.objectContaining({name: 'nova-imagem.png'}),
|
|
168
|
+
context: 'products',
|
|
169
|
+
peopleId: 3,
|
|
170
|
+
entityId: 12,
|
|
171
|
+
});
|
|
172
|
+
expect(mockRelationActions.save).toHaveBeenCalledWith({
|
|
173
|
+
product: '/products/12',
|
|
174
|
+
file: '/files/77',
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('permite que o pai gerencie upload anexado e remocao por contexto customizado', async () => {
|
|
179
|
+
const onChanged = jest.fn();
|
|
180
|
+
const onUploadFile = jest.fn().mockResolvedValue({id: 8, file: {id: 88}});
|
|
181
|
+
const onRemoveAttachment = jest.fn().mockResolvedValue({});
|
|
182
|
+
|
|
183
|
+
mockFileActions.getItems.mockResolvedValue({member: []});
|
|
184
|
+
selectFile.mockResolvedValue({
|
|
185
|
+
name: 'avatar.png',
|
|
186
|
+
type: 'image/png',
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
let tree;
|
|
190
|
+
|
|
191
|
+
await renderer.act(async () => {
|
|
192
|
+
tree = renderer.create(
|
|
193
|
+
mockReact.createElement(DefaultUpload, {
|
|
194
|
+
relationStoreName: 'people',
|
|
195
|
+
relationField: 'people',
|
|
196
|
+
relationResource: 'people',
|
|
197
|
+
entityId: 30,
|
|
198
|
+
companyId: 30,
|
|
199
|
+
attachments: [{file: {id: 91, fileName: 'avatar.png'}}],
|
|
200
|
+
context: 'people_media',
|
|
201
|
+
libraryContexts: ['people_media'],
|
|
202
|
+
title: 'avatar',
|
|
203
|
+
triggerLabel: 'Gerenciar avatar',
|
|
204
|
+
uploadButtonLabel: 'Enviar nova',
|
|
205
|
+
onChanged,
|
|
206
|
+
onUploadFile,
|
|
207
|
+
onRemoveAttachment,
|
|
208
|
+
uploadResultAlreadyAttached: true,
|
|
209
|
+
}),
|
|
210
|
+
);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
await renderer.act(async () => {
|
|
214
|
+
findButtonByLabel(tree.root, 'Gerenciar avatar').props.onPress();
|
|
215
|
+
await flush();
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
await renderer.act(async () => {
|
|
219
|
+
await findButtonByLabel(tree.root, 'Enviar nova').props.onPress();
|
|
220
|
+
await flush();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
expect(onUploadFile).toHaveBeenCalledWith(
|
|
224
|
+
expect.objectContaining({
|
|
225
|
+
context: 'people_media',
|
|
226
|
+
entityId: 30,
|
|
227
|
+
file: expect.objectContaining({name: 'avatar.png'}),
|
|
228
|
+
}),
|
|
229
|
+
);
|
|
230
|
+
expect(mockRelationActions.save).not.toHaveBeenCalled();
|
|
231
|
+
expect(onChanged).toHaveBeenCalled();
|
|
232
|
+
|
|
233
|
+
const removeButton = findButtonByAccessibilityLabel(tree.root, 'Remover');
|
|
234
|
+
expect(removeButton).toBeTruthy();
|
|
235
|
+
|
|
236
|
+
await renderer.act(async () => {
|
|
237
|
+
await removeButton.props.onPress();
|
|
238
|
+
await flush();
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
expect(onRemoveAttachment).toHaveBeenCalledWith(
|
|
242
|
+
expect.objectContaining({file: expect.objectContaining({id: 91})}),
|
|
243
|
+
);
|
|
244
|
+
expect(mockRelationActions.remove).not.toHaveBeenCalled();
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('normaliza o identificador da relacao antes da remocao customizada', async () => {
|
|
248
|
+
const onRemoveAttachment = jest.fn().mockResolvedValue({});
|
|
249
|
+
|
|
250
|
+
let tree;
|
|
251
|
+
|
|
252
|
+
await renderer.act(async () => {
|
|
253
|
+
tree = renderer.create(
|
|
254
|
+
mockReact.createElement(DefaultUpload, {
|
|
255
|
+
relationStoreName: 'people',
|
|
256
|
+
relationField: 'people',
|
|
257
|
+
relationResource: 'people',
|
|
258
|
+
entityId: 30,
|
|
259
|
+
companyId: 30,
|
|
260
|
+
attachments: [{mediaId: '/people_media/44', file: {id: 91, fileName: 'avatar.png'}}],
|
|
261
|
+
context: 'people_media',
|
|
262
|
+
title: 'avatar',
|
|
263
|
+
triggerLabel: 'Gerenciar avatar',
|
|
264
|
+
onRemoveAttachment,
|
|
265
|
+
}),
|
|
266
|
+
);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
await renderer.act(async () => {
|
|
270
|
+
await findButtonByAccessibilityLabel(tree.root, 'Remover').props.onPress();
|
|
271
|
+
await flush();
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
expect(onRemoveAttachment).toHaveBeenCalledWith(
|
|
275
|
+
expect.objectContaining({
|
|
276
|
+
id: '44',
|
|
277
|
+
mediaId: '/people_media/44',
|
|
278
|
+
file: expect.objectContaining({id: 91}),
|
|
279
|
+
}),
|
|
280
|
+
);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('renderiza apenas o gatilho customizado quando o conteudo inline fica desativado', async () => {
|
|
284
|
+
let tree;
|
|
285
|
+
|
|
286
|
+
await renderer.act(async () => {
|
|
287
|
+
tree = renderer.create(
|
|
288
|
+
mockReact.createElement(DefaultUpload, {
|
|
289
|
+
relationStoreName: 'people',
|
|
290
|
+
relationField: 'people',
|
|
291
|
+
relationResource: 'people',
|
|
292
|
+
entityId: 30,
|
|
293
|
+
companyId: 30,
|
|
294
|
+
attachments: [{file: {id: 91, fileName: 'avatar.png'}}],
|
|
295
|
+
context: 'people_media',
|
|
296
|
+
title: 'avatar',
|
|
297
|
+
showInlineContent: false,
|
|
298
|
+
renderTrigger: ({openManager}) =>
|
|
299
|
+
mockReact.createElement(
|
|
300
|
+
'touchableopacity',
|
|
301
|
+
{onPress: openManager},
|
|
302
|
+
mockReact.createElement('text', null, 'Abrir gerenciador customizado'),
|
|
303
|
+
),
|
|
304
|
+
}),
|
|
305
|
+
);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
expect(collectText(tree.root)).toContain('Abrir gerenciador customizado');
|
|
309
|
+
expect(collectText(tree.root)).not.toContain('avatar.png');
|
|
310
|
+
|
|
311
|
+
await renderer.act(async () => {
|
|
312
|
+
findButtonByLabel(tree.root, 'Abrir gerenciador customizado').props.onPress();
|
|
313
|
+
await flush();
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
expect(mockFileActions.getItems).toHaveBeenCalled();
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('nao recarrega a biblioteca quando arrays equivalentes sao recriados', async () => {
|
|
320
|
+
mockFileActions.getItems.mockResolvedValue({member: []});
|
|
321
|
+
let tree;
|
|
322
|
+
const renderProps = () => ({
|
|
323
|
+
relationStoreName: 'people',
|
|
324
|
+
relationField: 'people',
|
|
325
|
+
relationResource: 'people',
|
|
326
|
+
entityId: 30,
|
|
327
|
+
companyId: 30,
|
|
328
|
+
context: 'company_certificate',
|
|
329
|
+
libraryContexts: ['company_certificate'],
|
|
330
|
+
knownFileIds: [],
|
|
331
|
+
showInlineContent: false,
|
|
332
|
+
renderTrigger: ({openManager}) =>
|
|
333
|
+
mockReact.createElement('touchableopacity', {onPress: openManager}, 'Abrir'),
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
await renderer.act(async () => {
|
|
337
|
+
tree = renderer.create(mockReact.createElement(DefaultUpload, renderProps()));
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
await renderer.act(async () => {
|
|
341
|
+
tree.root.findByType('touchableopacity').props.onPress();
|
|
342
|
+
await flush();
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
await renderer.act(async () => {
|
|
346
|
+
tree.update(mockReact.createElement(DefaultUpload, renderProps()));
|
|
347
|
+
tree.update(mockReact.createElement(DefaultUpload, renderProps()));
|
|
348
|
+
await flush();
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
expect(mockFileActions.getItems).toHaveBeenCalledTimes(1);
|
|
352
|
+
});
|
|
353
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/** @jest-environment node */
|
|
2
|
+
|
|
3
|
+
/** app-community#433 — library lists all people_media files for the person */
|
|
4
|
+
const {
|
|
5
|
+
fetchLibraryFiles,
|
|
6
|
+
filesFromPeopleMediaRelations,
|
|
7
|
+
} = require('../../../../react/components/upload/defaultUploadLibrary');
|
|
8
|
+
|
|
9
|
+
describe('filesFromPeopleMediaRelations', () => {
|
|
10
|
+
it('extracts embedded file objects with id', () => {
|
|
11
|
+
const relations = [
|
|
12
|
+
{id: 1, file: {id: 10, fileName: 'a.png', '@id': '/files/10'}},
|
|
13
|
+
{id: 2, file: {id: 20, fileName: 'b.png'}},
|
|
14
|
+
{id: 3, file: null},
|
|
15
|
+
{id: 4},
|
|
16
|
+
];
|
|
17
|
+
const files = filesFromPeopleMediaRelations(relations);
|
|
18
|
+
expect(files).toHaveLength(2);
|
|
19
|
+
expect(files.map(f => f.id)).toEqual([10, 20]);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('fetchLibraryFiles', () => {
|
|
24
|
+
it('returns empty when getItems is missing', async () => {
|
|
25
|
+
const result = await fetchLibraryFiles({fileActions: {}});
|
|
26
|
+
expect(result).toEqual([]);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('sends itemsPerPage and merges people_media relations', async () => {
|
|
30
|
+
const getItems = jest.fn().mockResolvedValue({
|
|
31
|
+
member: [{id: 10, fileName: 'from-files.png', context: 'people_media'}],
|
|
32
|
+
});
|
|
33
|
+
const getPeopleMedia = jest.fn().mockResolvedValue({
|
|
34
|
+
member: [
|
|
35
|
+
{id: 1, file: {id: 10, fileName: 'from-files.png'}},
|
|
36
|
+
{id: 2, file: {id: 11, fileName: 'only-relation.png'}},
|
|
37
|
+
],
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const files = await fetchLibraryFiles({
|
|
41
|
+
fileActions: {getItems},
|
|
42
|
+
companyId: 103446,
|
|
43
|
+
fileType: 'image',
|
|
44
|
+
libraryContexts: ['people_media'],
|
|
45
|
+
peopleActions: {getPeopleMedia},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
expect(getItems).toHaveBeenCalledWith(
|
|
49
|
+
expect.objectContaining({
|
|
50
|
+
context: 'people_media',
|
|
51
|
+
people: '/people/103446',
|
|
52
|
+
itemsPerPage: 500,
|
|
53
|
+
page: 1,
|
|
54
|
+
}),
|
|
55
|
+
);
|
|
56
|
+
expect(getPeopleMedia).toHaveBeenCalledWith(
|
|
57
|
+
expect.objectContaining({
|
|
58
|
+
people: '/people/103446',
|
|
59
|
+
itemsPerPage: 100,
|
|
60
|
+
}),
|
|
61
|
+
);
|
|
62
|
+
const ids = files.map(f => String(f.id)).sort();
|
|
63
|
+
expect(ids).toEqual(['10', '11']);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('still returns /files results if people_media fetch fails', async () => {
|
|
67
|
+
const getItems = jest.fn().mockResolvedValue({
|
|
68
|
+
member: [{id: 5, fileName: 'ok.png'}],
|
|
69
|
+
});
|
|
70
|
+
const getPeopleMedia = jest.fn().mockRejectedValue(new Error('network'));
|
|
71
|
+
|
|
72
|
+
const files = await fetchLibraryFiles({
|
|
73
|
+
fileActions: {getItems},
|
|
74
|
+
companyId: 1,
|
|
75
|
+
libraryContexts: ['people_media'],
|
|
76
|
+
peopleActions: {getPeopleMedia},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
expect(files).toHaveLength(1);
|
|
80
|
+
expect(files[0].id).toBe(5);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { readFileSync } from 'node:fs';
|
|
7
|
+
|
|
8
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), '../../../../../');
|
|
9
|
+
const require = createRequire(import.meta.url);
|
|
10
|
+
const lib = require(join(root, 'src/react/components/upload/defaultUploadLibrary.js'));
|
|
11
|
+
|
|
12
|
+
test('people_media path never requires peopleIri to avoid GET /files', async () => {
|
|
13
|
+
const source = readFileSync(
|
|
14
|
+
join(root, 'src/react/components/upload/defaultUploadLibrary.js'),
|
|
15
|
+
'utf8',
|
|
16
|
+
);
|
|
17
|
+
assert.match(source, /if \(includesPeopleMedia\)/);
|
|
18
|
+
assert.doesNotMatch(
|
|
19
|
+
source,
|
|
20
|
+
/if \(includesPeopleMedia && peopleIri\)/,
|
|
21
|
+
);
|
|
22
|
+
// Without peopleIri still returns stubs only
|
|
23
|
+
const files = await lib.fetchLibraryFiles({
|
|
24
|
+
fileActions: {
|
|
25
|
+
getItems: async () => {
|
|
26
|
+
throw new Error('should not call getItems');
|
|
27
|
+
},
|
|
28
|
+
get: async () => {
|
|
29
|
+
throw new Error('should not call get');
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
companyId: null,
|
|
33
|
+
fileType: 'image',
|
|
34
|
+
libraryContexts: ['people_media'],
|
|
35
|
+
peopleActions: null,
|
|
36
|
+
knownFileIds: [8469],
|
|
37
|
+
});
|
|
38
|
+
assert.equal(files.length, 1);
|
|
39
|
+
assert.equal(files[0].id, 8469);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('inline attachments list can hide star/trash', () => {
|
|
43
|
+
const source = readFileSync(
|
|
44
|
+
join(root, 'src/react/components/upload/DefaultUploadAttachmentsList.js'),
|
|
45
|
+
'utf8',
|
|
46
|
+
);
|
|
47
|
+
assert.match(source, /showAttachmentActions/);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('manager shows remove for attached library items', () => {
|
|
51
|
+
const source = readFileSync(
|
|
52
|
+
join(root, 'src/react/components/upload/DefaultUploadManagerModal.js'),
|
|
53
|
+
'utf8',
|
|
54
|
+
);
|
|
55
|
+
assert.match(source, /trash-can-outline/);
|
|
56
|
+
assert.match(source, /handleRemove/);
|
|
57
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Behavioral tests for race / debounce contract of CEP lookup helpers.
|
|
3
|
+
* Hook itself is integration-tested via merge + request-id logic mirrored here.
|
|
4
|
+
*/
|
|
5
|
+
import {mergePostalCodeData, onlyDigits} from '../../../react/services/addressFormUtils';
|
|
6
|
+
|
|
7
|
+
describe('CEP lookup race contract', () => {
|
|
8
|
+
test('onlyDigits length gate for lookup', () => {
|
|
9
|
+
expect(onlyDigits('01310-10').length).toBe(7);
|
|
10
|
+
expect(onlyDigits('01310-100').length).toBe(8);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test('stale response simulation: last write wins by request id', () => {
|
|
14
|
+
let form = {
|
|
15
|
+
nickname: 'X',
|
|
16
|
+
number: '9',
|
|
17
|
+
complement: '',
|
|
18
|
+
street: '',
|
|
19
|
+
district: '',
|
|
20
|
+
city: '',
|
|
21
|
+
uf: '',
|
|
22
|
+
cep: '',
|
|
23
|
+
countryCode: 'BR',
|
|
24
|
+
countryName: 'Brazil',
|
|
25
|
+
};
|
|
26
|
+
let requestId = 0;
|
|
27
|
+
const apply = (id, data) => {
|
|
28
|
+
if (id !== requestId) return; // stale
|
|
29
|
+
form = mergePostalCodeData({...form, cep: data.cep}, data);
|
|
30
|
+
};
|
|
31
|
+
const first = ++requestId;
|
|
32
|
+
const second = ++requestId;
|
|
33
|
+
apply(first, {cep: '11111111', street: 'Old'});
|
|
34
|
+
apply(second, {cep: '01310100', street: 'Av Paulista'});
|
|
35
|
+
expect(form.street).toBe('Av Paulista');
|
|
36
|
+
expect(form.cep).toBe('01310100');
|
|
37
|
+
expect(form.number).toBe('9');
|
|
38
|
+
expect(form.nickname).toBe('X');
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# fluxo: address-cep-hydrate
|
|
2
|
+
|
|
3
|
+
Issue: ControleOnline/app-community#637
|
|
4
|
+
CEP: 12941040
|
|
5
|
+
Data: 2026-08-27
|
|
6
|
+
|
|
7
|
+
## Smoke UI (browser)
|
|
8
|
+
|
|
9
|
+
Harness: `src/tests/react/services/smoke-637/index.html`
|
|
10
|
+
Print: `src/tests/react/services/smoke-637/form-cep-12941040.png`
|
|
11
|
+
|
|
12
|
+
Algoritmo idêntico a `lookupPostalCode` (`addressGeo.js`):
|
|
13
|
+
API postal-codes → ViaCEP se street/city vazios → Nominatim se coords faltarem.
|
|
14
|
+
|
|
15
|
+
Campos com os mesmos `testID`s do `DefaultAddress`:
|
|
16
|
+
`address-cep-input`, `address-street-input`, `address-district-input`,
|
|
17
|
+
`address-city-input`, `address-latitude-input`, `address-longitude-input`.
|
|
18
|
+
|
|
19
|
+
### Observado no browser (2026-08-27)
|
|
20
|
+
|
|
21
|
+
- Tela aberta com formulário de endereço
|
|
22
|
+
- CEP `12941040` hidrata:
|
|
23
|
+
- rua = Rua Antônio Bonini
|
|
24
|
+
- bairro = Vila Santista
|
|
25
|
+
- cidade = Atibaia
|
|
26
|
+
- UF = SP
|
|
27
|
+
- lat = -23.117082 ≠ —
|
|
28
|
+
- lon = -46.5425915 ≠ —
|
|
29
|
+
- Provider merge: `viacep+googlemaps+viacep-client`
|
|
30
|
+
- Mapa OSM centrado em Vila Santista / Atibaia (pin na Rua Antônio Bonini)
|
|
31
|
+
- Mensagem *Postalcode services are not available* **não** ocorreu
|
|
32
|
+
- Console: sem erro da entrega no harness
|
|
33
|
+
|
|
34
|
+
## Passos runtime
|
|
35
|
+
|
|
36
|
+
1. `GET /postal-codes/12941040` (ADMIN) → **200**, street/city vazios, coords preenchidas
|
|
37
|
+
2. ViaCEP 200 → texto Atibaia/SP
|
|
38
|
+
3. Merge texto + coords da API
|
|
39
|
+
4. Leaflet `setView` Atibaia, zoom 16
|
|
40
|
+
|
|
41
|
+
## Resultado
|
|
42
|
+
|
|
43
|
+
**PASS** UI form + mapa Atibaia + lat/lon preenchidos
|
|
44
|
+
|
|
45
|
+
Pin de produto em staging/prod continua fora do papel Developer (não promove `staging`).
|