@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
@@ -1,11 +1,11 @@
1
- const React = require('react');
2
- const renderer = require('react-test-renderer');
3
- const {jest} = require('@jest/globals');
4
-
5
- const {describe, expect, it, beforeEach} = global;
6
-
7
- global.IS_REACT_ACT_ENVIRONMENT = true;
8
-
1
+ const React = require('react');
2
+ const renderer = require('react-test-renderer');
3
+ const {jest} = require('@jest/globals');
4
+
5
+ const {describe, expect, it, beforeEach} = global;
6
+
7
+ global.IS_REACT_ACT_ENVIRONMENT = true;
8
+
9
9
  const peopleActions = {
10
10
  getMediaTypes: jest.fn(),
11
11
  getPeopleMedia: jest.fn(),
@@ -14,7 +14,7 @@ const peopleActions = {
14
14
  deletePeopleMedia: jest.fn(),
15
15
  setItem: jest.fn(),
16
16
  };
17
-
17
+
18
18
  const peopleLinkActions = {
19
19
  getItems: jest.fn(),
20
20
  };
@@ -24,56 +24,56 @@ const messageActions = {
24
24
  showSuccess: jest.fn(),
25
25
  };
26
26
 
27
- jest.mock('@store', () => ({
28
- useStore: jest.fn(name => {
29
- if (name === 'people') {
30
- return {
31
- actions: peopleActions,
32
- getters: {
33
- currentCompany: {id: 12},
34
- },
35
- };
36
- }
37
-
38
- if (name === 'people_link') {
39
- return {
40
- actions: peopleLinkActions,
41
- getters: {},
42
- };
43
- }
44
-
45
- if (name === 'theme') {
46
- return {
47
- getters: {
48
- colors: {},
49
- },
50
- };
51
- }
52
-
53
- return {actions: {}, getters: {}};
54
- }),
55
- useStores: jest.fn(() => ({
56
- actions: peopleActions,
57
- getters: {
58
- currentCompany: {id: 12},
59
- },
60
- })),
61
- }));
62
-
63
- jest.mock('@react-navigation/native', () => ({
64
- useNavigation: () => ({
65
- push: jest.fn(),
66
- }),
67
- }));
68
-
69
- jest.mock('react-native', () => ({
70
- ActivityIndicator: props => React.createElement('activityindicator', props, props.children),
71
- Alert: {
72
- alert: jest.fn(),
73
- },
74
- Keyboard: {
75
- dismiss: jest.fn(),
76
- },
27
+ jest.mock('@store', () => ({
28
+ useStore: jest.fn(name => {
29
+ if (name === 'people') {
30
+ return {
31
+ actions: peopleActions,
32
+ getters: {
33
+ currentCompany: {id: 12},
34
+ },
35
+ };
36
+ }
37
+
38
+ if (name === 'people_link') {
39
+ return {
40
+ actions: peopleLinkActions,
41
+ getters: {},
42
+ };
43
+ }
44
+
45
+ if (name === 'theme') {
46
+ return {
47
+ getters: {
48
+ colors: {},
49
+ },
50
+ };
51
+ }
52
+
53
+ return {actions: {}, getters: {}};
54
+ }),
55
+ useStores: jest.fn(() => ({
56
+ actions: peopleActions,
57
+ getters: {
58
+ currentCompany: {id: 12},
59
+ },
60
+ })),
61
+ }));
62
+
63
+ jest.mock('@react-navigation/native', () => ({
64
+ useNavigation: () => ({
65
+ push: jest.fn(),
66
+ }),
67
+ }));
68
+
69
+ jest.mock('react-native', () => ({
70
+ ActivityIndicator: props => React.createElement('activityindicator', props, props.children),
71
+ Alert: {
72
+ alert: jest.fn(),
73
+ },
74
+ Keyboard: {
75
+ dismiss: jest.fn(),
76
+ },
77
77
  Platform: {OS: 'web'},
78
78
  ScrollView: props => React.createElement('scrollview', props, props.children),
79
79
  StyleSheet: {
@@ -82,36 +82,36 @@ jest.mock('react-native', () => ({
82
82
  Text: props => React.createElement('text', props, props.children),
83
83
  TextInput: props => React.createElement('textinput', props, props.children),
84
84
  TouchableOpacity: props => React.createElement('touchableopacity', props, props.children),
85
- View: props => React.createElement('view', props, props.children),
86
- }));
87
-
88
- jest.mock('react-native-vector-icons/Feather', () => props =>
89
- React.createElement('icon', props, props.children),
90
- {virtual: true},
91
- );
92
-
93
- jest.mock('react-native-vector-icons/FontAwesome', () => props =>
94
- React.createElement('icon', props, props.children),
95
- {virtual: true},
96
- );
97
-
98
- jest.mock('react-native-vector-icons/MaterialIcons', () => props =>
99
- React.createElement('icon', props, props.children),
100
- {virtual: true},
101
- );
102
-
103
- jest.mock('@react-native-picker/picker', () => ({
104
- Picker: props => React.createElement('picker', props, props.children),
105
- }), {virtual: true});
106
-
107
- jest.mock('@controleonline/ui-common/src/react/components/AnimatedModal', () => props =>
108
- (props.visible ? React.createElement('modal', props, props.children) : null),
109
- );
110
-
85
+ View: props => React.createElement('view', props, props.children),
86
+ }));
87
+
88
+ jest.mock('react-native-vector-icons/Feather', () => props =>
89
+ React.createElement('icon', props, props.children),
90
+ {virtual: true},
91
+ );
92
+
93
+ jest.mock('react-native-vector-icons/FontAwesome', () => props =>
94
+ React.createElement('icon', props, props.children),
95
+ {virtual: true},
96
+ );
97
+
98
+ jest.mock('react-native-vector-icons/MaterialIcons', () => props =>
99
+ React.createElement('icon', props, props.children),
100
+ {virtual: true},
101
+ );
102
+
103
+ jest.mock('@react-native-picker/picker', () => ({
104
+ Picker: props => React.createElement('picker', props, props.children),
105
+ }), {virtual: true});
106
+
107
+ jest.mock('@controleonline/ui-common/src/react/components/AnimatedModal', () => props =>
108
+ (props.visible ? React.createElement('modal', props, props.children) : null),
109
+ );
110
+
111
111
  jest.mock('@controleonline/ui-common/src/react/components/MessageService', () => ({
112
112
  useMessage: () => messageActions,
113
113
  }));
114
-
114
+
115
115
  jest.mock('@controleonline/ui-common/src/react/components/UserAvatar', () => props =>
116
116
  React.createElement('avatar', props, props.children),
117
117
  );
@@ -139,138 +139,138 @@ const MediaTab =
139
139
  require('@controleonline/ui-customers/src/react/components/tabs/MediaTab').default;
140
140
  const employeesTabModule =
141
141
  require('@controleonline/ui-customers/src/react/components/tabs/EmployeesTab');
142
- const EmployeesTab = employeesTabModule.default;
143
- const {
144
- formatEmployeeContactMeta,
145
- formatEmployeeContactTitle,
146
- } = employeesTabModule;
147
-
148
- const flush = () => new Promise(resolve => setImmediate(resolve));
149
-
150
- describe('people media tabs', () => {
151
- beforeEach(() => {
152
- jest.clearAllMocks();
153
- });
154
-
155
- it('loads company icons through the people store on SalesmanTab', async () => {
156
- peopleLinkActions.getItems.mockResolvedValue([
157
- {
158
- id: 1,
159
- linkType: 'sellers-client',
160
- people: {id: '/people/12'},
161
- company: {id: '/people/55', name: 'Companhia 55'},
162
- },
163
- ]);
164
- peopleActions.getPeopleMedia.mockResolvedValue([
165
- {
166
- id: 9,
167
- file: {id: 91, fileName: 'icon.png'},
168
- },
169
- ]);
170
-
171
- await renderer.act(async () => {
172
- renderer.create(
173
- React.createElement(SalesmanTab, {
174
- client: {id: 12},
175
- customStyles: {
176
- tabContent: {},
177
- section: {},
178
- loadingIndicator: {color: '#000'},
179
- emptyText: {},
180
- listItem: {},
181
- listItemWithEndAction: {},
182
- itemContent: {},
183
- listAvatarBrand: {backgroundColor: '#fff', borderColor: '#ddd'},
184
- listAvatarText: {color: '#000'},
185
- listAvatar: {},
186
- itemText: {},
187
- itemSubtext: {},
188
- iconButtonGhost: {},
189
- iconButtonGhostIcon: {color: '#000'},
190
- },
191
- linkType: 'sellers-client',
192
- emptyText: 'empty',
193
- errorText: 'error',
194
- }),
195
- );
196
- await flush();
197
- });
198
-
199
- expect(peopleActions.getPeopleMedia).toHaveBeenCalledWith({
200
- people: '/people/55',
201
- 'mediaType.type': 'icon',
202
- itemsPerPage: 1,
203
- });
204
- });
205
-
206
- it('loads avatars through the people store on EmployeesTab', async () => {
207
- peopleLinkActions.getItems.mockResolvedValue([
208
- {
209
- id: 2,
210
- linkType: 'employee',
211
- people: {id: '/people/77', peopleType: 'F'},
212
- company: {id: '/people/12'},
213
- },
214
- ]);
215
- peopleActions.getPeopleMedia.mockResolvedValue([
216
- {
217
- id: 10,
218
- file: {id: 101, fileName: 'avatar.png'},
219
- },
220
- ]);
221
-
222
- await renderer.act(async () => {
223
- renderer.create(
224
- React.createElement(EmployeesTab, {
225
- client: {id: 12},
226
- customStyles: {
227
- tabContent: {},
228
- section: {},
229
- sectionHeader: {},
230
- sectionTitle: {},
231
- iconButtonPrimary: {},
232
- iconButtonPrimaryIcon: {color: '#000'},
233
- loadingIndicator: {color: '#000'},
234
- emptyText: {},
235
- txt_title_emptyText: {},
236
- listItem: {},
237
- listItemWithEndAction: {},
238
- itemContent: {},
239
- listAvatarBrand: {backgroundColor: '#fff', borderColor: '#ddd'},
240
- listAvatarText: {color: '#000'},
241
- listAvatar: {},
242
- itemText: {},
243
- itemSubtext: {},
244
- iconButtonGhost: {},
245
- iconButtonGhostIcon: {color: '#000'},
246
- saveButton: {},
247
- saveButtonDisabled: {},
248
- saveButtonText: {color: '#000'},
249
- },
250
- }),
251
- );
252
- await flush();
253
- });
254
-
255
- expect(peopleActions.getPeopleMedia).toHaveBeenCalledWith({
256
- people: '/people/77',
257
- 'mediaType.type': 'avatar',
258
- itemsPerPage: 1,
259
- });
260
- });
261
-
142
+ const EmployeesTab = employeesTabModule.default;
143
+ const {
144
+ formatEmployeeContactMeta,
145
+ formatEmployeeContactTitle,
146
+ } = employeesTabModule;
147
+
148
+ const flush = () => new Promise(resolve => setImmediate(resolve));
149
+
150
+ describe('people media tabs', () => {
151
+ beforeEach(() => {
152
+ jest.clearAllMocks();
153
+ });
154
+
155
+ it('loads company icons through the people store on SalesmanTab', async () => {
156
+ peopleLinkActions.getItems.mockResolvedValue([
157
+ {
158
+ id: 1,
159
+ linkType: 'sellers-client',
160
+ people: {id: '/people/12'},
161
+ company: {id: '/people/55', name: 'Companhia 55'},
162
+ },
163
+ ]);
164
+ peopleActions.getPeopleMedia.mockResolvedValue([
165
+ {
166
+ id: 9,
167
+ file: {id: 91, fileName: 'icon.png'},
168
+ },
169
+ ]);
170
+
171
+ await renderer.act(async () => {
172
+ renderer.create(
173
+ React.createElement(SalesmanTab, {
174
+ client: {id: 12},
175
+ customStyles: {
176
+ tabContent: {},
177
+ section: {},
178
+ loadingIndicator: {color: '#000'},
179
+ emptyText: {},
180
+ listItem: {},
181
+ listItemWithEndAction: {},
182
+ itemContent: {},
183
+ listAvatarBrand: {backgroundColor: '#fff', borderColor: '#ddd'},
184
+ listAvatarText: {color: '#000'},
185
+ listAvatar: {},
186
+ itemText: {},
187
+ itemSubtext: {},
188
+ iconButtonGhost: {},
189
+ iconButtonGhostIcon: {color: '#000'},
190
+ },
191
+ linkType: 'sellers-client',
192
+ emptyText: 'empty',
193
+ errorText: 'error',
194
+ }),
195
+ );
196
+ await flush();
197
+ });
198
+
199
+ expect(peopleActions.getPeopleMedia).toHaveBeenCalledWith({
200
+ people: '/people/55',
201
+ 'mediaType.type': 'icon',
202
+ itemsPerPage: 1,
203
+ });
204
+ });
205
+
206
+ it('loads avatars through the people store on EmployeesTab', async () => {
207
+ peopleLinkActions.getItems.mockResolvedValue([
208
+ {
209
+ id: 2,
210
+ linkType: 'employee',
211
+ people: {id: '/people/77', peopleType: 'F'},
212
+ company: {id: '/people/12'},
213
+ },
214
+ ]);
215
+ peopleActions.getPeopleMedia.mockResolvedValue([
216
+ {
217
+ id: 10,
218
+ file: {id: 101, fileName: 'avatar.png'},
219
+ },
220
+ ]);
221
+
222
+ await renderer.act(async () => {
223
+ renderer.create(
224
+ React.createElement(EmployeesTab, {
225
+ client: {id: 12},
226
+ customStyles: {
227
+ tabContent: {},
228
+ section: {},
229
+ sectionHeader: {},
230
+ sectionTitle: {},
231
+ iconButtonPrimary: {},
232
+ iconButtonPrimaryIcon: {color: '#000'},
233
+ loadingIndicator: {color: '#000'},
234
+ emptyText: {},
235
+ txt_title_emptyText: {},
236
+ listItem: {},
237
+ listItemWithEndAction: {},
238
+ itemContent: {},
239
+ listAvatarBrand: {backgroundColor: '#fff', borderColor: '#ddd'},
240
+ listAvatarText: {color: '#000'},
241
+ listAvatar: {},
242
+ itemText: {},
243
+ itemSubtext: {},
244
+ iconButtonGhost: {},
245
+ iconButtonGhostIcon: {color: '#000'},
246
+ saveButton: {},
247
+ saveButtonDisabled: {},
248
+ saveButtonText: {color: '#000'},
249
+ },
250
+ }),
251
+ );
252
+ await flush();
253
+ });
254
+
255
+ expect(peopleActions.getPeopleMedia).toHaveBeenCalledWith({
256
+ people: '/people/77',
257
+ 'mediaType.type': 'avatar',
258
+ itemsPerPage: 1,
259
+ });
260
+ });
261
+
262
262
  it('formats contact rows as name alias and id link type', () => {
263
- const contact = {
264
- id: '/people/77',
265
- name: 'Maria Silva',
266
- alias: 'Mari',
267
- linkType: 'employee',
268
- peopleLink: {
269
- linkType: 'owner',
270
- },
271
- };
272
-
273
- expect(formatEmployeeContactTitle(contact)).toBe('MARIA SILVA / MARI');
263
+ const contact = {
264
+ id: '/people/77',
265
+ name: 'Maria Silva',
266
+ alias: 'Mari',
267
+ linkType: 'employee',
268
+ peopleLink: {
269
+ linkType: 'owner',
270
+ },
271
+ };
272
+
273
+ expect(formatEmployeeContactTitle(contact)).toBe('Maria Silva / Mari');
274
274
  expect(formatEmployeeContactMeta(contact)).toBe('ID: 77 / owner');
275
275
  });
276
276
 
@@ -0,0 +1,47 @@
1
+ import assert from 'node:assert/strict';
2
+ import fs from 'node:fs';
3
+ import test from 'node:test';
4
+
5
+ const helpers = fs.readFileSync(
6
+ new URL('../../../../react/components/tabs/generalTabHelpers.js', import.meta.url),
7
+ 'utf8',
8
+ );
9
+ const employeesHelpers = fs.readFileSync(
10
+ new URL('../../../../react/components/tabs/employeesTabHelpers.js', import.meta.url),
11
+ 'utf8',
12
+ );
13
+ const contacts = fs.readFileSync(
14
+ new URL('../../../../react/components/tabs/employeeContacts.js', import.meta.url),
15
+ 'utf8',
16
+ );
17
+ const generalTab = fs.readFileSync(
18
+ new URL('../../../../react/components/tabs/GeneralTab.js', import.meta.url),
19
+ 'utf8',
20
+ );
21
+ const createFields = fs.readFileSync(
22
+ new URL('../../../../react/components/tabs/EmployeeCreateFormFields.js', import.meta.url),
23
+ 'utf8',
24
+ );
25
+
26
+ test('peopleType contract is F/J only — contrato is not a People type', () => {
27
+ assert.match(helpers, /PEOPLE_TYPE_OPTIONS/);
28
+ assert.match(helpers, /value: 'F'/);
29
+ assert.match(helpers, /value: 'J'/);
30
+ assert.doesNotMatch(helpers, /value: 'contrato'/);
31
+ assert.match(helpers, /do not invent values/);
32
+ });
33
+
34
+ test('create payload and general save persist peopleType from the form', () => {
35
+ assert.match(employeesHelpers, /peopleType: normalizePeopleType\(peopleType\)/);
36
+ assert.match(generalTab, /peopleType,/);
37
+ assert.match(generalTab, /onUpdateClient\?\.\('peopleType', peopleType\)/);
38
+ assert.match(createFields, /PEOPLE_TYPE_OPTIONS/);
39
+ });
40
+
41
+ test('employee list keeps PJ collaborators linked to the company', () => {
42
+ assert.match(contacts, /peopleType F\|J both allowed as collaborators/);
43
+ assert.doesNotMatch(
44
+ contacts,
45
+ /peopleType \|\| ''\)\.toUpperCase\(\) === 'J'/,
46
+ );
47
+ });
@@ -0,0 +1,20 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import {createRequire} from 'node:module';
4
+
5
+ const require = createRequire(import.meta.url);
6
+ const {
7
+ resolveSeededLinkType,
8
+ normalizeLinkType,
9
+ } = require('../../../../react/components/tabs/generalTabHelpers.js');
10
+
11
+ test('resolveSeededLinkType prefers client linkType over route seed', () => {
12
+ assert.equal(resolveSeededLinkType('courier', 'owner'), 'courier');
13
+ assert.equal(resolveSeededLinkType('after-sales', 'employee'), 'after-sales');
14
+ });
15
+
16
+ test('resolveSeededLinkType falls back to route when client empty', () => {
17
+ assert.equal(resolveSeededLinkType('', 'owner'), 'owner');
18
+ assert.equal(resolveSeededLinkType(undefined, 'manager'), 'manager');
19
+ assert.equal(resolveSeededLinkType(null, ''), normalizeLinkType(''));
20
+ });
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Regression: GIT BUILD ERROR — Identifier 'COMPANY_ICON_MEDIA_TYPES' has already been declared
3
+ * (import from salesmanTabMedia + local const redeclaration in SalesmanTab).
4
+ * Refs: ControleOnline/ui-customers#20
5
+ */
6
+ import assert from 'node:assert/strict';
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+
11
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
+ const tabsDir = path.resolve(__dirname, '../../../../react/components/tabs');
13
+ const tabSrc = fs.readFileSync(path.join(tabsDir, 'SalesmanTab.js'), 'utf8');
14
+ const mediaSrc = fs.readFileSync(path.join(tabsDir, 'salesmanTabMedia.js'), 'utf8');
15
+
16
+ assert.match(
17
+ tabSrc,
18
+ /from ['"]\.\/salesmanTabMedia['"]/,
19
+ 'SalesmanTab must import from ./salesmanTabMedia',
20
+ );
21
+ assert.match(tabSrc, /COMPANY_ICON_MEDIA_TYPES/, 'SalesmanTab must reference COMPANY_ICON_MEDIA_TYPES');
22
+ assert.doesNotMatch(
23
+ tabSrc,
24
+ /const COMPANY_ICON_MEDIA_TYPES\s*=/,
25
+ 'SalesmanTab must NOT redeclare COMPANY_ICON_MEDIA_TYPES locally (causes SyntaxError on web bundle)',
26
+ );
27
+ assert.match(
28
+ mediaSrc,
29
+ /export \{[^}]*COMPANY_ICON_MEDIA_TYPES/,
30
+ 'salesmanTabMedia must export COMPANY_ICON_MEDIA_TYPES',
31
+ );
32
+
33
+ console.log('ok: salesmanTabMedia single source of truth for COMPANY_ICON_MEDIA_TYPES');
@@ -6,6 +6,8 @@ const {
6
6
  toTimezoneItem,
7
7
  extractCollectionItems,
8
8
  extractErrorMessage,
9
+ buildUsersListQuery,
10
+ embeddedUsersFromClient,
9
11
  } = require('../../../../react/components/tabs/usersTabHelpers');
10
12
 
11
13
  describe('usersTabHelpers', () => {
@@ -51,6 +53,24 @@ describe('usersTabHelpers', () => {
51
53
  expect(item.username).toBe('alice');
52
54
  });
53
55
 
56
+ test('buildUsersListQuery uses people IRI and does not send company', () => {
57
+ const query = buildUsersListQuery({id: 31468, company: 3});
58
+ expect(query.people).toBe('/people/31468');
59
+ expect(query.itemsPerPage).toBe(100);
60
+ expect(query.__storeMeta.dedupeKey).toBe('client-details-users-/people/31468');
61
+ expect(query.company).toBeUndefined();
62
+ expect(buildUsersListQuery({})).toBe(null);
63
+ });
64
+
65
+ test('embeddedUsersFromClient is fallback only and normalizes items', () => {
66
+ const items = embeddedUsersFromClient({
67
+ user: {id: 9, username: 'alice'},
68
+ });
69
+ expect(items).toHaveLength(1);
70
+ expect(items[0].username).toBe('alice');
71
+ expect(embeddedUsersFromClient({user: []})).toEqual([]);
72
+ });
73
+
54
74
  test('extractCollectionItems supports hydra and member', () => {
55
75
  expect(extractCollectionItems({'hydra:member': [{id: 1}]}).length).toBe(1);
56
76
  expect(extractCollectionItems({member: [{id: 2}]}).length).toBe(1);
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Unit tests for UsersTab pure helpers (#369).
3
+ */
4
+ import { describe, it } from 'node:test';
5
+ import assert from 'node:assert/strict';
6
+ import {
7
+ extractId,
8
+ toPeopleIri,
9
+ normalizeUserItem,
10
+ mapUsersForClient,
11
+ extractErrorMessage,
12
+ buildCreateUserPayload,
13
+ formatApiKeyPreview,
14
+ } from '../../../../react/components/tabs/usersTabHelpers.js';
15
+
16
+ describe('usersTabHelpers', () => {
17
+ it('extractId strips non-digits', () => {
18
+ assert.equal(extractId('/people/106218'), '106218');
19
+ assert.equal(extractId('106218'), '106218');
20
+ assert.equal(extractId(null), '');
21
+ });
22
+
23
+ it('toPeopleIri builds API Platform IRI', () => {
24
+ assert.equal(toPeopleIri(106218), '/people/106218');
25
+ assert.equal(toPeopleIri('/people/9'), '/people/9');
26
+ assert.equal(toPeopleIri(''), '');
27
+ });
28
+
29
+ it('buildCreateUserPayload uses people IRI', () => {
30
+ const payload = buildCreateUserPayload({
31
+ username: 'alice',
32
+ password: 'secret',
33
+ confirmPassword: 'secret',
34
+ peopleId: 106218,
35
+ });
36
+ assert.equal(payload.people, '/people/106218');
37
+ assert.equal(payload.username, 'alice');
38
+ });
39
+
40
+ it('extractErrorMessage maps 401 / Authentication required', () => {
41
+ const msg = extractErrorMessage({
42
+ response: {status: 401, data: {message: 'Authentication required'}},
43
+ });
44
+ assert.match(msg, /Autenticação necessária/);
45
+ assert.match(
46
+ extractErrorMessage({message: 'Authentication required'}),
47
+ /Autenticação necessária/,
48
+ );
49
+ });
50
+
51
+ it('normalizeUserItem and mapUsersForClient', () => {
52
+ const n = normalizeUserItem({id: '/users/1', username: 'bob', apiKey: 'k'});
53
+ assert.equal(n.id, '1');
54
+ assert.equal(n.username, 'bob');
55
+ const mapped = mapUsersForClient([n]);
56
+ assert.equal(mapped[0].username, 'bob');
57
+ });
58
+
59
+ it('formatApiKeyPreview truncates long keys', () => {
60
+ assert.equal(formatApiKeyPreview('short'), 'short');
61
+ assert.match(formatApiKeyPreview('abcdefghijklmnopqrstuvwxyz'), /\.\.\./);
62
+ });
63
+ });