Skip to content

Commit

Permalink
🐛(frontend) fix various cache order issues
Browse files Browse the repository at this point in the history
We had a bug where the cache invalidation triggered when a contract
is signed was not impacting useOrdersEnrollments. This was causing
the "Sign" button to be still displayed. A related bug where after
buying a product from the syllabus page, the newly created order
was not shown was also fixed by theses changes.

Fix #2157
  • Loading branch information
NathanVss committed Dec 6, 2023
1 parent c1c23c8 commit 51d6fa9
Show file tree
Hide file tree
Showing 4 changed files with 62 additions and 17 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 2 additions & 1 deletion src/frontend/js/hooks/useUnionResource/utils/fetchEntity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -633,6 +635,11 @@ describe('<DashboardItemOrder/> 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(
Expand All @@ -644,20 +651,27 @@ describe('<DashboardItemOrder/> 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.
Expand Down Expand Up @@ -704,7 +718,12 @@ describe('<DashboardItemOrder/> 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');
Expand All @@ -728,7 +747,12 @@ describe('<DashboardItemOrder/> 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.
Expand Down Expand Up @@ -766,16 +790,15 @@ describe('<DashboardItemOrder/> 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,
Expand All @@ -785,6 +808,26 @@ describe('<DashboardItemOrder/> 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 () => {
Expand Down

0 comments on commit 51d6fa9

Please sign in to comment.