test: fixed test cases after MFE optimization (#521)

* test: fixed ActionDropDown test cases

* test: fixed AlertBanner and EndorsedAlertBanner test cases

* test: fixed LearnerFooter test cases

* test: fixed LearnersView failed test cases

* test: fixed PostFooter failed test cases

* test: fixed PostLink failed test cases

* test: fixed LearnerPostsView failed test cases

* test: fixed console error and warnings in PostEditor

* test: fixed Post anonymously test condition
This commit is contained in:
Awais Ansari
2023-05-16 14:45:40 +05:00
committed by GitHub
parent 6d6c61dec2
commit 4f0b5f9c3d
12 changed files with 336 additions and 331 deletions

View File

@@ -1,16 +1,24 @@
import {
fireEvent, render, screen, waitFor,
} from '@testing-library/react';
import MockAdapter from 'axios-mock-adapter';
import { act } from 'react-dom/test-utils';
import { IntlProvider } from 'react-intl';
import { Factory } from 'rosie';
import { camelCaseObject, initializeMockApp, snakeCaseObject } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { AppProvider } from '@edx/frontend-platform/react';
import { ContentActions } from '../../data/constants';
import { initializeStore } from '../../store';
import { executeThunk } from '../../test-utils';
import messages from '../messages';
import { getCommentsApiUrl } from '../post-comments/data/api';
import { addComment, fetchThreadComments } from '../post-comments/data/thunks';
import { PostCommentsContext } from '../post-comments/postCommentsContext';
import { getThreadsApiUrl } from '../posts/data/api';
import { fetchThread } from '../posts/data/thunks';
import { ACTIONS_LIST } from '../utils';
import ActionsDropdown from './ActionsDropdown';
@@ -18,107 +26,151 @@ import '../post-comments/data/__factories__';
import '../posts/data/__factories__';
let store;
let axiosMock;
const commentsApiUrl = getCommentsApiUrl();
const threadsApiUrl = getThreadsApiUrl();
const discussionThreadId = 'thread-1';
const questionThreadId = 'thread-2';
const commentContent = 'This is a comment for thread-1';
let discussionThread;
let questionThread;
let comment;
function buildTestContent(buildParams, testMeta) {
const buildTestContent = (buildParams, testMeta) => {
const buildParamsSnakeCase = snakeCaseObject(buildParams);
return [
{
testFor: 'comments',
...camelCaseObject(Factory.build('comment', { ...buildParamsSnakeCase }, null)),
...testMeta,
},
{
testFor: 'question threads',
...camelCaseObject(Factory.build('thread', { ...buildParamsSnakeCase, type: 'question' }, null)),
...testMeta,
},
{
discussionThread = Factory.build('thread', { ...buildParamsSnakeCase, id: discussionThreadId }, null);
questionThread = Factory.build('thread', { ...buildParamsSnakeCase, id: questionThreadId }, null);
comment = Factory.build('comment', { ...buildParamsSnakeCase, thread_id: discussionThreadId }, null);
return {
discussion: {
testFor: 'discussion threads',
...camelCaseObject(Factory.build('thread', { ...buildParamsSnakeCase, type: 'discussion' }, null)),
contentType: 'POST',
...camelCaseObject(discussionThread),
...testMeta,
},
];
}
question: {
testFor: 'question threads',
contentType: 'POST',
...camelCaseObject(questionThread),
...testMeta,
},
comment: {
testFor: 'comments',
contentType: 'COMMENT',
type: 'discussion',
...camelCaseObject(comment),
...testMeta,
},
};
};
const canPerformActionTestData = ACTIONS_LIST
.map(({ action, conditions, label: { defaultMessage } }) => {
const buildParams = {
const mockThreadAndComment = async (response) => {
axiosMock.onGet(`${threadsApiUrl}${discussionThreadId}/`).reply(200, response);
axiosMock.onGet(`${threadsApiUrl}${questionThreadId}/`).reply(200, response);
axiosMock.onGet(commentsApiUrl).reply(200, Factory.build('commentsResult'));
axiosMock.onPost(commentsApiUrl).reply(200, response);
await executeThunk(fetchThread(discussionThreadId), store.dispatch, store.getState);
await executeThunk(fetchThread(questionThreadId), store.dispatch, store.getState);
await executeThunk(fetchThreadComments(discussionThreadId), store.dispatch, store.getState);
await executeThunk(addComment(commentContent, discussionThreadId, null), store.dispatch, store.getState);
};
const canPerformActionTestData = ACTIONS_LIST.flatMap(({
id, action, conditions, label: { defaultMessage },
}) => {
const buildParams = { editable_fields: [action] };
if (conditions) {
Object.entries(conditions).forEach(([conditionKey, conditionValue]) => {
buildParams[conditionKey] = conditionValue;
});
}
const testContent = buildTestContent(buildParams, { label: defaultMessage, action });
switch (id) {
case 'answer':
case 'unanswer':
return [testContent.question];
case 'endorse':
case 'unendorse':
return [testContent.comment, testContent.discussion];
default:
return [testContent.discussion, testContent.question, testContent.comment];
}
});
const canNotPerformActionTestData = ACTIONS_LIST.flatMap(({ action, conditions, label: { defaultMessage } }) => {
const label = defaultMessage;
if (!conditions) {
const content = buildTestContent({ editable_fields: [] }, { reason: 'field is not editable', label: defaultMessage });
return [content.discussion, content.question, content.comment];
}
const reversedConditions = Object.fromEntries(Object.entries(conditions).map(([key, value]) => [key, !value]));
const content = {
// can edit field, but doesn't pass conditions
...buildTestContent({
editable_fields: [action],
};
if (conditions) {
Object.entries(conditions)
.forEach(([conditionKey, conditionValue]) => {
buildParams[conditionKey] = conditionValue;
});
}
return buildTestContent(buildParams, { label: defaultMessage, action });
})
.flat();
...reversedConditions,
}, { reason: 'field is editable but does not pass condition', label, action }),
const canNotPerformActionTestData = ACTIONS_LIST
.map(({ action, conditions, label: { defaultMessage } }) => {
const label = defaultMessage;
let content;
if (!conditions) {
content = buildTestContent({ editable_fields: [] }, { reason: 'field is not editable', label: defaultMessage });
} else {
const reversedConditions = Object.keys(conditions)
.reduce(
(results, key) => ({
...results,
[key]: !conditions[key],
}),
{},
);
// passes conditions, but can't edit field
...(action === ContentActions.DELETE ? {} : buildTestContent({
editable_fields: [],
...conditions,
}, { reason: 'passes conditions but field is not editable', label, action })),
content = [
// can edit field, but doesn't pass conditions
...buildTestContent({
editable_fields: [action],
...reversedConditions,
}, { reason: 'field is editable but does not pass condition', label, action }),
// passes conditions, but can't edit field
...(action === ContentActions.DELETE
? []
: buildTestContent({
editable_fields: [],
...conditions,
}, { reason: 'passes conditions but field is not editable', label, action })
),
// can't edit field, and doesn't pass conditions
...buildTestContent({
editable_fields: [],
...reversedConditions,
}, { reason: 'can not edit field and does not match conditions', label, action }),
];
}
return content;
})
.flat();
// can't edit field, and doesn't pass conditions
...buildTestContent({
editable_fields: [],
...reversedConditions,
}, { reason: 'can not edit field and does not match conditions', label, action }),
};
function renderComponent(
commentOrPost,
{ disabled = false, actionHandlers = {} } = {},
) {
return [content.discussion, content.question, content.comment];
});
const renderComponent = ({
id = '',
contentType = 'POST',
closed = false,
type = 'discussion',
postId = '',
disabled = false,
actionHandlers = {},
} = {}) => {
render(
<IntlProvider locale="en">
<AppProvider store={store}>
<ActionsDropdown
commentOrPost={commentOrPost}
disabled={disabled}
actionHandlers={actionHandlers}
/>
<PostCommentsContext.Provider value={{
isClosed: closed,
postType: type,
postId,
}}
>
<ActionsDropdown
id={id}
disabled={disabled}
actionHandlers={actionHandlers}
contentType={contentType}
/>
</PostCommentsContext.Provider>
</AppProvider>
</IntlProvider>,
);
}
};
const findOpenActionsDropdownButton = async () => (
screen.findByRole('button', { name: messages.actionsAlt.defaultMessage })
);
describe('ActionsDropdown', () => {
beforeEach(async () => {
beforeEach(() => {
initializeMockApp({
authenticatedUser: {
userId: 3,
@@ -128,10 +180,13 @@ describe('ActionsDropdown', () => {
},
});
store = initializeStore();
Factory.resetAll();
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
});
it.each(buildTestContent())('can open drop down if enabled', async (commentOrPost) => {
renderComponent(commentOrPost, { disabled: false });
it.each(Object.values(buildTestContent()))('can open drop down if enabled', async (commentOrPost) => {
await mockThreadAndComment(commentOrPost);
renderComponent({ ...commentOrPost });
const openButton = await findOpenActionsDropdownButton();
await act(async () => {
@@ -141,8 +196,9 @@ describe('ActionsDropdown', () => {
await waitFor(() => expect(screen.queryByTestId('actions-dropdown-modal-popup')).toBeInTheDocument());
});
it.each(buildTestContent())('can not open drop down if disabled', async (commentOrPost) => {
renderComponent(commentOrPost, { disabled: true });
it.each(Object.values(buildTestContent()))('can not open drop down if disabled', async (commentOrPost) => {
await mockThreadAndComment(commentOrPost);
renderComponent({ ...commentOrPost, disabled: true });
const openButton = await findOpenActionsDropdownButton();
await act(async () => {
@@ -153,11 +209,9 @@ describe('ActionsDropdown', () => {
});
it('copy link action should be visible on posts', async () => {
const commentOrPost = {
testFor: 'thread',
...camelCaseObject(Factory.build('thread', { editable_fields: ['copy_link'] }, null)),
};
renderComponent(commentOrPost, { disabled: false });
const discussionObject = buildTestContent({ editable_fields: ['copy_link'] }).discussion;
await mockThreadAndComment(discussionObject);
renderComponent({ ...camelCaseObject(discussionObject) });
const openButton = await findOpenActionsDropdownButton();
await act(async () => {
@@ -168,11 +222,9 @@ describe('ActionsDropdown', () => {
});
it('copy link action should not be visible on a comment', async () => {
const commentOrPost = {
testFor: 'comments',
...camelCaseObject(Factory.build('comment', {}, null)),
};
renderComponent(commentOrPost, { disabled: false });
const commentObject = buildTestContent().comment;
await mockThreadAndComment(commentObject);
renderComponent({ ...camelCaseObject(commentObject) });
const openButton = await findOpenActionsDropdownButton();
await act(async () => {
@@ -183,15 +235,13 @@ describe('ActionsDropdown', () => {
});
describe.each(canPerformActionTestData)('Actions', ({
testFor, action, label, reason, ...commentOrPost
testFor, action, label, ...commentOrPost
}) => {
describe(`for ${testFor}`, () => {
it(`can "${label}" when allowed`, async () => {
await mockThreadAndComment(commentOrPost);
const mockHandler = jest.fn();
renderComponent(
commentOrPost,
{ actionHandlers: { [action]: mockHandler } },
);
renderComponent({ ...commentOrPost, actionHandlers: { [action]: mockHandler } });
const openButton = await findOpenActionsDropdownButton();
await act(async () => {
@@ -214,7 +264,8 @@ describe('ActionsDropdown', () => {
}) => {
describe(`for ${testFor}`, () => {
it(`can't "${label}" when ${reason}`, async () => {
renderComponent(commentOrPost);
await mockThreadAndComment(commentOrPost);
renderComponent({ ...commentOrPost });
const openButton = await findOpenActionsDropdownButton();
await act(async () => {

View File

@@ -31,7 +31,14 @@ function renderComponent(
value={{ courseId: 'course-v1:edX+TestX+Test_Course' }}
>
<AlertBanner
content={content}
author={content.author}
abuseFlagged={content.abuseFlagged}
lastEdit={content.lastEdit}
closed={content.closed}
closedBy={content.closedBy}
closeReason={content.closeReason}
editByLabel={content.editByLabel}
closedByLabel={content.closedByLabel}
/>
</DiscussionContext.Provider>
</AppProvider>

View File

@@ -8,6 +8,7 @@ import { AppProvider } from '@edx/frontend-platform/react';
import { ThreadType } from '../../data/constants';
import { initializeStore } from '../../store';
import messages from '../post-comments/messages';
import { PostCommentsContext } from '../post-comments/postCommentsContext';
import { DiscussionContext } from './context';
import EndorsedAlertBanner from './EndorsedAlertBanner';
@@ -30,10 +31,18 @@ function renderComponent(
<DiscussionContext.Provider
value={{ courseId: 'course-v1:edX+DemoX+Demo_Course' }}
>
<EndorsedAlertBanner
content={content}
postType={postType}
/>
<PostCommentsContext.Provider value={{
postType,
}}
>
<EndorsedAlertBanner
endorsed={content.endorsed}
endorsedAt={content.endorsedAt}
endorsedBy={content.endorsedBy}
endorsedByLabel={content.endorsedByLabel}
/>
</PostCommentsContext.Provider>
</DiscussionContext.Provider>
</AppProvider>
</IntlProvider>,

View File

@@ -82,6 +82,10 @@ describe('Learner Posts View', () => {
await executeThunk(fetchUserPosts(courseId), store.dispatch, store.getState);
});
afterEach(() => {
axiosMock.reset();
});
test('Reported icon is visible to moderator for post with reported comment', async () => {
await setUpPrivilages(axiosMock, store, true);
await waitFor(() => { renderComponent(); });
@@ -112,7 +116,9 @@ describe('Learner Posts View', () => {
const backButton = screen.getByLabelText('Back');
await act(() => fireEvent.click(backButton));
await act(() => {
fireEvent.click(backButton);
});
await waitFor(() => {
expect(lastLocation.pathname.endsWith('/learners')).toBeTruthy();
});
@@ -128,15 +134,13 @@ describe('Learner Posts View', () => {
expect(recentActivity).toBeInTheDocument();
});
it(`should display a list of the interactive posts of a selected learner and the posts count
should be equal to the API response count.`, async () => {
it('should display a list of the interactive posts of a selected learner', async () => {
await waitFor(() => {
renderComponent();
});
const posts = await container.querySelectorAll('.discussion-post');
expect(posts).toHaveLength(2);
expect(posts).toHaveLength(Object.values(store.getState().threads.threadsById).length);
});
it.each([
@@ -162,9 +166,9 @@ describe('Learner Posts View', () => {
await act(async () => {
fireEvent.click(activity);
});
await waitFor(() => {
const learners = container.querySelectorAll('.discussion-post');
expect(learners).toHaveLength(result);
});
});
@@ -175,8 +179,7 @@ describe('Learner Posts View', () => {
{ searchBy: 'cohort-1', result: 2 },
])('successfully display learners by %s.', async ({ searchBy, result }) => {
await setUpPrivilages(axiosMock, store, true);
axiosMock.onGet(getCohortsApiUrl(courseId))
.reply(200, Factory.buildList('cohort', 3));
axiosMock.onGet(getCohortsApiUrl(courseId)).reply(200, Factory.buildList('cohort', 3));
await executeThunk(fetchCourseCohorts(courseId), store.dispatch, store.getState);
await renderComponent();
@@ -191,9 +194,9 @@ describe('Learner Posts View', () => {
await act(async () => {
fireEvent.click(cohort);
});
await waitFor(() => {
const learners = container.querySelectorAll('.discussion-post');
expect(learners).toHaveLength(result);
});
});
@@ -211,6 +214,6 @@ describe('Learner Posts View', () => {
});
expect(loadMoreButton).not.toBeInTheDocument();
expect(container.querySelectorAll('.discussion-post')).toHaveLength(2);
expect(container.querySelectorAll('.discussion-post')).toHaveLength(4);
});
});

View File

@@ -37,6 +37,7 @@ function renderComponent() {
<DiscussionContext.Provider value={{
page: 'learners',
learnerUsername: 'learner-1',
courseId,
}}
>
<MemoryRouter initialEntries={[`/${courseId}/`]}>

View File

@@ -1,5 +1,7 @@
import { Factory } from 'rosie';
import '../../../posts/data/__factories__';
Factory.define('learner')
.sequence('id')
.attr('username', ['id'], (id) => `learner-${id}`)
@@ -91,50 +93,8 @@ Factory.define('learnerPosts')
'results',
['abuseFlaggedCount', 'courseId'],
(abuseFlaggedCount, courseId) => {
const threads = [];
for (let i = 0; i < 2; i++) {
threads.push({
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
editable_fields: [
'abuse_flagged',
'following',
'group_id',
'raw_body',
'closed',
'read',
'title',
'topic_id',
'type',
'voted',
'pinned',
],
id: `post_id${i}`,
author: 'test_user',
author_label: 'Staff',
abuse_flagged: false,
can_delete: true,
voted: false,
vote_count: 1,
title: `Title ${i}`,
raw_body: `<p>body ${i}</p>`,
preview_body: `<p>body ${i}</p>`,
course_id: courseId,
group_id: null,
group_name: null,
abuse_flagged_count: abuseFlaggedCount,
following: false,
comment_count: 8,
unread_comment_count: 0,
endorsed_comment_list_url: null,
non_endorsed_comment_list_url: null,
read: false,
has_endorsed: false,
pinned: false,
topic_id: 'topic',
});
}
return threads;
const attrs = { course_id: courseId, abuse_flagged_count: abuseFlaggedCount };
return Factory.buildList('thread', 2, attrs);
},
)
.attr('pagination', [], () => ({

View File

@@ -23,8 +23,9 @@ const LearnerFooter = ({
<div className="d-flex align-items-center pt-1 mt-2.5" style={{ marginBottom: '2px' }}>
<OverlayTrigger
placement="right"
id={`learner-${username}-responses`}
overlay={(
<Tooltip>
<Tooltip id={`learner-${username}-responses`}>
<div className="d-flex flex-column align-items-start">
{intl.formatMessage(messages.allActivity)}
</div>
@@ -38,8 +39,9 @@ const LearnerFooter = ({
</OverlayTrigger>
<OverlayTrigger
placement="right"
id={`learner-${username}-posts`}
overlay={(
<Tooltip>
<Tooltip id={`learner-${username}-posts`}>
<div className="d-flex flex-column align-items-start">
{intl.formatMessage(messages.posts)}
</div>
@@ -54,8 +56,9 @@ const LearnerFooter = ({
{Boolean(canSeeLearnerReportedStats) && (
<OverlayTrigger
placement="right"
id={`learner-${username}-flags`}
overlay={(
<Tooltip id={`learner-${username}`}>
<Tooltip id={`learner-${username}-flags`}>
<div className="d-flex flex-column align-items-start">
{Boolean(activeFlags)
&& (

View File

@@ -16,7 +16,15 @@ function renderComponent(learner) {
return render(
<IntlProvider locale="en">
<AppProvider store={store}>
<LearnerFooter learner={learner} />
<LearnerFooter
learner={learner}
inactiveFlags={learner.inactiveFlags}
activeFlags={learner.activeFlags}
threads={learner.threads}
responses={learner.responses}
replies={learner.replies}
username={learner.username}
/>
</AppProvider>
</IntlProvider>,
);

View File

@@ -5,6 +5,7 @@ Factory.define('thread')
.sequence('title', ['topic_id'], (idx, topicId) => `This is Thread-${idx} in topic ${topicId}`)
.sequence('raw_body', (idx) => `Some contents for **thread number ${idx}**.`)
.sequence('rendered_body', (idx) => `Some contents for <b>thread number ${idx}</b>.`)
.sequence('preview_body', (idx) => `Some contents for <b>thread number ${idx}</b>.`)
.sequence('type', (idx) => (idx % 2 === 1 ? 'discussion' : 'question'))
.sequence('pinned', idx => (idx < 3))
.sequence('topic_id', idx => `test-topic-${(idx % 3)}`)

View File

@@ -71,13 +71,12 @@ describe('PostEditor', () => {
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
const cwtopics = Factory.buildList('category', 2);
Factory.reset('topic');
axiosMock
.onGet(topicsApiUrl)
.reply(200, {
courseware_topics: cwtopics,
non_courseware_topics: Factory.buildList('topic', 3, {}, { topicPrefix: 'ncw-' }),
});
axiosMock.onGet(topicsApiUrl).reply(200, {
courseware_topics: cwtopics,
non_courseware_topics: Factory.buildList('topic', 3, {}, { topicPrefix: 'ncw-' }),
});
});
describe.each([
{
allowAnonymous: false,
@@ -110,50 +109,35 @@ describe('PostEditor', () => {
});
await executeThunk(fetchCourseTopics(courseId), store.dispatch, store.getState);
});
test(
`new post when anonymous posts are ${allowAnonymous ? '' : 'not '}allowed and anonymous posts to peers are ${allowAnonymousToPeers ? '' : 'not '}allowed`,
async () => {
await renderComponent();
expect(screen.queryByRole('heading'))
.toHaveTextContent('Add a post');
expect(screen.queryAllByRole('radio'))
.toHaveLength(2);
// 2 categories with 4 subcategories each
expect(screen.queryAllByText(/category-\d-topic \d/))
.toHaveLength(8);
// 3 non courseare topics
expect(screen.queryAllByText(/ncw-topic \d/))
.toHaveLength(3);
test(`new post when anonymous posts are ${allowAnonymous ? '' : 'not'} allowed and anonymous posts to peers are ${
allowAnonymousToPeers ? '' : 'not'} allowed`, async () => {
await renderComponent();
expect(screen.queryByRole('heading')).toHaveTextContent('Add a post');
expect(screen.queryAllByRole('radio')).toHaveLength(2);
// 2 categories with 4 subcategories each
expect(screen.queryAllByText(/category-\d-topic \d/)).toHaveLength(8);
// 3 non courseare topics
expect(screen.queryAllByText(/ncw-topic \d/)).toHaveLength(3);
expect(screen.queryByText('cohort', { exact: false })).not.toBeInTheDocument();
expect(screen.queryByText('Post anonymously')).not.toBeInTheDocument();
if (allowAnonymousToPeers) {
expect(screen.queryByText('Post anonymously to peers')).toBeInTheDocument();
} else {
expect(screen.queryByText('Post anonymously to peers')).not.toBeInTheDocument();
}
});
expect(screen.queryByText('cohort', { exact: false }))
.not.toBeInTheDocument();
if (allowAnonymous) {
expect(screen.queryByText('Post anonymously'))
.not.toBeInTheDocument();
} else {
expect(screen.queryByText('Post anonymously'))
.not.toBeInTheDocument();
}
if (allowAnonymousToPeers) {
expect(screen.queryByText('Post anonymously to peers'))
.toBeInTheDocument();
} else {
expect(screen.queryByText('Post anonymously to peers'))
.not
.toBeInTheDocument();
}
},
);
test('selectThread is not called while creating a new post', async () => {
const mockSelectThread = jest.fn();
jest.mock('../data/selectors', () => ({
selectThread: mockSelectThread,
}));
await renderComponent();
expect(mockSelectThread)
.not
.toHaveBeenCalled();
expect(mockSelectThread).not.toHaveBeenCalled();
});
});
@@ -162,8 +146,7 @@ describe('PostEditor', () => {
const dividedcw = ['category-1-topic-2', 'category-2-topic-1', 'category-2-topic-2'];
beforeEach(async () => {
axiosMock.onGet(getCohortsApiUrl(courseId))
.reply(200, Factory.buildList('cohort', 3));
axiosMock.onGet(getCohortsApiUrl(courseId)).reply(200, Factory.buildList('cohort', 3));
});
async function setupData(config = {}, settings = {}) {
@@ -187,69 +170,46 @@ describe('PostEditor', () => {
await setupData();
await renderComponent();
// Initially the user can't select a cohort
expect(screen.queryByRole('combobox', {
name: /cohort visibility/i,
}))
.not
.toBeInTheDocument();
expect(screen.queryByRole('combobox', { name: /cohort visibility/i })).not.toBeInTheDocument();
// If a cohorted topic is selected, the cohort visibility selector is displayed
['ncw-topic 2', 'category-1-topic 2', 'category-2-topic 1', 'category-2-topic 2'].forEach((topicName) => {
['ncw-topic 2', 'category-1-topic 2', 'category-2-topic 1', 'category-2-topic 2'].forEach(async (topicName) => {
act(() => {
userEvent.selectOptions(
screen.getByRole('combobox', {
name: /topic area/i,
}),
screen.getByRole('combobox', { name: /topic area/i }),
screen.getByRole('option', { name: topicName }),
);
});
expect(screen.queryByRole('combobox', {
name: /cohort visibility/i,
}))
.toBeInTheDocument();
expect(screen.queryByRole('combobox', { name: /cohort visibility/i })).toBeInTheDocument();
});
// Now if a non-cohorted topic is selected, the cohort visibility selector is hidden
['ncw-topic 1', 'category-1-topic 1', 'category-2-topic 4'].forEach((topicName) => {
['ncw-topic 1', 'category-1-topic 1', 'category-2-topic 4'].forEach(async (topicName) => {
act(() => {
userEvent.selectOptions(
screen.getByRole('combobox', {
name: /topic area/i,
}),
screen.getByRole('combobox', { name: /topic area/i }),
screen.getByRole('option', { name: topicName }),
);
});
expect(screen.queryByRole('combobox', {
name: /cohort visibility/i,
}))
.not
.toBeInTheDocument();
expect(screen.queryByRole('combobox', { name: /cohort visibility/i })).not.toBeInTheDocument();
});
});
test('test always divided inline', async () => {
await setupData({}, { alwaysDivideInlineDiscussions: true });
await renderComponent();
// Initially the user can't select a cohort
expect(screen.queryByRole('combobox', {
name: /cohort visibility/i,
}))
.not
.toBeInTheDocument();
expect(screen.queryByRole('combobox', { name: /cohort visibility/i })).not.toBeInTheDocument();
// All coursweare topics are divided
[1, 2].forEach(catId => {
[1, 2, 3, 4].forEach((topicId) => {
act(() => {
userEvent.selectOptions(
screen.getByRole('combobox', {
name: /topic area/i,
}),
screen.getByRole('combobox', { name: /topic area/i }),
screen.getByRole('option', { name: `category-${catId}-topic ${topicId}` }),
);
});
expect(screen.queryByRole('combobox', {
name: /cohort visibility/i,
}))
.toBeInTheDocument();
expect(screen.queryByRole('combobox', { name: /cohort visibility/i })).toBeInTheDocument();
});
});
@@ -257,46 +217,38 @@ describe('PostEditor', () => {
['ncw-topic 1', 'ncw-topic 3'].forEach((topicName) => {
act(() => {
userEvent.selectOptions(
screen.getByRole('combobox', {
name: /topic area/i,
}),
screen.getByRole('combobox', { name: /topic area/i }),
screen.getByRole('option', { name: topicName }),
);
});
expect(screen.queryByRole('combobox', {
name: /cohort visibility/i,
}))
.not
.toBeInTheDocument();
expect(screen.queryByRole('combobox', { name: /cohort visibility/i })).not.toBeInTheDocument();
});
});
test('test unprivileged user', async () => {
await setupData({ hasModerationPrivileges: false });
await renderComponent();
['ncw-topic 1', 'ncw-topic 2', 'category-1-topic 1', 'category-2-topic 1'].forEach((topicName) => {
act(() => {
userEvent.selectOptions(
screen.getByRole('combobox', {
name: /topic area/i,
}),
screen.getByRole('combobox', { name: /topic area/i }),
screen.getByRole('option', { name: topicName }),
);
});
// If a cohorted topic is selected, the cohort visibility selector is displayed
expect(screen.queryByRole('combobox', {
name: /cohort visibility/i,
}))
.not
.toBeInTheDocument();
expect(screen.queryByRole('combobox', { name: /cohort visibility/i })).not.toBeInTheDocument();
});
});
test('edit existing post should not show cohort selector to unprivileged users', async () => {
const threadId = 'thread-1';
await setupData({ hasModerationPrivileges: false });
axiosMock.onGet(`${threadsApiUrl}${threadId}/`)
.reply(200, Factory.build('thread'));
await executeThunk(fetchThread(threadId), store.dispatch, store.getState);
await renderComponent(true, `/${courseId}/posts/${threadId}/edit`);
await act(async () => {
axiosMock.onGet(`${threadsApiUrl}${threadId}/`).reply(200, Factory.build('thread'));
await executeThunk(fetchThread(threadId), store.dispatch, store.getState);
await renderComponent(true, `/${courseId}/posts/${threadId}/edit`);
});
['ncw-topic 1', 'ncw-topic 2', 'category-1-topic 1', 'category-2-topic 1'].forEach((topicName) => {
act(() => {
@@ -308,20 +260,18 @@ describe('PostEditor', () => {
);
});
// If a cohorted topic is selected, the cohort visibility selector is displayed
expect(screen.queryByRole('combobox', {
name: /cohort visibility/i,
}))
.not
.toBeInTheDocument();
expect(screen.queryByRole('combobox', { name: /cohort visibility/i })).not.toBeInTheDocument();
});
});
test('cancel posting of existing post', async () => {
const threadId = 'thread-1';
await setupData();
axiosMock.onGet(`${threadsApiUrl}${threadId}/`)
.reply(200, Factory.build('thread'));
await executeThunk(fetchThread(threadId), store.dispatch, store.getState);
await renderComponent(true, `/${courseId}/posts/${threadId}/edit`);
await act(async () => {
axiosMock.onGet(`${threadsApiUrl}${threadId}/`).reply(200, Factory.build('thread'));
await executeThunk(fetchThread(threadId), store.dispatch, store.getState);
await renderComponent(true, `/${courseId}/posts/${threadId}/edit`);
});
const cancelButton = screen.getByRole('button', { name: /cancel/i });
await act(async () => {
@@ -333,6 +283,7 @@ describe('PostEditor', () => {
describe('Edit codes', () => {
const threadId = 'thread-1';
beforeEach(async () => {
const dividedncw = ['ncw-topic-2'];
const dividedcw = ['category-1-topic-2', 'category-2-topic-1', 'category-2-topic-2'];
@@ -359,26 +310,26 @@ describe('PostEditor', () => {
},
});
await executeThunk(fetchCourseTopics(courseId), store.dispatch, store.getState);
axiosMock.onGet(`${threadsApiUrl}${threadId}/`)
.reply(200, Factory.build('thread'));
axiosMock.onGet(`${threadsApiUrl}${threadId}/`).reply(200, Factory.build('thread'));
await executeThunk(fetchThread(threadId), store.dispatch, store.getState);
});
test('Edit post and see reasons', async () => {
await renderComponent(true, `/${courseId}/posts/${threadId}/edit`);
expect(screen.queryByRole('combobox', {
name: /reason for editing/i,
}))
.toBeInTheDocument();
expect(screen.getAllByRole('option', {
name: /reason \d/i,
})).toHaveLength(2);
test('Edit post and see reasons', async () => {
await act(async () => {
await renderComponent(true, `/${courseId}/posts/${threadId}/edit`);
});
expect(screen.queryByRole('combobox', { name: /reason for editing/i })).toBeInTheDocument();
expect(screen.getAllByRole('option', { name: /reason \d/i })).toHaveLength(2);
});
it('should show Preview Panel', async () => {
await renderComponent(true, `/${courseId}/posts/${threadId}/edit`);
await act(() => fireEvent.click(container.querySelector('[data-testid="show-preview-button"]')));
await act(async () => {
fireEvent.click(container.querySelector('[data-testid="show-preview-button"]'));
});
await waitFor(() => {
expect(container.querySelector('[data-testid="hide-preview-button"]')).toBeInTheDocument();
expect(container.querySelector('[data-testid="show-preview-button"]')).not.toBeInTheDocument();
@@ -388,12 +339,18 @@ describe('PostEditor', () => {
it('should hide Preview Panel', async () => {
await renderComponent(true, `/${courseId}/posts/${threadId}/edit`);
await act(() => fireEvent.click(container.querySelector('[data-testid="show-preview-button"]')));
await act(async () => {
fireEvent.click(container.querySelector('[data-testid="show-preview-button"]'));
});
await waitFor(() => {
expect(container.querySelector('[data-testid="hide-preview-button"]')).toBeInTheDocument();
});
await act(() => fireEvent.click(container.querySelector('[data-testid="hide-preview-button"]')));
await act(async () => {
fireEvent.click(container.querySelector('[data-testid="hide-preview-button"]'));
});
await waitFor(() => {
expect(container.querySelector('[data-testid="show-preview-button"]')).toBeInTheDocument();
expect(container.querySelector('[data-testid="hide-preview-button"]')).not.toBeInTheDocument();

View File

@@ -18,7 +18,17 @@ function renderComponent(post, userHasModerationPrivileges = false) {
return render(
<IntlProvider locale="en">
<AppProvider store={store}>
<PostFooter post={post} userHasModerationPrivileges={userHasModerationPrivileges} />
<PostFooter
post={post}
userHasModerationPrivileges={userHasModerationPrivileges}
closed={post.closed}
following={post.following}
groupId={toString(post.groupId)}
groupName={post.groupName}
id={post.id}
voted={post.voted}
voteCount={post.voteCount}
/>
</AppProvider>
</IntlProvider>,
);
@@ -88,7 +98,7 @@ describe('PostFooter', () => {
expect(store.getState().threads.status === RequestStatus.IN_PROGRESS).toBeTruthy();
});
it('test follow button when following=false', async () => {
it('test follow button when following = false', async () => {
renderComponent({ ...mockPost, following: false });
expect(screen.queryByRole('button', { name: /follow/i })).not.toBeInTheDocument();
});

View File

@@ -3,6 +3,7 @@ import React from 'react';
import { render, screen } from '@testing-library/react';
import MockAdapter from 'axios-mock-adapter';
import { IntlProvider } from 'react-intl';
import { Factory } from 'rosie';
import { initializeMockApp } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
@@ -13,48 +14,52 @@ import { executeThunk } from '../../../test-utils';
import { DiscussionContext } from '../../common/context';
import { getCourseConfigApiUrl } from '../../data/api';
import { fetchCourseConfig } from '../../data/thunks';
import { getThreadsApiUrl } from '../data/api';
import { fetchThread } from '../data/thunks';
import PostLink from './PostLink';
import '../data/__factories__';
const courseId = 'course-v1:edX+DemoX+Demo_Course';
const courseConfigApiUrl = getCourseConfigApiUrl();
const threadsApiUrl = getThreadsApiUrl();
const postId = 'thread-1';
let store;
let axiosMock;
function renderComponent(post) {
const mockThread = async (id, abuseFlagged) => {
store = initializeStore();
Factory.resetAll();
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
axiosMock.onGet(`${courseConfigApiUrl}${courseId}/settings`).reply(200, {});
axiosMock.onGet(`${courseConfigApiUrl}${courseId}/`).reply(200, {
learners_tab_enabled: true,
has_moderation_privileges: true,
});
axiosMock.onGet(`${threadsApiUrl}${id}/`).reply(200, Factory.build('thread', {
id, abuse_flagged: abuseFlagged,
}));
await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState);
await executeThunk(fetchThread(id), store.dispatch, store.getState);
};
function renderComponent(id) {
return render(
<IntlProvider locale="en">
<AppProvider store={store}>
<DiscussionContext.Provider value={{ courseId }}>
<PostLink post={post} key={post.id} isSelected={() => true} />
<PostLink
key={id}
postId={id}
/>
</DiscussionContext.Provider>
</AppProvider>
</IntlProvider>,
);
}
const mockPost = {
abuseFlagged: false,
author: 'test-user',
commentCount: 5,
courseId: 'course-v1:edX+DemoX+Demo_Course',
following: false,
id: 'test-id',
pinned: false,
rawBody: '<p>a test post</p>',
hasEndorsed: false,
voted: false,
voteCount: 10,
previewBody: '<p>a test post</p>',
read: false,
title: 'test post',
topicId: 'i4x-edx-eiorguegnru-course-foobarbaz',
unreadCommentCount: 2,
groupName: null,
groupId: null,
createdAt: '2022-02-25T09:17:17Z',
closed: false,
};
describe('PostFooter', () => {
beforeEach(async () => {
initializeMockApp({
@@ -65,21 +70,17 @@ describe('PostFooter', () => {
roles: [],
},
});
store = initializeStore();
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
axiosMock.onGet(`${courseConfigApiUrl}${courseId}/`).reply(200, {
has_moderation_privileges: true,
});
axiosMock.onGet(`${courseConfigApiUrl}${courseId}/settings`).reply(200, {});
await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState);
});
it('has reported text only when abuseFlagged is true', async () => {
renderComponent(mockPost);
expect(screen.queryByTestId('reported-post')).toBeFalsy();
it.each([true, false])('has reported text only when abuseFlagged is %s', async (abuseFlagged) => {
await mockThread(postId, abuseFlagged);
renderComponent(postId);
renderComponent({ ...mockPost, abuseFlagged: true });
expect(screen.getByTestId('reported-post')).toBeTruthy();
if (abuseFlagged) {
expect(screen.getByText('Reported')).toBeTruthy();
} else {
expect(screen.queryByTestId('reported-post')).toBeFalsy();
}
});
});
@@ -93,21 +94,15 @@ describe('Post username', () => {
roles: [],
},
});
store = initializeStore();
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
axiosMock.onGet(`${courseConfigApiUrl}${courseId}/`).reply(200, {
learners_tab_enabled: true,
has_moderation_privileges: true,
});
axiosMock.onGet(`${courseConfigApiUrl}${courseId}/settings`).reply(200, {});
await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState);
});
it.each([
'anonymous',
'test-user',
])('is not clickable for %s user', async (user) => {
renderComponent({ ...mockPost, author: user });
])('is not clickable for %s user', async () => {
await mockThread(postId, false);
renderComponent(postId);
expect(screen.queryByTestId('learner-posts-link')).not.toBeInTheDocument();
});
});