refactor: Improve LibraryContext, convert tests to testUtils (#1345)
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
import { useParams } from 'react-router';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import type { MessageDescriptor } from 'react-intl';
|
||||
import {
|
||||
@@ -8,6 +7,7 @@ import { Add } from '@openedx/paragon/icons';
|
||||
import { ClearFiltersButton } from '../search-manager';
|
||||
import messages from './messages';
|
||||
import { useContentLibrary } from './data/apiHooks';
|
||||
import { useLibraryContext } from './common/context';
|
||||
|
||||
export const NoComponents = ({
|
||||
infoText = messages.noComponents,
|
||||
@@ -18,7 +18,7 @@ export const NoComponents = ({
|
||||
addBtnText?: MessageDescriptor;
|
||||
handleBtnClick: () => void;
|
||||
}) => {
|
||||
const { libraryId } = useParams();
|
||||
const { libraryId } = useLibraryContext();
|
||||
const { data: libraryData } = useContentLibrary(libraryId);
|
||||
const canEditLibrary = libraryData?.canEditLibrary ?? false;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import classNames from 'classnames';
|
||||
import { StudioFooter } from '@edx/frontend-component-footer';
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
} from '@openedx/paragon';
|
||||
import { Add, InfoOutline } from '@openedx/paragon/icons';
|
||||
import {
|
||||
Routes, Route, useLocation, useNavigate, useParams, useSearchParams,
|
||||
Routes, Route, useLocation, useNavigate, useSearchParams,
|
||||
} from 'react-router-dom';
|
||||
|
||||
import Loading from '../generic/Loading';
|
||||
@@ -33,7 +33,7 @@ import LibraryCollections from './collections/LibraryCollections';
|
||||
import LibraryHome from './LibraryHome';
|
||||
import { useContentLibrary } from './data/apiHooks';
|
||||
import { LibrarySidebar } from './library-sidebar';
|
||||
import { LibraryContext, SidebarBodyComponentId } from './common/context';
|
||||
import { SidebarBodyComponentId, useLibraryContext } from './common/context';
|
||||
import messages from './messages';
|
||||
|
||||
enum TabList {
|
||||
@@ -53,7 +53,7 @@ const HeaderActions = ({ canEditLibrary }: HeaderActionsProps) => {
|
||||
openInfoSidebar,
|
||||
closeLibrarySidebar,
|
||||
sidebarBodyComponent,
|
||||
} = useContext(LibraryContext);
|
||||
} = useLibraryContext();
|
||||
|
||||
if (!canEditLibrary) {
|
||||
return null;
|
||||
@@ -119,11 +119,7 @@ const LibraryAuthoringPage = () => {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { libraryId } = useParams();
|
||||
if (!libraryId) {
|
||||
// istanbul ignore next - This shouldn't be possible; it's just here to satisfy the type checker.
|
||||
throw new Error('Rendered without libraryId URL parameter');
|
||||
}
|
||||
const { libraryId } = useLibraryContext();
|
||||
const { data: libraryData, isLoading } = useContentLibrary(libraryId);
|
||||
|
||||
const currentPath = location.pathname.split('/').pop();
|
||||
@@ -131,7 +127,7 @@ const LibraryAuthoringPage = () => {
|
||||
const {
|
||||
sidebarBodyComponent,
|
||||
openInfoSidebar,
|
||||
} = useContext(LibraryContext);
|
||||
} = useLibraryContext();
|
||||
|
||||
useEffect(() => {
|
||||
openInfoSidebar();
|
||||
@@ -199,16 +195,12 @@ const LibraryAuthoringPage = () => {
|
||||
<Route
|
||||
path={TabList.home}
|
||||
element={(
|
||||
<LibraryHome
|
||||
libraryId={libraryId}
|
||||
tabList={TabList}
|
||||
handleTabChange={handleTabChange}
|
||||
/>
|
||||
<LibraryHome tabList={TabList} handleTabChange={handleTabChange} />
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
path={TabList.components}
|
||||
element={<LibraryComponents libraryId={libraryId} variant="full" />}
|
||||
element={<LibraryComponents variant="full" />}
|
||||
/>
|
||||
<Route
|
||||
path={TabList.collections}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { Stack } from '@openedx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
@@ -9,22 +8,21 @@ import { LibraryComponents } from './components';
|
||||
import LibrarySection from './components/LibrarySection';
|
||||
import LibraryRecentlyModified from './LibraryRecentlyModified';
|
||||
import messages from './messages';
|
||||
import { LibraryContext } from './common/context';
|
||||
import { useLibraryContext } from './common/context';
|
||||
|
||||
type LibraryHomeProps = {
|
||||
libraryId: string,
|
||||
tabList: { home: string, components: string, collections: string },
|
||||
handleTabChange: (key: string) => void,
|
||||
};
|
||||
|
||||
const LibraryHome = ({ libraryId, tabList, handleTabChange } : LibraryHomeProps) => {
|
||||
const LibraryHome = ({ tabList, handleTabChange } : LibraryHomeProps) => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
totalHits: componentCount,
|
||||
totalCollectionHits: collectionCount,
|
||||
isFiltered,
|
||||
} = useSearchContext();
|
||||
const { openAddContentSidebar } = useContext(LibraryContext);
|
||||
const { openAddContentSidebar } = useLibraryContext();
|
||||
|
||||
const renderEmptyState = () => {
|
||||
if (componentCount === 0 && collectionCount === 0) {
|
||||
@@ -35,7 +33,7 @@ const LibraryHome = ({ libraryId, tabList, handleTabChange } : LibraryHomeProps)
|
||||
|
||||
return (
|
||||
<Stack gap={3}>
|
||||
<LibraryRecentlyModified libraryId={libraryId} />
|
||||
<LibraryRecentlyModified />
|
||||
{
|
||||
renderEmptyState()
|
||||
|| (
|
||||
@@ -52,7 +50,7 @@ const LibraryHome = ({ libraryId, tabList, handleTabChange } : LibraryHomeProps)
|
||||
contentCount={componentCount}
|
||||
viewAllAction={() => handleTabChange(tabList.components)}
|
||||
>
|
||||
<LibraryComponents libraryId={libraryId} variant="preview" />
|
||||
<LibraryComponents variant="preview" />
|
||||
</LibrarySection>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -50,7 +50,7 @@ const LibraryLayout = () => {
|
||||
}, [goBack]);
|
||||
|
||||
return (
|
||||
<LibraryProvider>
|
||||
<LibraryProvider libraryId={libraryId}>
|
||||
<Routes>
|
||||
{/*
|
||||
TODO: we should be opening this editor as a modal, not making it a separate page/URL.
|
||||
|
||||
@@ -9,8 +9,9 @@ import messages from './messages';
|
||||
import ComponentCard from './components/ComponentCard';
|
||||
import { useLibraryBlockTypes } from './data/apiHooks';
|
||||
import CollectionCard from './components/CollectionCard';
|
||||
import { useLibraryContext } from './common/context';
|
||||
|
||||
const RecentlyModified: React.FC<{ libraryId: string }> = ({ libraryId }) => {
|
||||
const RecentlyModified: React.FC<Record<never, never>> = () => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
hits,
|
||||
@@ -18,6 +19,7 @@ const RecentlyModified: React.FC<{ libraryId: string }> = ({ libraryId }) => {
|
||||
totalHits,
|
||||
totalCollectionHits,
|
||||
} = useSearchContext();
|
||||
const { libraryId } = useLibraryContext();
|
||||
|
||||
const componentCount = totalHits + totalCollectionHits;
|
||||
// Since we only display a fixed number of items in preview,
|
||||
@@ -68,13 +70,16 @@ const RecentlyModified: React.FC<{ libraryId: string }> = ({ libraryId }) => {
|
||||
: null;
|
||||
};
|
||||
|
||||
const LibraryRecentlyModified: React.FC<{ libraryId: string }> = ({ libraryId }) => (
|
||||
<SearchContextProvider
|
||||
extraFilter={`context_key = "${libraryId}"`}
|
||||
overrideSearchSortOrder={SearchSortOption.RECENTLY_MODIFIED}
|
||||
>
|
||||
<RecentlyModified libraryId={libraryId} />
|
||||
</SearchContextProvider>
|
||||
);
|
||||
const LibraryRecentlyModified: React.FC<Record<never, never>> = () => {
|
||||
const { libraryId } = useLibraryContext();
|
||||
return (
|
||||
<SearchContextProvider
|
||||
extraFilter={`context_key = "${libraryId}"`}
|
||||
overrideSearchSortOrder={SearchSortOption.RECENTLY_MODIFIED}
|
||||
>
|
||||
<RecentlyModified />
|
||||
</SearchContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default LibraryRecentlyModified;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
fireEvent,
|
||||
render,
|
||||
render as baseRender,
|
||||
screen,
|
||||
waitFor,
|
||||
initializeMocks,
|
||||
@@ -8,18 +8,23 @@ import {
|
||||
import { mockContentLibrary } from '../data/api.mocks';
|
||||
import { getCreateLibraryBlockUrl, getLibraryPasteClipboardUrl } from '../data/api';
|
||||
import { mockBroadcastChannel, mockClipboardEmpty, mockClipboardHtml } from '../../generic/data/api.mock';
|
||||
import { LibraryProvider } from '../common/context';
|
||||
import AddContentContainer from './AddContentContainer';
|
||||
|
||||
mockBroadcastChannel();
|
||||
|
||||
const { libraryId } = mockContentLibrary;
|
||||
const renderOpts = { path: '/library/:libraryId/*', params: { libraryId } };
|
||||
const render = () => baseRender(<AddContentContainer />, {
|
||||
path: '/library/:libraryId/*',
|
||||
params: { libraryId },
|
||||
extraWrapper: ({ children }) => <LibraryProvider libraryId={libraryId}>{ children }</LibraryProvider>,
|
||||
});
|
||||
|
||||
describe('<AddContentContainer />', () => {
|
||||
it('should render content buttons', () => {
|
||||
initializeMocks();
|
||||
mockClipboardEmpty.applyMock();
|
||||
render(<AddContentContainer />);
|
||||
render();
|
||||
expect(screen.getByRole('button', { name: /collection/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /text/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /problem/i })).toBeInTheDocument();
|
||||
@@ -36,7 +41,7 @@ describe('<AddContentContainer />', () => {
|
||||
const url = getCreateLibraryBlockUrl(libraryId);
|
||||
axiosMock.onPost(url).reply(200);
|
||||
|
||||
render(<AddContentContainer />, renderOpts);
|
||||
render();
|
||||
|
||||
const textButton = screen.getByRole('button', { name: /text/i });
|
||||
fireEvent.click(textButton);
|
||||
@@ -48,9 +53,9 @@ describe('<AddContentContainer />', () => {
|
||||
initializeMocks();
|
||||
// Simulate having an HTML block in the clipboard:
|
||||
const getClipboardSpy = mockClipboardHtml.applyMock();
|
||||
const doc = render(<AddContentContainer />, renderOpts);
|
||||
render();
|
||||
expect(getClipboardSpy).toHaveBeenCalled(); // Hmm, this is getting called three times! Refactor to use react-query.
|
||||
await waitFor(() => expect(doc.queryByRole('button', { name: /paste from clipboard/i })).toBeInTheDocument());
|
||||
await waitFor(() => expect(screen.queryByRole('button', { name: /paste from clipboard/i })).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('should paste content', async () => {
|
||||
@@ -61,7 +66,7 @@ describe('<AddContentContainer />', () => {
|
||||
const pasteUrl = getLibraryPasteClipboardUrl(libraryId);
|
||||
axiosMock.onPost(pasteUrl).reply(200);
|
||||
|
||||
render(<AddContentContainer />, renderOpts);
|
||||
render();
|
||||
|
||||
expect(getClipboardSpy).toHaveBeenCalled(); // Hmm, this is getting called four times! Refactor to use react-query.
|
||||
|
||||
@@ -79,7 +84,7 @@ describe('<AddContentContainer />', () => {
|
||||
const pasteUrl = getLibraryPasteClipboardUrl(libraryId);
|
||||
axiosMock.onPost(pasteUrl).reply(400);
|
||||
|
||||
render(<AddContentContainer />, renderOpts);
|
||||
render();
|
||||
|
||||
const pasteButton = await screen.findByRole('button', { name: /paste from clipboard/i });
|
||||
fireEvent.click(pasteButton);
|
||||
|
||||
@@ -23,9 +23,9 @@ import { useCopyToClipboard } from '../../generic/clipboard';
|
||||
import { getCanEdit } from '../../course-unit/data/selectors';
|
||||
import { useCreateLibraryBlock, useLibraryPasteClipboard, useUpdateCollectionComponents } from '../data/apiHooks';
|
||||
import { getEditUrl } from '../components/utils';
|
||||
import { useLibraryContext } from '../common/context';
|
||||
|
||||
import messages from './messages';
|
||||
import { LibraryContext } from '../common/context';
|
||||
|
||||
type ContentType = {
|
||||
name: string,
|
||||
@@ -73,7 +73,7 @@ const AddContentContainer = () => {
|
||||
const { showPasteXBlock } = useCopyToClipboard(canEdit);
|
||||
const {
|
||||
openCreateCollectionModal,
|
||||
} = React.useContext(LibraryContext);
|
||||
} = useLibraryContext();
|
||||
|
||||
const collectionButtonData = {
|
||||
name: intl.formatMessage(messages.collectionButton),
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { useContext } from 'react';
|
||||
import { Stack } from '@openedx/paragon';
|
||||
import { NoComponents, NoSearchResults } from '../EmptyStates';
|
||||
import { useSearchContext } from '../../search-manager';
|
||||
import { LibraryComponents } from '../components';
|
||||
import messages from './messages';
|
||||
import { LibraryContext } from '../common/context';
|
||||
import { useLibraryContext } from '../common/context';
|
||||
|
||||
const LibraryCollectionComponents = ({ libraryId }: { libraryId: string }) => {
|
||||
const LibraryCollectionComponents = () => {
|
||||
const { totalHits: componentCount, isFiltered } = useSearchContext();
|
||||
const { openAddContentSidebar } = useContext(LibraryContext);
|
||||
const { openAddContentSidebar } = useLibraryContext();
|
||||
|
||||
if (componentCount === 0) {
|
||||
return isFiltered
|
||||
@@ -25,7 +24,7 @@ const LibraryCollectionComponents = ({ libraryId }: { libraryId: string }) => {
|
||||
return (
|
||||
<Stack direction="vertical" gap={3}>
|
||||
<h3 className="text-gray">Content ({componentCount})</h3>
|
||||
<LibraryComponents libraryId={libraryId} variant="full" />
|
||||
<LibraryComponents variant="full" />
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useContext, useEffect } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { StudioFooter } from '@edx/frontend-component-footer';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
SearchSortWidget,
|
||||
} from '../../search-manager';
|
||||
import { useCollection, useContentLibrary } from '../data/apiHooks';
|
||||
import { LibraryContext } from '../common/context';
|
||||
import { useLibraryContext } from '../common/context';
|
||||
import messages from './messages';
|
||||
import { LibrarySidebar } from '../library-sidebar';
|
||||
import LibraryCollectionComponents from './LibraryCollectionComponents';
|
||||
@@ -36,7 +36,7 @@ const HeaderActions = ({ canEditLibrary }: { canEditLibrary: boolean; }) => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
openAddContentSidebar,
|
||||
} = useContext(LibraryContext);
|
||||
} = useLibraryContext();
|
||||
|
||||
if (!canEditLibrary) {
|
||||
return null;
|
||||
@@ -104,7 +104,7 @@ const LibraryCollectionPage = () => {
|
||||
const {
|
||||
sidebarBodyComponent,
|
||||
openCollectionInfoSidebar,
|
||||
} = useContext(LibraryContext);
|
||||
} = useLibraryContext();
|
||||
|
||||
const {
|
||||
data: collectionData,
|
||||
@@ -189,7 +189,7 @@ const LibraryCollectionPage = () => {
|
||||
<div className="flex-grow-1" />
|
||||
<SearchSortWidget />
|
||||
</div>
|
||||
<LibraryCollectionComponents libraryId={libraryId} />
|
||||
<LibraryCollectionComponents />
|
||||
</SearchContextProvider>
|
||||
</Container>
|
||||
<StudioFooter />
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { useLoadOnScroll } from '../../hooks';
|
||||
import { useSearchContext } from '../../search-manager';
|
||||
import { NoComponents, NoSearchResults } from '../EmptyStates';
|
||||
import CollectionCard from '../components/CollectionCard';
|
||||
import { LIBRARY_SECTION_PREVIEW_LIMIT } from '../components/LibrarySection';
|
||||
import messages from './messages';
|
||||
import { LibraryContext } from '../common/context';
|
||||
import { useLibraryContext } from '../common/context';
|
||||
|
||||
type LibraryCollectionsProps = {
|
||||
variant: 'full' | 'preview',
|
||||
@@ -29,7 +27,7 @@ const LibraryCollections = ({ variant }: LibraryCollectionsProps) => {
|
||||
isFiltered,
|
||||
} = useSearchContext();
|
||||
|
||||
const { openCreateCollectionModal } = useContext(LibraryContext);
|
||||
const { openCreateCollectionModal } = useLibraryContext();
|
||||
|
||||
const collectionList = variant === 'preview' ? collectionHits.slice(0, LIBRARY_SECTION_PREVIEW_LIMIT) : collectionHits;
|
||||
|
||||
|
||||
@@ -9,36 +9,38 @@ export enum SidebarBodyComponentId {
|
||||
}
|
||||
|
||||
export interface LibraryContextData {
|
||||
/** The ID of the current library */
|
||||
libraryId: string;
|
||||
// Sidebar stuff - only one sidebar is active at any given time:
|
||||
sidebarBodyComponent: SidebarBodyComponentId | null;
|
||||
closeLibrarySidebar: () => void;
|
||||
openAddContentSidebar: () => void;
|
||||
openInfoSidebar: () => void;
|
||||
openComponentInfoSidebar: (usageKey: string) => void;
|
||||
currentComponentUsageKey?: string;
|
||||
// "Create New Collection" modal
|
||||
isCreateCollectionModalOpen: boolean;
|
||||
openCreateCollectionModal: () => void;
|
||||
closeCreateCollectionModal: () => void;
|
||||
// Current collection
|
||||
openCollectionInfoSidebar: (collectionId: string) => void;
|
||||
currentCollectionId?: string;
|
||||
}
|
||||
|
||||
export const LibraryContext = React.createContext({
|
||||
sidebarBodyComponent: null,
|
||||
closeLibrarySidebar: () => {},
|
||||
openAddContentSidebar: () => {},
|
||||
openInfoSidebar: () => {},
|
||||
openComponentInfoSidebar: (_usageKey: string) => {}, // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
isCreateCollectionModalOpen: false,
|
||||
openCreateCollectionModal: () => {},
|
||||
closeCreateCollectionModal: () => {},
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
openCollectionInfoSidebar: (_collectionId: string) => {},
|
||||
} as LibraryContextData);
|
||||
/**
|
||||
* Library Context.
|
||||
* Always available when we're in the context of a single library.
|
||||
*
|
||||
* Get this using `useLibraryContext()`
|
||||
*
|
||||
* Not used on the "library list" on Studio home.
|
||||
*/
|
||||
const LibraryContext = React.createContext<LibraryContextData | undefined>(undefined);
|
||||
|
||||
/**
|
||||
* React component to provide `LibraryContext`
|
||||
*/
|
||||
export const LibraryProvider = (props: { children?: React.ReactNode }) => {
|
||||
export const LibraryProvider = (props: { children?: React.ReactNode, libraryId: string }) => {
|
||||
const [sidebarBodyComponent, setSidebarBodyComponent] = React.useState<SidebarBodyComponentId | null>(null);
|
||||
const [currentComponentUsageKey, setCurrentComponentUsageKey] = React.useState<string>();
|
||||
const [currentCollectionId, setcurrentCollectionId] = React.useState<string>();
|
||||
@@ -76,7 +78,8 @@ export const LibraryProvider = (props: { children?: React.ReactNode }) => {
|
||||
setSidebarBodyComponent(SidebarBodyComponentId.CollectionInfo);
|
||||
}, []);
|
||||
|
||||
const context = React.useMemo(() => ({
|
||||
const context = React.useMemo<LibraryContextData>(() => ({
|
||||
libraryId: props.libraryId,
|
||||
sidebarBodyComponent,
|
||||
closeLibrarySidebar,
|
||||
openAddContentSidebar,
|
||||
@@ -89,6 +92,7 @@ export const LibraryProvider = (props: { children?: React.ReactNode }) => {
|
||||
openCollectionInfoSidebar,
|
||||
currentCollectionId,
|
||||
}), [
|
||||
props.libraryId,
|
||||
sidebarBodyComponent,
|
||||
closeLibrarySidebar,
|
||||
openAddContentSidebar,
|
||||
@@ -108,3 +112,12 @@ export const LibraryProvider = (props: { children?: React.ReactNode }) => {
|
||||
</LibraryContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export function useLibraryContext(): LibraryContextData {
|
||||
const ctx = React.useContext(LibraryContext);
|
||||
if (ctx === undefined) {
|
||||
/* istanbul ignore next */
|
||||
throw new Error('useLibraryContext() was used in a component without a <LibraryProvider> ancestor.');
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
initializeMocks,
|
||||
fireEvent,
|
||||
render,
|
||||
render as baseRender,
|
||||
screen,
|
||||
} from '../../testUtils';
|
||||
|
||||
import { LibraryProvider } from '../common/context';
|
||||
import { type CollectionHit } from '../../search-manager';
|
||||
import CollectionCard from './CollectionCard';
|
||||
|
||||
@@ -28,6 +30,10 @@ const CollectionHitSample: CollectionHit = {
|
||||
tags: {},
|
||||
};
|
||||
|
||||
const render = (ui: React.ReactElement) => baseRender(ui, {
|
||||
extraWrapper: ({ children }) => <LibraryProvider libraryId="lib:Axim:TEST">{ children }</LibraryProvider>,
|
||||
});
|
||||
|
||||
describe('<CollectionCard />', () => {
|
||||
beforeEach(() => {
|
||||
initializeMocks();
|
||||
|
||||
@@ -6,11 +6,10 @@ import {
|
||||
IconButton,
|
||||
} from '@openedx/paragon';
|
||||
import { MoreVert } from '@openedx/paragon/icons';
|
||||
import { useContext } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { type CollectionHit } from '../../search-manager';
|
||||
import { LibraryContext } from '../common/context';
|
||||
import { useLibraryContext } from '../common/context';
|
||||
import BaseComponentCard from './BaseComponentCard';
|
||||
import messages from './messages';
|
||||
|
||||
@@ -48,7 +47,7 @@ const CollectionCard = ({ collectionHit }: CollectionCardProps) => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
openCollectionInfoSidebar,
|
||||
} = useContext(LibraryContext);
|
||||
} = useLibraryContext();
|
||||
|
||||
const {
|
||||
type,
|
||||
|
||||
@@ -1,27 +1,21 @@
|
||||
import React from 'react';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { render, fireEvent, waitFor } from '@testing-library/react';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import type { Store } from 'redux';
|
||||
|
||||
import { ToastProvider } from '../../generic/toast-context';
|
||||
import {
|
||||
fireEvent,
|
||||
render as baseRender,
|
||||
screen,
|
||||
waitFor,
|
||||
initializeMocks,
|
||||
} from '../../testUtils';
|
||||
import { LibraryProvider } from '../common/context';
|
||||
import { getClipboardUrl } from '../../generic/data/api';
|
||||
import { ContentHit } from '../../search-manager';
|
||||
import initializeStore from '../../store';
|
||||
import ComponentCard from './ComponentCard';
|
||||
|
||||
let store: Store;
|
||||
let axiosMock: MockAdapter;
|
||||
|
||||
const contentHit: ContentHit = {
|
||||
id: '1',
|
||||
usageKey: 'lb:org1:demolib:html:a1fa8bdd-dc67-4976-9bf5-0ea75a9bca3d',
|
||||
type: 'library_block',
|
||||
blockId: 'a1fa8bdd-dc67-4976-9bf5-0ea75a9bca3d',
|
||||
contextKey: 'lb:org1:Demo_Course',
|
||||
contextKey: 'lib:org1:Demo_Course',
|
||||
org: 'org1',
|
||||
breadcrumbs: [{ displayName: 'Demo Lib' }],
|
||||
displayName: 'Text Display Name',
|
||||
@@ -47,57 +41,32 @@ const clipboardBroadcastChannelMock = {
|
||||
|
||||
(global as any).BroadcastChannel = jest.fn(() => clipboardBroadcastChannelMock);
|
||||
|
||||
const RootWrapper = () => (
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en">
|
||||
<ToastProvider>
|
||||
<ComponentCard
|
||||
contentHit={contentHit}
|
||||
blockTypeDisplayName="text"
|
||||
/>
|
||||
</ToastProvider>
|
||||
</IntlProvider>
|
||||
</AppProvider>
|
||||
);
|
||||
const libraryId = 'lib:org1:Demo_Course';
|
||||
const render = () => baseRender(<ComponentCard contentHit={contentHit} blockTypeDisplayName="text" />, {
|
||||
extraWrapper: ({ children }) => <LibraryProvider libraryId={libraryId}>{ children }</LibraryProvider>,
|
||||
});
|
||||
|
||||
describe('<ComponentCard />', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
store = initializeStore();
|
||||
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
axiosMock.restore();
|
||||
});
|
||||
|
||||
it('should render the card with title and description', () => {
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
initializeMocks();
|
||||
render();
|
||||
|
||||
expect(getByText('Text Display Formated Name')).toBeInTheDocument();
|
||||
expect(getByText('This is a text: ID=1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Text Display Formated Name')).toBeInTheDocument();
|
||||
expect(screen.getByText('This is a text: ID=1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should call the updateClipboard function when the copy button is clicked', async () => {
|
||||
const { axiosMock, mockShowToast } = initializeMocks();
|
||||
axiosMock.onPost(getClipboardUrl()).reply(200, {});
|
||||
const { getByRole, getByTestId, getByText } = render(<RootWrapper />);
|
||||
render();
|
||||
|
||||
// Open menu
|
||||
expect(getByTestId('component-card-menu-toggle')).toBeInTheDocument();
|
||||
fireEvent.click(getByTestId('component-card-menu-toggle'));
|
||||
expect(screen.getByTestId('component-card-menu-toggle')).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByTestId('component-card-menu-toggle'));
|
||||
|
||||
// Click copy to clipboard
|
||||
expect(getByRole('button', { name: 'Copy to clipboard' })).toBeInTheDocument();
|
||||
fireEvent.click(getByRole('button', { name: 'Copy to clipboard' }));
|
||||
expect(screen.getByRole('button', { name: 'Copy to clipboard' })).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Copy to clipboard' }));
|
||||
|
||||
expect(axiosMock.history.post.length).toBe(1);
|
||||
expect(axiosMock.history.post[0].data).toBe(
|
||||
@@ -105,21 +74,22 @@ describe('<ComponentCard />', () => {
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText('Component copied to clipboard')).toBeInTheDocument();
|
||||
expect(mockShowToast).toHaveBeenCalledWith('Component copied to clipboard');
|
||||
});
|
||||
});
|
||||
|
||||
it('should show error message if the api call fails', async () => {
|
||||
const { axiosMock, mockShowToast } = initializeMocks();
|
||||
axiosMock.onPost(getClipboardUrl()).reply(400);
|
||||
const { getByRole, getByTestId, getByText } = render(<RootWrapper />);
|
||||
render();
|
||||
|
||||
// Open menu
|
||||
expect(getByTestId('component-card-menu-toggle')).toBeInTheDocument();
|
||||
fireEvent.click(getByTestId('component-card-menu-toggle'));
|
||||
expect(screen.getByTestId('component-card-menu-toggle')).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByTestId('component-card-menu-toggle'));
|
||||
|
||||
// Click copy to clipboard
|
||||
expect(getByRole('button', { name: 'Copy to clipboard' })).toBeInTheDocument();
|
||||
fireEvent.click(getByRole('button', { name: 'Copy to clipboard' }));
|
||||
expect(screen.getByRole('button', { name: 'Copy to clipboard' })).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Copy to clipboard' }));
|
||||
|
||||
expect(axiosMock.history.post.length).toBe(1);
|
||||
expect(axiosMock.history.post[0].data).toBe(
|
||||
@@ -127,7 +97,7 @@ describe('<ComponentCard />', () => {
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText('Failed to copy component to clipboard')).toBeInTheDocument();
|
||||
expect(mockShowToast).toHaveBeenCalledWith('Failed to copy component to clipboard');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@ import { Link } from 'react-router-dom';
|
||||
import { updateClipboard } from '../../generic/data/api';
|
||||
import { ToastContext } from '../../generic/toast-context';
|
||||
import { type ContentHit } from '../../search-manager';
|
||||
import { LibraryContext } from '../common/context';
|
||||
import { useLibraryContext } from '../common/context';
|
||||
import messages from './messages';
|
||||
import { STUDIO_CLIPBOARD_CHANNEL } from '../../constants';
|
||||
import { getEditUrl } from './utils';
|
||||
@@ -66,7 +66,7 @@ export const ComponentMenu = ({ usageKey }: { usageKey: string }) => {
|
||||
const ComponentCard = ({ contentHit, blockTypeDisplayName } : ComponentCardProps) => {
|
||||
const {
|
||||
openComponentInfoSidebar,
|
||||
} = useContext(LibraryContext);
|
||||
} = useLibraryContext();
|
||||
|
||||
const {
|
||||
blockType,
|
||||
|
||||
@@ -1,26 +1,24 @@
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import fetchMock from 'fetch-mock-jest';
|
||||
import type { Store } from 'redux';
|
||||
|
||||
import {
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
initializeMocks,
|
||||
} from '../../testUtils';
|
||||
import { getContentSearchConfigUrl } from '../../search-manager/data/api';
|
||||
import { SearchContextProvider } from '../../search-manager/SearchManager';
|
||||
import { mockLibraryBlockTypes, mockContentLibrary } from '../data/api.mocks';
|
||||
import mockEmptyResult from '../../search-modal/__mocks__/empty-search-result.json';
|
||||
import initializeStore from '../../store';
|
||||
import { LibraryProvider } from '../common/context';
|
||||
import { libraryComponentsMock } from '../__mocks__';
|
||||
import LibraryComponents from './LibraryComponents';
|
||||
|
||||
const searchEndpoint = 'http://mock.meilisearch.local/multi-search';
|
||||
|
||||
const mockUseLibraryBlockTypes = jest.fn();
|
||||
mockLibraryBlockTypes.applyMock();
|
||||
mockContentLibrary.applyMock();
|
||||
const mockFetchNextPage = jest.fn();
|
||||
const mockUseSearchContext = jest.fn();
|
||||
const mockUseContentLibrary = jest.fn();
|
||||
|
||||
const data = {
|
||||
totalHits: 1,
|
||||
@@ -33,17 +31,6 @@ const data = {
|
||||
isFiltered: false,
|
||||
};
|
||||
|
||||
let store: Store;
|
||||
let axiosMock: MockAdapter;
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const returnEmptyResult = (_url: string, req) => {
|
||||
const requestData = JSON.parse(req.body?.toString() ?? '');
|
||||
const query = requestData?.queries[0]?.q ?? '';
|
||||
@@ -56,28 +43,6 @@ const returnEmptyResult = (_url: string, req) => {
|
||||
return mockEmptyResult;
|
||||
};
|
||||
|
||||
const blockTypeData = {
|
||||
data: [
|
||||
{
|
||||
blockType: 'html',
|
||||
displayName: 'Text',
|
||||
},
|
||||
{
|
||||
blockType: 'video',
|
||||
displayName: 'Video',
|
||||
},
|
||||
{
|
||||
blockType: 'problem',
|
||||
displayName: 'Problem',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
jest.mock('../data/apiHooks', () => ({
|
||||
useLibraryBlockTypes: () => mockUseLibraryBlockTypes(),
|
||||
useContentLibrary: () => mockUseContentLibrary(),
|
||||
}));
|
||||
|
||||
jest.mock('../../search-manager', () => ({
|
||||
...jest.requireActual('../../search-manager'),
|
||||
useSearchContext: () => mockUseSearchContext(),
|
||||
@@ -90,36 +55,19 @@ const clipboardBroadcastChannelMock = {
|
||||
|
||||
(global as any).BroadcastChannel = jest.fn(() => clipboardBroadcastChannelMock);
|
||||
|
||||
const RootWrapper = (props) => (
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en" messages={{}}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<SearchContextProvider>
|
||||
<LibraryComponents libraryId="1" {...props} />
|
||||
</SearchContextProvider>
|
||||
</QueryClientProvider>
|
||||
</IntlProvider>
|
||||
</AppProvider>
|
||||
);
|
||||
const withLibraryId = (libraryId: string) => ({
|
||||
extraWrapper: ({ children }: { children: React.ReactNode }) => (
|
||||
<LibraryProvider libraryId={libraryId}>{children}</LibraryProvider>
|
||||
),
|
||||
});
|
||||
|
||||
describe('<LibraryComponents />', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
store = initializeStore();
|
||||
mockUseLibraryBlockTypes.mockReturnValue(blockTypeData);
|
||||
mockUseSearchContext.mockReturnValue(data);
|
||||
const { axiosMock } = initializeMocks();
|
||||
|
||||
fetchMock.post(searchEndpoint, returnEmptyResult, { overwriteRoutes: true });
|
||||
|
||||
// The API method to get the Meilisearch connection details uses Axios:
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
axiosMock.onGet(getContentSearchConfigUrl()).reply(200, {
|
||||
url: 'http://mock.meilisearch.local',
|
||||
index_name: 'studio',
|
||||
@@ -128,7 +76,8 @@ describe('<LibraryComponents />', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
fetchMock.reset();
|
||||
mockFetchNextPage.mockReset();
|
||||
});
|
||||
|
||||
it('should render empty state', async () => {
|
||||
@@ -136,15 +85,10 @@ describe('<LibraryComponents />', () => {
|
||||
...data,
|
||||
totalHits: 0,
|
||||
});
|
||||
mockUseContentLibrary.mockReturnValue({
|
||||
data: {
|
||||
canEditLibrary: true,
|
||||
},
|
||||
});
|
||||
|
||||
render(<RootWrapper />);
|
||||
render(<LibraryComponents variant="full" />, withLibraryId(mockContentLibrary.libraryId));
|
||||
expect(await screen.findByText(/you have not added any content to this library yet\./i));
|
||||
expect(screen.getByRole('button', { name: /add component/i })).toBeInTheDocument();
|
||||
expect(await screen.findByRole('button', { name: /add component/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render empty state without add content button', async () => {
|
||||
@@ -152,13 +96,8 @@ describe('<LibraryComponents />', () => {
|
||||
...data,
|
||||
totalHits: 0,
|
||||
});
|
||||
mockUseContentLibrary.mockReturnValue({
|
||||
data: {
|
||||
canEditLibrary: false,
|
||||
},
|
||||
});
|
||||
|
||||
render(<RootWrapper />);
|
||||
render(<LibraryComponents variant="full" />, withLibraryId(mockContentLibrary.libraryIdReadOnly));
|
||||
expect(await screen.findByText(/you have not added any content to this library yet\./i));
|
||||
expect(screen.queryByRole('button', { name: /add component/i })).not.toBeInTheDocument();
|
||||
});
|
||||
@@ -169,7 +108,7 @@ describe('<LibraryComponents />', () => {
|
||||
hits: libraryComponentsMock,
|
||||
isFetching: false,
|
||||
});
|
||||
render(<RootWrapper variant="full" />);
|
||||
render(<LibraryComponents variant="full" />, withLibraryId(mockContentLibrary.libraryId));
|
||||
|
||||
expect(await screen.findByText('This is a text: ID=1')).toBeInTheDocument();
|
||||
expect(screen.getByText('This is a text: ID=2')).toBeInTheDocument();
|
||||
@@ -185,7 +124,7 @@ describe('<LibraryComponents />', () => {
|
||||
hits: libraryComponentsMock,
|
||||
isFetching: false,
|
||||
});
|
||||
render(<RootWrapper variant="preview" />);
|
||||
render(<LibraryComponents variant="preview" />, withLibraryId(mockContentLibrary.libraryId));
|
||||
|
||||
expect(await screen.findByText('This is a text: ID=1')).toBeInTheDocument();
|
||||
expect(screen.getByText('This is a text: ID=2')).toBeInTheDocument();
|
||||
@@ -203,7 +142,7 @@ describe('<LibraryComponents />', () => {
|
||||
hasNextPage: true,
|
||||
});
|
||||
|
||||
render(<RootWrapper variant="full" />);
|
||||
render(<LibraryComponents variant="full" />, withLibraryId(mockContentLibrary.libraryId));
|
||||
|
||||
Object.defineProperty(window, 'innerHeight', { value: 800 });
|
||||
Object.defineProperty(document.body, 'scrollHeight', { value: 1600 });
|
||||
@@ -221,7 +160,7 @@ describe('<LibraryComponents />', () => {
|
||||
hasNextPage: true,
|
||||
});
|
||||
|
||||
render(<RootWrapper variant="preview" />);
|
||||
render(<LibraryComponents variant="preview" />, withLibraryId(mockContentLibrary.libraryId));
|
||||
|
||||
Object.defineProperty(window, 'innerHeight', { value: 800 });
|
||||
Object.defineProperty(document.body, 'scrollHeight', { value: 1600 });
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useContext, useMemo } from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { useLoadOnScroll } from '../../hooks';
|
||||
import { useSearchContext } from '../../search-manager';
|
||||
@@ -6,10 +6,9 @@ import { NoComponents, NoSearchResults } from '../EmptyStates';
|
||||
import { useLibraryBlockTypes } from '../data/apiHooks';
|
||||
import ComponentCard from './ComponentCard';
|
||||
import { LIBRARY_SECTION_PREVIEW_LIMIT } from './LibrarySection';
|
||||
import { LibraryContext } from '../common/context';
|
||||
import { useLibraryContext } from '../common/context';
|
||||
|
||||
type LibraryComponentsProps = {
|
||||
libraryId: string,
|
||||
variant: 'full' | 'preview',
|
||||
};
|
||||
|
||||
@@ -20,7 +19,7 @@ type LibraryComponentsProps = {
|
||||
* - 'full': Show all components with Infinite scroll pagination.
|
||||
* - 'preview': Show first 4 components without pagination.
|
||||
*/
|
||||
const LibraryComponents = ({ libraryId, variant }: LibraryComponentsProps) => {
|
||||
const LibraryComponents = ({ variant }: LibraryComponentsProps) => {
|
||||
const {
|
||||
hits,
|
||||
totalHits: componentCount,
|
||||
@@ -29,11 +28,11 @@ const LibraryComponents = ({ libraryId, variant }: LibraryComponentsProps) => {
|
||||
fetchNextPage,
|
||||
isFiltered,
|
||||
} = useSearchContext();
|
||||
const { openAddContentSidebar } = useContext(LibraryContext);
|
||||
const { libraryId, openAddContentSidebar } = useLibraryContext();
|
||||
|
||||
const componentList = variant === 'preview' ? hits.slice(0, LIBRARY_SECTION_PREVIEW_LIMIT) : hits;
|
||||
|
||||
// TODO add this to LibraryContext
|
||||
// TODO get rid of "useLibraryBlockTypes". Use <BlockTypeLabel> instead.
|
||||
const { data: blockTypesData } = useLibraryBlockTypes(libraryId);
|
||||
const blockTypes = useMemo(() => {
|
||||
const result = {};
|
||||
|
||||
@@ -5,12 +5,12 @@ import {
|
||||
Form,
|
||||
ModalDialog,
|
||||
} from '@openedx/paragon';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Formik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import FormikControl from '../../generic/FormikControl';
|
||||
import { LibraryContext } from '../common/context';
|
||||
import { useLibraryContext } from '../common/context';
|
||||
import messages from './messages';
|
||||
import { useCreateLibraryCollection } from '../data/apiHooks';
|
||||
import { ToastContext } from '../../generic/toast-context';
|
||||
@@ -18,15 +18,12 @@ import { ToastContext } from '../../generic/toast-context';
|
||||
const CreateCollectionModal = () => {
|
||||
const intl = useIntl();
|
||||
const navigate = useNavigate();
|
||||
const { libraryId } = useParams();
|
||||
if (!libraryId) {
|
||||
throw new Error('Rendered without libraryId URL parameter');
|
||||
}
|
||||
const create = useCreateLibraryCollection(libraryId!);
|
||||
const {
|
||||
libraryId,
|
||||
isCreateCollectionModalOpen,
|
||||
closeCreateCollectionModal,
|
||||
} = React.useContext(LibraryContext);
|
||||
} = useLibraryContext();
|
||||
const create = useCreateLibraryCollection(libraryId);
|
||||
const { showToast } = React.useContext(ToastContext);
|
||||
|
||||
const handleCreate = React.useCallback((values) => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useContext } from 'react';
|
||||
import React from 'react';
|
||||
import {
|
||||
Stack,
|
||||
Icon,
|
||||
@@ -10,7 +10,7 @@ import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { AddContentContainer, AddContentHeader } from '../add-content';
|
||||
import { CollectionInfo, CollectionInfoHeader } from '../collections';
|
||||
import { ContentLibrary } from '../data/api';
|
||||
import { LibraryContext, SidebarBodyComponentId } from '../common/context';
|
||||
import { SidebarBodyComponentId, useLibraryContext } from '../common/context';
|
||||
import { ComponentInfo, ComponentInfoHeader } from '../component-info';
|
||||
import { LibraryInfo, LibraryInfoHeader } from '../library-info';
|
||||
import messages from '../messages';
|
||||
@@ -35,7 +35,7 @@ const LibrarySidebar = ({ library }: LibrarySidebarProps) => {
|
||||
closeLibrarySidebar,
|
||||
currentComponentUsageKey,
|
||||
currentCollectionId,
|
||||
} = useContext(LibraryContext);
|
||||
} = useLibraryContext();
|
||||
|
||||
const bodyComponentMap = {
|
||||
[SidebarBodyComponentId.AddContent]: <AddContentContainer />,
|
||||
|
||||
@@ -46,6 +46,10 @@ export interface RouteOptions {
|
||||
routerProps?: MemoryRouterProps;
|
||||
}
|
||||
|
||||
export interface WrapperOptions {
|
||||
extraWrapper?: React.FunctionComponent<{ children: React.ReactNode; }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* This component works together with the custom `render()` method we have in
|
||||
* this file to provide whatever react-router context you need for your
|
||||
@@ -111,14 +115,14 @@ const RouterAndRoute: React.FC<RouteOptions> = ({
|
||||
);
|
||||
};
|
||||
|
||||
function makeWrapper({ ...routeArgs }: RouteOptions) {
|
||||
function makeWrapper({ extraWrapper, ...routeArgs }: WrapperOptions & RouteOptions) {
|
||||
const AllTheProviders = ({ children }) => (
|
||||
<AppProvider store={reduxStore} wrapWithRouter={false}>
|
||||
<IntlProvider locale="en" messages={{}}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ToastContext.Provider value={mockToastContext}>
|
||||
<RouterAndRoute {...routeArgs}>
|
||||
{children}
|
||||
{extraWrapper ? React.createElement(extraWrapper, undefined, children) : children}
|
||||
</RouterAndRoute>
|
||||
</ToastContext.Provider>
|
||||
</QueryClientProvider>
|
||||
@@ -132,7 +136,7 @@ function makeWrapper({ ...routeArgs }: RouteOptions) {
|
||||
* Same as render() from `@testing-library/react` but this one provides all the
|
||||
* wrappers our React components need to render properly.
|
||||
*/
|
||||
function customRender(ui: React.ReactElement, options: RouteOptions = {}): RenderResult {
|
||||
function customRender(ui: React.ReactElement, options: WrapperOptions & RouteOptions = {}): RenderResult {
|
||||
return render(ui, { wrapper: makeWrapper(options) });
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user