richie-education 2.25.0-b2.dev124 → 2.25.0-b2.dev128

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.
@@ -1,19 +1,13 @@
1
1
  import { renderHook, act, waitFor } from '@testing-library/react';
2
- import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
3
- import { IntlProvider } from 'react-intl';
4
2
  import fetchMock from 'fetch-mock';
5
- import { PropsWithChildren } from 'react';
3
+ import queryString from 'query-string';
6
4
  import { PaginatedResourceQuery } from 'types/Joanie';
7
- import { History, HistoryContext } from 'hooks/useHistory';
8
- import { createTestQueryClient } from 'utils/test/createTestQueryClient';
9
- import { SessionProvider } from 'contexts/SessionContext';
10
5
  import { Deferred } from 'utils/test/deferred';
11
-
12
6
  import { noop } from 'utils';
13
7
  import { mockPaginatedResponse } from 'utils/test/mockPaginatedResponse';
14
8
  import { PER_PAGE } from 'settings';
15
9
  import { HttpError, HttpStatusCode } from 'utils/errors/HttpError';
16
- import { FetchEntityData } from './utils/fetchEntities';
10
+ import { BaseAppWrapper } from 'utils/test/wrappers/BaseAppWrapper';
17
11
  import { QueryConfig, FetchDataFunction } from './utils/fetchEntity';
18
12
  import useUnionResource from '.';
19
13
 
@@ -37,49 +31,6 @@ interface TestDataB {
37
31
  created_on: string;
38
32
  }
39
33
 
