Skip to content

Commit a1be315

Browse files
test: refactor editor test rendering
1 parent 6406f8f commit a1be315

File tree

5 files changed

+109
-118
lines changed

5 files changed

+109
-118
lines changed
Lines changed: 47 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,98 +1,59 @@
1-
import { render, waitFor, act } from '@testing-library/react';
2-
import { configureStore } from '@reduxjs/toolkit';
3-
import { MemoryRouter } from 'react-router-dom';
4-
import { AppProvider } from '@edx/frontend-platform/react';
5-
import { IntlProvider } from '@edx/frontend-platform/i18n';
6-
import { initializeMockApp } from '@edx/frontend-platform';
7-
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
1+
import { thunkActions } from '@src/editors/data/redux';
82
import VideoEditorModal from './VideoEditorModal';
9-
import { thunkActions } from '../../../data/redux';
3+
import { initializeMocks, waitFor, act } from '@src/testUtils';
4+
import editorRender, { getEditorStore, PartialEditorState } from '@src/editors/editorTestRender';
105

11-
jest.mock('../../../data/redux', () => ({
12-
...jest.requireActual('../../../data/redux'),
13-
thunkActions: {
14-
video: {
15-
loadVideoData: jest
16-
.fn()
17-
.mockImplementation(() => ({ type: 'MOCK_ACTION' })),
18-
},
19-
},
20-
}));
6+
thunkActions.video.loadVideoData = jest.fn().mockImplementation(() => ({ type: 'MOCK_ACTION' }));
217

