-
Notifications
You must be signed in to change notification settings - Fork 222
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
* feat: add discussions tab Adds code to load the discussions MFE in an iframe in the tab so the user isn't redirected to the LMS. Adds code for the discussions tab, making it dynamically resize based on contents using a postMessage API. * feat: update path based on user navigation inside discussions MFE The discussions MFE will send path change events via the postMessage API so that the learning MFE path can be kept in sync. This will allow reloading a page without having the iframe revert to same path each time.
- Loading branch information
Showing
9 changed files
with
288 additions
and
35 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import { getConfig } from '@edx/frontend-platform'; | ||
import { injectIntl } from '@edx/frontend-platform/i18n'; | ||
import React, { useState } from 'react'; | ||
import { useSelector } from 'react-redux'; | ||
import { generatePath, useHistory } from 'react-router'; | ||
import { useParams } from 'react-router-dom'; | ||
import { useIFrameHeight, useIFramePluginEvents } from '../../generic/hooks'; | ||
|
||
function DiscussionTab() { | ||
const { courseId } = useSelector(state => state.courseHome); | ||
const { path } = useParams(); | ||
const [originalPath] = useState(path); | ||
const history = useHistory(); | ||
|
||
const [, iFrameHeight] = useIFrameHeight(); | ||
useIFramePluginEvents({ | ||
'discussions.navigate': (payload) => { | ||
const basePath = generatePath('/course/:courseId/discussion', { courseId }); | ||
history.push(`${basePath}/${payload.path}`); | ||
}, | ||
}); | ||
const discussionsUrl = `${getConfig().DISCUSSIONS_MFE_BASE_URL}/${courseId}/${originalPath}`; | ||
return ( | ||
<iframe | ||
src={discussionsUrl} | ||
className="d-flex w-100 border-0" | ||
height={iFrameHeight} | ||
style={{ minHeight: '60rem' }} | ||
title="discussion" | ||
/> | ||
); | ||
} | ||
|
||
DiscussionTab.propTypes = {}; | ||
|
||
export default injectIntl(DiscussionTab); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
import { getConfig, history } from '@edx/frontend-platform'; | ||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; | ||
import { AppProvider } from '@edx/frontend-platform/react'; | ||
import { render } from '@testing-library/react'; | ||
import MockAdapter from 'axios-mock-adapter'; | ||
import React from 'react'; | ||
import { Route } from 'react-router'; | ||
import { Factory } from 'rosie'; | ||
import { UserMessagesProvider } from '../../generic/user-messages'; | ||
import { | ||
initializeMockApp, messageEvent, screen, waitFor, | ||
} from '../../setupTest'; | ||
import initializeStore from '../../store'; | ||
import { TabContainer } from '../../tab-page'; | ||
import { appendBrowserTimezoneToUrl } from '../../utils'; | ||
import { fetchDiscussionTab } from '../data/thunks'; | ||
import DiscussionTab from './DiscussionTab'; | ||
|
||
initializeMockApp(); | ||
jest.mock('@edx/frontend-platform/analytics'); | ||
|
||
describe('DiscussionTab', () => { | ||
let axiosMock; | ||
let store; | ||
let component; | ||
|
||
beforeEach(() => { | ||
axiosMock = new MockAdapter(getAuthenticatedHttpClient()); | ||
store = initializeStore(); | ||
component = ( | ||
<AppProvider store={store}> | ||
<UserMessagesProvider> | ||
<Route path="/course/:courseId/discussion"> | ||
<TabContainer tab="discussion" fetch={fetchDiscussionTab} slice="courseHome"> | ||
<DiscussionTab /> | ||
</TabContainer> | ||
</Route> | ||
</UserMessagesProvider> | ||
</AppProvider> | ||
); | ||
}); | ||
|
||
const courseMetadata = Factory.build('courseHomeMetadata', { user_timezone: 'America/New_York' }); | ||
const { id: courseId } = courseMetadata; | ||
|
||
let courseMetadataUrl = `${getConfig().LMS_BASE_URL}/api/course_home/course_metadata/${courseId}`; | ||
courseMetadataUrl = appendBrowserTimezoneToUrl(courseMetadataUrl); | ||
|
||
beforeEach(() => { | ||
axiosMock.onGet(courseMetadataUrl).reply(200, courseMetadata); | ||
history.push(`/course/${courseId}/discussion`); // so tab can pull course id from url | ||
|
||
render(component); | ||
}); | ||
|
||
it('resizes when it gets a size hint from iframe', async () => { | ||
window.postMessage({ ...messageEvent, payload: { height: 1234 } }, '*'); | ||
await waitFor(() => expect(screen.getByTitle('discussion')) | ||
.toHaveAttribute('height', String(1234))); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
69 changes: 69 additions & 0 deletions
69
src/courseware/course/sidebar/sidebars/discussions/DiscussionsSidebar.test.jsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import { getConfig } from '@edx/frontend-platform'; | ||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; | ||
import MockAdapter from 'axios-mock-adapter'; | ||
import React from 'react'; | ||
import { | ||
initializeMockApp, initializeTestStore, render, screen, | ||
} from '../../../../../setupTest'; | ||
import { executeThunk } from '../../../../../utils'; | ||
import { buildTopicsFromUnits } from '../../../../data/__factories__/discussionTopics.factory'; | ||
import { getCourseDiscussionTopics } from '../../../../data/thunks'; | ||
import SidebarContext from '../../SidebarContext'; | ||
import DiscussionsSidebar from './DiscussionsSidebar'; | ||
|
||
initializeMockApp(); | ||
|
||
describe('Discussions Trigger', () => { | ||
let axiosMock; | ||
let mockData; | ||
let courseId; | ||
let unitId; | ||
|
||
beforeEach(async () => { | ||
const store = await initializeTestStore({ | ||
excludeFetchCourse: false, | ||
excludeFetchSequence: false, | ||
}); | ||
axiosMock = new MockAdapter(getAuthenticatedHttpClient()); | ||
const state = store.getState(); | ||
courseId = state.courseware.courseId; | ||
[unitId] = Object.keys(state.models.units); | ||
|
||
mockData = { | ||
courseId, | ||
unitId, | ||
currentSidebar: 'DISCUSSIONS', | ||
}; | ||
|
||
axiosMock.onGet(`${getConfig().LMS_BASE_URL}/api/discussion/v1/courses/${courseId}`).reply( | ||
200, | ||
{ | ||
provider: 'openedx', | ||
}, | ||
); | ||
axiosMock.onGet(`${getConfig().LMS_BASE_URL}/api/discussion/v2/course_topics/${courseId}`) | ||
.reply(200, buildTopicsFromUnits(state.models.units)); | ||
await executeThunk(getCourseDiscussionTopics(courseId), store.dispatch); | ||
}); | ||
|
||
function renderWithProvider(testData = {}) { | ||
const { container } = render( | ||
<SidebarContext.Provider value={{ ...mockData, ...testData }}> | ||
<DiscussionsSidebar /> | ||
</SidebarContext.Provider>, | ||
); | ||
return container; | ||
} | ||
|
||
it('should show up if unit discussions associated with it', async () => { | ||
renderWithProvider(); | ||
expect(screen.queryByTitle('Discussions')).toBeInTheDocument(); | ||
expect(screen.queryByTitle('Discussions')) | ||
.toHaveAttribute('src', `http://localhost:2002/${courseId}/topics/topic-1?inContext`); | ||
}); | ||
|
||
it('should show nothing if unit has no discussions associated with it', async () => { | ||
renderWithProvider({ unitId: 'no-discussion' }); | ||
expect(screen.queryByTitle('Discussions')).not.toBeInTheDocument(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import { render, screen, waitFor } from '@testing-library/react'; | ||
import { useEventListener, useIFrameHeight } from './hooks'; | ||
|
||
describe('Hooks', () => { | ||
test('useEventListener', async () => { | ||
const handler = jest.fn(); | ||
const TestComponent = () => { | ||
useEventListener('message', handler); | ||
return (<div data-testid="testid" />); | ||
}; | ||
render(<TestComponent />); | ||
|
||
await screen.findByTestId('testid'); | ||
window.postMessage({ test: 'test' }, '*'); | ||
await waitFor(() => expect(handler).toHaveBeenCalled()); | ||
}); | ||
test('useIFrameHeight', async () => { | ||
const onLoaded = jest.fn(); | ||
const TestComponent = () => { | ||
const [hasLoaded, height] = useIFrameHeight(onLoaded); | ||
return ( | ||
<div data-testid="testid"> | ||
<span data-testid="loaded"> | ||
{String(hasLoaded)} | ||
</span> | ||
<span data-testid="height"> | ||
{String(height)} | ||
</span> | ||
</div> | ||
); | ||
}; | ||
render(<TestComponent />); | ||
|
||
await screen.findByTestId('testid'); | ||
expect(screen.getByTestId('loaded')).toHaveTextContent('false'); | ||
expect(screen.getByTestId('height')).toHaveTextContent('null'); | ||
window.postMessage({ | ||
type: 'plugin.resize', | ||
payload: { height: 1234 }, | ||
}, '*'); | ||
await waitFor(() => expect(onLoaded).toHaveBeenCalled()); | ||
await waitFor(() => expect(screen.getByTestId('loaded')).toHaveTextContent('true')); | ||
expect(screen.getByTestId('height')).toHaveTextContent('1234'); | ||
}); | ||
}); |
Oops, something went wrong.