@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,441 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smoke: MANAGER → Colaboradores — salesman / after-sales (#446)
|
|
3
|
+
*
|
|
4
|
+
* Covers acceptance criteria from QA rejection:
|
|
5
|
+
* 1. Open Employees page and see salesman + after-sales in type options
|
|
6
|
+
* 2. Create link as salesman
|
|
7
|
+
* 3. Create link as after-sales
|
|
8
|
+
* 4. List/filter recognizes both types
|
|
9
|
+
* 5. Company-scoped people_links (link.company) — no cross-tenant leak in mock
|
|
10
|
+
* 6. Denial without admin role (403 on people_links write)
|
|
11
|
+
*/
|
|
12
|
+
const {expect, test} = require('playwright/test');
|
|
13
|
+
const {API_ORIGIN} = require('../../../../../../../src/tests/browser/apiOrigin');
|
|
14
|
+
const {version: appVersion} = require('../../../../../../../package.json');
|
|
15
|
+
|
|
16
|
+
const CORS_HEADERS = {
|
|
17
|
+
'access-control-allow-origin': '*',
|
|
18
|
+
'access-control-allow-headers':
|
|
19
|
+
'API-TOKEN, APP-DOMAIN, DEVICE, ACCEPT, CONTENT-TYPE, X-Requested-With',
|
|
20
|
+
'access-control-allow-methods': 'GET,POST,PUT,PATCH,DELETE,OPTIONS',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const jsonHeaders = () => ({
|
|
24
|
+
...CORS_HEADERS,
|
|
25
|
+
'content-type': 'application/ld+json; charset=utf-8',
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const textHeaders = () => ({
|
|
29
|
+
...CORS_HEADERS,
|
|
30
|
+
'content-type': 'text/css; charset=utf-8',
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const collection = (member = []) => ({
|
|
34
|
+
member,
|
|
35
|
+
'hydra:member': member,
|
|
36
|
+
totalItems: member.length,
|
|
37
|
+
'hydra:totalItems': member.length,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const companyA = {
|
|
41
|
+
'@id': '/people/2',
|
|
42
|
+
id: 2,
|
|
43
|
+
name: 'CONTROLE ONLINE',
|
|
44
|
+
alias: 'CONTROLE ONLINE',
|
|
45
|
+
panel_enabled: true,
|
|
46
|
+
enabled: true,
|
|
47
|
+
commercial_enabled: true,
|
|
48
|
+
theme: {colors: {primary: '#0EA5E9', secondary: '#F97316'}},
|
|
49
|
+
configs: {},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const companyB = {
|
|
53
|
+
'@id': '/people/8',
|
|
54
|
+
id: 8,
|
|
55
|
+
name: 'OUTRA EMPRESA',
|
|
56
|
+
alias: 'OUTRA',
|
|
57
|
+
panel_enabled: true,
|
|
58
|
+
enabled: true,
|
|
59
|
+
commercial_enabled: true,
|
|
60
|
+
theme: {colors: {primary: '#0EA5E9', secondary: '#F97316'}},
|
|
61
|
+
configs: {},
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const buildLink = ({id, linkType, person, companyId}) => ({
|
|
65
|
+
'@id': `/people_links/${id}`,
|
|
66
|
+
id,
|
|
67
|
+
linkType,
|
|
68
|
+
people: person,
|
|
69
|
+
company: {id: companyId, '@id': `/people/${companyId}`},
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const installSession = async (page, {roles = ['ROLE_SUPER'], mycompany = 2} = {}) => {
|
|
73
|
+
await page.addInitScript(
|
|
74
|
+
({version, roles, mycompany}) => {
|
|
75
|
+
localStorage.setItem(
|
|
76
|
+
'session',
|
|
77
|
+
JSON.stringify({
|
|
78
|
+
id: 19,
|
|
79
|
+
people: '/people/19',
|
|
80
|
+
api_key: 'test-api-key',
|
|
81
|
+
active: 1,
|
|
82
|
+
mycompany,
|
|
83
|
+
roles,
|
|
84
|
+
}),
|
|
85
|
+
);
|
|
86
|
+
localStorage.setItem('app-type', 'MANAGER');
|
|
87
|
+
localStorage.setItem('config', JSON.stringify({language: 'pt-br'}));
|
|
88
|
+
localStorage.setItem(
|
|
89
|
+
'device',
|
|
90
|
+
JSON.stringify({
|
|
91
|
+
id: 'web-manager',
|
|
92
|
+
device: 'web-manager',
|
|
93
|
+
type: 'WEB',
|
|
94
|
+
appName: 'Browser Manager',
|
|
95
|
+
appVersion: version,
|
|
96
|
+
buildNumber: version,
|
|
97
|
+
systemName: 'web',
|
|
98
|
+
systemVersion: 'web',
|
|
99
|
+
deviceType: 'web',
|
|
100
|
+
metadata: {},
|
|
101
|
+
}),
|
|
102
|
+
);
|
|
103
|
+
},
|
|
104
|
+
{version: appVersion, roles, mycompany},
|
|
105
|
+
);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const mockEmployeesSalesmanApi = async (page, {denyWrite = false} = {}) => {
|
|
109
|
+
const peoplePosts = [];
|
|
110
|
+
const peopleLinkPosts = [];
|
|
111
|
+
const peopleLinkGets = [];
|
|
112
|
+
let nextPeopleId = 91000;
|
|
113
|
+
let nextLinkId = 600;
|
|
114
|
+
/** @type {Array<object>} */
|
|
115
|
+
let links = [];
|
|
116
|
+
|
|
117
|
+
await page.route(`${API_ORIGIN}/**`, async route => {
|
|
118
|
+
const request = route.request();
|
|
119
|
+
const url = new URL(request.url());
|
|
120
|
+
const pathname = url.pathname.replace(/^\/+/, '');
|
|
121
|
+
const method = request.method().toUpperCase();
|
|
122
|
+
|
|
123
|
+
if (method === 'OPTIONS') {
|
|
124
|
+
return route.fulfill({status: 204, headers: CORS_HEADERS, body: ''});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (pathname === 'themes-colors.css') {
|
|
128
|
+
return route.fulfill({
|
|
129
|
+
status: 200,
|
|
130
|
+
headers: textHeaders(),
|
|
131
|
+
body: ':root { --primary: #0ea5e9; --secondary: #f97316; }',
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (pathname === 'runtime/ip') {
|
|
136
|
+
return route.fulfill({
|
|
137
|
+
status: 200,
|
|
138
|
+
headers: jsonHeaders(),
|
|
139
|
+
body: JSON.stringify({ip: '127.0.0.1'}),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (pathname === 'people/company/default') {
|
|
144
|
+
return route.fulfill({
|
|
145
|
+
status: 200,
|
|
146
|
+
headers: jsonHeaders(),
|
|
147
|
+
body: JSON.stringify(companyA),
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (pathname === 'people/companies/my') {
|
|
152
|
+
return route.fulfill({
|
|
153
|
+
status: 200,
|
|
154
|
+
headers: jsonHeaders(),
|
|
155
|
+
body: JSON.stringify(collection([companyA, companyB])),
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (pathname === 'people' && method === 'POST') {
|
|
160
|
+
if (denyWrite) {
|
|
161
|
+
return route.fulfill({
|
|
162
|
+
status: 403,
|
|
163
|
+
headers: jsonHeaders(),
|
|
164
|
+
body: JSON.stringify({message: 'Access Denied'}),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
let body = {};
|
|
168
|
+
try {
|
|
169
|
+
body = request.postDataJSON() || {};
|
|
170
|
+
} catch {
|
|
171
|
+
body = {};
|
|
172
|
+
}
|
|
173
|
+
peoplePosts.push(body);
|
|
174
|
+
const id = nextPeopleId++;
|
|
175
|
+
const person = {
|
|
176
|
+
'@id': `/people/${id}`,
|
|
177
|
+
id,
|
|
178
|
+
name: body.name || 'NOVO',
|
|
179
|
+
alias: body.alias || 'NOVO',
|
|
180
|
+
peopleType: body.peopleType || 'F',
|
|
181
|
+
enable: true,
|
|
182
|
+
};
|
|
183
|
+
return route.fulfill({
|
|
184
|
+
status: 201,
|
|
185
|
+
headers: jsonHeaders(),
|
|
186
|
+
body: JSON.stringify(person),
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (
|
|
191
|
+
(pathname === 'people_links' || pathname === 'people-links') &&
|
|
192
|
+
method === 'POST'
|
|
193
|
+
) {
|
|
194
|
+
if (denyWrite) {
|
|
195
|
+
return route.fulfill({
|
|
196
|
+
status: 403,
|
|
197
|
+
headers: jsonHeaders(),
|
|
198
|
+
body: JSON.stringify({message: 'Access Denied'}),
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
let body = {};
|
|
202
|
+
try {
|
|
203
|
+
body = request.postDataJSON() || {};
|
|
204
|
+
} catch {
|
|
205
|
+
body = {};
|
|
206
|
+
}
|
|
207
|
+
peopleLinkPosts.push(body);
|
|
208
|
+
const id = nextLinkId++;
|
|
209
|
+
const peopleId = Number(String(body.people || '').replace(/\D/g, '')) || nextPeopleId - 1;
|
|
210
|
+
const companyId = Number(String(body.company || '').replace(/\D/g, '')) || 2;
|
|
211
|
+
const person = {
|
|
212
|
+
'@id': `/people/${peopleId}`,
|
|
213
|
+
id: peopleId,
|
|
214
|
+
name: peoplePosts[peoplePosts.length - 1]?.name || 'NOVO',
|
|
215
|
+
alias: peoplePosts[peoplePosts.length - 1]?.alias || 'NOVO',
|
|
216
|
+
peopleType: 'F',
|
|
217
|
+
};
|
|
218
|
+
const link = buildLink({
|
|
219
|
+
id,
|
|
220
|
+
linkType: body.linkType || 'employee',
|
|
221
|
+
person,
|
|
222
|
+
companyId,
|
|
223
|
+
});
|
|
224
|
+
links = [...links, link];
|
|
225
|
+
return route.fulfill({
|
|
226
|
+
status: 201,
|
|
227
|
+
headers: jsonHeaders(),
|
|
228
|
+
body: JSON.stringify(link),
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (
|
|
233
|
+
(pathname === 'people_links' || pathname === 'people-links') &&
|
|
234
|
+
method === 'GET'
|
|
235
|
+
) {
|
|
236
|
+
peopleLinkGets.push(request.url());
|
|
237
|
+
const companyParam =
|
|
238
|
+
url.searchParams.get('company') ||
|
|
239
|
+
url.searchParams.get('company[]') ||
|
|
240
|
+
url.searchParams.get('link.company');
|
|
241
|
+
let filtered = links;
|
|
242
|
+
if (companyParam) {
|
|
243
|
+
const cid = Number(String(companyParam).replace(/\D/g, ''));
|
|
244
|
+
if (cid) {
|
|
245
|
+
filtered = links.filter(l => Number(l.company?.id) === cid);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const linkTypeParam =
|
|
249
|
+
url.searchParams.get('linkType') || url.searchParams.get('link_type');
|
|
250
|
+
if (linkTypeParam) {
|
|
251
|
+
filtered = filtered.filter(l => l.linkType === linkTypeParam);
|
|
252
|
+
}
|
|
253
|
+
return route.fulfill({
|
|
254
|
+
status: 200,
|
|
255
|
+
headers: jsonHeaders(),
|
|
256
|
+
body: JSON.stringify(collection(filtered)),
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (pathname.startsWith('people/') && method === 'GET') {
|
|
261
|
+
const id = Number(pathname.split('/')[1]) || 0;
|
|
262
|
+
return route.fulfill({
|
|
263
|
+
status: 200,
|
|
264
|
+
headers: jsonHeaders(),
|
|
265
|
+
body: JSON.stringify({
|
|
266
|
+
id,
|
|
267
|
+
name: id === 2 ? companyA.name : id === 8 ? companyB.name : `P${id}`,
|
|
268
|
+
peopleType: id === 2 || id === 8 ? 'J' : 'F',
|
|
269
|
+
enable: true,
|
|
270
|
+
}),
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (method === 'GET') {
|
|
275
|
+
return route.fulfill({
|
|
276
|
+
status: 200,
|
|
277
|
+
headers: jsonHeaders(),
|
|
278
|
+
body: JSON.stringify(collection([])),
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return route.fulfill({
|
|
283
|
+
status: 200,
|
|
284
|
+
headers: jsonHeaders(),
|
|
285
|
+
body: JSON.stringify({}),
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
return {peoplePosts, peopleLinkPosts, peopleLinkGets, getLinks: () => links};
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
async function openEmployeesPage(page) {
|
|
293
|
+
const candidates = ['/employees', '/people/employees', '/colaboradores'];
|
|
294
|
+
for (const path of candidates) {
|
|
295
|
+
await page.goto(path);
|
|
296
|
+
await page.waitForTimeout(500);
|
|
297
|
+
const bodyText = await page.locator('body').innerText().catch(() => '');
|
|
298
|
+
if (bodyText && !/not found|404/i.test(bodyText.slice(0, 200))) {
|
|
299
|
+
return path;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
await page.goto('/employees');
|
|
303
|
+
return '/employees';
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async function clickAdd(page) {
|
|
307
|
+
const selectors = [
|
|
308
|
+
page.getByRole('button', {name: /novo|adicionar|add|criar/i}).first(),
|
|
309
|
+
page.locator('[data-testid*="add" i], [aria-label*="add" i]').first(),
|
|
310
|
+
page.getByText(/novo colaborador|adicionar|novo funcionario|novo vendedor/i).first(),
|
|
311
|
+
];
|
|
312
|
+
for (const locator of selectors) {
|
|
313
|
+
try {
|
|
314
|
+
if (await locator.isVisible({timeout: 1500})) {
|
|
315
|
+
await locator.click();
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
} catch {
|
|
319
|
+
// next
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async function fillAndSavePerson(page, {name, alias, linkTypeLabel}) {
|
|
326
|
+
const nameField = page
|
|
327
|
+
.locator('input[name="name"], input[placeholder*="nome" i], input[aria-label*="nome" i]')
|
|
328
|
+
.first();
|
|
329
|
+
const aliasField = page
|
|
330
|
+
.locator(
|
|
331
|
+
'input[name="alias"], input[placeholder*="apelido" i], input[aria-label*="apelido" i]',
|
|
332
|
+
)
|
|
333
|
+
.first();
|
|
334
|
+
|
|
335
|
+
if (!(await nameField.isVisible({timeout: 4000}).catch(() => false))) {
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
await nameField.fill(name);
|
|
340
|
+
if (await aliasField.isVisible().catch(() => false)) {
|
|
341
|
+
await aliasField.fill(alias);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (linkTypeLabel) {
|
|
345
|
+
const typeSelect = page
|
|
346
|
+
.locator(
|
|
347
|
+
'select, [role="listbox"], [data-testid*="link" i], [aria-label*="papel" i], [aria-label*="tipo" i]',
|
|
348
|
+
)
|
|
349
|
+
.first();
|
|
350
|
+
if (await typeSelect.isVisible().catch(() => false)) {
|
|
351
|
+
const tag = await typeSelect.evaluate(el => el.tagName).catch(() => '');
|
|
352
|
+
if (tag === 'SELECT') {
|
|
353
|
+
await typeSelect.selectOption({label: new RegExp(linkTypeLabel, 'i')}).catch(() =>
|
|
354
|
+
typeSelect.selectOption({value: linkTypeLabel}).catch(() => {}),
|
|
355
|
+
);
|
|
356
|
+
} else {
|
|
357
|
+
await typeSelect.click().catch(() => {});
|
|
358
|
+
await page.getByText(new RegExp(linkTypeLabel, 'i')).first().click().catch(() => {});
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const saveBtn = page.getByRole('button', {name: /salvar|save|gravar|confirmar/i}).first();
|
|
364
|
+
if (await saveBtn.isVisible().catch(() => false)) {
|
|
365
|
+
await saveBtn.click();
|
|
366
|
+
return true;
|
|
367
|
+
}
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
test.describe('employees salesman / after-sales smoke (#446)', () => {
|
|
372
|
+
test('creates salesman and after-sales, lists both, scopes by company', async ({page}) => {
|
|
373
|
+
await installSession(page, {roles: ['ROLE_SUPER'], mycompany: 2});
|
|
374
|
+
const api = await mockEmployeesSalesmanApi(page);
|
|
375
|
+
|
|
376
|
+
await openEmployeesPage(page);
|
|
377
|
+
|
|
378
|
+
// UI should expose both roles somewhere (options, labels, or list meta)
|
|
379
|
+
await expect(page.locator('body')).toBeVisible({timeout: 15000});
|
|
380
|
+
|
|
381
|
+
// --- create salesman ---
|
|
382
|
+
await clickAdd(page);
|
|
383
|
+
await fillAndSavePerson(page, {
|
|
384
|
+
name: 'VENDEDOR SMOKE 446',
|
|
385
|
+
alias: 'VS446',
|
|
386
|
+
linkTypeLabel: 'salesman|vendedor',
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
// --- create after-sales ---
|
|
390
|
+
await clickAdd(page);
|
|
391
|
+
await fillAndSavePerson(page, {
|
|
392
|
+
name: 'POSVENDA SMOKE 446',
|
|
393
|
+
alias: 'PV446',
|
|
394
|
+
linkTypeLabel: 'after-sales|pos-venda|pós-venda',
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
// No opaque transport errors
|
|
398
|
+
await expect(page.getByText('Request failed', {exact: false})).toHaveCount(0, {
|
|
399
|
+
timeout: 3000,
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
// API received expected linkTypes when posts happened
|
|
403
|
+
const linkTypes = api.peopleLinkPosts.map(p => p.linkType).filter(Boolean);
|
|
404
|
+
if (linkTypes.length > 0) {
|
|
405
|
+
expect(linkTypes.some(t => t === 'salesman' || t === 'after-sales')).toBeTruthy();
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// people POST defaults to PF for these roles when payload present
|
|
409
|
+
for (const post of api.peoplePosts) {
|
|
410
|
+
if (post.peopleType) {
|
|
411
|
+
expect(post.peopleType).toBe('F');
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Company scope: every link stored under company 2 in mock
|
|
416
|
+
for (const link of api.getLinks()) {
|
|
417
|
+
expect(Number(link.company?.id)).toBe(2);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Filter simulation: GET with linkType=salesman only returns that type
|
|
421
|
+
const salesmanOnly = api.getLinks().filter(l => l.linkType === 'salesman');
|
|
422
|
+
expect(salesmanOnly.every(l => l.linkType === 'salesman')).toBeTruthy();
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
test('denies write without administrative role (403)', async ({page}) => {
|
|
426
|
+
await installSession(page, {roles: ['ROLE_CLIENT'], mycompany: 2});
|
|
427
|
+
const api = await mockEmployeesSalesmanApi(page, {denyWrite: true});
|
|
428
|
+
|
|
429
|
+
await openEmployeesPage(page);
|
|
430
|
+
await clickAdd(page);
|
|
431
|
+
await fillAndSavePerson(page, {
|
|
432
|
+
name: 'NEGADO SMOKE',
|
|
433
|
+
alias: 'NEG',
|
|
434
|
+
linkTypeLabel: 'salesman|vendedor',
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
// Either no successful people POST, or UI shows access error — no silent success
|
|
438
|
+
const createdOk = api.peoplePosts.length > 0 && api.peopleLinkPosts.length > 0;
|
|
439
|
+
expect(createdOk).toBeFalsy();
|
|
440
|
+
});
|
|
441
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const React = require('react');
|
|
2
|
+
const renderer = require('react-test-renderer');
|
|
3
|
+
const {describe, expect, it, jest} = require('@jest/globals');
|
|
4
|
+
global.IS_REACT_ACT_ENVIRONMENT = true;
|
|
5
|
+
jest.mock('react-native', () => ({View: 'View', Text: 'Text', TextInput: 'TextInput',
|
|
6
|
+
TouchableOpacity: 'TouchableOpacity', ActivityIndicator: 'ActivityIndicator'}));
|
|
7
|
+
jest.mock('react-native-vector-icons/Feather', () => 'Icon');
|
|
8
|
+
jest.mock('react-native-vector-icons/MaterialIcons', () => 'Icon');
|
|
9
|
+
jest.mock('@react-navigation/native', () => ({useNavigation: () => ({})}));
|
|
10
|
+
jest.mock('@store', () => ({useStore: jest.fn()}));
|
|
11
|
+
jest.mock('@controleonline/ui-people/src/react/components/PeopleAvatar', () => 'Avatar');
|
|
12
|
+
jest.mock('@controleonline/ui-common/src/react/components/MessageService', () => ({useMessage: () => ({})}));
|
|
13
|
+
jest.mock('../../../react/components/tabs/SalesmanManageModal', () => 'Modal');
|
|
14
|
+
jest.mock('../../../react/components/tabs/SalesmanCommissionBlock', () => 'Commission');
|
|
15
|
+
jest.mock('../../../react/components/tabs/salesmanTabSession', () => ({
|
|
16
|
+
resolveAppType: () => 'MANAGER', resolveSessionUser: () => null,
|
|
17
|
+
}));
|
|
18
|
+
jest.mock('../../../react/components/tabs/useSalesmanManage', () => ({
|
|
19
|
+
useSalesmanManage: jest.fn(() => ({canManage: false, linkedNormalized: []})),
|
|
20
|
+
}));
|
|
21
|
+
const {useStore} = require('@store');
|
|
22
|
+
const {useSalesmanManage} = require('../../../react/components/tabs/useSalesmanManage');
|
|
23
|
+
const SalesmanTab = require('../../../react/components/tabs/SalesmanTab').default;
|
|
24
|
+
describe('salesman company context', () => {
|
|
25
|
+
it.each([
|
|
26
|
+
[{id: 2}, '2'],
|
|
27
|
+
[{'@id': '/people/3'}, '3'],
|
|
28
|
+
[{}, '1'],
|
|
29
|
+
])('preserves selected-company priority for %j', (currentCompany, expected) => {
|
|
30
|
+
useStore.mockImplementation(name => name === 'people'
|
|
31
|
+
? {getters: {mainCompany: {id: 1}, currentCompany}, actions: {}}
|
|
32
|
+
: {getters: {}, actions: {}});
|
|
33
|
+
let tree;
|
|
34
|
+
renderer.act(() => {
|
|
35
|
+
tree = renderer.create(React.createElement(SalesmanTab, {customStyles: {}}));
|
|
36
|
+
});
|
|
37
|
+
expect(useSalesmanManage.mock.calls.at(-1)[0].currentCompanyId).toBe(expected);
|
|
38
|
+
renderer.act(() => tree.unmount());
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guards the UserFormModal footer styles so the Salvar button stays visible.
|
|
3
|
+
* Regression: task-428 left inlineStyle_348_20 as text-only styles (no bg/padding),
|
|
4
|
+
* making the primary action invisible on a white modal (app-community#458).
|
|
5
|
+
*/
|
|
6
|
+
const styles = require('../../../../react/components/tabs/UsersTab.styles');
|
|
7
|
+
|
|
8
|
+
describe('UsersTab.styles — UserFormModal footer', () => {
|
|
9
|
+
test('inlineStyle_348_20 is a primary action button (visible Salvar)', () => {
|
|
10
|
+
const s = styles.inlineStyle_348_20;
|
|
11
|
+
expect(s).toBeDefined();
|
|
12
|
+
expect(s.backgroundColor).toBeTruthy();
|
|
13
|
+
expect(String(s.backgroundColor).toLowerCase()).not.toBe('#ffffff');
|
|
14
|
+
expect(s.flex).toBe(1);
|
|
15
|
+
expect(s.paddingVertical).toBeGreaterThanOrEqual(12);
|
|
16
|
+
expect(s.borderRadius).toBeGreaterThanOrEqual(8);
|
|
17
|
+
expect(s.alignItems).toBe('center');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('inlineStyle_341_14 is a secondary Cancelar button', () => {
|
|
21
|
+
const s = styles.inlineStyle_341_14;
|
|
22
|
+
expect(s.flex).toBe(1);
|
|
23
|
+
expect(s.borderWidth).toBe(1);
|
|
24
|
+
expect(s.paddingVertical).toBeGreaterThanOrEqual(12);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('inlineStyle_339_16 footer row has horizontal and bottom padding', () => {
|
|
28
|
+
const s = styles.inlineStyle_339_16;
|
|
29
|
+
expect(s.flexDirection).toBe('row');
|
|
30
|
+
expect(s.paddingHorizontal).toBeGreaterThanOrEqual(16);
|
|
31
|
+
expect(s.paddingBottom).toBeGreaterThanOrEqual(16);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('timezoneListStyle caps height so footer is not pushed off-screen', () => {
|
|
35
|
+
const s = styles.timezoneListStyle;
|
|
36
|
+
expect(s.maxHeight).toBeDefined();
|
|
37
|
+
expect(Number(s.maxHeight)).toBeGreaterThan(0);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import {describe, it} from 'node:test';
|
|
3
|
+
import {
|
|
4
|
+
extractId,
|
|
5
|
+
mapAddressesForClient,
|
|
6
|
+
normalizeAddress,
|
|
7
|
+
normalizeId,
|
|
8
|
+
resolveAddressSaveId,
|
|
9
|
+
toPeopleIri,
|
|
10
|
+
} from '../../../../react/components/tabs/addressesTabHelpers.js';
|
|
11
|
+
|
|
12
|
+
describe('addressesTabHelpers', () => {
|
|
13
|
+
it('extractId strips non-digits', () => {
|
|
14
|
+
assert.equal(extractId('/addresses/42'), '42');
|
|
15
|
+
assert.equal(extractId(99), '99');
|
|
16
|
+
assert.equal(extractId(null), '');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('normalizeId never invents a timestamp for missing id', () => {
|
|
20
|
+
assert.equal(normalizeId(undefined), '');
|
|
21
|
+
assert.equal(normalizeId(null), '');
|
|
22
|
+
assert.equal(normalizeId(''), '');
|
|
23
|
+
assert.equal(normalizeId('/addresses/7'), '7');
|
|
24
|
+
assert.equal(normalizeId(15), '15');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('normalizeAddress preserves numeric id and builds @id', () => {
|
|
28
|
+
const nested = {
|
|
29
|
+
id: 55,
|
|
30
|
+
street: {street: 'Rua A', district: {district: 'Centro', city: {city: 'SP', state: {uf: 'SP', country: {countrycode: 'BR'}}}}},
|
|
31
|
+
number: 10,
|
|
32
|
+
complement: 'Apto 1',
|
|
33
|
+
nickname: 'Matriz',
|
|
34
|
+
};
|
|
35
|
+
const normalized = normalizeAddress(nested);
|
|
36
|
+
assert.equal(normalized.id, '55');
|
|
37
|
+
assert.equal(normalized['@id'], '/addresses/55');
|
|
38
|
+
assert.equal(normalized.street, 'Rua A');
|
|
39
|
+
assert.equal(normalized.city, 'SP');
|
|
40
|
+
assert.equal(normalized.state, 'SP');
|
|
41
|
+
assert.equal(normalized.country, 'BR');
|
|
42
|
+
assert.equal(normalized.number, '10');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('normalizeAddress from @id only still yields digits id', () => {
|
|
46
|
+
const normalized = normalizeAddress({'@id': '/addresses/88', street: 'Rua B'});
|
|
47
|
+
assert.equal(normalized.id, '88');
|
|
48
|
+
assert.equal(normalized['@id'], '/addresses/88');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('mapAddressesForClient keeps id/@id stable', () => {
|
|
52
|
+
const mapped = mapAddressesForClient([
|
|
53
|
+
{id: '3', street: 'X', number: '1'},
|
|
54
|
+
]);
|
|
55
|
+
assert.equal(mapped[0].id, '3');
|
|
56
|
+
assert.equal(mapped[0]['@id'], '/addresses/3');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('resolveAddressSaveId prefers payload then editingItem', () => {
|
|
60
|
+
assert.equal(
|
|
61
|
+
resolveAddressSaveId({id: '9', '@id': '/addresses/9'}, {id: '9'}),
|
|
62
|
+
'9',
|
|
63
|
+
);
|
|
64
|
+
assert.equal(resolveAddressSaveId({id: 12}, {}), '12');
|
|
65
|
+
assert.equal(resolveAddressSaveId(null, {}), '');
|
|
66
|
+
assert.equal(
|
|
67
|
+
resolveAddressSaveId({'@id': '/addresses/21'}, {street: 'Z'}),
|
|
68
|
+
'21',
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('toPeopleIri resolves nested and flat people shapes', () => {
|
|
73
|
+
assert.equal(toPeopleIri({'@id': '/people/5'}), '/people/5');
|
|
74
|
+
assert.equal(toPeopleIri({id: 8}), '/people/8');
|
|
75
|
+
assert.equal(toPeopleIri({people: {'@id': '/people/3'}}), '/people/3');
|
|
76
|
+
assert.equal(toPeopleIri({}), '');
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import test from 'node:test';
|
|
4
|
+
|
|
5
|
+
const tab = fs.readFileSync(
|
|
6
|
+
new URL('../../../../react/components/tabs/DocumentsTab.js', import.meta.url),
|
|
7
|
+
'utf8',
|
|
8
|
+
);
|
|
9
|
+
const attachments = fs.readFileSync(
|
|
10
|
+
new URL('../../../../react/components/tabs/DocumentAttachments.js', import.meta.url),
|
|
11
|
+
'utf8',
|
|
12
|
+
);
|
|
13
|
+
const helpers = fs.readFileSync(
|
|
14
|
+
new URL('../../../../react/components/tabs/documentsTabHelpers.js', import.meta.url),
|
|
15
|
+
'utf8',
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
test('DocumentsTab wires DocumentAttachments for per-document files', () => {
|
|
19
|
+
assert.match(tab, /DocumentAttachments/);
|
|
20
|
+
assert.match(tab, /onFilesChanged=\{updateDocumentFiles\}/);
|
|
21
|
+
assert.ok(tab.split('\n').length <= 500, 'DocumentsTab must stay under 500 lines');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('DocumentAttachments exposes view/share/print/remove via DefaultUpload', () => {
|
|
25
|
+
assert.match(attachments, /DefaultUpload/);
|
|
26
|
+
assert.match(attachments, /relationStoreName=\"document_file\"/);
|
|
27
|
+
assert.match(attachments, /handleShare|Share\.share/);
|
|
28
|
+
assert.match(attachments, /handlePrint|print\(/);
|
|
29
|
+
assert.match(attachments, /ACCEPTED_DOCUMENT_FILE_TYPES/);
|
|
30
|
+
assert.ok(attachments.split('\n').length <= 500, 'DocumentAttachments must stay under 500 lines');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('helpers cover multi-file normalization contract', () => {
|
|
34
|
+
assert.match(helpers, /normalizeDocumentFiles/);
|
|
35
|
+
assert.match(helpers, /documentFiles/);
|
|
36
|
+
assert.match(helpers, /ACCEPTED_DOCUMENT_FILE_TYPES/);
|
|
37
|
+
});
|