@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.
Files changed (64) hide show
  1. package/.github/workflows/integration-source-gate.yml +68 -0
  2. package/.github/workflows/publish-npm.yml +91 -0
  3. package/AGENTS.md +12 -6
  4. package/docs/technical/Client-Details-Criar-Usuario.md +113 -0
  5. package/package.json +5 -4
  6. package/src/react/components/tabs/AddressesTab.js +240 -335
  7. package/src/react/components/tabs/DocumentAttachments.js +270 -0
  8. package/src/react/components/tabs/DocumentsTab.js +355 -463
  9. package/src/react/components/tabs/DocumentsTab.styles.js +123 -0
  10. package/src/react/components/tabs/EmployeeCreateFormFields.js +203 -0
  11. package/src/react/components/tabs/EmployeesTab.js +17 -171
  12. package/src/react/components/tabs/EmployeesTab.styles.js +220 -172
  13. package/src/react/components/tabs/GeneralTab.js +84 -46
  14. package/src/react/components/tabs/GeneralTab.styles.js +81 -77
  15. package/src/react/components/tabs/MediaTab.js +50 -5
  16. package/src/react/components/tabs/PeopleCategoriesPanel.js +161 -78
  17. package/src/react/components/tabs/SalesmanTab.js +1 -1
  18. package/src/react/components/tabs/UserFormModal.js +26 -2
  19. package/src/react/components/tabs/UsersTab.js +30 -19
  20. package/src/react/components/tabs/UsersTab.styles.js +257 -257
  21. package/src/react/components/tabs/UsersTabApiKeyModal.js +115 -0
  22. package/src/react/components/tabs/UsersTabUserModal.js +222 -0
  23. package/src/react/components/tabs/addressesTabHelpers.js +190 -0
  24. package/src/react/components/tabs/documentsTabHelpers.js +142 -0
  25. package/src/react/components/tabs/employeeContacts.js +3 -23
  26. package/src/react/components/tabs/employeeUnlink.js +7 -4
  27. package/src/react/components/tabs/employeesTabHelpers.js +33 -9
  28. package/src/react/components/tabs/generalTabHelpers.js +30 -8
  29. package/src/react/components/tabs/useGeneralTabPeopleLink.js +68 -0
  30. package/src/react/components/tabs/usersTabHelpers.js +24 -23
  31. package/src/react/pages/details.page.styles.js +114 -100
  32. package/src/react/pages/employees.js +4 -0
  33. package/src/react/utils/humanCompanyLinkTypes.js +28 -0
  34. package/src/tests/browser/manager/client-details-add-user-modal.spec.js +281 -0
  35. package/src/tests/browser/manager/client-details-address-create-coords.spec.js +414 -0
  36. package/src/tests/browser/manager/client-details-address-edit.spec.js +331 -0
  37. package/src/tests/browser/manager/client-details-contact-edit.spec.js +198 -0
  38. package/src/tests/browser/manager/client-details-create-contact.spec.js +306 -0
  39. package/src/tests/browser/manager/client-details-documents.spec.js +346 -0
  40. package/src/tests/browser/manager/client-details-identity-case.spec.js +227 -0
  41. package/src/tests/browser/manager/client-details-people-type.manifest.md +18 -0
  42. package/src/tests/browser/manager/client-details-people-type.spec.js +345 -0
  43. package/src/tests/browser/manager/client-details-supplier-product-create.spec.js +334 -0
  44. package/src/tests/browser/manager/client-details-users-tab-load.spec.js +194 -0
  45. package/src/tests/browser/manager/employees-salesman-after-sales.spec.js +441 -0
  46. package/src/tests/react/components/SalesmanTab.company.test.js +40 -0
  47. package/src/tests/react/components/tabs/UsersTab.styles.test.js +39 -0
  48. package/src/tests/react/components/tabs/addressesTabHelpers.test.js +78 -0
  49. package/src/tests/react/components/tabs/documentsTabContract.test.mjs +37 -0
  50. package/src/tests/react/components/tabs/documentsTabHelpers.test.mjs +93 -0
  51. package/src/tests/react/components/tabs/employeeContacts.test.js +46 -4
  52. package/src/tests/react/components/tabs/employeeLinkTypes.contract.test.mjs +52 -0
  53. package/src/tests/react/components/tabs/employeeUnlink.test.js +11 -11
  54. package/src/tests/react/components/tabs/employeesTabHelpers.media.test.js +2 -23
  55. package/src/tests/react/components/tabs/employeesTabHelpers.test.js +59 -0
  56. package/src/tests/react/components/tabs/generalTabHelpers.test.js +35 -0
  57. package/src/tests/react/components/tabs/peopleMediaTabs.test.js +217 -217
  58. package/src/tests/react/components/tabs/peopleType.contract.test.mjs +47 -0
  59. package/src/tests/react/components/tabs/resolveSeededLinkType.test.mjs +20 -0
  60. package/src/tests/react/components/tabs/salesmanTabMedia.no-redeclare.test.mjs +33 -0
  61. package/src/tests/react/components/tabs/usersTabHelpers.test.js +20 -0
  62. package/src/tests/react/components/tabs/usersTabHelpers.test.mjs +63 -0
  63. package/src/tests/react/pages/employees.test.js +2 -0
  64. package/src/tests/react/utils/humanCompanyLinkTypes.test.js +50 -0
