fix: [AA-1207] unify source of tabs (#861)
Courseware and courseHome both provide tabs to the mfe. This PR unifies the calls so that tab descriptions are only fetched from courseHome metadata Remove jest-chain dependencies to make test errors more usable.
This commit is contained in:
@@ -49,12 +49,10 @@ describe('Notes Visibility', () => {
|
||||
render(<NotesVisibility {...mockData} />);
|
||||
|
||||
const button = screen.getByRole('switch', { name: 'Show Notes' });
|
||||
expect(button)
|
||||
.not.toBeChecked()
|
||||
.toHaveClass('text-success');
|
||||
expect(button.querySelector('svg'))
|
||||
.toHaveClass('fa-pencil-alt')
|
||||
.toHaveAttribute('aria-hidden', 'true');
|
||||
expect(button).not.toBeChecked();
|
||||
expect(button).toHaveClass('text-success');
|
||||
expect(button.querySelector('svg')).toHaveClass('fa-pencil-alt');
|
||||
expect(button.querySelector('svg')).toHaveAttribute('aria-hidden', 'true');
|
||||
});
|
||||
|
||||
it('shows notes', () => {
|
||||
@@ -63,12 +61,10 @@ describe('Notes Visibility', () => {
|
||||
render(<NotesVisibility {...testData} />);
|
||||
|
||||
const button = screen.getByRole('switch', { name: 'Hide Notes' });
|
||||
expect(button)
|
||||
.toBeChecked()
|
||||
.toHaveClass('text-secondary');
|
||||
expect(button.querySelector('svg'))
|
||||
.toHaveClass('fa-pencil-alt')
|
||||
.toHaveAttribute('aria-hidden', 'true');
|
||||
expect(button).toBeChecked();
|
||||
expect(button).toHaveClass('text-secondary');
|
||||
expect(button.querySelector('svg')).toHaveClass('fa-pencil-alt');
|
||||
expect(button.querySelector('svg')).toHaveAttribute('aria-hidden', 'true');
|
||||
});
|
||||
|
||||
it('handles click', async () => {
|
||||
@@ -84,9 +80,8 @@ describe('Notes Visibility', () => {
|
||||
|
||||
fireEvent.click(screen.getByRole('switch', { name: 'Show Notes' }));
|
||||
await waitFor(() => expect(mockFn).toHaveBeenCalled());
|
||||
expect(mockFn)
|
||||
.toHaveBeenCalledTimes(1)
|
||||
.toHaveBeenCalledWith('tools.toggleNotes');
|
||||
expect(mockFn).toHaveBeenCalledTimes(1);
|
||||
expect(mockFn).toHaveBeenCalledWith('tools.toggleNotes');
|
||||
|
||||
expect(axiosMock.history.put).toHaveLength(1);
|
||||
expect(axiosMock.history.put[0].url).toEqual(visibilityUrl);
|
||||
|
||||
@@ -24,32 +24,36 @@ jest.mock('@edx/frontend-platform/analytics');
|
||||
describe('Course Exit Pages', () => {
|
||||
let axiosMock;
|
||||
let store;
|
||||
const defaultMetadata = Factory.build('courseMetadata', {
|
||||
const coursewareMetadata = Factory.build('courseMetadata', {
|
||||
user_has_passing_grade: true,
|
||||
end: '2014-02-05T05:00:00Z',
|
||||
});
|
||||
const { courseBlocks: defaultCourseBlocks } = buildSimpleCourseBlocks(defaultMetadata.id, defaultMetadata.name);
|
||||
const courseId = coursewareMetadata.id;
|
||||
const courseHomeMetadata = Factory.build('courseHomeMetadata');
|
||||
const { courseBlocks: defaultCourseBlocks } = buildSimpleCourseBlocks(courseId, coursewareMetadata.name);
|
||||
|
||||
let courseMetadataUrl = `${getConfig().LMS_BASE_URL}/api/courseware/course/${defaultMetadata.id}`;
|
||||
courseMetadataUrl = appendBrowserTimezoneToUrl(courseMetadataUrl);
|
||||
let coursewareMetadataUrl = `${getConfig().LMS_BASE_URL}/api/courseware/course/${courseId}`;
|
||||
coursewareMetadataUrl = appendBrowserTimezoneToUrl(coursewareMetadataUrl);
|
||||
const courseHomeMetadataUrl = appendBrowserTimezoneToUrl(`${getConfig().LMS_BASE_URL}/api/course_home/course_metadata/${courseId}`);
|
||||
const discoveryRecommendationsUrl = new RegExp(`${getConfig().DISCOVERY_API_BASE_URL}/api/v1/course_recommendations/*`);
|
||||
const enrollmentsUrl = new RegExp(`${getConfig().LMS_BASE_URL}/api/enrollment/v1/enrollment*`);
|
||||
const learningSequencesUrlRegExp = new RegExp(`${getConfig().LMS_BASE_URL}/api/learning_sequences/v1/course_outline/*`);
|
||||
|
||||
function setMetadata(attributes) {
|
||||
const courseMetadata = { ...defaultMetadata, ...attributes };
|
||||
axiosMock.onGet(courseMetadataUrl).reply(200, courseMetadata);
|
||||
const courseMetadata = { ...coursewareMetadata, ...attributes };
|
||||
axiosMock.onGet(coursewareMetadataUrl).reply(200, courseMetadata);
|
||||
}
|
||||
|
||||
async function fetchAndRender(component) {
|
||||
await executeThunk(fetchCourse(defaultMetadata.id), store.dispatch);
|
||||
await executeThunk(fetchCourse(courseId), store.dispatch);
|
||||
render(component, { store });
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
store = initializeStore();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
axiosMock.onGet(courseMetadataUrl).reply(200, defaultMetadata);
|
||||
axiosMock.onGet(coursewareMetadataUrl).reply(200, coursewareMetadata);
|
||||
axiosMock.onGet(courseHomeMetadataUrl).reply(200, courseHomeMetadata);
|
||||
axiosMock.onGet(discoveryRecommendationsUrl).reply(200,
|
||||
Factory.build('courseRecommendations', {}, { numRecs: 2 }));
|
||||
axiosMock.onGet(enrollmentsUrl).reply(200, []);
|
||||
@@ -94,7 +98,7 @@ describe('Course Exit Pages', () => {
|
||||
},
|
||||
});
|
||||
await fetchAndRender(<CourseExit />);
|
||||
expect(global.location.href).toEqual(`http://localhost/course/${defaultMetadata.id}`);
|
||||
expect(global.location.href).toEqual(`http://localhost/course/${courseId}`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -160,7 +164,7 @@ describe('Course Exit Pages', () => {
|
||||
it('Displays verify identity link', async () => {
|
||||
setMetadata({
|
||||
certificate_data: { cert_status: 'unverified' },
|
||||
verify_identity_url: `${getConfig().LMS_BASE_URL}/verify_student/verify-now/${defaultMetadata.id}/`,
|
||||
verify_identity_url: `${getConfig().LMS_BASE_URL}/verify_student/verify-now/${courseId}/`,
|
||||
});
|
||||
await fetchAndRender(<CourseCelebration />);
|
||||
expect(screen.getByRole('link', { name: 'Verify ID now' })).toBeInTheDocument();
|
||||
@@ -171,7 +175,7 @@ describe('Course Exit Pages', () => {
|
||||
setMetadata({
|
||||
certificate_data: { cert_status: 'unverified' },
|
||||
verification_status: 'pending',
|
||||
verify_identity_url: `${getConfig().LMS_BASE_URL}/verify_student/verify-now/${defaultMetadata.id}/`,
|
||||
verify_identity_url: `${getConfig().LMS_BASE_URL}/verify_student/verify-now/${courseId}/`,
|
||||
});
|
||||
await fetchAndRender(<CourseCelebration />);
|
||||
expect(screen.getByText('Your ID verification is pending and your certificate will be available once approved.')).toBeInTheDocument();
|
||||
@@ -367,7 +371,7 @@ describe('Course Exit Pages', () => {
|
||||
describe('Course in progress experience', () => {
|
||||
it('Displays link to dates tab', async () => {
|
||||
setMetadata({ user_has_passing_grade: false });
|
||||
const { courseBlocks } = buildSimpleCourseBlocks(defaultMetadata.id, defaultMetadata.name,
|
||||
const { courseBlocks } = buildSimpleCourseBlocks(courseId, coursewareMetadata.name,
|
||||
{ hasScheduledContent: true });
|
||||
axiosMock.onGet(learningSequencesUrlRegExp).reply(200, buildOutlineFromBlocks(courseBlocks));
|
||||
|
||||
@@ -394,7 +398,7 @@ describe('Course Exit Pages', () => {
|
||||
const url = `${getConfig().LMS_BASE_URL}/api/course_home/save_course_goal`;
|
||||
await waitFor(() => {
|
||||
expect(axiosMock.history.post[0].url).toMatch(url);
|
||||
expect(axiosMock.history.post[0].data).toMatch(`{"course_id":"${defaultMetadata.id}","subscribed_to_reminders":false}`);
|
||||
expect(axiosMock.history.post[0].data).toMatch(`{"course_id":"${courseId}","subscribed_to_reminders":false}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,7 +16,8 @@ import { logClick, logVisit } from './utils';
|
||||
|
||||
function CourseInProgress({ intl }) {
|
||||
const { courseId } = useSelector(state => state.courseware);
|
||||
const { org, tabs, title } = useModel('coursewareMeta', courseId);
|
||||
const { org, title } = useModel('coursewareMeta', courseId);
|
||||
const { tabs } = useModel('courseHomeMeta', courseId);
|
||||
const { administrator } = getAuthenticatedUser();
|
||||
|
||||
// Get dates tab link for 'view course schedule' button
|
||||
|
||||
@@ -16,7 +16,8 @@ import { logClick, logVisit } from './utils';
|
||||
|
||||
function CourseNonPassing({ intl }) {
|
||||
const { courseId } = useSelector(state => state.courseware);
|
||||
const { org, tabs, title } = useModel('coursewareMeta', courseId);
|
||||
const { org, title } = useModel('coursewareMeta', courseId);
|
||||
const { tabs } = useModel('courseHomeMeta', courseId);
|
||||
const { administrator } = getAuthenticatedUser();
|
||||
|
||||
// Get progress tab link for 'view grades' button
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
} from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
import truncate from 'truncate-html';
|
||||
import { useModel } from '../../../generic/model-store/hooks';
|
||||
import { useModel } from '../../../generic/model-store';
|
||||
import fetchCourseRecommendations from './data/thunks';
|
||||
import { FAILED, LOADED, LOADING } from './data/slice';
|
||||
import CatalogSuggestion from './CatalogSuggestion';
|
||||
@@ -106,8 +106,8 @@ function CourseCard({
|
||||
<Card.ImageCap src={image.src} />
|
||||
<Card.Header title={truncate(title, 70, { reserveLastWord: -1 })} subtitle={subtitle} size="sm" />
|
||||
{/* Section is needed for internal vertical spacing to work out. If you can remove, be my guest */}
|
||||
<Card.Section />
|
||||
<Card.Footer textElement={intl.formatMessage(messages.recommendationsCourseFooter)} />
|
||||
<Card.Section> <></> </Card.Section>
|
||||
<Card.Footer textElement={intl.formatMessage(messages.recommendationsCourseFooter)}><></></Card.Footer>
|
||||
</Card>
|
||||
</Hyperlink>
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useModel } from '../../../../generic/model-store';
|
||||
import messages from './messages';
|
||||
|
||||
function HiddenAfterDue({ courseId, intl }) {
|
||||
const { tabs } = useModel('coursewareMeta', courseId);
|
||||
const { tabs } = useModel('courseHomeMeta', courseId);
|
||||
|
||||
const progressTab = tabs.find(tab => tab.slug === 'progress');
|
||||
const progressLink = progressTab && progressTab.url && (
|
||||
|
||||
@@ -28,6 +28,9 @@ describe('NotificationTray', () => {
|
||||
let courseMetadataUrl = `${getConfig().LMS_BASE_URL}/api/courseware/course/${defaultMetadata.id}`;
|
||||
courseMetadataUrl = appendBrowserTimezoneToUrl(courseMetadataUrl);
|
||||
|
||||
const courseHomeMetadata = Factory.build('courseHomeMetadata');
|
||||
const courseHomeMetadataUrl = appendBrowserTimezoneToUrl(`${getConfig().LMS_BASE_URL}/api/course_home/course_metadata/${courseId}`);
|
||||
|
||||
function setMetadata(attributes, options) {
|
||||
const courseMetadata = Factory.build('courseMetadata', attributes, options);
|
||||
axiosMock.onGet(courseMetadataUrl).reply(200, courseMetadata);
|
||||
@@ -43,6 +46,7 @@ describe('NotificationTray', () => {
|
||||
store = initializeStore();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
axiosMock.onGet(courseMetadataUrl).reply(200, defaultMetadata);
|
||||
axiosMock.onGet(courseHomeMetadataUrl).reply(200, courseHomeMetadata);
|
||||
});
|
||||
|
||||
it('renders notification tray and close tray button', async () => {
|
||||
|
||||
@@ -84,17 +84,6 @@ export async function getLearningSequencesOutline(courseId) {
|
||||
return normalizeLearningSequencesData(data);
|
||||
}
|
||||
|
||||
function normalizeTabUrls(id, tabs) {
|
||||
// If api doesn't return the mfe base url, change tab url to point back to LMS
|
||||
return tabs.map((tab) => {
|
||||
let { url } = tab;
|
||||
if (url[0] === '/') {
|
||||
url = `${getConfig().LMS_BASE_URL}${tab.url}`;
|
||||
}
|
||||
return { ...tab, url };
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeMetadata(metadata) {
|
||||
const requestTime = Date.now();
|
||||
const responseTime = requestTime;
|
||||
@@ -120,7 +109,6 @@ function normalizeMetadata(metadata) {
|
||||
isStaff: data.is_staff,
|
||||
license: data.license,
|
||||
verifiedMode: camelCaseObject(data.verified_mode),
|
||||
tabs: normalizeTabUrls(data.id, camelCaseObject(data.tabs)),
|
||||
userTimezone: data.user_timezone,
|
||||
showCalculator: data.show_calculator,
|
||||
notes: camelCaseObject(data.notes),
|
||||
|
||||
@@ -243,9 +243,6 @@ describe('Courseware Service', () => {
|
||||
generate: '2013-02-05T05:00:00Z',
|
||||
matcher: dateRegex,
|
||||
}),
|
||||
tabs: eachLike({
|
||||
title: 'Course', slug: 'courseware', priority: 0, type: 'courseware', url: `${getConfig().BASE_URL}/course/course-v1:edX+DemoX+Demo_Course/home`,
|
||||
}),
|
||||
user_timezone: null,
|
||||
verified_mode: like({
|
||||
access_expiration_date: term({
|
||||
@@ -329,13 +326,6 @@ describe('Courseware Service', () => {
|
||||
sku: '8CF08E5',
|
||||
upgradeUrl: `${getConfig().ECOMMERCE_BASE_URL}/basket/add/?sku=8CF08E5`,
|
||||
},
|
||||
tabs: [{
|
||||
title: 'Course',
|
||||
slug: 'courseware',
|
||||
priority: 0,
|
||||
type: 'courseware',
|
||||
url: `${getConfig().BASE_URL}/course/course-v1:edX+DemoX+Demo_Course/home`,
|
||||
}],
|
||||
userTimezone: null,
|
||||
showCalculator: false,
|
||||
notes: { enabled: false, visible: true },
|
||||
|
||||
@@ -29,7 +29,7 @@ export function fetchCourse(courseId) {
|
||||
Promise.allSettled([
|
||||
getCourseMetadata(courseId),
|
||||
getLearningSequencesOutline(courseId),
|
||||
getCourseHomeCourseMetadata(courseId),
|
||||
getCourseHomeCourseMetadata(courseId, 'courseware'),
|
||||
]).then(([
|
||||
courseMetadataResult,
|
||||
learningSequencesOutlineResult,
|
||||
|
||||
Reference in New Issue
Block a user