eionet2-dashboard 3.2.1 → 3.2.3

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/CHANGELOG.md +37 -0
  2. package/Jenkinsfile +2 -5
  3. package/api/getGraphData/index.js +3 -4
  4. package/package.json +2 -2
  5. package/tabs/package-lock.json +2927 -1866
  6. package/tabs/package.json +3 -3
  7. package/tabs/public/auth-start.html +2 -2
  8. package/tabs/src/components/App.test.jsx +67 -0
  9. package/tabs/src/components/BottomMenu.test.jsx +94 -0
  10. package/tabs/src/components/CustomColumnResizeIcon.jsx +1 -1
  11. package/tabs/src/components/CustomColumnResizeIcon.test.jsx +17 -0
  12. package/tabs/src/components/CustomDrawer.test.jsx +14 -0
  13. package/tabs/src/components/EventDialogTitle.test.jsx +25 -0
  14. package/tabs/src/components/HtmlBox.test.jsx +41 -0
  15. package/tabs/src/components/Privacy.test.jsx +11 -0
  16. package/tabs/src/components/ResizableGrid.test.jsx +64 -0
  17. package/tabs/src/components/Tab.test.jsx +463 -0
  18. package/tabs/src/components/TabConfig.test.jsx +27 -0
  19. package/tabs/src/components/TabPanel.test.jsx +31 -0
  20. package/tabs/src/components/TermsOfUse.test.jsx +11 -0
  21. package/tabs/src/components/UnderConstruction.test.jsx +13 -0
  22. package/tabs/src/components/UserMenu.test.jsx +53 -0
  23. package/tabs/src/components/activity/Activity.test.jsx +218 -0
  24. package/tabs/src/components/activity/ConsultationList.test.jsx +114 -0
  25. package/tabs/src/components/activity/EventList.test.jsx +164 -0
  26. package/tabs/src/components/activity/GroupsTags.test.jsx +23 -0
  27. package/tabs/src/components/activity/ObligationList.test.jsx +46 -0
  28. package/tabs/src/components/activity/PublicationList.test.jsx +46 -0
  29. package/tabs/src/components/activity/Reporting.test.jsx +11 -0
  30. package/tabs/src/components/event_rating/EventRating.test.jsx +63 -0
  31. package/tabs/src/components/event_rating/EventRatingDialog.test.jsx +28 -0
  32. package/tabs/src/components/event_registration/Approval.test.jsx +25 -0
  33. package/tabs/src/components/event_registration/ApprovalDialog.test.jsx +25 -0
  34. package/tabs/src/components/event_registration/ApprovalList.test.jsx +36 -0
  35. package/tabs/src/components/event_registration/EventExternalRegistration.test.jsx +219 -0
  36. package/tabs/src/components/event_registration/EventRegistration.test.jsx +208 -0
  37. package/tabs/src/components/my_country/AtAGlance.test.jsx +157 -0
  38. package/tabs/src/components/my_country/CountryMembers.test.jsx +117 -0
  39. package/tabs/src/components/my_country/CountryProgress.test.jsx +21 -0
  40. package/tabs/src/components/my_country/DataReporters.jsx +2 -3
  41. package/tabs/src/components/my_country/DataReporters.test.jsx +155 -0
  42. package/tabs/src/components/my_country/FullCircularProgress.test.jsx +19 -0
  43. package/tabs/src/components/my_country/GroupView.test.jsx +165 -0
  44. package/tabs/src/components/my_country/GroupsBoard.test.jsx +30 -0
  45. package/tabs/src/components/my_country/IndicatorCard.test.jsx +27 -0
  46. package/tabs/src/components/my_country/ManagementBoard.test.jsx +119 -0
  47. package/tabs/src/components/my_country/MyCountry.test.jsx +220 -0
  48. package/tabs/src/components/my_country/ProgressGauge.test.jsx +34 -0
  49. package/tabs/src/components/my_country/ScientificCommittee.test.jsx +11 -0
  50. package/tabs/src/components/my_country/UserCard.test.jsx +24 -0
  51. package/tabs/src/components/my_country/YearlyProgress.test.jsx +33 -0
  52. package/tabs/src/components/self_service/UserEdit.test.jsx +213 -0
  53. package/tabs/src/data/apiProvider.test.js +228 -0
  54. package/tabs/src/data/icsHelper.test.js +76 -0
  55. package/tabs/src/data/provider.test.js +351 -0
  56. package/tabs/src/data/reportingProvider.test.js +103 -0
  57. package/tabs/src/data/selfServiceProvider.test.js +108 -0
  58. package/tabs/src/data/selfServiceSharepointProvider.test.js +100 -0
  59. package/tabs/src/data/sharepointProvider.test.js +669 -0
  60. package/tabs/src/data/validator.test.js +34 -2
  61. package/tabs/yarn.lock +415 -414
  62. package/tabs/src/components/CustomGridToolbar.jsx +0 -202
