feat: Profile image on user posts (#574)
* feat: add env variable to display image * feat: refactor after review, updated tests
This commit is contained in:
1
.env
1
.env
@@ -22,5 +22,6 @@ USER_INFO_COOKIE_NAME=''
|
|||||||
SUPPORT_URL=''
|
SUPPORT_URL=''
|
||||||
LEARNER_FEEDBACK_URL=''
|
LEARNER_FEEDBACK_URL=''
|
||||||
STAFF_FEEDBACK_URL=''
|
STAFF_FEEDBACK_URL=''
|
||||||
|
ENABLE_PROFILE_IMAGE=''
|
||||||
# Fallback in local style files
|
# Fallback in local style files
|
||||||
PARAGON_THEME_URLS={}
|
PARAGON_THEME_URLS={}
|
||||||
|
|||||||
@@ -23,5 +23,6 @@ USER_INFO_COOKIE_NAME='edx-user-info'
|
|||||||
SUPPORT_URL='https://support.edx.org'
|
SUPPORT_URL='https://support.edx.org'
|
||||||
LEARNER_FEEDBACK_URL=''
|
LEARNER_FEEDBACK_URL=''
|
||||||
STAFF_FEEDBACK_URL=''
|
STAFF_FEEDBACK_URL=''
|
||||||
|
ENABLE_PROFILE_IMAGE=''
|
||||||
# Fallback in local style files
|
# Fallback in local style files
|
||||||
PARAGON_THEME_URLS={}
|
PARAGON_THEME_URLS={}
|
||||||
|
|||||||
@@ -21,3 +21,4 @@ USER_INFO_COOKIE_NAME='edx-user-info'
|
|||||||
SUPPORT_URL='https://support.edx.org'
|
SUPPORT_URL='https://support.edx.org'
|
||||||
LEARNER_FEEDBACK_URL=''
|
LEARNER_FEEDBACK_URL=''
|
||||||
STAFF_FEEDBACK_URL=''
|
STAFF_FEEDBACK_URL=''
|
||||||
|
ENABLE_PROFILE_IMAGE=''
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ const Comment = ({
|
|||||||
const {
|
const {
|
||||||
id, parentId, childCount, abuseFlagged, endorsed, threadId, endorsedAt, endorsedBy, endorsedByLabel, renderedBody,
|
id, parentId, childCount, abuseFlagged, endorsed, threadId, endorsedAt, endorsedBy, endorsedByLabel, renderedBody,
|
||||||
voted, following, voteCount, authorLabel, author, createdAt, lastEdit, rawBody, closed, closedBy, closeReason,
|
voted, following, voteCount, authorLabel, author, createdAt, lastEdit, rawBody, closed, closedBy, closeReason,
|
||||||
editByLabel, closedByLabel,
|
editByLabel, closedByLabel, users: postUsers,
|
||||||
} = comment;
|
} = comment;
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const hasChildren = childCount > 0;
|
const hasChildren = childCount > 0;
|
||||||
@@ -209,6 +209,7 @@ const Comment = ({
|
|||||||
closed={closed}
|
closed={closed}
|
||||||
createdAt={createdAt}
|
createdAt={createdAt}
|
||||||
lastEdit={lastEdit}
|
lastEdit={lastEdit}
|
||||||
|
postUsers={postUsers}
|
||||||
/>
|
/>
|
||||||
{isEditing ? (
|
{isEditing ? (
|
||||||
<CommentEditor
|
<CommentEditor
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { Avatar } from '@openedx/paragon';
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
|
|
||||||
import { AvatarOutlineAndLabelColors } from '../../../../data/constants';
|
import { AvatarOutlineAndLabelColors } from '../../../../data/constants';
|
||||||
import { AuthorLabel } from '../../../common';
|
import { AuthorLabel } from '../../../common';
|
||||||
import { useAlertBannerVisible } from '../../../data/hooks';
|
import { useAlertBannerVisible } from '../../../data/hooks';
|
||||||
@@ -17,6 +19,7 @@ const CommentHeader = ({
|
|||||||
closed,
|
closed,
|
||||||
createdAt,
|
createdAt,
|
||||||
lastEdit,
|
lastEdit,
|
||||||
|
postUsers,
|
||||||
}) => {
|
}) => {
|
||||||
const colorClass = AvatarOutlineAndLabelColors[authorLabel];
|
const colorClass = AvatarOutlineAndLabelColors[authorLabel];
|
||||||
const hasAnyAlert = useAlertBannerVisible({
|
const hasAnyAlert = useAlertBannerVisible({
|
||||||
@@ -27,6 +30,10 @@ const CommentHeader = ({
|
|||||||
});
|
});
|
||||||
const authorAvatar = useSelector(selectAuthorAvatar(author));
|
const authorAvatar = useSelector(selectAuthorAvatar(author));
|
||||||
|
|
||||||
|
const profileImage = getConfig()?.ENABLE_PROFILE_IMAGE === 'true'
|
||||||
|
? Object.values(postUsers ?? {})[0]?.profile?.image
|
||||||
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames('d-flex flex-row justify-content-between', {
|
<div className={classNames('d-flex flex-row justify-content-between', {
|
||||||
'mt-2': hasAnyAlert,
|
'mt-2': hasAnyAlert,
|
||||||
@@ -36,7 +43,7 @@ const CommentHeader = ({
|
|||||||
<Avatar
|
<Avatar
|
||||||
className={`border-0 ml-0.5 mr-2.5 ${colorClass ? `outline-${colorClass}` : 'outline-anonymous'}`}
|
className={`border-0 ml-0.5 mr-2.5 ${colorClass ? `outline-${colorClass}` : 'outline-anonymous'}`}
|
||||||
alt={author}
|
alt={author}
|
||||||
src={authorAvatar?.imageUrlSmall}
|
src={profileImage?.hasImage ? profileImage?.imageUrlSmall : authorAvatar}
|
||||||
style={{
|
style={{
|
||||||
width: '32px',
|
width: '32px',
|
||||||
height: '32px',
|
height: '32px',
|
||||||
@@ -65,6 +72,7 @@ CommentHeader.propTypes = {
|
|||||||
editorUsername: PropTypes.string,
|
editorUsername: PropTypes.string,
|
||||||
reason: PropTypes.string,
|
reason: PropTypes.string,
|
||||||
}),
|
}),
|
||||||
|
postUsers: PropTypes.shape({}).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
CommentHeader.defaultProps = {
|
CommentHeader.defaultProps = {
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import { IntlProvider } from 'react-intl';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { MemoryRouter } from 'react-router';
|
||||||
|
|
||||||
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
|
|
||||||
|
import DiscussionContext from '../../../common/context';
|
||||||
|
import { useAlertBannerVisible } from '../../../data/hooks';
|
||||||
|
import CommentHeader from './CommentHeader';
|
||||||
|
|
||||||
|
jest.mock('react-redux', () => ({ useSelector: jest.fn() }));
|
||||||
|
jest.mock('@edx/frontend-platform', () => ({ getConfig: jest.fn() }));
|
||||||
|
jest.mock('../../../data/hooks', () => ({ useAlertBannerVisible: jest.fn() }));
|
||||||
|
|
||||||
|
const defaultProps = {
|
||||||
|
author: 'test-user',
|
||||||
|
authorLabel: 'staff',
|
||||||
|
abuseFlagged: false,
|
||||||
|
closed: false,
|
||||||
|
createdAt: '2025-09-23T10:00:00Z',
|
||||||
|
lastEdit: null,
|
||||||
|
postUsers: {
|
||||||
|
'test-user': {
|
||||||
|
profile: { image: { hasImage: true, imageUrlSmall: 'http://avatar.test/img.png' } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderComponent = (
|
||||||
|
props = {},
|
||||||
|
ctx = { courseId: 'course-v1:edX+DemoX+Demo_Course', enableInContextSidebar: false },
|
||||||
|
) => render(
|
||||||
|
<IntlProvider locale="en">
|
||||||
|
<MemoryRouter>
|
||||||
|
<DiscussionContext.Provider value={ctx}>
|
||||||
|
<CommentHeader {...defaultProps} {...props} />
|
||||||
|
</DiscussionContext.Provider>
|
||||||
|
</MemoryRouter>
|
||||||
|
</IntlProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('CommentHeader', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
useSelector.mockReturnValue('http://fallback-avatar.png');
|
||||||
|
useAlertBannerVisible.mockReturnValue(false);
|
||||||
|
getConfig.mockReturnValue({ ENABLE_PROFILE_IMAGE: 'true' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders author and avatar with profile image when ENABLE_PROFILE_IMAGE=true', () => {
|
||||||
|
renderComponent();
|
||||||
|
const avatarImg = screen.getByAltText('test-user');
|
||||||
|
expect(avatarImg).toHaveAttribute('src', 'http://avatar.test/img.png');
|
||||||
|
expect(screen.getByText('test-user')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses redux avatar if profile image is disabled by config', () => {
|
||||||
|
getConfig.mockReturnValue({ ENABLE_PROFILE_IMAGE: 'false' });
|
||||||
|
const { container } = renderComponent();
|
||||||
|
const avatar = container.querySelector('.outline-staff, .outline-anonymous');
|
||||||
|
expect(avatar).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('applies anonymous class if no color class is found', () => {
|
||||||
|
const { container } = renderComponent({ authorLabel: null });
|
||||||
|
expect(container.querySelector('.outline-anonymous')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds margin-top if alert banner is visible', () => {
|
||||||
|
useAlertBannerVisible.mockReturnValue(true);
|
||||||
|
const { container } = renderComponent();
|
||||||
|
expect(container.firstChild).toHaveClass('mt-2');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -33,7 +33,7 @@ const Post = ({ handleAddResponseButton, openRestrictionDialogue }) => {
|
|||||||
const {
|
const {
|
||||||
topicId, abuseFlagged, closed, pinned, voted, hasEndorsed, following, closedBy, voteCount, groupId, groupName,
|
topicId, abuseFlagged, closed, pinned, voted, hasEndorsed, following, closedBy, voteCount, groupId, groupName,
|
||||||
closeReason, authorLabel, type: postType, author, title, createdAt, renderedBody, lastEdit, editByLabel,
|
closeReason, authorLabel, type: postType, author, title, createdAt, renderedBody, lastEdit, editByLabel,
|
||||||
closedByLabel,
|
closedByLabel, users: postUsers,
|
||||||
} = useSelector(selectThread(postId));
|
} = useSelector(selectThread(postId));
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@@ -187,6 +187,7 @@ const Post = ({ handleAddResponseButton, openRestrictionDialogue }) => {
|
|||||||
lastEdit={lastEdit}
|
lastEdit={lastEdit}
|
||||||
postType={postType}
|
postType={postType}
|
||||||
title={title}
|
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-primary-500">
|
||||||
<HTMLLoader htmlNode={renderedBody} componentId="post" cssClassName="html-loader w-100" testId={postId} />
|
<HTMLLoader htmlNode={renderedBody} componentId="post" cssClassName="html-loader w-100" testId={postId} />
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Question } from '@openedx/paragon/icons';
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
import { AvatarOutlineAndLabelColors, ThreadType } from '../../../data/constants';
|
import { AvatarOutlineAndLabelColors, ThreadType } from '../../../data/constants';
|
||||||
@@ -15,7 +16,7 @@ import { selectAuthorAvatar } from '../data/selectors';
|
|||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
export const PostAvatar = React.memo(({
|
export const PostAvatar = React.memo(({
|
||||||
author, postType, authorLabel, fromPostLink, read,
|
author, postType, authorLabel, fromPostLink, read, postUsers,
|
||||||
}) => {
|
}) => {
|
||||||
const outlineColor = AvatarOutlineAndLabelColors[authorLabel];
|
const outlineColor = AvatarOutlineAndLabelColors[authorLabel];
|
||||||
const authorAvatars = useSelector(selectAuthorAvatar(author));
|
const authorAvatars = useSelector(selectAuthorAvatar(author));
|
||||||
@@ -40,6 +41,10 @@ export const PostAvatar = React.memo(({
|
|||||||
return spacing;
|
return spacing;
|
||||||
}, [postType]);
|
}, [postType]);
|
||||||
|
|
||||||
|
const profileImage = getConfig()?.ENABLE_PROFILE_IMAGE === 'true'
|
||||||
|
? Object.values(postUsers ?? {})[0]?.profile?.image
|
||||||
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={avatarSpacing}>
|
<div className={avatarSpacing}>
|
||||||
{postType === ThreadType.QUESTION && (
|
{postType === ThreadType.QUESTION && (
|
||||||
@@ -62,8 +67,8 @@ export const PostAvatar = React.memo(({
|
|||||||
height: avatarSize,
|
height: avatarSize,
|
||||||
width: avatarSize,
|
width: avatarSize,
|
||||||
}}
|
}}
|
||||||
|
src={profileImage?.hasImage ? profileImage?.imageUrlSmall : authorAvatars?.imageUrlSmall}
|
||||||
alt={author}
|
alt={author}
|
||||||
src={authorAvatars?.imageUrlSmall}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -75,6 +80,7 @@ PostAvatar.propTypes = {
|
|||||||
authorLabel: PropTypes.string,
|
authorLabel: PropTypes.string,
|
||||||
fromPostLink: PropTypes.bool,
|
fromPostLink: PropTypes.bool,
|
||||||
read: PropTypes.bool,
|
read: PropTypes.bool,
|
||||||
|
postUsers: PropTypes.shape({}).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
PostAvatar.defaultProps = {
|
PostAvatar.defaultProps = {
|
||||||
@@ -94,6 +100,7 @@ const PostHeader = ({
|
|||||||
title,
|
title,
|
||||||
postType,
|
postType,
|
||||||
preview,
|
preview,
|
||||||
|
postUsers,
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const showAnsweredBadge = preview && hasEndorsed && postType === ThreadType.QUESTION;
|
const showAnsweredBadge = preview && hasEndorsed && postType === ThreadType.QUESTION;
|
||||||
@@ -105,7 +112,7 @@ const PostHeader = ({
|
|||||||
return (
|
return (
|
||||||
<div className={classNames('d-flex flex-fill mw-100', { 'mt-10px': hasAnyAlert && !preview })}>
|
<div className={classNames('d-flex flex-fill mw-100', { 'mt-10px': hasAnyAlert && !preview })}>
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<PostAvatar postType={postType} author={author} authorLabel={authorLabel} />
|
<PostAvatar postType={postType} author={author} authorLabel={authorLabel} postUsers={postUsers} />
|
||||||
</div>
|
</div>
|
||||||
<div className="align-items-center d-flex flex-row">
|
<div className="align-items-center d-flex flex-row">
|
||||||
<div className="d-flex flex-column justify-content-start mw-100">
|
<div className="d-flex flex-column justify-content-start mw-100">
|
||||||
@@ -155,6 +162,7 @@ PostHeader.propTypes = {
|
|||||||
reason: PropTypes.string,
|
reason: PropTypes.string,
|
||||||
}),
|
}),
|
||||||
closed: PropTypes.bool,
|
closed: PropTypes.bool,
|
||||||
|
postUsers: PropTypes.shape({}).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
PostHeader.defaultProps = {
|
PostHeader.defaultProps = {
|
||||||
|
|||||||
148
src/discussions/posts/post/PostHeader.test.jsx
Normal file
148
src/discussions/posts/post/PostHeader.test.jsx
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import { IntlProvider } from 'react-intl';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
|
|
||||||
|
import { AvatarOutlineAndLabelColors, ThreadType } from '../../../data/constants';
|
||||||
|
import DiscussionContext from '../../common/context';
|
||||||
|
import { useAlertBannerVisible } from '../../data/hooks';
|
||||||
|
import PostHeader, { PostAvatar } from './PostHeader';
|
||||||
|
|
||||||
|
jest.mock('react-redux', () => ({ useSelector: jest.fn() }));
|
||||||
|
jest.mock('@edx/frontend-platform', () => ({ getConfig: jest.fn() }));
|
||||||
|
jest.mock('../../data/hooks', () => ({ useAlertBannerVisible: jest.fn() }));
|
||||||
|
|
||||||
|
const defaultPostUsers = {
|
||||||
|
'test-user': {
|
||||||
|
profile: { image: { hasImage: true, imageUrlSmall: 'http://avatar.test/img.png' } },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const ctxValue = { courseId: 'course-v1:edX+DemoX+Demo_Course', enableInContextSidebar: false };
|
||||||
|
|
||||||
|
function renderWithContext(ui) {
|
||||||
|
return render(
|
||||||
|
<IntlProvider locale="en">
|
||||||
|
<MemoryRouter>
|
||||||
|
<DiscussionContext.Provider value={ctxValue}>
|
||||||
|
{ui}
|
||||||
|
</DiscussionContext.Provider>
|
||||||
|
</MemoryRouter>
|
||||||
|
</IntlProvider>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('PostAvatar', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
useSelector.mockReturnValue({ imageUrlSmall: 'http://redux-avatar.png' });
|
||||||
|
getConfig.mockReturnValue({ ENABLE_PROFILE_IMAGE: 'true' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders avatar with profile image when ENABLE_PROFILE_IMAGE=true', () => {
|
||||||
|
renderWithContext(
|
||||||
|
<PostAvatar
|
||||||
|
author="test-user"
|
||||||
|
postType={ThreadType.DISCUSSION}
|
||||||
|
authorLabel="Staff"
|
||||||
|
postUsers={defaultPostUsers}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const avatarImg = screen.getByAltText('test-user');
|
||||||
|
expect(avatarImg).toHaveAttribute('src', 'http://avatar.test/img.png');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to redux avatar if no profile image', () => {
|
||||||
|
renderWithContext(
|
||||||
|
<PostAvatar
|
||||||
|
author="test-user"
|
||||||
|
postType={ThreadType.DISCUSSION}
|
||||||
|
authorLabel="Staff"
|
||||||
|
postUsers={{ 'test-user': { profile: { image: { hasImage: false, imageUrlSmall: null } } } }}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const avatarImg = screen.getByAltText('test-user');
|
||||||
|
expect(avatarImg).toHaveAttribute('src', 'http://redux-avatar.png');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('applies Staff outline class if authorLabel provided', () => {
|
||||||
|
renderWithContext(
|
||||||
|
<PostAvatar
|
||||||
|
author="test-user"
|
||||||
|
postType={ThreadType.DISCUSSION}
|
||||||
|
authorLabel="Staff"
|
||||||
|
postUsers={defaultPostUsers}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const avatar = screen.getByAltText('test-user');
|
||||||
|
expect(avatar.className).toMatch(`outline-${AvatarOutlineAndLabelColors.Staff}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('applies anonymous outline class if no authorLabel', () => {
|
||||||
|
const { container } = renderWithContext(
|
||||||
|
<PostAvatar
|
||||||
|
author="test-user"
|
||||||
|
postType={ThreadType.DISCUSSION}
|
||||||
|
authorLabel={null}
|
||||||
|
postUsers={defaultPostUsers}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(container.querySelector('.outline-anonymous')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('PostHeader', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
useSelector.mockReturnValue({ imageUrlSmall: 'http://redux-avatar.png' });
|
||||||
|
getConfig.mockReturnValue({ ENABLE_PROFILE_IMAGE: 'true' });
|
||||||
|
useAlertBannerVisible.mockReturnValue(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderHeader = (props = {}) => renderWithContext(
|
||||||
|
<PostHeader
|
||||||
|
author="test-user"
|
||||||
|
authorLabel="Staff"
|
||||||
|
abuseFlagged={false}
|
||||||
|
closed={false}
|
||||||
|
createdAt="2025-09-23T10:00:00Z"
|
||||||
|
lastEdit={null}
|
||||||
|
postUsers={defaultPostUsers}
|
||||||
|
title="Sample Post Title"
|
||||||
|
postType={ThreadType.DISCUSSION}
|
||||||
|
hasEndorsed={false}
|
||||||
|
preview={false}
|
||||||
|
{...props}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
it('renders post title and author', () => {
|
||||||
|
renderHeader();
|
||||||
|
expect(screen.getByText('Sample Post Title')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('test-user')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds answered badge for endorsed QUESTION preview', () => {
|
||||||
|
renderHeader({ postType: ThreadType.QUESTION, hasEndorsed: true, preview: true });
|
||||||
|
expect(screen.getByText(/answered/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds mt-10px class if alert banner is visible', () => {
|
||||||
|
useAlertBannerVisible.mockReturnValue(true);
|
||||||
|
const { container } = renderHeader({ preview: false });
|
||||||
|
expect(container.firstChild).toHaveClass('mt-10px');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to anonymous if no author provided', () => {
|
||||||
|
renderHeader({ author: '' });
|
||||||
|
expect(screen.getByText(/anonymous/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -36,6 +36,7 @@ const PostLink = ({
|
|||||||
const {
|
const {
|
||||||
topicId, hasEndorsed, type, author, authorLabel, abuseFlagged, abuseFlaggedCount, read, commentCount,
|
topicId, hasEndorsed, type, author, authorLabel, abuseFlagged, abuseFlaggedCount, read, commentCount,
|
||||||
unreadCommentCount, id, pinned, previewBody, title, voted, voteCount, following, groupId, groupName, createdAt,
|
unreadCommentCount, id, pinned, previewBody, title, voted, voteCount, following, groupId, groupName, createdAt,
|
||||||
|
users: postUsers,
|
||||||
} = useSelector(selectThread(postId));
|
} = useSelector(selectThread(postId));
|
||||||
const { pathname } = discussionsPath(Routes.COMMENTS.PAGES[page], {
|
const { pathname } = discussionsPath(Routes.COMMENTS.PAGES[page], {
|
||||||
0: enableInContextSidebar ? 'in-context' : undefined,
|
0: enableInContextSidebar ? 'in-context' : undefined,
|
||||||
@@ -83,6 +84,7 @@ const PostLink = ({
|
|||||||
authorLabel={authorLabel}
|
authorLabel={authorLabel}
|
||||||
fromPostLink
|
fromPostLink
|
||||||
read={isPostRead}
|
read={isPostRead}
|
||||||
|
postUsers={postUsers}
|
||||||
/>
|
/>
|
||||||
<div className="d-flex flex-column flex-fill" style={{ minWidth: 0 }}>
|
<div className="d-flex flex-column flex-fill" style={{ minWidth: 0 }}>
|
||||||
<div className="d-flex flex-column justify-content-start mw-100 flex-fill" style={{ marginBottom: '-3px' }}>
|
<div className="d-flex flex-column justify-content-start mw-100 flex-fill" style={{ marginBottom: '-3px' }}>
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ initialize({
|
|||||||
LEARNING_BASE_URL: process.env.LEARNING_BASE_URL,
|
LEARNING_BASE_URL: process.env.LEARNING_BASE_URL,
|
||||||
LEARNER_FEEDBACK_URL: process.env.LEARNER_FEEDBACK_URL,
|
LEARNER_FEEDBACK_URL: process.env.LEARNER_FEEDBACK_URL,
|
||||||
STAFF_FEEDBACK_URL: process.env.STAFF_FEEDBACK_URL,
|
STAFF_FEEDBACK_URL: process.env.STAFF_FEEDBACK_URL,
|
||||||
|
ENABLE_PROFILE_IMAGE: process.env.ENABLE_PROFILE_IMAGE,
|
||||||
}, 'DiscussionsConfig');
|
}, 'DiscussionsConfig');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user