Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions frontend-base/jest.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const { createConfig } = require('@openedx/frontend-base/tools');

module.exports = createConfig('test', {
testEnvironment: '<rootDir>/jsdom-environment-with-node-fetch.mjs',
setupFilesAfterEnv: [
'<rootDir>/src/setupTest.js',
],
Expand All @@ -13,6 +14,7 @@ module.exports = createConfig('test', {
'src/setupTest.js',
'src/i18n',
'src/__mocks__',
'src/tests',
],
moduleNameMapper: {
'^@src/(.*)$': '<rootDir>/src/$1',
Expand Down
16 changes: 16 additions & 0 deletions frontend-base/jsdom-environment-with-node-fetch.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// This file can be removed if https://github.com/jsdom/jsdom/issues/1724 is resolved.
import jsdom from 'jest-environment-jsdom';

const JSDOMEnvironment = jsdom.default;

export default class JSDOMEnvironmentWithNodeFetch extends JSDOMEnvironment {
constructor(...args) {
super(...args);
this.global.AbortController = AbortController;
this.global.AbortSignal = AbortSignal;
this.global.fetch = fetch;
this.global.Headers = Headers;
this.global.Request = Request;
this.global.Response = Response;
}
}
21 changes: 21 additions & 0 deletions frontend-base/src/StubPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Container } from '@openedx/paragon';
import { useParams } from 'react-router-dom';

interface StubPageProps {
page: string;
}

// Stands in for a page until its port replaces it; deleted with the last port.
const StubPage = ({ page }: StubPageProps) => {
const params = useParams();
return (
<Container as="main" size="xl" className="py-4">
<p>{page}: not yet ported</p>
<ul>
{Object.entries(params).map(([name, value]) => <li key={name}>{`${name}=${value}`}</li>)}
</ul>
</Container>
);
};

export default StubPage;
102 changes: 100 additions & 2 deletions frontend-base/src/app.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,107 @@
import {
getUrlByRouteRole,
mergeSiteConfig,
providesCourseBarMasqueradeRolesId,
providesCourseBarRolesId,
} from '@openedx/frontend-base';

import { learningApp } from './index';
import {
courseDatesRole,
courseOutlineRole,
courseProgressRole,
coursewareRole,
learningCourseRole,
learningRole,
} from './constants';

const [main] = learningApp.routes ?? [];
const [course, preview, ...nonCourse] = main.children ?? [];

describe('learningApp', () => {
it('declares the Learning app id with no routes or slot operations yet', () => {
it('declares the Learning app id with no slot operations yet', () => {
expect(learningApp.appId).toBe('org.openedx.frontend.app.learning');
expect(learningApp.routes).toEqual([]);
expect(learningApp.slots).toEqual([]);
});

it('provides the course role for the course bar and the masquerade bar', () => {
expect(learningApp.provides).toEqual({
[providesCourseBarRolesId]: [learningCourseRole],
[providesCourseBarMasqueradeRolesId]: [learningCourseRole],
});
});

it('declares the learning role on the root route', () => {
expect(main.path).toBe('learning');
expect(main.handle?.roles).toEqual([learningRole]);
});

it('declares the course role on the course and preview layout routes', () => {
expect(course.path).toBe('course/:courseId');
expect(course.handle?.roles).toEqual([learningCourseRole]);
expect(preview.path).toBe('preview/course/:courseId');
expect(preview.handle?.roles).toEqual([learningCourseRole]);
});

it('declares a destination role on four course pages and none on the rest', () => {
const declaredRolesByCoursePath = Object.fromEntries(
(course.children ?? []).map((route) => [route.index ? '(index)' : route.path, route.handle?.roles]),
);
expect(declaredRolesByCoursePath).toEqual({
home: [courseOutlineRole],
dates: [courseDatesRole],
progress: [courseProgressRole],
'progress/:targetUserId': [courseProgressRole],
'discussion/:path/*': undefined,
live: undefined,
'(index)': [coursewareRole],
':sequenceId': [coursewareRole],
':sequenceId/:unitId': [coursewareRole],
'course-end': undefined,
'access-denied': undefined,
});
expect((preview.children ?? []).map((route) => [route.path, route.handle?.roles])).toEqual([
[':sequenceId', [coursewareRole]],
[':sequenceId/:unitId', [coursewareRole]],
]);
expect(nonCourse.map((route) => [route.path, route.handle?.roles])).toEqual([
['goal-unsubscribe/:token', undefined],
['redirect/*', undefined],
['preferences-unsubscribe/:userToken/:updatePatch?', undefined],
]);
});
});

describe('role resolution', () => {
beforeAll(() => {
mergeSiteConfig({ apps: [learningApp] });
});

it('learning resolves to the root, not to any page', () => {
expect(getUrlByRouteRole(learningRole)).toBe('/learning');
});

it('learningCourse resolves to the course, which prefix-matches every tab', () => {
expect(getUrlByRouteRole(learningCourseRole)).toBe('/learning/course/:courseId');
});

it('courseOutline resolves to the outline tab', () => {
expect(getUrlByRouteRole(courseOutlineRole)).toBe('/learning/course/:courseId/home');
});

it('courseDates resolves to the dates tab', () => {
expect(getUrlByRouteRole(courseDatesRole)).toBe('/learning/course/:courseId/dates');
});

it("courseProgress resolves to the learner's own progress, not another user's", () => {
const url = getUrlByRouteRole(courseProgressRole);
expect(url).toBe('/learning/course/:courseId/progress');
expect(url).not.toMatch(/:targetUserId/);
});

it('courseware resolves to the bare course path (an index route, hence the slash), not a sequence, unit or preview path', () => {
const url = getUrlByRouteRole(coursewareRole);
expect(url).toBe('/learning/course/:courseId/');
expect(url).not.toMatch(/:sequenceId|:unitId|preview/);
});
});
2 changes: 2 additions & 0 deletions frontend-base/src/app.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { App } from '@openedx/frontend-base';
import { appId } from '@src/constants';
import provides from '@src/provides';
import routes from '@src/routes';
import slots from '@src/slots';

const app: App = {
appId,
routes,
provides,
slots,
};

Expand Down
6 changes: 6 additions & 0 deletions frontend-base/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,8 @@
export const appId = 'org.openedx.frontend.app.learning';

export const learningRole = 'org.openedx.frontend.role.learning';
export const learningCourseRole = 'org.openedx.frontend.role.learningCourse';
export const courseOutlineRole = 'org.openedx.frontend.role.courseOutline';
export const courseDatesRole = 'org.openedx.frontend.role.courseDates';
export const courseProgressRole = 'org.openedx.frontend.role.courseProgress';
export const coursewareRole = 'org.openedx.frontend.role.courseware';
10 changes: 10 additions & 0 deletions frontend-base/src/provides.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { providesCourseBarMasqueradeRolesId, providesCourseBarRolesId } from '@openedx/frontend-base';

import { learningCourseRole } from './constants';

const provides = {
[providesCourseBarRolesId]: [learningCourseRole],
[providesCourseBarMasqueradeRolesId]: [learningCourseRole],
};

export default provides;
150 changes: 150 additions & 0 deletions frontend-base/src/routes.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { runPageRules } from '@openedx/frontend-base';
import { screen } from '@testing-library/react';

import routes from '@src/routes';
import { renderRoutes } from '@src/tests/render';

const courseId = 'course-v1:OpenedX+DemoX+DemoCourse';

const renderAt = (path: string) => renderRoutes(routes, `/learning${path}`);
const expectStub = async (page: string, params: Record<string, string>) => {
expect(await screen.findByText(`${page}: not yet ported`)).toBeInTheDocument();
Object.entries(params).forEach(([name, value]) => {
expect(screen.getByText(`${name}=${value}`)).toBeInTheDocument();
});
};

describe('routes', () => {
describe('outline', () => {
it('renders at course/:courseId/home', async () => {
renderAt(`/course/${courseId}/home`);
await expectStub('outline', { courseId });
});
});

describe('dates', () => {
it('renders at course/:courseId/dates', async () => {
renderAt(`/course/${courseId}/dates`);
await expectStub('dates', { courseId });
});
});

describe('progress', () => {
it('renders at course/:courseId/progress', async () => {
renderAt(`/course/${courseId}/progress`);
await expectStub('progress', { courseId });
});

it('renders at course/:courseId/progress/:targetUserId with the target user', async () => {
renderAt(`/course/${courseId}/progress/42`);
await expectStub('progress', { courseId, targetUserId: '42' });
});
});

describe('discussion', () => {
it('renders at course/:courseId/discussion/:path/* with the path and the rest', async () => {
renderAt(`/course/${courseId}/discussion/topics/abc`);
await expectStub('discussion', { courseId, path: 'topics', '*': 'abc' });
});
});

describe('live', () => {
it('renders at course/:courseId/live', async () => {
renderAt(`/course/${courseId}/live`);
await expectStub('live', { courseId });
});
});

describe('courseware', () => {
it('renders at course/:courseId', async () => {
renderAt(`/course/${courseId}`);
await expectStub('courseware', { courseId });
});

it('renders at course/:courseId/:sequenceId with the sequence', async () => {
renderAt(`/course/${courseId}/seq`);
await expectStub('courseware', { courseId, sequenceId: 'seq' });
});

it('renders at course/:courseId/:sequenceId/:unitId with both ids', async () => {
renderAt(`/course/${courseId}/seq/unit`);
await expectStub('courseware', { courseId, sequenceId: 'seq', unitId: 'unit' });
});

it('renders at preview/course/:courseId/:sequenceId with the sequence', async () => {
renderAt(`/preview/course/${courseId}/seq`);
await expectStub('courseware', { courseId, sequenceId: 'seq' });
});

it('renders at preview/course/:courseId/:sequenceId/:unitId with both ids', async () => {
renderAt(`/preview/course/${courseId}/seq/unit`);
await expectStub('courseware', { courseId, sequenceId: 'seq', unitId: 'unit' });
});
});

describe('courseExit', () => {
it('renders at course/:courseId/course-end', async () => {
renderAt(`/course/${courseId}/course-end`);
await expectStub('courseExit', { courseId });
});
});

describe('accessDenied', () => {
it('renders at course/:courseId/access-denied', async () => {
renderAt(`/course/${courseId}/access-denied`);
await expectStub('accessDenied', { courseId });
});
});

describe('goalUnsubscribe', () => {
it('renders at goal-unsubscribe/:token with the token', async () => {
renderAt('/goal-unsubscribe/tok');
await expectStub('goalUnsubscribe', { token: 'tok' });
});
});

describe('redirect', () => {
it('renders at redirect/* with the rest', async () => {
renderAt('/redirect/dashboard');
await expectStub('redirect', { '*': 'dashboard' });
});
});

describe('preferencesUnsubscribe', () => {
it('renders at preferences-unsubscribe/:userToken with the token', async () => {
renderAt('/preferences-unsubscribe/tok');
await expectStub('preferencesUnsubscribe', { userToken: 'tok' });
});

it('renders at preferences-unsubscribe/:userToken/:updatePatch with the patch', async () => {
renderAt('/preferences-unsubscribe/tok/patch');
await expectStub('preferencesUnsubscribe', { userToken: 'tok', updatePatch: 'patch' });
});
});

describe('page rules', () => {
it.each([
`/course/${courseId}/home`,
`/course/${courseId}/dates`,
`/course/${courseId}/progress`,
`/course/${courseId}/progress/42`,
`/course/${courseId}/discussion/topics/abc`,
`/course/${courseId}/live`,
`/course/${courseId}`,
`/course/${courseId}/seq`,
`/course/${courseId}/seq/unit`,
`/preview/course/${courseId}/seq`,
`/preview/course/${courseId}/seq/unit`,
`/course/${courseId}/course-end`,
`/course/${courseId}/access-denied`,
'/goal-unsubscribe/tok',
'/redirect/dashboard',
'/preferences-unsubscribe/tok',
'/preferences-unsubscribe/tok/patch',
])('reports no page-rule diagnostics at %s', async (path) => {
renderAt(path);
await screen.findByText(/: not yet ported$/);
expect(runPageRules(document)).toEqual([]);
});
});
});
Loading
Loading