@@ -0,0 +1,28 @@
1
+ import React from 'react';
2
+ import { renderToStaticMarkup } from 'react-dom/server';
3
+ import { EventRatingDialog } from './EventRatingDialog';
4
+
5
+ jest.mock('../../data/hooks/useConfiguration', () => ({
6
+ useConfiguration: () => ({ DateFormatDashboard: 'dd-MMM-yyyy' }),
7
+ }));
8
+ jest.mock('../event_rating/EventRating', () => ({
9
+ EventRating: () => <div>event-rating-content</div>,
10
+ }));
11
+ jest.mock('../EventDialogTitle', () => ({
12
+ EventDialogTitle: () => <div>event-dialog-title</div>,
13
+ }));
14
+
15
+ describe('EventRatingDialog', () => {
16
+ test('renders title and rating component', () => {
17
+ const html = renderToStaticMarkup(
18
+ <EventRatingDialog
19
+ open={true}
20
+ handleClose={jest.fn()}
21
+ event={{ Title: 'Event 1', MeetingStart: new Date(), MeetingEnd: new Date() }}
22
+ participant={{}}
23
+ />,
24
+ );
25
+
26
+ expect(html).toContain('MuiDialog-root');
27
+ });
28
+ });
@@ -0,0 +1,25 @@
1
+ import React from 'react';
2
+ import { renderToStaticMarkup } from 'react-dom/server';
3
+ import { Approval } from './Approval';
4
+
5
+ describe('Approval', () => {
6
+ test('renders participant details and invited by NFP state', () => {
7
+ const html = renderToStaticMarkup(
8
+ <Approval
9
+ participant={{
10
+ id: 1,
11
+ ParticipantName: 'User 1',
12
+ Email: 'user1@example.org',
13
+ NFPApproved: 'No value',
14
+ PhysicalParticipation: false,
15
+ EEAReimbursementRequested: false,
16
+ IsInvitedByNFP: true,
17
+ }}
18
+ />,
19
+ );
20
+
21
+ expect(html).toContain('User 1');
22
+ expect(html).toContain('user1@example.org');
23
+ expect(html).toContain('Invited by NFP');
24
+ });
25
+ });
@@ -0,0 +1,25 @@
1
+ import React from 'react';
2
+ import { renderToStaticMarkup } from 'react-dom/server';
3
+ import { ApprovalDialog } from './ApprovalDialog';
4
+
5
+ jest.mock('./ApprovalList', () => ({
6
+ ApprovalList: () => <div>approval-list-content</div>,
7
+ }));
8
+ jest.mock('../EventDialogTitle', () => ({
9
+ EventDialogTitle: () => <div>event-dialog-title</div>,
10
+ }));
11
+
12
+ describe('ApprovalDialog', () => {
13
+ test('renders approval list and event title section', () => {
14
+ const html = renderToStaticMarkup(
15
+ <ApprovalDialog
16
+ open={true}
17
+ handleClose={jest.fn()}
18
+ event={{ Title: 'Event 1' }}
19
+ userInfo={{}}
20
+ />,
21
+ );
22
+
23
+ expect(html).toContain('MuiDialog-root');
24
+ });
25
+ });
@@ -0,0 +1,36 @@
1
+ import React from 'react';
2
+ import { renderToStaticMarkup } from 'react-dom/server';
3
+ import { ApprovalList } from './ApprovalList';
4
+
5
+ jest.mock('../../data/hooks/useConfiguration', () => ({
6
+ useConfiguration: () => ({ EventApprovalInfo: 'Approval info' }),
7
+ }));
8
+ jest.mock('../../data/sharepointProvider', () => ({
9
+ patchParticipants: jest.fn(),
10
+ }));
11
+ jest.mock('./Approval', () => ({
12
+ Approval: ({ participant }) => <div>approval-{participant.id}</div>,
13
+ }));
14
+ jest.mock('../HtmlBox', () => ({
15
+ HtmlBox: ({ html }) => <div>{html}</div>,
16
+ }));
17
+
18
+ describe('ApprovalList', () => {
19
+ test('renders participants list and update button', () => {
20
+ const html = renderToStaticMarkup(
21
+ <ApprovalList
22
+ event={{
23
+ Participants: [
24
+ { id: 1, ParticipantName: 'A', Email: 'a@example.org' },
25
+ { id: 2, ParticipantName: 'B', Email: 'b@example.org' },
26
+ ],
27
+ }}
28
+ />,
29
+ );
30
+
31
+ expect(html).toContain('approval-1');
32
+ expect(html).toContain('approval-2');
33
+ expect(html).toContain('Approval info');
34
+ expect(html).toContain('Update');
35
+ });
36
+ });
@@ -0,0 +1,219 @@
1
+ jest.mock('react', () => {
2
+ const actual = jest.requireActual('react');
3
+ return {
4
+ ...actual,
5
+ React: actual,
6
+ useState: jest.fn(actual.useState),
7
+ };
8
+ });
9
+
10
+ import React from 'react';
11
+ import { renderToStaticMarkup } from 'react-dom/server';
12
+ import { EventExternalRegistration } from './EventExternalRegistration';
13
+ import { postParticipant } from '../../data/sharepointProvider';
14
+ import { getUserByMail } from '../../data/provider';
15
+
16
+ const buttonHandlers = [];
17
+
18
+ jest.mock('@mui/material', () => {
19
+ const ReactLocal = require('react');
20
+ const passthrough =
21
+ (Tag = 'div') =>
22
+ ({ children }) =>
23
+ ReactLocal.createElement(Tag, {}, children);
24
+
25
+ const labelToText = (children) => {
26
+ if (typeof children === 'string') {
27
+ return children;
28
+ }
29
+ if (Array.isArray(children)) {
30
+ return children
31
+ .filter((v) => typeof v === 'string')
32
+ .join(' ')
33
+ .trim();
34
+ }
35
+ return '';
36
+ };
37
+
38
+ return {
39
+ Alert: passthrough(),
40
+ Box: passthrough(),
41
+ Checkbox: passthrough('input'),
42
+ TextField: ({ label }) => <div>{label || ''}</div>,
43
+ Button: ({ onClick, children }) => {
44
+ const label = labelToText(children);
45
+ if (onClick) {
46
+ buttonHandlers.push({ label, onClick });
47
+ }
48
+ return <button>{children}</button>;
49
+ },
50
+ FormControlLabel: ({ label, control }) => (
51
+ <div>
52
+ {label}
53
+ {control}
54
+ </div>
55
+ ),
56
+ CircularProgress: passthrough('span'),
57
+ Backdrop: passthrough(),
58
+ };
59
+ });
60
+
61
+ jest.mock('@mui/icons-material/Check', () => () => <span>check-icon</span>);
62
+ jest.mock('@mui/icons-material/Save', () => () => <span>save-icon</span>);
63
+
64
+ jest.mock('../../data/hooks/useConfiguration', () => ({
65
+ useConfiguration: () => ({
66
+ NFPInvitationInfoMessage: 'Invitation info',
67
+ NFPInvitationSuccessMessage: 'Invitation success',
68
+ EventInvitationByNFPError: 'Already invited',
69
+ }),
70
+ }));
71
+
72
+ jest.mock('../../data/sharepointProvider', () => ({
73
+ postParticipant: jest.fn(),
74
+ }));
75
+
76
+ jest.mock('../../data/provider', () => ({
77
+ getUserByMail: jest.fn(),
78
+ }));
79
+
80
+ jest.mock('../HtmlBox', () => ({
81
+ HtmlBox: ({ html }) => <div>{html}</div>,
82
+ }));
83
+
84
+ function mockStateSequence(values) {
85
+ let index = 0;
86
+ React.useState.mockImplementation((initialValue) => {
87
+ if (index < values.length) {
88
+ const current = values[index];
89
+ index += 1;
90
+ return [current, jest.fn()];
91
+ }
92
+ return [initialValue, jest.fn()];
93
+ });
94
+ }
95
+
96
+ function buildState(participantOverride = {}) {
97
+ const participant = {
98
+ MeetingId: 1,
99
+ ParticipantName: 'John Doe',
100
+ Email: 'john@example.org',
101
+ PhysicalParticipation: false,
102
+ EEAReimbursementRequested: false,
103
+ Registered: true,
104
+ RegistrationDate: new Date('2025-01-01'),
105
+ Country: 'RO',
106
+ NFPApproved: 'Approved',
107
+ CustomMeetingRequest: 'Registered by NFP',
108
+ IsInvitedByNFP: true,
109
+ ...participantOverride,
110
+ };
111
+
112
+ return [participant, false, '', {}, false, false, false];
113
+ }
114
+
115
+ describe('EventExternalRegistration', () => {
116
+ const baseEvent = {
117
+ id: 1,
118
+ IsOffline: true,
119
+ Participants: [],
120
+ NoOfRegistered: 0,
121
+ };
122
+
123
+ beforeEach(() => {
124
+ jest.clearAllMocks();
125
+ buttonHandlers.length = 0;
126
+ React.useState.mockImplementation((initialValue) => [initialValue, jest.fn()]);
127
+ getUserByMail.mockResolvedValue(null);
128
+ postParticipant.mockResolvedValue({ id: 77 });
129
+ });
130
+
131
+ test('renders external registration form', () => {
132
+ mockStateSequence(buildState());
133
+
134
+ const html = renderToStaticMarkup(
135
+ <EventExternalRegistration event={{ ...baseEvent }} userInfo={{ country: 'RO' }} />,
136
+ );
137
+
138
+ expect(html).toContain('Invitation info');
139
+ expect(html).toContain('Register');
140
+ expect(html).toContain('Physical participation');
141
+ expect(html).toContain('Reimbursement requested');
142
+ });
143
+
144
+ test('successfully registers external participant', async () => {
145
+ const event = {
146
+ ...baseEvent,
147
+ Participants: [
148
+ { Email: 'a@x.org', Registered: true },
149
+ { Email: 'b@x.org', Registered: false },
150
+ ],
151
+ };
152
+
153
+ const state = buildState({ Email: 'new.user@domain.org', ParticipantName: 'New User' });
154
+ const participant = state[0];
155
+ mockStateSequence(state);
156
+
157
+ renderToStaticMarkup(<EventExternalRegistration event={event} userInfo={{ country: 'RO' }} />);
158
+
159
+ const register = buttonHandlers.find((b) => b.label === 'Register');
160
+ expect(register).toBeDefined();
161
+
162
+ await register.onClick();
163
+
164
+ expect(getUserByMail).toHaveBeenCalledWith('new.user@domain.org');
165
+ expect(postParticipant).toHaveBeenCalledWith(participant, event);
166
+ expect(participant.id).toBe(77);
167
+ expect(event.Participants.length).toBe(3);
168
+ expect(event.NoOfRegistered).toBe(2);
169
+ });
170
+
171
+ test('does not post when email is invalid', async () => {
172
+ const state = buildState({ Email: 'invalid-email', ParticipantName: 'Valid Name' });
173
+ mockStateSequence(state);
174
+
175
+ renderToStaticMarkup(
176
+ <EventExternalRegistration event={{ ...baseEvent }} userInfo={{ country: 'RO' }} />,
177
+ );
178
+
179
+ const register = buttonHandlers.find((b) => b.label === 'Register');
180
+ await register.onClick();
181
+
182
+ expect(getUserByMail).not.toHaveBeenCalled();
183
+ expect(postParticipant).not.toHaveBeenCalled();
184
+ });
185
+
186
+ test('does not post EEA addresses', async () => {
187
+ const state = buildState({ Email: 'user@eea.europa.eu', ParticipantName: 'Valid Name' });
188
+ mockStateSequence(state);
189
+
190
+ renderToStaticMarkup(
191
+ <EventExternalRegistration event={{ ...baseEvent }} userInfo={{ country: 'RO' }} />,
192
+ );
193
+
194
+ const register = buttonHandlers.find((b) => b.label === 'Register');
195
+ await register.onClick();
196
+
197
+ expect(getUserByMail).not.toHaveBeenCalled();
198
+ expect(postParticipant).not.toHaveBeenCalled();
199
+ });
200
+
201
+ test('does not post when user exists or already invited', async () => {
202
+ const event = {
203
+ ...baseEvent,
204
+ Participants: [{ Email: 'existing@domain.org', Registered: true }],
205
+ };
206
+
207
+ const state = buildState({ Email: 'existing@domain.org', ParticipantName: 'Valid Name' });
208
+ mockStateSequence(state);
209
+ getUserByMail.mockResolvedValue({ SharepointUser: true });
210
+
211
+ renderToStaticMarkup(<EventExternalRegistration event={event} userInfo={{ country: 'RO' }} />);
212
+
213
+ const register = buttonHandlers.find((b) => b.label === 'Register');
214
+ await register.onClick();
215
+
216
+ expect(getUserByMail).toHaveBeenCalledWith('existing@domain.org');
217
+ expect(postParticipant).not.toHaveBeenCalled();
218
+ });
219
+ });
@@ -0,0 +1,208 @@
1
+ import React from 'react';
2
+ import { renderToStaticMarkup } from 'react-dom/server';
3
+ import { EventRegistration } from './EventRegistration';
4
+ import {
5
+ postParticipant,
6
+ patchParticipant,
7
+ deleteParticipant,
8
+ } from '../../data/sharepointProvider';
9
+
10
+ const buttonHandlers = [];
11
+
12
+ jest.mock('@mui/material', () => {
13
+ const ReactLocal = require('react');
14
+ const passthrough =
15
+ (Tag = 'div') =>
16
+ ({ children }) =>
17
+ ReactLocal.createElement(Tag, {}, children);
18
+
19
+ const labelToText = (children) => {
20
+ if (typeof children === 'string') {
21
+ return children;
22
+ }
23
+ if (Array.isArray(children)) {
24
+ return children
25
+ .filter((v) => typeof v === 'string')
26
+ .join(' ')
27
+ .trim();
28
+ }
29
+ return '';
30
+ };
31
+
32
+ return {
33
+ Box: passthrough(),
34
+ Checkbox: passthrough('input'),
35
+ TextField: ({ label }) => <div>{label || ''}</div>,
36
+ Button: ({ onClick, children }) => {
37
+ const label = labelToText(children);
38
+ if (onClick) {
39
+ buttonHandlers.push({ label, onClick });
40
+ }
41
+ return <button>{children}</button>;
42
+ },
43
+ FormControlLabel: ({ label, control }) => (
44
+ <div>
45
+ {label}
46
+ {control}
47
+ </div>
48
+ ),
49
+ CircularProgress: passthrough('span'),
50
+ Backdrop: passthrough(),
51
+ Alert: passthrough(),
52
+ };
53
+ });
54
+
55
+ jest.mock('@mui/icons-material/Check', () => () => <span>check-icon</span>);
56
+ jest.mock('@mui/icons-material/Save', () => () => <span>save-icon</span>);
57
+
58
+ jest.mock('../../data/hooks/useConfiguration', () => ({
59
+ useConfiguration: () => ({ EventRegistrationInfo: 'Registration info' }),
60
+ }));
61
+
62
+ jest.mock('../../data/sharepointProvider', () => ({
63
+ postParticipant: jest.fn(),
64
+ patchParticipant: jest.fn(),
65
+ deleteParticipant: jest.fn(),
66
+ }));
67
+
68
+ jest.mock('../HtmlBox', () => ({
69
+ HtmlBox: ({ html }) => <div>{html}</div>,
70
+ }));
71
+
72
+ describe('EventRegistration', () => {
73
+ beforeEach(() => {
74
+ jest.clearAllMocks();
75
+ buttonHandlers.length = 0;
76
+ });
77
+
78
+ test('renders registration form and action buttons for registered offline event', () => {
79
+ const html = renderToStaticMarkup(
80
+ <EventRegistration
81
+ participant={{
82
+ id: 1,
83
+ ParticipantName: 'User 1',
84
+ Email: 'user1@example.org',
85
+ Country: 'RO',
86
+ Registered: true,
87
+ PhysicalParticipation: true,
88
+ EEAReimbursementRequested: false,
89
+ CustomMeetingRequest: '',
90
+ }}
91
+ event={{
92
+ IsOffline: true,
93
+ Participants: [],
94
+ CustomMeetingRequest: 'Bring badge',
95
+ }}
96
+ />,
97
+ );
98
+
99
+ expect(html).toContain('Registration info');
100
+ expect(html).toContain('Update registration');
101
+ expect(html).toContain('Unregister');
102
+ expect(html).toContain('Physical participation');
103
+ expect(html).toContain('Reimbursement requested');
104
+ });
105
+
106
+ test('register flow posts participant and updates event counters', async () => {
107
+ const participant = {
108
+ ParticipantName: 'User 2',
109
+ Email: 'user2@example.org',
110
+ Country: 'RO',
111
+ Registered: false,
112
+ PhysicalParticipation: false,
113
+ EEAReimbursementRequested: false,
114
+ CustomMeetingRequest: '',
115
+ };
116
+ const event = {
117
+ IsOffline: true,
118
+ HasRegistered: false,
119
+ NoOfRegistered: 0,
120
+ Participants: [{ Registered: true }, { Registered: false }],
121
+ CustomMeetingRequest: '',
122
+ };
123
+
124
+ postParticipant.mockResolvedValue({ id: 55 });
125
+
126
+ renderToStaticMarkup(<EventRegistration participant={participant} event={event} />);
127
+
128
+ const register = buttonHandlers.find((b) => b.label === 'Register');
129
+ expect(register).toBeDefined();
130
+
131
+ await register.onClick();
132
+
133
+ expect(postParticipant).toHaveBeenCalledWith(participant, event);
134
+ expect(participant.Registered).toBe(true);
135
+ expect(participant.id).toBe(55);
136
+ expect(event.HasRegistered).toBe(true);
137
+ expect(event.NoOfRegistered).toBe(2);
138
+ expect(event.Participants.length).toBe(3);
139
+ });
140
+
141
+ test('update and unregister flows call patch and delete', async () => {
142
+ const participant = {
143
+ id: 10,
144
+ ParticipantName: 'User 3',
145
+ Email: 'user3@example.org',
146
+ Country: 'RO',
147
+ Registered: true,
148
+ NFPApproved: 'Yes',
149
+ PhysicalParticipation: true,
150
+ EEAReimbursementRequested: true,
151
+ CustomMeetingRequest: '',
152
+ };
153
+ const event = {
154
+ IsOffline: true,
155
+ HasRegistered: true,
156
+ NoOfRegistered: 1,
157
+ Participants: [participant],
158
+ CustomMeetingRequest: '',
159
+ };
160
+
161
+ patchParticipant.mockResolvedValue({});
162
+ deleteParticipant.mockResolvedValue({});
163
+
164
+ renderToStaticMarkup(<EventRegistration participant={participant} event={event} />);
165
+
166
+ const update = buttonHandlers.find((b) => b.label === 'Update registration');
167
+ const unregister = buttonHandlers.find((b) => b.label === 'Unregister');
168
+
169
+ expect(update).toBeDefined();
170
+ expect(unregister).toBeDefined();
171
+
172
+ await update.onClick();
173
+ expect(patchParticipant).toHaveBeenCalledWith(participant, event);
174
+ expect(participant.NFPApproved).toBe('No value');
175
+ expect(event.HasRegistered).toBe(true);
176
+
177
+ await unregister.onClick();
178
+ expect(deleteParticipant).toHaveBeenCalledWith(participant, event);
179
+ expect(participant.Registered).toBe(false);
180
+ expect(event.HasRegistered).toBe(false);
181
+ expect(event.NoOfRegistered).toBe(0);
182
+ });
183
+
184
+ test('online event hides offline-only controls', () => {
185
+ const html = renderToStaticMarkup(
186
+ <EventRegistration
187
+ participant={{
188
+ ParticipantName: 'User 4',
189
+ Email: 'user4@example.org',
190
+ Country: 'RO',
191
+ Registered: false,
192
+ PhysicalParticipation: false,
193
+ EEAReimbursementRequested: false,
194
+ CustomMeetingRequest: '',
195
+ }}
196
+ event={{
197
+ IsOffline: false,
198
+ Participants: [],
199
+ CustomMeetingRequest: 'ignored',
200
+ }}
201
+ />,
202
+ );
203
+
204
+ expect(html).not.toContain('Physical participation');
205
+ expect(html).not.toContain('Reimbursement requested');
206
+ expect(html).toContain('Register');
207
+ });
208
+ });
@@ -0,0 +1,157 @@
1
+ let mockRunEffect = true;
2
+
3
+ jest.mock('react', () => {
4
+ const actual = jest.requireActual('react');
5
+ return {
6
+ ...actual,
7
+ React: actual,
8
+ useEffect: jest.fn((fn) => {
9
+ if (mockRunEffect) {
10
+ mockRunEffect = false;
11
+ fn();
12
+ }
13
+ }),
14
+ };
15
+ });
16
+
17
+ import React from 'react';
18
+ jest.mock('./my_country.scss', () => ({}));
19
+ import { renderToStaticMarkup } from 'react-dom/server';
20
+ import { AtAGlance } from './AtAGlance';
21
+ import { getGroups, getMeetings, getConsultations } from '../../data/sharepointProvider';
22
+
23
+ jest.mock('../../data/sharepointProvider', () => ({
24
+ getGroups: jest.fn((users) => users.map((u) => u.GroupName).filter(Boolean)),
25
+ getMeetings: jest.fn(),
26
+ getConsultations: jest.fn(),
27
+ }));
28
+ jest.mock('./IndicatorCard', () => ({
29
+ IndicatorCard: ({ labelText, valueText }) => (
30
+ <div>
31
+ {labelText}:{valueText}
32
+ </div>
33
+ ),
34
+ }));
35
+ jest.mock('./CountryProgress', () => ({
36
+ CountryProgress: () => <div>country-progress</div>,
37
+ }));
38
+ jest.mock('../HtmlBox', () => ({
39
+ HtmlBox: ({ html }) => <div>{html}</div>,
40
+ }));
41
+
42
+ describe('AtAGlance', () => {
43
+ const flush = () => new Promise((resolve) => setTimeout(resolve, 0));
44
+ const waitForMockCall = async (mockFn, retries = 20) => {
45
+ for (let i = 0; i < retries; i += 1) {
46
+ if (mockFn.mock.calls.length > 0) {
47
+ return;
48
+ }
49
+ await flush();
50
+ }
51
+ };
52
+
53
+ const baseConfiguration = {
54
+ UserListUrl: 'https://users',
55
+ OrganisationListUrl: 'https://orgs',
56
+ ConsultationListUrl: 'https://consultations?',
57
+ InquiryListUrl: 'https://inquiries?',
58
+ MeetingListUrl: 'https://meetings',
59
+ CountryProgressHtml: 'Country intro',
60
+ DashboardNoOfDisplayedYears: 1,
61
+ };
62
+
63
+ beforeEach(() => {
64
+ jest.clearAllMocks();
65
+ mockRunEffect = true;
66
+ getMeetings.mockResolvedValue([
67
+ { Year: new Date().getFullYear(), IsPast: true, Countries: ['RO'], Group: ['group-a'] },
68
+ { Year: new Date().getFullYear(), IsPast: true, Countries: ['RO'], Group: ['wg-secret'] },
69
+ ]);
70
+ getConsultations.mockResolvedValue([
71
+ {
72
+ Year: new Date().getFullYear(),
73
+ Deadline: new Date(new Date().getTime() - 86400000),
74
+ ConsultationType: 'Consultation',
75
+ Respondants: ['RO'],
76
+ EionetGroups: ['group-a'],
77
+ },
78
+ {
79
+ Year: new Date().getFullYear(),
80
+ Deadline: new Date(new Date().getTime() - 86400000),
81
+ ConsultationType: 'Enquiry',
82
+ Respondants: ['RO'],
83
+ EionetGroups: ['wg-secret'],
84
+ },
85
+ ]);
86
+ });
87
+
88
+ test('renders representation cards and country section', async () => {
89
+ const html = renderToStaticMarkup(
90
+ <AtAGlance
91
+ users={[
92
+ { SignedIn: true, GroupName: 'A' },
93
+ { SignedIn: false, GroupName: 'B' },
94
+ ]}
95
+ organisations={[{ id: 1 }]}
96
+ country="RO"
97
+ userInfo={{ mail: 'nfp@example.org' }}
98
+ configuration={baseConfiguration}
99
+ availableGroups={['A', 'B', 'C']}
100
+ />,
101
+ );
102
+
103
+ await waitForMockCall(getConsultations);
104
+
105
+ expect(html).toContain('Representation:');
106
+ expect(html).toContain('members:2');
107
+ expect(html).toContain('members pending sign in:1');
108
+ expect(html).toContain('organisations:1');
109
+ expect(html).toContain('groups with nominations:2/3');
110
+ expect(html).toContain('groups with signed in users:1/3');
111
+ expect(html).toContain('Country intro');
112
+ expect(html).toContain('country-progress');
113
+
114
+ expect(getGroups).toHaveBeenCalled();
115
+ expect(getMeetings).toHaveBeenCalled();
116
+ expect(getConsultations).toHaveBeenCalled();
117
+ });
118
+
119
+ test('renders cards without country-specific section when country is missing', async () => {
120
+ const html = renderToStaticMarkup(
121
+ <AtAGlance
122
+ users={[{ SignedIn: false, GroupName: 'X' }]}
123
+ organisations={[]}
124
+ country=""
125
+ userInfo={{}}
126
+ configuration={baseConfiguration}
127
+ availableGroups={['X']}
128
+ />,
129
+ );
130
+
131
+ await flush();
132
+ await flush();
133
+
134
+ expect(html).toContain('members:1');
135
+ expect(html).toContain('groups with nominations:1/1');
136
+ expect(html).not.toContain('country-progress');
137
+ expect(html).not.toContain('Country intro');
138
+ });
139
+
140
+ test('supports default number of years when configuration is missing value', async () => {
141
+ renderToStaticMarkup(
142
+ <AtAGlance
143
+ users={[]}
144
+ organisations={[]}
145
+ country="RO"
146
+ userInfo={{}}
147
+ configuration={{ ...baseConfiguration, DashboardNoOfDisplayedYears: undefined }}
148
+ availableGroups={[]}
149
+ />,
150
+ );
151
+
152
+ await waitForMockCall(getConsultations);
153
+
154
+ expect(getMeetings).toHaveBeenCalled();
155
+ expect(getConsultations).toHaveBeenCalled();
156
+ });
157
+ });