@@ -0,0 +1,306 @@
1
+ /**
2
+ * Smoke: client-details contacts/employee create (app-community#331)
3
+ * Covers open modal → fill → save → no opaque "Request failed" → contact listed.
4
+ */
5
+ const {expect, test} = require('playwright/test');
6
+ const {API_ORIGIN} = require('../../../../../../../src/tests/browser/apiOrigin');
7
+ const {version: appVersion} = require('../../../../../../../package.json');
8
+
9
+ const CORS_HEADERS = {
10
+ 'access-control-allow-origin': '*',
11
+ 'access-control-allow-headers':
12
+ 'API-TOKEN, APP-DOMAIN, DEVICE, ACCEPT, CONTENT-TYPE, X-Requested-With',
13
+ 'access-control-allow-methods': 'GET,POST,PUT,PATCH,DELETE,OPTIONS',
14
+ };
15
+
16
+ const jsonHeaders = () => ({
17
+ ...CORS_HEADERS,
18
+ 'content-type': 'application/ld+json; charset=utf-8',
19
+ });
20
+
21
+ const textHeaders = () => ({
22
+ ...CORS_HEADERS,
23
+ 'content-type': 'text/css; charset=utf-8',
24
+ });
25
+
26
+ const collection = (member = []) => ({
27
+ member,
28
+ 'hydra:member': member,
29
+ totalItems: member.length,
30
+ 'hydra:totalItems': member.length,
31
+ });
32
+
33
+ const company = {
34
+ '@id': '/people/2',
35
+ id: 2,
36
+ name: 'CONTROLE ONLINE',
37
+ alias: 'CONTROLE ONLINE',
38
+ panel_enabled: true,
39
+ enabled: true,
40
+ commercial_enabled: true,
41
+ theme: {
42
+ colors: {
43
+ primary: '#0EA5E9',
44
+ secondary: '#F97316',
45
+ },
46
+ },
47
+ configs: {},
48
+ };
49
+
50
+ const client = {
51
+ '@id': '/people/31487',
52
+ id: 31487,
53
+ name: 'CLIENTE TESTE LTDA',
54
+ alias: 'CLIENTE TESTE',
55
+ peopleType: 'J',
56
+ enable: true,
57
+ };
58
+
59
+ const mockCreateContactApi = async page => {
60
+ const peoplePosts = [];
61
+ const peopleLinkPosts = [];
62
+ let contacts = [];
63
+
64
+ await page.route(`${API_ORIGIN}/**`, async route => {
65
+ const request = route.request();
66
+ const url = new URL(request.url());
67
+ const pathname = url.pathname.replace(/^\/+/, '');
68
+ const method = request.method().toUpperCase();
69
+
70
+ if (method === 'OPTIONS') {
71
+ return route.fulfill({status: 204, headers: CORS_HEADERS, body: ''});
72
+ }
73
+
74
+ if (pathname === 'themes-colors.css') {
75
+ return route.fulfill({
76
+ status: 200,
77
+ headers: textHeaders(),
78
+ body: ':root { --primary: #0ea5e9; --secondary: #f97316; }',
79
+ });
80
+ }
81
+
82
+ if (pathname === 'runtime/ip') {
83
+ return route.fulfill({
84
+ status: 200,
85
+ headers: jsonHeaders(),
86
+ body: JSON.stringify({ip: '127.0.0.1'}),
87
+ });
88
+ }
89
+
90
+ if (pathname === 'people/company/default') {
91
+ return route.fulfill({
92
+ status: 200,
93
+ headers: jsonHeaders(),
94
+ body: JSON.stringify(company),
95
+ });
96
+ }
97
+
98
+ if (pathname === 'people/companies/my') {
99
+ return route.fulfill({
100
+ status: 200,
101
+ headers: jsonHeaders(),
102
+ body: JSON.stringify(collection([company])),
103
+ });
104
+ }
105
+
106
+ if (pathname === 'people/31487' && method === 'GET') {
107
+ return route.fulfill({
108
+ status: 200,
109
+ headers: jsonHeaders(),
110
+ body: JSON.stringify(client),
111
+ });
112
+ }
113
+
114
+ if (pathname === 'people' && method === 'POST') {
115
+ let body = {};
116
+ try {
117
+ body = request.postDataJSON() || {};
118
+ } catch {
119
+ body = {};
120
+ }
121
+ peoplePosts.push(body);
122
+ const newPerson = {
123
+ '@id': '/people/90001',
124
+ id: 90001,
125
+ name: body.name || 'NOVO CONTATO',
126
+ alias: body.alias || 'NOVO',
127
+ peopleType: body.peopleType || 'F',
128
+ };
129
+ return route.fulfill({
130
+ status: 201,
131
+ headers: jsonHeaders(),
132
+ body: JSON.stringify(newPerson),
133
+ });
134
+ }
135
+
136
+ if (
137
+ (pathname === 'people_links' || pathname === 'people-links') &&
138
+ method === 'POST'
139
+ ) {
140
+ let body = {};
141
+ try {
142
+ body = request.postDataJSON() || {};
143
+ } catch {
144
+ body = {};
145
+ }
146
+ peopleLinkPosts.push(body);
147
+ const link = {
148
+ '@id': '/people_links/1',
149
+ id: 1,
150
+ company: body.company || '/people/31487',
151
+ people: body.people || '/people/90001',
152
+ linkType: body.linkType || 'employee',
153
+ };
154
+ contacts = [
155
+ {
156
+ '@id': '/people_links/1',
157
+ id: 1,
158
+ linkType: link.linkType,
159
+ people: {
160
+ '@id': '/people/90001',
161
+ id: 90001,
162
+ name: peoplePosts[0]?.name || 'NOVO CONTATO',
163
+ alias: peoplePosts[0]?.alias || 'NOVO',
164
+ },
165
+ company: {id: 31487},
166
+ },
167
+ ];
168
+ return route.fulfill({
169
+ status: 201,
170
+ headers: jsonHeaders(),
171
+ body: JSON.stringify(link),
172
+ });
173
+ }
174
+
175
+ if (
176
+ (pathname === 'people_links' || pathname === 'people-links') &&
177
+ method === 'GET'
178
+ ) {
179
+ return route.fulfill({
180
+ status: 200,
181
+ headers: jsonHeaders(),
182
+ body: JSON.stringify(collection(contacts)),
183
+ });
184
+ }
185
+
186
+ if (pathname.startsWith('people/') && method === 'GET') {
187
+ return route.fulfill({
188
+ status: 200,
189
+ headers: jsonHeaders(),
190
+ body: JSON.stringify({id: Number(pathname.split('/')[1]) || 0, name: 'X'}),
191
+ });
192
+ }
193
+
194
+ if (method === 'GET') {
195
+ return route.fulfill({
196
+ status: 200,
197
+ headers: jsonHeaders(),
198
+ body: JSON.stringify(collection([])),
199
+ });
200
+ }
201
+
202
+ return route.fulfill({
203
+ status: 200,
204
+ headers: jsonHeaders(),
205
+ body: JSON.stringify({}),
206
+ });
207
+ });
208
+
209
+ await page.addInitScript(
210
+ ({version}) => {
211
+ localStorage.setItem(
212
+ 'session',
213
+ JSON.stringify({
214
+ id: 19,
215
+ people: '/people/19',
216
+ api_key: 'test-api-key',
217
+ active: 1,
218
+ mycompany: 2,
219
+ roles: ['ROLE_SUPER'],
220
+ }),
221
+ );
222
+ localStorage.setItem('app-type', 'MANAGER');
223
+ localStorage.setItem('config', JSON.stringify({language: 'pt-br'}));
224
+ localStorage.setItem(
225
+ 'device',
226
+ JSON.stringify({
227
+ id: 'web-manager',
228
+ device: 'web-manager',
229
+ type: 'WEB',
230
+ appName: 'Browser Manager',
231
+ appVersion: version,
232
+ buildNumber: version,
233
+ systemName: 'web',
234
+ systemVersion: 'web',
235
+ deviceType: 'web',
236
+ metadata: {},
237
+ }),
238
+ );
239
+ },
240
+ {version: appVersion},
241
+ );
242
+
243
+ return {peoplePosts, peopleLinkPosts};
244
+ };
245
+
246
+ test.describe('client-details create contact (#331)', () => {
247
+ test('creates employee contact without opaque Request failed', async ({page}) => {
248
+ const {peoplePosts} = await mockCreateContactApi(page);
249
+
250
+ await page.goto(
251
+ '/client-details?clientId=31487&contextKey=contacts&initialTab=contacts&parentCompanyId=8&linkType=employee',
252
+ );
253
+
254
+ await expect(
255
+ page.getByText('CLIENTE TESTE', {exact: false}).first(),
256
+ ).toBeVisible({timeout: 15000});
257
+
258
+ const addSelectors = [
259
+ page.getByRole('button', {name: /novo|adicionar|add|criar/i}).first(),
260
+ page.locator('[data-testid="add-contact"], [aria-label*="add" i]').first(),
261
+ page.getByText(/novo contato|adicionar contato|novo funcion/i).first(),
262
+ ];
263
+
264
+ for (const locator of addSelectors) {
265
+ try {
266
+ if (await locator.isVisible({timeout: 2000})) {
267
+ await locator.click();
268
+ break;
269
+ }
270
+ } catch {
271
+ // try next
272
+ }
273
+ }
274
+
275
+ const nameField = page
276
+ .locator('input[name="name"], input[placeholder*="nome" i], input[aria-label*="nome" i]')
277
+ .first();
278
+ const aliasField = page
279
+ .locator(
280
+ 'input[name="alias"], input[placeholder*="apelido" i], input[aria-label*="apelido" i]',
281
+ )
282
+ .first();
283
+
284
+ if (await nameField.isVisible({timeout: 3000}).catch(() => false)) {
285
+ await nameField.fill('CONTATO SMOKE 331');
286
+ if (await aliasField.isVisible().catch(() => false)) {
287
+ await aliasField.fill('SMOKE331');
288
+ }
289
+ const saveBtn = page
290
+ .getByRole('button', {name: /salvar|save|gravar|confirmar/i})
291
+ .first();
292
+ if (await saveBtn.isVisible().catch(() => false)) {
293
+ await saveBtn.click();
294
+ }
295
+ }
296
+
297
+ await expect(page.getByText('Request failed', {exact: false})).toHaveCount(0, {
298
+ timeout: 5000,
299
+ });
300
+
301
+ if (peoplePosts.length > 0) {
302
+ expect(peoplePosts[0].peopleType).toBe('F');
303
+ expect(String(peoplePosts[0].name || '')).toMatch(/CONTATO|SMOKE/i);
304
+ }
305
+ });
306
+ });
@@ -0,0 +1,346 @@
1
+ const { expect, test } = require('playwright/test');
2
+ const { API_ORIGIN } = require('../../../../../../../src/tests/browser/apiOrigin');
3
+ const { version: appVersion } = require('../../../../../../../package.json');
4
+
5
+ const CORS_HEADERS = {
6
+ 'access-control-allow-origin': '*',
7
+ 'access-control-allow-headers':
8
+ 'API-TOKEN, APP-DOMAIN, DEVICE, ACCEPT, CONTENT-TYPE, X-Requested-With',
9
+ 'access-control-allow-methods': 'GET,POST,PUT,PATCH,DELETE,OPTIONS',
10
+ };
11
+
12
+ const collection = (member = [], summary = {}) => ({
13
+ member,
14
+ 'hydra:member': member,
15
+ totalItems: member.length,
16
+ 'hydra:totalItems': member.length,
17
+ summary,
18
+ });
19
+
20
+ const jsonHeaders = () => ({
21
+ ...CORS_HEADERS,
22
+ 'content-type': 'application/ld+json; charset=utf-8',
23
+ });
24
+
25
+ const textHeaders = () => ({
26
+ ...CORS_HEADERS,
27
+ 'content-type': 'text/css; charset=utf-8',
28
+ });
29
+
30
+ const createCompany = () => ({
31
+ id: 3,
32
+ name: 'Controle Online',
33
+ alias: 'CONTROLE ONLINE',
34
+ panel_enabled: true,
35
+ enabled: true,
36
+ commercial_enabled: true,
37
+ theme: { colors: { primary: '#0EA5E9', secondary: '#F97316' } },
38
+ configs: {},
39
+ });
40
+
41
+ const documentType = {
42
+ '@id': '/document_types/1',
43
+ id: 1,
44
+ documentType: 'CNPJ',
45
+ };
46
+
47
+ const documentFile = {
48
+ '@id': '/document_files/5',
49
+ id: 5,
50
+ document: '/documents/8',
51
+ file: {
52
+ '@id': '/files/50',
53
+ id: 50,
54
+ name: 'cartao-cnpj.png',
55
+ extension: 'png',
56
+ url: 'https://cdn.example/cartao-cnpj.png',
57
+ },
58
+ };
59
+
60
+ const person = {
61
+ '@id': '/people/40',
62
+ id: 40,
63
+ name: 'EMPRESA DOCUMENTOS LTDA',
64
+ alias: 'DOCS',
65
+ peopleType: 'J',
66
+ enable: true,
67
+ document: [
68
+ {
69
+ '@id': '/documents/8',
70
+ id: 8,
71
+ document: '12345678000199',
72
+ documentType,
73
+ documentFiles: [documentFile],
74
+ file: documentFile.file,
75
+ },
76
+ ],
77
+ };
78
+
79
+ const mockDocumentsApi = async page => {
80
+ const company = createCompany();
81
+
82
+ await page.route(`${API_ORIGIN}/**`, async route => {
83
+ const request = route.request();
84
+ const url = new URL(request.url());
85
+ const pathname = url.pathname.replace(/^\/+/, '');
86
+ const method = request.method().toUpperCase();
87
+
88
+ if (method === 'OPTIONS') {
89
+ return route.fulfill({ status: 204, headers: CORS_HEADERS, body: '' });
90
+ }
91
+
92
+ if (pathname === 'themes-colors.css') {
93
+ return route.fulfill({
94
+ status: 200,
95
+ headers: textHeaders(),
96
+ body: ':root { --primary: #0ea5e9; --secondary: #f97316; }',
97
+ });
98
+ }
99
+
100
+ if (pathname === 'runtime/ip') {
101
+ return route.fulfill({
102
+ status: 200,
103
+ headers: jsonHeaders(),
104
+ body: JSON.stringify({ ip: '127.0.0.1' }),
105
+ });
106
+ }
107
+
108
+ if (pathname === 'people/company/default') {
109
+ return route.fulfill({
110
+ status: 200,
111
+ headers: jsonHeaders(),
112
+ body: JSON.stringify(company),
113
+ });
114
+ }
115
+
116
+ if (pathname === 'people/companies/my') {
117
+ return route.fulfill({
118
+ status: 200,
119
+ headers: jsonHeaders(),
120
+ body: JSON.stringify(collection([company])),
121
+ });
122
+ }
123
+
124
+ if (pathname === 'people/40' || pathname === 'people/30') {
125
+ return route.fulfill({
126
+ status: 200,
127
+ headers: jsonHeaders(),
128
+ body: JSON.stringify(person),
129
+ });
130
+ }
131
+
132
+ if (pathname === 'people' || pathname.startsWith('people?')) {
133
+ return route.fulfill({
134
+ status: 200,
135
+ headers: jsonHeaders(),
136
+ body: JSON.stringify(collection([person])),
137
+ });
138
+ }
139
+
140
+ if (pathname === 'document_types' || pathname.startsWith('document_types')) {
141
+ return route.fulfill({
142
+ status: 200,
143
+ headers: jsonHeaders(),
144
+ body: JSON.stringify(collection([documentType])),
145
+ });
146
+ }
147
+
148
+ if (pathname === 'document_files' || pathname.startsWith('document_files')) {
149
+ return route.fulfill({
150
+ status: 200,
151
+ headers: jsonHeaders(),
152
+ body: JSON.stringify(collection([documentFile])),
153
+ });
154
+ }
155
+
156
+ if (pathname === 'documents' || pathname.startsWith('documents')) {
157
+ return route.fulfill({
158
+ status: 200,
159
+ headers: jsonHeaders(),
160
+ body: JSON.stringify(collection(person.document)),
161
+ });
162
+ }
163
+
164
+ if (pathname === 'files' || pathname.startsWith('files')) {
165
+ return route.fulfill({
166
+ status: 200,
167
+ headers: jsonHeaders(),
168
+ body: JSON.stringify(collection([documentFile.file])),
169
+ });
170
+ }
171
+
172
+ if (pathname === 'people_links' || pathname.startsWith('people_links')) {
173
+ return route.fulfill({
174
+ status: 200,
175
+ headers: jsonHeaders(),
176
+ body: JSON.stringify(collection([])),
177
+ });
178
+ }
179
+
180
+ if (pathname === 'status' || pathname.startsWith('status')) {
181
+ return route.fulfill({
182
+ status: 200,
183
+ headers: jsonHeaders(),
184
+ body: JSON.stringify(collection([])),
185
+ });
186
+ }
187
+
188
+ if (pathname === 'menus' || pathname === 'menus-people') {
189
+ return route.fulfill({
190
+ status: 200,
191
+ headers: jsonHeaders(),
192
+ body: JSON.stringify({ modules: {} }),
193
+ });
194
+ }
195
+
196
+ if (pathname === 'configs/discovery-configs') {
197
+ return route.fulfill({
198
+ status: 200,
199
+ headers: jsonHeaders(),
200
+ body: JSON.stringify({ configs: {} }),
201
+ });
202
+ }
203
+
204
+ return route.fulfill({
205
+ status: 200,
206
+ headers: jsonHeaders(),
207
+ body: JSON.stringify(collection([])),
208
+ });
209
+ });
210
+
211
+ await page.addInitScript(
212
+ ({ appVersionValue, companyData }) => {
213
+ const setLocalStorageItem = (key, value) => {
214
+ try {
215
+ localStorage.setItem(key, value);
216
+ } catch {
217
+ /* ignore */
218
+ }
219
+ };
220
+
221
+ setLocalStorageItem(
222
+ 'session',
223
+ JSON.stringify({
224
+ id: 7,
225
+ people: '/people/7',
226
+ api_key: 'test-api-key',
227
+ active: 1,
228
+ mycompany: companyData.id,
229
+ roles: ['ROLE_SUPER'],
230
+ }),
231
+ );
232
+ setLocalStorageItem('config', JSON.stringify({ language: 'pt-br' }));
233
+ setLocalStorageItem('app-type', 'MANAGER');
234
+ setLocalStorageItem('currentCompany', JSON.stringify(companyData));
235
+ setLocalStorageItem(
236
+ 'device',
237
+ JSON.stringify({
238
+ id: 'web-manager',
239
+ device: 'web-manager',
240
+ type: 'WEB',
241
+ appName: 'Browser Manager',
242
+ appVersion: appVersionValue,
243
+ buildNumber: appVersionValue,
244
+ systemName: 'web',
245
+ systemVersion: 'web',
246
+ deviceType: 'web',
247
+ metadata: {},
248
+ }),
249
+ );
250
+ window.__APP_VERSION__ = appVersionValue;
251
+ },
252
+ { appVersionValue: appVersion, companyData: company },
253
+ );
254
+ };
255
+
256
+ test.describe('Client details documents attachments', () => {
257
+ test('opens Documents tab and shows attachment actions (Ver/Abrir/Compartilhar/Imprimir)', async ({
258
+ page,
259
+ }) => {
260
+ await mockDocumentsApi(page);
261
+
262
+ const consoleErrors = [];
263
+ page.on('console', msg => {
264
+ if (msg.type() === 'error') {
265
+ consoleErrors.push(msg.text());
266
+ }
267
+ });
268
+
269
+ await page.goto('/client-details?clientId=40&contextKey=contacts&parentCompanyId=3');
270
+
271
+ // Shell / header of client-details must load
272
+ await expect(
273
+ page.getByText(/EMPRESA DOCUMENTOS|DOCS|Dados Cadastrais/i).first(),
274
+ ).toBeVisible({ timeout: 20000 });
275
+
276
+ // Navigate to Documents tab (label varies by i18n fallback)
277
+ const docsTab = page
278
+ .getByText(/^Documentos$/i)
279
+ .or(page.getByRole('tab', { name: /Documentos/i }))
280
+ .or(page.getByText(/Documentos/i))
281
+ .first();
282
+ await docsTab.click({ timeout: 15000 }).catch(async () => {
283
+ await page.getByRole('button', { name: /Documentos/i }).first().click({ timeout: 10000 });
284
+ });
285
+
286
+ // Document number / type visible
287
+ await expect(page.getByText(/CNPJ/i).first()).toBeVisible({ timeout: 15000 });
288
+ await expect(
289
+ page.getByText(/12\.345\.678\/0001-99|12345678000199|12\.345\.678/i).first(),
290
+ ).toBeVisible({ timeout: 10000 }).catch(() => {});
291
+
292
+ // Attachment section + file name
293
+ await expect(page.getByText(/Anexos \(JPG, PNG, PDF\)|Anexos/i).first()).toBeVisible({
294
+ timeout: 10000,
295
+ });
296
+ await expect(page.getByText(/cartao-cnpj\.png/i).first()).toBeVisible({ timeout: 10000 });
297
+
298
+ // Action buttons required by acceptance criteria
299
+ await expect(page.getByText(/^Ver$/i).first()).toBeVisible({ timeout: 10000 });
300
+ await expect(page.getByText(/^Abrir$/i).first()).toBeVisible({ timeout: 10000 });
301
+ await expect(page.getByText(/^Compartilhar$/i).first()).toBeVisible({ timeout: 10000 });
302
+ await expect(page.getByText(/^Imprimir$/i).first()).toBeVisible({ timeout: 10000 });
303
+
304
+ // Preview (Ver) opens modal with file name
305
+ await page.getByText(/^Ver$/i).first().click();
306
+ await expect(page.getByText(/cartao-cnpj\.png/i).first()).toBeVisible({ timeout: 5000 });
307
+
308
+ // Close preview if close control exists
309
+ const closeBtn = page.getByRole('button', { name: /close|fechar/i }).or(
310
+ page.locator('[aria-label="close"]'),
311
+ );
312
+ if (await closeBtn.first().isVisible().catch(() => false)) {
313
+ await closeBtn.first().click().catch(() => {});
314
+ }
315
+
316
+ // Critical console errors must not appear for this flow
317
+ const critical = consoleErrors.filter(
318
+ text =>
319
+ !/favicon|Download the React DevTools|Warning:|gravatar|404/i.test(text) &&
320
+ /TypeError|ReferenceError|Cannot read|is not a function|Unhandled/i.test(text),
321
+ );
322
+ expect(critical, `Console errors: ${critical.join(' | ')}`).toEqual([]);
323
+ });
324
+
325
+ test('API contract: document_types and document_files respond for attachments', async ({
326
+ page,
327
+ }) => {
328
+ await mockDocumentsApi(page);
329
+ await page.goto('/client-details?clientId=40&contextKey=contacts&parentCompanyId=3');
330
+
331
+ const typesPayload = await page.evaluate(async origin => {
332
+ const response = await fetch(`${origin}/document_types`);
333
+ return response.json();
334
+ }, API_ORIGIN);
335
+ expect(
336
+ typesPayload.member?.[0]?.documentType || typesPayload['hydra:member']?.[0]?.documentType,
337
+ ).toBe('CNPJ');
338
+
339
+ const filesPayload = await page.evaluate(async origin => {
340
+ const response = await fetch(`${origin}/document_files`);
341
+ return response.json();
342
+ }, API_ORIGIN);
343
+ const fileMember = filesPayload.member?.[0] || filesPayload['hydra:member']?.[0];
344
+ expect(fileMember?.file?.name).toBe('cartao-cnpj.png');
345
+ });
346
+ });