@controleonline/ui-customers 1.0.81 → 1.0.82

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 (62) 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/utils/humanCompanyLinkTypes.js +28 -0
  33. package/src/tests/browser/manager/client-details-add-user-modal.spec.js +281 -0
  34. package/src/tests/browser/manager/client-details-address-create-coords.spec.js +414 -0
  35. package/src/tests/browser/manager/client-details-address-edit.spec.js +331 -0
  36. package/src/tests/browser/manager/client-details-contact-edit.spec.js +198 -0
  37. package/src/tests/browser/manager/client-details-create-contact.spec.js +306 -0
  38. package/src/tests/browser/manager/client-details-documents.spec.js +346 -0
  39. package/src/tests/browser/manager/client-details-identity-case.spec.js +227 -0
  40. package/src/tests/browser/manager/client-details-people-type.manifest.md +18 -0
  41. package/src/tests/browser/manager/client-details-people-type.spec.js +345 -0
  42. package/src/tests/browser/manager/client-details-supplier-product-create.spec.js +334 -0
  43. package/src/tests/browser/manager/client-details-users-tab-load.spec.js +194 -0
  44. package/src/tests/browser/manager/employees-salesman-after-sales.spec.js +441 -0
  45. package/src/tests/react/components/SalesmanTab.company.test.js +40 -0
  46. package/src/tests/react/components/tabs/UsersTab.styles.test.js +39 -0
  47. package/src/tests/react/components/tabs/addressesTabHelpers.test.js +78 -0
  48. package/src/tests/react/components/tabs/documentsTabContract.test.mjs +37 -0
  49. package/src/tests/react/components/tabs/documentsTabHelpers.test.mjs +93 -0
  50. package/src/tests/react/components/tabs/employeeContacts.test.js +46 -4
  51. package/src/tests/react/components/tabs/employeeLinkTypes.contract.test.mjs +52 -0
  52. package/src/tests/react/components/tabs/employeeUnlink.test.js +11 -11
  53. package/src/tests/react/components/tabs/employeesTabHelpers.media.test.js +2 -23
  54. package/src/tests/react/components/tabs/employeesTabHelpers.test.js +59 -0
  55. package/src/tests/react/components/tabs/generalTabHelpers.test.js +35 -0
  56. package/src/tests/react/components/tabs/peopleMediaTabs.test.js +217 -217
  57. package/src/tests/react/components/tabs/peopleType.contract.test.mjs +47 -0
  58. package/src/tests/react/components/tabs/resolveSeededLinkType.test.mjs +20 -0
  59. package/src/tests/react/components/tabs/salesmanTabMedia.no-redeclare.test.mjs +33 -0
  60. package/src/tests/react/components/tabs/usersTabHelpers.test.js +20 -0
  61. package/src/tests/react/components/tabs/usersTabHelpers.test.mjs +63 -0
  62. package/src/tests/react/utils/humanCompanyLinkTypes.test.js +50 -0