40
- const renderUseUnionResource = <
41
- DataA extends FetchEntityData,
42
- DataB extends FetchEntityData,
43
- FiltersA extends PaginatedResourceQuery,
44
- FiltersB extends PaginatedResourceQuery,
45
- >(
46
- queryAConfig: QueryConfig<DataA, FiltersA>,
47
- queryBConfig: QueryConfig<DataB, FiltersB>,
48
- ) => {
49
- const Wrapper = ({ client, children }: PropsWithChildren<{ client?: QueryClient }>) => {
50
- const historyPushState = jest.fn();
51
- const historyReplaceState = jest.fn();
52
- const makeHistoryOf: (params: any) => History = () => [
53
- {
54
- state: { name: '', data: {} },
55
- title: '',
56
- url: `/`,
57
- },
58
- historyPushState,
59
- historyReplaceState,
60
- ];
61
-
62
- return (
63
- <QueryClientProvider client={client ?? createTestQueryClient({ user: true })}>
64
- <IntlProvider locale="en">
65
- <HistoryContext.Provider value={makeHistoryOf({})}>
66
- <SessionProvider>{children}</SessionProvider>
67
- </HistoryContext.Provider>
68
- </IntlProvider>
69
- </QueryClientProvider>
70
- );
71
- };
72
-
73
- return renderHook(
74
- () =>
75
- useUnionResource<DataA, DataB, FiltersA, FiltersB>({
76
- queryAConfig,
77
- queryBConfig,
78
- }),
79
- { wrapper: Wrapper },
80
- );
81
- };
82
-
83
34
  describe('useUnionResource', () => {
84
35
  const perPage = PER_PAGE.useUnionResources;
85
36
  let dataAList: TestDataA[];
@@ -111,14 +62,14 @@ describe('useUnionResource', () => {
111
62
 
112
63
  { name: 'TestDataB', id: '14', created_on: '2022-12-03' },
113
64
  ];
114
- const dummyFetchWrapper = async (url: string) => {
115
- const res = await fetch(url);
65
+ const dummyFetchWrapper = async (url: string, queryParams: { [key: string]: any }) => {
66
+ const res = await fetch(`${url}?${queryString.stringify(queryParams)}`);
116
67
  return res.json();
117
68
  };
118
- const fetchDataA = ({ page }: { page: number }) =>
119
- dummyFetchWrapper(`http://data.a/?page=${page}`);
69
+ const fetchDataA = ({ page, isFiltered }: { page: number; isFiltered?: boolean }) =>
70
+ dummyFetchWrapper('http://data.a/', { page, isFiltered });
120
71
  const fetchDataB = ({ page }: { page: number }) =>
121
- dummyFetchWrapper(`http://data.b/?page=${page}`);
72
+ dummyFetchWrapper('http://data.b/', { page });
122
73
 
123
74
  queryAConfig = {
124
75
  queryKey: ['resourceA'],
@@ -132,11 +83,6 @@ describe('useUnionResource', () => {
132
83
  };
133
84
  });
134
85
 
135
- afterEach(() => {
136
- jest.clearAllMocks();
137
- fetchMock.restore();
138
- });
139
-
140
86
  it('should handle loading state', async () => {
141
87
  const dataADeferred = new Deferred();
142
88
  const dataBDeferred = new Deferred();
@@ -144,12 +90,16 @@ describe('useUnionResource', () => {
144
90
  const pendingDataBPromise = () => dataBDeferred.promise;
145
91
  queryAConfig.fn = pendingDataAPromise as FetchDataFunction<TestDataA, TestDataAFilters>;
146
92
  queryBConfig.fn = pendingDataBPromise as FetchDataFunction<TestDataB, PaginatedResourceQuery>;
147
- const { result } = renderUseUnionResource<
148
- TestDataA,
149
- TestDataB,
150
- TestDataAFilters,
151
- PaginatedResourceQuery
152
- >(queryAConfig, queryBConfig);
93
+
94
+ const { result } = renderHook(
95
+ () =>
96
+ useUnionResource<TestDataA, TestDataB, TestDataAFilters, PaginatedResourceQuery>({
97
+ queryAConfig,
98
+ queryBConfig,
99
+ }),
100
+ { wrapper: BaseAppWrapper },
101
+ );
102
+
153
103
  expect(result.current.isLoading).toBe(true);
154
104
  expect(result.current.hasMore).toBe(false);
155
105
 
@@ -164,12 +114,15 @@ describe('useUnionResource', () => {
164
114
  it('should render less than 1 page of dataA', async () => {
165
115
  fetchMock.get('http://data.a/?page=1', mockPaginatedResponse([dataAList[0]], 1, false));
166
116
  fetchMock.get('http://data.b/?page=1', mockPaginatedResponse([], 0, false));
167
- const { result } = renderUseUnionResource<
168
- TestDataA,
169
- TestDataB,
170
- TestDataAFilters,
171
- PaginatedResourceQuery
172
- >(queryAConfig, queryBConfig);
117
+
118
+ const { result } = renderHook(
119
+ () =>
120
+ useUnionResource<TestDataA, TestDataB, TestDataAFilters, PaginatedResourceQuery>({
121
+ queryAConfig,
122
+ queryBConfig,
123
+ }),
124
+ { wrapper: BaseAppWrapper },
125
+ );
173
126
 
174
127
  await waitFor(() => expect(result.current.isLoading).toBe(false));
175
128
  expect(result.current.hasMore).toBe(false);
@@ -180,12 +133,15 @@ describe('useUnionResource', () => {
180
133
  it('should render less than 1 page of dataB', async () => {
181
134
  fetchMock.get('http://data.a/?page=1', mockPaginatedResponse([], 0, false));
182
135
  fetchMock.get('http://data.b/?page=1', mockPaginatedResponse([dataBList[0]], 1, false));
183
- const { result } = renderUseUnionResource<
184
- TestDataA,
185
- TestDataB,
186
- TestDataAFilters,
187
- PaginatedResourceQuery
188
- >(queryAConfig, queryBConfig);
136
+
137
+ const { result } = renderHook(
138
+ () =>
139
+ useUnionResource<TestDataA, TestDataB, TestDataAFilters, PaginatedResourceQuery>({
140
+ queryAConfig,
141
+ queryBConfig,
142
+ }),
143
+ { wrapper: BaseAppWrapper },
144
+ );
189
145
 
190
146
  await waitFor(() => expect(result.current.isLoading).toBe(false));
191
147
  expect(result.current.hasMore).toBe(false);
@@ -196,12 +152,15 @@ describe('useUnionResource', () => {
196
152
  it('should renders less than 1 page of both dataA and dataB', async () => {
197
153
  fetchMock.get('http://data.a/?page=1', mockPaginatedResponse([dataAList[0]], 1, false));
198
154
  fetchMock.get('http://data.b/?page=1', mockPaginatedResponse([dataBList[0]], 1, false));
199
- const { result } = renderUseUnionResource<
200
- TestDataA,
201
- TestDataB,
202
- TestDataAFilters,
203
- PaginatedResourceQuery
204
- >(queryAConfig, queryBConfig);
155
+
156
+ const { result } = renderHook(
157
+ () =>
158
+ useUnionResource<TestDataA, TestDataB, TestDataAFilters, PaginatedResourceQuery>({
159
+ queryAConfig,
160
+ queryBConfig,
161
+ }),
162
+ { wrapper: BaseAppWrapper },
163
+ );
205
164
 
206
165
  await waitFor(() => expect(result.current.isLoading).toBe(false));
207
166
  expect(result.current.hasMore).toBe(false);
@@ -237,12 +196,14 @@ describe('useUnionResource', () => {
237
196
  mockPaginatedResponse(dataBList.slice(perPage * 2, perPage * 3), dataAList.length, false),
238
197
  );
239
198
 
240
- const { result } = renderUseUnionResource<
241
- TestDataA,
242
- TestDataB,
243
- TestDataAFilters,
244
- PaginatedResourceQuery
245
- >(queryAConfig, queryBConfig);
199
+ const { result } = renderHook(
200
+ () =>
201
+ useUnionResource<TestDataA, TestDataB, TestDataAFilters, PaginatedResourceQuery>({
202
+ queryAConfig,
203
+ queryBConfig,
204
+ }),
205
+ { wrapper: BaseAppWrapper },
206
+ );
246
207
 
247
208
  await waitFor(() => expect(result.current.isLoading).toBe(false));
248
209
  expect(result.current.hasMore).toBe(true);
@@ -297,12 +258,16 @@ describe('useUnionResource', () => {
297
258
  const dataADeferred = new Deferred();
298
259
  const pendingDataAPromise = () => dataADeferred.promise;
299
260
  queryAConfig.fn = pendingDataAPromise as FetchDataFunction<TestDataA, TestDataAFilters>;
300
- const { result } = renderUseUnionResource<
301
- TestDataA,
302
- TestDataB,
303
- TestDataAFilters,
304
- PaginatedResourceQuery
305
- >(queryAConfig, queryBConfig);
261
+
262
+ const { result } = renderHook(
263
+ () =>
264
+ useUnionResource<TestDataA, TestDataB, TestDataAFilters, PaginatedResourceQuery>({
265
+ queryAConfig,
266
+ queryBConfig,
267
+ }),
268
+ { wrapper: BaseAppWrapper },
269
+ );
270
+
306
271
  expect(result.current.isLoading).toBe(true);
307
272
 
308
273
  await act(() => {
@@ -313,4 +278,40 @@ describe('useUnionResource', () => {
313
278
  expect(result.current.isLoading).toBe(false);
314
279
  expect(result.current.error).toBe('An error occurred while fetching data. Please retry later.');
315
280
  });
281
+
282
+ it('should refetch data when filters change', async () => {
283
+ fetchMock.get('http://data.a/?page=1', mockPaginatedResponse([dataAList[0]], 1, false));
284
+ fetchMock.get('http://data.b/?page=1', mockPaginatedResponse([], 0, false));
285
+
286
+ const { result, rerender } = renderHook(
287
+ (queries: {
288
+ queryA?: QueryConfig<TestDataA, TestDataAFilters>;
289
+ queryB?: QueryConfig<TestDataB, PaginatedResourceQuery>;
290
+ }) =>
291
+ useUnionResource<TestDataA, TestDataB, TestDataAFilters, PaginatedResourceQuery>({
292
+ queryAConfig: queries?.queryA || queryAConfig,
293
+ queryBConfig: queries?.queryB || queryBConfig,
294
+ }),
295
+ { wrapper: BaseAppWrapper },
296
+ );
297
+
298
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
299
+ let calledUrls = fetchMock.calls().map((call) => call[0]);
300
+ expect(calledUrls).toHaveLength(2);
301
+ expect(calledUrls).toContain('http://data.a/?page=1');
302
+ expect(calledUrls).toContain('http://data.b/?page=1');
303
+
304
+ queryAConfig.filters = { isFiltered: true };
305
+ fetchMock.get(
306
+ 'http://data.a/?isFiltered=true&page=1',
307
+ mockPaginatedResponse([dataAList[0]], 1, false),
308
+ );
309
+ rerender({ queryA: queryAConfig, queryB: queryBConfig });
310
+ expect(result.current.isLoading).toBe(true);
311
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
312
+
313
+ calledUrls = fetchMock.calls().map((call) => call[0]);
314
+ expect(calledUrls).toHaveLength(4);
315
+ expect(calledUrls).toContain('http://data.a/?isFiltered=true&page=1');
316
+ });
316
317
  });
@@ -90,6 +90,8 @@ const useUnionResource = <
90
90
  const eofRef = useRef<Record<string, number>>(queryClient.getQueryData(eofQueryKey) ?? {});
91
91
  log('eof', eofRef.current);
92
92
 
93
+ const [unionQueryKey, setUnionQueryKey] = useState<string>();
94
+
93
95
  const reset = () => {
94
96
  setStack([]);
95
97
  setPage(0);
@@ -104,6 +106,18 @@ const useUnionResource = <
104
106
  useQueryKeyInvalidateListener(queryBConfig.queryKey, reset);
105
107
  }
106
108
 
109
+ useEffect(() => {
110
+ // filters have changes, new results will be fetch.
111
+ // let's reset every previous fetches states
112
+ reset();
113
+
114
+ // We need to fetch new results.
115
+ // reset all states isn't enought. If we've a research without results
116
+ // then the next reset would do nothing.
117
+ // to force execution of useEffect::fetchNewPage(), we use a uniq key build with current queries filters.
118
+ setUnionQueryKey(JSON.stringify(queryAConfig.filters) + JSON.stringify(queryBConfig.filters));
119
+ }, [JSON.stringify(queryAConfig.filters), JSON.stringify(queryBConfig.filters)]);
120
+
107
121
  useEffect(() => {
108
122
  async function fetchNewPage() {
109
123
  const {
@@ -145,11 +159,18 @@ const useUnionResource = <
145
159
 
146
160
  // we request more entities than we can display in the right order
147
161
  // it's time for new fetching and sorting.
148
- if (cursor > integrityCount) {
162
+ if (!isSyncing && cursor > integrityCount) {
149
163
  setIsSyncing(true);
150
164
  fetchNewPage();
151
165
  }
152
- }, [cursor, stack.length]); // stack.length is added in the dependency array to force a new fetch on reset.
166
+ }, [
167
+ cursor,
168
+ // FIXME(rlecellier): when stack.length === 0, invalidate the query will not refetch.
169
+ // stack.length is added in the dependency array to force a new fetch on reset.
170
+ stack.length,
171
+ // unionQueryKey assure that we refetch data when query filters change.
172
+ unionQueryKey,
173
+ ]);
153
174
 
154
175
  const cursorToUse = Math.min(cursor, integrityCount);
155
176
  const next = () => {
@@ -1,13 +1,8 @@
1
- import { act, getByRole, render, screen, waitFor } from '@testing-library/react';
2
- import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
3
- import { IntlProvider } from 'react-intl';
1
+ import { act, getByRole, screen, waitFor } from '@testing-library/react';
2
+ import { QueryClient } from '@tanstack/react-query';
4
3
  import fetchMock from 'fetch-mock';
5
4
  import userEvent from '@testing-library/user-event';
6
- import {
7
- UserFactory,
8
- RichieContextFactory as mockRichieContextFactory,
9
- } from 'utils/test/factories/richie';
10
- import { History, HistoryContext } from 'hooks/useHistory';
5
+ import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
11
6
  import { DashboardTest } from 'widgets/Dashboard/components/DashboardTest';
12
7
  import {
13
8
  CourseProductRelationFactory,
@@ -15,7 +10,6 @@ import {
15
10
  CredentialOrderFactory,
16
11
  } from 'utils/test/factories/joanie';
17
12
  import { createTestQueryClient } from 'utils/test/createTestQueryClient';
18
- import { SessionProvider } from 'contexts/SessionContext';
19
13
  import { LearnerDashboardPaths } from 'widgets/Dashboard/utils/learnerRouteMessages';
20
14
  import { CourseLight, CourseProductRelation, Enrollment, CredentialOrder } from 'types/Joanie';
21
15
  import { expectNoSpinner, expectSpinner } from 'utils/test/expectSpinner';
@@ -25,6 +19,9 @@ import { isOrder } from 'pages/DashboardCourses/useOrdersEnrollments';
25
19
  import { noop } from 'utils';
26
20
  import { PER_PAGE } from 'settings';
27
21
  import { HttpStatusCode } from 'utils/errors/HttpError';
22
+ import { setupJoanieSession } from 'utils/test/wrappers/JoanieAppWrapper';
23
+ import { render } from 'utils/test/render';
24
+ import { BaseJoanieAppWrapper } from 'utils/test/wrappers/BaseJoanieAppWrapper';
28
25
 
29
26
  jest.mock('utils/context', () => ({
30
27
  __esModule: true,
@@ -52,44 +49,8 @@ jest.mock('hooks/useIntersectionObserver', () => ({
52
49
  }));
53
50
 
54
51
  describe('<DashboardCourses/>', () => {
52
+ setupJoanieSession();
55
53
  const perPage = PER_PAGE.useOrdersEnrollments;
56
- const historyPushState = jest.fn();
57
- const historyReplaceState = jest.fn();
58
- const makeHistoryOf: (params: any) => History = () => [
59
- {
60
- state: { name: '', data: {} },
61
- title: '',
62
- url: `/`,
63
- },
64
- historyPushState,
65
- historyReplaceState,
66
- ];
67
-
68
- beforeEach(() => {
69
- fetchMock.get('https://joanie.endpoint/api/v1.0/addresses/', []);
70
- fetchMock.get('https://joanie.endpoint/api/v1.0/credit-cards/', []);
71
- fetchMock.get('https://joanie.endpoint/api/v1.0/orders/', []);
72
- });
73
-
74
- afterEach(() => {
75
- jest.clearAllMocks();
76
- fetchMock.restore();
77
- });
78
-
79
- const Wrapper = ({ client }: { client?: QueryClient }) => {
80
- const user = UserFactory().one();
81
- return (
82
- <QueryClientProvider client={client ?? createTestQueryClient({ user })}>
83
- <IntlProvider locale="en">
84
- <HistoryContext.Provider value={makeHistoryOf({})}>
85
- <SessionProvider>
86
- <DashboardTest initialRoute={LearnerDashboardPaths.COURSES} />
87
- </SessionProvider>
88
- </HistoryContext.Provider>
89
- </IntlProvider>
90
- </QueryClientProvider>
91
- );
92
- };
93
54
 
94
55
  const mockOrders = (orders: CredentialOrder[], client?: QueryClient) => {
95
56
  const relations: Record<string, CourseProductRelation> = {};
@@ -150,7 +111,9 @@ describe('<DashboardCourses/>', () => {
150
111
  enrollmentsDeferred.promise,
151
112
  );
152
113
 
153
- render(<Wrapper />);
114
+ render(<DashboardTest initialRoute={LearnerDashboardPaths.COURSES} />, {
115
+ wrapper: BaseJoanieAppWrapper,
116
+ });
154
117
 
155
118
  await expectSpinner('Loading orders and enrollments...');
156
119
  expect(await screen.queryByRole('button', { name: 'Load more' })).not.toBeInTheDocument();
@@ -242,7 +205,9 @@ describe('<DashboardCourses/>', () => {
242
205
 
243
206
  const entities = merge(orders, enrollments);
244
207
 
245
- render(<Wrapper client={client} />);
208
+ render(<DashboardTest initialRoute={LearnerDashboardPaths.COURSES} />, {
209
+ wrapper: BaseJoanieAppWrapper,
210
+ });
246
211
 
247
212
  // Slice 1.
248
213
  await expectNoSpinner('Loading orders and enrollments...');
@@ -277,7 +242,10 @@ describe('<DashboardCourses/>', () => {
277
242
  { results: [], next: null, previous: null, count: 0 },
278
243
  );
279
244
 
280
- render(<Wrapper />);
245
+ render(<DashboardTest initialRoute={LearnerDashboardPaths.COURSES} />, {
246
+ wrapper: BaseJoanieAppWrapper,
247
+ });
248
+
281
249
  ordersDeferred.resolve({
282
250
  status: HttpStatusCode.INTERNAL_SERVER_ERROR,
283
251
  body: 'Internal Server Error',
@@ -55,7 +55,7 @@ describe('pages/CourseLearnerDataGrid', () => {
55
55
  expect(cells[2]).toHaveTextContent(
56
56
  intl.formatDate(new Date(courseOrder.created_on), DEFAULT_DATE_FORMAT),
57
57
  );
58
- expect(cells[3]).toHaveTextContent('Completed');
58
+ expect(cells[3]).toHaveTextContent('Certified');
59
59
  expect(within(cells[4]).getByText('Contact')).toBeInTheDocument();
60
60
  });
61
61
  });
@@ -3,7 +3,7 @@ import { Button, DataGrid, DataGridProps, PaginationProps, Row } from '@openfun/
3
3
  import { useMemo } from 'react';
4
4
  import { NestedCourseOrder } from 'types/Joanie';
5
5
  import { DEFAULT_DATE_FORMAT } from 'hooks/useDateFormat';
6
- import OrderStateMessage from 'widgets/Dashboard/components/DashboardItem/Order/OrderStateMessage';
6
+ import OrderStateTeacherMessage from 'widgets/Dashboard/components/DashboardItem/Order/OrderStateTeacherMessage';
7
7
  import DashboardListAvatar from 'widgets/Dashboard/components/DashboardListAvatar';
8
8
 
9
9
  const messages = defineMessages({
@@ -74,10 +74,8 @@ const CourseLearnerDataGrid = ({
74
74
  headerName: intl.formatMessage(messages.columnState),
75
75
  enableSorting: false,
76
76
  renderCell: (params: { row: Row }) => {
77
- // TODO(rlecellier): create NestedOrderCourseStateMessage that's get label dedicated to teatchers.
78
- // like signature needed for studdent is awaiting signature for a teacher.
79
77
  return (
80
- <OrderStateMessage
78
+ <OrderStateTeacherMessage
81
79
  order={params.row.courseOrder}
82
80
  contractDefinition={params.row.courseOrder.product.contract_definition_id}
83
81
  />
@@ -7,10 +7,52 @@ import {
7
7
  NestedCourseOrder,
8
8
  } from 'types/Joanie';
9
9
 
10
+ export enum OrderStatus {
11
+ DRAFT = 'draft',
12
+ SUBMITTED = 'submitted',
13
+ PENDING = 'pending',
14
+ CANCELED = 'canceled',
15
+ WAITING_SIGNATURE = 'waiting_signature',
16
+ WAITING_COUNTER_SIGNATURE = 'waiting_counter_signature',
17
+ COMPLETED = 'completed',
18
+ ON_GOING = 'on_going',
19
+ }
20
+
10
21
  /**
11
22
  * Helper class for orders
12
23
  */
13
24
  export class OrderHelper {
25
+ static getState(order: Order | NestedCourseOrder, contractDefinition?: ContractDefinition) {
26
+ const { certificate_id: certificateId } = order;
27
+
28
+ if (order.state === OrderState.VALIDATED) {
29
+ if (OrderHelper.orderNeedsSignature(order, contractDefinition)) {
30
+ return OrderStatus.WAITING_SIGNATURE;
31
+ }
32
+ if (OrderHelper.orderNeedsCounterSignature(order)) {
33
+ return OrderStatus.WAITING_COUNTER_SIGNATURE;
34
+ }
35
+ if (certificateId) {
36
+ return OrderStatus.COMPLETED;
37
+ } else {
38
+ return OrderStatus.ON_GOING;
39
+ }
40
+ }
41
+
42
+ const orderStatusMap = {
43
+ [OrderState.DRAFT]: OrderStatus.DRAFT,
44
+ [OrderState.SUBMITTED]: OrderStatus.SUBMITTED,
45
+ [OrderState.PENDING]: OrderStatus.PENDING,
46
+ [OrderState.CANCELED]: OrderStatus.CANCELED,
47
+ };
48
+
49
+ if (order.state in orderStatusMap) {
50
+ return orderStatusMap[order.state];
51
+ }
52
+
53
+ return null;
54
+ }
55
+
14
56
  /**
15
57
  * return an Order from the given list that match given productId
16
58
  */
@@ -33,4 +75,16 @@ export class OrderHelper {
33
75
  !(order.contract && order.contract.student_signed_on)
34
76
  );
35
77
  }
78
+
79
+ /**
80
+ * tell us if a order need to be sign by the organization.
81
+ */
82
+ static orderNeedsCounterSignature(order: Order | NestedCourseOrder) {
83
+ return (
84
+ order?.state === OrderState.VALIDATED &&
85
+ order.contract &&
86
+ order.contract.student_signed_on &&
87
+ !order.contract.organization_signed_on
88
+ );
89
+ }
36
90
  }
@@ -146,7 +146,7 @@ export const ContractFactory = factory((): Contract => {
146
146
  export const ContractLightFactory = factory((): ContractLight => {
147
147
  return {
148
148
  id: faker.string.uuid(),
149
- student_signed_on: faker.date.past().toISOString(),
149
+ student_signed_on: null,
150
150
  organization_signed_on: null,
151
151
  };
152
152
  });
@@ -0,0 +1,19 @@
1
+ import { PropsWithChildren } from 'react';
2
+ import { SessionProvider } from 'contexts/SessionContext';
3
+ import { IntlWrapper } from './IntlWrapper';
4
+ import { ReactQueryWrapper } from './ReactQueryWrapper';
5
+ import { AppWrapperProps } from './types';
6
+
7
+ export const BaseAppWrapper = ({
8
+ children,
9
+ intlOptions,
10
+ queryOptions,
11
+ }: PropsWithChildren<AppWrapperProps>) => {
12
+ return (
13
+ <IntlWrapper {...(intlOptions || { locale: 'en' })}>
14
+ <ReactQueryWrapper {...(queryOptions || {})}>
15
+ <SessionProvider>{children}</SessionProvider>
16
+ </ReactQueryWrapper>
17
+ </IntlWrapper>
18
+ );
19
+ };
@@ -0,0 +1,20 @@
1
+ import { PropsWithChildren } from 'react';
2
+
3
+ import JoanieSessionProvider from 'contexts/SessionContext/JoanieSessionProvider';
4
+ import { IntlWrapper } from './IntlWrapper';
5
+ import { ReactQueryWrapper } from './ReactQueryWrapper';
6
+ import { AppWrapperProps } from './types';
7
+
8
+ export const BaseJoanieAppWrapper = ({
9
+ children,
10
+ intlOptions,
11
+ queryOptions,
12
+ }: PropsWithChildren<AppWrapperProps>) => {
13
+ return (
14
+ <IntlWrapper {...(intlOptions || { locale: 'en' })}>
15
+ <ReactQueryWrapper {...(queryOptions || {})}>
16
+ <JoanieSessionProvider>{children}</JoanieSessionProvider>
17
+ </ReactQueryWrapper>
18
+ </IntlWrapper>
19
+ );
20
+ };
@@ -1,12 +1,10 @@
1
1
  import { CunninghamProvider } from '@openfun/cunningham-react';
2
2
  import { PropsWithChildren } from 'react';
3
3
  import fetchMock from 'fetch-mock';
4
- import JoanieSessionProvider from 'contexts/SessionContext/JoanieSessionProvider';
5
4
  import { DashboardBreadcrumbsProvider } from 'widgets/Dashboard/contexts/DashboardBreadcrumbsContext';
6
- import { IntlWrapper } from './IntlWrapper';
7
- import { ReactQueryWrapper } from './ReactQueryWrapper';
8
5
  import { RouterWrapper } from './RouterWrapper';
9
6
  import { AppWrapperProps } from './types';
7
+ import { BaseJoanieAppWrapper } from './BaseJoanieAppWrapper';
10
8
 
11
9
  export const setupJoanieSession = () => {
12
10
  beforeEach(() => {
@@ -21,10 +19,6 @@ export const setupJoanieSession = () => {
21
19
  };
22
20
  };
23
21
 
24
- export const JoanieSessionWrapper = ({ children }: PropsWithChildren) => {
25
- return <JoanieSessionProvider>{children}</JoanieSessionProvider>;
26
- };
27
-
28
22
  export const JoanieAppWrapper = ({
29
23
  children,
30
24
  intlOptions,
@@ -32,16 +26,12 @@ export const JoanieAppWrapper = ({
32
26
  routerOptions,
33
27
  }: PropsWithChildren<AppWrapperProps>) => {
34
28
  return (
35
- <IntlWrapper {...(intlOptions || { locale: 'en' })}>
29
+ <BaseJoanieAppWrapper intlOptions={intlOptions} queryOptions={queryOptions}>
36
30
  <CunninghamProvider>
37
- <ReactQueryWrapper {...(queryOptions || {})}>
38
- <JoanieSessionWrapper>
39
- <DashboardBreadcrumbsProvider>
40
- <RouterWrapper {...routerOptions}>{children}</RouterWrapper>
41
- </DashboardBreadcrumbsProvider>
42
- </JoanieSessionWrapper>
43
- </ReactQueryWrapper>
31
+ <DashboardBreadcrumbsProvider>
32
+ <RouterWrapper {...routerOptions}>{children}</RouterWrapper>
33
+ </DashboardBreadcrumbsProvider>
44
34
  </CunninghamProvider>
45
- </IntlWrapper>
35
+ </BaseJoanieAppWrapper>
46
36
  );
47
37
  };
@@ -20,7 +20,7 @@ import { DashboardSubItemsList } from '../DashboardSubItemsList';
20
20
  import { DashboardItemCourseEnrolling } from '../CourseEnrolling';
21
21
  import { DashboardItem } from '../index';
22
22
  import { DashboardItemContract } from '../Contract';
23
- import OrderStateMessage from './OrderStateMessage';
23
+ import OrderStateLearnerMessage from './OrderStateLearnerMessage';
24
24
 
25
25
  const messages = defineMessages({
26
26
  accessCourse: {
@@ -159,7 +159,7 @@ export const DashboardItemOrder = ({
159
159
  <div className="dashboard-item-order__footer">
160
160
  <div className="dashboard-item__block__status">
161
161
  <Icon name={IconTypeEnum.SCHOOL} />
162
- <OrderStateMessage
162
+ <OrderStateLearnerMessage
163
163
  order={order}
164
164
  contractDefinition={product?.contract_definition}
165
165
  />
@@ -1,13 +1,9 @@
1
1
  // FIXME: this test is about useUnionResource behavior.
2
2
  // we need to rewrite it in useUnionResource tests suite as small and generic as possible.
3
- import { QueryClientProvider } from '@tanstack/react-query';
4
- import { IntlProvider } from 'react-intl';
5
3
  import fetchMock from 'fetch-mock';
6
- import { act, render, screen, waitFor, within } from '@testing-library/react';
4
+ import { act, screen, waitFor, within } from '@testing-library/react';
7
5
  import userEvent from '@testing-library/user-event';
8
6
  import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
9
- import { createTestQueryClient } from 'utils/test/createTestQueryClient';
10
- import { SessionProvider } from 'contexts/SessionContext';
11
7
  import { DashboardTest } from 'widgets/Dashboard/components/DashboardTest';
12
8
  import {
13
9
  ContractFactory,
@@ -19,6 +15,9 @@ import { LearnerDashboardPaths } from 'widgets/Dashboard/utils/learnerRouteMessa
19
15
  import { Deferred } from 'utils/test/deferred';
20
16
  import { expectNoSpinner, expectSpinner } from 'utils/test/expectSpinner';
21
17
  import { CONTRACT_SETTINGS } from 'settings';
18
+ import { setupJoanieSession } from 'utils/test/wrappers/JoanieAppWrapper';
19
+ import { render } from 'utils/test/render';
20
+ import { BaseJoanieAppWrapper } from 'utils/test/wrappers/BaseJoanieAppWrapper';
22
21
 
23
22
  jest.mock('utils/context', () => ({
24
23
  __esModule: true,
@@ -35,31 +34,15 @@ jest.mock('hooks/useIntersectionObserver', () => ({
35
34
  }));
36
35
 
37
36
  describe('<DashboardItemOrder/> Contract', () => {
38
- const Wrapper = (route: string) => {
39
- return (
40
- <QueryClientProvider client={createTestQueryClient({ user: true })}>
41
- <IntlProvider locale="en">
42
- <SessionProvider>
43
- <DashboardTest initialRoute={route} />
44
- </SessionProvider>
45
- </IntlProvider>
46
- </QueryClientProvider>
47
- );
48
- };
37
+ setupJoanieSession();
49
38
 
50
39
  beforeEach(() => {
51
40
  jest.useFakeTimers();
52
41
  jest.clearAllTimers();
53
-
54
- fetchMock.get('https://joanie.endpoint/api/v1.0/addresses/', []);
55
- fetchMock.get('https://joanie.endpoint/api/v1.0/credit-cards/', []);
56
- fetchMock.get('https://joanie.endpoint/api/v1.0/orders/', []);
57
42
  });
58
43
 
59
44
  afterEach(() => {
60
45
  jest.restoreAllMocks();
61
- jest.clearAllMocks();
62
- fetchMock.restore();
63
46
  });
64
47
 
65
48
  describe('writable', () => {
@@ -103,9 +86,10 @@ describe('<DashboardItemOrder/> Contract', () => {
103
86
  // RTL too. See https://github.com/testing-library/user-event/issues/833.
104
87
  const user = userEvent.setup({ delay: null });
105
88
 
106
- render(Wrapper(LearnerDashboardPaths.COURSES));
89
+ render(<DashboardTest initialRoute={LearnerDashboardPaths.COURSES} />, {
90
+ wrapper: BaseJoanieAppWrapper,
91
+ });
107
92
 
108
- // console.log('apiCalls:2', fetchMock.calls().length);
109
93
  await expectNoSpinner('Loading orders and enrollments...');
110
94
 
111
95
  expect(
@@ -7,22 +7,10 @@ import {
7
7
  CredentialOrderFactory,
8
8
  } from 'utils/test/factories/joanie';
9
9
  import { OrderState } from 'types/Joanie';
10
- import OrderStateMessage, { messages } from '.';
10
+ import OrderStateLearnerMessage, { messages } from '.';
11
11
 
12
12
  const intl = createIntl({ locale: 'en' });
13
13
 
14
- /*
15
- @TODO (rlecellier): Rewrite everything with these guidlignes
16
- Order lifecycle:
17
- * Draft: order has been created
18
- * Submitted: order information have been validated
19
- * Pending: payment has failed but can be retried
20
- * Validated:
21
- * Completed (Validated with generated certificate)
22
- * On going (Validated without generated certificate)
23
- * Signature needed: !order.product.contract.isSign
24
- * Canceled: has been canceled
25
- */
26
14
  describe('<DashboardItemOrder/>', () => {
27
15
  const Wrapper = ({ children }: PropsWithChildren) => (
28
16
  <IntlProvider locale="en">{children}</IntlProvider>
@@ -39,7 +27,7 @@ describe('<DashboardItemOrder/>', () => {
39
27
  const order = CredentialOrderFactory({ state }).one();
40
28
  render(
41
29
  <Wrapper>
42
- <OrderStateMessage order={order} />
30
+ <OrderStateLearnerMessage order={order} />
43
31
  </Wrapper>,
44
32
  );
45
33
  expect(screen.getByText(expectedMessage)).toBeInTheDocument();
@@ -60,7 +48,7 @@ describe('<DashboardItemOrder/>', () => {
60
48
  }).one();
61
49
  render(
62
50
  <Wrapper>
63
- <OrderStateMessage order={orderWithContract} />
51
+ <OrderStateLearnerMessage order={orderWithContract} />
64
52
  </Wrapper>,
65
53
  );
66
54
  expect(screen.getByText(expectedMessage)).toBeInTheDocument();
@@ -77,7 +65,7 @@ describe('<DashboardItemOrder/>', () => {
77
65
 
78
66
  render(
79
67
  <Wrapper>
80
- <OrderStateMessage order={order} contractDefinition={contractDefinition} />
68
+ <OrderStateLearnerMessage order={order} contractDefinition={contractDefinition} />
81
69
  </Wrapper>,
82
70
  );
83
71
  expect(screen.getByText('Signature required')).toBeInTheDocument();
@@ -91,7 +79,7 @@ describe('<DashboardItemOrder/>', () => {
91
79
  }).one();
92
80
  render(
93
81
  <Wrapper>
94
- <OrderStateMessage order={order} />
82
+ <OrderStateLearnerMessage order={order} />
95
83
  </Wrapper>,
96
84
  );
97
85
  expect(
@@ -104,12 +92,15 @@ describe('<DashboardItemOrder/>', () => {
104
92
  it('should display message for validated order that have a generated certificate', () => {
105
93
  const order = CredentialOrderFactory({
106
94
  state: OrderState.VALIDATED,
107
- contract: ContractFactory({ student_signed_on: new Date().toISOString() }).one(),
95
+ contract: ContractFactory({
96
+ student_signed_on: new Date().toISOString(),
97
+ organization_signed_on: new Date().toISOString(),
98
+ }).one(),
108
99
  certificate_id: 'FAKE_CERTIFICATE_ID',
109
100
  }).one();
110
101
  render(
111
102
  <Wrapper>
112
- <OrderStateMessage order={order} />
103
+ <OrderStateLearnerMessage order={order} />
113
104
  </Wrapper>,
114
105
  );
115
106
  expect(
@@ -0,0 +1,60 @@
1
+ import { defineMessages } from 'react-intl';
2
+ import OrderStateMessage, { OrderStateMessageBaseProps } from '../OrderStateMessage';
3
+
4
+ export const messages = defineMessages({
5
+ statusDraft: {
6
+ id: 'components.DashboardItem.Order.OrderStateMessage.statusDraft',
7
+ description: 'Status shown on the dashboard order item when order is draft.',
8
+ defaultMessage: 'Draft',
9
+ },
10
+ statusSubmitted: {
11
+ id: 'components.DashboardItem.Order.OrderStateMessage.statusSubmitted',
12
+ description: 'Status shown on the dashboard order item when order is submitted.',
13
+ defaultMessage: 'Submitted',
14
+ },
15
+ statusPending: {
16
+ id: 'components.DashboardItem.Order.OrderStateMessage.statusPending',
17
+ description: 'Status shown on the dashboard order item when order is pending.',
18
+ defaultMessage: 'Pending',
19
+ },
20
+ statusOnGoing: {
21
+ id: 'components.DashboardItem.Order.OrderStateMessage.statusOnGoing',
22
+ description:
23
+ 'Status shown on the dashboard order item when order is validated with no certificate',
24
+ defaultMessage: 'On going',
25
+ },
26
+ statusCompleted: {
27
+ id: 'components.DashboardItem.Order.OrderStateMessage.statusCompleted',
28
+ description:
29
+ 'Status shown on the dashboard order item when order is validated with certificate',
30
+ defaultMessage: 'Completed',
31
+ },
32
+ statusWaitingSignature: {
33
+ id: 'components.DashboardItem.Order.OrderStateMessage.statusWaitingSignature',
34
+ description:
35
+ "Status shown on the dashboard order item when order is validated with contract's learner signature missing.",
36
+ defaultMessage: 'Signature required',
37
+ },
38
+ statusWaitingCounterSignature: {
39
+ id: 'components.DashboardItem.Order.OrderStateMessage.statusWaitingCounterSignature',
40
+ description:
41
+ "Status shown on the dashboard order item when order is validated with contract's organization signature missing.",
42
+ defaultMessage: 'On going',
43
+ },
44
+ statusCanceled: {
45
+ id: 'components.DashboardItem.Order.OrderStateMessage.statusCanceled',
46
+ description: 'Status shown on the dashboard order item when order is canceled',
47
+ defaultMessage: 'Canceled',
48
+ },
49
+ statusOther: {
50
+ id: 'components.DashboardItem.Order.OrderStateMessage.statusOther',
51
+ description: 'Status shown on the dashboard order item when order status is unknown',
52
+ defaultMessage: '{state}',
53
+ },
54
+ });
55
+
56
+ const OrderStateLearnerMessage = (props: OrderStateMessageBaseProps) => {
57
+ return <OrderStateMessage {...props} messages={messages} />;
58
+ };
59
+
60
+ export default OrderStateLearnerMessage;
@@ -1,4 +1,4 @@
1
- import { FormattedMessage, defineMessages } from 'react-intl';
1
+ import { FormattedMessage, MessageDescriptor } from 'react-intl';
2
2
  import { useEffect } from 'react';
3
3
  import {
4
4
  CertificateOrder,
@@ -9,96 +9,46 @@ import {
9
9
  } from 'types/Joanie';
10
10
  import { StringHelper } from 'utils/StringHelper';
11
11
  import { handle } from 'utils/errors/handle';
12
- import { OrderHelper } from 'utils/OrderHelper';
12
+ import { OrderHelper, OrderStatus } from 'utils/OrderHelper';
13
13
 
14
- export const messages = defineMessages({
15
- statusDraft: {
16
- id: 'components.DashboardItem.Order.OrderStateMessage.statusDraft',
17
- description: 'Status shown on the dashboard order item when order is draft.',
18
- defaultMessage: 'Draft',
19
- },
20
- statusSubmitted: {
21
- id: 'components.DashboardItem.Order.OrderStateMessage.statusSubmitted',
22
- description: 'Status shown on the dashboard order item when order is submitted.',
23
- defaultMessage: 'Submitted',
24
- },
25
- statusPending: {
26
- id: 'components.DashboardItem.Order.OrderStateMessage.statusPending',
27
- description: 'Status shown on the dashboard order item when order is pending.',
28
- defaultMessage: 'Pending',
29
- },
30
- statusOnGoing: {
31
- id: 'components.DashboardItem.Order.OrderStateMessage.statusOnGoing',
32
- description:
33
- 'Status shown on the dashboard order item when order is validated with no certificate',
34
- defaultMessage: 'On going',
35
- },
36
- statusCompleted: {
37
- id: 'components.DashboardItem.Order.OrderStateMessage.statusCompleted',
38
- description:
39
- 'Status shown on the dashboard order item when order is validated with certificate',
40
- defaultMessage: 'Completed',
41
- },
42
- statusWaitingSignature: {
43
- id: 'components.DashboardItem.Order.OrderStateMessage.statusWaitingSignature',
44
- description:
45
- "Status shown on the dashboard order item when order is validated with contract's learner signature missing.",
46
- defaultMessage: 'Signature required',
47
- },
48
- statusCanceled: {
49
- id: 'components.DashboardItem.Order.OrderStateMessage.statusCanceled',
50
- description: 'Status shown on the dashboard order item when order is canceled',
51
- defaultMessage: 'Canceled',
52
- },
53
- statusOther: {
54
- id: 'components.DashboardItem.Order.OrderStateMessage.statusOther',
55
- description: 'Status shown on the dashboard order item when order status is unknown',
56
- defaultMessage: '{state}',
57
- },
58
- });
59
-
60
- interface OrderStateMessageProps {
14
+ export interface OrderStateMessageBaseProps {
61
15
  order: CredentialOrder | CertificateOrder | NestedCourseOrder;
62
16
  contractDefinition?: ContractDefinition;
63
17
  }
64
18
 
65
- const OrderStateMessage = ({ order, contractDefinition }: OrderStateMessageProps) => {
66
- const { certificate_id: certificateId } = order;
67
- const orderStatusMessages = {
68
- [OrderState.DRAFT]: messages.statusDraft,
69
- [OrderState.SUBMITTED]: messages.statusSubmitted,
70
- [OrderState.PENDING]: messages.statusPending,
71
- [OrderState.CANCELED]: messages.statusCanceled,
72
- };
19
+ interface OrderStateMessageProps extends OrderStateMessageBaseProps {
20
+ messages: Record<string, MessageDescriptor>;
21
+ }
73
22
 
23
+ const OrderStateMessage = ({ order, contractDefinition, messages }: OrderStateMessageProps) => {
74
24
  useEffect(() => {
75
25
  if (!Object.values(OrderState).includes(order.state)) {
76
26
  handle(new Error(`Unknown order state ${order.state}`));
77
27
  }
78
28
  }, [order.state]);
79
29
 
80
- if (order.state === OrderState.VALIDATED) {
81
- if (OrderHelper.orderNeedsSignature(order, contractDefinition)) {
82
- return <FormattedMessage {...messages.statusWaitingSignature} />;
83
- }
84
-
85
- if (certificateId) {
86
- return <FormattedMessage {...messages.statusCompleted} />;
87
- } else {
88
- return <FormattedMessage {...messages.statusOnGoing} />;
89
- }
90
- }
91
-
92
- if (order.state in orderStatusMessages) {
93
- return <FormattedMessage {...orderStatusMessages[order.state]} />;
30
+ const orderStatusMessagesMap = {
31
+ [OrderStatus.DRAFT]: messages.statusDraft,
32
+ [OrderStatus.SUBMITTED]: messages.statusSubmitted,
33
+ [OrderStatus.PENDING]: messages.statusPending,
34
+ [OrderStatus.CANCELED]: messages.statusCanceled,
35
+ [OrderStatus.WAITING_SIGNATURE]: messages.statusWaitingSignature,
36
+ [OrderStatus.WAITING_COUNTER_SIGNATURE]: messages.statusWaitingCounterSignature,
37
+ [OrderStatus.COMPLETED]: messages.statusCompleted,
38
+ [OrderStatus.ON_GOING]: messages.statusOnGoing,
39
+ };
40
+ const status = OrderHelper.getState(order, contractDefinition);
41
+
42
+ if (status === null) {
43
+ return (
44
+ <FormattedMessage
45
+ {...messages.statusOther}
46
+ values={{ state: StringHelper.capitalizeFirst(order.state) }}
47
+ />
48
+ );
94
49
  }
95
50
 
96
- return (
97
- <FormattedMessage
98
- {...messages.statusOther}
99
- values={{ state: StringHelper.capitalizeFirst(order.state) }}
100
- />
101
- );
51
+ return <FormattedMessage {...orderStatusMessagesMap[status]} />;
102
52
  };
103
53
 
104
54
  export default OrderStateMessage;
@@ -0,0 +1,127 @@
1
+ import React, { PropsWithChildren } from 'react';
2
+ import { render, screen } from '@testing-library/react';
3
+ import { IntlProvider, createIntl } from 'react-intl';
4
+ import {
5
+ ContractDefinitionFactory,
6
+ ContractFactory,
7
+ CredentialOrderFactory,
8
+ } from 'utils/test/factories/joanie';
9
+ import { OrderState } from 'types/Joanie';
10
+ import OrderStateTeacherMessage, { messages } from '.';
11
+
12
+ const intl = createIntl({ locale: 'en' });
13
+
14
+ describe('<OrderStateTeacherMessage/>', () => {
15
+ const Wrapper = ({ children }: PropsWithChildren) => (
16
+ <IntlProvider locale="en">{children}</IntlProvider>
17
+ );
18
+
19
+ it.each([
20
+ [OrderState.DRAFT, 'Pending'],
21
+ [OrderState.SUBMITTED, 'Pending'],
22
+ [OrderState.PENDING, 'Pending'],
23
+ [OrderState.CANCELED, 'Canceled'],
24
+ ])(
25
+ 'should display message from order state: %s when order have no contract',
26
+ (state, expectedMessage) => {
27
+ const order = CredentialOrderFactory({ state }).one();
28
+ render(
29
+ <Wrapper>
30
+ <OrderStateTeacherMessage order={order} />
31
+ </Wrapper>,
32
+ );
33
+ expect(screen.getByText(expectedMessage)).toBeInTheDocument();
34
+ },
35
+ );
36
+
37
+ it.each([
38
+ [OrderState.DRAFT, 'Pending'],
39
+ [OrderState.SUBMITTED, 'Pending'],
40
+ [OrderState.PENDING, 'Pending'],
41
+ [OrderState.CANCELED, 'Canceled'],
42
+ ])(
43
+ 'should display message from order state: %s when order have contract',
44
+ (state, expectedMessage) => {
45
+ const orderWithContract = CredentialOrderFactory({
46
+ state,
47
+ contract: ContractFactory().one(),
48
+ }).one();
49
+ render(
50
+ <Wrapper>
51
+ <OrderStateTeacherMessage
52
+ order={orderWithContract}
53
+ contractDefinition={ContractDefinitionFactory().one()}
54
+ />
55
+ </Wrapper>,
56
+ );
57
+ expect(screen.getByText(expectedMessage)).toBeInTheDocument();
58
+ },
59
+ );
60
+
61
+ it('should display message for validated order that need learner signature', () => {
62
+ const order = CredentialOrderFactory({
63
+ state: OrderState.VALIDATED,
64
+ contract: null,
65
+ }).one();
66
+
67
+ const contractDefinition = ContractDefinitionFactory().one();
68
+
69
+ render(
70
+ <Wrapper>
71
+ <OrderStateTeacherMessage order={order} contractDefinition={contractDefinition} />
72
+ </Wrapper>,
73
+ );
74
+ expect(screen.getByText("Pending for learner's signature")).toBeInTheDocument();
75
+ });
76
+
77
+ it('should display message for validated order that need organization signature', () => {
78
+ const order = CredentialOrderFactory({
79
+ state: OrderState.VALIDATED,
80
+ contract: ContractFactory({
81
+ student_signed_on: new Date().toISOString(),
82
+ }).one(),
83
+ }).one();
84
+ const contractDefinition = ContractDefinitionFactory().one();
85
+
86
+ render(
87
+ <Wrapper>
88
+ <OrderStateTeacherMessage order={order} contractDefinition={contractDefinition} />
89
+ </Wrapper>,
90
+ );
91
+ expect(screen.getByText('To be signed')).toBeInTheDocument();
92
+ });
93
+
94
+ it("should display message for validated order that don't have a generated certificate", () => {
95
+ const order = CredentialOrderFactory({
96
+ state: OrderState.VALIDATED,
97
+ certificate_id: undefined,
98
+ }).one();
99
+ render(
100
+ <Wrapper>
101
+ <OrderStateTeacherMessage order={order} />
102
+ </Wrapper>,
103
+ );
104
+ expect(
105
+ screen.getByText(intl.formatMessage(messages.statusOnGoing), {
106
+ exact: false,
107
+ }),
108
+ );
109
+ });
110
+
111
+ it('should display message for validated order that have a generated certificate', () => {
112
+ const order = CredentialOrderFactory({
113
+ state: OrderState.VALIDATED,
114
+ certificate_id: 'FAKE_CERTIFICATE_ID',
115
+ }).one();
116
+ render(
117
+ <Wrapper>
118
+ <OrderStateTeacherMessage order={order} />
119
+ </Wrapper>,
120
+ );
121
+ expect(
122
+ screen.getByText(intl.formatMessage(messages.statusCompleted), {
123
+ exact: false,
124
+ }),
125
+ );
126
+ });
127
+ });
@@ -0,0 +1,60 @@
1
+ import { defineMessages } from 'react-intl';
2
+ import OrderStateMessage, { OrderStateMessageBaseProps } from '../OrderStateMessage';
3
+
4
+ export const messages = defineMessages({
5
+ statusDraft: {
6
+ id: 'components.DashboardItem.Order.OrderStateTeacherMessage.statusDraft',
7
+ description: 'Status shown on the dashboard order item when order is draft.',
8
+ defaultMessage: 'Pending',
9
+ },
10
+ statusSubmitted: {
11
+ id: 'components.DashboardItem.Order.OrderStateTeacherMessage.statusSubmitted',
12
+ description: 'Status shown on the dashboard order item when order is submitted.',
13
+ defaultMessage: 'Pending',
14
+ },
15
+ statusPending: {
16
+ id: 'components.DashboardItem.Order.OrderStateTeacherMessage.statusPending',
17
+ description: 'Status shown on the dashboard order item when order is pending.',
18
+ defaultMessage: 'Pending',
19
+ },
20
+ statusOnGoing: {
21
+ id: 'components.DashboardItem.Order.OrderStateTeacherMessage.statusOnGoing',
22
+ description:
23
+ 'Status shown on the dashboard order item when order is validated with no certificate',
24
+ defaultMessage: 'Enrolled',
25
+ },
26
+ statusCompleted: {
27
+ id: 'components.DashboardItem.Order.OrderStateTeacherMessage.statusCompleted',
28
+ description:
29
+ 'Status shown on the dashboard order item when order is validated with certificate',
30
+ defaultMessage: 'Certified',
31
+ },
32
+ statusWaitingSignature: {
33
+ id: 'components.DashboardItem.Order.OrderStateTeacherMessage.statusWaitingSignature',
34
+ description:
35
+ "Status shown on the dashboard order item when order is validated with contract's learner signature missing.",
36
+ defaultMessage: "Pending for learner's signature",
37
+ },
38
+ statusWaitingCounterSignature: {
39
+ id: 'components.DashboardItem.Order.OrderStateTeacherMessage.statusWaitingCounterSignature',
40
+ description:
41
+ "Status shown on the dashboard order item when order is validated with contract's organization signature missing.",
42
+ defaultMessage: 'To be signed',
43
+ },
44
+ statusCanceled: {
45
+ id: 'components.DashboardItem.Order.OrderStateTeacherMessage.statusCanceled',
46
+ description: 'Status shown on the dashboard order item when order is canceled',
47
+ defaultMessage: 'Canceled',
48
+ },
49
+ statusOther: {
50
+ id: 'components.DashboardItem.Order.OrderStateTeacherMessage.statusOther',
51
+ description: 'Status shown on the dashboard order item when order status is unknown',
52
+ defaultMessage: '{state}',
53
+ },
54
+ });
55
+
56
+ const OrderStateTeacherMessage = (props: OrderStateMessageBaseProps) => {
57
+ return <OrderStateMessage {...props} messages={messages} />;
58
+ };
59
+
60
+ export default OrderStateTeacherMessage;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "richie-education",
3
- "version": "2.25.0-b2.dev124",
3
+ "version": "2.25.0-b2.dev128",
4
4
  "description": "A CMS to build learning portals for Open Education",
5
5
  "main": "sandbox/manage.py",
6
6
  "scripts": {