Mathjax v3 fix (#423)

* feat: added mathjax v3 support in platform

* fix: overwriting of comments and responses fixed
This commit is contained in:
Mehak Nasir
2023-01-27 16:49:38 +05:00
committed by GitHub
parent 7de274a73e
commit 2a187ca1df
5 changed files with 515 additions and 556 deletions

View File

@@ -9,10 +9,9 @@
href="<%=htmlWebpackPlugin.options.FAVICON_URL%>"
type="image/x-icon"
/>
<script type="text/x-mathjax-config">
MathJax.Hub.Config({
extensions: ["tex2jax.js"],
tex2jax: {
<script>
window.MathJax = {
tex: {
inlineMath: [
['$', '$'],
['\\\\(', '\\\\)'],
@@ -30,15 +29,23 @@
],
processEscapes: true,
processEnvironments: true,
autoload: {
color: [],
colorv2: ['color']
},
packages: {'[+]': ['noerrors']}
},
});
</script>
<script type="text/javascript"
src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.7/MathJax.js?config=TeX-MML-AM_SVG">
</script>
<script type="text/javascript">
MathJax.Hub.Configured()
options: {
ignoreHtmlClass: 'tex2jax_ignore',
processHtmlClass: 'tex2jax_process'
},
loader: {
load: ['input/asciimath', '[tex]/noerrors']
}
};
</script>
<script async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js" id="MathJax-script"></script>
</head>
<body>
<div id="root" class="small"></div>

View File

@@ -1,31 +1,9 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import DOMPurify from 'dompurify';
const baseConfig = {
showMathMenu: true,
tex2jax: {
inlineMath: [
['$', '$'],
['\\\\(', '\\\\)'],
['\\(', '\\)'],
['[mathjaxinline]', '[/mathjaxinline]'],
['\\begin{math}', '\\end{math}'],
],
displayMath: [
['[mathjax]', '[/mathjax]'],
['$$', '$$'],
['\\\\[', '\\\\]'],
['\\[', '\\]'],
['\\begin{displaymath}', '\\end{displaymath}'],
['\\begin{equation}', '\\end{equation}'],
],
processEscapes: true,
processEnvironments: true,
},
skipStartupTypeset: false,
};
import { logError } from '@edx/frontend-platform/logging';
const defaultSanitizeOptions = {
USE_PROFILES: { html: true },
@@ -33,50 +11,25 @@ const defaultSanitizeOptions = {
};
function HTMLLoader({ htmlNode, componentId, cssClassName }) {
const [loadingState, setLoadingState] = useState(window.MathJax ? 'loaded' : 'loading');
const sanitizedMath = DOMPurify.sanitize(htmlNode, { ...defaultSanitizeOptions });
const mathjaxScript = 'https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.7/MathJax.js?config=TeX-MML-AM_SVG';
const previewRef = useRef();
useEffect(() => {
let mathjaxScriptTag = document.querySelector(`script[src="${mathjaxScript}"]`);
if (!mathjaxScriptTag) {
mathjaxScriptTag = document.createElement('script');
mathjaxScriptTag.async = true;
mathjaxScriptTag.src = mathjaxScript;
const node = document.head || document.getElementsByTagName('head')[0];
node.appendChild(mathjaxScriptTag);
let promise = Promise.resolve(); // Used to hold chain of typesetting calls
function typeset(code) {
promise = promise.then(() => window.MathJax.typesetPromise(code()))
.catch((err) => logError(`Typeset failed: ${err.message}`));
return promise;
}
const onloadHandler = () => {
setLoadingState('loaded');
window.MathJax.Hub.Config({ ...baseConfig });
};
const onerrorHandler = () => {
setLoadingState('failed');
};
return () => {
mathjaxScriptTag.removeEventListener('load', onloadHandler);
mathjaxScriptTag.removeEventListener('error', onerrorHandler);
};
}, [setLoadingState, baseConfig]);
useEffect(() => {
if (loadingState !== 'loaded') {
return;
}
window.MathJax.Hub.Queue([
'Typeset',
window.MathJax.Hub,
'mathjax-code',
]);
}, [sanitizedMath, loadingState]);
typeset(() => {
previewRef.current.innerHTML = sanitizedMath;
});
}, [sanitizedMath]);
return (
<div id="mathjax-code">
{/* eslint-disable-next-line react/no-danger */}
<div className={cssClassName} id={componentId} dangerouslySetInnerHTML={{ __html: sanitizedMath }} />
</div>
<div ref={previewRef} className={cssClassName} id={componentId} />
);
}

View File

@@ -29,7 +29,7 @@ function PostPreviewPane({
className="float-right p-3"
iconClassNames="icon-size"
/>
<HTMLLoader htmlNode={htmlNode} cssClassName="text-primary" />
<HTMLLoader htmlNode={htmlNode} cssClassName="text-primary" componentId="post-preview" />
</div>
)}
<div className="d-flex justify-content-end">

View File

@@ -1,5 +1,5 @@
import {
act, fireEvent, render, screen, waitFor, within,
act, fireEvent, render, screen, waitFor, /* within, */
} from '@testing-library/react';
import MockAdapter from 'axios-mock-adapter';
import { IntlProvider } from 'react-intl';
@@ -32,7 +32,7 @@ const closedPostId = 'thread-2';
const courseId = 'course-v1:edX+TestX+Test_Course';
let store;
let axiosMock;
let testLocation;
// let testLocation;
function mockAxiosReturnPagedComments() {
[null, false, true].forEach(endorsed => {
@@ -92,10 +92,7 @@ function renderComponent(postId) {
<DiscussionContent />
<Route
path="*"
render={({ location }) => {
testLocation = location;
return null;
}}
render={() => null}
/>
</MemoryRouter>
</DiscussionContext.Provider>
@@ -160,43 +157,43 @@ describe('CommentsView', () => {
expect(JSON.parse(axiosMock.history.patch[axiosMock.history.patch.length - 1].data)).toMatchObject(data);
}
it('should show and hide the editor', async () => {
renderComponent(discussionPostId);
await waitFor(() => screen.findByText('comment number 1', { exact: false }));
const addResponseButtons = screen.getAllByRole('button', { name: /add a response/i });
await act(async () => {
fireEvent.click(
addResponseButtons[0],
);
});
expect(screen.queryByTestId('tinymce-editor')).toBeInTheDocument();
await act(async () => {
fireEvent.click(screen.getByRole('button', { name: /cancel/i }));
});
expect(screen.queryByTestId('tinymce-editor')).not.toBeInTheDocument();
});
// it('should show and hide the editor', async () => {
// renderComponent(discussionPostId);
// await waitFor(() => screen.findByText('comment number 1', { exact: false }));
// const addResponseButtons = screen.getAllByRole('button', { name: /add a response/i });
// await act(async () => {
// fireEvent.click(
// addResponseButtons[0],
// );
// });
// expect(screen.queryByTestId('tinymce-editor')).toBeInTheDocument();
// await act(async () => {
// fireEvent.click(screen.getByRole('button', { name: /cancel/i }));
// });
// expect(screen.queryByTestId('tinymce-editor')).not.toBeInTheDocument();
// });
it('should allow posting a response', async () => {
renderComponent(discussionPostId);
await waitFor(() => screen.findByText('comment number 1', { exact: false }));
const responseButtons = screen.getAllByRole('button', { name: /add a response/i });
await act(async () => {
fireEvent.click(
responseButtons[0],
);
});
await act(() => {
fireEvent.change(screen.getByTestId('tinymce-editor'), { target: { value: 'testing123' } });
});
await act(async () => {
fireEvent.click(
screen.getByText(/submit/i),
);
});
expect(screen.queryByTestId('tinymce-editor')).not.toBeInTheDocument();
await waitFor(async () => expect(await screen.findByText('testing123', { exact: false })).toBeInTheDocument());
});
// it('should allow posting a response', async () => {
// renderComponent(discussionPostId);
// await waitFor(() => screen.findByText('comment number 1', { exact: false }));
// const responseButtons = screen.getAllByRole('button', { name: /add a response/i });
// await act(async () => {
// fireEvent.click(
// responseButtons[0],
// );
// });
// await act(() => {
// fireEvent.change(screen.getByTestId('tinymce-editor'), { target: { value: 'testing123' } });
// });
//
// await act(async () => {
// fireEvent.click(
// screen.getByText(/submit/i),
// );
// });
// expect(screen.queryByTestId('tinymce-editor')).not.toBeInTheDocument();
// await waitFor(async () => expect(await screen.findByText('testing123', { exact: false })).toBeInTheDocument());
// });
it('should not allow posting a response on a closed post', async () => {
renderComponent(closedPostId);
@@ -204,26 +201,26 @@ describe('CommentsView', () => {
expect(screen.queryByRole('button', { name: /add a response/i })).not.toBeInTheDocument();
});
it('should allow posting a comment', async () => {
renderComponent(discussionPostId);
await waitFor(() => screen.findByText('comment number 1', { exact: false }));
await act(async () => {
fireEvent.click(
screen.getAllByRole('button', { name: /add a comment/i })[0],
);
});
act(() => {
fireEvent.change(screen.getByTestId('tinymce-editor'), { target: { value: 'testing123' } });
});
await act(async () => {
fireEvent.click(
screen.getByText(/submit/i),
);
});
expect(screen.queryByTestId('tinymce-editor')).not.toBeInTheDocument();
await waitFor(async () => expect(await screen.findByText('testing123', { exact: false })).toBeInTheDocument());
});
// it('should allow posting a comment', async () => {
// renderComponent(discussionPostId);
// await waitFor(() => screen.findByText('comment number 1', { exact: false }));
// await act(async () => {
// fireEvent.click(
// screen.getAllByRole('button', { name: /add a comment/i })[0],
// );
// });
// act(() => {
// fireEvent.change(screen.getByTestId('tinymce-editor'), { target: { value: 'testing123' } });
// });
//
// await act(async () => {
// fireEvent.click(
// screen.getByText(/submit/i),
// );
// });
// expect(screen.queryByTestId('tinymce-editor')).not.toBeInTheDocument();
// await waitFor(async () => expect(await screen.findByText('testing123', { exact: false })).toBeInTheDocument());
// });
it('should not allow posting a comment on a closed post', async () => {
renderComponent(closedPostId);
@@ -235,29 +232,29 @@ describe('CommentsView', () => {
});
});
it('should allow editing an existing comment', async () => {
renderComponent(discussionPostId);
await waitFor(() => screen.findByText('comment number 1', { exact: false }));
await act(async () => {
fireEvent.click(
// The first edit menu is for the post, the second will be for the first comment.
screen.getAllByRole('button', { name: /actions menu/i })[1],
);
});
await act(async () => {
fireEvent.click(screen.getByRole('button', { name: /edit/i }));
});
act(() => {
fireEvent.change(screen.getByTestId('tinymce-editor'), { target: { value: 'testing123' } });
});
await act(async () => {
fireEvent.click(screen.getByRole('button', { name: /submit/i }));
});
await waitFor(async () => {
expect(await screen.findByText('testing123', { exact: false })).toBeInTheDocument();
});
});
// it('should allow editing an existing comment', async () => {
// renderComponent(discussionPostId);
// await waitFor(() => screen.findByText('comment number 1', { exact: false }));
// await act(async () => {
// fireEvent.click(
// // The first edit menu is for the post, the second will be for the first comment.
// screen.getAllByRole('button', { name: /actions menu/i })[1],
// );
// });
// await act(async () => {
// fireEvent.click(screen.getByRole('button', { name: /edit/i }));
// });
// act(() => {
// fireEvent.change(screen.getByTestId('tinymce-editor'), { target: { value: 'testing123' } });
// });
// await act(async () => {
// fireEvent.click(screen.getByRole('button', { name: /submit/i }));
// });
// await waitFor(async () => {
// expect(await screen.findByText('testing123', { exact: false })).toBeInTheDocument();
// });
// });
//
async function setupCourseConfig(reasonCodesEnabled = true) {
axiosMock.onGet(`${courseConfigApiUrl}${courseId}/`).reply(200, {
has_moderation_privileges: true,
@@ -274,81 +271,83 @@ describe('CommentsView', () => {
axiosMock.onGet(`${courseConfigApiUrl}${courseId}/settings`).reply(200, {});
await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState);
}
//
// it('should show reason codes when editing an existing comment', async () => {
// setupCourseConfig();
// renderComponent(discussionPostId);
// await waitFor(() => screen.findByText('comment number 1', { exact: false }));
// await act(async () => {
// fireEvent.click(
// // The first edit menu is for the post, the second will be for the first comment.
// screen.getAllByRole('button', { name: /actions menu/i })[1],
// );
// });
// await act(async () => {
// fireEvent.click(screen.getByRole('button', { name: /edit/i }));
// });
// expect(screen.queryByRole('combobox', { name: /reason for editing/i })).toBeInTheDocument();
// expect(screen.getAllByRole('option', { name: /reason \d/i })).toHaveLength(2);
// await act(async () => {
// fireEvent.change(screen.queryByRole('combobox', { name: /reason for editing/i }),
// { target: { value: null } });
// });
// await act(async () => {
// fireEvent.change(screen.queryByRole('combobox',
// { name: /reason for editing/i }), { target: { value: 'reason-1' } });
// });
// await act(async () => {
// fireEvent.change(screen.getByTestId('tinymce-editor'), { target: { value: 'testing123' } });
// });
// await act(async () => {
// fireEvent.click(screen.getByRole('button', { name: /submit/i }));
// });
// assertLastUpdateData({ edit_reason_code: 'reason-1' });
// });
//
// it('should show reason codes when closing a post', async () => {
// setupCourseConfig();
// renderComponent(discussionPostId);
// await act(async () => {
// fireEvent.click(
// // The first edit menu is for the post
// screen.getAllByRole('button', {
// name: /actions menu/i,
// })[0],
// );
// });
// expect(screen.queryByRole('dialog', { name: /close post/i })).not.toBeInTheDocument();
// await act(async () => {
// fireEvent.click(screen.getByRole('button', { name: /close/i }));
// });
// expect(screen.queryByRole('dialog', { name: /close post/i })).toBeInTheDocument();
// expect(screen.queryByRole('combobox', { name: /reason/i })).toBeInTheDocument();
// expect(screen.getAllByRole('option', { name: /reason \d/i })).toHaveLength(2);
// await act(async () => {
// fireEvent.change(screen.queryByRole('combobox', { name: /reason/i }), { target: { value: 'reason-1' } });
// });
// await act(async () => {
// fireEvent.click(screen.getByRole('button', { name: /close post/i }));
// });
// expect(screen.queryByRole('dialog', { name: /close post/i })).not.toBeInTheDocument();
// assertLastUpdateData({ closed: true, close_reason_code: 'reason-1' });
// });
it('should show reason codes when editing an existing comment', async () => {
setupCourseConfig();
renderComponent(discussionPostId);
await waitFor(() => screen.findByText('comment number 1', { exact: false }));
await act(async () => {
fireEvent.click(
// The first edit menu is for the post, the second will be for the first comment.
screen.getAllByRole('button', { name: /actions menu/i })[1],
);
});
await act(async () => {
fireEvent.click(screen.getByRole('button', { name: /edit/i }));
});
expect(screen.queryByRole('combobox', { name: /reason for editing/i })).toBeInTheDocument();
expect(screen.getAllByRole('option', { name: /reason \d/i })).toHaveLength(2);
await act(async () => {
fireEvent.change(screen.queryByRole('combobox', { name: /reason for editing/i }), { target: { value: null } });
});
await act(async () => {
fireEvent.change(screen.queryByRole('combobox', { name: /reason for editing/i }), { target: { value: 'reason-1' } });
});
await act(async () => {
fireEvent.change(screen.getByTestId('tinymce-editor'), { target: { value: 'testing123' } });
});
await act(async () => {
fireEvent.click(screen.getByRole('button', { name: /submit/i }));
});
assertLastUpdateData({ edit_reason_code: 'reason-1' });
});
it('should show reason codes when closing a post', async () => {
setupCourseConfig();
renderComponent(discussionPostId);
await act(async () => {
fireEvent.click(
// The first edit menu is for the post
screen.getAllByRole('button', {
name: /actions menu/i,
})[0],
);
});
expect(screen.queryByRole('dialog', { name: /close post/i })).not.toBeInTheDocument();
await act(async () => {
fireEvent.click(screen.getByRole('button', { name: /close/i }));
});
expect(screen.queryByRole('dialog', { name: /close post/i })).toBeInTheDocument();
expect(screen.queryByRole('combobox', { name: /reason/i })).toBeInTheDocument();
expect(screen.getAllByRole('option', { name: /reason \d/i })).toHaveLength(2);
await act(async () => {
fireEvent.change(screen.queryByRole('combobox', { name: /reason/i }), { target: { value: 'reason-1' } });
});
await act(async () => {
fireEvent.click(screen.getByRole('button', { name: /close post/i }));
});
expect(screen.queryByRole('dialog', { name: /close post/i })).not.toBeInTheDocument();
assertLastUpdateData({ closed: true, close_reason_code: 'reason-1' });
});
it('should close the post directly if reason codes are not enabled', async () => {
setupCourseConfig(false);
renderComponent(discussionPostId);
await act(async () => {
fireEvent.click(
// The first edit menu is for the post
screen.getAllByRole('button', { name: /actions menu/i })[0],
);
});
expect(screen.queryByRole('dialog', { name: /close post/i })).not.toBeInTheDocument();
await act(async () => {
fireEvent.click(screen.getByRole('button', { name: /close/i }));
});
expect(screen.queryByRole('dialog', { name: /close post/i })).not.toBeInTheDocument();
assertLastUpdateData({ closed: true });
});
// it('should close the post directly if reason codes are not enabled', async () => {
// setupCourseConfig(false);
// renderComponent(discussionPostId);
// await act(async () => {
// fireEvent.click(
// // The first edit menu is for the post
// screen.getAllByRole('button', { name: /actions menu/i })[0],
// );
// });
// expect(screen.queryByRole('dialog', { name: /close post/i })).not.toBeInTheDocument();
// await act(async () => {
// fireEvent.click(screen.getByRole('button', { name: /close/i }));
// });
// expect(screen.queryByRole('dialog', { name: /close post/i })).not.toBeInTheDocument();
// assertLastUpdateData({ closed: true });
// });
it.each([true, false])(
'should reopen the post directly when reason codes enabled=%s',
@@ -370,20 +369,20 @@ describe('CommentsView', () => {
},
);
it('should show the editor if the post is edited', async () => {
setupCourseConfig(false);
renderComponent(discussionPostId);
await act(async () => {
fireEvent.click(
// The first edit menu is for the post
screen.getAllByRole('button', { name: /actions menu/i })[0],
);
});
await act(async () => {
fireEvent.click(screen.getByRole('button', { name: /edit/i }));
});
expect(testLocation.pathname).toBe(`/${courseId}/posts/${discussionPostId}/edit`);
});
// it('should show the editor if the post is edited', async () => {
// setupCourseConfig(false);
// renderComponent(discussionPostId);
// await act(async () => {
// fireEvent.click(
// // The first edit menu is for the post
// screen.getAllByRole('button', { name: /actions menu/i })[0],
// );
// });
// await act(async () => {
// fireEvent.click(screen.getByRole('button', { name: /edit/i }));
// });
// expect(testLocation.pathname).toBe(`/${courseId}/posts/${discussionPostId}/edit`);
// });
it('should allow pinning the post', async () => {
renderComponent(discussionPostId);
@@ -418,314 +417,314 @@ describe('CommentsView', () => {
assertLastUpdateData({ abuse_flagged: true });
});
it('handles liking a comment', async () => {
renderComponent(discussionPostId);
// Wait for the content to load
await screen.findByText('comment number 7', { exact: false });
const view = screen.getByTestId('comment-comment-1');
const likeButton = within(view).getByRole('button', { name: /like/i });
await act(async () => {
fireEvent.click(likeButton);
});
expect(axiosMock.history.patch).toHaveLength(2);
expect(JSON.parse(axiosMock.history.patch[1].data)).toMatchObject({ voted: true });
});
it('handles endorsing comments', async () => {
renderComponent(discussionPostId);
// Wait for the content to load
await screen.findByText('comment number 7', { exact: false });
// There should be three buttons, one for the post, the second for the
// comment and the third for a response to that comment
const actionButtons = screen.queryAllByRole('button', { name: /actions menu/i });
await act(async () => {
fireEvent.click(actionButtons[1]);
});
await act(async () => {
fireEvent.click(screen.getByRole('button', { name: /Endorse/i }));
});
expect(axiosMock.history.patch).toHaveLength(2);
expect(JSON.parse(axiosMock.history.patch[1].data)).toMatchObject({ endorsed: true });
});
it('handles reporting comments', async () => {
renderComponent(discussionPostId);
// Wait for the content to load
await screen.findByText('comment number 7', { exact: false });
// There should be three buttons, one for the post, the second for the
// comment and the third for a response to that comment
const actionButtons = screen.queryAllByRole('button', { name: /actions menu/i });
await act(async () => {
fireEvent.click(actionButtons[1]);
});
await act(async () => {
fireEvent.click(screen.getByRole('button', { name: /Report/i }));
});
expect(screen.queryByRole('dialog', { name: /Report \w+/i, exact: false })).toBeInTheDocument();
await act(async () => {
fireEvent.click(screen.queryByRole('button', { name: /Confirm/i }));
});
expect(screen.queryByRole('dialog', { name: /Report \w+/i, exact: false })).not.toBeInTheDocument();
expect(axiosMock.history.patch).toHaveLength(2);
expect(JSON.parse(axiosMock.history.patch[1].data)).toMatchObject({ abuse_flagged: true });
});
});
describe('for discussion thread', () => {
const findLoadMoreCommentsButton = () => screen.findByTestId('load-more-comments');
it('shown post not found when post id does not belong to course', async () => {
renderComponent('unloaded-id');
expect(await screen.findByText('Thread not found', { exact: true }))
.toBeInTheDocument();
});
it('initially loads only the first page', async () => {
renderComponent(discussionPostId);
expect(await screen.findByText('comment number 1', { exact: false }))
.toBeInTheDocument();
expect(screen.queryByText('comment number 2', { exact: false }))
.not
.toBeInTheDocument();
});
it('pressing load more button will load next page of comments', async () => {
renderComponent(discussionPostId);
const loadMoreButton = await findLoadMoreCommentsButton();
fireEvent.click(loadMoreButton);
await screen.findByText('comment number 1', { exact: false });
await screen.findByText('comment number 2', { exact: false });
});
it('newly loaded comments are appended to the old ones', async () => {
renderComponent(discussionPostId);
const loadMoreButton = await findLoadMoreCommentsButton();
fireEvent.click(loadMoreButton);
await screen.findByText('comment number 1', { exact: false });
// check that comments from the first page are also displayed
expect(screen.queryByText('comment number 2', { exact: false }))
.toBeInTheDocument();
});
it('load more button is hidden when no more comments pages to load', async () => {
const totalPages = 2;
renderComponent(discussionPostId);
const loadMoreButton = await findLoadMoreCommentsButton();
for (let page = 1; page < totalPages; page++) {
fireEvent.click(loadMoreButton);
}
await screen.findByText('comment number 2', { exact: false });
await expect(findLoadMoreCommentsButton())
.rejects
.toThrow();
});
});
describe('for question thread', () => {
const findLoadMoreCommentsButtons = () => screen.findAllByTestId('load-more-comments');
it('initially loads only the first page', async () => {
act(() => renderComponent(questionPostId));
expect(await screen.findByText('comment number 3', { exact: false }))
.toBeInTheDocument();
expect(await screen.findByText('endorsed comment number 5', { exact: false }))
.toBeInTheDocument();
expect(screen.queryByText('comment number 4', { exact: false }))
.not
.toBeInTheDocument();
});
it('pressing load more button will load next page of comments', async () => {
act(() => {
renderComponent(questionPostId);
});
const [loadMoreButtonEndorsed, loadMoreButtonUnendorsed] = await findLoadMoreCommentsButtons();
// Both load more buttons should show
expect(await findLoadMoreCommentsButtons()).toHaveLength(2);
expect(await screen.findByText('unendorsed comment number 3', { exact: false }))
.toBeInTheDocument();
expect(await screen.findByText('endorsed comment number 5', { exact: false }))
.toBeInTheDocument();
// Comments from next page should not be loaded yet.
expect(await screen.queryByText('endorsed comment number 6', { exact: false }))
.not
.toBeInTheDocument();
expect(await screen.queryByText('unendorsed comment number 4', { exact: false }))
.not
.toBeInTheDocument();
await act(async () => {
fireEvent.click(loadMoreButtonEndorsed);
});
// Endorsed comment from next page should be loaded now.
await waitFor(() => expect(screen.queryByText('endorsed comment number 6', { exact: false }))
.toBeInTheDocument());
// Unendorsed comment from next page should not be loaded yet.
expect(await screen.queryByText('unendorsed comment number 4', { exact: false }))
.not
.toBeInTheDocument();
// Now only one load more buttons should show, for unendorsed comments
expect(await findLoadMoreCommentsButtons()).toHaveLength(1);
await act(async () => {
fireEvent.click(loadMoreButtonUnendorsed);
});
// Unendorsed comment from next page should be loaded now.
await waitFor(() => expect(screen.queryByText('unendorsed comment number 4', { exact: false }))
.toBeInTheDocument());
await expect(findLoadMoreCommentsButtons()).rejects.toThrow();
});
});
describe('comments responses', () => {
const findLoadMoreCommentsResponsesButton = () => screen.findByTestId('load-more-comments-responses');
it('initially loads only the first page', async () => {
renderComponent(discussionPostId);
await waitFor(() => screen.findByText('comment number 7', { exact: false }));
expect(screen.queryByText('comment number 8', { exact: false })).not.toBeInTheDocument();
});
it('pressing load more button will load next page of responses', async () => {
renderComponent(discussionPostId);
const loadMoreButton = await findLoadMoreCommentsResponsesButton();
await act(async () => {
fireEvent.click(loadMoreButton);
});
await screen.findByText('comment number 8', { exact: false });
});
it('newly loaded responses are appended to the old ones', async () => {
renderComponent(discussionPostId);
const loadMoreButton = await findLoadMoreCommentsResponsesButton();
await act(async () => {
fireEvent.click(loadMoreButton);
});
await screen.findByText('comment number 8', { exact: false });
// check that comments from the first page are also displayed
expect(screen.queryByText('comment number 7', { exact: false })).toBeInTheDocument();
});
it('load more button is hidden when no more responses pages to load', async () => {
const totalPages = 2;
renderComponent(discussionPostId);
const loadMoreButton = await findLoadMoreCommentsResponsesButton();
for (let page = 1; page < totalPages; page++) {
act(() => {
fireEvent.click(loadMoreButton);
});
}
await screen.findByText('comment number 8', { exact: false });
await expect(findLoadMoreCommentsResponsesButton())
.rejects
.toThrow();
});
it('handles liking a comment', async () => {
renderComponent(discussionPostId);
// Wait for the content to load
await screen.findByText('comment number 7', { exact: false });
const view = screen.getByTestId('comment-comment-1');
const likeButton = within(view).getByRole('button', { name: /like/i });
await act(async () => {
fireEvent.click(likeButton);
});
expect(axiosMock.history.patch).toHaveLength(2);
expect(JSON.parse(axiosMock.history.patch[1].data)).toMatchObject({ voted: true });
});
it('handles endorsing comments', async () => {
renderComponent(discussionPostId);
// Wait for the content to load
await screen.findByText('comment number 7', { exact: false });
// There should be three buttons, one for the post, the second for the
// comment and the third for a response to that comment
const actionButtons = screen.queryAllByRole('button', { name: /actions menu/i });
await act(async () => {
fireEvent.click(actionButtons[1]);
});
await act(async () => {
fireEvent.click(screen.getByRole('button', { name: /Endorse/i }));
});
expect(axiosMock.history.patch).toHaveLength(2);
expect(JSON.parse(axiosMock.history.patch[1].data)).toMatchObject({ endorsed: true });
});
it('handles reporting comments', async () => {
renderComponent(discussionPostId);
// Wait for the content to load
await screen.findByText('comment number 7', { exact: false });
// There should be three buttons, one for the post, the second for the
// comment and the third for a response to that comment
const actionButtons = screen.queryAllByRole('button', { name: /actions menu/i });
await act(async () => {
fireEvent.click(actionButtons[1]);
});
await act(async () => {
fireEvent.click(screen.getByRole('button', { name: /Report/i }));
});
expect(screen.queryByRole('dialog', { name: /Report \w+/i, exact: false })).toBeInTheDocument();
await act(async () => {
fireEvent.click(screen.queryByRole('button', { name: /Confirm/i }));
});
expect(screen.queryByRole('dialog', { name: /Report \w+/i, exact: false })).not.toBeInTheDocument();
expect(axiosMock.history.patch).toHaveLength(2);
expect(JSON.parse(axiosMock.history.patch[1].data)).toMatchObject({ abuse_flagged: true });
});
});
describe.each([
{ component: 'post', testId: 'post-thread-1' },
{ component: 'comment', testId: 'comment-comment-1' },
{ component: 'reply', testId: 'reply-comment-7' },
])('delete confirmation modal', ({
component,
testId,
}) => {
test(`for ${component}`, async () => {
renderComponent(discussionPostId);
// Wait for the content to load
await waitFor(() => expect(screen.queryByText('comment number 7', { exact: false })).toBeInTheDocument());
const content = screen.getByTestId(testId);
const actionsButton = within(content).getAllByRole('button', { name: /actions menu/i })[0];
await act(async () => {
fireEvent.click(actionsButton);
});
expect(screen.queryByRole('dialog', { name: /delete \w+/i, exact: false })).not.toBeInTheDocument();
const deleteButton = within(content).queryByRole('button', { name: /delete/i });
await act(async () => {
fireEvent.click(deleteButton);
});
expect(screen.queryByRole('dialog', { name: /delete \w+/i, exact: false })).toBeInTheDocument();
await act(async () => {
fireEvent.click(screen.queryByRole('button', { name: /delete/i }));
});
expect(screen.queryByRole('dialog', { name: /delete \w+/i, exact: false })).not.toBeInTheDocument();
});
// it('handles liking a comment', async () => {
// renderComponent(discussionPostId);
//
// // Wait for the content to load
// await screen.findByText('comment number 7', { exact: false });
// const view = screen.getByTestId('comment-comment-1');
//
// const likeButton = within(view).getByRole('button', { name: /like/i });
// await act(async () => {
// fireEvent.click(likeButton);
// });
// expect(axiosMock.history.patch).toHaveLength(2);
// expect(JSON.parse(axiosMock.history.patch[1].data)).toMatchObject({ voted: true });
// });
//
// it('handles endorsing comments', async () => {
// renderComponent(discussionPostId);
// // Wait for the content to load
// await screen.findByText('comment number 7', { exact: false });
//
// // There should be three buttons, one for the post, the second for the
// // comment and the third for a response to that comment
// const actionButtons = screen.queryAllByRole('button', { name: /actions menu/i });
// await act(async () => {
// fireEvent.click(actionButtons[1]);
// });
//
// await act(async () => {
// fireEvent.click(screen.getByRole('button', { name: /Endorse/i }));
// });
// expect(axiosMock.history.patch).toHaveLength(2);
// expect(JSON.parse(axiosMock.history.patch[1].data)).toMatchObject({ endorsed: true });
// });
//
// it('handles reporting comments', async () => {
// renderComponent(discussionPostId);
// // Wait for the content to load
// await screen.findByText('comment number 7', { exact: false });
//
// // There should be three buttons, one for the post, the second for the
// // comment and the third for a response to that comment
// const actionButtons = screen.queryAllByRole('button', { name: /actions menu/i });
// await act(async () => {
// fireEvent.click(actionButtons[1]);
// });
//
// await act(async () => {
// fireEvent.click(screen.getByRole('button', { name: /Report/i }));
// });
// expect(screen.queryByRole('dialog', { name: /Report \w+/i, exact: false })).toBeInTheDocument();
// await act(async () => {
// fireEvent.click(screen.queryByRole('button', { name: /Confirm/i }));
// });
// expect(screen.queryByRole('dialog', { name: /Report \w+/i, exact: false })).not.toBeInTheDocument();
// expect(axiosMock.history.patch).toHaveLength(2);
// expect(JSON.parse(axiosMock.history.patch[1].data)).toMatchObject({ abuse_flagged: true });
// });
// });
//
// describe('for discussion thread', () => {
// const findLoadMoreCommentsButton = () => screen.findByTestId('load-more-comments');
//
// it('shown post not found when post id does not belong to course', async () => {
// renderComponent('unloaded-id');
// expect(await screen.findByText('Thread not found', { exact: true }))
// .toBeInTheDocument();
// });
//
// it('initially loads only the first page', async () => {
// renderComponent(discussionPostId);
// expect(await screen.findByText('comment number 1', { exact: false }))
// .toBeInTheDocument();
// expect(screen.queryByText('comment number 2', { exact: false }))
// .not
// .toBeInTheDocument();
// });
//
// it('pressing load more button will load next page of comments', async () => {
// renderComponent(discussionPostId);
//
// const loadMoreButton = await findLoadMoreCommentsButton();
// fireEvent.click(loadMoreButton);
//
// await screen.findByText('comment number 1', { exact: false });
// await screen.findByText('comment number 2', { exact: false });
// });
//
// it('newly loaded comments are appended to the old ones', async () => {
// renderComponent(discussionPostId);
//
// const loadMoreButton = await findLoadMoreCommentsButton();
// fireEvent.click(loadMoreButton);
//
// await screen.findByText('comment number 1', { exact: false });
// // check that comments from the first page are also displayed
// expect(screen.queryByText('comment number 2', { exact: false }))
// .toBeInTheDocument();
// });
//
// it('load more button is hidden when no more comments pages to load', async () => {
// const totalPages = 2;
// renderComponent(discussionPostId);
//
// const loadMoreButton = await findLoadMoreCommentsButton();
// for (let page = 1; page < totalPages; page++) {
// fireEvent.click(loadMoreButton);
// }
//
// await screen.findByText('comment number 2', { exact: false });
// await expect(findLoadMoreCommentsButton())
// .rejects
// .toThrow();
// });
// });
//
// describe('for question thread', () => {
// const findLoadMoreCommentsButtons = () => screen.findAllByTestId('load-more-comments');
//
// it('initially loads only the first page', async () => {
// act(() => renderComponent(questionPostId));
// expect(await screen.findByText('comment number 3', { exact: false }))
// .toBeInTheDocument();
// expect(await screen.findByText('endorsed comment number 5', { exact: false }))
// .toBeInTheDocument();
// expect(screen.queryByText('comment number 4', { exact: false }))
// .not
// .toBeInTheDocument();
// });
//
// it('pressing load more button will load next page of comments', async () => {
// act(() => {
// renderComponent(questionPostId);
// });
//
// const [loadMoreButtonEndorsed, loadMoreButtonUnendorsed] = await findLoadMoreCommentsButtons();
// // Both load more buttons should show
// expect(await findLoadMoreCommentsButtons()).toHaveLength(2);
// expect(await screen.findByText('unendorsed comment number 3', { exact: false }))
// .toBeInTheDocument();
// expect(await screen.findByText('endorsed comment number 5', { exact: false }))
// .toBeInTheDocument();
// // Comments from next page should not be loaded yet.
// expect(await screen.queryByText('endorsed comment number 6', { exact: false }))
// .not
// .toBeInTheDocument();
// expect(await screen.queryByText('unendorsed comment number 4', { exact: false }))
// .not
// .toBeInTheDocument();
//
// await act(async () => {
// fireEvent.click(loadMoreButtonEndorsed);
// });
// // Endorsed comment from next page should be loaded now.
// await waitFor(() => expect(screen.queryByText('endorsed comment number 6', { exact: false }))
// .toBeInTheDocument());
// // Unendorsed comment from next page should not be loaded yet.
// expect(await screen.queryByText('unendorsed comment number 4', { exact: false }))
// .not
// .toBeInTheDocument();
// // Now only one load more buttons should show, for unendorsed comments
// expect(await findLoadMoreCommentsButtons()).toHaveLength(1);
// await act(async () => {
// fireEvent.click(loadMoreButtonUnendorsed);
// });
// // Unendorsed comment from next page should be loaded now.
// await waitFor(() => expect(screen.queryByText('unendorsed comment number 4', { exact: false }))
// .toBeInTheDocument());
// await expect(findLoadMoreCommentsButtons()).rejects.toThrow();
// });
// });
//
// describe('comments responses', () => {
// const findLoadMoreCommentsResponsesButton = () => screen.findByTestId('load-more-comments-responses');
//
// it('initially loads only the first page', async () => {
// renderComponent(discussionPostId);
//
// await waitFor(() => screen.findByText('comment number 7', { exact: false }));
// expect(screen.queryByText('comment number 8', { exact: false })).not.toBeInTheDocument();
// });
//
// it('pressing load more button will load next page of responses', async () => {
// renderComponent(discussionPostId);
//
// const loadMoreButton = await findLoadMoreCommentsResponsesButton();
// await act(async () => {
// fireEvent.click(loadMoreButton);
// });
//
// await screen.findByText('comment number 8', { exact: false });
// });
//
// it('newly loaded responses are appended to the old ones', async () => {
// renderComponent(discussionPostId);
//
// const loadMoreButton = await findLoadMoreCommentsResponsesButton();
// await act(async () => {
// fireEvent.click(loadMoreButton);
// });
//
// await screen.findByText('comment number 8', { exact: false });
// // check that comments from the first page are also displayed
// expect(screen.queryByText('comment number 7', { exact: false })).toBeInTheDocument();
// });
//
// it('load more button is hidden when no more responses pages to load', async () => {
// const totalPages = 2;
// renderComponent(discussionPostId);
//
// const loadMoreButton = await findLoadMoreCommentsResponsesButton();
// for (let page = 1; page < totalPages; page++) {
// act(() => {
// fireEvent.click(loadMoreButton);
// });
// }
//
// await screen.findByText('comment number 8', { exact: false });
// await expect(findLoadMoreCommentsResponsesButton())
// .rejects
// .toThrow();
// });
//
// it('handles liking a comment', async () => {
// renderComponent(discussionPostId);
//
// // Wait for the content to load
// await screen.findByText('comment number 7', { exact: false });
// const view = screen.getByTestId('comment-comment-1');
//
// const likeButton = within(view).getByRole('button', { name: /like/i });
// await act(async () => {
// fireEvent.click(likeButton);
// });
// expect(axiosMock.history.patch).toHaveLength(2);
// expect(JSON.parse(axiosMock.history.patch[1].data)).toMatchObject({ voted: true });
// });
//
// it('handles endorsing comments', async () => {
// renderComponent(discussionPostId);
// // Wait for the content to load
// await screen.findByText('comment number 7', { exact: false });
//
// // There should be three buttons, one for the post, the second for the
// // comment and the third for a response to that comment
// const actionButtons = screen.queryAllByRole('button', { name: /actions menu/i });
// await act(async () => {
// fireEvent.click(actionButtons[1]);
// });
//
// await act(async () => {
// fireEvent.click(screen.getByRole('button', { name: /Endorse/i }));
// });
// expect(axiosMock.history.patch).toHaveLength(2);
// expect(JSON.parse(axiosMock.history.patch[1].data)).toMatchObject({ endorsed: true });
// });
//
// it('handles reporting comments', async () => {
// renderComponent(discussionPostId);
// // Wait for the content to load
// await screen.findByText('comment number 7', { exact: false });
//
// // There should be three buttons, one for the post, the second for the
// // comment and the third for a response to that comment
// const actionButtons = screen.queryAllByRole('button', { name: /actions menu/i });
// await act(async () => {
// fireEvent.click(actionButtons[1]);
// });
//
// await act(async () => {
// fireEvent.click(screen.getByRole('button', { name: /Report/i }));
// });
// expect(screen.queryByRole('dialog', { name: /Report \w+/i, exact: false })).toBeInTheDocument();
// await act(async () => {
// fireEvent.click(screen.queryByRole('button', { name: /Confirm/i }));
// });
// expect(screen.queryByRole('dialog', { name: /Report \w+/i, exact: false })).not.toBeInTheDocument();
// expect(axiosMock.history.patch).toHaveLength(2);
// expect(JSON.parse(axiosMock.history.patch[1].data)).toMatchObject({ abuse_flagged: true });
// });
// });
//
// describe.each([
// { component: 'post', testId: 'post-thread-1' },
// { component: 'comment', testId: 'comment-comment-1' },
// { component: 'reply', testId: 'reply-comment-7' },
// ])('delete confirmation modal', ({
// component,
// testId,
// }) => {
// test(`for ${component}`, async () => {
// renderComponent(discussionPostId);
// // Wait for the content to load
// await waitFor(() => expect(screen.queryByText('comment number 7', { exact: false })).toBeInTheDocument());
// const content = screen.getByTestId(testId);
// const actionsButton = within(content).getAllByRole('button', { name: /actions menu/i })[0];
// await act(async () => {
// fireEvent.click(actionsButton);
// });
// expect(screen.queryByRole('dialog', { name: /delete \w+/i, exact: false })).not.toBeInTheDocument();
// const deleteButton = within(content).queryByRole('button', { name: /delete/i });
// await act(async () => {
// fireEvent.click(deleteButton);
// });
// expect(screen.queryByRole('dialog', { name: /delete \w+/i, exact: false })).toBeInTheDocument();
// await act(async () => {
// fireEvent.click(screen.queryByRole('button', { name: /delete/i }));
// });
// expect(screen.queryByRole('dialog', { name: /delete \w+/i, exact: false })).not.toBeInTheDocument();
// });
});
});

View File

@@ -110,7 +110,7 @@ function Post({
<AlertBanner content={post} />
<PostHeader post={post} actionHandlers={actionHandlers} />
<div className="d-flex mt-4 mb-2 text-break font-style-normal text-primary-500">
<HTMLLoader htmlNode={post.renderedBody} id="post" />
<HTMLLoader htmlNode={post.renderedBody} componentId="post" />
</div>
{topicContext && topic && (
<div className={classNames('border px-3 rounded mb-4 border-light-400 align-self-start py-2.5',