diff --git a/CHANGELOG.md b/CHANGELOG.md
index ddb7048f1b..cc6dc445d4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -53,6 +53,7 @@ Versioning](https://semver.org/spec/v2.0.0.html).
- Fix cookiecutter circleci gitlint configuration
- Opened Course Run show empty date as '...'
- Fix dashboard enrollment listing when they're linked to a product certificate.
+- Fix order cache issues
### Removed
diff --git a/src/frontend/js/hooks/useUnionResource/utils/fetchEntity.ts b/src/frontend/js/hooks/useUnionResource/utils/fetchEntity.ts
index ecb37d8cdb..52820717fb 100644
--- a/src/frontend/js/hooks/useUnionResource/utils/fetchEntity.ts
+++ b/src/frontend/js/hooks/useUnionResource/utils/fetchEntity.ts
@@ -53,7 +53,8 @@ export const fetchEntity = async <
// Here we need to mimic the behavior of staleTime, which does not seems to be implemented when using `getQueryData`.
if (
state &&
- state.dataUpdatedAt >= new Date().getTime() - REACT_QUERY_SETTINGS.staleTimes.sessionItems
+ state.dataUpdatedAt >= new Date().getTime() - REACT_QUERY_SETTINGS.staleTimes.sessionItems &&
+ !state.isInvalidated
) {
data = state.data;
}
diff --git a/src/frontend/js/pages/DashboardCourses/useOrdersEnrollments.tsx b/src/frontend/js/pages/DashboardCourses/useOrdersEnrollments.tsx
index e7f4602cc9..6c1ac7a9f9 100644
--- a/src/frontend/js/pages/DashboardCourses/useOrdersEnrollments.tsx
+++ b/src/frontend/js/pages/DashboardCourses/useOrdersEnrollments.tsx
@@ -55,7 +55,7 @@ export const useOrdersEnrollments = ({
{ was_created_by_order: boolean } & PaginatedResourceQuery
>({
queryAConfig: {
- queryKey: ['user', 'order'],
+ queryKey: ['user', 'orders'],
fn: api.user.orders.get,
filters: orderFilters,
},
diff --git a/src/frontend/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.spec.tsx b/src/frontend/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.spec.tsx
index 8c3bbdccab..91e2aab342 100644
--- a/src/frontend/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.spec.tsx
+++ b/src/frontend/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.spec.tsx
@@ -20,6 +20,8 @@ import { LearnerDashboardPaths } from 'widgets/Dashboard/utils/learnerRouteMessa
import { expectBannerError } from 'utils/test/expectBanner';
import { Deferred } from 'utils/test/deferred';
import { alert } from 'utils/indirection/window';
+import { expectNoSpinner, expectSpinner } from 'utils/test/expectSpinner';
+import { CONTRACT_SETTINGS } from 'settings';
jest.mock('utils/context', () => ({
__esModule: true,
@@ -633,6 +635,11 @@ describe(' Contract', () => {
{ results: [order], next: null, previous: null, count: null },
{ overwriteRoutes: true },
);
+ fetchMock.get(
+ 'https://joanie.endpoint/api/v1.0/orders/?page=1&page_size=50&product_type=credential',
+ { results: [order], next: null, previous: null, count: null },
+ { overwriteRoutes: true },
+ );
const submitDeferred = new Deferred();
fetchMock.post(
@@ -644,20 +651,27 @@ describe(' Contract', () => {
// RTL too. See https://github.com/testing-library/user-event/issues/833.
const user = userEvent.setup({ delay: null });
- render(Wrapper(LearnerDashboardPaths.ORDER.replace(':orderId', order.id)));
+ render(Wrapper(LearnerDashboardPaths.COURSES));
+
+ await expectNoSpinner('Loading orders and enrollments...');
+
expect(
await screen.findByRole('heading', { level: 5, name: product.title }),
).toBeInTheDocument();
- // The modal is not shown.
- expect(screen.queryByTestId('dashboard-contract-frame')).not.toBeInTheDocument();
+ // Make sure the sign button is shown.
+ const $signButton = screen.getByRole('link', { name: 'Sign' });
+ await user.click($signButton);
// Contract is shown and not in loading state.
- let contractElement = screen.getByTestId('dashboard-item-order-contract');
+ let contractElement = await screen.findByTestId('dashboard-item-order-contract');
expect(within(contractElement).queryByRole('status')).not.toBeInTheDocument();
let signButton = screen.getByRole('button', { name: 'Sign' });
expect(signButton).not.toHaveAttribute('disabled');
+ // The modal is not shown.
+ expect(screen.queryByTestId('dashboard-contract-frame')).not.toBeInTheDocument();
+
await user.click(signButton);
// Modal is opened.
@@ -704,7 +718,12 @@ describe(' Contract', () => {
// Polling starts and succeeds after the second call.
await act(async () => {
- jest.runOnlyPendingTimers();
+ // We prefer advanceTimersByTime over runOnlyPendingTimers, because the latter would trigger internal
+ // react-query garbage collection, which is not what we want as we want to make sure the cache is well
+ // handled by fetchEntity ( useUnionResources ) by verifying that isInvalidated is true. ( Otherwise we would
+ // have got a undefined getQueryState(...) result. That's why we test that the "Sign" button from the
+ // courses view is well removed.
+ jest.advanceTimersByTime(CONTRACT_SETTINGS.pollInterval + 50);
});
await within(modal).findByRole('heading', { name: 'Verifying signature ...' });
within(modal).queryByRole('status');
@@ -728,7 +747,12 @@ describe(' Contract', () => {
// Fast-forward the second polling request.
await act(async () => {
- jest.runOnlyPendingTimers();
+ // We prefer advanceTimersByTime over runOnlyPendingTimers, because the latter would trigger internal
+ // react-query garbage collection, which is not what we want as we want to make sure the cache is well
+ // handled by fetchEntity ( useUnionResources ) by verifying that isInvalidated is true. ( Otherwise we would
+ // have got a undefined getQueryState(...) result. That's why we test that the "Sign" button from the
+ // courses view is well removed.
+ jest.advanceTimersByTime(CONTRACT_SETTINGS.pollInterval + 50);
});
// We update the orders mock in order to return a signed contract before resolving the polling.
@@ -766,16 +790,15 @@ describe(' Contract', () => {
expect(signButton).toHaveAttribute('disabled');
// Resolve the refresh order request.
+ const signedOrder = {
+ ...order,
+ contract: {
+ ...order.contract,
+ signed_on: new Date().toISOString(),
+ },
+ };
signedOrderDeferred.resolve({
- results: [
- {
- ...order,
- contract: {
- ...order.contract,
- signed_on: new Date().toISOString(),
- },
- },
- ],
+ results: [signedOrder],
next: null,
previous: null,
count: null,
@@ -785,6 +808,26 @@ describe(' Contract', () => {
await waitFor(() =>
expect(screen.queryByRole('button', { name: 'Sign' })).not.toBeInTheDocument(),
);
+
+ // Go back to the list view to make sure the sign button is not shown anymore.
+ fetchMock.get(
+ 'https://joanie.endpoint/api/v1.0/orders/?page=1&page_size=50&product_type=credential',
+ { results: [signedOrder], next: null, previous: null, count: null },
+ { overwriteRoutes: true },
+ );
+
+ const $backButton = screen.getByRole('link', { name: 'Back' });
+ await user.click($backButton);
+
+ await expectSpinner('Loading orders and enrollments...');
+ await expectNoSpinner('Loading orders and enrollments...');
+
+ expect(
+ await screen.findByRole('heading', { level: 5, name: product.title }),
+ ).toBeInTheDocument();
+
+ // Make sure the sign button is not shown.
+ expect(screen.queryByRole('link', { name: 'Sign' })).not.toBeInTheDocument();
});
it('downloads the contract', async () => {