richie-education 2.25.0-b2.dev31 → 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.
Files changed (22) hide show
  1. package/js/api/joanie.ts +26 -3
  2. package/js/hooks/useContractArchive/index.download.spec.tsx +119 -0
  3. package/js/hooks/useContractArchive/index.spec.tsx +91 -0
  4. package/js/hooks/useContractArchive/index.ts +83 -0
  5. package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.spec.tsx +136 -0
  6. package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.timer.spec.tsx +144 -0
  7. package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.tsx +73 -0
  8. package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.spec.tsx +166 -0
  9. package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.tsx +23 -8
  10. package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.spec.tsx +74 -0
  11. package/js/pages/TeacherDashboardContractsLayout/hooks/useCheckContractArchiveExists/index.spec.tsx +124 -0
  12. package/js/pages/TeacherDashboardContractsLayout/hooks/useCheckContractArchiveExists/index.tsx +73 -0
  13. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/contractArchiveLocalStorage.spec.ts +85 -0
  14. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/contractArchiveLocalStorage.ts +50 -0
  15. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/index.spec.tsx +266 -0
  16. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/index.tsx +153 -0
  17. package/js/pages/TeacherDashboardContractsLayout/hooks/useHasContractToDownload/index.spec.tsx +100 -0
  18. package/js/pages/TeacherDashboardContractsLayout/hooks/useHasContractToDownload/index.tsx +23 -0
  19. package/js/settings.ts +7 -0
  20. package/js/types/Joanie.ts +5 -0
  21. package/js/utils/errors/HttpError.ts +1 -0
  22. 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
- if (response.headers.get('Content-Type') === 'application/pdf') {
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
+ });