22-
const queryClient = new QueryClient({
23-
defaultOptions: {
24-
queries: {
25-
retry: false,
8+
const initialState: PartialEditorState = {
9+
app: {
10+
videos: [],
11+
learningContextId: 'course-v1:test+test+test',
12+
blockId: 'some-block-id',
13+
courseDetails: {},
14+
},
15+
requests: {
16+
uploadAsset: { status: 'inactive', response: {} as any },
17+
uploadTranscript: { status: 'inactive', response: {} as any },
18+
deleteTranscript: { status: 'inactive', response: {} as any },
19+
fetchVideos: { status: 'inactive', response: {} as any },
20+
},
21+
video: {
22+
videoSource: '',
23+
videoId: '',
24+
fallbackVideos: ['', ''],
25+
allowVideoDownloads: false,
26+
allowVideoSharing: { level: 'block', value: false },
27+
thumbnail: null,
28+
transcripts: [],
29+
selectedVideoTranscriptUrls: {},
30+
allowTranscriptDownloads: false,
31+
duration: {
32+
startTime: '00:00:00',
33+
stopTime: '00:00:00',
34+
total: '00:00:00',
2635
},
2736
},
28-
});
37+
};
2938

3039
describe('VideoUploader', () => {
31-
let store;
32-
3340
beforeEach(async () => {
34-
store = configureStore({
35-
reducer: (state, action) => (action && action.newState ? action.newState : state),
36-
preloadedState: {
37-
app: {
38-
videos: [],
39-
learningContextId: 'course-v1:test+test+test',
40-
blockId: 'some-block-id',
41-
courseDetails: {},
42-
},
43-
requests: {
44-
uploadAsset: { status: 'inactive' },
45-
uploadTranscript: { status: 'inactive' },
46-
deleteTranscript: { status: 'inactive' },
47-
fetchVideos: { status: 'inactive' },
48-
},
49-
video: {
50-
videoSource: '',
51-
videoId: '',
52-
fallbackVideos: ['', ''],
53-
allowVideoDownloads: false,
54-
allowVideoSharing: { level: 'block', value: false },
55-
thumbnail: null,
56-
transcripts: [],
57-
transcriptHandlerUrl: '',
58-
selectedVideoTranscriptUrls: {},
59-
allowTranscriptDownloads: false,
60-
duration: {
61-
startTime: '00:00:00',
62-
stopTime: '00:00:00',
63-
total: '00:00:00',
64-
},
65-
},
66-
},
67-
});
68-
initializeMockApp({
69-
authenticatedUser: {
70-
userId: 3,
71-
username: 'test-user',
72-
administrator: true,
73-
roles: [],
74-
},
75-
});
41+
initializeMocks();
7642
});
7743

78-
const renderComponent = async () => render(
79-
<AppProvider store={store} wrapWithRouter={false}>
80-
<IntlProvider locale="en">
81-
<QueryClientProvider client={queryClient}>
82-
<MemoryRouter
83-
initialEntries={[
84-
'/some/path?selectedVideoId=id_1&selectedVideoUrl=https://video.com',
85-
]}
86-
>
87-
<VideoEditorModal isLibrary={false} />
88-
</MemoryRouter>
89-
</QueryClientProvider>
90-
</IntlProvider>
91-
</AppProvider>,
44+
const renderComponent = () => editorRender(
45+
<VideoEditorModal isLibrary={false} />,
46+
{
47+
path: '/some/path',
48+
routerProps: {
49+
initialEntries: ['/some/path?selectedVideoId=id_1&selectedVideoUrl=https://video.com'],
50+
},
51+
initialState,
52+
}
9253
);
9354

9455
it('should render the component and call loadVideoData with correct parameters', async () => {
95-
await renderComponent();
56+
renderComponent();
9657
await waitFor(() => {
9758
expect(thunkActions.video.loadVideoData).toHaveBeenCalledWith(
9859
'id_1',
@@ -102,10 +63,11 @@ describe('VideoUploader', () => {
10263
});
10364

10465
it('should call loadVideoData again when isLoaded state changes', async () => {
105-
await renderComponent();
66+
renderComponent();
10667
await waitFor(() => {
107-
expect(thunkActions.video.loadVideoData).toHaveBeenCalledTimes(2);
68+
expect(thunkActions.video.loadVideoData).toHaveBeenCalledTimes(1);
10869
});
70+
const store = getEditorStore();
10971

11072
act(() => {
11173
store.dispatch({
@@ -121,7 +83,7 @@ describe('VideoUploader', () => {
12183
});
12284

12385
await waitFor(() => {
124-
expect(thunkActions.video.loadVideoData).toHaveBeenCalledTimes(3);
86+
expect(thunkActions.video.loadVideoData).toHaveBeenCalledTimes(2);
12587
});
12688
});
12789
});

src/editors/data/redux/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ const rootReducer = (state: any, action: any) => {
2424
if (action.type === 'resetEditor') {
2525
return editorReducer(undefined, action);
2626
}
27+
// For test purposes only:
28+
if (action.type === 'UPDATE_STATE') {
29+
return action.newState;
30+
}
2731

2832
return editorReducer(state, action);
2933
};

src/editors/editorTestRender.jsx

Lines changed: 0 additions & 28 deletions
This file was deleted.

src/editors/editorTestRender.tsx

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import React from 'react';
2+
import { Provider } from 'react-redux';
3+
import { type Store } from 'redux';
4+
import { render as baseRender, type RouteOptions } from '../testUtils';
5+
import { EditorContextProvider } from './EditorContext';
6+
import { createStore } from './data/store';
7+
import { EditorState } from './data/redux';
8+
9+
/**
10+
* Partial<EditorState> only allows top-level keys to be missing. This is an
11+
* even more partial state that allows sub-keys to be missing.
12+
*/
13+
export type PartialEditorState = {
14+
[P in keyof EditorState]?: Partial<EditorState[P]> | undefined;
15+
};
16+
17+
interface Options {
18+
learningContextId?: string;
19+
initialState?: PartialEditorState;
20+
}
21+
22+
let editorStore: Store<EditorState>;
23+
24+
/**
25+
* Custom render function for testing React components with the editor context and Redux store.
26+
*
27+
* Wraps the provided UI in both the EditorContextProvider and Redux Provider,
28+
* ensuring that components under test have access to the necessary context and store.
29+
*
30+
* @param {React.ReactElement} ui - The React element to render.
31+
* @param options - Options
32+
* @returns {RenderResult} The result of the render, as returned by RTL render.
33+
*/
34+
export const editorRender = (ui, {
35+
learningContextId = 'course-v1:Org+COURSE+RUN',
36+
initialState = undefined,
37+
...routerOptions
38+
}: Options & RouteOptions = {}) => {
39+
editorStore = createStore(initialState);
40+
return baseRender(ui, {
41+
extraWrapper: ({ children }) => (
42+
<EditorContextProvider learningContextId={learningContextId}>
43+
<Provider store={editorStore}>
44+
{children}
45+
</Provider>
46+
</EditorContextProvider>
47+
),
48+
...routerOptions,
49+
});
50+
};
51+
52+
export function getEditorStore() {
53+
return editorStore;
54+
}
55+
56+
export default editorRender;

src/testUtils.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -155,17 +155,14 @@ const defaultUser = {
155155
*
156156
* Returns the new `axiosMock` in case you need to mock out axios requests.
157157
*/
158-
export function initializeMocks({ user = defaultUser, initialState = undefined, customReduxStoreCreator = null }: {
158+
export function initializeMocks({ user = defaultUser, initialState = undefined }: {
159159
user?: { userId: number, username: string },
160160
initialState?: Record<string, any>, // TODO: proper typing for our redux state
161-
customReduxStoreCreator?: ((preLoadedState: Record<string, any>) => Store) | null,
162161
} = {}) {
163162
initializeMockApp({
164163
authenticatedUser: user,
165164
});
166-
// Use a custom redux store creator if provided
167-
// e.g. use a different store like `createStore` of src/editors/data/store.ts which changes the redux state shape
168-
reduxStore = customReduxStoreCreator?.(initialState as any) ?? initializeReduxStore(initialState as any);
165+
reduxStore = initializeReduxStore(initialState as any);
169166
queryClient = new QueryClient({
170167
defaultOptions: {
171168
queries: {

0 commit comments

Comments
 (0)