fix: text editor opening blank with no images (#492)

* fix: pasting and images only insert at beginning

* fix: add image click not showing gallery

* chore: increase code coverage

* fix: empty string when no srcs need updates

* fix: assest to static in raw editor
This commit is contained in:
Kristin Aoki
2024-07-10 13:53:40 -04:00
committed by GitHub
parent fc3cd9a9ce
commit b6ff6230e7
11 changed files with 133 additions and 82 deletions

View File

@@ -16,10 +16,12 @@ export const ExplanationWidget = ({
intl,
}) => {
const { editorRef, refReady, setEditorRef } = prepareEditorRef();
const solutionContent = replaceStaticWithAsset({
initialContent: settings?.solutionExplanation || '',
const initialContent = settings?.solutionExplanation || '';
const newContent = replaceStaticWithAsset({
initialContent,
learningContextId,
});
const solutionContent = newContent || initialContent;
if (!refReady) { return null; }
return (
<div className="tinyMceWidget mt-4 text-primary-500">

View File

@@ -1,4 +1,3 @@
/* eslint-disable react/prop-types */
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { formatMessage } from '../../../../../../testUtils';
@@ -24,11 +23,11 @@ jest.mock('../../../../../data/redux', () => ({
}));
jest.mock('../../../../../sharedComponents/TinyMceWidget/hooks', () => ({
...jest.requireActual('../../../../../sharedComponents/TinyMceWidget/hooks'),
prepareEditorRef: jest.fn(() => ({
refReady: true,
setEditorRef: jest.fn().mockName('prepareEditorRef.setEditorRef'),
})),
replaceStaticWithAsset: jest.fn(() => 'This is my solution'),
}));
describe('SolutionWidget', () => {

View File

@@ -16,10 +16,12 @@ export const QuestionWidget = ({
intl,
}) => {
const { editorRef, refReady, setEditorRef } = prepareEditorRef();
const questionContent = replaceStaticWithAsset({
initialContent: question,
const initialContent = question;
const newContent = replaceStaticWithAsset({
initialContent,
learningContextId,
});
const questionContent = newContent || initialContent;
if (!refReady) { return null; }
return (
<div className="tinyMceWidget">

View File

@@ -1,4 +1,3 @@
/* eslint-disable react/prop-types */
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { formatMessage } from '../../../../../../testUtils';
@@ -29,11 +28,11 @@ jest.mock('../../../../../data/redux', () => ({
}));
jest.mock('../../../../../sharedComponents/TinyMceWidget/hooks', () => ({
...jest.requireActual('../../../../../sharedComponents/TinyMceWidget/hooks'),
prepareEditorRef: jest.fn(() => ({
refReady: true,
setEditorRef: jest.fn().mockName('prepareEditorRef.setEditorRef'),
})),
replaceStaticWithAsset: jest.fn(() => 'This is my question'),
}));
describe('QuestionWidget', () => {

View File

@@ -191,3 +191,52 @@ exports[`TextEditor snapshots renders as expected with default behavior 1`] = `
</div>
</EditorContainer>
`;
exports[`TextEditor snapshots renders static images with relative paths 1`] = `
<EditorContainer
getContent={
{
"getContent": {
"editorRef": {
"current": {
"value": "something",
},
},
"showRawEditor": false,
},
}
}
onClose={[MockFunction props.onClose]}
returnFunction={null}
>
<div
className="editor-body h-75 overflow-auto"
>
<Toast
onClose={[MockFunction hooks.nullMethod]}
show={false}
>
<FormattedMessage
defaultMessage="Error: Could Not Load Text Content"
description="Error Message Dispayed When HTML content fails to Load"
id="authoring.texteditor.load.error"
/>
</Toast>
<[object Object]
editorContentHtml="eDiTablE Text with <img src="/asset+org+run+type@asset+block@img.jpg" />"
editorRef={
{
"current": {
"value": "something",
},
}
}
editorType="text"
height="100%"
initializeEditor={[MockFunction args.intializeEditor]}
minHeight={500}
setEditorRef={[MockFunction hooks.prepareEditorRef.setEditorRef]}
/>
</div>
</EditorContainer>
`;

View File

@@ -32,10 +32,12 @@ export const TextEditor = ({
intl,
}) => {
const { editorRef, refReady, setEditorRef } = prepareEditorRef();
const editorContent = blockValue ? replaceStaticWithAsset({
initialContent: blockValue.data.data,
const initialContent = blockValue ? blockValue.data.data : '';
const newContent = replaceStaticWithAsset({
initialContent,
learningContextId,
}) : '';
});
const editorContent = newContent || initialContent;
if (!refReady) { return null; }

View File

@@ -25,12 +25,12 @@ jest.mock('./hooks', () => ({
}));
jest.mock('../../sharedComponents/TinyMceWidget/hooks', () => ({
...jest.requireActual('../../sharedComponents/TinyMceWidget/hooks'),
prepareEditorRef: jest.fn(() => ({
editorRef: { current: { value: 'something' } },
refReady: true,
setEditorRef: jest.fn().mockName('hooks.prepareEditorRef.setEditorRef'),
})),
replaceStaticWithAsset: jest.fn(() => 'eDiTablE Text'),
}));
jest.mock('react', () => {
@@ -88,6 +88,13 @@ describe('TextEditor', () => {
test('renders as expected with default behavior', () => {
expect(shallow(<TextEditor {...props} />).snapshot).toMatchSnapshot();
});
test('renders static images with relative paths', () => {
const updatedProps = {
...props,
blockValue: { data: { data: 'eDiTablE Text with <img src="/static/img.jpg" />' } },
};
expect(shallow(<TextEditor {...updatedProps} />).snapshot).toMatchSnapshot();
});
test('not yet loaded, Spinner appears', () => {
expect(shallow(<TextEditor {...props} blockFinished={false} />).snapshot).toMatchSnapshot();
});

View File

@@ -1,52 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`RawEditor renders as expected with content equal to null 1`] = `
<div>
<Alert
variant="danger"
>
You are using the raw
html
editor.
</Alert>
</div>
`;
exports[`RawEditor renders as expected with default behavior 1`] = `
<div>
<Alert
variant="danger"
>
You are using the raw
html
editor.
</Alert>
<injectIntl(ShimmedIntlComponent)
innerRef={
{
"current": {
"value": "Ref Value",
},
}
}
lang="html"
value="eDiTablE Text"
/>
</div>
`;
exports[`RawEditor renders as expected with lang equal to xml 1`] = `
<div>
<injectIntl(ShimmedIntlComponent)
innerRef={
{
"current": {
"value": "Ref Value",
},
}
}
lang="xml"
value="eDiTablE Text"
/>
</div>
`;

View File

@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import { Alert } from '@openedx/paragon';
import CodeEditor from '../CodeEditor';
import { setAssetToStaticUrl } from '../TinyMceWidget/hooks';
function getValue(content) {
if (!content) { return null; }
@@ -15,7 +16,8 @@ export const RawEditor = ({
content,
lang,
}) => {
const value = getValue(content);
const value = getValue(content) || '';
const staticUpdate = setAssetToStaticUrl({ editorValue: value });
return (
<div>
@@ -27,8 +29,9 @@ export const RawEditor = ({
{ value ? (
<CodeEditor
innerRef={editorRef}
value={value}
value={staticUpdate}
lang={lang}
data-testid="code-editor"
/>
) : null}

View File

@@ -1,8 +1,17 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import '@testing-library/jest-dom/extend-expect';
import { RawEditor } from '.';
jest.unmock('@openedx/paragon');
const renderComponent = (props) => render(
<IntlProvider locale="en">
<RawEditor {...props} />
</IntlProvider>,
);
describe('RawEditor', () => {
const defaultProps = {
editorRef: {
@@ -10,7 +19,7 @@ describe('RawEditor', () => {
value: 'Ref Value',
},
},
content: { data: { data: 'eDiTablE Text' } },
content: { data: { data: 'eDiTablE Text HtmL' } },
lang: 'html',
};
const xmlProps = {
@@ -19,7 +28,7 @@ describe('RawEditor', () => {
value: 'Ref Value',
},
},
content: { data: { data: 'eDiTablE Text' } },
content: { data: { data: 'eDiTablE Text XMl' } },
lang: 'xml',
};
const noContentProps = {
@@ -33,13 +42,39 @@ describe('RawEditor', () => {
width: { width: '80%' },
};
test('renders as expected with default behavior', () => {
expect(shallow(<RawEditor {...defaultProps} />).snapshot).toMatchSnapshot();
it('renders as expected with default behavior', () => {
renderComponent(defaultProps);
expect(screen.getByRole('alert')).toBeVisible();
expect(screen.getByText('eDiTablE Text HtmL')).toBeVisible();
});
test('renders as expected with lang equal to xml', () => {
expect(shallow(<RawEditor {...xmlProps} />).snapshot).toMatchSnapshot();
it('updates the assets to static srcs', () => {
const updatedProps = {
...defaultProps,
content: 'pick <img src="/asset-v1:org+run+term+type@asset+block@img.jpeg" /> or <img src="/assets/courseware/v1/hash/asset-v1:org+run+term+type@asset+block/img2.jpeg" />',
};
renderComponent(updatedProps);
expect(screen.getByText('"/static/img.jpeg"')).toBeVisible();
expect(screen.getByText('"/static/img2.jpeg"')).toBeVisible();
expect(screen.queryByText('"/asset-v1:org+run+term+type@asset+block@img.jpeg"')).toBeNull();
expect(screen.queryByText('"/assets/courseware/v1/hash/asset-v1:org+run+term+type@asset+block/img2.jpeg"')).toBeNull();
});
test('renders as expected with content equal to null', () => {
expect(shallow(<RawEditor {...noContentProps} />).snapshot).toMatchSnapshot();
it('renders as expected with lang equal to xml', () => {
renderComponent(xmlProps);
expect(screen.queryByRole('alert')).toBeNull();
expect(screen.getByText('eDiTablE Text XMl')).toBeVisible();
});
it('renders as expected with content equal to null', () => {
renderComponent(noContentProps);
expect(screen.getByRole('alert')).toBeVisible();
expect(screen.queryByTestId('code-editor')).toBeNull();
});
});

View File

@@ -403,15 +403,20 @@ export const setAssetToStaticUrl = ({ editorValue, lmsEndpointUrl }) => {
let content = editorValue.replace(regExLmsEndpointUrl, '');
const assetSrcs = typeof content === 'string' ? content.split(/(src="|src=&quot;|href="|href=&quot)/g) : [];
assetSrcs.forEach(src => {
if (src.startsWith('/asset')) {
assetSrcs.filter(src => src.startsWith('/asset')).forEach(src => {
let nameFromEditorSrc;
if (src.match(/\/assets\/.+\/asset-v1:\S+[+]\S+[@]\S+[+]\S+\//)?.length >= 1) {
const assetBlockName = src.substring(0, src.search(/("|&quot;)/));
const dividedSrc = assetBlockName.split(/\/assets\/.+\/asset-v1:\S+[+]\S+[@]\S+[+]\S+\//);
[, nameFromEditorSrc] = dividedSrc;
} else {
const assetBlockName = src.substring(src.indexOf('@') + 1, src.search(/("|&quot;)/));
const nameFromEditorSrc = assetBlockName.substring(assetBlockName.indexOf('@') + 1);
const portableUrl = getStaticUrl({ displayName: nameFromEditorSrc });
const currentSrc = src.substring(0, src.search(/("|&quot;)/));
const updatedContent = content.replace(currentSrc, portableUrl);
content = updatedContent;
nameFromEditorSrc = assetBlockName.substring(assetBlockName.indexOf('@') + 1);
}
const portableUrl = getStaticUrl({ displayName: nameFromEditorSrc });
const currentSrc = src.substring(0, src.search(/("|&quot;)/));
const updatedContent = content.replace(currentSrc, portableUrl);
content = updatedContent;
});
return content;
};