@controleonline/ui-default 1.0.270 → 1.0.273
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/.github/workflows/publish-npm.yml +91 -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 +272 -0
- package/docs/wiki.md +1 -0
- package/package.json +4 -3
- package/scripts/validate-wiki-pointer.cjs +13 -0
- package/src/react/components/address/DefaultAddress.js +446 -0
- package/src/react/components/address/DefaultAddress.styles.js +133 -0
- package/src/react/components/address/DefaultAddressCoordFields.js +59 -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 +148 -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 +60 -48
- package/src/react/components/filters/DefaultExternalFilters.js +324 -265
- package/src/react/components/filters/filterValue.js +65 -0
- package/src/react/components/form/DefaultForm.js +99 -79
- package/src/react/components/help/DefaultTooltip.js +48 -12
- package/src/react/components/help/DefaultTooltip.styles.js +20 -0
- 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 +296 -141
- 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 +262 -213
- 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 +22 -3
- 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 +217 -111
- package/src/react/components/table/DefaultTable.styles.js +369 -310
- package/src/react/components/table/DefaultTable.utils.js +38 -4
- 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 +357 -325
- 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 +406 -0
- package/src/react/components/upload/DefaultUpload.styles.js +153 -0
- package/src/react/components/upload/DefaultUploadAttachmentsList.js +146 -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 +159 -0
- package/src/react/index.js +6 -0
- package/src/react/services/addressFormUtils.js +262 -0
- package/src/react/services/addressGeo.js +230 -0
- package/src/react/utils/tableVisibleColumnsPreferences.js +113 -15
- package/src/tests/react/components/address/defaultAddressHelpers.test.js +104 -0
- package/src/tests/react/components/files/DefaultFile.test.js +22 -6
- package/src/tests/react/components/files/DefaultFileColumn.company.test.js +24 -0
- package/src/tests/react/components/form/DefaultForm.test.js +123 -0
- package/src/tests/react/components/help/DefaultTooltip.test.js +93 -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/DefaultTable.utils.test.js +10 -0
- package/src/tests/react/components/table/DefaultTableControls.filters.test.mjs +21 -0
- package/src/tests/react/components/table/DefaultTableRowActions.test.js +23 -0
- package/src/tests/react/components/table/DefaultTableRows.rowActions.test.js +152 -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 +104 -0
- package/src/tests/react/components/upload/peopleMediaLibraryPreview.test.mjs +57 -0
- package/src/tests/react/filters/filterValue.test.js +46 -0
- package/src/tests/react/hooks/usePostalCodeLookup.test.js +107 -0
- package/src/tests/react/inputs/defaultInputUtils.test.js +9 -1
- package/src/tests/react/services/SMOKE-637-cep-hydrate.md +45 -0
- package/src/tests/react/services/addressFormUtils.test.js +179 -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 +96 -49
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import {
|
|
2
|
+
countActiveFilters,
|
|
3
|
+
isFilledFilterValue,
|
|
4
|
+
omitEmptyFilters,
|
|
5
|
+
} from '../../../react/components/filters/filterValue';
|
|
6
|
+
|
|
7
|
+
describe('filterValue', () => {
|
|
8
|
+
it('treats empty strings, null and empty collections as inactive', () => {
|
|
9
|
+
expect(isFilledFilterValue('')).toBe(false);
|
|
10
|
+
expect(isFilledFilterValue(' ')).toBe(false);
|
|
11
|
+
expect(isFilledFilterValue(null)).toBe(false);
|
|
12
|
+
expect(isFilledFilterValue(undefined)).toBe(false);
|
|
13
|
+
expect(isFilledFilterValue([])).toBe(false);
|
|
14
|
+
expect(isFilledFilterValue({})).toBe(false);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('does not count date shortcut all or empty custom range', () => {
|
|
18
|
+
expect(isFilledFilterValue({shortcut: 'all'})).toBe(false);
|
|
19
|
+
expect(
|
|
20
|
+
isFilledFilterValue({
|
|
21
|
+
shortcut: 'custom',
|
|
22
|
+
customRange: {from: '', to: ''},
|
|
23
|
+
}),
|
|
24
|
+
).toBe(false);
|
|
25
|
+
expect(isFilledFilterValue({shortcut: '7d'})).toBe(true);
|
|
26
|
+
expect(
|
|
27
|
+
isFilledFilterValue({
|
|
28
|
+
shortcut: 'custom',
|
|
29
|
+
customRange: {from: '2026-01-01', to: ''},
|
|
30
|
+
}),
|
|
31
|
+
).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('clears leftover empty filters when counting and omitting', () => {
|
|
35
|
+
const filters = {
|
|
36
|
+
status: '',
|
|
37
|
+
search: ' ',
|
|
38
|
+
period: {shortcut: 'all', customRange: {from: '', to: ''}},
|
|
39
|
+
page: 1,
|
|
40
|
+
category: 'sales',
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
expect(countActiveFilters(filters)).toBe(1);
|
|
44
|
+
expect(omitEmptyFilters(filters)).toEqual({category: 'sales'});
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
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
|
+
});
|
|
41
|
+
|
|
42
|
+
import {
|
|
43
|
+
GEOCODE_MISS_MESSAGE,
|
|
44
|
+
isGeocodeMiss,
|
|
45
|
+
mergePostalCodeData,
|
|
46
|
+
parseOptionalCoordinate,
|
|
47
|
+
} from '../../../react/services/addressFormUtils';
|
|
48
|
+
|
|
49
|
+
describe('geocode miss (#696)', () => {
|
|
50
|
+
test('isGeocodeMiss when address text exists and coords are null', () => {
|
|
51
|
+
expect(
|
|
52
|
+
isGeocodeMiss({
|
|
53
|
+
street: 'Rua Lotus',
|
|
54
|
+
district: 'Condomínio Jardim das Palmeiras',
|
|
55
|
+
city: 'Bragança Paulista',
|
|
56
|
+
uf: 'SP',
|
|
57
|
+
latitude: null,
|
|
58
|
+
longitude: null,
|
|
59
|
+
}),
|
|
60
|
+
).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('isGeocodeMiss false when Nominatim returns coords', () => {
|
|
64
|
+
expect(
|
|
65
|
+
isGeocodeMiss({
|
|
66
|
+
street: 'Rua Antônio Bonini',
|
|
67
|
+
city: 'Atibaia',
|
|
68
|
+
uf: 'SP',
|
|
69
|
+
latitude: -23.12,
|
|
70
|
+
longitude: -46.55,
|
|
71
|
+
}),
|
|
72
|
+
).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('mergePostalCodeData clears previous coords when lookup returns null', () => {
|
|
76
|
+
const prev = {
|
|
77
|
+
street: 'Old',
|
|
78
|
+
latitude: -23.1,
|
|
79
|
+
longitude: -46.5,
|
|
80
|
+
number: '10',
|
|
81
|
+
complement: '',
|
|
82
|
+
nickname: 'X',
|
|
83
|
+
cep: '12941040',
|
|
84
|
+
};
|
|
85
|
+
const next = mergePostalCodeData(prev, {
|
|
86
|
+
cep: '12924022',
|
|
87
|
+
street: 'Rua Lotus',
|
|
88
|
+
city: 'Bragança Paulista',
|
|
89
|
+
uf: 'SP',
|
|
90
|
+
latitude: null,
|
|
91
|
+
longitude: null,
|
|
92
|
+
});
|
|
93
|
+
expect(next.street).toBe('Rua Lotus');
|
|
94
|
+
expect(next.latitude).toBeNull();
|
|
95
|
+
expect(next.longitude).toBeNull();
|
|
96
|
+
expect(next.number).toBe('10');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('parseOptionalCoordinate accepts comma decimals', () => {
|
|
100
|
+
expect(parseOptionalCoordinate('-23,5505')).toBeCloseTo(-23.5505);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('GEOCODE_MISS_MESSAGE is PT', () => {
|
|
104
|
+
expect(GEOCODE_MISS_MESSAGE).toMatch(/localização no mapa/);
|
|
105
|
+
expect(GEOCODE_MISS_MESSAGE).toMatch(/latitude e longitude/);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -12,11 +12,19 @@ jest.mock('@controleonline/ui-common/src/react/utils/storeColumns', () => ({
|
|
|
12
12
|
|
|
13
13
|
const {
|
|
14
14
|
buildReadPresentationStyles,
|
|
15
|
+
isIconColumn,
|
|
15
16
|
mapOptions,
|
|
16
17
|
resolveCellPresentation,
|
|
17
18
|
} = require('../../../react/components/inputs/defaultInputUtils');
|
|
18
|
-
|
|
19
|
+
|
|
19
20
|
describe('defaultInputUtils', () => {
|
|
21
|
+
it('identifies icon columns by input type or field name', () => {
|
|
22
|
+
expect(isIconColumn({inputType: 'icon', name: 'symbol'})).toBe(true);
|
|
23
|
+
expect(isIconColumn({type: 'ICON', name: 'symbol'})).toBe(true);
|
|
24
|
+
expect(isIconColumn({name: 'icon'})).toBe(true);
|
|
25
|
+
expect(isIconColumn({name: 'status'})).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
|
|
20
28
|
it('deduplicates list options by normalized key', () => {
|
|
21
29
|
const column = {
|
|
22
30
|
formatList(item) {
|
|
@@ -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`).
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import {
|
|
2
|
+
mergePostalCodeData,
|
|
3
|
+
clearPostalCodeDerivedFields,
|
|
4
|
+
onlyDigits,
|
|
5
|
+
hydrateAddressFromRow,
|
|
6
|
+
hasCoordinates,
|
|
7
|
+
parseOptionalCoordinate,
|
|
8
|
+
formatCepMask,
|
|
9
|
+
normalizeCepDigits,
|
|
10
|
+
} from '../../../react/services/addressFormUtils';
|
|
11
|
+
|
|
12
|
+
describe('addressFormUtils', () => {
|
|
13
|
+
test('onlyDigits strips non-digits and keeps order', () => {
|
|
14
|
+
expect(onlyDigits('01310-100')).toBe('01310100');
|
|
15
|
+
expect(onlyDigits('abc')).toBe('');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('mergePostalCodeData fills empty fields from API', () => {
|
|
19
|
+
const prev = {
|
|
20
|
+
nickname: 'Casa',
|
|
21
|
+
number: '100',
|
|
22
|
+
complement: 'Apto 1',
|
|
23
|
+
cep: '',
|
|
24
|
+
street: '',
|
|
25
|
+
district: '',
|
|
26
|
+
city: '',
|
|
27
|
+
uf: '',
|
|
28
|
+
stateName: '',
|
|
29
|
+
countryCode: 'BR',
|
|
30
|
+
countryName: 'Brazil',
|
|
31
|
+
latitude: null,
|
|
32
|
+
longitude: null,
|
|
33
|
+
};
|
|
34
|
+
const data = {
|
|
35
|
+
cep: '01310100',
|
|
36
|
+
street: 'Av Paulista',
|
|
37
|
+
district: 'Bela Vista',
|
|
38
|
+
city: 'São Paulo',
|
|
39
|
+
uf: 'SP',
|
|
40
|
+
state: 'São Paulo',
|
|
41
|
+
country: 'Brasil',
|
|
42
|
+
latitude: -23.5,
|
|
43
|
+
longitude: -46.6,
|
|
44
|
+
};
|
|
45
|
+
const next = mergePostalCodeData(prev, data);
|
|
46
|
+
expect(next.street).toBe('Av Paulista');
|
|
47
|
+
expect(next.district).toBe('Bela Vista');
|
|
48
|
+
expect(next.city).toBe('São Paulo');
|
|
49
|
+
expect(next.uf).toBe('SP');
|
|
50
|
+
expect(next.cep).toBe('01310100');
|
|
51
|
+
expect(next.number).toBe(''); // cleared on CEP lookup (#746)
|
|
52
|
+
expect(next.complement).toBe('Apto 1');
|
|
53
|
+
expect(next.nickname).toBe('Casa');
|
|
54
|
+
expect(next.countryCode).toBe('BR');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('mergePostalCodeData clears number but keeps complement/nickname (#746)', () => {
|
|
58
|
+
const prev = {
|
|
59
|
+
nickname: 'Trabalho',
|
|
60
|
+
number: '42',
|
|
61
|
+
complement: 'Sala 3',
|
|
62
|
+
street: 'Rua X',
|
|
63
|
+
district: 'Centro',
|
|
64
|
+
city: 'Rio',
|
|
65
|
+
uf: 'RJ',
|
|
66
|
+
cep: '20000000',
|
|
67
|
+
countryCode: 'BR',
|
|
68
|
+
countryName: 'Brazil',
|
|
69
|
+
};
|
|
70
|
+
const data = {
|
|
71
|
+
cep: '01310100',
|
|
72
|
+
street: 'Av Paulista',
|
|
73
|
+
district: 'Bela Vista',
|
|
74
|
+
city: 'São Paulo',
|
|
75
|
+
uf: 'SP',
|
|
76
|
+
country: 'Brazil',
|
|
77
|
+
};
|
|
78
|
+
const next = mergePostalCodeData(prev, data);
|
|
79
|
+
expect(next.number).toBe('');
|
|
80
|
+
expect(next.complement).toBe('Sala 3');
|
|
81
|
+
expect(next.nickname).toBe('Trabalho');
|
|
82
|
+
expect(next.street).toBe('Av Paulista');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('preserveFilledFields keeps existing street/city', () => {
|
|
86
|
+
const prev = {
|
|
87
|
+
nickname: '',
|
|
88
|
+
number: '1',
|
|
89
|
+
complement: '',
|
|
90
|
+
street: 'Minha Rua',
|
|
91
|
+
district: '',
|
|
92
|
+
city: 'Minha Cidade',
|
|
93
|
+
uf: '',
|
|
94
|
+
cep: '01310100',
|
|
95
|
+
countryCode: 'BR',
|
|
96
|
+
countryName: 'Brazil',
|
|
97
|
+
};
|
|
98
|
+
const data = {
|
|
99
|
+
street: 'API Street',
|
|
100
|
+
district: 'API Dist',
|
|
101
|
+
city: 'API City',
|
|
102
|
+
uf: 'SP',
|
|
103
|
+
cep: '01310100',
|
|
104
|
+
};
|
|
105
|
+
const next = mergePostalCodeData(prev, data, {preserveFilledFields: true});
|
|
106
|
+
expect(next.street).toBe('Minha Rua');
|
|
107
|
+
expect(next.city).toBe('Minha Cidade');
|
|
108
|
+
expect(next.district).toBe('API Dist');
|
|
109
|
+
expect(next.uf).toBe('SP');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('hydrateAddressFromRow maps nested street entities', () => {
|
|
113
|
+
const row = {
|
|
114
|
+
nickname: 'HQ',
|
|
115
|
+
number: 10,
|
|
116
|
+
complement: 'Bloco B',
|
|
117
|
+
street: {
|
|
118
|
+
street: 'Rua Nested',
|
|
119
|
+
district: {
|
|
120
|
+
district: 'Centro',
|
|
121
|
+
city: {
|
|
122
|
+
city: 'Campinas',
|
|
123
|
+
state: {uf: 'SP', state: 'São Paulo', country: {countrycode: 'BR', countryname: 'Brazil'}},
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
cep: {cep: '13000000'},
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
const form = hydrateAddressFromRow(row);
|
|
130
|
+
expect(form.street).toBe('Rua Nested');
|
|
131
|
+
expect(form.district).toBe('Centro');
|
|
132
|
+
expect(form.city).toBe('Campinas');
|
|
133
|
+
expect(form.uf).toBe('SP');
|
|
134
|
+
expect(form.cep).toBe('13000000');
|
|
135
|
+
expect(form.number).toBe('10');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('hasCoordinates requires finite lat/lng', () => {
|
|
139
|
+
expect(hasCoordinates({latitude: -23, longitude: -46})).toBe(true);
|
|
140
|
+
expect(hasCoordinates({latitude: null, longitude: -46})).toBe(false);
|
|
141
|
+
expect(hasCoordinates({latitude: 0, longitude: 0})).toBe(false);
|
|
142
|
+
expect(hasCoordinates({latitude: '0', longitude: '0'})).toBe(false);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('parseOptionalCoordinate returns null for empty/invalid', () => {
|
|
146
|
+
expect(parseOptionalCoordinate('')).toBeNull();
|
|
147
|
+
expect(parseOptionalCoordinate(' ')).toBeNull();
|
|
148
|
+
expect(parseOptionalCoordinate(null)).toBeNull();
|
|
149
|
+
expect(parseOptionalCoordinate(undefined)).toBeNull();
|
|
150
|
+
expect(parseOptionalCoordinate('abc')).toBeNull();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('parseOptionalCoordinate parses valid decimal coordinates', () => {
|
|
154
|
+
expect(parseOptionalCoordinate('-23.5505')).toBeCloseTo(-23.5505);
|
|
155
|
+
expect(parseOptionalCoordinate('-46.6333')).toBeCloseTo(-46.6333);
|
|
156
|
+
expect(parseOptionalCoordinate('0')).toBe(0);
|
|
157
|
+
expect(parseOptionalCoordinate(' 12.5 ')).toBeCloseTo(12.5);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe('formatCepMask / normalizeCepDigits', () => {
|
|
162
|
+
test('normalizeCepDigits caps at 8 digits', () => {
|
|
163
|
+
expect(normalizeCepDigits('013101001')).toBe('01310100');
|
|
164
|
+
expect(normalizeCepDigits('01310-1001')).toBe('01310100');
|
|
165
|
+
expect(normalizeCepDigits('01310-100')).toBe('01310100');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('formatCepMask applies #####-###', () => {
|
|
169
|
+
expect(formatCepMask('01310')).toBe('01310');
|
|
170
|
+
expect(formatCepMask('013101')).toBe('01310-1');
|
|
171
|
+
expect(formatCepMask('01310100')).toBe('01310-100');
|
|
172
|
+
expect(formatCepMask('013101001')).toBe('01310-100');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('lookup gate: only 8 digits', () => {
|
|
176
|
+
expect(onlyDigits(formatCepMask('01310-10')).length).toBe(7);
|
|
177
|
+
expect(onlyDigits(formatCepMask('01310-100')).length).toBe(8);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/* global describe, expect, test, beforeEach */
|
|
2
|
+
|
|
3
|
+
jest.mock('@controleonline/ui-common/src/api', () => ({
|
|
4
|
+
api: {
|
|
5
|
+
fetch: jest.fn(),
|
|
6
|
+
},
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
const {api} = require('@controleonline/ui-common/src/api');
|
|
10
|
+
const {lookupPostalCode} = require('../../../react/services/addressGeo');
|
|
11
|
+
const {mergePostalCodeData} = require('../../../react/services/addressFormUtils');
|
|
12
|
+
|
|
13
|
+
const CEP = '12941040';
|
|
14
|
+
|
|
15
|
+
const API_PROD_INCOMPLETE = {
|
|
16
|
+
cep: CEP,
|
|
17
|
+
street: '',
|
|
18
|
+
district: '',
|
|
19
|
+
city: '',
|
|
20
|
+
state: '',
|
|
21
|
+
uf: '',
|
|
22
|
+
country: 'Brasil',
|
|
23
|
+
latitude: -23.117082,
|
|
24
|
+
longitude: -46.5425915,
|
|
25
|
+
provider: 'viacep+googlemaps',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
describe('lookupPostalCode #637 (prod payload + client fallback)', () => {
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
api.fetch.mockReset();
|
|
31
|
+
global.fetch = jest.fn();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('API 200 with empty street/city is completed by ViaCEP; coords stay from API', async () => {
|
|
35
|
+
api.fetch.mockResolvedValue(API_PROD_INCOMPLETE);
|
|
36
|
+
global.fetch.mockResolvedValue({
|
|
37
|
+
ok: true,
|
|
38
|
+
json: async () => ({
|
|
39
|
+
erro: false,
|
|
40
|
+
logradouro: 'Rua Antônio Bonini',
|
|
41
|
+
bairro: 'Vila Santista',
|
|
42
|
+
localidade: 'Atibaia',
|
|
43
|
+
uf: 'SP',
|
|
44
|
+
}),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const payload = await lookupPostalCode(CEP);
|
|
48
|
+
|
|
49
|
+
expect(api.fetch).toHaveBeenCalledWith(`postal-codes/${CEP}`, {
|
|
50
|
+
method: 'GET',
|
|
51
|
+
});
|
|
52
|
+
expect(payload.street).toBe('Rua Antônio Bonini');
|
|
53
|
+
expect(payload.city).toBe('Atibaia');
|
|
54
|
+
expect(payload.uf).toBe('SP');
|
|
55
|
+
expect(payload.district).toBe('Vila Santista');
|
|
56
|
+
expect(Number(payload.latitude)).toBeCloseTo(-23.117, 2);
|
|
57
|
+
expect(Number(payload.longitude)).toBeCloseTo(-46.543, 2);
|
|
58
|
+
expect(String(payload.provider)).toContain('viacep-client');
|
|
59
|
+
|
|
60
|
+
const form = mergePostalCodeData(
|
|
61
|
+
{
|
|
62
|
+
nickname: '',
|
|
63
|
+
number: '',
|
|
64
|
+
complement: '',
|
|
65
|
+
cep: CEP,
|
|
66
|
+
street: '',
|
|
67
|
+
district: '',
|
|
68
|
+
city: '',
|
|
69
|
+
uf: '',
|
|
70
|
+
countryCode: 'BR',
|
|
71
|
+
latitude: null,
|
|
72
|
+
longitude: null,
|
|
73
|
+
},
|
|
74
|
+
payload,
|
|
75
|
+
);
|
|
76
|
+
expect(form.street).toBe('Rua Antônio Bonini');
|
|
77
|
+
expect(form.city).toBe('Atibaia');
|
|
78
|
+
expect(form.uf).toBe('SP');
|
|
79
|
+
expect(String(form.latitude)).not.toBe('');
|
|
80
|
+
expect(String(form.latitude)).not.toBe('—');
|
|
81
|
+
expect(String(form.longitude)).not.toBe('—');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('does not throw Postalcode services are not available when ViaCEP fills text', async () => {
|
|
85
|
+
api.fetch.mockRejectedValue({
|
|
86
|
+
message: 'Postalcode services are not available',
|
|
87
|
+
response: {data: {detail: 'Postalcode services are not available'}},
|
|
88
|
+
});
|
|
89
|
+
global.fetch.mockResolvedValue({
|
|
90
|
+
ok: true,
|
|
91
|
+
json: async () => ({
|
|
92
|
+
logradouro: 'Rua Antônio Bonini',
|
|
93
|
+
bairro: 'Vila Santista',
|
|
94
|
+
localidade: 'Atibaia',
|
|
95
|
+
uf: 'SP',
|
|
96
|
+
}),
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const payload = await lookupPostalCode(CEP);
|
|
100
|
+
expect(payload.street).toBe('Rua Antônio Bonini');
|
|
101
|
+
expect(payload.city).toBe('Atibaia');
|
|
102
|
+
expect(payload.uf).toBe('SP');
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isPostalLookupIncomplete,
|
|
3
|
+
isValidCoord,
|
|
4
|
+
mergePostalLookupPayload,
|
|
5
|
+
} from '../../../react/services/addressGeo';
|
|
6
|
+
import {mergePostalCodeData} from '../../../react/services/addressFormUtils';
|
|
7
|
+
|
|
8
|
+
describe('addressGeo CEP fallback helpers', () => {
|
|
9
|
+
test('isValidCoord rejects empty and 0,0 defaults', () => {
|
|
10
|
+
expect(isValidCoord(null)).toBe(false);
|
|
11
|
+
expect(isValidCoord(0)).toBe(false);
|
|
12
|
+
expect(isValidCoord(-23.117)).toBe(true);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('incomplete when API returns coords without street/city', () => {
|
|
16
|
+
expect(
|
|
17
|
+
isPostalLookupIncomplete({
|
|
18
|
+
street: '',
|
|
19
|
+
city: '',
|
|
20
|
+
latitude: -23.117082,
|
|
21
|
+
longitude: -46.5425915,
|
|
22
|
+
provider: 'viacep+googlemaps',
|
|
23
|
+
}),
|
|
24
|
+
).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('merge fills ViaCEP text and keeps API coords', () => {
|
|
28
|
+
const merged = mergePostalLookupPayload(
|
|
29
|
+
{
|
|
30
|
+
street: '',
|
|
31
|
+
city: '',
|
|
32
|
+
latitude: -23.117082,
|
|
33
|
+
longitude: -46.5425915,
|
|
34
|
+
provider: 'viacep+googlemaps',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
street: 'Rua Antônio Bonini',
|
|
38
|
+
district: 'Vila Santista',
|
|
39
|
+
city: 'Atibaia',
|
|
40
|
+
uf: 'SP',
|
|
41
|
+
country: 'Brasil',
|
|
42
|
+
},
|
|
43
|
+
'viacep-client',
|
|
44
|
+
);
|
|
45
|
+
expect(merged.street).toBe('Rua Antônio Bonini');
|
|
46
|
+
expect(merged.city).toBe('Atibaia');
|
|
47
|
+
expect(merged.uf).toBe('SP');
|
|
48
|
+
expect(merged.latitude).toBe(-23.117082);
|
|
49
|
+
expect(merged.provider).toBe('viacep+googlemaps+viacep-client');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('#637 form hydrate: API empty street + ViaCEP + coords → Atibaia fields', () => {
|
|
53
|
+
const apiProd = {
|
|
54
|
+
cep: '12941040',
|
|
55
|
+
street: '',
|
|
56
|
+
district: '',
|
|
57
|
+
city: '',
|
|
58
|
+
state: '',
|
|
59
|
+
uf: '',
|
|
60
|
+
country: 'Brasil',
|
|
61
|
+
latitude: -23.117082,
|
|
62
|
+
longitude: -46.5425915,
|
|
63
|
+
provider: 'viacep+googlemaps',
|
|
64
|
+
};
|
|
65
|
+
expect(isPostalLookupIncomplete(apiProd)).toBe(true);
|
|
66
|
+
|
|
67
|
+
const afterViaCep = mergePostalLookupPayload(
|
|
68
|
+
apiProd,
|
|
69
|
+
{
|
|
70
|
+
street: 'Rua Antônio Bonini',
|
|
71
|
+
district: 'Vila Santista',
|
|
72
|
+
city: 'Atibaia',
|
|
73
|
+
uf: 'SP',
|
|
74
|
+
country: 'Brasil',
|
|
75
|
+
provider: 'viacep-client',
|
|
76
|
+
},
|
|
77
|
+
'viacep-client',
|
|
78
|
+
);
|
|
79
|
+
expect(isPostalLookupIncomplete(afterViaCep)).toBe(false);
|
|
80
|
+
|
|
81
|
+
const form = mergePostalCodeData(
|
|
82
|
+
{
|
|
83
|
+
nickname: '',
|
|
84
|
+
number: '',
|
|
85
|
+
complement: '',
|
|
86
|
+
cep: '12941040',
|
|
87
|
+
street: '',
|
|
88
|
+
district: '',
|
|
89
|
+
city: '',
|
|
90
|
+
uf: '',
|
|
91
|
+
countryCode: 'BR',
|
|
92
|
+
latitude: null,
|
|
93
|
+
longitude: null,
|
|
94
|
+
},
|
|
95
|
+
afterViaCep,
|
|
96
|
+
);
|
|
97
|
+
expect(form.street).toBe('Rua Antônio Bonini');
|
|
98
|
+
expect(form.city).toBe('Atibaia');
|
|
99
|
+
expect(form.uf).toBe('SP');
|
|
100
|
+
expect(Number(form.latitude)).toBeCloseTo(-23.117, 2);
|
|
101
|
+
expect(Number(form.longitude)).toBeCloseTo(-46.543, 2);
|
|
102
|
+
expect(form.street).not.toBe('—');
|
|
103
|
+
expect(form.city).not.toBe('—');
|
|
104
|
+
});
|
|
105
|
+
});
|
|
Binary file
|