Repository navigation
Description
Part of #1905. Depends on #2187.
Goal: Learning's route tree exists in the frontend-base package and the shell knows about it: one root route at learning, a child route for every URL Learning serves today, each rendering a stub page that names itself as not yet ported, roles on every route, and provides so the shell header's course bar lists Learning's tabs and switches between them client-side. Nothing fetches, no provider mounts, no page is ported.
Why first: routing and roles are the one thing every page port depends on, and they can be verified on their own. With stubs in place, a tester sees the course bar render Learning's tabs and navigate to each one, which proves the route ids, the roles and the provides wiring before any page carries them.
Note
The plan below was generated by Claude (Claude Code) and reviewed before posting.
Claude Plan
Baseline
- The package at the top of build: fail frontend-base package tests on unexpected console output #2219:
routesandslotsare empty arrays,Main.tsx(aCurrentAppProvider, a Helmet title and anOutlet) is unused, 2 tests, the suite fails on unexpected console output. - The shell's course bar and masquerade bar take the course from the active route's params:
CourseTabsNavigationandMasqueradeBarcalluseParams()forcourseId, and the Studio link also readsunitId. Learning's param names are already those, so the routes below keep them. - The shell mounts the course bar when a role listed under
org.openedx.frontend.provides.courseBarRoles.v1is active, where ancestor routes' roles count as active; it makes a tab a client-side link when that role's route path prefix-matches the tab's pathname (isClientRoute), and the tab list itself comes fromcourse_metadataas absolute LMS URLs, matched by pathname (findActiveTab). A single role on the rootlearningroute therefore satisfies every check for every tab, which is how instructor dashboard declares its one role under both ids. - Legacy Learning never required a signed-in user at initialization (anonymous access to public courses), so no route takes
authenticatedLoaderhere; a page port that needs one adds it.
Route tree
src/routes.tsx exports a RoleRouteObject[] with one root and eleven children, paths relative to the root so the array carries no site basename:
-
Root:
path: 'learning',id: 'org.openedx.frontend.route.learning.main',handle.roles: ['org.openedx.frontend.role.learning'],Component: Main(the template'sMain.tsx, which is theCurrentAppProviderthe shell's per-app config reads through, plus an outlet; the Learning-wide providers land on it with the first page that needs each).learningmeans "inside the Learning app" and is not what the course bar is given. -
Two course layout routes under the root,
course/:courseIdandpreview/course/:courseId, each a bare outlet carryingorg.openedx.frontend.role.learningCourse: "this route is a course page", the statement a course-bar role makes (frontend-base's ADR 0013 example and instructor dashboard's role both sit on a:courseIdroute). The course pages are their children; the three non-course pages sit directly under the root, so the shell's course bar and masquerade bar, which readcourseIdfrom the route params, mount only where one exists. -
Page routes, each
id: 'org.openedx.frontend.route.learning.<page>', each rendering<StubPage page="..." />(the page key, which is also the route id's last segment and matches the role's noun where a role exists), with a role only where another app would resolve it to link there (paths below are relative tolearning; under the course layouts thecourse/:courseIdandpreview/course/:courseIdprefixes are the layout's):page path(s) under learningrole course home (outline tab) course/:courseId/homeorg.openedx.frontend.role.courseOutlinedates course/:courseId/datesorg.openedx.frontend.role.courseDatesprogress course/:courseId/progress,course/:courseId/progress/:targetUserIdorg.openedx.frontend.role.courseProgressdiscussion course/:courseId/discussion/:path/*none (ported as a redirect, Port the discussion tab to frontend-base as a redirect #2204) live course/:courseId/livenone (reached only from the course bar, which navigates by the tab URLs the LMS supplies) courseware course/:courseId,course/:courseId/:sequenceId,course/:courseId/:sequenceId/:unitId,preview/course/:courseId/:sequenceId,preview/course/:courseId/:sequenceId/:unitIdorg.openedx.frontend.role.coursewarecourse exit course/:courseId/course-endnone (reached only from inside courseware) access denied course/:courseId/access-deniednone (internal state) goal unsubscribe goal-unsubscribe/:tokennone (reached from emailed URLs) redirect landing redirect/*none (internal state) preferences unsubscribe preferences-unsubscribe/:userToken/:updatePatch?none (reached from emailed URLs) Each page is a named array of its route objects written out in full (
coursewareRoutes,progressRoutes, ...), spread into its layout's children in table order; the bare course path is the course layout's index route; a page with several paths has several objects sharing its stub and role, with the id on the first. No helper: a port replaces its page's array with real routes, so the explicit shape is the lasting one; react-router ranks a static segment above a param, sodatesbeats:sequenceIdunder the course layout as it does onmaster. The eleven paths aremaster'sDECODE_ROUTESandROUTESwith the leading slash removed; the legacy constants are rewritten here, not moved, so the manifest gets no line. -
src/constants.tsholds the four page role ids besideappIdandlearningRole, flat camelCase after catalog's. The four are the pages with a plausible consumer outside Learning, and each is also a tab the course bar lists. Nothing exports them: other apps name the role they resolve as a string, as learner dashboard does for catalog'scoursesrole. -
src/StubPage.tsx: one component, one line, the page name and "not yet ported", no translation since it never ships. Each port deletes its use; the file goes with the last. -
No catch-all: the shell's router renders its not-found page for an unmatched path. Not here: the URL-decoding and path-fixes wrappers (they come with the courseware container, Port the courseware container and redirects to frontend-base #2195), providers, data, the header widget, slots.
provides
src/provides.ts, as instructor dashboard's: the learningCourse role in an array under both providesCourseBarRolesId and providesCourseBarMasqueradeRolesId (the shell's docstring form), wired into src/app.ts. It resolves to /learning/course/:courseId, which prefix-matches every tab URL, so the bar client-navigates to every Learning tab. The masquerade bar then renders on any course route when the masquerade endpoint says the user may; a tab that the LMS lists and no app provides is still shown, as a full-page link.
Tests
src/routes.test.tsx: onedescribeper page and one test per path, each rendering the exported array withcreateMemoryRouterat that path and asserting the stub names the page and lists the params the path captures. A page's block changes with its port (the stub assertion becomes the page's own) rather than being deleted, so the file keeps saying that every URL reaches its page through the real array. No unmatched-path test: without the shell'serrorElementthe router's default error boundary logs to the console, and the not-found page is the shell's, checked on the dev site.jsdom-environment-with-node-fetch.mjs: jsdom has no Fetch API (Implement the Fetch API jsdom/jsdom#1724, still open) and react-router's data router builds aRequestfor any route withlazyor a loader, so a jest environment extendingjest-environment-jsdomforwards Node's ownfetch,Headers,Request,Response,AbortControllerandAbortSignalinto the sandbox. react-router's own suite assigns the same globals in its jest setup (fromundici, since a setup file runs inside the sandbox); an environment runs outside it and can hand over the classes Node ships.whatwg-fetchwas rejected on react-router's refusal to support a non-spec-compliant polyfill ([Bug]:useActionDatareturns rawResponsewhen using polyfilledfetchremix-run/react-router#9722). The redirect layers' loaders return aResponseand need the same globals.src/app.test.tsgrows theprovidesassertion: the root role under both ids, every child route's declaredhandle.roles, and arole resolutionblock with one test per role naming the URL it resolves to, since for the two roles declared on several paths that is the declaration order, which other apps' links depend on.- A shared render helper,
src/tests/render.tsx, wrapping a memory router and the intl provider, is introduced here since every later page test needs it; the query client joins when the dates port brings the first fetch.src/tests/is wheremasterkeeps its test helpers (and account and profile theirs); the build config excludes it, as account's does, so helpers never ship indist/, and coverage excludes it, as it does the setup file and the mocks. src/routes.test.tsxalso assertsexpect(runPageRules(document)).toEqual([])at every path, in adescribe('page rules')of its own:runPageRulesand itsmainLandmarkrule (more than one<main>withouthiddenis an error) come from fix: move the skip link's focus to the app's main landmark frontend-base#329, first published in 2.0.0-alpha.17, which feat: scaffold the frontend-base package #2217 locks. A page's port moves its path out of the list and asserts the rules in its own route test.- No fetch is mocked because nothing fetches; the suite must stay silent under the console hook.
Commits
Two, squashed on merge: feat: add the frontend-base route tree with stub pages, roles and provides, then test: assert the page rules on every route. Checks: npm run lint, npm test, npm run build, npm run build:ci in frontend-base/; the root make validate does not see the package.
Stack
bsmith/frame-routes above bsmith/test-console-noise, opened as a draft.
Manual testing
On the package's dev site against Tutor dev, signed in as a staff user, with the LMS's learning URL pointing at apps.local.openedx.io:2000/learning and tutor's mfe service stopped:
/learning/course/<courseId>/home: the shell header and footer render, the course bar lists the course's tabs fromcourse_metadata, and the page body is the course home stub.- Each tab in the bar switches to its stub without a full page load (the dev console keeps its earlier lines; the network panel shows no document request).
- The masquerade bar shows on course routes for staff.
- A courseware path with sequence and unit ids, a preview path, the progress path with a target user, and the three non-course paths each show their stub.
- An unmatched path under
/learningshows the shell's not-found page with header and footer.
Not in scope
- Any page content, provider, fetch or slot; the header title widget (Replicate Learning header for frontend-base #2193); the masquerade bar's Learning extras (Wire up the frontend-base masquerade bar #2194).
- The redirect of the discussion route (Port the discussion tab to frontend-base as a redirect #2204) and the loaders of the redirect landing page (Port the course access redirects to frontend-base #2205); both are stubs here.
Acceptance
- Every path in the table renders its stub under the shell; an unmatched path renders the shell's not-found page
- The shell course bar lists the course's tabs and navigates to Learning's tabs without a full page load; the masquerade bar appears on course routes for staff
- The route array has no site-basename assumption
- Route tests cover every path in the table; the suite stays silent
- Lint, tests and both builds of the package pass; the legacy workflow stays green;
npm run devfrom the package serves the stubs - No manifest line: the route constants are rewritten, not moved
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsDone
Activity
[-]Route tree with stub pages, roles and provides[/-][+]Add frontend-base route tree with stub pages, roles and provides[/+]feat: give the frontend-base package Learning's identity
feat: give the frontend-base package Learning's identity
feat: add the frontend-base route tree with stub pages, roles and pro…
feat: give the frontend-base package Learning's identity
feat: add the frontend-base route tree with stub pages, roles and pro…
test: assert the page rules on every route
feat: scaffold the frontend-base package (#2217)
feat: add the frontend-base route tree with stub pages, roles and pro…
feat: scaffold the frontend-base package (#2217)
feat: add the frontend-base route tree with stub pages, roles and pro…