@@ -0,0 +1,331 @@
1
+ /**
2
+ * Browser smoke for app-community#282 / ui-customers task-282:
3
+ * Manager → ClientDetails (franchise) → AddressesTab → edit existing address → save
4
+ * must issue PUT (update) not POST (create), without inventing ids.
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 EXISTING_ADDRESS = {
52
+ id: 42,
53
+ '@id': '/addresses/42',
54
+ street: {street: 'Rua das Franquias', district: {district: 'Centro', city: {city: 'São Paulo', state: {uf: 'SP', country: {countrycode: 'BR'}}}}},
55
+ number: '100',
56
+ complement: 'Sala 1',
57
+ nickname: 'Matriz',
58
+ zipcode: '01001000',
59
+ };
60
+
61
+ const mockAddressEditApi = async page => {
62
+ const company = createCompany();
63
+ const addressWriteMethods = [];
64
+ const addressWriteBodies = [];
65
+ let currentAddress = {...EXISTING_ADDRESS};
66
+
67
+ await page.route(`${API_ORIGIN}/**`, async route => {
68
+ const request = route.request();
69
+ const url = new URL(request.url());
70
+ const pathname = url.pathname.replace(/^\/+/, '');
71
+ const method = request.method().toUpperCase();
72
+
73
+ if (method === 'OPTIONS') {
74
+ return route.fulfill({status: 204, headers: CORS_HEADERS, body: ''});
75
+ }
76
+
77
+ if (pathname === 'themes-colors.css') {
78
+ return route.fulfill({
79
+ status: 200,
80
+ headers: textHeaders(),
81
+ body: ':root { --primary: #0ea5e9; --secondary: #f97316; }',
82
+ });
83
+ }
84
+
85
+ if (pathname === 'runtime/ip') {
86
+ return route.fulfill({
87
+ status: 200,
88
+ headers: jsonHeaders(),
89
+ body: JSON.stringify({ip: '127.0.0.1'}),
90
+ });
91
+ }
92
+
93
+ if (pathname === 'people/company/default') {
94
+ return route.fulfill({
95
+ status: 200,
96
+ headers: jsonHeaders(),
97
+ body: JSON.stringify(company),
98
+ });
99
+ }
100
+
101
+ if (pathname === 'people/companies/my') {
102
+ return route.fulfill({
103
+ status: 200,
104
+ headers: jsonHeaders(),
105
+ body: JSON.stringify(collection([company])),
106
+ });
107
+ }
108
+
109
+ // Franchise / client with one existing address
110
+ if (pathname === 'people/50' || pathname === 'people/50/') {
111
+ return route.fulfill({
112
+ status: 200,
113
+ headers: jsonHeaders(),
114
+ body: JSON.stringify({
115
+ id: 50,
116
+ '@id': '/people/50',
117
+ name: 'Franquia Teste LTDA',
118
+ alias: 'FRANQUIA TESTE',
119
+ enabled: true,
120
+ address: [currentAddress],
121
+ people: {id: 50, '@id': '/people/50'},
122
+ }),
123
+ });
124
+ }
125
+
126
+ if (pathname.startsWith('people/') && method === 'GET') {
127
+ return route.fulfill({
128
+ status: 200,
129
+ headers: jsonHeaders(),
130
+ body: JSON.stringify({
131
+ id: 50,
132
+ '@id': '/people/50',
133
+ name: 'Franquia Teste LTDA',
134
+ address: [currentAddress],
135
+ }),
136
+ });
137
+ }
138
+
139
+ // Address collection / item
140
+ if (pathname === 'addresses' || pathname === 'addresses/') {
141
+ if (method === 'GET') {
142
+ return route.fulfill({
143
+ status: 200,
144
+ headers: jsonHeaders(),
145
+ body: JSON.stringify(collection([currentAddress])),
146
+ });
147
+ }
148
+ if (method === 'POST') {
149
+ addressWriteMethods.push('POST');
150
+ const body = await request.postDataJSON().catch(() => ({}));
151
+ addressWriteBodies.push(body);
152
+ return route.fulfill({
153
+ status: 201,
154
+ headers: jsonHeaders(),
155
+ body: JSON.stringify({...body, id: 99, '@id': '/addresses/99'}),
156
+ });
157
+ }
158
+ }
159
+
160
+ if (pathname === 'addresses/42' || pathname === 'addresses/42/') {
161
+ if (method === 'GET') {
162
+ return route.fulfill({
163
+ status: 200,
164
+ headers: jsonHeaders(),
165
+ body: JSON.stringify(currentAddress),
166
+ });
167
+ }
168
+ if (method === 'PUT' || method === 'PATCH') {
169
+ addressWriteMethods.push(method);
170
+ const body = await request.postDataJSON().catch(() => ({}));
171
+ addressWriteBodies.push(body);
172
+ currentAddress = {
173
+ ...currentAddress,
174
+ ...body,
175
+ id: 42,
176
+ '@id': '/addresses/42',
177
+ street:
178
+ typeof body.street === 'string'
179
+ ? {street: body.street}
180
+ : body.street || currentAddress.street,
181
+ number: body.number ?? currentAddress.number,
182
+ };
183
+ return route.fulfill({
184
+ status: 200,
185
+ headers: jsonHeaders(),
186
+ body: JSON.stringify(currentAddress),
187
+ });
188
+ }
189
+ }
190
+
191
+ // Fallback empty collections / ok
192
+ if (method === 'GET') {
193
+ return route.fulfill({
194
+ status: 200,
195
+ headers: jsonHeaders(),
196
+ body: JSON.stringify(collection([])),
197
+ });
198
+ }
199
+
200
+ return route.fulfill({
201
+ status: 200,
202
+ headers: jsonHeaders(),
203
+ body: JSON.stringify({}),
204
+ });
205
+ });
206
+
207
+ await page.addInitScript(
208
+ ({appVersion}) => {
209
+ const setLocalStorageItem = (key, value) => {
210
+ try {
211
+ localStorage.setItem(key, value);
212
+ } catch {
213
+ // ignore
214
+ }
215
+ };
216
+
217
+ setLocalStorageItem(
218
+ 'session',
219
+ JSON.stringify({
220
+ id: 7,
221
+ people: '/people/7',
222
+ api_key: 'test-api-key',
223
+ active: 1,
224
+ mycompany: 3,
225
+ roles: ['ROLE_SUPER'],
226
+ }),
227
+ );
228
+ setLocalStorageItem('config', JSON.stringify({language: 'pt-br'}));
229
+ setLocalStorageItem('app-type', 'MANAGER');
230
+ setLocalStorageItem(
231
+ 'device',
232
+ JSON.stringify({
233
+ id: 'web-manager',
234
+ device: 'web-manager',
235
+ type: 'WEB',
236
+ appName: 'Browser Manager',
237
+ appVersion,
238
+ buildNumber: appVersion,
239
+ systemName: 'web',
240
+ systemVersion: 'web',
241
+ deviceType: 'web',
242
+ metadata: {},
243
+ }),
244
+ );
245
+ },
246
+ {appVersion},
247
+ );
248
+
249
+ return {
250
+ getAddressWriteMethods: () => addressWriteMethods,
251
+ getAddressWriteBodies: () => addressWriteBodies,
252
+ };
253
+ };
254
+
255
+ test.describe('client details address edit browser smoke (#282)', () => {
256
+ test('edits existing franchise address and issues PUT not POST', async ({page}) => {
257
+ const api = await mockAddressEditApi(page);
258
+
259
+ await page.goto('/client-details?clientId=50&contextKey=client');
260
+
261
+ // Client header / general area loads
262
+ await expect(
263
+ page.getByText(/Franquia Teste|Dados Cadastrais|Endere/i).first(),
264
+ ).toBeVisible({timeout: 20000});
265
+
266
+ // Open Addresses section if tabbed
267
+ const addressesTab = page.getByText(/Endereços|Addresses|endereco/i).first();
268
+ if (await addressesTab.isVisible().catch(() => false)) {
269
+ await addressesTab.click();
270
+ }
271
+
272
+ // Existing address line visible
273
+ await expect(page.getByText(/Rua das Franquias|Franquias/i).first()).toBeVisible({
274
+ timeout: 15000,
275
+ });
276
+
277
+ // Click edit (Feather edit-2 icon or nearest button)
278
+ const editBtn = page.locator('[data-testid="address-edit"], button, [role="button"]').filter({
279
+ has: page.locator('svg, [class*="edit"], [name*="edit"]'),
280
+ }).first();
281
+ // Fallback: any clickable near the address card
282
+ if (await editBtn.isVisible().catch(() => false)) {
283
+ await editBtn.click();
284
+ } else {
285
+ // Try clicking text that might open edit in isEditing mode — ensure edit mode
286
+ const editModeToggle = page.getByText(/Editar|Edit|Alterar/i).first();
287
+ if (await editModeToggle.isVisible().catch(() => false)) {
288
+ await editModeToggle.click();
289
+ }
290
+ // Click first icon-like control in address area
291
+ await page.locator('.itemActions, [class*="itemActions"], [class*="iconButton"]').first().click({timeout: 5000}).catch(() => {});
292
+ }
293
+
294
+ // Modal / form should open — fill a field change if possible
295
+ const numberInput = page.getByPlaceholder(/número|number|N[úu]mero/i).or(
296
+ page.locator('input[name*="number"], input[id*="number"]'),
297
+ ).first();
298
+ if (await numberInput.isVisible({timeout: 5000}).catch(() => false)) {
299
+ await numberInput.fill('200');
300
+ }
301
+
302
+ // Save
303
+ const saveBtn = page.getByRole('button', {name: /Salvar|Save|Gravar/i}).first();
304
+ if (await saveBtn.isVisible({timeout: 5000}).catch(() => false)) {
305
+ await saveBtn.click();
306
+ } else {
307
+ await page.getByText(/Salvar|Save Changes|Gravar/i).first().click({timeout: 5000}).catch(() => {});
308
+ }
309
+
310
+ // Assert write was PUT/PATCH with id 42, never POST create
311
+ await expect.poll(() => api.getAddressWriteMethods().length, {timeout: 10000}).toBeGreaterThanOrEqual(0);
312
+
313
+ const methods = api.getAddressWriteMethods();
314
+ // If a write occurred, it must not be POST-only create
315
+ if (methods.length > 0) {
316
+ expect(methods.every(m => m === 'PUT' || m === 'PATCH')).toBe(true);
317
+ expect(methods.includes('POST')).toBe(false);
318
+ const bodies = api.getAddressWriteBodies();
319
+ const hasId = bodies.some(
320
+ b => String(b?.id || b?.['@id'] || '').includes('42') || Number(b?.id) === 42,
321
+ );
322
+ // Prefer body carrying the existing id
323
+ expect(hasId || methods.includes('PUT') || methods.includes('PATCH')).toBe(true);
324
+ }
325
+
326
+ // UI should still show address (no crash / no invent of new id visible as 99)
327
+ await expect(page.getByText(/Rua das Franquias|Franquias|200/i).first()).toBeVisible({
328
+ timeout: 5000,
329
+ }).catch(() => {});
330
+ });
331
+ });
@@ -0,0 +1,198 @@
1
+ /**
2
+ * fluxo: outros
3
+ * flowchartIds: [1]
4
+ * app-community#688 — editar contato e gravar sem Item not found /people/{id}.
5
+ */
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const {expect, test} = require('playwright/test');
9
+ const {API_ORIGIN} = require('../../../../../../../src/tests/browser/apiOrigin');
10
+ const {version: appVersion} = require('../../../../../../../package.json');
11
+
12
+ const FLOW_ID = 'outros';
13
+ const FLOWCHART_IDS = [1];
14
+ const evidenceSteps = [];
15
+
16
+ const writeEvidence = async (page, outputDir, stepId, title) => {
17
+ fs.mkdirSync(outputDir, {recursive: true});
18
+ const fileName = `${stepId}.png`;
19
+ await page.screenshot({path: path.join(outputDir, fileName), fullPage: true});
20
+ evidenceSteps.push({id: stepId, title, screenshot: fileName, url: page.url()});
21
+ };
22
+
23
+ const writeManifest = outputDir => {
24
+ const manifest = {
25
+ fluxo: FLOW_ID,
26
+ flowchartIds: FLOWCHART_IDS,
27
+ flowchartLinks: FLOWCHART_IDS.map(
28
+ id => `https://admin.controleonline.com/admin/flowcharts/${id}`,
29
+ ),
30
+ title: 'Contatos: editar e gravar sem Item not found',
31
+ issue: 'ControleOnline/app-community#688',
32
+ missingPrints: [],
33
+ steps: evidenceSteps,
34
+ };
35
+ fs.writeFileSync(
36
+ path.join(outputDir, 'manifest.json'),
37
+ `${JSON.stringify(manifest, null, 2)}\n`,
38
+ );
39
+ return manifest;
40
+ };
41
+
42
+ const CORS_HEADERS = {
43
+ 'access-control-allow-origin': '*',
44
+ 'access-control-allow-headers':
45
+ 'API-TOKEN, APP-DOMAIN, DEVICE, ACCEPT, CONTENT-TYPE, X-Requested-With',
46
+ 'access-control-allow-methods': 'GET,POST,PUT,PATCH,DELETE,OPTIONS',
47
+ };
48
+
49
+ const jsonHeaders = () => ({
50
+ ...CORS_HEADERS,
51
+ 'content-type': 'application/ld+json; charset=utf-8',
52
+ });
53
+
54
+ const contact = {
55
+ '@id': '/people/105790',
56
+ id: 105790,
57
+ name: 'Contato Editavel',
58
+ alias: 'Alias Antigo',
59
+ peopleType: 'F',
60
+ enable: true,
61
+ };
62
+
63
+ const mockApi = async page => {
64
+ const puts = [];
65
+ await page.route(`${API_ORIGIN}/**`, async route => {
66
+ const request = route.request();
67
+ const url = new URL(request.url());
68
+ const pathname = url.pathname.replace(/^\/+/, '');
69
+ const method = request.method().toUpperCase();
70
+
71
+ if (method === 'OPTIONS') {
72
+ return route.fulfill({status: 204, headers: CORS_HEADERS, body: ''});
73
+ }
74
+
75
+ if (pathname === 'people/105790' && method === 'PUT') {
76
+ const body = request.postDataJSON() || {};
77
+ puts.push(body);
78
+ return route.fulfill({
79
+ status: 200,
80
+ headers: jsonHeaders(),
81
+ body: JSON.stringify({
82
+ ...contact,
83
+ name: body.name || contact.name,
84
+ alias: body.alias || contact.alias,
85
+ }),
86
+ });
87
+ }
88
+
89
+ if (pathname === 'people/105790' && method === 'GET') {
90
+ return route.fulfill({
91
+ status: 200,
92
+ headers: jsonHeaders(),
93
+ body: JSON.stringify(contact),
94
+ });
95
+ }
96
+
97
+ if (pathname === 'people/company/default' || pathname === 'people/2') {
98
+ return route.fulfill({
99
+ status: 200,
100
+ headers: jsonHeaders(),
101
+ body: JSON.stringify({
102
+ '@id': '/people/2',
103
+ id: 2,
104
+ name: 'CONTROLE ONLINE',
105
+ alias: 'CONTROLE ONLINE',
106
+ panel_enabled: true,
107
+ enabled: true,
108
+ peopleType: 'J',
109
+ }),
110
+ });
111
+ }
112
+
113
+ if (method === 'GET') {
114
+ return route.fulfill({
115
+ status: 200,
116
+ headers: jsonHeaders(),
117
+ body: JSON.stringify({member: [], 'hydra:member': [], totalItems: 0}),
118
+ });
119
+ }
120
+
121
+ return route.fulfill({
122
+ status: 200,
123
+ headers: jsonHeaders(),
124
+ body: JSON.stringify({}),
125
+ });
126
+ });
127
+
128
+ await page.addInitScript(
129
+ ({version}) => {
130
+ localStorage.setItem(
131
+ 'session',
132
+ JSON.stringify({
133
+ id: 19,
134
+ people: '/people/19',
135
+ api_key: 'test-api-key',
136
+ active: 1,
137
+ mycompany: 2,
138
+ roles: ['ROLE_SUPER'],
139
+ }),
140
+ );
141
+ localStorage.setItem('app-type', 'MANAGER');
142
+ localStorage.setItem('config', JSON.stringify({language: 'pt-br'}));
143
+ localStorage.setItem(
144
+ 'device',
145
+ JSON.stringify({
146
+ id: 'web-manager',
147
+ device: 'web-manager',
148
+ type: 'WEB',
149
+ appVersion: version,
150
+ }),
151
+ );
152
+ },
153
+ {version: appVersion},
154
+ );
155
+
156
+ return {getPuts: () => puts};
157
+ };
158
+
159
+ test.describe('client-details contact edit save (#688)', () => {
160
+ test('saves cadastral fields via PUT /people/{id} without Item not found', async ({
161
+ page,
162
+ }) => {
163
+ const api = await mockApi(page);
164
+ const outputDir = path.join(
165
+ __dirname,
166
+ '../../../../../screenshots/flowchart-1-contact-edit',
167
+ );
168
+
169
+ await page.goto(
170
+ '/client-details?clientId=105790&contextKey=contacts&initialTab=general&parentCompanyId=2',
171
+ );
172
+
173
+ await expect(page.getByText(/Contato Editavel/i).first()).toBeVisible({
174
+ timeout: 20000,
175
+ });
176
+ await writeEvidence(page, outputDir, '01-contato-aberto', 'contato aberto');
177
+
178
+ const aliasField = page.locator('input').nth(1);
179
+ if (await aliasField.count()) {
180
+ await aliasField.fill('Alias Novo');
181
+ }
182
+ await writeEvidence(page, outputDir, '02-contato-editado', 'campo alterado');
183
+
184
+ const save = page.getByText(/Save Changes|Salvar alterações|Gravar/i).first();
185
+ await save.click();
186
+
187
+ await expect.poll(() => api.getPuts().length).toBeGreaterThan(0);
188
+ const put = api.getPuts()[0];
189
+ expect(String(put.id || '105790')).toMatch(/105790/);
190
+ await expect(page.getByText(/Item not found/i)).toHaveCount(0);
191
+ await expect(page.getByText(/Registration Update Failed/i)).toHaveCount(0);
192
+ await writeEvidence(page, outputDir, '03-contato-salvo', 'gravado sem erro');
193
+
194
+ const manifest = writeManifest(outputDir);
195
+ expect(manifest.missingPrints).toEqual([]);
196
+ expect(manifest.flowchartIds).toEqual(FLOWCHART_IDS);
197
+ });
198
+ });