richie-education 2.25.0-b2.dev32 → 2.25.0-b2.dev34
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.
- package/js/api/joanie.ts +26 -3
- package/js/hooks/useContractArchive/index.download.spec.tsx +119 -0
- package/js/hooks/useContractArchive/index.spec.tsx +91 -0
- package/js/hooks/useContractArchive/index.ts +83 -0
- package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.spec.tsx +136 -0
- package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.timer.spec.tsx +144 -0
- package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.tsx +73 -0
- package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.spec.tsx +166 -0
- package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.tsx +23 -8
- package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.spec.tsx +74 -0
- package/js/pages/TeacherDashboardContractsLayout/hooks/useCheckContractArchiveExists/index.spec.tsx +124 -0
- package/js/pages/TeacherDashboardContractsLayout/hooks/useCheckContractArchiveExists/index.tsx +73 -0
- package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/contractArchiveLocalStorage.spec.ts +85 -0
- package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/contractArchiveLocalStorage.ts +50 -0
- package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/index.spec.tsx +266 -0
- package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/index.tsx +153 -0
- package/js/pages/TeacherDashboardContractsLayout/hooks/useHasContractToDownload/index.spec.tsx +100 -0
- package/js/pages/TeacherDashboardContractsLayout/hooks/useHasContractToDownload/index.tsx +23 -0
- package/js/settings.ts +7 -0
- package/js/types/Joanie.ts +5 -0
- package/js/utils/errors/HttpError.ts +1 -0
- package/package.json +1 -1
package/js/api/joanie.ts
CHANGED
|
@@ -12,7 +12,7 @@ import queryString from 'query-string';
|
|
|
12
12
|
import type * as Joanie from 'types/Joanie';
|
|
13
13
|
import { AuthenticationApi } from 'api/authentication';
|
|
14
14
|
import context from 'utils/context';
|
|
15
|
-
import { HttpError } from 'utils/errors/HttpError';
|
|
15
|
+
import { HttpError, HttpStatusCode } from 'utils/errors/HttpError';
|
|
16
16
|
import { JOANIE_API_VERSION } from 'settings';
|
|
17
17
|
import { ResourcesQuery } from 'hooks/useResources';
|
|
18
18
|
import { ObjectHelper } from 'utils/ObjectHelper';
|
|
@@ -20,14 +20,15 @@ import { Maybe } from 'types/utils';
|
|
|
20
20
|
|
|
21
21
|
interface CheckStatusOptions {
|
|
22
22
|
fallbackValue: any;
|
|
23
|
-
ignoredErrorStatus: number[];
|
|
23
|
+
ignoredErrorStatus: (number | HttpStatusCode)[];
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export function getResponseBody(response: Response) {
|
|
27
27
|
if (response.headers.get('Content-Type') === 'application/json') {
|
|
28
28
|
return response.json();
|
|
29
29
|
}
|
|
30
|
-
|
|
30
|
+
const fileType = ['application/pdf', 'application/zip'];
|
|
31
|
+
if (fileType.includes(response.headers.get('Content-Type') || '')) {
|
|
31
32
|
return response.blob();
|
|
32
33
|
}
|
|
33
34
|
return response.text();
|
|
@@ -154,6 +155,10 @@ export const getRoutes = () => {
|
|
|
154
155
|
contracts: {
|
|
155
156
|
get: `${baseUrl}/contracts/:id/`,
|
|
156
157
|
download: `${baseUrl}/contracts/:id/download/`,
|
|
158
|
+
zip_archive: {
|
|
159
|
+
create: `${baseUrl}/contracts/zip-archive/`,
|
|
160
|
+
get: `${baseUrl}/contracts/zip-archive/:id/`,
|
|
161
|
+
},
|
|
157
162
|
},
|
|
158
163
|
},
|
|
159
164
|
organizations: {
|
|
@@ -379,6 +384,24 @@ const API = (): Joanie.API => {
|
|
|
379
384
|
method: 'GET',
|
|
380
385
|
}).then(checkStatus);
|
|
381
386
|
},
|
|
387
|
+
zip_archive: {
|
|
388
|
+
check: async (id) => {
|
|
389
|
+
return fetchWithJWT(ROUTES.user.contracts.zip_archive.get.replace(':id', id), {
|
|
390
|
+
method: 'OPTIONS',
|
|
391
|
+
});
|
|
392
|
+
},
|
|
393
|
+
create: async (payload) => {
|
|
394
|
+
return fetchWithJWT(ROUTES.user.contracts.zip_archive.create, {
|
|
395
|
+
method: 'POST',
|
|
396
|
+
body: JSON.stringify(payload),
|
|
397
|
+
}).then(checkStatus);
|
|
398
|
+
},
|
|
399
|
+
get: async (id) => {
|
|
400
|
+
return fetchWithJWT(ROUTES.user.contracts.zip_archive.get.replace(':id', id), {
|
|
401
|
+
method: 'GET',
|
|
402
|
+
}).then(checkStatus);
|
|
403
|
+
},
|
|
404
|
+
},
|
|
382
405
|
},
|
|
383
406
|
},
|
|
384
407
|
organizations: {
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import fetchMock from 'fetch-mock';
|
|
2
|
+
import { PropsWithChildren } from 'react';
|
|
3
|
+
import { QueryClientProvider } from '@tanstack/react-query';
|
|
4
|
+
import { IntlProvider } from 'react-intl';
|
|
5
|
+
import { fireEvent, renderHook, waitFor } from '@testing-library/react';
|
|
6
|
+
import { faker } from '@faker-js/faker';
|
|
7
|
+
import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
|
|
8
|
+
import { createTestQueryClient } from 'utils/test/createTestQueryClient';
|
|
9
|
+
import { handle } from 'utils/errors/handle';
|
|
10
|
+
import { SessionProvider } from 'contexts/SessionContext';
|
|
11
|
+
import { Deferred } from 'utils/test/deferred';
|
|
12
|
+
import { HttpStatusCode } from 'utils/errors/HttpError';
|
|
13
|
+
import useContractArchive from '.';
|
|
14
|
+
|
|
15
|
+
jest.mock('utils/errors/handle');
|
|
16
|
+
jest.mock('utils/context', () => ({
|
|
17
|
+
__esModule: true,
|
|
18
|
+
default: mockRichieContextFactory({
|
|
19
|
+
authentication: { backend: 'fonzie', endpoint: 'https://auth.test' },
|
|
20
|
+
joanie_backend: { endpoint: 'https://joanie.test' },
|
|
21
|
+
}).one(),
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
const mockHandle = handle as jest.MockedFn<typeof handle>;
|
|
25
|
+
|
|
26
|
+
describe('useContractArchive', () => {
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
fetchMock.get('https://joanie.test/api/v1.0/orders/', []);
|
|
29
|
+
fetchMock.get('https://joanie.test/api/v1.0/addresses/', []);
|
|
30
|
+
fetchMock.get('https://joanie.test/api/v1.0/credit-cards/', []);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
beforeAll(() => {
|
|
34
|
+
// eslint-disable-next-line compat/compat
|
|
35
|
+
URL.createObjectURL = jest.fn();
|
|
36
|
+
// eslint-disable-next-line compat/compat
|
|
37
|
+
URL.revokeObjectURL = jest.fn();
|
|
38
|
+
HTMLAnchorElement.prototype.click = jest.fn();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
jest.clearAllMocks();
|
|
43
|
+
fetchMock.restore();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const Wrapper = ({ children }: PropsWithChildren) => {
|
|
47
|
+
return (
|
|
48
|
+
<QueryClientProvider client={createTestQueryClient({ user: true })}>
|
|
49
|
+
<IntlProvider locale="en">
|
|
50
|
+
<SessionProvider>{children}</SessionProvider>
|
|
51
|
+
</IntlProvider>
|
|
52
|
+
</QueryClientProvider>
|
|
53
|
+
);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
it('downloads the certificate', async () => {
|
|
57
|
+
const archiveId = faker.string.uuid();
|
|
58
|
+
const DOWNLOAD_URL = `https://joanie.test/api/v1.0/contracts/zip-archive/${archiveId}/`;
|
|
59
|
+
const deferred = new Deferred();
|
|
60
|
+
fetchMock.get(DOWNLOAD_URL, deferred.promise);
|
|
61
|
+
|
|
62
|
+
const { result } = renderHook(() => useContractArchive(), {
|
|
63
|
+
wrapper: Wrapper,
|
|
64
|
+
});
|
|
65
|
+
await waitFor(() => expect(result.current).not.toBeNull());
|
|
66
|
+
|
|
67
|
+
expect(fetchMock.called(DOWNLOAD_URL)).toBe(false);
|
|
68
|
+
// eslint-disable-next-line compat/compat
|
|
69
|
+
expect(URL.createObjectURL).toHaveBeenCalledTimes(0);
|
|
70
|
+
// eslint-disable-next-line compat/compat
|
|
71
|
+
expect(URL.revokeObjectURL).toHaveBeenCalledTimes(0);
|
|
72
|
+
|
|
73
|
+
result.current.methods.get(archiveId);
|
|
74
|
+
deferred.resolve(HttpStatusCode.OK);
|
|
75
|
+
|
|
76
|
+
await waitFor(() => {
|
|
77
|
+
expect(fetchMock.called(DOWNLOAD_URL)).toBe(true);
|
|
78
|
+
// eslint-disable-next-line compat/compat
|
|
79
|
+
expect(URL.createObjectURL).toHaveBeenCalledTimes(1);
|
|
80
|
+
// eslint-disable-next-line compat/compat
|
|
81
|
+
expect(URL.revokeObjectURL).toHaveBeenCalledTimes(0);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// - A event listener should have attached to window to know when window is blurred.
|
|
85
|
+
// This event is triggered in browser when the download pop up is displayed.
|
|
86
|
+
fireEvent.blur(window);
|
|
87
|
+
// eslint-disable-next-line compat/compat
|
|
88
|
+
expect(URL.revokeObjectURL).toHaveBeenCalledTimes(1);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('handles an error if certificate download request fails', async () => {
|
|
92
|
+
const archiveId = faker.string.uuid();
|
|
93
|
+
const DOWNLOAD_URL = `https://joanie.test/api/v1.0/contracts/zip-archive/${archiveId}/`;
|
|
94
|
+
fetchMock.get(DOWNLOAD_URL, HttpStatusCode.UNAUTHORIZED);
|
|
95
|
+
|
|
96
|
+
const { result } = renderHook(() => useContractArchive(), {
|
|
97
|
+
wrapper: Wrapper,
|
|
98
|
+
});
|
|
99
|
+
await waitFor(() => expect(result.current).not.toBeNull());
|
|
100
|
+
|
|
101
|
+
expect(fetchMock.called(DOWNLOAD_URL)).toBe(false);
|
|
102
|
+
expect(mockHandle).not.toHaveBeenCalled();
|
|
103
|
+
// eslint-disable-next-line compat/compat
|
|
104
|
+
expect(URL.createObjectURL).toHaveBeenCalledTimes(0);
|
|
105
|
+
// eslint-disable-next-line compat/compat
|
|
106
|
+
expect(URL.revokeObjectURL).toHaveBeenCalledTimes(0);
|
|
107
|
+
|
|
108
|
+
await result.current.methods.get(archiveId);
|
|
109
|
+
|
|
110
|
+
await waitFor(() => {
|
|
111
|
+
expect(fetchMock.called(DOWNLOAD_URL)).toBe(true);
|
|
112
|
+
expect(mockHandle).toHaveBeenNthCalledWith(1, new Error('Unauthorized'));
|
|
113
|
+
// eslint-disable-next-line compat/compat
|
|
114
|
+
expect(URL.createObjectURL).toHaveBeenCalledTimes(0);
|
|
115
|
+
// eslint-disable-next-line compat/compat
|
|
116
|
+
expect(URL.revokeObjectURL).toHaveBeenCalledTimes(0);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { faker } from '@faker-js/faker';
|
|
2
|
+
import fetchMock from 'fetch-mock';
|
|
3
|
+
import { renderHook } from '@testing-library/react';
|
|
4
|
+
import { IntlProvider } from 'react-intl';
|
|
5
|
+
import { PropsWithChildren } from 'react';
|
|
6
|
+
import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
|
|
7
|
+
import JoanieApiProvider from 'contexts/JoanieApiContext';
|
|
8
|
+
import { HttpStatusCode } from 'utils/errors/HttpError';
|
|
9
|
+
import useContractArchive from '.';
|
|
10
|
+
|
|
11
|
+
jest.mock('utils/context', () => ({
|
|
12
|
+
__esModule: true,
|
|
13
|
+
default: mockRichieContextFactory({
|
|
14
|
+
authentication: { backend: 'fonzie', endpoint: 'https://auth.test' },
|
|
15
|
+
joanie_backend: { endpoint: 'https://joanie.test' },
|
|
16
|
+
}).one(),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
describe('useContractArchive', () => {
|
|
20
|
+
const Wrapper = ({ children }: PropsWithChildren) => {
|
|
21
|
+
return (
|
|
22
|
+
<IntlProvider locale="en">
|
|
23
|
+
<JoanieApiProvider>{children}</JoanieApiProvider>
|
|
24
|
+
</IntlProvider>
|
|
25
|
+
);
|
|
26
|
+
};
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
fetchMock.restore();
|
|
29
|
+
});
|
|
30
|
+
it.each([
|
|
31
|
+
{
|
|
32
|
+
label: `response code: ${HttpStatusCode.NO_CONTENT}`,
|
|
33
|
+
statusCode: HttpStatusCode.NO_CONTENT,
|
|
34
|
+
expectedValue: true,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
label: `response code: ${HttpStatusCode.NOT_FOUND}`,
|
|
38
|
+
statusCode: HttpStatusCode.NOT_FOUND,
|
|
39
|
+
expectedValue: false,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
label: `response code: ${HttpStatusCode.FORBIDDEN}`,
|
|
43
|
+
statusCode: HttpStatusCode.FORBIDDEN,
|
|
44
|
+
expectedValue: false,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
label: `response code: ${HttpStatusCode.INTERNAL_SERVER_ERROR}`,
|
|
48
|
+
statusCode: HttpStatusCode.INTERNAL_SERVER_ERROR,
|
|
49
|
+
expectedValue: false,
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
label: `response code: ${HttpStatusCode.UNAUTHORIZED}`,
|
|
53
|
+
statusCode: HttpStatusCode.UNAUTHORIZED,
|
|
54
|
+
expectedValue: false,
|
|
55
|
+
},
|
|
56
|
+
])(
|
|
57
|
+
'check method should return the right value for response code: $label',
|
|
58
|
+
async ({ statusCode, expectedValue }) => {
|
|
59
|
+
const contractArchiveId = faker.string.uuid();
|
|
60
|
+
fetchMock.mock(
|
|
61
|
+
(url, options) => {
|
|
62
|
+
return (
|
|
63
|
+
options.method === 'OPTIONS' &&
|
|
64
|
+
url === `https://joanie.test/api/v1.0/contracts/zip-archive/${contractArchiveId}/`
|
|
65
|
+
);
|
|
66
|
+
},
|
|
67
|
+
new Response('', { status: statusCode }),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const { result } = renderHook(useContractArchive, {
|
|
71
|
+
wrapper: Wrapper,
|
|
72
|
+
});
|
|
73
|
+
const response = await result.current.methods.check(contractArchiveId);
|
|
74
|
+
expect(response).toBe(expectedValue);
|
|
75
|
+
},
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
it('create should return a contractArchiveId', async () => {
|
|
79
|
+
const contractArchiveId = faker.string.uuid();
|
|
80
|
+
fetchMock.post('https://joanie.test/api/v1.0/contracts/zip-archive/', {
|
|
81
|
+
url: `http://test.url/uri/${contractArchiveId}/uri/?toto=tata`,
|
|
82
|
+
});
|
|
83
|
+
const { result } = renderHook(useContractArchive, {
|
|
84
|
+
wrapper: Wrapper,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const organizationId = faker.string.uuid();
|
|
88
|
+
const response = await result.current.methods.create(organizationId);
|
|
89
|
+
expect(response).toBe(contractArchiveId);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { useJoanieApi } from 'contexts/JoanieApiContext';
|
|
2
|
+
import { HttpStatusCode } from 'utils/errors/HttpError';
|
|
3
|
+
import { handle } from 'utils/errors/handle';
|
|
4
|
+
|
|
5
|
+
const extractArchiveId = (url: string): string => {
|
|
6
|
+
const uuidRegex = /[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}/;
|
|
7
|
+
const match = url.match(uuidRegex);
|
|
8
|
+
|
|
9
|
+
if (match === null) {
|
|
10
|
+
const error = new Error(
|
|
11
|
+
'Unable to extract `contractArchiveId` from `contract.zip_archive.create` response',
|
|
12
|
+
);
|
|
13
|
+
handle(error);
|
|
14
|
+
throw error;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return match[0];
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// TODO: should be factorized with useDownloadCertificate
|
|
21
|
+
// and maybe DownloadContractButton
|
|
22
|
+
const buildArchiveFromBlob = (fileName: string, blob: Blob) => {
|
|
23
|
+
// eslint-disable-next-line compat/compat
|
|
24
|
+
const url = URL.createObjectURL(blob);
|
|
25
|
+
const $link = document.createElement('a');
|
|
26
|
+
$link.href = url;
|
|
27
|
+
$link.download = fileName;
|
|
28
|
+
|
|
29
|
+
const revokeObject = () => {
|
|
30
|
+
// eslint-disable-next-line compat/compat
|
|
31
|
+
URL.revokeObjectURL(url);
|
|
32
|
+
window.removeEventListener('blur', revokeObject);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
window.addEventListener('blur', revokeObject);
|
|
36
|
+
$link.click();
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const useContractArchive = () => {
|
|
40
|
+
const api = useJoanieApi();
|
|
41
|
+
return {
|
|
42
|
+
methods: {
|
|
43
|
+
check: async (archiveId: string): Promise<boolean> => {
|
|
44
|
+
const response = await api.user.contracts.zip_archive.check(archiveId);
|
|
45
|
+
|
|
46
|
+
if (response.ok) {
|
|
47
|
+
if (response.status === HttpStatusCode.NO_CONTENT) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
handle(
|
|
51
|
+
new Error(
|
|
52
|
+
`Unknown success code ${response.status} for OPTION request to contract's zip_archive endpoint.`,
|
|
53
|
+
),
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return false;
|
|
58
|
+
},
|
|
59
|
+
get: async (archiveId: string): Promise<true | void> => {
|
|
60
|
+
try {
|
|
61
|
+
const response = api.user.contracts.zip_archive.get(archiveId);
|
|
62
|
+
buildArchiveFromBlob('contracts.zip', await response);
|
|
63
|
+
return true;
|
|
64
|
+
} catch (error) {
|
|
65
|
+
handle(error);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// FIXME: two thing could happen here with HttpStatusCode.NOT_FOUND response:
|
|
69
|
+
// * nothing found because the zip is generating.
|
|
70
|
+
// * nothing found because the zip doesn't and will never exist.
|
|
71
|
+
},
|
|
72
|
+
create: async (organizationId: string): Promise<string> => {
|
|
73
|
+
const response = await api.user.contracts.zip_archive.create({
|
|
74
|
+
organization_id: organizationId,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return extractArchiveId(response.url);
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export default useContractArchive;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { faker } from '@faker-js/faker';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import { IntlProvider } from 'react-intl';
|
|
4
|
+
import { PropsWithChildren } from 'react';
|
|
5
|
+
import { QueryClientProvider } from '@tanstack/react-query';
|
|
6
|
+
import userEvent from '@testing-library/user-event';
|
|
7
|
+
import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
|
|
8
|
+
import JoanieApiProvider from 'contexts/JoanieApiContext';
|
|
9
|
+
|
|
10
|
+
import { createTestQueryClient } from 'utils/test/createTestQueryClient';
|
|
11
|
+
import { ContractDownloadStatus } from 'pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive';
|
|
12
|
+
|
|
13
|
+
import BulkDownloadContractButton from '.';
|
|
14
|
+
|
|
15
|
+
jest.mock('utils/context', () => ({
|
|
16
|
+
__esModule: true,
|
|
17
|
+
default: mockRichieContextFactory({
|
|
18
|
+
authentication: { backend: 'fonzie', endpoint: 'https://auth.test' },
|
|
19
|
+
joanie_backend: { endpoint: 'https://joanie.test' },
|
|
20
|
+
}).one(),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
let mockDownloadContractArchive = jest.fn(() => Promise<void>);
|
|
24
|
+
let mockCreateContractArchive = jest.fn(() => Promise<void>);
|
|
25
|
+
let mockDownloadContractArchiveStatus: ContractDownloadStatus;
|
|
26
|
+
jest.mock('pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive', () => ({
|
|
27
|
+
__esModule: true,
|
|
28
|
+
...jest.requireActual('pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive'),
|
|
29
|
+
default: () => ({
|
|
30
|
+
downloadContractArchive: mockDownloadContractArchive,
|
|
31
|
+
createContractArchive: mockCreateContractArchive,
|
|
32
|
+
status: mockDownloadContractArchiveStatus,
|
|
33
|
+
}),
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
let mockHasContractToDownload: boolean;
|
|
37
|
+
jest.mock('pages/TeacherDashboardContractsLayout/hooks/useHasContractToDownload/index.tsx', () => ({
|
|
38
|
+
__esModule: true,
|
|
39
|
+
default: () => mockHasContractToDownload,
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
describe('TeacherDashboardContractsLayout/BulkDownloadContractButton', () => {
|
|
43
|
+
const Wrapper = ({ children }: PropsWithChildren) => {
|
|
44
|
+
return (
|
|
45
|
+
<IntlProvider locale="en">
|
|
46
|
+
<QueryClientProvider client={createTestQueryClient({ user: true })}>
|
|
47
|
+
<JoanieApiProvider>{children}</JoanieApiProvider>
|
|
48
|
+
</QueryClientProvider>
|
|
49
|
+
</IntlProvider>
|
|
50
|
+
);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
// useDownloadContractArchive mocked values
|
|
55
|
+
mockHasContractToDownload = false;
|
|
56
|
+
mockDownloadContractArchive = jest.fn(() => Promise<void>);
|
|
57
|
+
mockCreateContractArchive = jest.fn(() => Promise<void>);
|
|
58
|
+
mockDownloadContractArchiveStatus = ContractDownloadStatus.IDLE;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
afterEach(() => {
|
|
62
|
+
jest.resetAllMocks();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("shouldn't render generate archive button for archive generation IDLE", async () => {
|
|
66
|
+
mockDownloadContractArchiveStatus = ContractDownloadStatus.IDLE;
|
|
67
|
+
|
|
68
|
+
render(
|
|
69
|
+
<Wrapper>
|
|
70
|
+
<BulkDownloadContractButton organizationId={faker.string.uuid()} />
|
|
71
|
+
</Wrapper>,
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const $button = screen.queryByRole('button', { name: /Request contracts archive/ });
|
|
75
|
+
expect($button).toBeInTheDocument();
|
|
76
|
+
expect($button).toBeEnabled();
|
|
77
|
+
|
|
78
|
+
const user = userEvent.setup();
|
|
79
|
+
await user.click($button!);
|
|
80
|
+
expect(mockDownloadContractArchive).toHaveBeenCalledTimes(1);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("shouldn't render waiting archive button for archive generation PENDING", async () => {
|
|
84
|
+
mockDownloadContractArchiveStatus = ContractDownloadStatus.PENDING;
|
|
85
|
+
|
|
86
|
+
render(
|
|
87
|
+
<Wrapper>
|
|
88
|
+
<BulkDownloadContractButton organizationId={faker.string.uuid()} />
|
|
89
|
+
</Wrapper>,
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const $button = screen.queryByRole('button', { name: /Generating contracts archive.../ });
|
|
93
|
+
expect($button).toBeInTheDocument();
|
|
94
|
+
expect($button).toBeDisabled();
|
|
95
|
+
|
|
96
|
+
const user = userEvent.setup();
|
|
97
|
+
await user.click($button!);
|
|
98
|
+
expect(mockDownloadContractArchive).not.toHaveBeenCalled();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("shouldn't render download button for archive is READY", async () => {
|
|
102
|
+
mockDownloadContractArchiveStatus = ContractDownloadStatus.READY;
|
|
103
|
+
|
|
104
|
+
render(
|
|
105
|
+
<Wrapper>
|
|
106
|
+
<BulkDownloadContractButton organizationId={faker.string.uuid()} />
|
|
107
|
+
</Wrapper>,
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const $button = screen.queryByRole('button', { name: /Download contracts archive/ });
|
|
111
|
+
expect($button).toBeInTheDocument();
|
|
112
|
+
expect($button).toBeEnabled();
|
|
113
|
+
|
|
114
|
+
const user = userEvent.setup();
|
|
115
|
+
await user.click($button!);
|
|
116
|
+
expect(mockDownloadContractArchive).toHaveBeenCalledTimes(1);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should render disabled download button when INITIALIZING', async () => {
|
|
120
|
+
mockDownloadContractArchiveStatus = ContractDownloadStatus.INITIALIZING;
|
|
121
|
+
|
|
122
|
+
render(
|
|
123
|
+
<Wrapper>
|
|
124
|
+
<BulkDownloadContractButton organizationId={faker.string.uuid()} />
|
|
125
|
+
</Wrapper>,
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const $button = screen.queryByRole('button', { name: /Download contracts archive/ });
|
|
129
|
+
expect($button).toBeInTheDocument();
|
|
130
|
+
expect($button).toBeDisabled();
|
|
131
|
+
|
|
132
|
+
const user = userEvent.setup();
|
|
133
|
+
await user.click($button!);
|
|
134
|
+
expect(mockDownloadContractArchive).not.toHaveBeenCalled();
|
|
135
|
+
});
|
|
136
|
+
});
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { faker } from '@faker-js/faker';
|
|
2
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
3
|
+
import { IntlProvider } from 'react-intl';
|
|
4
|
+
import { PropsWithChildren } from 'react';
|
|
5
|
+
import { QueryClientProvider } from '@tanstack/react-query';
|
|
6
|
+
import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
|
|
7
|
+
import JoanieApiProvider from 'contexts/JoanieApiContext';
|
|
8
|
+
import { createTestQueryClient } from 'utils/test/createTestQueryClient';
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
getStoredContractArchiveId,
|
|
12
|
+
storeContractArchiveId,
|
|
13
|
+
unstoreContractArchiveId,
|
|
14
|
+
} from 'pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/contractArchiveLocalStorage';
|
|
15
|
+
import { CONTRACT_DOWNLOAD_SETTINGS } from 'settings';
|
|
16
|
+
import BulkDownloadContractButton from '.';
|
|
17
|
+
|
|
18
|
+
jest.mock('utils/context', () => ({
|
|
19
|
+
__esModule: true,
|
|
20
|
+
default: mockRichieContextFactory({
|
|
21
|
+
authentication: { backend: 'fonzie', endpoint: 'https://auth.test' },
|
|
22
|
+
joanie_backend: { endpoint: 'https://joanie.test' },
|
|
23
|
+
}).one(),
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
let mockHasContractToDownload: boolean;
|
|
27
|
+
jest.mock('pages/TeacherDashboardContractsLayout/hooks/useHasContractToDownload/index.tsx', () => ({
|
|
28
|
+
__esModule: true,
|
|
29
|
+
default: () => mockHasContractToDownload,
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
const mockCheckArchive = jest.fn();
|
|
33
|
+
const mockCreateArchive = jest.fn();
|
|
34
|
+
const mockGetArchive = jest.fn();
|
|
35
|
+
jest.mock('hooks/useContractArchive', () => ({
|
|
36
|
+
__esModule: true,
|
|
37
|
+
default: () => ({
|
|
38
|
+
methods: { get: mockGetArchive, create: mockCreateArchive, check: mockCheckArchive },
|
|
39
|
+
}),
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
describe('TeacherDashboardContractsLayout/BulkDownloadContractButton with fake timer', () => {
|
|
43
|
+
const Wrapper = ({ children }: PropsWithChildren) => {
|
|
44
|
+
return (
|
|
45
|
+
<IntlProvider locale="en">
|
|
46
|
+
<QueryClientProvider client={createTestQueryClient({ user: true })}>
|
|
47
|
+
<JoanieApiProvider>{children}</JoanieApiProvider>
|
|
48
|
+
</QueryClientProvider>
|
|
49
|
+
</IntlProvider>
|
|
50
|
+
);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
let contractArchiveId: string;
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
mockHasContractToDownload = true;
|
|
56
|
+
|
|
57
|
+
const now = Date.now();
|
|
58
|
+
const unvalidCreationTime =
|
|
59
|
+
now - CONTRACT_DOWNLOAD_SETTINGS.contractArchiveLocalVaklidityDurationMs * 2;
|
|
60
|
+
|
|
61
|
+
jest.useFakeTimers();
|
|
62
|
+
jest.setSystemTime(new Date(unvalidCreationTime));
|
|
63
|
+
contractArchiveId = faker.string.uuid();
|
|
64
|
+
storeContractArchiveId(contractArchiveId);
|
|
65
|
+
jest.setSystemTime(new Date(now));
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
afterEach(() => {
|
|
69
|
+
jest.runOnlyPendingTimers();
|
|
70
|
+
jest.useRealTimers();
|
|
71
|
+
jest.clearAllMocks();
|
|
72
|
+
unstoreContractArchiveId();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("should return IDLE status and clear stored id when archive doesn't exists on the server", async () => {
|
|
76
|
+
mockHasContractToDownload = true;
|
|
77
|
+
mockCheckArchive.mockResolvedValue(false);
|
|
78
|
+
|
|
79
|
+
render(
|
|
80
|
+
<Wrapper>
|
|
81
|
+
<BulkDownloadContractButton organizationId={faker.string.uuid()} />
|
|
82
|
+
</Wrapper>,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const $downloadButton = screen.queryByRole('button', {
|
|
86
|
+
name: /Download contracts archive/,
|
|
87
|
+
});
|
|
88
|
+
expect($downloadButton).toBeInTheDocument();
|
|
89
|
+
|
|
90
|
+
// BulkDownloadButton is disabled until initiliazed
|
|
91
|
+
expect($downloadButton).toBeDisabled();
|
|
92
|
+
expect(mockCheckArchive).toHaveBeenCalledTimes(1);
|
|
93
|
+
|
|
94
|
+
// Button should initalize with idle state and propose an archive generation
|
|
95
|
+
await waitFor(() => {
|
|
96
|
+
const $createArchiveButton = screen.queryByRole('button', {
|
|
97
|
+
name: /Request contracts archive/,
|
|
98
|
+
});
|
|
99
|
+
expect($createArchiveButton).toBeInTheDocument();
|
|
100
|
+
expect($createArchiveButton).toBeEnabled();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
expect(getStoredContractArchiveId()).toBe(null);
|
|
104
|
+
expect(mockGetArchive).not.toHaveBeenCalled();
|
|
105
|
+
expect(mockCreateArchive).not.toHaveBeenCalled();
|
|
106
|
+
|
|
107
|
+
// polling shouldn't start, mockCheckArchive shouldn't been called more than once
|
|
108
|
+
expect(mockCheckArchive).toHaveBeenCalledTimes(1);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should return READY status when archive exists on the server', async () => {
|
|
112
|
+
mockHasContractToDownload = true;
|
|
113
|
+
mockCheckArchive.mockResolvedValue(true);
|
|
114
|
+
|
|
115
|
+
render(
|
|
116
|
+
<Wrapper>
|
|
117
|
+
<BulkDownloadContractButton organizationId={faker.string.uuid()} />
|
|
118
|
+
</Wrapper>,
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const $initButton = screen.queryByRole('button', {
|
|
122
|
+
name: /Download contracts archive/,
|
|
123
|
+
});
|
|
124
|
+
expect($initButton).toBeInTheDocument();
|
|
125
|
+
// BulkDownloadButton is disabled until initiliazed
|
|
126
|
+
expect($initButton).toBeDisabled();
|
|
127
|
+
expect(mockCheckArchive).toHaveBeenCalledTimes(1);
|
|
128
|
+
|
|
129
|
+
await waitFor(() => {
|
|
130
|
+
const $downloadButton = screen.queryByRole('button', {
|
|
131
|
+
name: /Download contracts archive/,
|
|
132
|
+
});
|
|
133
|
+
expect($downloadButton).toBeInTheDocument();
|
|
134
|
+
expect($downloadButton).toBeEnabled();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
expect(getStoredContractArchiveId()).toBe(contractArchiveId);
|
|
138
|
+
expect(mockGetArchive).not.toHaveBeenCalled();
|
|
139
|
+
expect(mockCreateArchive).not.toHaveBeenCalled();
|
|
140
|
+
|
|
141
|
+
// polling shouldn't start, mockCheckArchive shouldn't been called more than once
|
|
142
|
+
expect(mockCheckArchive).toHaveBeenCalledTimes(1);
|
|
143
|
+
});
|
|
144
|
+
});
|