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.
- 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/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.tsx
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Button } from '@openfun/cunningham-react';
|
|
2
|
+
import { FormattedMessage, defineMessages } from 'react-intl';
|
|
3
|
+
import { useEffect } from 'react';
|
|
4
|
+
import useDownloadContractArchive, {
|
|
5
|
+
ContractDownloadStatus,
|
|
6
|
+
} from 'pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive';
|
|
7
|
+
import { Organization } from 'types/Joanie';
|
|
8
|
+
|
|
9
|
+
const messages = defineMessages({
|
|
10
|
+
bulkDownloadButtonDownloadLabel: {
|
|
11
|
+
defaultMessage: 'Download contracts archive',
|
|
12
|
+
description: 'The label of the bulk download button when the zip archive is ready for download',
|
|
13
|
+
id: 'pages.TeacherDashboardContractsLayout.BulkDownloadContractButton.bulkDownloadButtonDownloadLabel',
|
|
14
|
+
},
|
|
15
|
+
bulkDownloadButtonPendingLabel: {
|
|
16
|
+
defaultMessage: 'Generating contracts archive...',
|
|
17
|
+
description: 'The label of the bulk download button when archive generation is pending',
|
|
18
|
+
id: 'pages.TeacherDashboardContractsLayout.BulkDownloadContractButton.bulkDownloadButtonPendingLabel',
|
|
19
|
+
},
|
|
20
|
+
bulkDownloadButtonRequestArchiveLabel: {
|
|
21
|
+
defaultMessage: 'Request contracts archive',
|
|
22
|
+
description: 'The label of the bulk download button to request the generation of a zip archive',
|
|
23
|
+
id: 'pages.TeacherDashboardContractsLayout.BulkDownloadContractButton.bulkDownloadButtonRequestArchiveLabel',
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
interface BulkDownloadContractButtonProps {
|
|
28
|
+
organizationId: Organization['id'];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const BulkDownloadContractButton = ({ organizationId }: BulkDownloadContractButtonProps) => {
|
|
32
|
+
const { downloadContractArchive, createContractArchive, status } = useDownloadContractArchive({
|
|
33
|
+
organizationId,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
// Trigger contract's archive polling when generation had already been requested
|
|
38
|
+
if (status === ContractDownloadStatus.PENDING) {
|
|
39
|
+
createContractArchive();
|
|
40
|
+
}
|
|
41
|
+
}, [status]);
|
|
42
|
+
|
|
43
|
+
if (status === ContractDownloadStatus.PENDING) {
|
|
44
|
+
return (
|
|
45
|
+
<Button
|
|
46
|
+
disabled={true}
|
|
47
|
+
color="tertiary"
|
|
48
|
+
size="small"
|
|
49
|
+
icon={<div className="spinner spinner--small" />}
|
|
50
|
+
>
|
|
51
|
+
<FormattedMessage {...messages.bulkDownloadButtonPendingLabel} />
|
|
52
|
+
</Button>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<Button
|
|
58
|
+
onClick={downloadContractArchive}
|
|
59
|
+
disabled={status === ContractDownloadStatus.INITIALIZING}
|
|
60
|
+
color={status === ContractDownloadStatus.READY ? 'primary' : 'tertiary'}
|
|
61
|
+
size="small"
|
|
62
|
+
icon={<span className="material-icons">download</span>}
|
|
63
|
+
>
|
|
64
|
+
<FormattedMessage
|
|
65
|
+
{...(status === ContractDownloadStatus.IDLE
|
|
66
|
+
? messages.bulkDownloadButtonRequestArchiveLabel
|
|
67
|
+
: messages.bulkDownloadButtonDownloadLabel)}
|
|
68
|
+
/>
|
|
69
|
+
</Button>
|
|
70
|
+
);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export default BulkDownloadContractButton;
|
|
@@ -0,0 +1,166 @@
|
|
|
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 { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
|
|
7
|
+
import JoanieApiProvider from 'contexts/JoanieApiContext';
|
|
8
|
+
|
|
9
|
+
import { createTestQueryClient } from 'utils/test/createTestQueryClient';
|
|
10
|
+
import { ContractDownloadStatus } from 'pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive';
|
|
11
|
+
import ContractActionsBar from '.';
|
|
12
|
+
|
|
13
|
+
jest.mock('utils/context', () => ({
|
|
14
|
+
__esModule: true,
|
|
15
|
+
default: mockRichieContextFactory({
|
|
16
|
+
authentication: { backend: 'fonzie', endpoint: 'https://auth.test' },
|
|
17
|
+
joanie_backend: { endpoint: 'https://joanie.test' },
|
|
18
|
+
}).one(),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
let mockCanSignContracts: boolean;
|
|
22
|
+
let mockContractsToSignCount: number;
|
|
23
|
+
jest.mock('pages/TeacherDashboardContractsLayout/hooks/useTeacherContractsToSign', () => ({
|
|
24
|
+
__esModule: true,
|
|
25
|
+
default: () => ({
|
|
26
|
+
canSignContracts: mockCanSignContracts,
|
|
27
|
+
contractsToSignCount: mockContractsToSignCount,
|
|
28
|
+
}),
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
let mockDownloadContractArchive: () => Promise<void>;
|
|
32
|
+
let mockCreateContractArchive: () => Promise<void>;
|
|
33
|
+
let mockDownloadContractArchiveStatus: ContractDownloadStatus;
|
|
34
|
+
jest.mock('pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive', () => ({
|
|
35
|
+
__esModule: true,
|
|
36
|
+
...jest.requireActual('pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive'),
|
|
37
|
+
default: () => ({
|
|
38
|
+
downloadArchive: mockDownloadContractArchive,
|
|
39
|
+
createContractArchive: mockCreateContractArchive,
|
|
40
|
+
status: mockDownloadContractArchiveStatus,
|
|
41
|
+
}),
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
let mockHasContractToDownload: boolean;
|
|
45
|
+
jest.mock('pages/TeacherDashboardContractsLayout/hooks/useHasContractToDownload/index.tsx', () => ({
|
|
46
|
+
__esModule: true,
|
|
47
|
+
default: () => mockHasContractToDownload,
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
describe('TeacherDashboardContractsLayout/ContractActionsBar', () => {
|
|
51
|
+
const Wrapper = ({ children }: PropsWithChildren) => {
|
|
52
|
+
return (
|
|
53
|
+
<IntlProvider locale="en">
|
|
54
|
+
<QueryClientProvider client={createTestQueryClient({ user: true })}>
|
|
55
|
+
<JoanieApiProvider>{children}</JoanieApiProvider>
|
|
56
|
+
</QueryClientProvider>
|
|
57
|
+
</IntlProvider>
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
beforeAll(() => {
|
|
62
|
+
const modalExclude = document.createElement('div');
|
|
63
|
+
modalExclude.setAttribute('id', 'modal-exclude');
|
|
64
|
+
document.body.appendChild(modalExclude);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
beforeEach(() => {
|
|
68
|
+
// useTeacherContractsToSign mocked values
|
|
69
|
+
mockCanSignContracts = true;
|
|
70
|
+
mockContractsToSignCount = 1;
|
|
71
|
+
|
|
72
|
+
// useDownloadContractArchive mocked values
|
|
73
|
+
mockHasContractToDownload = false;
|
|
74
|
+
mockDownloadContractArchive = jest.fn(() => Promise.resolve());
|
|
75
|
+
mockCreateContractArchive = jest.fn(() => Promise.resolve());
|
|
76
|
+
mockDownloadContractArchiveStatus = ContractDownloadStatus.IDLE;
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
afterEach(() => {
|
|
80
|
+
jest.resetAllMocks();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("shouldn't display both sign and download button", () => {
|
|
84
|
+
mockHasContractToDownload = true;
|
|
85
|
+
mockCanSignContracts = true;
|
|
86
|
+
mockContractsToSignCount = 1;
|
|
87
|
+
|
|
88
|
+
render(
|
|
89
|
+
<Wrapper>
|
|
90
|
+
<ContractActionsBar
|
|
91
|
+
courseProductRelationId={faker.string.uuid()}
|
|
92
|
+
organizationId={faker.string.uuid()}
|
|
93
|
+
/>
|
|
94
|
+
</Wrapper>,
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
expect(screen.getByTestId('teacher-contracts-list-actionsBar')).toBeInTheDocument();
|
|
98
|
+
expect(screen.getByRole('button', { name: /Sign all pending contracts/ })).toBeInTheDocument();
|
|
99
|
+
expect(screen.getByRole('button', { name: /Request contracts archive/ })).toBeInTheDocument();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("shouldn't only display sign button", () => {
|
|
103
|
+
mockHasContractToDownload = false;
|
|
104
|
+
mockCanSignContracts = true;
|
|
105
|
+
mockContractsToSignCount = 1;
|
|
106
|
+
|
|
107
|
+
render(
|
|
108
|
+
<Wrapper>
|
|
109
|
+
<ContractActionsBar
|
|
110
|
+
courseProductRelationId={faker.string.uuid()}
|
|
111
|
+
organizationId={faker.string.uuid()}
|
|
112
|
+
/>
|
|
113
|
+
</Wrapper>,
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
expect(screen.getByTestId('teacher-contracts-list-actionsBar')).toBeInTheDocument();
|
|
117
|
+
expect(screen.getByRole('button', { name: /Sign all pending contracts/ })).toBeInTheDocument();
|
|
118
|
+
expect(
|
|
119
|
+
screen.queryByRole('button', { name: /Request contracts archive/ }),
|
|
120
|
+
).not.toBeInTheDocument();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("shouldn't only display download button", () => {
|
|
124
|
+
mockHasContractToDownload = true;
|
|
125
|
+
mockCanSignContracts = false;
|
|
126
|
+
mockContractsToSignCount = 0;
|
|
127
|
+
|
|
128
|
+
render(
|
|
129
|
+
<Wrapper>
|
|
130
|
+
<ContractActionsBar
|
|
131
|
+
courseProductRelationId={faker.string.uuid()}
|
|
132
|
+
organizationId={faker.string.uuid()}
|
|
133
|
+
/>
|
|
134
|
+
</Wrapper>,
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
expect(screen.getByTestId('teacher-contracts-list-actionsBar')).toBeInTheDocument();
|
|
138
|
+
expect(screen.getByRole('button', { name: /Request contracts archive/ })).toBeInTheDocument();
|
|
139
|
+
expect(
|
|
140
|
+
screen.queryByRole('button', { name: /Sign all pending contracts/ }),
|
|
141
|
+
).not.toBeInTheDocument();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should return nothing when no actions are available', () => {
|
|
145
|
+
mockHasContractToDownload = false;
|
|
146
|
+
mockCanSignContracts = false;
|
|
147
|
+
mockContractsToSignCount = 0;
|
|
148
|
+
|
|
149
|
+
render(
|
|
150
|
+
<Wrapper>
|
|
151
|
+
<ContractActionsBar
|
|
152
|
+
courseProductRelationId={faker.string.uuid()}
|
|
153
|
+
organizationId={faker.string.uuid()}
|
|
154
|
+
/>
|
|
155
|
+
</Wrapper>,
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
expect(screen.queryByTestId('teacher-contracts-list-actionsBar')).not.toBeInTheDocument();
|
|
159
|
+
expect(
|
|
160
|
+
screen.queryByRole('button', { name: /Request contracts archive/ }),
|
|
161
|
+
).not.toBeInTheDocument();
|
|
162
|
+
expect(
|
|
163
|
+
screen.queryByRole('button', { name: /Sign all pending contracts/ }),
|
|
164
|
+
).not.toBeInTheDocument();
|
|
165
|
+
});
|
|
166
|
+
});
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import classNames from 'classnames';
|
|
1
2
|
import { Organization, CourseProductRelation } from 'types/Joanie';
|
|
2
3
|
import useTeacherContractsToSign from 'pages/TeacherDashboardContractsLayout/hooks/useTeacherContractsToSign';
|
|
4
|
+
import useHasContractToDownload from 'pages/TeacherDashboardContractsLayout/hooks/useHasContractToDownload';
|
|
3
5
|
import SignOrganizationContractButton from '../SignOrganizationContractButton';
|
|
6
|
+
import BulkDownloadContractButton from '../BulkDownloadContractButton';
|
|
4
7
|
|
|
5
8
|
interface ContractActionsProps {
|
|
6
9
|
organizationId: Organization['id'];
|
|
@@ -12,15 +15,27 @@ const ContractActionsBar = ({ organizationId, courseProductRelationId }: Contrac
|
|
|
12
15
|
organizationId,
|
|
13
16
|
courseProductRelationId,
|
|
14
17
|
});
|
|
18
|
+
const hasContractToDownload = useHasContractToDownload(organizationId);
|
|
19
|
+
|
|
20
|
+
const nbAvailableActions = [canSignContracts, hasContractToDownload].filter((val) => val).length;
|
|
15
21
|
return (
|
|
16
|
-
|
|
17
|
-
<div
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
nbAvailableActions > 0 && (
|
|
23
|
+
<div
|
|
24
|
+
className={classNames('dashboard__page__actions-row', {
|
|
25
|
+
'dashboard__page__actions-row--space-between': nbAvailableActions > 1,
|
|
26
|
+
'dashboard__page__actions-row--end': nbAvailableActions === 1,
|
|
27
|
+
})}
|
|
28
|
+
data-testid="teacher-contracts-list-actionsBar"
|
|
29
|
+
>
|
|
30
|
+
{canSignContracts && (
|
|
31
|
+
<div>
|
|
32
|
+
<SignOrganizationContractButton
|
|
33
|
+
organizationId={organizationId}
|
|
34
|
+
contractToSignCount={contractsToSignCount}
|
|
35
|
+
/>
|
|
36
|
+
</div>
|
|
37
|
+
)}
|
|
38
|
+
{hasContractToDownload && <BulkDownloadContractButton organizationId={organizationId} />}
|
|
24
39
|
</div>
|
|
25
40
|
)
|
|
26
41
|
);
|
|
@@ -0,0 +1,74 @@
|
|
|
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 { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
|
|
7
|
+
import JoanieApiProvider from 'contexts/JoanieApiContext';
|
|
8
|
+
import { createTestQueryClient } from 'utils/test/createTestQueryClient';
|
|
9
|
+
|
|
10
|
+
import SignOrganizationContractButton from '.';
|
|
11
|
+
|
|
12
|
+
jest.mock('utils/context', () => ({
|
|
13
|
+
__esModule: true,
|
|
14
|
+
default: mockRichieContextFactory({
|
|
15
|
+
authentication: { backend: 'fonzie', endpoint: 'https://auth.test' },
|
|
16
|
+
joanie_backend: { endpoint: 'https://joanie.test' },
|
|
17
|
+
}).one(),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
describe('TeacherDashboardContractsLayout/SignOrganizationContractButton', () => {
|
|
21
|
+
const Wrapper = ({ children }: PropsWithChildren) => {
|
|
22
|
+
return (
|
|
23
|
+
<IntlProvider locale="en">
|
|
24
|
+
<QueryClientProvider client={createTestQueryClient({ user: true })}>
|
|
25
|
+
<JoanieApiProvider>{children}</JoanieApiProvider>
|
|
26
|
+
</QueryClientProvider>
|
|
27
|
+
</IntlProvider>
|
|
28
|
+
);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
beforeAll(() => {
|
|
32
|
+
const modalExclude = document.createElement('div');
|
|
33
|
+
modalExclude.setAttribute('id', 'modal-exclude');
|
|
34
|
+
document.body.appendChild(modalExclude);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
jest.resetAllMocks();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("shouldn't render sign button and <OrganizationContractFrame/> when contractToSignCount > 0", () => {
|
|
42
|
+
render(
|
|
43
|
+
<Wrapper>
|
|
44
|
+
<SignOrganizationContractButton
|
|
45
|
+
organizationId={faker.string.uuid()}
|
|
46
|
+
contractToSignCount={1}
|
|
47
|
+
/>
|
|
48
|
+
</Wrapper>,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
expect(screen.getByRole('button', { name: /Sign all pending contracts/ })).toBeInTheDocument();
|
|
52
|
+
|
|
53
|
+
const DashboardContractFramePortal = document.getElementsByClassName('ReactModalPortal');
|
|
54
|
+
expect(DashboardContractFramePortal).toHaveLength(1);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("shouldn't only render <OrganizationContractFrame/> when contractToSignCount is 0", () => {
|
|
58
|
+
render(
|
|
59
|
+
<Wrapper>
|
|
60
|
+
<SignOrganizationContractButton
|
|
61
|
+
organizationId={faker.string.uuid()}
|
|
62
|
+
contractToSignCount={0}
|
|
63
|
+
/>
|
|
64
|
+
</Wrapper>,
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
expect(
|
|
68
|
+
screen.queryByRole('button', { name: /Sign all pending contracts/ }),
|
|
69
|
+
).not.toBeInTheDocument();
|
|
70
|
+
|
|
71
|
+
const DashboardContractFramePortal = document.getElementsByClassName('ReactModalPortal');
|
|
72
|
+
expect(DashboardContractFramePortal).toHaveLength(1);
|
|
73
|
+
});
|
|
74
|
+
});
|
package/js/pages/TeacherDashboardContractsLayout/hooks/useCheckContractArchiveExists/index.spec.tsx
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { faker } from '@faker-js/faker';
|
|
2
|
+
import fetchMock from 'fetch-mock';
|
|
3
|
+
import { act, renderHook, waitFor } 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 {
|
|
9
|
+
storeContractArchiveId,
|
|
10
|
+
unstoreContractArchiveId,
|
|
11
|
+
} from '../useDownloadContractArchive/contractArchiveLocalStorage';
|
|
12
|
+
import useCheckContractArchiveExists from '.';
|
|
13
|
+
|
|
14
|
+
jest.mock('utils/context', () => ({
|
|
15
|
+
__esModule: true,
|
|
16
|
+
default: mockRichieContextFactory({
|
|
17
|
+
authentication: { backend: 'fonzie', endpoint: 'https://auth.test' },
|
|
18
|
+
joanie_backend: { endpoint: 'https://joanie.test' },
|
|
19
|
+
}).one(),
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
jest.mock('settings', () => ({
|
|
23
|
+
...jest.requireActual('settings'),
|
|
24
|
+
CONTRACT_DOWNLOAD_SETTINGS: {
|
|
25
|
+
...jest.requireActual('settings').CONTRACT_DOWNLOAD_SETTINGS,
|
|
26
|
+
pollInterval: 100,
|
|
27
|
+
},
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
const mockCheckArchive = jest.fn();
|
|
31
|
+
jest.mock('hooks/useContractArchive', () => ({
|
|
32
|
+
__esModule: true,
|
|
33
|
+
default: () => ({
|
|
34
|
+
methods: { get: jest.fn(), create: jest.fn(), check: mockCheckArchive },
|
|
35
|
+
}),
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
describe('useCheckContractArchiveExists', () => {
|
|
39
|
+
const Wrapper = ({ children }: PropsWithChildren) => {
|
|
40
|
+
return (
|
|
41
|
+
<IntlProvider locale="en">
|
|
42
|
+
<JoanieApiProvider>{children}</JoanieApiProvider>
|
|
43
|
+
</IntlProvider>
|
|
44
|
+
);
|
|
45
|
+
};
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
// Joanie providers calls
|
|
48
|
+
fetchMock.get('https://joanie.test/api/v1.0/orders/', []);
|
|
49
|
+
fetchMock.get('https://joanie.test/api/v1.0/credit-cards/', []);
|
|
50
|
+
fetchMock.get('https://joanie.test/api/v1.0/addresses/', []);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
afterEach(() => {
|
|
54
|
+
jest.resetAllMocks();
|
|
55
|
+
fetchMock.restore();
|
|
56
|
+
unstoreContractArchiveId();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should do nothing and return default value when no contractArchiveId is stored', () => {
|
|
60
|
+
const { result } = renderHook(useCheckContractArchiveExists, {
|
|
61
|
+
wrapper: Wrapper,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
expect(result.current.isContractArchiveExists).toBe(false);
|
|
65
|
+
expect(mockCheckArchive).not.toHaveBeenCalled();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should check if archive exist when a id is stored', async () => {
|
|
69
|
+
storeContractArchiveId(faker.string.uuid());
|
|
70
|
+
mockCheckArchive.mockResolvedValue(true);
|
|
71
|
+
|
|
72
|
+
const { result } = renderHook(useCheckContractArchiveExists, {
|
|
73
|
+
wrapper: Wrapper,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
expect(result.current.isContractArchiveExists).toBeNull();
|
|
77
|
+
await waitFor(() => {
|
|
78
|
+
expect(mockCheckArchive).toHaveBeenCalledTimes(1);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
expect(result.current.isPolling).toBe(false);
|
|
82
|
+
expect(result.current.isContractArchiveExists).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should do nothing when enable is false', () => {
|
|
86
|
+
storeContractArchiveId(faker.string.uuid());
|
|
87
|
+
mockCheckArchive.mockResolvedValue(true);
|
|
88
|
+
|
|
89
|
+
const { result } = renderHook(() => useCheckContractArchiveExists({ enable: false }), {
|
|
90
|
+
wrapper: Wrapper,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
expect(result.current.isContractArchiveExists).toBe(false);
|
|
94
|
+
expect(mockCheckArchive).not.toHaveBeenCalled();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should trigger polling when checkArchiveExist is call', async () => {
|
|
98
|
+
const { result, rerender } = renderHook(useCheckContractArchiveExists, {
|
|
99
|
+
wrapper: Wrapper,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
mockCheckArchive.mockResolvedValue(false);
|
|
103
|
+
act(() => {
|
|
104
|
+
result.current.checkArchiveExists(faker.string.uuid());
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
await waitFor(() => {
|
|
108
|
+
expect(mockCheckArchive).toHaveBeenCalledTimes(1);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
expect(result.current.isContractArchiveExists).toBe(false);
|
|
112
|
+
|
|
113
|
+
// isPolling it need's a rerender to be updated
|
|
114
|
+
rerender();
|
|
115
|
+
expect(result.current.isPolling).toBe(true);
|
|
116
|
+
|
|
117
|
+
mockCheckArchive.mockResolvedValue(true);
|
|
118
|
+
await waitFor(() => {
|
|
119
|
+
expect(mockCheckArchive).toHaveBeenCalledTimes(2);
|
|
120
|
+
});
|
|
121
|
+
expect(result.current.isPolling).toBe(false);
|
|
122
|
+
expect(result.current.isContractArchiveExists).toBe(true);
|
|
123
|
+
});
|
|
124
|
+
});
|
package/js/pages/TeacherDashboardContractsLayout/hooks/useCheckContractArchiveExists/index.tsx
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import useContractArchive from 'hooks/useContractArchive';
|
|
3
|
+
import { CONTRACT_DOWNLOAD_SETTINGS } from 'settings';
|
|
4
|
+
import { Nullable } from 'types/utils';
|
|
5
|
+
import { getStoredContractArchiveId } from '../useDownloadContractArchive/contractArchiveLocalStorage';
|
|
6
|
+
|
|
7
|
+
export interface UseCheckContractArchiveExistsProps {
|
|
8
|
+
enable: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const useCheckContractArchiveExist = (
|
|
12
|
+
{ enable }: UseCheckContractArchiveExistsProps = { enable: true },
|
|
13
|
+
) => {
|
|
14
|
+
// Contract's archive api interface
|
|
15
|
+
const {
|
|
16
|
+
methods: { check: checkArchiveExist },
|
|
17
|
+
} = useContractArchive();
|
|
18
|
+
|
|
19
|
+
// Store if the contract's archive exists or not on the server
|
|
20
|
+
// stay null until fetched
|
|
21
|
+
const [isContractArchiveExists, setIsContractArchiveExists] = useState<Nullable<boolean>>(null);
|
|
22
|
+
|
|
23
|
+
const timeoutRef = useRef<NodeJS.Timeout>();
|
|
24
|
+
|
|
25
|
+
// This method will check if the archive exists on the server
|
|
26
|
+
// option.polling === true will recursivly poll archive existence
|
|
27
|
+
const checkArchiveExists = async (
|
|
28
|
+
archiveId: string,
|
|
29
|
+
options: { polling: boolean } = { polling: true },
|
|
30
|
+
) => {
|
|
31
|
+
clearTimeout(timeoutRef.current);
|
|
32
|
+
timeoutRef.current = undefined;
|
|
33
|
+
|
|
34
|
+
const isExists = await checkArchiveExist(archiveId);
|
|
35
|
+
setIsContractArchiveExists(isExists);
|
|
36
|
+
|
|
37
|
+
if (!options.polling) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!isExists) {
|
|
42
|
+
timeoutRef.current = setTimeout(
|
|
43
|
+
() => checkArchiveExists(archiveId),
|
|
44
|
+
CONTRACT_DOWNLOAD_SETTINGS.pollInterval,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// This effect will initialize isContractArchiveExists value
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
const storedContractArchiveId = getStoredContractArchiveId();
|
|
52
|
+
if (enable && storedContractArchiveId) {
|
|
53
|
+
checkArchiveExists(storedContractArchiveId, { polling: false });
|
|
54
|
+
} else {
|
|
55
|
+
setIsContractArchiveExists(false);
|
|
56
|
+
}
|
|
57
|
+
}, [enable]);
|
|
58
|
+
|
|
59
|
+
// Be sure to clear any timeout before unmouting the hook.
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
return () => {
|
|
62
|
+
clearTimeout(timeoutRef.current);
|
|
63
|
+
};
|
|
64
|
+
}, []);
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
isPolling: !!timeoutRef.current,
|
|
68
|
+
isContractArchiveExists,
|
|
69
|
+
checkArchiveExists,
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export default useCheckContractArchiveExist;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { faker } from '@faker-js/faker';
|
|
2
|
+
import { CONTRACT_DOWNLOAD_SETTINGS } from 'settings';
|
|
3
|
+
import {
|
|
4
|
+
getStoredContractArchiveId,
|
|
5
|
+
isStoredContractArchiveIdExpired,
|
|
6
|
+
storeContractArchiveId,
|
|
7
|
+
unstoreContractArchiveId,
|
|
8
|
+
} from './contractArchiveLocalStorage';
|
|
9
|
+
|
|
10
|
+
describe('contractArchiveLocalStorage', () => {
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
unstoreContractArchiveId();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('should store and unstore contractArchiveId and creation date in localStorage', () => {
|
|
16
|
+
expect(
|
|
17
|
+
localStorage.getItem(CONTRACT_DOWNLOAD_SETTINGS.contractArchiveLocalStorageKey),
|
|
18
|
+
).toBeNull();
|
|
19
|
+
|
|
20
|
+
const contractArchiveId = faker.string.uuid();
|
|
21
|
+
storeContractArchiveId(contractArchiveId);
|
|
22
|
+
expect(localStorage.getItem(CONTRACT_DOWNLOAD_SETTINGS.contractArchiveLocalStorageKey)).toMatch(
|
|
23
|
+
new RegExp(`[0-9]+::${contractArchiveId}`),
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
unstoreContractArchiveId();
|
|
27
|
+
expect(
|
|
28
|
+
localStorage.getItem(CONTRACT_DOWNLOAD_SETTINGS.contractArchiveLocalStorageKey),
|
|
29
|
+
).toBeNull();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should retrieve contractArchiveId from localStorage', () => {
|
|
33
|
+
const contractArchiveId = faker.string.uuid();
|
|
34
|
+
storeContractArchiveId(contractArchiveId);
|
|
35
|
+
|
|
36
|
+
const retrievedcontractArchiveId = getStoredContractArchiveId();
|
|
37
|
+
expect(retrievedcontractArchiveId).toBe(contractArchiveId);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it.each([
|
|
41
|
+
{
|
|
42
|
+
label: 'outdated creation date in the past',
|
|
43
|
+
now: Date.now(),
|
|
44
|
+
storageCreationTime:
|
|
45
|
+
Date.now() - CONTRACT_DOWNLOAD_SETTINGS.contractArchiveLocalVaklidityDurationMs * 2,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
label: 'outdated creation date in the future',
|
|
49
|
+
now: Date.now(),
|
|
50
|
+
storageCreationTime:
|
|
51
|
+
Date.now() + CONTRACT_DOWNLOAD_SETTINGS.contractArchiveLocalVaklidityDurationMs * 2,
|
|
52
|
+
},
|
|
53
|
+
])(
|
|
54
|
+
'isStoredContractArchiveIdExpired should be true for $label',
|
|
55
|
+
({ now, storageCreationTime }) => {
|
|
56
|
+
jest.useFakeTimers();
|
|
57
|
+
jest.setSystemTime(new Date(storageCreationTime));
|
|
58
|
+
const contractArchiveId = faker.string.uuid();
|
|
59
|
+
storeContractArchiveId(contractArchiveId);
|
|
60
|
+
|
|
61
|
+
jest.setSystemTime(new Date(now));
|
|
62
|
+
expect(isStoredContractArchiveIdExpired()).toBe(true);
|
|
63
|
+
|
|
64
|
+
jest.runOnlyPendingTimers();
|
|
65
|
+
jest.useRealTimers();
|
|
66
|
+
},
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
it("isStoredContractArchiveIdExpired should be true false storage isn't expired", () => {
|
|
70
|
+
const now = Date.now();
|
|
71
|
+
const validCreationTime =
|
|
72
|
+
now - CONTRACT_DOWNLOAD_SETTINGS.contractArchiveLocalVaklidityDurationMs / 2;
|
|
73
|
+
|
|
74
|
+
jest.useFakeTimers();
|
|
75
|
+
jest.setSystemTime(new Date(validCreationTime));
|
|
76
|
+
const contractArchiveId = faker.string.uuid();
|
|
77
|
+
storeContractArchiveId(contractArchiveId);
|
|
78
|
+
|
|
79
|
+
jest.setSystemTime(new Date(now));
|
|
80
|
+
expect(isStoredContractArchiveIdExpired()).toBe(false);
|
|
81
|
+
|
|
82
|
+
jest.runOnlyPendingTimers();
|
|
83
|
+
jest.useRealTimers();
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { CONTRACT_DOWNLOAD_SETTINGS } from 'settings';
|
|
2
|
+
|
|
3
|
+
const generateLocalStorageId = (contractArchiveId: string) => {
|
|
4
|
+
return `${Date.now()}::${contractArchiveId}`;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
const storeContractArchiveId = (contractArchiveId: string) => {
|
|
8
|
+
localStorage.setItem(
|
|
9
|
+
CONTRACT_DOWNLOAD_SETTINGS.contractArchiveLocalStorageKey,
|
|
10
|
+
generateLocalStorageId(contractArchiveId),
|
|
11
|
+
);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const unstoreContractArchiveId = () => {
|
|
15
|
+
localStorage.removeItem(CONTRACT_DOWNLOAD_SETTINGS.contractArchiveLocalStorageKey);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const getStoredContractArchiveId = () => {
|
|
19
|
+
const value = localStorage.getItem(CONTRACT_DOWNLOAD_SETTINGS.contractArchiveLocalStorageKey);
|
|
20
|
+
if (value === null) {
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const [, contractArchiveId] = value.split('::');
|
|
25
|
+
return contractArchiveId;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const isStoredContractArchiveIdExpired = () => {
|
|
29
|
+
const value = localStorage.getItem(CONTRACT_DOWNLOAD_SETTINGS.contractArchiveLocalStorageKey);
|
|
30
|
+
if (value === null) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
const [creationTimestamp] = value.split('::');
|
|
34
|
+
|
|
35
|
+
const bounds: number[] = [Date.now(), parseInt(creationTimestamp, 10)];
|
|
36
|
+
// reverse bounds when computer time change.
|
|
37
|
+
if (bounds[0] > bounds[1]) {
|
|
38
|
+
bounds.reverse();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const [begin, end] = bounds;
|
|
42
|
+
return end - begin > CONTRACT_DOWNLOAD_SETTINGS.contractArchiveLocalVaklidityDurationMs;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export {
|
|
46
|
+
storeContractArchiveId,
|
|
47
|
+
unstoreContractArchiveId,
|
|
48
|
+
getStoredContractArchiveId,
|
|
49
|
+
isStoredContractArchiveIdExpired,
|
|
50
|
+
};
|