Skip to content

Commit 52235eb

Browse files
Lunyachekleangseu-edx
authored andcommitted
feat: create component to decode params
1 parent aa380e8 commit 52235eb

File tree

5 files changed

+186
-15
lines changed

5 files changed

+186
-15
lines changed

src/courseware/CoursewareRedirectLandingPage.jsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { PageRoute } from '@edx/frontend-platform/react';
77
import queryString from 'query-string';
88
import PageLoading from '../generic/PageLoading';
99

10+
import DecodePageRoute from '../decode-page-route';
11+
1012
const CoursewareRedirectLandingPage = () => {
1113
const { path } = useRouteMatch();
1214
return (
@@ -21,7 +23,7 @@ const CoursewareRedirectLandingPage = () => {
2123
/>
2224

2325
<Switch>
24-
<PageRoute
26+
<DecodePageRoute
2527
path={`${path}/survey/:courseId`}
2628
render={({ match }) => {
2729
global.location.assign(`${getConfig().LMS_BASE_URL}/courses/${match.params.courseId}/survey`);
@@ -40,7 +42,7 @@ const CoursewareRedirectLandingPage = () => {
4042
global.location.assign(`${getConfig().LMS_BASE_URL}${consentPath}`);
4143
}}
4244
/>
43-
<PageRoute
45+
<DecodePageRoute
4446
path={`${path}/home/:courseId`}
4547
render={({ match }) => {
4648
global.location.assign(`/course/${match.params.courseId}/home`);
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`DecodePageRoute should not modify the url if it does not need to be decoded 1`] = `
4+
<div>
5+
PageRoute: {
6+
"computedMatch": {
7+
"path": "/course/:courseId/home",
8+
"url": "/course/course-v1:edX+DemoX+Demo_Course/home",
9+
"isExact": true,
10+
"params": {
11+
"courseId": "course-v1:edX+DemoX+Demo_Course"
12+
}
13+
}
14+
}
15+
</div>
16+
`;

src/decode-page-route/index.jsx

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import PropTypes from 'prop-types';
2+
import { PageRoute } from '@edx/frontend-platform/react';
3+
import React from 'react';
4+
import { useHistory, generatePath } from 'react-router';
5+
6+
export const decodeUrl = (encodedUrl) => {
7+
const decodedUrl = decodeURIComponent(encodedUrl);
8+
if (encodedUrl === decodedUrl) {
9+
return encodedUrl;
10+
}
11+
return decodeUrl(decodedUrl);
12+
};
13+
14+
const DecodePageRoute = (props) => {
15+
const history = useHistory();
16+
if (props.computedMatch) {
17+
const { url, path, params } = props.computedMatch;
18+
19+
Object.keys(params).forEach((param) => {
20+
// only decode params not the entire url.
21+
// it is just to be safe and less prone to errors
22+
params[param] = decodeUrl(params[param]);
23+
});
24+
25+
const newUrl = generatePath(path, params);
26+
27+
// if the url get decoded, reroute to the decoded url
28+
if (newUrl !== url) {
29+
history.replace(newUrl);
30+
}
31+
}
32+
33+
return <PageRoute {...props} />;
34+
};
35+
36+
DecodePageRoute.propTypes = {
37+
computedMatch: PropTypes.shape({
38+
url: PropTypes.string.isRequired,
39+
path: PropTypes.string.isRequired,
40+
// eslint-disable-next-line react/forbid-prop-types
41+
params: PropTypes.any,
42+
}),
43+
};
44+
45+
DecodePageRoute.defaultProps = {
46+
computedMatch: null,
47+
};
48+
49+
export default DecodePageRoute;

src/decode-page-route/index.test.jsx

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import React from 'react';
2+
import { render } from '@testing-library/react';
3+
import { createMemoryHistory } from 'history';
4+
import { Router, matchPath } from 'react-router';
5+
import DecodePageRoute, { decodeUrl } from '.';
6+
7+
const decodedCourseId = 'course-v1:edX+DemoX+Demo_Course';
8+
const encodedCourseId = encodeURIComponent(decodedCourseId);
9+
const deepEncodedCourseId = (() => {
10+
let path = encodedCourseId;
11+
for (let i = 0; i < 5; i++) {
12+
path = encodeURIComponent(path);
13+
}
14+
return path;
15+
})();
16+
17+
jest.mock('@edx/frontend-platform/react', () => ({
18+
PageRoute: (props) => `PageRoute: ${JSON.stringify(props, null, 2)}`,
19+
}));
20+
21+
const renderPage = (props) => {
22+
const memHistory = createMemoryHistory({
23+
initialEntries: [props?.path],
24+
});
25+
26+
const history = {
27+
...memHistory,
28+
replace: jest.fn(),
29+
};
30+
31+
const { container } = render(
32+
<Router history={history}>
33+
<DecodePageRoute computedMatch={props} />
34+
</Router>,
35+
);
36+
37+
return {
38+
container,
39+
history,
40+
props,
41+
};
42+
};
43+
44+
describe('DecodePageRoute', () => {
45+
it('should not modify the url if it does not need to be decoded', () => {
46+
const props = matchPath(`/course/${decodedCourseId}/home`, {
47+
path: '/course/:courseId/home',
48+
});
49+
const { container, history } = renderPage(props);
50+
51+
expect(props.url).toContain(decodedCourseId);
52+
expect(history.replace).not.toHaveBeenCalled();
53+
expect(container).toMatchSnapshot();
54+
});
55+
56+
it('should decode the url and replace the history if necessary', () => {
57+
const props = matchPath(`/course/${encodedCourseId}/home`, {
58+
path: '/course/:courseId/home',
59+
});
60+
const { history } = renderPage(props);
61+
62+
expect(props.url).not.toContain(decodedCourseId);
63+
expect(props.url).toContain(encodedCourseId);
64+
expect(history.replace.mock.calls[0][0]).toContain(decodedCourseId);
65+
});
66+
67+
it('should decode the url multiple times if necessary', () => {
68+
const props = matchPath(`/course/${deepEncodedCourseId}/home`, {
69+
path: '/course/:courseId/home',
70+
});
71+
const { history } = renderPage(props);
72+
73+
expect(props.url).not.toContain(decodedCourseId);
74+
expect(props.url).toContain(deepEncodedCourseId);
75+
expect(history.replace.mock.calls[0][0]).toContain(decodedCourseId);
76+
});
77+
78+
it('should only decode the url params and not the entire url', () => {
79+
const decodedUnitId = 'some+thing';
80+
const encodedUnitId = encodeURIComponent(decodedUnitId);
81+
const props = matchPath(`/course/${deepEncodedCourseId}/${encodedUnitId}/${encodedUnitId}`, {
82+
path: `/course/:courseId/${encodedUnitId}/:unitId`,
83+
});
84+
const { history } = renderPage(props);
85+
86+
const decodedUrls = history.replace.mock.calls[0][0].split('/');
87+
88+
// unitId get decoded
89+
expect(decodedUrls.pop()).toContain(decodedUnitId);
90+
91+
// path remain encoded
92+
expect(decodedUrls.pop()).toContain(encodedUnitId);
93+
94+
// courseId get decoded
95+
expect(decodedUrls.pop()).toContain(decodedCourseId);
96+
});
97+
});
98+
99+
describe('decodeUrl', () => {
100+
expect(decodeUrl(decodedCourseId)).toEqual(decodedCourseId);
101+
expect(decodeUrl(encodedCourseId)).toEqual(decodedCourseId);
102+
expect(decodeUrl(deepEncodedCourseId)).toEqual(decodedCourseId);
103+
});

src/index.jsx

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import NoticesProvider from './generic/notices';
3737
import PathFixesProvider from './generic/path-fixes';
3838
import LiveTab from './course-home/live-tab/LiveTab';
3939
import CourseAccessErrorPage from './generic/CourseAccessErrorPage';
40+
import DecodePageRoute from './decode-page-route';
4041

4142
subscribe(APP_READY, () => {
4243
ReactDOM.render(
@@ -50,28 +51,28 @@ subscribe(APP_READY, () => {
5051
<Switch>
5152
<PageRoute exact path="/goal-unsubscribe/:token" component={GoalUnsubscribe} />
5253
<PageRoute path="/redirect" component={CoursewareRedirectLandingPage} />
53-
<PageRoute path="/course/:courseId/access-denied" component={CourseAccessErrorPage} />
54-
<PageRoute path="/course/:courseId/home">
54+
<DecodePageRoute path="/course/:courseId/access-denied" component={CourseAccessErrorPage} />
55+
<DecodePageRoute path="/course/:courseId/home">
5556
<TabContainer tab="outline" fetch={fetchOutlineTab} slice="courseHome">
5657
<OutlineTab />
5758
</TabContainer>
58-
</PageRoute>
59-
<PageRoute path="/course/:courseId/live">
59+
</DecodePageRoute>
60+
<DecodePageRoute path="/course/:courseId/live">
6061
<TabContainer tab="lti_live" fetch={fetchLiveTab} slice="courseHome">
6162
<LiveTab />
6263
</TabContainer>
63-
</PageRoute>
64-
<PageRoute path="/course/:courseId/dates">
64+
</DecodePageRoute>
65+
<DecodePageRoute path="/course/:courseId/dates">
6566
<TabContainer tab="dates" fetch={fetchDatesTab} slice="courseHome">
6667
<DatesTab />
6768
</TabContainer>
68-
</PageRoute>
69-
<PageRoute path="/course/:courseId/discussion/:path*">
69+
</DecodePageRoute>
70+
<DecodePageRoute path="/course/:courseId/discussion/:path*">
7071
<TabContainer tab="discussion" fetch={fetchDiscussionTab} slice="courseHome">
7172
<DiscussionTab />
7273
</TabContainer>
73-
</PageRoute>
74-
<PageRoute
74+
</DecodePageRoute>
75+
<DecodePageRoute
7576
path={[
7677
'/course/:courseId/progress/:targetUserId/',
7778
'/course/:courseId/progress',
@@ -86,12 +87,12 @@ subscribe(APP_READY, () => {
8687
</TabContainer>
8788
)}
8889
/>
89-
<PageRoute path="/course/:courseId/course-end">
90+
<DecodePageRoute path="/course/:courseId/course-end">
9091
<TabContainer tab="courseware" fetch={fetchCourse} slice="courseware">
9192
<CourseExit />
9293
</TabContainer>
93-
</PageRoute>
94-
<PageRoute
94+
</DecodePageRoute>
95+
<DecodePageRoute
9596
path={[
9697
'/course/:courseId/:sequenceId/:unitId',
9798
'/course/:courseId/:sequenceId',

0 commit comments

Comments
 (0)