Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { getConfig } from '@edx/frontend-platform';
import { selectors } from '../../../../../data/redux';
import messages from './messages';
import TinyMceWidget from '../../../../../sharedComponents/TinyMceWidget';
import { prepareEditorRef, replaceStaticWithAsset } from '../../../../../sharedComponents/TinyMceWidget/hooks';
import { prepareEditorRef, useProcessedEditorContent } from '../../../../../sharedComponents/TinyMceWidget/hooks';

const ExplanationWidget = ({
// redux
Expand All @@ -19,12 +19,11 @@ const ExplanationWidget = ({
}) => {
const intl = useIntl();
const { editorRef, refReady, setEditorRef } = prepareEditorRef();
const initialContent = settings?.solutionExplanation || '';
const newContent = replaceStaticWithAsset({
initialContent,

const solutionContent = useProcessedEditorContent({
initialContent: settings?.solutionExplanation || '',
learningContextId,
});
const solutionContent = newContent || initialContent;
let staticRootUrl;
if (isLibrary) {
staticRootUrl = `${getConfig().STUDIO_BASE_URL }/library_assets/blocks/${ blockId }/`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { getConfig } from '@edx/frontend-platform';
import { selectors } from '../../../../../data/redux';
import messages from './messages';
import TinyMceWidget from '../../../../../sharedComponents/TinyMceWidget';
import { prepareEditorRef, replaceStaticWithAsset } from '../../../../../sharedComponents/TinyMceWidget/hooks';
import { prepareEditorRef, useProcessedEditorContent } from '../../../../../sharedComponents/TinyMceWidget/hooks';

const QuestionWidget = ({
// redux
Expand All @@ -19,12 +19,11 @@ const QuestionWidget = ({
}) => {
const intl = useIntl();
const { editorRef, refReady, setEditorRef } = prepareEditorRef();
const initialContent = question;
const newContent = replaceStaticWithAsset({
initialContent,

const questionContent = useProcessedEditorContent({
initialContent: question,
learningContextId,
});
const questionContent = newContent || initialContent;
let staticRootUrl;
if (isLibrary) {
staticRootUrl = `${getConfig().STUDIO_BASE_URL }/library_assets/blocks/${ blockId }/`;
Expand Down
13 changes: 8 additions & 5 deletions src/editors/containers/TextEditor/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import RawEditor from '../../sharedComponents/RawEditor';
import * as hooks from './hooks';
import messages from './messages';
import TinyMceWidget from '../../sharedComponents/TinyMceWidget';
import { prepareEditorRef, replaceStaticWithAsset } from '../../sharedComponents/TinyMceWidget/hooks';
import { prepareEditorRef, useProcessedEditorContent } from '../../sharedComponents/TinyMceWidget/hooks';

const TextEditor = ({
onClose,
Expand All @@ -32,15 +32,16 @@ const TextEditor = ({
learningContextId,
images,
isLibrary,
validateAssetUrl,
}) => {
const intl = useIntl();
const { editorRef, refReady, setEditorRef } = prepareEditorRef();
const initialContent = blockValue ? blockValue.data.data : '';
const newContent = replaceStaticWithAsset({
initialContent,

const editorContent = useProcessedEditorContent({
initialContent: blockValue ? blockValue.data.data : '',
learningContextId,
validateAssetUrl,
});
const editorContent = newContent || initialContent;
let staticRootUrl;
if (isLibrary) {
staticRootUrl = `${getConfig().STUDIO_BASE_URL }/library_assets/blocks/${ blockId }/`;
Expand Down Expand Up @@ -106,6 +107,7 @@ TextEditor.defaultProps = {
blockValue: null,
blockFinished: null,
returnFunction: null,
validateAssetUrl: null,
};
TextEditor.propTypes = {
onClose: PropTypes.func.isRequired,
Expand All @@ -122,6 +124,7 @@ TextEditor.propTypes = {
learningContextId: PropTypes.string, // This should be required but is NULL when the store is in initial state :/
images: PropTypes.shape({}).isRequired,
isLibrary: PropTypes.bool.isRequired,
validateAssetUrl: PropTypes.bool,
};

export const mapStateToProps = (state) => ({
Expand Down
13 changes: 10 additions & 3 deletions src/editors/containers/TextEditor/index.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import React from 'react';
import { render, screen, initializeMocks } from '@src/testUtils';
import {
render, screen, initializeMocks, waitFor,
} from '@src/testUtils';
import { actions, selectors } from '../../data/redux';
import { RequestKeys } from '../../data/constants/requests';
import { TextEditorInternal as TextEditor, mapStateToProps, mapDispatchToProps } from '.';
Expand Down Expand Up @@ -67,15 +69,20 @@ describe('TextEditor', () => {
expect(element?.getAttribute('editorcontenthtml')).toBe('eDiTablE Text');
});

test('renders static images with relative paths', () => {
test('renders static images with relative paths', async () => {
const updatedProps = {
...props,
validateAssetUrl: false,
blockValue: { data: { data: 'eDiTablE Text with <img src="/static/img.jpg" />' } },
};
const { container } = render(<TextEditor {...updatedProps} />);
const element = container.querySelector('tinymcewidget');
expect(element).toBeInTheDocument();
expect(element?.getAttribute('editorcontenthtml')).toBe('eDiTablE Text with <img src="/asset+org+run+type@[email protected]" />');
await waitFor(() => {
expect(element?.getAttribute('editorcontenthtml')).toBe(
'eDiTablE Text with <img src="/asset+org+run+type@[email protected]" />',
);
});
});
test('not yet loaded, Spinner appears', () => {
const { container } = render(<TextEditor {...props} blockFinished={false} />);
Expand Down
109 changes: 99 additions & 10 deletions src/editors/sharedComponents/TinyMceWidget/hooks.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import 'CourseAuthoring/editors/setupEditorTest';
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import * as keyUtils from '../../../generic/key-utils';
import { MockUseState } from '../../testUtils';

import * as tinyMCE from '../../data/constants/tinyMCE';
Expand All @@ -19,6 +21,10 @@ jest.mock('react', () => ({
useCallback: (cb, prereqs) => ({ cb, prereqs }),
}));

jest.mock('@edx/frontend-platform/auth', () => ({
getAuthenticatedHttpClient: jest.fn(),
}));

const state = new MockUseState(module);
const moduleKeys = keyStore(module);

Expand Down Expand Up @@ -192,36 +198,119 @@ describe('TinyMceEditor hooks', () => {
const initialContent = `<img src="/static/soMEImagEURl1.jpeg"/><a href="/assets/v1/${baseAssetUrl}/test.pdf">test</a><img src="/${baseAssetUrl}@correct.png" /><img src="/${baseAssetUrl}/correct.png" />`;
const learningContextId = 'course-v1:org+test+run';
const lmsEndpointUrl = getConfig().LMS_BASE_URL;
it('returns updated src for text editor to update content', () => {

beforeEach(() => {
jest.clearAllMocks();
});

it('returns updated src for text editor to update content', async () => {
const expected = `<img src="/${baseAssetUrl}@soMEImagEURl1.jpeg"/><a href="/${baseAssetUrl}@test.pdf">test</a><img src="/${baseAssetUrl}@correct.png" /><img src="/${baseAssetUrl}@correct.png" />`;
const actual = module.replaceStaticWithAsset({ initialContent, learningContextId });
const actual = await module.replaceStaticWithAsset({
initialContent,
learningContextId,
validateAssetUrl: false,
});
expect(actual).toEqual(expected);
});
it('returns updated src with absolute url for expandable editor to update content', () => {
const editorType = 'expandable';
it('returns updated src with absolute url for expandable editor to update content', async () => {
const expected = `<img src="${lmsEndpointUrl}/${baseAssetUrl}@soMEImagEURl1.jpeg"/><a href="${lmsEndpointUrl}/${baseAssetUrl}@test.pdf">test</a><img src="${lmsEndpointUrl}/${baseAssetUrl}@correct.png" /><img src="${lmsEndpointUrl}/${baseAssetUrl}@correct.png" />`;
const actual = module.replaceStaticWithAsset({
const actual = await module.replaceStaticWithAsset({
initialContent,
editorType,
editorType: 'expandable',
lmsEndpointUrl,
learningContextId,
validateAssetUrl: false,
});
expect(actual).toEqual(expected);
});
it('returns false when there are no srcs to update', () => {
it('returns false when there are no srcs to update', async () => {
const content = '<div>Hello world!</div>';
const actual = module.replaceStaticWithAsset({ initialContent: content, learningContextId });
const actual = await module.replaceStaticWithAsset({ initialContent: content, learningContextId });
expect(actual).toBeFalsy();
});
it('does not convert static URLs with subdirectories but converts direct static files', () => {
it('does not convert static URLs with subdirectories but converts direct static files', async () => {
const contentWithSubdirectory = '<img src="/static/images/placeholder-faculty.png"/><img src="/static/example.jpg"/>';
const expected = `<img src="/static/images/placeholder-faculty.png"/><img src="/${baseAssetUrl}@example.jpg"/>`;
const actual = module.replaceStaticWithAsset({
const actual = await module.replaceStaticWithAsset({
initialContent: contentWithSubdirectory,
learningContextId,
validateAssetUrl: false,
});
expect(actual).toEqual(expected);
});

it('replaces multiple static assets in one content string', async () => {
const content = `
<img src="/static/a.png"/>
<img src="/static/b.png"/>
`;

const result = await module.replaceStaticWithAsset({
initialContent: content,
learningContextId,
validateAssetUrl: false,
});

expect(result).toBeTruthy();
});

it('validateAssetUrl success path replaces url', async () => {
getAuthenticatedHttpClient.mockReturnValue({
get: jest.fn(() => Promise.resolve({})),
});

const content = '<img src="/static/test.png"/>';

const result = await module.replaceStaticWithAsset({
initialContent: content,
learningContextId,
validateAssetUrl: true,
});

expect(result).toBeTruthy();
});

it('validateAssetUrl failure path keeps original content', async () => {
getAuthenticatedHttpClient.mockReturnValue({
get: jest.fn(() => Promise.reject(new Error('404'))),
});

const content = '<img src="/static/test.png"/>';

const result = await module.replaceStaticWithAsset({
initialContent: content,
learningContextId,
validateAssetUrl: true,
});

expect(result).toBeFalsy();
});

it('handles library keys correctly', async () => {
jest.spyOn(keyUtils, 'isLibraryKey').mockReturnValue(true);

const content = '<img src="/static/test.png"/>';

const result = await module.replaceStaticWithAsset({
initialContent: content,
learningContextId: 'lib:test',
validateAssetUrl: false,
});

expect(result).toContain('static/test.png');
});

it('returns false when asset already valid and no replacement needed', async () => {
const content = '<img src="/asset-v1:test+org+run+type@[email protected]"/>';

const result = await module.replaceStaticWithAsset({
initialContent: content,
learningContextId,
validateAssetUrl: false,
});

expect(result).toBe(false);
});
});
describe('setAssetToStaticUrl', () => {
it('returns content with updated img links', () => {
Expand Down
Loading