Compare commits
7 Commits
aansari/is
...
renovate/r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd202a3a91 | ||
|
|
e1383d1e65 | ||
|
|
f7b5615660 | ||
|
|
0d5e3b8a1c | ||
|
|
4e90bbc756 | ||
|
|
b4dcaca660 | ||
|
|
5defe5cbd4 |
9447
package-lock.json
generated
9447
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -51,8 +51,8 @@
|
||||
"react-google-recaptcha-v3": "^1.11.0",
|
||||
"react-helmet": "6.1.0",
|
||||
"react-redux": "7.2.9",
|
||||
"react-router": "6.18.0",
|
||||
"react-router-dom": "6.18.0",
|
||||
"react-router": "6.30.3",
|
||||
"react-router-dom": "6.30.3",
|
||||
"redux": "4.2.1",
|
||||
"regenerator-runtime": "0.14.1",
|
||||
"timeago.js": "4.0.2",
|
||||
|
||||
@@ -75,7 +75,7 @@ const HoverCard = ({
|
||||
const actionFunction = actionHandlers[endorseIcons.action];
|
||||
actionFunction();
|
||||
}}
|
||||
className={['endorse', 'unendorse'].includes(endorseIcons.id) ? 'text-dark-500' : 'text-success-500'}
|
||||
className="text-primary"
|
||||
size="sm"
|
||||
alt="Endorse"
|
||||
/>
|
||||
|
||||
@@ -14,5 +14,5 @@ export const selectLearnerSorting = () => state => state.learners.sortedBy;
|
||||
export const selectLearnerNextPage = () => state => state.learners.nextPage;
|
||||
|
||||
export const selectLearnerAvatar = author => state => (
|
||||
state.learners.learnerProfiles[author]?.profileImage?.imageUrlLarge
|
||||
state.learners.learnerProfiles[author]?.profileImage?.imageUrlSmall
|
||||
);
|
||||
|
||||
@@ -9,7 +9,9 @@ import {
|
||||
} from 'react-router-dom';
|
||||
import { Factory } from 'rosie';
|
||||
|
||||
import { camelCaseObject, initializeMockApp } from '@edx/frontend-platform';
|
||||
import {
|
||||
camelCaseObject, getConfig, initializeMockApp, setConfig,
|
||||
} from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
|
||||
@@ -738,6 +740,20 @@ describe('ThreadView', () => {
|
||||
expect(screen.queryByTestId('comment-comment-4'))
|
||||
.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('it show avatar for reply author when ENABLE_PROFILE_IMAGE is true', async () => {
|
||||
setConfig({
|
||||
...getConfig(),
|
||||
ENABLE_PROFILE_IMAGE: 'true',
|
||||
});
|
||||
await waitFor(() => renderComponent(discussionPostId));
|
||||
|
||||
const comment = await waitFor(() => screen.findByTestId('comment-comment-1'));
|
||||
|
||||
expect(comment).toBeInTheDocument();
|
||||
const replyAuthorAvatar = within(comment).getAllByAltText('edx');
|
||||
expect(replyAuthorAvatar.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('for question thread', () => {
|
||||
|
||||
@@ -46,7 +46,7 @@ const Comment = ({
|
||||
const {
|
||||
id, parentId, childCount, abuseFlagged, endorsed, threadId, endorsedAt, endorsedBy, endorsedByLabel, renderedBody,
|
||||
voted, following, voteCount, authorLabel, author, createdAt, lastEdit, rawBody, closed, closedBy, closeReason,
|
||||
editByLabel, closedByLabel, users: postUsers,
|
||||
editByLabel, closedByLabel, users: commentUsers,
|
||||
} = comment;
|
||||
const intl = useIntl();
|
||||
const hasChildren = childCount > 0;
|
||||
@@ -209,7 +209,7 @@ const Comment = ({
|
||||
closed={closed}
|
||||
createdAt={createdAt}
|
||||
lastEdit={lastEdit}
|
||||
postUsers={postUsers}
|
||||
commentUsers={commentUsers}
|
||||
/>
|
||||
{isEditing ? (
|
||||
<CommentEditor
|
||||
@@ -226,7 +226,7 @@ const Comment = ({
|
||||
/>
|
||||
) : (
|
||||
<HTMLLoader
|
||||
cssClassName="comment-body html-loader text-break mt-14px font-style text-primary-500"
|
||||
cssClassName="comment-body html-loader text-break mt-14px font-style text-gray-700"
|
||||
componentId="comment"
|
||||
htmlNode={renderedBody}
|
||||
testId={id}
|
||||
|
||||
@@ -19,7 +19,7 @@ const CommentHeader = ({
|
||||
closed,
|
||||
createdAt,
|
||||
lastEdit,
|
||||
postUsers,
|
||||
commentUsers,
|
||||
}) => {
|
||||
const colorClass = AvatarOutlineAndLabelColors[authorLabel];
|
||||
const hasAnyAlert = useAlertBannerVisible({
|
||||
@@ -31,7 +31,7 @@ const CommentHeader = ({
|
||||
const authorAvatar = useSelector(selectAuthorAvatar(author));
|
||||
|
||||
const profileImage = getConfig()?.ENABLE_PROFILE_IMAGE === 'true'
|
||||
? Object.values(postUsers ?? {})[0]?.profile?.image
|
||||
? Object.values(commentUsers ?? {})[0]?.profile?.image
|
||||
: null;
|
||||
|
||||
return (
|
||||
@@ -43,7 +43,7 @@ const CommentHeader = ({
|
||||
<Avatar
|
||||
className={`border-0 ml-0.5 mr-2.5 ${colorClass ? `outline-${colorClass}` : 'outline-anonymous'}`}
|
||||
alt={author}
|
||||
src={profileImage?.hasImage ? profileImage?.imageUrlSmall : authorAvatar}
|
||||
src={profileImage?.hasImage ? profileImage?.imageUrlSmall : authorAvatar?.imageUrlSmall}
|
||||
style={{
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
@@ -72,7 +72,7 @@ CommentHeader.propTypes = {
|
||||
editorUsername: PropTypes.string,
|
||||
reason: PropTypes.string,
|
||||
}),
|
||||
postUsers: PropTypes.shape({}).isRequired,
|
||||
commentUsers: PropTypes.shape({}).isRequired,
|
||||
};
|
||||
|
||||
CommentHeader.defaultProps = {
|
||||
|
||||
@@ -22,7 +22,7 @@ const defaultProps = {
|
||||
closed: false,
|
||||
createdAt: '2025-09-23T10:00:00Z',
|
||||
lastEdit: null,
|
||||
postUsers: {
|
||||
commentUsers: {
|
||||
'test-user': {
|
||||
profile: { image: { hasImage: true, imageUrlSmall: 'http://avatar.test/img.png' } },
|
||||
},
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Avatar, useToggle } from '@openedx/paragon';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import * as timeago from 'timeago.js';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import HTMLLoader from '../../../../components/HTMLLoader';
|
||||
@@ -24,7 +25,7 @@ import CommentEditor from './CommentEditor';
|
||||
const Reply = ({ responseId }) => {
|
||||
timeago.register('time-locale', timeLocale);
|
||||
const {
|
||||
id, abuseFlagged, author, authorLabel, endorsed, lastEdit, closed, closedBy,
|
||||
id, abuseFlagged, author, authorLabel, endorsed, lastEdit, closed, closedBy, users: replyUsers,
|
||||
closeReason, createdAt, threadId, parentId, rawBody, renderedBody, editByLabel, closedByLabel,
|
||||
} = useSelector(selectCommentOrResponseById(responseId));
|
||||
const intl = useIntl();
|
||||
@@ -78,6 +79,10 @@ const Reply = ({ responseId }) => {
|
||||
[ContentActions.REPORT]: handleAbusedFlag,
|
||||
}), [handleEditContent, handleReplyEndorse, showDeleteConfirmation, handleAbusedFlag]);
|
||||
|
||||
const profileImage = getConfig()?.ENABLE_PROFILE_IMAGE === 'true'
|
||||
? Object.values(replyUsers ?? {})[0]?.profile?.image
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className="d-flex flex-column mt-2.5 " data-testid={`reply-${id}`} role="listitem">
|
||||
<Confirmation
|
||||
@@ -123,7 +128,7 @@ const Reply = ({ responseId }) => {
|
||||
<Avatar
|
||||
className={`ml-0.5 mt-0.5 border-0 ${colorClass ? `outline-${colorClass}` : 'outline-anonymous'}`}
|
||||
alt={author}
|
||||
src={authorAvatar?.imageUrlSmall}
|
||||
src={profileImage?.hasImage ? profileImage?.imageUrlSmall : authorAvatar?.imageUrlSmall}
|
||||
style={{
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
@@ -168,7 +173,7 @@ const Reply = ({ responseId }) => {
|
||||
<HTMLLoader
|
||||
componentId="reply"
|
||||
htmlNode={renderedBody}
|
||||
cssClassName="html-loader text-break font-style text-primary-500"
|
||||
cssClassName="html-loader text-break font-style text-gray-700"
|
||||
testId={id}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -30,6 +30,16 @@ Factory.define('comment')
|
||||
parent_id: null,
|
||||
children: [],
|
||||
abuse_flagged_any_user: false,
|
||||
users: {
|
||||
edx: {
|
||||
profile: {
|
||||
image: {
|
||||
hasImage: true,
|
||||
imageUrlSmall: 'http://test.site/default-avatar-small.png',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Factory.define('commentsResult')
|
||||
|
||||
@@ -143,6 +143,7 @@ const Post = ({ handleAddResponseButton, openRestrictionDialogue }) => {
|
||||
onClose={hideDeleteConfirmation}
|
||||
confirmAction={handleDeleteConfirmation}
|
||||
closeButtonVariant="tertiary"
|
||||
confirmButtonVariant="danger"
|
||||
confirmButtonText={intl.formatMessage(messages.deleteConfirmationDelete)}
|
||||
/>
|
||||
{!abuseFlagged && (
|
||||
@@ -152,7 +153,6 @@ const Post = ({ handleAddResponseButton, openRestrictionDialogue }) => {
|
||||
description={intl.formatMessage(messages.reportPostDescription)}
|
||||
onClose={hideReportConfirmation}
|
||||
confirmAction={handleReportConfirmation}
|
||||
confirmButtonVariant="danger"
|
||||
/>
|
||||
)}
|
||||
<HoverCard
|
||||
@@ -189,7 +189,7 @@ const Post = ({ handleAddResponseButton, openRestrictionDialogue }) => {
|
||||
title={title}
|
||||
postUsers={postUsers}
|
||||
/>
|
||||
<div className="d-flex mt-14px text-break font-style text-primary-500">
|
||||
<div className="d-flex mt-14px text-break font-style text-gray-700">
|
||||
<HTMLLoader htmlNode={renderedBody} componentId="post" cssClassName="html-loader w-100" testId={postId} />
|
||||
</div>
|
||||
{(topicContext || topic) && (
|
||||
|
||||
157
src/discussions/posts/post/Post.test.jsx
Normal file
157
src/discussions/posts/post/Post.test.jsx
Normal file
@@ -0,0 +1,157 @@
|
||||
import React from 'react';
|
||||
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import Post from './Post';
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
...jest.requireActual('react-redux'),
|
||||
useDispatch: () => jest.fn(),
|
||||
useSelector: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: () => ({ pathname: '/test' }),
|
||||
useNavigate: () => jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@edx/frontend-platform/i18n', () => ({
|
||||
useIntl: () => ({
|
||||
formatMessage: (msg) => ((msg && msg.defaultMessage) ? msg.defaultMessage : 'test-message'),
|
||||
}),
|
||||
defineMessages: (msgs) => msgs,
|
||||
}));
|
||||
|
||||
jest.mock('@openedx/paragon', () => {
|
||||
const actual = jest.requireActual('@openedx/paragon');
|
||||
// eslint-disable-next-line global-require
|
||||
const PropTypes = require('prop-types');
|
||||
|
||||
const MockHyperlink = ({ children }) => <div>{children}</div>;
|
||||
MockHyperlink.propTypes = { children: PropTypes.node.isRequired };
|
||||
|
||||
return {
|
||||
...actual,
|
||||
Hyperlink: MockHyperlink,
|
||||
useToggle: actual.useToggle,
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../common', () => {
|
||||
// eslint-disable-next-line global-require
|
||||
const PropTypes = require('prop-types');
|
||||
|
||||
const MockConfirmation = ({ confirmButtonVariant, isOpen }) => {
|
||||
if (!isOpen) { return null; }
|
||||
return (
|
||||
<div data-testid="mock-confirmation" data-variant={confirmButtonVariant}>
|
||||
Mock Confirmation
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
MockConfirmation.propTypes = {
|
||||
confirmButtonVariant: PropTypes.string,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react/prop-types
|
||||
const MockAlertBanner = () => <div />;
|
||||
|
||||
return {
|
||||
Confirmation: MockConfirmation,
|
||||
AlertBanner: MockAlertBanner,
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('./PostHeader', () => function MockPostHeader() { return <div>PostHeader</div>; });
|
||||
jest.mock('./PostFooter', () => function MockPostFooter() { return <div>PostFooter</div>; });
|
||||
jest.mock('./ClosePostReasonModal', () => function MockCloseModal() { return <div />; });
|
||||
jest.mock('../../../components/HTMLLoader', () => function MockLoader() { return <div>Body Content</div>; });
|
||||
|
||||
jest.mock('../../common/HoverCard', () => {
|
||||
// eslint-disable-next-line global-require
|
||||
const { ContentActions } = require('../../../data/constants');
|
||||
// eslint-disable-next-line global-require
|
||||
const PropTypes = require('prop-types');
|
||||
|
||||
const MockHoverCard = ({ actionHandlers }) => (
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="trigger-delete"
|
||||
onClick={() => actionHandlers[ContentActions.DELETE] && actionHandlers[ContentActions.DELETE]()}
|
||||
>
|
||||
Delete Post
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="trigger-report"
|
||||
onClick={() => actionHandlers[ContentActions.REPORT] && actionHandlers[ContentActions.REPORT]()}
|
||||
>
|
||||
Report Post
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
MockHoverCard.propTypes = {
|
||||
actionHandlers: PropTypes.shape({}).isRequired,
|
||||
};
|
||||
|
||||
return MockHoverCard;
|
||||
});
|
||||
|
||||
describe('Post Component - Delete/Report Confirmation', () => {
|
||||
const mockPostId = '123';
|
||||
|
||||
beforeEach(() => {
|
||||
useSelector.mockReturnValue({
|
||||
topicId: 'topic-1',
|
||||
abuseFlagged: false,
|
||||
closed: false,
|
||||
pinned: false,
|
||||
voted: false,
|
||||
following: false,
|
||||
author: {},
|
||||
title: 'Test Post',
|
||||
renderedBody: '<div>Hello</div>',
|
||||
users: {},
|
||||
});
|
||||
});
|
||||
|
||||
const renderPost = () => {
|
||||
// eslint-disable-next-line global-require
|
||||
const DiscussionContext = require('../../common/context').default;
|
||||
|
||||
return render(
|
||||
<DiscussionContext.Provider value={{ postId: mockPostId, enableInContextSidebar: false, courseId: 'course-1' }}>
|
||||
<Post
|
||||
handleAddResponseButton={jest.fn()}
|
||||
openRestrictionDialogue={jest.fn()}
|
||||
/>
|
||||
</DiscussionContext.Provider>,
|
||||
);
|
||||
};
|
||||
|
||||
it('passes "danger" variant to Confirmation modal when deleting a post', () => {
|
||||
renderPost();
|
||||
|
||||
const deleteBtn = screen.getByTestId('trigger-delete');
|
||||
fireEvent.click(deleteBtn);
|
||||
|
||||
const confirmation = screen.getByTestId('mock-confirmation');
|
||||
expect(confirmation).toHaveAttribute('data-variant', 'danger');
|
||||
});
|
||||
|
||||
it('does NOT pass "danger" variant to Confirmation modal when reporting a post', () => {
|
||||
renderPost();
|
||||
|
||||
const reportBtn = screen.getByTestId('trigger-report');
|
||||
fireEvent.click(reportBtn);
|
||||
|
||||
const confirmation = screen.getByTestId('mock-confirmation');
|
||||
expect(confirmation).not.toHaveAttribute('data-variant', 'danger');
|
||||
});
|
||||
});
|
||||
@@ -126,7 +126,7 @@ const PostHeader = ({
|
||||
</div>
|
||||
) : (
|
||||
<h5
|
||||
className="mb-0 font-style text-primary-500"
|
||||
className="mb-0 font-style"
|
||||
style={{ lineHeight: '21px' }}
|
||||
aria-level="1"
|
||||
tabIndex="-1"
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { mergeConfig } from '@edx/frontend-platform';
|
||||
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
import 'babel-polyfill';
|
||||
|
||||
@@ -66,3 +68,10 @@ global.ResizeObserver = jest.fn().mockImplementation(() => ({
|
||||
}));
|
||||
|
||||
jest.setTimeout(1000000);
|
||||
|
||||
mergeConfig({
|
||||
LEARNING_BASE_URL: process.env.LEARNING_BASE_URL,
|
||||
LEARNER_FEEDBACK_URL: process.env.LEARNER_FEEDBACK_URL,
|
||||
STAFF_FEEDBACK_URL: process.env.STAFF_FEEDBACK_URL,
|
||||
ENABLE_PROFILE_IMAGE: process.env.ENABLE_PROFILE_IMAGE || 'false',
|
||||
}, 'DiscussionsConfig');
|
||||
|
||||
Reference in New Issue
Block a user