@controleonline/ui-default 1.0.269 → 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.
Files changed (91) hide show
  1. package/.github/workflows/integration-source-gate.yml +68 -0
  2. package/.github/workflows/manager-worker.yml +19 -0
  3. package/AGENTS.md +46 -50
  4. package/docs/technical/Contrato-de-colunas.md +67 -0
  5. package/docs/technical/Default-Components.md +76 -0
  6. package/docs/technical/DefaultForm.md +80 -0
  7. package/docs/technical/DefaultInput-e-DefaultSelect.md +73 -0
  8. package/docs/technical/DefaultTable.md +203 -0
  9. package/docs/wiki.md +1 -0
  10. package/package.json +22 -21
  11. package/scripts/validate-wiki-pointer.cjs +13 -0
  12. package/src/react/components/address/DefaultAddress.js +435 -0
  13. package/src/react/components/address/DefaultAddress.styles.js +133 -0
  14. package/src/react/components/address/DefaultAddressMapPane.js +55 -0
  15. package/src/react/components/address/LatLonReadonlyFields.js +51 -0
  16. package/src/react/components/address/defaultAddressHelpers.js +163 -0
  17. package/src/react/components/address/defaultAddressStyles.js +136 -0
  18. package/src/react/components/address/defaultAddressUtils.js +129 -0
  19. package/src/react/components/extra-data/DefaultExtraData.js +152 -0
  20. package/src/react/components/extra-data/DefaultExtraData.styles.js +61 -0
  21. package/src/react/components/files/DefaultFile.js +231 -46
  22. package/src/react/components/files/DefaultFileColumn.js +123 -0
  23. package/src/react/components/files/DefaultFileColumn.styles.js +52 -0
  24. package/src/react/components/files/DefaultFileColumn.utils.js +104 -0
  25. package/src/react/components/filters/CompactFilterSelector.js +51 -35
  26. package/src/react/components/filters/DefaultColumnFilter.js +58 -41
  27. package/src/react/components/filters/DefaultExternalFilters.js +331 -265
  28. package/src/react/components/form/DefaultForm.js +99 -79
  29. package/src/react/components/help/DefaultTooltip.js +59 -33
  30. package/src/react/components/help/DefaultTooltip.styles.js +23 -3
  31. package/src/react/components/inputs/DefaultColorInput.js +153 -0
  32. package/src/react/components/inputs/DefaultFeatherIconPicker.js +146 -0
  33. package/src/react/components/inputs/DefaultFeatherIconPicker.styles.js +113 -0
  34. package/src/react/components/inputs/DefaultInput.js +69 -5
  35. package/src/react/components/inputs/DefaultInput.styles.js +26 -0
  36. package/src/react/components/inputs/DefaultSelect.js +134 -123
  37. package/src/react/components/inputs/defaultInputUtils.js +69 -43
  38. package/src/react/components/map/DefaultGoogleMap.styles.js +9 -2
  39. package/src/react/components/map/DefaultGoogleMap.web.js +49 -28
  40. package/src/react/components/map/DefaultMap.native.js +73 -73
  41. package/src/react/components/map/DefaultMap.shared.js +911 -762
  42. package/src/react/components/map/DefaultMap.web.js +124 -68
  43. package/src/react/components/table/DefaultColumnMenu.js +109 -108
  44. package/src/react/components/table/DefaultDebug.js +45 -38
  45. package/src/react/components/table/DefaultSearchModal.js +9 -1
  46. package/src/react/components/table/DefaultTable.grouping.js +98 -0
  47. package/src/react/components/table/DefaultTable.js +215 -109
  48. package/src/react/components/table/DefaultTable.styles.js +367 -314
  49. package/src/react/components/table/DefaultTableBody.js +13 -8
  50. package/src/react/components/table/DefaultTableCards.js +20 -1
  51. package/src/react/components/table/DefaultTableControls.js +180 -205
  52. package/src/react/components/table/DefaultTableImportModal.js +108 -0
  53. package/src/react/components/table/DefaultTableInput.js +1 -0
  54. package/src/react/components/table/DefaultTableRowActions.js +1 -1
  55. package/src/react/components/table/DefaultTableRows.js +224 -363
  56. package/src/react/components/table/DefaultTableSearch.js +28 -5
  57. package/src/react/components/table/DefaultTableToolbar.js +92 -3
  58. package/src/react/components/table/useDefaultTablePagination.js +56 -7
  59. package/src/react/components/table/useDefaultTableStoreSync.js +164 -0
  60. package/src/react/components/upload/DefaultUpload.js +436 -0
  61. package/src/react/components/upload/DefaultUpload.styles.js +153 -0
  62. package/src/react/components/upload/DefaultUploadAttachmentsList.js +138 -0
  63. package/src/react/components/upload/DefaultUploadManagerModal.js +247 -0
  64. package/src/react/components/upload/defaultUploadHelpers.js +108 -0
  65. package/src/react/components/upload/defaultUploadLibrary.js +248 -0
  66. package/src/react/components/upload/fileUpload.js +93 -0
  67. package/src/react/hooks/usePostalCodeLookup.js +145 -0
  68. package/src/react/index.js +6 -0
  69. package/src/react/services/addressFormUtils.js +196 -0
  70. package/src/react/services/addressGeo.js +227 -0
  71. package/src/react/utils/tableVisibleColumnsPreferences.js +29 -12
  72. package/src/tests/react/components/address/defaultAddressHelpers.test.js +104 -0
  73. package/src/tests/react/components/files/DefaultFile.test.js +6 -6
  74. package/src/tests/react/components/form/DefaultForm.test.js +123 -0
  75. package/src/tests/react/components/inputs/DefaultSelect.test.js +108 -79
  76. package/src/tests/react/components/table/DefaultTable.grouping.test.js +49 -0
  77. package/src/tests/react/components/table/DefaultTable.test.js +54 -2
  78. package/src/tests/react/components/table/DefaultTableControls.filters.test.mjs +21 -0
  79. package/src/tests/react/components/table/resolveRowBackgroundColor.test.mjs +45 -0
  80. package/src/tests/react/components/table/useDefaultTableStoreSync.test.js +161 -0
  81. package/src/tests/react/components/upload/DefaultUpload.test.js +353 -0
  82. package/src/tests/react/components/upload/defaultUploadLibrary.test.js +82 -0
  83. package/src/tests/react/components/upload/peopleMediaLibraryPreview.test.mjs +57 -0
  84. package/src/tests/react/hooks/usePostalCodeLookup.test.js +40 -0
  85. package/src/tests/react/services/SMOKE-637-cep-hydrate.md +45 -0
  86. package/src/tests/react/services/addressFormUtils.test.js +176 -0
  87. package/src/tests/react/services/addressGeoLookup.async.test.js +104 -0
  88. package/src/tests/react/services/addressGeoLookup.test.js +105 -0
  89. package/src/tests/react/services/smoke-637/form-cep-12941040.png +0 -0
  90. package/src/tests/react/services/smoke-637/index.html +132 -0
  91. 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`).