Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🚧(frontend) migrate to react-query v4 & react 18 #1812

Merged
merged 4 commits into from
Nov 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ Versioning](https://semver.org/spec/v2.0.0.html).

- Fix courses badges css.

### Changed

- Migrate to React 18 and React-Query v4

## [2.17.0]

### Added
Expand Down
3 changes: 1 addition & 2 deletions docs/joanie-connection.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@ JOANIE = {

## Access Token
### Lifetime configuration
Access Token is stored within the SessionStorage through
[react-query client persistor](https://github.com/openfun/richie/blob/643d7bbdb7f9a02a86360607a7b37c587e70be1a/src/frontend/js/utils/react-query/createSessionStoragePersistor/index.ts).
Access Token is stored within the SessionStorage through [react-query client persister](https://tanstack.com/query/v4/docs/plugins/persistQueryClient).
By default, richie frontend considered access token as stale after 5 minutes. You can change this
value into [`settings.ts`](https://github.com/openfun/richie/blob/643d7bbdb7f9a02a86360607a7b37c587e70be1a/src/frontend/js/settings.ts)
by editing `REACT_QUERY_SETTINGS.staleTimes.session`.
Expand Down
11 changes: 0 additions & 11 deletions src/frontend/jest/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@
import '@testing-library/jest-dom/extend-expect';
NathanVss marked this conversation as resolved.
Show resolved Hide resolved
import { Request, Response } from 'node-fetch';

import { setLogger } from 'react-query';
import { noop } from 'utils';

// Mock Request & Reponse objects
global.Request = Request as any;
global.Response = Response as any;
Expand All @@ -25,11 +22,3 @@ RESET_MODULE_EXCEPTIONS.forEach((moduleName) => {
return mockActualRegistry[moduleName];
});
});

/* Prevent log error during tests */
setLogger({
// eslint-disable-next-line no-console
log: console.log,
warn: console.warn,
error: noop,
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { fireEvent, getByText, render, screen } from '@testing-library/react';
import { act } from '@testing-library/react-hooks';
import { act, fireEvent, getByText, render, screen } from '@testing-library/react';
import { IntlProvider } from 'react-intl';
import { Address } from 'types/Joanie';
import * as mockFactories from 'utils/test/factories';
Expand Down
55 changes: 17 additions & 38 deletions src/frontend/js/components/AddressesManagement/index.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
/**
* Test suite for AddressesManagement component
*/
import { fireEvent, render, screen } from '@testing-library/react';
import { act } from '@testing-library/react-hooks';
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
import fetchMock from 'fetch-mock';
import { IntlProvider } from 'react-intl';
import { QueryClientProvider } from 'react-query';
import { QueryClientProvider } from '@tanstack/react-query';
import * as mockFactories from 'utils/test/factories';
import { SessionProvider } from 'data/SessionProvider';
import { REACT_QUERY_SETTINGS, RICHIE_USER_TOKEN } from 'settings';
import type * as Joanie from 'types/Joanie';
import createQueryClient from 'utils/react-query/createQueryClient';
import { createTestQueryClient } from 'utils/test/createTestQueryClient';
import AddressesManagement from '.';

jest.mock('utils/context', () => ({
Expand All @@ -28,20 +26,6 @@ jest.mock('utils/indirection/window', () => ({
}));

describe('AddressesManagement', () => {
const initializeUser = () => {
const user = mockFactories.FonzieUserFactory.generate();

sessionStorage.setItem(
REACT_QUERY_SETTINGS.cacheStorage.key,
JSON.stringify(
mockFactories.PersistedClientFactory({
queries: [mockFactories.QueryStateFactory('user', { data: user })],
}),
),
);
sessionStorage.setItem(RICHIE_USER_TOKEN, user.access_token);
};

const handleClose = jest.fn();
const selectAddress = jest.fn();

Expand All @@ -53,16 +37,14 @@ describe('AddressesManagement', () => {
afterEach(() => {
jest.clearAllMocks();
fetchMock.restore();
sessionStorage.clear();
});

it('renders a go back button', async () => {
initializeUser();
fetchMock.get('https://joanie.endpoint/api/addresses/', []);

await act(async () => {
render(
<QueryClientProvider client={createQueryClient({ persistor: true })}>
<QueryClientProvider client={createTestQueryClient({ user: true })}>
<IntlProvider locale="en">
<SessionProvider>
<AddressesManagement handleClose={handleClose} selectAddress={selectAddress} />
Expand All @@ -82,15 +64,14 @@ describe('AddressesManagement', () => {
});

it("renders the user's addresses", async () => {
initializeUser();
const addresses = mockFactories.AddressFactory.generate(Math.ceil(Math.random() * 5));
fetchMock.get('https://joanie.endpoint/api/addresses/', addresses);

let container: HTMLElement;

await act(async () => {
({ container } = render(
<QueryClientProvider client={createQueryClient({ persistor: true })}>
<QueryClientProvider client={createTestQueryClient({ user: true })}>
<IntlProvider locale="en">
<SessionProvider>
<AddressesManagement handleClose={handleClose} selectAddress={selectAddress} />
Expand All @@ -100,9 +81,11 @@ describe('AddressesManagement', () => {
));
});

// All user's addresses should be displayed
const $addresses = container!.querySelectorAll('.registered-addresses-item');
expect($addresses).toHaveLength(addresses.length);
await waitFor(() => {
// All user's addresses should be displayed
const $addresses = container!.querySelectorAll('.registered-addresses-item');
return expect($addresses).toHaveLength(addresses.length);
});

addresses.forEach((address: Joanie.Address) => {
const $address = screen.getByTestId(`address-${address.id}-title`);
Expand All @@ -122,12 +105,11 @@ describe('AddressesManagement', () => {
});

it('renders a form to create an address', async () => {
initializeUser();
fetchMock.get('https://joanie.endpoint/api/addresses/', []);

await act(async () => {
render(
<QueryClientProvider client={createQueryClient({ persistor: true })}>
<QueryClientProvider client={createTestQueryClient({ user: true })}>
<IntlProvider locale="en">
<SessionProvider>
<AddressesManagement handleClose={handleClose} selectAddress={selectAddress} />
Expand Down Expand Up @@ -208,13 +190,12 @@ describe('AddressesManagement', () => {
});

it('renders a form to edit an address when user selects an address to edit', async () => {
initializeUser();
const address = mockFactories.AddressFactory.generate();
fetchMock.get('https://joanie.endpoint/api/addresses/', [address]);

await act(async () => {
render(
<QueryClientProvider client={createQueryClient({ persistor: true })}>
<QueryClientProvider client={createTestQueryClient({ user: true })}>
<IntlProvider locale="en">
<SessionProvider>
<AddressesManagement handleClose={handleClose} selectAddress={selectAddress} />
Expand All @@ -231,7 +212,7 @@ describe('AddressesManagement', () => {
screen.getByRole('button', { name: 'Use this address' });

// - Then user selects an address to edit
let $editButton = screen.getByRole('button', {
let $editButton = await screen.findByRole('button', {
name: `Edit "${address.title}" address`,
exact: true,
});
Expand Down Expand Up @@ -349,14 +330,13 @@ describe('AddressesManagement', () => {
});

it('allows user to delete an existing address', async () => {
initializeUser();
const address = mockFactories.AddressFactory.generate();
fetchMock.get('https://joanie.endpoint/api/addresses/', [address]);

let container: HTMLElement;
await act(async () => {
({ container } = render(
<QueryClientProvider client={createQueryClient({ persistor: true })}>
<QueryClientProvider client={createTestQueryClient({ user: true })}>
<IntlProvider locale="en">
<SessionProvider>
<AddressesManagement handleClose={handleClose} selectAddress={selectAddress} />
Expand All @@ -371,7 +351,7 @@ describe('AddressesManagement', () => {
.delete(`https://joanie.endpoint/api/addresses/${address.id}/`, {})
.get('https://joanie.endpoint/api/addresses/', [], { overwriteRoutes: true });

const $deleteButton = screen.getByRole('button', {
const $deleteButton = await screen.findByRole('button', {
name: `Delete "${address.title}" address`,
exact: true,
});
Expand All @@ -389,14 +369,13 @@ describe('AddressesManagement', () => {
});

it('allows user to promote an address as main', async () => {
initializeUser();
const [address1, address2] = mockFactories.AddressFactory.generate(2);
address1.is_main = true;
fetchMock.get('https://joanie.endpoint/api/addresses/', [address1, address2]);

await act(async () => {
render(
<QueryClientProvider client={createQueryClient({ persistor: true })}>
<QueryClientProvider client={createTestQueryClient({ user: true })}>
<IntlProvider locale="en">
<SessionProvider>
<AddressesManagement handleClose={handleClose} selectAddress={selectAddress} />
Expand Down Expand Up @@ -429,7 +408,7 @@ describe('AddressesManagement', () => {
},
);

const $promoteButton = screen.getByRole('button', {
const $promoteButton = await screen.findByRole('button', {
name: `Define "${address2.title}" address as main`,
exact: true,
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import faker from 'faker';
import { act, renderHook } from '@testing-library/react-hooks';
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import { act, renderHook } from '@testing-library/react';
import { Maybe } from 'types/utils';
import validationSchema from './validationSchema';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { act, fireEvent, render, screen } from '@testing-library/react';
import fetchMock from 'fetch-mock';
import type { PropsWithChildren } from 'react';
import { IntlProvider } from 'react-intl';
import { QueryClientProvider } from 'react-query';
import { QueryClientProvider } from '@tanstack/react-query';
import {
ContextFactory as mockContextFactory,
CourseFactory,
Expand All @@ -14,9 +14,9 @@ import {
import JoanieApiProvider from 'data/JoanieApiProvider';
import { CourseCodeProvider } from 'data/CourseCodeProvider';
import type { Course, CourseRun, Enrollment, OrderLite } from 'types/Joanie';
import createQueryClient from 'utils/react-query/createQueryClient';
import { Deferred } from 'utils/test/deferred';
import { Priority } from 'types';
import { createTestQueryClient } from 'utils/test/createTestQueryClient';
import { CourseRunList, EnrollableCourseRunList, EnrolledCourseRun } from '.';

jest.mock('utils/context', () => ({
Expand Down Expand Up @@ -109,7 +109,7 @@ describe('CourseProductCourseRuns', () => {
const Wrapper = ({ code, children }: PropsWithChildren<{ code: string }>) => (
<IntlProvider locale="en">
<CourseCodeProvider code={code}>
<QueryClientProvider client={createQueryClient()}>
<QueryClientProvider client={createTestQueryClient()}>
<JoanieApiProvider>{children}</JoanieApiProvider>
</QueryClientProvider>
</CourseCodeProvider>
Expand Down Expand Up @@ -299,6 +299,7 @@ describe('CourseProductCourseRuns', () => {
// it should be enabled already to allow early user feedback
expect($button.disabled).toBe(false);

fetchMock.post('https://joanie.test/api/enrollments/', []);
NathanVss marked this conversation as resolved.
Show resolved Hide resolved
await act(async () => {
// - Select the first course run
const $radio = screen.getByRole('radio', {
Expand All @@ -309,12 +310,11 @@ describe('CourseProductCourseRuns', () => {
fireEvent.click($radio);
fireEvent.click($button);
});

// - As the selected course run is not yet opened for enrollment,
// a message should inform user that he/she cannot enroll now.
// it should be focused so that screen reader users understand better
// the submit button should stay enabled to always allow user feedback on its actions
const error = screen.getByText(
const error = await screen.findByText(
`Enrollment will open on ${dateFormatter.format(new Date(courseRun.enrollment_start))}`,
);
expect(document.activeElement).toEqual(error);
Expand All @@ -326,7 +326,7 @@ describe('CourseProductCourseRuns', () => {
const Wrapper = ({ code, children }: PropsWithChildren<{ code: string }>) => (
<IntlProvider locale="en">
<CourseCodeProvider code={code}>
<QueryClientProvider client={createQueryClient()}>
<QueryClientProvider client={createTestQueryClient()}>
<JoanieApiProvider>{children}</JoanieApiProvider>
</QueryClientProvider>
</CourseCodeProvider>
Expand Down Expand Up @@ -397,7 +397,7 @@ describe('CourseProductCourseRuns', () => {
});

// - While request is pending, an accessible spinner should be displayed
screen.getByRole('status', { name: 'Unrolling...' });
await screen.findByRole('status', { name: 'Unrolling...' });

await act(async () => {
enrollmentDeferred.resolve(200);
Expand Down
6 changes: 3 additions & 3 deletions src/frontend/js/components/CourseProductItem/index.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { getByText, render, screen } from '@testing-library/react';
import fetchMock from 'fetch-mock';
import type { PropsWithChildren } from 'react';
import { IntlProvider } from 'react-intl';
import { QueryClientProvider } from 'react-query';
import { QueryClientProvider } from '@tanstack/react-query';
import {
CertificateProductFactory,
ContextFactory as mockContextFactory,
Expand All @@ -13,7 +13,7 @@ import {
import { CourseCodeProvider } from 'data/CourseCodeProvider';
import JoanieApiProvider from 'data/JoanieApiProvider';
import { CourseRun, Enrollment, OrderLite, Product } from 'types/Joanie';
import createQueryClient from 'utils/react-query/createQueryClient';
import { createTestQueryClient } from 'utils/test/createTestQueryClient';
import CourseProductItem from '.';

jest.mock('utils/context', () => ({
Expand Down Expand Up @@ -74,7 +74,7 @@ describe('CourseProductItem', () => {
<IntlProvider locale="en">
<CourseCodeProvider code={code}>
<JoanieApiProvider>
<QueryClientProvider client={createQueryClient()}>{children}</QueryClientProvider>
<QueryClientProvider client={createTestQueryClient()}>{children}</QueryClientProvider>
</JoanieApiProvider>
</CourseCodeProvider>
</IntlProvider>
Expand Down
12 changes: 7 additions & 5 deletions src/frontend/js/components/CourseProductsList/index.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { act, render, screen, waitFor } from '@testing-library/react';
import fetchMock from 'fetch-mock';
import type { PropsWithChildren } from 'react';
import { IntlProvider } from 'react-intl';
import { QueryClientProvider } from 'react-query';
import { QueryClientProvider } from '@tanstack/react-query';
import {
ContextFactory as mockContextFactory,
CourseFactory,
Expand All @@ -12,7 +12,7 @@ import { Deferred } from 'utils/test/deferred';
import type { Props as CourseProductItemProps } from 'components/CourseProductItem';
import JoanieApiProvider from 'data/JoanieApiProvider';
import type { Course, CourseProduct, OrderLite } from 'types/Joanie';
import createQueryClient from 'utils/react-query/createQueryClient';
import { createTestQueryClient } from 'utils/test/createTestQueryClient';
import CourseProductsList from '.';

jest.mock('utils/context', () => ({
Expand All @@ -36,7 +36,7 @@ jest.mock('components/CourseProductItem', () => ({
describe('CourseProductsList', () => {
const Wrapper = ({ children }: PropsWithChildren<{}>) => (
<IntlProvider locale="en">
<QueryClientProvider client={createQueryClient()}>
<QueryClientProvider client={createTestQueryClient()}>
<JoanieApiProvider>{children}</JoanieApiProvider>
</QueryClientProvider>
</IntlProvider>
Expand Down Expand Up @@ -89,7 +89,7 @@ describe('CourseProductsList', () => {
});

// - As course does not have products, it should render nothing.
expect(container.children).toHaveLength(0);
await waitFor(() => expect(container.children).toHaveLength(0));
});

it('renders one <CourseProductItem /> per course product', async () => {
Expand Down Expand Up @@ -123,7 +123,9 @@ describe('CourseProductsList', () => {
});

// - It should render one <CourseProductItem /> per product
expect(screen.queryAllByTestId('product-widget')).toHaveLength(course.products.length);
await waitFor(() =>
expect(screen.queryAllByTestId('product-widget')).toHaveLength(course.products.length),
);
// - It should also pass order information if user owns a product
expect(screen.queryAllByTestId('product-widget__price')).toHaveLength(1);
});
Expand Down