@controleonline/ui-customers 1.0.81 → 1.0.83
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/publish-npm.yml +91 -0
- package/AGENTS.md +12 -6
- package/docs/technical/Client-Details-Criar-Usuario.md +113 -0
- package/package.json +5 -4
- package/src/react/components/tabs/AddressesTab.js +240 -335
- package/src/react/components/tabs/DocumentAttachments.js +270 -0
- package/src/react/components/tabs/DocumentsTab.js +355 -463
- package/src/react/components/tabs/DocumentsTab.styles.js +123 -0
- package/src/react/components/tabs/EmployeeCreateFormFields.js +203 -0
- package/src/react/components/tabs/EmployeesTab.js +17 -171
- package/src/react/components/tabs/EmployeesTab.styles.js +220 -172
- package/src/react/components/tabs/GeneralTab.js +84 -46
- package/src/react/components/tabs/GeneralTab.styles.js +81 -77
- package/src/react/components/tabs/MediaTab.js +50 -5
- package/src/react/components/tabs/PeopleCategoriesPanel.js +161 -78
- package/src/react/components/tabs/SalesmanTab.js +1 -1
- package/src/react/components/tabs/UserFormModal.js +26 -2
- package/src/react/components/tabs/UsersTab.js +30 -19
- package/src/react/components/tabs/UsersTab.styles.js +257 -257
- package/src/react/components/tabs/UsersTabApiKeyModal.js +115 -0
- package/src/react/components/tabs/UsersTabUserModal.js +222 -0
- package/src/react/components/tabs/addressesTabHelpers.js +190 -0
- package/src/react/components/tabs/documentsTabHelpers.js +142 -0
- package/src/react/components/tabs/employeeContacts.js +3 -23
- package/src/react/components/tabs/employeeUnlink.js +7 -4
- package/src/react/components/tabs/employeesTabHelpers.js +33 -9
- package/src/react/components/tabs/generalTabHelpers.js +30 -8
- package/src/react/components/tabs/useGeneralTabPeopleLink.js +68 -0
- package/src/react/components/tabs/usersTabHelpers.js +24 -23
- package/src/react/pages/details.page.styles.js +114 -100
- package/src/react/pages/employees.js +4 -0
- package/src/react/utils/humanCompanyLinkTypes.js +28 -0
- package/src/tests/browser/manager/client-details-add-user-modal.spec.js +281 -0
- package/src/tests/browser/manager/client-details-address-create-coords.spec.js +414 -0
- package/src/tests/browser/manager/client-details-address-edit.spec.js +331 -0
- package/src/tests/browser/manager/client-details-contact-edit.spec.js +198 -0
- package/src/tests/browser/manager/client-details-create-contact.spec.js +306 -0
- package/src/tests/browser/manager/client-details-documents.spec.js +346 -0
- package/src/tests/browser/manager/client-details-identity-case.spec.js +227 -0
- package/src/tests/browser/manager/client-details-people-type.manifest.md +18 -0
- package/src/tests/browser/manager/client-details-people-type.spec.js +345 -0
- package/src/tests/browser/manager/client-details-supplier-product-create.spec.js +334 -0
- package/src/tests/browser/manager/client-details-users-tab-load.spec.js +194 -0
- package/src/tests/browser/manager/employees-salesman-after-sales.spec.js +441 -0
- package/src/tests/react/components/SalesmanTab.company.test.js +40 -0
- package/src/tests/react/components/tabs/UsersTab.styles.test.js +39 -0
- package/src/tests/react/components/tabs/addressesTabHelpers.test.js +78 -0
- package/src/tests/react/components/tabs/documentsTabContract.test.mjs +37 -0
- package/src/tests/react/components/tabs/documentsTabHelpers.test.mjs +93 -0
- package/src/tests/react/components/tabs/employeeContacts.test.js +46 -4
- package/src/tests/react/components/tabs/employeeLinkTypes.contract.test.mjs +52 -0
- package/src/tests/react/components/tabs/employeeUnlink.test.js +11 -11
- package/src/tests/react/components/tabs/employeesTabHelpers.media.test.js +2 -23
- package/src/tests/react/components/tabs/employeesTabHelpers.test.js +59 -0
- package/src/tests/react/components/tabs/generalTabHelpers.test.js +35 -0
- package/src/tests/react/components/tabs/peopleMediaTabs.test.js +217 -217
- package/src/tests/react/components/tabs/peopleType.contract.test.mjs +47 -0
- package/src/tests/react/components/tabs/resolveSeededLinkType.test.mjs +20 -0
- package/src/tests/react/components/tabs/salesmanTabMedia.no-redeclare.test.mjs +33 -0
- package/src/tests/react/components/tabs/usersTabHelpers.test.js +20 -0
- package/src/tests/react/components/tabs/usersTabHelpers.test.mjs +63 -0
- package/src/tests/react/pages/employees.test.js +2 -0
- package/src/tests/react/utils/humanCompanyLinkTypes.test.js +50 -0
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser smoke — app-community#458
|
|
3
|
+
* Modal "Adicionar Usuário" must show a visible Salvar button (not only Cancelar).
|
|
4
|
+
* Regression: task-428 left primary footer style as text-only → invisible on white modal.
|
|
5
|
+
*/
|
|
6
|
+
const {expect, test} = require('playwright/test');
|
|
7
|
+
const {API_ORIGIN} = require('../../../../../../../src/tests/browser/apiOrigin');
|
|
8
|
+
const {version: appVersion} = require('../../../../../../../package.json');
|
|
9
|
+
|
|
10
|
+
const CORS_HEADERS = {
|
|
11
|
+
'access-control-allow-origin': '*',
|
|
12
|
+
'access-control-allow-headers':
|
|
13
|
+
'API-TOKEN, APP-DOMAIN, DEVICE, ACCEPT, CONTENT-TYPE, X-Requested-With',
|
|
14
|
+
'access-control-allow-methods': 'GET,POST,PUT,PATCH,DELETE,OPTIONS',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const collection = (member = [], summary = {}) => ({
|
|
18
|
+
member,
|
|
19
|
+
'hydra:member': member,
|
|
20
|
+
totalItems: member.length,
|
|
21
|
+
'hydra:totalItems': member.length,
|
|
22
|
+
summary,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const jsonHeaders = () => ({
|
|
26
|
+
...CORS_HEADERS,
|
|
27
|
+
'content-type': 'application/ld+json; charset=utf-8',
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const textHeaders = () => ({
|
|
31
|
+
...CORS_HEADERS,
|
|
32
|
+
'content-type': 'text/css; charset=utf-8',
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const createCompany = () => ({
|
|
36
|
+
id: 3,
|
|
37
|
+
name: 'Controle Online',
|
|
38
|
+
alias: 'CONTROLE ONLINE',
|
|
39
|
+
panel_enabled: true,
|
|
40
|
+
enabled: true,
|
|
41
|
+
commercial_enabled: true,
|
|
42
|
+
theme: {
|
|
43
|
+
colors: {
|
|
44
|
+
primary: '#0EA5E9',
|
|
45
|
+
secondary: '#F97316',
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
configs: {},
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const createPerson = () => ({
|
|
52
|
+
'@id': '/people/25',
|
|
53
|
+
id: 25,
|
|
54
|
+
name: 'CLAUDIO MOSES FELTRIN MEDEIROS',
|
|
55
|
+
alias: 'CLAUDIO',
|
|
56
|
+
peopleType: 'F',
|
|
57
|
+
enable: true,
|
|
58
|
+
user: [],
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const TIMEZONES = [
|
|
62
|
+
{'@id': '/timezones/1', id: 1, name: 'America/Sao_Paulo', enabled: true},
|
|
63
|
+
{'@id': '/timezones/2', id: 2, name: 'America/Campo_Grande', enabled: true},
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
const mockClientDetailsAddUserApi = async page => {
|
|
67
|
+
const company = createCompany();
|
|
68
|
+
const person = createPerson();
|
|
69
|
+
const createdUsers = [];
|
|
70
|
+
|
|
71
|
+
await page.route(`${API_ORIGIN}/**`, async route => {
|
|
72
|
+
const request = route.request();
|
|
73
|
+
const url = new URL(request.url());
|
|
74
|
+
const pathname = url.pathname.replace(/^\/+/, '');
|
|
75
|
+
const method = request.method().toUpperCase();
|
|
76
|
+
|
|
77
|
+
if (method === 'OPTIONS') {
|
|
78
|
+
return route.fulfill({status: 204, headers: CORS_HEADERS, body: ''});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (pathname === 'themes-colors.css') {
|
|
82
|
+
return route.fulfill({
|
|
83
|
+
status: 200,
|
|
84
|
+
headers: textHeaders(),
|
|
85
|
+
body: ':root { --primary: #0ea5e9; --secondary: #f97316; }',
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (pathname === 'runtime/ip') {
|
|
90
|
+
return route.fulfill({
|
|
91
|
+
status: 200,
|
|
92
|
+
headers: jsonHeaders(),
|
|
93
|
+
body: JSON.stringify({ip: '127.0.0.1'}),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (pathname === 'people/company/default') {
|
|
98
|
+
return route.fulfill({
|
|
99
|
+
status: 200,
|
|
100
|
+
headers: jsonHeaders(),
|
|
101
|
+
body: JSON.stringify(company),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (pathname === 'people/companies/my') {
|
|
106
|
+
return route.fulfill({
|
|
107
|
+
status: 200,
|
|
108
|
+
headers: jsonHeaders(),
|
|
109
|
+
body: JSON.stringify(collection([company])),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (pathname === 'people/25' || pathname === 'people/25/') {
|
|
114
|
+
return route.fulfill({
|
|
115
|
+
status: 200,
|
|
116
|
+
headers: jsonHeaders(),
|
|
117
|
+
body: JSON.stringify(person),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (pathname === 'timezones' || pathname.startsWith('timezones')) {
|
|
122
|
+
return route.fulfill({
|
|
123
|
+
status: 200,
|
|
124
|
+
headers: jsonHeaders(),
|
|
125
|
+
body: JSON.stringify(collection(TIMEZONES)),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (pathname === 'users' || pathname.startsWith('users')) {
|
|
130
|
+
if (method === 'POST') {
|
|
131
|
+
let body = {};
|
|
132
|
+
try {
|
|
133
|
+
body = request.postDataJSON() || {};
|
|
134
|
+
} catch {
|
|
135
|
+
body = {};
|
|
136
|
+
}
|
|
137
|
+
const created = {
|
|
138
|
+
'@id': `/users/${100 + createdUsers.length}`,
|
|
139
|
+
id: 100 + createdUsers.length,
|
|
140
|
+
username: body.username || body.name || 'newuser',
|
|
141
|
+
role: body.role || 'ROLE_USER',
|
|
142
|
+
timezone: body.timezone || '/timezones/1',
|
|
143
|
+
};
|
|
144
|
+
createdUsers.push(created);
|
|
145
|
+
return route.fulfill({
|
|
146
|
+
status: 201,
|
|
147
|
+
headers: jsonHeaders(),
|
|
148
|
+
body: JSON.stringify(created),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
return route.fulfill({
|
|
152
|
+
status: 200,
|
|
153
|
+
headers: jsonHeaders(),
|
|
154
|
+
body: JSON.stringify(collection(createdUsers)),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (pathname === 'people_links' || pathname.startsWith('people_links')) {
|
|
159
|
+
return route.fulfill({
|
|
160
|
+
status: 200,
|
|
161
|
+
headers: jsonHeaders(),
|
|
162
|
+
body: JSON.stringify(collection([])),
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (pathname === 'menus-people') {
|
|
167
|
+
return route.fulfill({
|
|
168
|
+
status: 200,
|
|
169
|
+
headers: jsonHeaders(),
|
|
170
|
+
body: JSON.stringify({modules: {}}),
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (pathname === 'configs/discovery-configs') {
|
|
175
|
+
return route.fulfill({
|
|
176
|
+
status: 200,
|
|
177
|
+
headers: jsonHeaders(),
|
|
178
|
+
body: JSON.stringify({configs: {}}),
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return route.fulfill({
|
|
183
|
+
status: 200,
|
|
184
|
+
headers: jsonHeaders(),
|
|
185
|
+
body: JSON.stringify(collection([])),
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
await page.addInitScript(
|
|
190
|
+
({appVersion}) => {
|
|
191
|
+
const setLocalStorageItem = (key, value) => {
|
|
192
|
+
try {
|
|
193
|
+
localStorage.setItem(key, value);
|
|
194
|
+
} catch {
|
|
195
|
+
// ignore
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
setLocalStorageItem(
|
|
200
|
+
'session',
|
|
201
|
+
JSON.stringify({
|
|
202
|
+
id: 7,
|
|
203
|
+
people: '/people/7',
|
|
204
|
+
api_key: 'test-api-key',
|
|
205
|
+
active: 1,
|
|
206
|
+
mycompany: 3,
|
|
207
|
+
roles: ['ROLE_SUPER'],
|
|
208
|
+
}),
|
|
209
|
+
);
|
|
210
|
+
setLocalStorageItem('config', JSON.stringify({language: 'pt-br'}));
|
|
211
|
+
setLocalStorageItem('app-type', 'MANAGER');
|
|
212
|
+
setLocalStorageItem(
|
|
213
|
+
'device',
|
|
214
|
+
JSON.stringify({
|
|
215
|
+
id: 'web-manager',
|
|
216
|
+
device: 'web-manager',
|
|
217
|
+
type: 'WEB',
|
|
218
|
+
appName: 'Browser Manager',
|
|
219
|
+
appVersion,
|
|
220
|
+
buildNumber: appVersion,
|
|
221
|
+
systemName: 'web',
|
|
222
|
+
systemVersion: 'web',
|
|
223
|
+
deviceType: 'web',
|
|
224
|
+
metadata: {},
|
|
225
|
+
}),
|
|
226
|
+
);
|
|
227
|
+
},
|
|
228
|
+
{appVersion},
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
return {getCreatedUsers: () => createdUsers};
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
test.describe('client-details Adicionar Usuário modal (#458)', () => {
|
|
235
|
+
test('shows visible Salvar button in Adicionar Usuário modal', async ({page}) => {
|
|
236
|
+
await mockClientDetailsAddUserApi(page);
|
|
237
|
+
|
|
238
|
+
await page.goto(
|
|
239
|
+
'/client-details?clientId=25&contextKey=client&initialTab=users',
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
await expect(page.getByText('Usuários', {exact: true}).first()).toBeVisible({
|
|
243
|
+
timeout: 20000,
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
const addBtn = page
|
|
247
|
+
.getByLabel('Adicionar usuário')
|
|
248
|
+
.or(page.getByTestId('users-tab-add-user'))
|
|
249
|
+
.first();
|
|
250
|
+
await expect(addBtn).toBeVisible({timeout: 10000});
|
|
251
|
+
await addBtn.click();
|
|
252
|
+
|
|
253
|
+
await expect(page.getByText('Adicionar Usuário', {exact: true})).toBeVisible({
|
|
254
|
+
timeout: 10000,
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
await expect(page.getByText('Cancelar', {exact: true})).toBeVisible();
|
|
258
|
+
|
|
259
|
+
const salvar = page
|
|
260
|
+
.getByLabel('Salvar')
|
|
261
|
+
.or(page.getByTestId('user-form-save'))
|
|
262
|
+
.or(page.getByText('Salvar', {exact: true}))
|
|
263
|
+
.first();
|
|
264
|
+
await expect(salvar).toBeVisible({timeout: 5000});
|
|
265
|
+
|
|
266
|
+
const box = await salvar.boundingBox();
|
|
267
|
+
expect(box).toBeTruthy();
|
|
268
|
+
expect(box.width).toBeGreaterThan(20);
|
|
269
|
+
expect(box.height).toBeGreaterThan(10);
|
|
270
|
+
|
|
271
|
+
// Timezone list present (must not push footer off-screen)
|
|
272
|
+
await expect(
|
|
273
|
+
page.getByText(/America\/Sao_Paulo|Timezone|timezone/i).first(),
|
|
274
|
+
).toBeVisible({timeout: 10000}).catch(() => {});
|
|
275
|
+
|
|
276
|
+
await page.getByText('Cancelar', {exact: true}).click();
|
|
277
|
+
await expect(page.getByText('Adicionar Usuário', {exact: true})).toHaveCount(0, {
|
|
278
|
+
timeout: 8000,
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
});
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser smoke for app-community#283 / ui-customers task-283:
|
|
3
|
+
* Manager → ClientDetails (franchise) → AddressesTab → create address
|
|
4
|
+
* - CEP lookup returns latitude/longitude (Google Maps enrichment path)
|
|
5
|
+
* - optional manual Latitude/Longitude fields are visible and persist on save
|
|
6
|
+
*/
|
|
7
|
+
const {expect, test} = require('playwright/test');
|
|
8
|
+
const {API_ORIGIN} = require('../../../../../../../src/tests/browser/apiOrigin');
|
|
9
|
+
const {version: appVersion} = require('../../../../../../../package.json');
|
|
10
|
+
|
|
11
|
+
const CORS_HEADERS = {
|
|
12
|
+
'access-control-allow-origin': '*',
|
|
13
|
+
'access-control-allow-headers':
|
|
14
|
+
'API-TOKEN, APP-DOMAIN, DEVICE, ACCEPT, CONTENT-TYPE, X-Requested-With',
|
|
15
|
+
'access-control-allow-methods': 'GET,POST,PUT,PATCH,DELETE,OPTIONS',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const collection = (member = [], summary = {}) => ({
|
|
19
|
+
member,
|
|
20
|
+
'hydra:member': member,
|
|
21
|
+
totalItems: member.length,
|
|
22
|
+
'hydra:totalItems': member.length,
|
|
23
|
+
summary,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const jsonHeaders = () => ({
|
|
27
|
+
...CORS_HEADERS,
|
|
28
|
+
'content-type': 'application/ld+json; charset=utf-8',
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const textHeaders = () => ({
|
|
32
|
+
...CORS_HEADERS,
|
|
33
|
+
'content-type': 'text/css; charset=utf-8',
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const createCompany = () => ({
|
|
37
|
+
id: 3,
|
|
38
|
+
name: 'Controle Online',
|
|
39
|
+
alias: 'CONTROLE ONLINE',
|
|
40
|
+
panel_enabled: true,
|
|
41
|
+
enabled: true,
|
|
42
|
+
commercial_enabled: true,
|
|
43
|
+
theme: {
|
|
44
|
+
colors: {
|
|
45
|
+
primary: '#0EA5E9',
|
|
46
|
+
secondary: '#F97316',
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
configs: {},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
/** CEP response with coords (simulates Postmon/ViaCEP empty + GMaps enrichment). */
|
|
53
|
+
const CEP_WITH_COORDS = {
|
|
54
|
+
cep: '01310100',
|
|
55
|
+
street: 'Avenida Paulista',
|
|
56
|
+
district: 'Bela Vista',
|
|
57
|
+
city: 'São Paulo',
|
|
58
|
+
uf: 'SP',
|
|
59
|
+
state: 'São Paulo',
|
|
60
|
+
country: 'BR',
|
|
61
|
+
countryCode: 'BR',
|
|
62
|
+
latitude: -23.561414,
|
|
63
|
+
longitude: -46.655881,
|
|
64
|
+
provider: 'googlemaps',
|
|
65
|
+
map: {staticUrl: 'https://example.com/static-map'},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const mockAddressCreateCoordsApi = async page => {
|
|
69
|
+
const company = createCompany();
|
|
70
|
+
const addressWriteMethods = [];
|
|
71
|
+
const addressWriteBodies = [];
|
|
72
|
+
const postalLookups = [];
|
|
73
|
+
let savedAddresses = [];
|
|
74
|
+
|
|
75
|
+
await page.route(`${API_ORIGIN}/**`, async route => {
|
|
76
|
+
const request = route.request();
|
|
77
|
+
const url = new URL(request.url());
|
|
78
|
+
const pathname = url.pathname.replace(/^\/+/, '');
|
|
79
|
+
const method = request.method().toUpperCase();
|
|
80
|
+
|
|
81
|
+
if (method === 'OPTIONS') {
|
|
82
|
+
return route.fulfill({status: 204, headers: CORS_HEADERS, body: ''});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (pathname === 'themes-colors.css') {
|
|
86
|
+
return route.fulfill({
|
|
87
|
+
status: 200,
|
|
88
|
+
headers: textHeaders(),
|
|
89
|
+
body: ':root { --primary: #0ea5e9; --secondary: #f97316; }',
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (pathname === 'runtime/ip') {
|
|
94
|
+
return route.fulfill({
|
|
95
|
+
status: 200,
|
|
96
|
+
headers: jsonHeaders(),
|
|
97
|
+
body: JSON.stringify({ip: '127.0.0.1'}),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (pathname === 'people/company/default') {
|
|
102
|
+
return route.fulfill({
|
|
103
|
+
status: 200,
|
|
104
|
+
headers: jsonHeaders(),
|
|
105
|
+
body: JSON.stringify(company),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (pathname === 'people/companies/my') {
|
|
110
|
+
return route.fulfill({
|
|
111
|
+
status: 200,
|
|
112
|
+
headers: jsonHeaders(),
|
|
113
|
+
body: JSON.stringify(collection([company])),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Franchise / client WITHOUT existing address (create path)
|
|
118
|
+
if (pathname === 'people/50' || pathname === 'people/50/') {
|
|
119
|
+
return route.fulfill({
|
|
120
|
+
status: 200,
|
|
121
|
+
headers: jsonHeaders(),
|
|
122
|
+
body: JSON.stringify({
|
|
123
|
+
id: 50,
|
|
124
|
+
'@id': '/people/50',
|
|
125
|
+
name: 'Franquia Teste LTDA',
|
|
126
|
+
alias: 'FRANQUIA TESTE',
|
|
127
|
+
enabled: true,
|
|
128
|
+
address: savedAddresses,
|
|
129
|
+
people: {id: 50, '@id': '/people/50'},
|
|
130
|
+
}),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (pathname.startsWith('people/') && pathname.includes('/address')) {
|
|
135
|
+
if (method === 'GET') {
|
|
136
|
+
return route.fulfill({
|
|
137
|
+
status: 200,
|
|
138
|
+
headers: jsonHeaders(),
|
|
139
|
+
body: JSON.stringify(collection(savedAddresses)),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// postal-codes/{cep} — backend GMaps enrichment path for #283
|
|
145
|
+
if (pathname.startsWith('postal-codes/')) {
|
|
146
|
+
postalLookups.push({pathname, method});
|
|
147
|
+
return route.fulfill({
|
|
148
|
+
status: 200,
|
|
149
|
+
headers: jsonHeaders(),
|
|
150
|
+
body: JSON.stringify(CEP_WITH_COORDS),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (pathname === 'address-geo/countries' || pathname.startsWith('address-geo/')) {
|
|
155
|
+
return route.fulfill({
|
|
156
|
+
status: 200,
|
|
157
|
+
headers: jsonHeaders(),
|
|
158
|
+
body: JSON.stringify(
|
|
159
|
+
collection([
|
|
160
|
+
{id: 1, countrycode: 'BR', countryname: 'Brazil', code: 'BR', name: 'Brazil'},
|
|
161
|
+
]),
|
|
162
|
+
),
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (pathname === 'addresses' || pathname === 'addresses/' || pathname.startsWith('addresses')) {
|
|
167
|
+
if (method === 'POST' || method === 'PUT' || method === 'PATCH') {
|
|
168
|
+
let body = {};
|
|
169
|
+
try {
|
|
170
|
+
body = request.postDataJSON() || {};
|
|
171
|
+
} catch {
|
|
172
|
+
body = {};
|
|
173
|
+
}
|
|
174
|
+
addressWriteMethods.push(method);
|
|
175
|
+
addressWriteBodies.push(body);
|
|
176
|
+
const id = body.id || 99;
|
|
177
|
+
const saved = {
|
|
178
|
+
id,
|
|
179
|
+
'@id': `/addresses/${id}`,
|
|
180
|
+
street: {
|
|
181
|
+
street: body.street || CEP_WITH_COORDS.street,
|
|
182
|
+
district: {
|
|
183
|
+
district: body.district || CEP_WITH_COORDS.district,
|
|
184
|
+
city: {
|
|
185
|
+
city: body.city || CEP_WITH_COORDS.city,
|
|
186
|
+
state: {
|
|
187
|
+
uf: body.state || body.uf || 'SP',
|
|
188
|
+
country: {countrycode: body.country || 'BR'},
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
cep: {cep: body.cep || CEP_WITH_COORDS.cep},
|
|
193
|
+
},
|
|
194
|
+
number: body.number || '',
|
|
195
|
+
complement: body.complement || '',
|
|
196
|
+
nickname: body.nickname || '',
|
|
197
|
+
latitude: body.latitude,
|
|
198
|
+
longitude: body.longitude,
|
|
199
|
+
};
|
|
200
|
+
savedAddresses = [saved];
|
|
201
|
+
return route.fulfill({
|
|
202
|
+
status: 200,
|
|
203
|
+
headers: jsonHeaders(),
|
|
204
|
+
body: JSON.stringify(saved),
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
if (method === 'GET') {
|
|
208
|
+
return route.fulfill({
|
|
209
|
+
status: 200,
|
|
210
|
+
headers: jsonHeaders(),
|
|
211
|
+
body: JSON.stringify(collection(savedAddresses)),
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Default soft OK for other API calls
|
|
217
|
+
return route.fulfill({
|
|
218
|
+
status: 200,
|
|
219
|
+
headers: jsonHeaders(),
|
|
220
|
+
body: JSON.stringify(collection([])),
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
getAddressWriteMethods: () => addressWriteMethods.slice(),
|
|
226
|
+
getAddressWriteBodies: () => addressWriteBodies.slice(),
|
|
227
|
+
getPostalLookups: () => postalLookups.slice(),
|
|
228
|
+
};
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
test.describe('franchise address create coords browser smoke (#283)', () => {
|
|
232
|
+
test.beforeEach(async ({page}) => {
|
|
233
|
+
await page.addInitScript(
|
|
234
|
+
({appVersion}) => {
|
|
235
|
+
const set = (k, v) => {
|
|
236
|
+
try {
|
|
237
|
+
localStorage.setItem(k, v);
|
|
238
|
+
} catch {}
|
|
239
|
+
};
|
|
240
|
+
set(
|
|
241
|
+
'session',
|
|
242
|
+
JSON.stringify({
|
|
243
|
+
id: 7,
|
|
244
|
+
people: '/people/7',
|
|
245
|
+
api_key: 'test',
|
|
246
|
+
active: 1,
|
|
247
|
+
mycompany: 3,
|
|
248
|
+
roles: ['ROLE_ADMIN'],
|
|
249
|
+
}),
|
|
250
|
+
);
|
|
251
|
+
set('config', JSON.stringify({language: 'pt-br'}));
|
|
252
|
+
set('app-type', 'ERP');
|
|
253
|
+
set(
|
|
254
|
+
'device',
|
|
255
|
+
JSON.stringify({
|
|
256
|
+
id: 'web',
|
|
257
|
+
device: 'web',
|
|
258
|
+
type: 'WEB',
|
|
259
|
+
appVersion,
|
|
260
|
+
buildNumber: appVersion,
|
|
261
|
+
}),
|
|
262
|
+
);
|
|
263
|
+
},
|
|
264
|
+
{appVersion},
|
|
265
|
+
);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test('creates address with CEP coords and optional manual lat/long', async ({page}) => {
|
|
269
|
+
const api = await mockAddressCreateCoordsApi(page);
|
|
270
|
+
|
|
271
|
+
await page.goto('/client-details?clientId=50&contextKey=client');
|
|
272
|
+
|
|
273
|
+
await expect(
|
|
274
|
+
page.getByText(/Franquia Teste|Dados Cadastrais|Endere/i).first(),
|
|
275
|
+
).toBeVisible({timeout: 20000});
|
|
276
|
+
|
|
277
|
+
const addressesTab = page.getByText(/Endereços|Addresses|endereco/i).first();
|
|
278
|
+
if (await addressesTab.isVisible().catch(() => false)) {
|
|
279
|
+
await addressesTab.click();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Enter edit mode if needed so "add address" is available
|
|
283
|
+
const editModeToggle = page.getByText(/Editar|Edit|Alterar/i).first();
|
|
284
|
+
if (await editModeToggle.isVisible().catch(() => false)) {
|
|
285
|
+
await editModeToggle.click();
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Open create address (empty list → add button)
|
|
289
|
+
const addBtn = page
|
|
290
|
+
.getByRole('button', {name: /Adicionar|Novo|Add|Criar/i})
|
|
291
|
+
.or(page.getByText(/Adicionar endere|Novo endere|Add address/i))
|
|
292
|
+
.first();
|
|
293
|
+
if (await addBtn.isVisible({timeout: 8000}).catch(() => false)) {
|
|
294
|
+
await addBtn.click();
|
|
295
|
+
} else {
|
|
296
|
+
// Fallback: plus / add icon in addresses area
|
|
297
|
+
await page
|
|
298
|
+
.locator('[data-testid="address-add"], [class*="iconButton"], button')
|
|
299
|
+
.filter({has: page.locator('svg, [class*="plus"], [name*="plus"]')})
|
|
300
|
+
.first()
|
|
301
|
+
.click({timeout: 5000})
|
|
302
|
+
.catch(() => {});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Form should show optional lat/long fields (DefaultAddress #283)
|
|
306
|
+
const latLabel = page.getByText(/Latitude/i).first();
|
|
307
|
+
const lngLabel = page.getByText(/Longitude/i).first();
|
|
308
|
+
await expect(latLabel).toBeVisible({timeout: 15000});
|
|
309
|
+
await expect(lngLabel).toBeVisible({timeout: 5000});
|
|
310
|
+
|
|
311
|
+
// Fill CEP and blur to trigger postal-codes lookup (GMaps enrichment)
|
|
312
|
+
const cepInput = page
|
|
313
|
+
.getByPlaceholder(/CEP|cep|postal/i)
|
|
314
|
+
.or(page.locator('input[name*="cep"], input[id*="cep"]'))
|
|
315
|
+
.first();
|
|
316
|
+
if (await cepInput.isVisible({timeout: 5000}).catch(() => false)) {
|
|
317
|
+
await cepInput.fill('01310-100');
|
|
318
|
+
await cepInput.blur();
|
|
319
|
+
} else {
|
|
320
|
+
// RN web often uses unlabeled TextInput — fill first numeric-looking field in modal
|
|
321
|
+
const inputs = page.locator('input');
|
|
322
|
+
const count = await inputs.count();
|
|
323
|
+
for (let i = 0; i < count; i++) {
|
|
324
|
+
const input = inputs.nth(i);
|
|
325
|
+
const ph = (await input.getAttribute('placeholder')) || '';
|
|
326
|
+
const kb = (await input.getAttribute('inputmode')) || '';
|
|
327
|
+
if (/cep|postal/i.test(ph) || kb === 'numeric') {
|
|
328
|
+
await input.fill('01310100');
|
|
329
|
+
await input.blur();
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Allow lookup to complete
|
|
336
|
+
await page.waitForTimeout(800);
|
|
337
|
+
await expect.poll(() => api.getPostalLookups().length, {timeout: 10000}).toBeGreaterThan(0);
|
|
338
|
+
|
|
339
|
+
// Manual optional coords: set explicit values (overrides / confirms path)
|
|
340
|
+
const latInput = page
|
|
341
|
+
.locator('input')
|
|
342
|
+
.filter({has: page.locator('xpath=ancestor::*[contains(., "Latitude")]')})
|
|
343
|
+
.first();
|
|
344
|
+
// Fallback fill by visible labels proximity
|
|
345
|
+
const allInputs = page.locator('input');
|
|
346
|
+
const inputCount = await allInputs.count();
|
|
347
|
+
let filledManual = false;
|
|
348
|
+
for (let i = 0; i < inputCount; i++) {
|
|
349
|
+
const input = allInputs.nth(i);
|
|
350
|
+
const val = await input.inputValue().catch(() => '');
|
|
351
|
+
// After CEP merge, lat may already be filled; ensure we can edit
|
|
352
|
+
const nearLat = await input.evaluate(el => {
|
|
353
|
+
const root = el.closest('div,label,section') || el.parentElement;
|
|
354
|
+
return /latitude/i.test((root && root.textContent) || '');
|
|
355
|
+
}).catch(() => false);
|
|
356
|
+
if (nearLat) {
|
|
357
|
+
await input.fill('-23.55');
|
|
358
|
+
filledManual = true;
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
if (val && /^-?\d+\.\d+/.test(val) && Number(val) < 0 && Number(val) > -30) {
|
|
362
|
+
// likely already auto-filled latitude from CEP
|
|
363
|
+
filledManual = true;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
// If structure is flat, still try setting last two decimal inputs
|
|
367
|
+
if (!filledManual && inputCount >= 2) {
|
|
368
|
+
await allInputs.nth(inputCount - 2).fill('-23.55').catch(() => {});
|
|
369
|
+
await allInputs.nth(inputCount - 1).fill('-46.63').catch(() => {});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Street/number if still empty after CEP
|
|
373
|
+
const numberInput = page
|
|
374
|
+
.getByPlaceholder(/número|number|N[úu]mero/i)
|
|
375
|
+
.or(page.locator('input[name*="number"], input[id*="number"]'))
|
|
376
|
+
.first();
|
|
377
|
+
if (await numberInput.isVisible({timeout: 2000}).catch(() => false)) {
|
|
378
|
+
const current = await numberInput.inputValue().catch(() => '');
|
|
379
|
+
if (!current) await numberInput.fill('1578');
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Save
|
|
383
|
+
const saveBtn = page.getByRole('button', {name: /Salvar|Save|Gravar/i}).first();
|
|
384
|
+
if (await saveBtn.isVisible({timeout: 5000}).catch(() => false)) {
|
|
385
|
+
await saveBtn.click();
|
|
386
|
+
} else {
|
|
387
|
+
await page.getByText(/Salvar|Save Changes|Gravar/i).first().click({timeout: 5000}).catch(() => {});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Assert create write happened with coordinates
|
|
391
|
+
await expect
|
|
392
|
+
.poll(() => api.getAddressWriteMethods().length, {timeout: 12000})
|
|
393
|
+
.toBeGreaterThan(0);
|
|
394
|
+
|
|
395
|
+
const methods = api.getAddressWriteMethods();
|
|
396
|
+
expect(methods.some(m => m === 'POST' || m === 'PUT' || m === 'PATCH')).toBe(true);
|
|
397
|
+
|
|
398
|
+
const bodies = api.getAddressWriteBodies();
|
|
399
|
+
expect(bodies.length).toBeGreaterThan(0);
|
|
400
|
+
const body = bodies[bodies.length - 1];
|
|
401
|
+
const lat = Number(body.latitude);
|
|
402
|
+
const lng = Number(body.longitude);
|
|
403
|
+
expect(Number.isFinite(lat)).toBe(true);
|
|
404
|
+
expect(Number.isFinite(lng)).toBe(true);
|
|
405
|
+
// Either auto from CEP (Paulista) or manual override
|
|
406
|
+
expect(lat).toBeLessThan(0);
|
|
407
|
+
expect(lng).toBeLessThan(0);
|
|
408
|
+
|
|
409
|
+
// UI should show created address without crash
|
|
410
|
+
await expect(
|
|
411
|
+
page.getByText(/Paulista|Franquia|Endere|1578|-23/i).first(),
|
|
412
|
+
).toBeVisible({timeout: 10000}).catch(() => {});
|
|
413
|
+
});
|
|
414
|
+
});
|